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
|
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"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
webutils "git.mstar.dev/mstar/goutils/http"
|
webutils "git.mstar.dev/mstar/goutils/http"
|
||||||
"git.mstar.dev/mstar/goutils/other"
|
"git.mstar.dev/mstar/goutils/other"
|
||||||
|
"git.mstar.dev/mstar/goutils/sliceutils"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/rs/zerolog/hlog"
|
"github.com/rs/zerolog/hlog"
|
||||||
"github.com/rs/zerolog/log"
|
"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 {
|
func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) bool {
|
||||||
log := hlog.FromRequest(r)
|
log := hlog.FromRequest(r)
|
||||||
activityId := object["id"].(string)
|
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}
|
dbNote.RepliesTo = sql.NullString{Valid: true, String: replyUrl}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feed, err := dbgen.Feed.Where(dbgen.Feed.OwnerId.Eq(targetUserId), dbgen.Feed.IsDefault.Is(true)).
|
totalNoteTargets := slices.Concat(objectNote.To, objectNote.CC)
|
||||||
First()
|
// TODO: Find all feeds this note needs to be added to
|
||||||
if err != nil {
|
// Depends on note targets (cc & to tags)
|
||||||
log.Error().Err(err).Msg("Failed to get feed for targeted user inbox")
|
// Includes specific user IDs, nothing else -> DMs, only the personal feeds of those users
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
// Includes https://www.w3.org/ns/activitystreams#Public (& creator follower list) ->
|
||||||
return false
|
// 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()
|
tx := dbgen.Q.Begin()
|
||||||
err = tx.Note.Create(&dbNote)
|
err = tx.Note.Create(&dbNote)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -801,6 +986,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any)
|
||||||
ObjectId: dbNote.ID,
|
ObjectId: dbNote.ID,
|
||||||
ObjectType: uint32(models.ActivitystreamsActivityTargetNote),
|
ObjectType: uint32(models.ActivitystreamsActivityTargetNote),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Activity.Create(&createActivity)
|
err = tx.Activity.Create(&createActivity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
|
@ -808,17 +994,25 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any)
|
||||||
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
err = tx.NoteToFeed.Create(&models.NoteToFeed{
|
|
||||||
Reason: string(models.FeedAppearanceReasonFollowUser),
|
feedRels := sliceutils.Map(targetFeeds, func(f models.Feed) *models.NoteToFeed {
|
||||||
NoteId: dbNote.ID,
|
return &models.NoteToFeed{
|
||||||
FeedId: uint64(feed.ID),
|
Reason: string(models.FeedAppearanceReasonFollowUser),
|
||||||
|
NoteId: dbNote.ID,
|
||||||
|
FeedId: uint64(f.ID),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
err = tx.NoteToFeed.Create(feedRels...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = 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)
|
_ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Any("note", dbNote).Msg("Failed to submit note creation in db")
|
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}", objectNote)
|
||||||
router.HandleFunc("GET /note/{id}/reactions", noteReactions)
|
router.HandleFunc("GET /note/{id}/reactions", noteReactions)
|
||||||
router.HandleFunc("GET /emote/{id}", objectEmote)
|
router.HandleFunc("GET /emote/{id}", objectEmote)
|
||||||
|
router.HandleFunc("POST /inbox", globalInbox)
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue