Add global inbox, move announce
This commit is contained in:
parent
7ac4c628b8
commit
72e1c67488
4 changed files with 229 additions and 33 deletions
23
activitypub/translators/announce.go
Normal file
23
activitypub/translators/announce.go
Normal 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")
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue