From d84a693b228edb3d3e30d6afa07c45cfb2320664 Mon Sep 17 00:00:00 2001 From: mStar Date: Thu, 8 May 2025 08:32:02 +0200 Subject: [PATCH] Add inbox follow request handling - Not tested yet - Undo needs to be extended to also handle undo of follow --- storage-new/dbgen/reactions.gen.go | 6 +- .../dbgen/user_to_user_relations.gen.go | 6 +- storage-new/models/ActivityTargetType.go | 2 + storage-new/models/UserRelationType.go | 1 + storage-new/models/UserToUserRelation.go | 2 +- web/public/api/activitypub/inbox.go | 130 ++++++++++++++++-- 6 files changed, 130 insertions(+), 17 deletions(-) diff --git a/storage-new/dbgen/reactions.gen.go b/storage-new/dbgen/reactions.gen.go index a85776c..d984385 100644 --- a/storage-new/dbgen/reactions.gen.go +++ b/storage-new/dbgen/reactions.gen.go @@ -33,7 +33,7 @@ func newReaction(db *gorm.DB, opts ...gen.DOOption) reaction { _reaction.DeletedAt = field.NewField(tableName, "deleted_at") _reaction.NoteId = field.NewString(tableName, "note_id") _reaction.ReactorId = field.NewString(tableName, "reactor_id") - _reaction.EmoteId = field.NewUint(tableName, "emote_id") + _reaction.EmoteId = field.NewField(tableName, "emote_id") _reaction.Note = reactionBelongsToNote{ db: db.Session(&gorm.Session{}), @@ -430,7 +430,7 @@ type reaction struct { DeletedAt field.Field NoteId field.String ReactorId field.String - EmoteId field.Uint + EmoteId field.Field Note reactionBelongsToNote Reactor reactionBelongsToReactor @@ -458,7 +458,7 @@ func (r *reaction) updateTableName(table string) *reaction { r.DeletedAt = field.NewField(table, "deleted_at") r.NoteId = field.NewString(table, "note_id") r.ReactorId = field.NewString(table, "reactor_id") - r.EmoteId = field.NewUint(table, "emote_id") + r.EmoteId = field.NewField(table, "emote_id") r.fillFieldMap() diff --git a/storage-new/dbgen/user_to_user_relations.gen.go b/storage-new/dbgen/user_to_user_relations.gen.go index ce28141..1989e93 100644 --- a/storage-new/dbgen/user_to_user_relations.gen.go +++ b/storage-new/dbgen/user_to_user_relations.gen.go @@ -30,7 +30,7 @@ func newUserToUserRelation(db *gorm.DB, opts ...gen.DOOption) userToUserRelation _userToUserRelation.ID = field.NewUint64(tableName, "id") _userToUserRelation.UserId = field.NewString(tableName, "user_id") _userToUserRelation.TargetUserId = field.NewString(tableName, "target_user_id") - _userToUserRelation.Relation = field.NewField(tableName, "relation") + _userToUserRelation.Relation = field.NewString(tableName, "relation") _userToUserRelation.User = userToUserRelationBelongsToUser{ db: db.Session(&gorm.Session{}), @@ -222,7 +222,7 @@ type userToUserRelation struct { ID field.Uint64 UserId field.String TargetUserId field.String - Relation field.Field + Relation field.String User userToUserRelationBelongsToUser TargetUser userToUserRelationBelongsToTargetUser @@ -245,7 +245,7 @@ func (u *userToUserRelation) updateTableName(table string) *userToUserRelation { u.ID = field.NewUint64(table, "id") u.UserId = field.NewString(table, "user_id") u.TargetUserId = field.NewString(table, "target_user_id") - u.Relation = field.NewField(table, "relation") + u.Relation = field.NewString(table, "relation") u.fillFieldMap() diff --git a/storage-new/models/ActivityTargetType.go b/storage-new/models/ActivityTargetType.go index 0a4546b..830c66a 100644 --- a/storage-new/models/ActivityTargetType.go +++ b/storage-new/models/ActivityTargetType.go @@ -11,6 +11,7 @@ const ( ActivitystreamsActivityTargetUser ActivitystreamsActivityTargetBoost ActivitystreamsActivityTargetReaction + ActivitystreamsActivityTargetFollow ) func (n *ActivitystreamsActivityTargetType) Value() (driver.Value, error) { @@ -29,4 +30,5 @@ var AllActivitystreamsActivityTargetTypes = []ActivitystreamsActivityTargetType{ ActivitystreamsActivityTargetUser, ActivitystreamsActivityTargetBoost, ActivitystreamsActivityTargetReaction, + ActivitystreamsActivityTargetFollow, } diff --git a/storage-new/models/UserRelationType.go b/storage-new/models/UserRelationType.go index 673852b..30387c5 100644 --- a/storage-new/models/UserRelationType.go +++ b/storage-new/models/UserRelationType.go @@ -7,6 +7,7 @@ type RelationType string const ( RelationFollow RelationType = "follow" // X follows Y + RelationFollowRequest RelationType = "follow-request" // X has requested to follow Y but is not yet following Y RelationMute RelationType = "mute" // X has Y muted (X doesn't see Y, but Y still X) RelationNoBoosts RelationType = "no-boosts" // X has Ys boosts muted RelationBlock RelationType = "block" // X has Y blocked (X doesn't see Y and Y doesn't see X) diff --git a/storage-new/models/UserToUserRelation.go b/storage-new/models/UserToUserRelation.go index 162dcfa..a8ccc34 100644 --- a/storage-new/models/UserToUserRelation.go +++ b/storage-new/models/UserToUserRelation.go @@ -9,5 +9,5 @@ type UserToUserRelation struct { UserId string TargetUser User // The user Y described in [RelationType] TargetUserId string - Relation RelationType `gorm:"type:relation_type"` + Relation string `gorm:"type:relation_type"` } diff --git a/web/public/api/activitypub/inbox.go b/web/public/api/activitypub/inbox.go index 906cc6a..92563d6 100644 --- a/web/public/api/activitypub/inbox.go +++ b/web/public/api/activitypub/inbox.go @@ -15,6 +15,7 @@ import ( "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/shared" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" ) @@ -86,6 +87,8 @@ func userInbox(w http.ResponseWriter, r *http.Request) { handleLike(w, r, data) case "Undo": handleUndo(w, r, data) + case "Follow": + handleFollow(w, r, data) default: webutils.ProblemDetailsStatusOnly(w, 500) } @@ -222,27 +225,28 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) { ) return } - // TODO: I *think* undo is only actively used for likes, but could also be used for - // undoing create's and thus removing notes - var likeActivityId string + // FIXME: Also handle other undo cases, such as follows + var targetObjectId string + var targetObjectType string // I *think* the spec says that this must be an object. Not sure though switch target := rawTarget.(type) { case string: - likeActivityId = target + targetObjectId = target case map[string]any: objType, ok := target["type"].(string) - if !ok || objType != "Like" { + if !ok { webutils.ProblemDetails( w, http.StatusBadRequest, "/errors/bad-request-data", "Bad activity data", - other.IntoPointer(`Undone object is not of type like. If you receive this error for a valid undo for a different activity type,please contact the Linstrom developers with the activity type`), + other.IntoPointer(`Target object does not have a type`), nil, ) return } - likeActivityId, ok = target["id"].(string) + targetObjectType = objType + targetObjectId, ok = target["id"].(string) if !ok { webutils.ProblemDetails( w, @@ -267,7 +271,7 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) { } // TODO: For above todo, get target resource via likeActivityId and remove approprietly // Don't just assume it was a like being undone - act, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(likeActivityId), dbgen.Activity.Type.Eq("like")). + act, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(targetObjectId), dbgen.Activity.Type.Eq("like")). First() switch err { case gorm.ErrRecordNotFound: @@ -276,7 +280,7 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) { default: log.Error(). Err(err). - Str("activity-id", likeActivityId). + Str("activity-id", targetObjectId). Msg("Error while looking for find activity") webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return @@ -290,7 +294,7 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) { default: log.Error(). Err(err). - Str("activity-id", likeActivityId). + Str("activity-id", targetObjectId). Msg("Error while looking for find activity") webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return @@ -319,3 +323,109 @@ func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) { webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } } + +func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) { + log := hlog.FromRequest(r) + log.Debug().Msg("Received follow request") + _ = object["id"].(string) + actorApId, ok := object["actor"].(string) + if !ok { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Request data needs to contain a field "actor" with a string value`), + nil, + ) + return + } + targetUrl, ok := object["object"].(string) + if !ok { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Request data needs to contain a field "object" of type string`), + nil, + ) + return + } + followedMatch := objectIdRegex.FindStringSubmatch(targetUrl) + if len(followedMatch) != 2 { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Object must be a link to a Linstrom AP user`), + nil, + ) + return + } + followedId := followedMatch[1] + followed, err := dbgen.User.Where(dbgen.User.ID.Eq(followedId)).First() + switch err { + case gorm.ErrRecordNotFound: + webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) + return + case nil: + default: + log.Error().Err(err).Str("target-id", followedId).Msg("Failed to get account from db") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + follower, err := activitypub.ImportRemoteAccountByAPUrl(actorApId) + u2u := dbgen.UserToUserRelation + followRelations, err := u2u.Where( + u2u.UserId.Eq(follower.ID), + u2u.TargetUserId.Eq(followed.ID), + u2u.Or( + u2u.Relation.Eq(models.RelationFollow), + u2u.Relation.Eq(models.RelationFollowRequest), + ), + ).Count() + if err != nil { + log.Error(). + Err(err). + Str("follower", follower.ID). + Str("followed", followedId). + Msg("Failed to count follow relations") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + if followRelations > 0 { + return + } + tx := dbgen.Q.Begin() + req := models.UserToUserRelation{ + Relation: string(models.RelationFollowRequest), + TargetUserId: followed.ID, + UserId: follower.ID, + } + err = tx.UserToUserRelation.Create(&req) + if err != nil { + tx.Rollback() + log.Error().Err(err).Any("follow-request", req).Msg("Failed to store follow request") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + activity := models.Activity{ + Id: shared.NewId(), + Type: string(models.ActivityFollow), + ObjectId: fmt.Sprint(req.ID), + ObjectType: uint32(models.ActivitystreamsActivityTargetFollow), + } + err = tx.Activity.Create(&activity) + if err != nil { + tx.Rollback() + log.Error().Err(err).Any("activity", activity).Msg("Failed to store follow activity") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + err = tx.Commit() + if err != nil { + log.Error().Err(err).Msg("Failed to commit follow activity transaction") + } +}