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

@ -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.