From e1051e81ac38716f787a87d6764188291a0aa17e Mon Sep 17 00:00:00 2001 From: mstar Date: Thu, 8 May 2025 13:03:49 +0200 Subject: [PATCH] Fix follow activity insertion, impl undo follow - Follow created a new id instead of using the Id of the activity for the db activity --- storage-new/models/UserToUserRelation.go | 2 +- web/public/api/activitypub/inbox.go | 147 ++-------------- web/public/api/activitypub/inboxUndo.go | 209 +++++++++++++++++++++++ 3 files changed, 225 insertions(+), 133 deletions(-) create mode 100644 web/public/api/activitypub/inboxUndo.go diff --git a/storage-new/models/UserToUserRelation.go b/storage-new/models/UserToUserRelation.go index a8ccc34..86343ed 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 string `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 92563d6..dcb47e2 100644 --- a/web/public/api/activitypub/inbox.go +++ b/web/public/api/activitypub/inbox.go @@ -7,7 +7,6 @@ import ( "io" "net/http" "regexp" - "strconv" webutils "git.mstar.dev/mstar/goutils/http" "git.mstar.dev/mstar/goutils/other" @@ -15,7 +14,6 @@ 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" ) @@ -198,136 +196,21 @@ func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { } } -func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) { - log := hlog.FromRequest(r) - _ = object["id"].(string) - _, 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 - } - rawTarget, ok := object["object"] - if !ok { - webutils.ProblemDetails( - w, - http.StatusBadRequest, - "/errors/bad-request-data", - "Bad activity data", - other.IntoPointer(`Request data needs to contain a field "object"`), - nil, - ) - return - } - // 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: - targetObjectId = target - case map[string]any: - objType, ok := target["type"].(string) - if !ok { - webutils.ProblemDetails( - w, - http.StatusBadRequest, - "/errors/bad-request-data", - "Bad activity data", - other.IntoPointer(`Target object does not have a type`), - nil, - ) - return - } - targetObjectType = objType - targetObjectId, ok = target["id"].(string) - if !ok { - webutils.ProblemDetails( - w, - http.StatusBadRequest, - "/errors/bad-request-data", - "Bad activity data", - other.IntoPointer(`Missing id in undone object`), - nil, - ) - return - } - default: - 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 or object`), - nil, - ) - return - } - // 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(targetObjectId), dbgen.Activity.Type.Eq("like")). - First() - switch err { - case gorm.ErrRecordNotFound: - return - case nil: - default: - log.Error(). - Err(err). - Str("activity-id", targetObjectId). - Msg("Error while looking for find activity") - webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return - } - reactionId := uint(other.Must(strconv.ParseUint(act.ObjectId, 10, 64))) - reaction, err := dbgen.Reaction.Where(dbgen.Reaction.ID.Eq(reactionId)).First() - switch err { - case gorm.ErrRecordNotFound: - return - case nil: - default: - log.Error(). - Err(err). - Str("activity-id", targetObjectId). - Msg("Error while looking for find activity") - webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return - } - tx := dbgen.Q.Begin() - _, err = tx.Activity.Where(dbgen.Activity.Id.Eq(act.Id)).Delete() - if err != nil { - _ = tx.Rollback() - log.Error().Err(err).Str("activity-id", act.Id).Msg("Failed to delete activity on undo") - webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return - } - _, err = tx.Reaction.Where(dbgen.Reaction.ID.Eq(reaction.ID)).Delete() - if err != nil { - _ = tx.Rollback() - log.Error(). - Err(err). - Uint("reaction-id", reaction.ID). - Msg("Failed to delete reaction on undo") - webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return - } - err = tx.Commit() - if err != nil { - log.Error().Err(err).Msg("Failed to delete reaction and activity") - 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) + objectId, ok := object["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 + } actorApId, ok := object["actor"].(string) if !ok { webutils.ProblemDetails( @@ -382,8 +265,8 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) u2u.UserId.Eq(follower.ID), u2u.TargetUserId.Eq(followed.ID), u2u.Or( - u2u.Relation.Eq(models.RelationFollow), - u2u.Relation.Eq(models.RelationFollowRequest), + u2u.Relation.Eq(string(models.RelationFollow)), + u2u.Relation.Eq(string(models.RelationFollowRequest)), ), ).Count() if err != nil { @@ -412,7 +295,7 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) return } activity := models.Activity{ - Id: shared.NewId(), + Id: objectId, Type: string(models.ActivityFollow), ObjectId: fmt.Sprint(req.ID), ObjectType: uint32(models.ActivitystreamsActivityTargetFollow), diff --git a/web/public/api/activitypub/inboxUndo.go b/web/public/api/activitypub/inboxUndo.go new file mode 100644 index 0000000..38edcaf --- /dev/null +++ b/web/public/api/activitypub/inboxUndo.go @@ -0,0 +1,209 @@ +package activitypub + +import ( + "net/http" + "strconv" + + webutils "git.mstar.dev/mstar/goutils/http" + "git.mstar.dev/mstar/goutils/other" + "github.com/rs/zerolog/hlog" + "gorm.io/gorm" + + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" +) + +func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) { + log := hlog.FromRequest(r) + _ = object["id"].(string) + _, 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 + } + rawTarget, ok := object["object"] + if !ok { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Request data needs to contain a field "object"`), + nil, + ) + return + } + // 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: + targetObjectId = target + case map[string]any: + objType, ok := target["type"].(string) + if !ok { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Target object does not have a type`), + nil, + ) + return + } + targetObjectType = objType + targetObjectId, ok = target["id"].(string) + if !ok { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Missing id in undone object`), + nil, + ) + return + } + default: + 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 or object`), + nil, + ) + return + } + switch targetObjectType { + case "Like": + undoLike(w, r, object, targetObjectId) + case "Follow": + undoFollow(w, r, object, targetObjectId) + default: + log.Error(). + Str("undo-target-type", targetObjectType). + Msg("Unknown/unimplemented undo target type") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + } +} + +func undoLike(w http.ResponseWriter, r *http.Request, object map[string]any, targetId string) { + log := hlog.FromRequest(r) + act, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(targetId), dbgen.Activity.Type.Eq("like")). + First() + switch err { + case gorm.ErrRecordNotFound: + return + case nil: + default: + log.Error(). + Err(err). + Str("activity-id", targetId). + Msg("Error while looking for like activity") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + reactionId := uint(other.Must(strconv.ParseUint(act.ObjectId, 10, 64))) + reaction, err := dbgen.Reaction.Where(dbgen.Reaction.ID.Eq(reactionId)).First() + switch err { + case gorm.ErrRecordNotFound: + return + case nil: + default: + log.Error(). + Err(err). + Str("activity-id", targetId). + Msg("Error while looking for find activity") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + tx := dbgen.Q.Begin() + _, err = tx.Activity.Where(dbgen.Activity.Id.Eq(act.Id)).Delete() + if err != nil { + _ = tx.Rollback() + log.Error().Err(err).Str("activity-id", act.Id).Msg("Failed to delete activity on undo") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + _, err = tx.Reaction.Where(dbgen.Reaction.ID.Eq(reaction.ID)).Delete() + if err != nil { + _ = tx.Rollback() + log.Error(). + Err(err). + Uint("reaction-id", reaction.ID). + Msg("Failed to delete reaction on undo") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + err = tx.Commit() + if err != nil { + log.Error().Err(err).Msg("Failed to delete reaction and activity") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + } +} + +func undoFollow(w http.ResponseWriter, r *http.Request, object map[string]any, targetId string) { + log := hlog.FromRequest(r) + act, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(targetId), dbgen.Activity.Type.Eq("follow")). + First() + switch err { + case gorm.ErrRecordNotFound: + return + case nil: + default: + log.Error(). + Err(err). + Str("activity-id", targetId). + Msg("Error while looking for follow activity") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + relationId := other.Must(strconv.ParseUint(act.ObjectId, 10, 64)) + tx := dbgen.Q.Begin() + _, err = tx.Activity.Where(dbgen.Activity.Id.Eq(act.Id)).Delete() + if err != nil { + _ = tx.Rollback() + log.Error(). + Err(err). + Str("activity-id", act.Id). + Msg("Failed to delete follow activity on undo") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + // Delete all activities that reference the follow activity (so accept/reject if exists) + _, err = tx.Activity.Where(dbgen.Activity.ObjectId.Eq(act.Id)).Delete() + if err != nil { + _ = tx.Rollback() + log.Error(). + Err(err). + Str("activity-id", act.Id). + Msg("Failed to delete accept/reject activity for follow on undo") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + _, err = tx.UserToUserRelation.Where(dbgen.UserToUserRelation.ID.Eq(relationId)).Delete() + if err != nil { + _ = tx.Rollback() + log.Error(). + Err(err). + Str("activity-id", act.Id). + Uint64("relation-id", relationId). + Msg("Failed to delete user-to-user relation for follow on undo") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + err = tx.Commit() + if err != nil { + log.Error().Err(err).Msg("Failed to delete reaction and activity") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + } +}