Add global inbox, move announce

This commit is contained in:
Melody Becker 2025-07-07 12:40:37 +02:00
parent 7ac4c628b8
commit 72e1c67488
Signed by: mstar
SSH key fingerprint: SHA256:9VAo09aaVNTWKzPW7Hq2LW+ox9OdwmTSHRoD4mlz1yI
4 changed files with 229 additions and 33 deletions

View file

@ -0,0 +1,23 @@
package translators
import (
"context"
"time"
)
// Announce is boost
type activityAnnounceOut struct {
Context any `json:"@context,omitempty"`
Id string
Type string // Always "Announce"
Actor string // The one doing the boost
Published time.Time
To []string
CC []string
Object string // Link to object being boosted
}
func AnnounceFromStorage(ctx context.Context, id string) (*activityAnnounceOut, error) {
panic("not implemented")
}

View file

@ -1,23 +1 @@
package activitypub
import (
"context"
"time"
)
// Announce is boost
type activityAnnounceOut struct {
Context any `json:"@context,omitempty"`
Id string
Type string // Always "Announce"
Actor string // The one doing the boost
Published time.Time
To []string
CC []string
Object string // Link to object being boosted
}
func announceFromStorage(ctx context.Context, id string) (*activityAnnounceOut, error) {
panic("not implemented")
}

View file

@ -8,12 +8,14 @@ import (
"io"
"net/http"
"regexp"
"slices"
"strconv"
"strings"
"time"
webutils "git.mstar.dev/mstar/goutils/http"
"git.mstar.dev/mstar/goutils/other"
"git.mstar.dev/mstar/goutils/sliceutils"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog/hlog"
"github.com/rs/zerolog/log"
@ -139,6 +141,108 @@ func userInbox(w http.ResponseWriter, r *http.Request) {
}
}
func globalInbox(w http.ResponseWriter, r *http.Request) {
log := hlog.FromRequest(r)
body, err := io.ReadAll(r.Body)
log.Trace().
Err(err).
Bytes("body", body).
Any("headers", r.Header).
Msg("Global inbox message")
data := map[string]any{}
err = json.Unmarshal(body, &data)
if err != nil {
_ = webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"Bad activity data",
other.IntoPointer("Body to inbox needs to be json"),
nil,
)
return
}
if _, ok := data["@context"]; !ok {
_ = webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"Bad activity data",
other.IntoPointer("Request data needs to contain context"),
nil,
)
return
}
objectType, ok := data["type"].(string)
if !ok {
_ = webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"Bad activity data",
other.IntoPointer(`Request data needs to contain a field "type" with a string value`),
nil,
)
return
}
_, ok = data["id"].(string)
if !ok {
_ = webutils.ProblemDetails(
w,
http.StatusBadRequest,
"/errors/bad-request-data",
"Bad activity data",
other.IntoPointer(`Request data needs to contain a field "id" with a string value`),
nil,
)
return
}
log.Debug().Str("object-type", objectType).Msg("Inbox message")
log.Trace().Bytes("body", body).Msg("Inbox raw message")
// TODO: Decide how to handle the handler failing for whatever reason
// Add object to unhandled message table and try again later?
// Discard it? And how would a handler return that it failed?
ok = true
switch objectType {
case "Like":
ok = handleLike(w, r, data)
case "Undo":
ok = handleUndo(w, r, data)
case "Accept":
ok = handleAccept(w, r, data)
case "Reject":
ok = handleReject(w, r, data)
case "Create":
ok = handleCreate(w, r, data)
default:
log.Warn().
Str("object-type", objectType).
Msg("Unknown message type, storing for later processing")
err = dbgen.UnhandledMessage.Create(&models.UnhandledMessage{
ForUserId: "",
GlobalInbox: true,
RawData: body,
})
if err != nil {
log.Error().Err(err).Msg("Failed to store unhandled message for later")
}
_ = webutils.ProblemDetailsStatusOnly(w, 500)
}
if !ok {
err = dbgen.UnhandledMessage.Create(&models.UnhandledMessage{
RawData: body,
ForUserId: "",
GlobalInbox: true,
})
if err != nil {
log.Error().
Err(err).
Bytes("body", body).
Msg("Failed to store failed global inbound message for later processing")
}
}
}
func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) bool {
log := hlog.FromRequest(r)
activityId := object["id"].(string)
@ -780,13 +884,94 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any)
dbNote.RepliesTo = sql.NullString{Valid: true, String: replyUrl}
}
}
feed, err := dbgen.Feed.Where(dbgen.Feed.OwnerId.Eq(targetUserId), dbgen.Feed.IsDefault.Is(true)).
First()
if err != nil {
log.Error().Err(err).Msg("Failed to get feed for targeted user inbox")
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return false
totalNoteTargets := slices.Concat(objectNote.To, objectNote.CC)
// TODO: Find all feeds this note needs to be added to
// Depends on note targets (cc & to tags)
// Includes specific user IDs, nothing else -> DMs, only the personal feeds of those users
// Includes https://www.w3.org/ns/activitystreams#Public (& creator follower list) ->
// Public feed (unlisted has same combo, but iirc doesn't get sent out by server)
// Includes only creator follower list -> Follower only -> only feeds that explicitly follow creator
u2u := dbgen.UserToUserRelation
targetFeeds := []models.Feed{}
if sliceutils.Contains(totalNoteTargets, "https://www.w3.org/ns/activitystreams#Public") {
// Public post, add to global and following feeds
dbNote.AccessLevel = models.NOTE_TARGET_PUBLIC
followerIds, err := u2u.GetLocalFollowerIdsOfId(actingUser.ID)
if err != nil {
log.Error().
Err(err).
Str("follow-target", actingUser.ID).
Msg("Failed to get ids for followers")
return false
}
userFeeds, err := dbgen.Feed.Where(dbgen.Feed.OwnerId.In(followerIds...)).Find()
if err != nil {
log.Error().
Err(err).
Str("follow-target", actingUser.ID).
Strs("follower-ids", followerIds).
Msg("Failed to get feeds for followers")
return false
}
globalFeed, err := dbgen.Feed.Where(dbgen.Feed.Name.Eq(models.GlobalFeedName)).First()
if err != nil {
log.Error().
Err(err).
Msg("Failed to get global feed")
return false
}
targetFeeds = slices.Concat(
targetFeeds,
sliceutils.Map(userFeeds, func(t *models.Feed) models.Feed { return *t }),
)
targetFeeds = append(targetFeeds, *globalFeed)
} else {
if sliceutils.ContainsFunc(totalNoteTargets, func(x string) bool {
return strings.HasPrefix(x, actingUser.ID)
}) {
// Contains an url starting with the author's id, so assume that it's for followers
dbNote.AccessLevel = models.NOTE_TARGET_FOLLOWERS
followerIds, err := u2u.GetLocalFollowerIdsOfId(actingUser.ID)
if err != nil {
log.Error().
Err(err).
Str("follow-target", actingUser.ID).
Msg("Failed to get ids for followers")
return false
}
userFeeds, err := dbgen.Feed.Where(dbgen.Feed.OwnerId.In(followerIds...)).Find()
if err != nil {
log.Error().
Err(err).
Str("follow-target", actingUser.ID).
Strs("follower-ids", followerIds).
Msg("Failed to get feeds for followers")
return false
}
targetFeeds = sliceutils.Map(userFeeds, func(t *models.Feed) models.Feed { return *t })
} else {
// Neither followers collection url nor public marker, private message
dbNote.AccessLevel = models.NOTE_TARGET_DM
userFeeds, err := dbgen.Feed.
LeftJoin(dbgen.User, dbgen.User.ID.EqCol(dbgen.Feed.OwnerId)).
LeftJoin(dbgen.RemoteServer, dbgen.RemoteServer.ID.EqCol(dbgen.User.ServerId)).
LeftJoin(dbgen.UserRemoteLinks, dbgen.UserRemoteLinks.ID.EqCol(dbgen.User.RemoteInfoId)).
Where(dbgen.RemoteServer.IsSelf.Is(true)).Where(
dbgen.User.ID.In(totalNoteTargets...),
).Or(dbgen.UserRemoteLinks.ApLink.In(totalNoteTargets...)).Find()
if err != nil {
log.Error().
Err(err).
Str("follow-target", actingUser.ID).
Strs("targeted-ids", totalNoteTargets).
Msg("Failed to get feeds for directly messaged users")
return false
}
targetFeeds = sliceutils.Map(userFeeds, func(t *models.Feed) models.Feed { return *t })
}
}
tx := dbgen.Q.Begin()
err = tx.Note.Create(&dbNote)
if err != nil {
@ -801,6 +986,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any)
ObjectId: dbNote.ID,
ObjectType: uint32(models.ActivitystreamsActivityTargetNote),
}
err = tx.Activity.Create(&createActivity)
if err != nil {
_ = tx.Rollback()
@ -808,17 +994,25 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any)
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return false
}
err = tx.NoteToFeed.Create(&models.NoteToFeed{
Reason: string(models.FeedAppearanceReasonFollowUser),
NoteId: dbNote.ID,
FeedId: uint64(feed.ID),
feedRels := sliceutils.Map(targetFeeds, func(f models.Feed) *models.NoteToFeed {
return &models.NoteToFeed{
Reason: string(models.FeedAppearanceReasonFollowUser),
NoteId: dbNote.ID,
FeedId: uint64(f.ID),
}
})
err = tx.NoteToFeed.Create(feedRels...)
if err != nil {
_ = tx.Rollback()
log.Error().Err(err).Any("note", dbNote).Msg("Failed to create note to feed relation in db")
log.Error().
Err(err).
Any("note", dbNote).
Msg("Failed to create note to feed relations in db")
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
return false
}
err = tx.Commit()
if err != nil {
log.Error().Err(err).Any("note", dbNote).Msg("Failed to submit note creation in db")

View file

@ -20,5 +20,6 @@ func BuildActivitypubRouter() http.Handler {
router.HandleFunc("GET /note/{id}", objectNote)
router.HandleFunc("GET /note/{id}/reactions", noteReactions)
router.HandleFunc("GET /emote/{id}", objectEmote)
router.HandleFunc("POST /inbox", globalInbox)
return router
}