Add follower and following collections
All checks were successful
/ docker (push) Successful in 4m34s

This commit is contained in:
Melody Becker 2025-05-11 18:28:51 +02:00
parent b75db5676b
commit af6ff2dd30
Signed by: mstar
SSH key fingerprint: SHA256:vkXfS9FG2pVNVfvDrzd1VW9n8VJzqqdKQGljxxX8uK8
11 changed files with 431 additions and 22 deletions

View file

@ -8,6 +8,8 @@ func BuildActivitypubRouter() http.Handler {
router := http.NewServeMux()
router.HandleFunc("/user/{id}", users)
router.HandleFunc("/user/{id}/inbox", userInbox)
router.HandleFunc("/user/{id}/followers", userFollowers)
router.HandleFunc("/user/{id}/following", userFollowing)
router.HandleFunc("/activity/accept/{id}", activityAccept)
router.HandleFunc("/activity/create/{id}", activityCreate)
router.HandleFunc("/activity/delete/{id}", activityDelete)

View file

@ -4,23 +4,24 @@ import "net/http"
// Used for both unordered and ordered
type collectionOut struct {
Context any
Summary string
Type string
Items []any
Id string
TotalItems int
First *collectionPageOut
Context any `json:"@context,omitempty"`
Summary string `json:"summary,omitempty"`
Type string `json:"type"`
Items []any `json:"items,omitempty"`
Id string `json:"id"`
TotalItems int `json:"totalItems"`
First string `json:"first"`
}
// Used for both unordered and ordered
type collectionPageOut struct {
Context any
Type string
Id string
PartOf string
Next string
Items []any
Context any `json:"@context,omitempty"`
Type string `json:"type"`
Id string `json:"id"`
PartOf string `json:"partOf"`
Next string `json:"next,omitempty"`
Previous string `json:"prev,omitempty"`
Items []any `json:"items"`
}
// Unordered collections handler

View file

@ -368,10 +368,6 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any)
log.Error().Err(err).Msg("Failed to marshal accept")
return
}
log.Debug().
Bytes("body", body).
Str("target", follower.RemoteInfo.InboxLink).
Msg("Sending follow accept out")
res, err := webshared.RequestSignedCavage(
"POST",
follower.RemoteInfo.InboxLink,

View file

@ -4,11 +4,14 @@ import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/goutils/other"
"git.mstar.dev/mstar/goutils/sliceutils"
"github.com/rs/zerolog/hlog"
"gorm.io/gorm"
"git.mstar.dev/mstar/linstrom/activitypub"
"git.mstar.dev/mstar/linstrom/config"
@ -50,6 +53,8 @@ func users(w http.ResponseWriter, r *http.Request) {
SpeakAsCat bool `json:"speakAsCat"`
IsCat bool `json:"isCat"`
RestrictedFollow bool `json:"manuallyApprovesFollowers"`
Following string `json:"following"`
Followers string `json:"followers"`
}
log := hlog.FromRequest(r)
userId := r.PathValue("id")
@ -91,6 +96,8 @@ func users(w http.ResponseWriter, r *http.Request) {
PublicUrl: config.GlobalConfig.General.GetFullPublicUrl() + "/user/" + user.Username,
Discoverable: user.Indexable,
RestrictedFollow: user.RestrictedFollow,
Following: apUrl + "/following",
Followers: apUrl + "/followers",
}
if user.Description != "" {
data.Description = &user.Description
@ -139,6 +146,181 @@ func users(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, string(encoded))
}
func userFollowing(w http.ResponseWriter, r *http.Request) {
log := hlog.FromRequest(r)
userId := r.PathValue("id")
pageNrStr := r.FormValue("page")
exists, err := dbgen.User.DoesUserWithIdExist(userId)
if err != nil {
log.Error().Err(err).Str("id", userId).Msg("Failed to check if user exists")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
if !exists {
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
return
}
apUrl := userIdToApUrl(userId)
var data []byte
followingCount, err := dbgen.UserToUserRelation.CountFollowingForId(userId)
if err != nil {
log.Error().Err(err).Str("id", userId).Msg("Failed to get following count")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
if pageNrStr == "" {
col := collectionOut{
Context: "https://www.w3.org/ns/activitystreams",
Type: "OrderedCollection",
Id: apUrl + "/following",
TotalItems: followingCount,
First: apUrl + "/following?page=0",
}
data, err = json.Marshal(col)
if err != nil {
log.Error().Err(err).Any("raw", data).Msg("Failed to marshal following collection page")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
} else {
pageNr, err := strconv.Atoi(pageNrStr)
if err != nil {
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-page",
"bad page number",
other.IntoPointer("page number must be an uint"),
nil,
)
return
}
hasNextPage := followingCount-(pageNr+1)*50 > 0
hasPreviousPage := pageNr > 0
links, err := dbgen.UserToUserRelation.GetFollowingApLinksPagedForId(userId, pageNr)
switch err {
case gorm.ErrRecordNotFound:
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
return
case nil:
default:
log.Error().Err(err).Str("id", userId).Msg("Failed to get account via id")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
page := collectionPageOut{
Context: "https://www.w3.org/ns/activitystreams",
Type: "OrderedCollectionPage",
Id: fmt.Sprintf("%s/following?page=%d", apUrl, pageNr),
PartOf: userIdToApUrl(userId) + "/following",
Items: sliceutils.Map(links, func(t string) any { return t }),
}
if hasNextPage {
page.Next = fmt.Sprintf("%s/following?page=%d", apUrl, pageNr+1)
}
if hasPreviousPage {
page.Next = fmt.Sprintf("%s/following?page=%d", apUrl, pageNr-1)
}
data, err = json.Marshal(page)
if err != nil {
log.Error().Err(err).Any("raw", page).Msg("Failed to marshal following collection page")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
}
w.Header().Add("Content-Type", "application/activity+json")
fmt.Fprint(w, string(data))
}
func userFollowers(w http.ResponseWriter, r *http.Request) {
log := hlog.FromRequest(r)
userId := r.PathValue("id")
pageNrStr := r.FormValue("page")
exists, err := dbgen.User.DoesUserWithIdExist(userId)
if err != nil {
log.Error().Err(err).Str("id", userId).Msg("Failed to check if user exists")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
if !exists {
log.Debug().Str("id", userId).Msg("user not found")
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
return
}
apUrl := userIdToApUrl(userId)
var data []byte
followersCount, err := dbgen.UserToUserRelation.CountFollowersForId(userId)
if err != nil {
log.Error().Err(err).Str("id", userId).Msg("Failed to get followers count")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
if pageNrStr == "" {
col := collectionOut{
Context: activitypub.BaseLdContext,
Type: "OrderedCollection",
Id: apUrl + "/followers",
TotalItems: followersCount,
First: apUrl + "/followers?page=0",
}
data, err = json.Marshal(col)
if err != nil {
log.Error().Err(err).Any("raw", data).Msg("Failed to marshal followers collection page")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
} else {
pageNr, err := strconv.Atoi(pageNrStr)
if err != nil {
webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-page",
"bad page number",
other.IntoPointer("page number must be an uint"),
nil,
)
return
}
hasNextPage := followersCount-(pageNr+1)*50 > 0
hasPreviousPage := pageNr > 0
links, err := dbgen.UserToUserRelation.GetFollowerApLinksPagedForId(userId, pageNr)
switch err {
case gorm.ErrRecordNotFound:
log.Debug().Msg("No followers found")
webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound)
return
case nil:
default:
log.Error().Err(err).Str("id", userId).Msg("Failed to get account via id")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
page := collectionPageOut{
Context: activitypub.BaseLdContext,
Type: "OrderedCollectionPage",
Id: fmt.Sprintf("%s/followers?page=%d", apUrl, pageNr),
PartOf: userIdToApUrl(userId) + "/followers",
Items: sliceutils.Map(links, func(t string) any { return t }),
}
if hasNextPage {
page.Next = fmt.Sprintf("%s/followers?page=%d", apUrl, pageNr+1)
}
if hasPreviousPage {
page.Next = fmt.Sprintf("%s/followers?page=%d", apUrl, pageNr-1)
}
data, err = json.Marshal(page)
if err != nil {
log.Error().Err(err).Any("raw", page).Msg("Failed to marshal followers collection page")
webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return
}
}
log.Debug().Bytes("body", data).Msg("Sending collection(page) out")
w.Header().Add("Content-Type", "application/activity+json")
fmt.Fprint(w, string(data))
}
/*
Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then
Fuck you.