diff --git a/web/public/api/activitypub/inbox.go b/web/public/api/activitypub/inbox.go index f276e65..f96bd03 100644 --- a/web/public/api/activitypub/inbox.go +++ b/web/public/api/activitypub/inbox.go @@ -1,6 +1,7 @@ package activitypub import ( + "context" "database/sql" "encoding/json" "fmt" @@ -15,6 +16,7 @@ import ( "git.mstar.dev/mstar/goutils/other" "github.com/mitchellh/mapstructure" "github.com/rs/zerolog/hlog" + "github.com/rs/zerolog/log" "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" @@ -92,19 +94,20 @@ func userInbox(w http.ResponseWriter, r *http.Request) { // 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": - handleLike(w, r, data) + ok = handleLike(w, r, data) case "Undo": - handleUndo(w, r, data) + ok = handleUndo(w, r, data) case "Follow": - handleFollow(w, r, data) + ok = handleFollow(w, r, data) case "Accept": - handleAccept(w, r, data) + ok = handleAccept(w, r, data) case "Reject": - handleReject(w, r, data) + ok = handleReject(w, r, data) case "Create": - handleCreate(w, r, data) + ok = handleCreate(w, r, data) default: log.Warn(). Str("object-type", objectType). @@ -119,9 +122,23 @@ func userInbox(w http.ResponseWriter, r *http.Request) { } _ = webutils.ProblemDetailsStatusOnly(w, 500) } + if !ok { + err = dbgen.UnhandledMessage.Create(&models.UnhandledMessage{ + RawData: body, + ForUserId: userId, + GlobalInbox: false, + }) + if err != nil { + log.Error(). + Err(err). + Bytes("body", body). + Str("user-id", userId). + Msg("Failed to store failed inbound message for later processing") + } + } } -func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { +func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) bool { log := hlog.FromRequest(r) activityId := object["id"].(string) likerUrl, ok := object["actor"].(string) @@ -134,7 +151,7 @@ func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { other.IntoPointer(`Request data needs to contain a field "actor" with a string value`), nil, ) - return + return true } // TODO: Account for case where object is embedded in like targetUrl, ok := object["object"].(string) @@ -147,7 +164,7 @@ func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { other.IntoPointer(`Request data needs to contain a field "object" with a string value`), nil, ) - return + return true } targetIdMatches := objectIdRegex.FindStringSubmatch(targetUrl) if len(targetIdMatches) != 2 { @@ -156,7 +173,7 @@ func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { Str("url", targetUrl). Msg("Url didn't match id extractor regex") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return true } targetId := targetIdMatches[1] // Assume likes can only happen on notes for now @@ -173,11 +190,11 @@ func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { other.IntoPointer("There is no note with the target id"), nil, ) - return + return true default: log.Error().Err(err).Str("note-id", targetId).Msg("Failed to get note from db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } // Import liker after verifying that target note is correct liker, err := activitypub.ImportRemoteAccountByAPUrl(likerUrl) @@ -187,7 +204,7 @@ func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { Str("liker-url", likerUrl). Msg("Failed to import liking remote account") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } reaction := models.Reaction{ Note: *note, @@ -204,6 +221,7 @@ func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { _ = tx.Rollback() log.Error().Err(err).Any("raw-reaction", reaction).Msg("Failed to store reaction in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return false } // TODO: Create corresponding activity too activity := models.Activity{ @@ -217,15 +235,18 @@ func handleLike(w http.ResponseWriter, r *http.Request, object map[string]any) { _ = tx.Rollback() log.Error().Err(err).Any("raw-reaction", reaction).Msg("Failed to store reaction in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return false } err = tx.Commit() if err != nil { log.Error().Err(err).Msg("Failed to commit reaction transaction to db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return false } + return true } -func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) { +func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) bool { log := hlog.FromRequest(r) log.Debug().Msg("Received follow request") objectId, ok := object["id"].(string) @@ -238,7 +259,7 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data needs to contain a field "id" with a string value`), nil, ) - return + return true } actorApId, ok := object["actor"].(string) if !ok { @@ -250,7 +271,7 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data needs to contain a field "actor" with a string value`), nil, ) - return + return true } targetUrl, ok := object["object"].(string) if !ok { @@ -262,7 +283,7 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data needs to contain a field "object" of type string`), nil, ) - return + return true } followedMatch := objectIdRegex.FindStringSubmatch(targetUrl) if len(followedMatch) != 2 { @@ -274,25 +295,25 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Object must be a link to a Linstrom AP user`), nil, ) - return + return true } 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 + return true case nil: default: log.Error().Err(err).Str("target-id", followedId).Msg("Failed to get account from db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } follower, err := activitypub.ImportRemoteAccountByAPUrl(actorApId) if err != nil { log.Error().Err(err).Msg("Failed to import following account") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } u2u := dbgen.UserToUserRelation followRelations, err := u2u.Where( @@ -310,10 +331,10 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) Str("followed", followedId). Msg("Failed to count follow relations") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } if followRelations > 0 { - return + return true } tx := dbgen.Q.Begin() req := models.UserToUserRelation{ @@ -326,7 +347,7 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) _ = tx.Rollback() log.Error().Err(err).Any("follow-request", req).Msg("Failed to store follow request") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } activity := models.Activity{ Id: objectId, @@ -339,79 +360,31 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) _ = tx.Rollback() log.Error().Err(err).Any("activity", activity).Msg("Failed to store follow activity") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } err = tx.Commit() if err != nil { log.Error().Err(err).Msg("Failed to commit follow activity transaction") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } if !followed.RestrictedFollow { - tx = dbgen.Q.Begin() - _, err = u2u.Where(u2u.ID.Eq(req.ID)).UpdateColumn(u2u.Relation, models.RelationFollow) - if err != nil { - _ = tx.Rollback() - log.Error().Err(err).Msg("Failed to update follow to confirmed") - _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return - } - acceptActivity := models.Activity{ - Id: shared.NewId(), - Type: string(models.ActivityAccept), - ObjectId: activity.Id, - ObjectType: uint32(models.ActivitystreamsActivityTargetActivity), - } - err = tx.Activity.Create(&acceptActivity) - if err != nil { - _ = tx.Rollback() - log.Error().Err(err).Msg("Failed to store accept activity in db") - _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return - } - err = tx.Commit() - if err != nil { - log.Error().Err(err).Msg("Failed to commit follow accept to db") - _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return - } + // TODO: Move this to a separate function go func() { - // TODO: Maybe move this part to a separate function - time.Sleep(time.Millisecond * 20) - webAccept, err := AcceptFromStorage(r.Context(), acceptActivity.Id) + err := AcceptFollow(req.ID, followedId, followedId) if err != nil { - log.Error().Err(err).Msg("Failed to get accept from db") - return - } - webAccept.Context = activitypub.BaseLdContext - body, err := json.Marshal(webAccept) - if err != nil { - log.Error().Err(err).Msg("Failed to marshal accept") - return - } - res, _, err := webshared.RequestSigned( - "POST", - follower.RemoteInfo.InboxLink, - body, - followed, - ) - if err != nil { - log.Error().Err(err).Msg("Failed to send accept") - return - } - if res.StatusCode >= 400 { - body, _ = io.ReadAll(res.Body) log.Error(). - Int("status-code", res.StatusCode). - Bytes("body", body). - Msg("Post of accept failed") + Err(err). + Uint64("follow-request-id", req.ID). + Msg("Failed to auto-accept follow request") } }() } + return true } // WARN: Untested as can't send follow activities yet -func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) { +func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) bool { log := hlog.FromRequest(r) rawTarget, ok := object["object"] if !ok { @@ -423,7 +396,7 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data needs to contain a field "object"`), nil, ) - return + return true } // FIXME: Also handle other undo cases, such as follows var targetObjectId string @@ -443,7 +416,7 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Target object type must be a string with value "Follow"`), nil, ) - return + return true } targetObjectId, ok = target["id"].(string) if !ok { @@ -455,7 +428,7 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Missing id in undone object`), nil, ) - return + return true } default: _ = webutils.ProblemDetails( @@ -466,7 +439,7 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data needs to contain a field "object" of type string or object`), nil, ) - return + return true } internalIdMatch := objectIdRegex.FindStringSubmatch(targetObjectId) if len(internalIdMatch) != 2 { @@ -478,14 +451,14 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data target object is not internal id`), nil, ) - return + return true } internalId := internalIdMatch[1] followActivity, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(internalId)).First() switch err { case gorm.ErrRecordNotFound: _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) - return + return true case nil: default: log.Error(). @@ -493,7 +466,7 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) Str("target-id", internalId). Msg("Failed to get target follow activity from db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } relationId := other.Must(strconv.ParseUint(followActivity.ObjectId, 10, 64)) dbrel := dbgen.UserToUserRelation @@ -504,7 +477,7 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) case gorm.ErrRecordNotFound: // No need to rollback, nothing was done _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) - return + return true case nil: default: _ = tx.Rollback() @@ -513,7 +486,7 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) Str("target-id", internalId). Msg("Failed to update follow status to confirmed follow") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } activity := models.Activity{ Id: object["id"].(string), @@ -529,7 +502,7 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) Str("target-id", internalId). Msg("Failed to store accept activity in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } err = tx.Commit() if err != nil { @@ -538,12 +511,13 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) Str("target-id", internalId). Msg("Failed to commit accept transaction") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } + return true } // WARN: Untested as can't send follow activities yet -func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) { +func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) bool { log := hlog.FromRequest(r) rawTarget, ok := object["object"] if !ok { @@ -555,7 +529,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data needs to contain a field "object"`), nil, ) - return + return true } // FIXME: Also handle other undo cases, such as follows var targetObjectId string @@ -575,7 +549,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Target object type must be a string with value "Follow"`), nil, ) - return + return true } targetObjectId, ok = target["id"].(string) if !ok { @@ -587,7 +561,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Missing id in undone object`), nil, ) - return + return true } default: _ = webutils.ProblemDetails( @@ -598,7 +572,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data needs to contain a field "object" of type string or object`), nil, ) - return + return true } internalIdMatch := objectIdRegex.FindStringSubmatch(targetObjectId) if len(internalIdMatch) != 2 { @@ -610,14 +584,14 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer(`Request data target object is not internal id`), nil, ) - return + return true } internalId := internalIdMatch[1] followActivity, err := dbgen.Activity.Where(dbgen.Activity.Id.Eq(internalId)).First() switch err { case gorm.ErrRecordNotFound: _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) - return + return true case nil: default: log.Error(). @@ -625,7 +599,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) Str("target-id", internalId). Msg("Failed to get target follow activity from db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } relationId := other.Must(strconv.ParseUint(followActivity.ObjectId, 10, 64)) dbrel := dbgen.UserToUserRelation @@ -635,7 +609,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) case gorm.ErrRecordNotFound: // No need to rollback, nothing was done _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) - return + return true case nil: default: _ = tx.Rollback() @@ -644,7 +618,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) Str("target-id", internalId). Msg("Failed to delete follow status") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } _, err = tx.Activity.Where( dbgen.Activity.ObjectId.Eq(followActivity.Id), @@ -655,7 +629,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) _ = tx.Rollback() log.Error().Err(err).Msg("Failed to delete accept for later rejected follow") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } activity := models.Activity{ Id: object["id"].(string), @@ -671,7 +645,7 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) Str("target-id", internalId). Msg("Failed to store accept activity in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } err = tx.Commit() if err != nil { @@ -680,11 +654,12 @@ func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) Str("target-id", internalId). Msg("Failed to commit accept transaction") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } + return true } -func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) { +func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) bool { log := hlog.FromRequest(r) activity := ActivityCreate{} err := mapstructure.Decode(object, &activity) @@ -694,7 +669,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) Any("raw", object). Msg("Failed to marshal create activity to proper type") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return true } actingUser, err := activitypub.ImportRemoteAccountByAPUrl(activity.Actor) if err != nil { @@ -703,7 +678,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) Str("actor", activity.Actor). Msg("Failed to import remote actor for note") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return true } switch val := activity.Object.(type) { case string: @@ -711,14 +686,14 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) if err != nil { log.Error().Err(err).Msg("Failed to get local actor for importing targeted note") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } _, err = activitypub.ImportRemoteNote(val, actor) if err != nil { log.Error().Err(err).Str("note-url", val).Msg("Failed to import remote note that landed as id in the inbox") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } - return + return false case map[string]any: default: _ = webutils.ProblemDetails( @@ -729,7 +704,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer("Bad object data for create activity. Must be a struct or string"), nil, ) - return + return true } obj := activity.Object.(map[string]any) // Dumb hack since published timestamp is still a string at this point @@ -747,7 +722,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) Any("raw", activity.Object). Msg("Failed to unmarshal create object into note") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return true } if objectNote.Type != "Note" { _ = webutils.ProblemDetails( @@ -758,7 +733,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) other.IntoPointer("Bad object data for create activity. object.type must be 'Note'"), nil, ) - return + return true } dbNote := models.Note{ ID: objectNote.Id, @@ -788,7 +763,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) _ = tx.Rollback() log.Error().Err(err).Any("note", dbNote).Msg("Failed to create note in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } createActivity := models.Activity{ Type: string(models.ActivityCreate), @@ -801,12 +776,83 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) _ = tx.Rollback() log.Error().Err(err).Any("note", dbNote).Msg("Failed to create note create activity in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } err = tx.Commit() if err != nil { log.Error().Err(err).Any("note", dbNote).Msg("Failed to submit note creation in db") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) - return + return false } + return true +} + +func AcceptFollow( + ctx context.Context, + followRequestRelationId uint64, + followerId, followedId string, +) error { + u2u := dbgen.UserToUserRelation + a := dbgen.Activity + followRequestActivity, err := a.Where(a.ObjectId.Eq(fmt.Sprint(followRequestRelationId))). + First() + if err != nil { + return other.Error("webaction", "failed to get follow activity", err) + } + follower, err := dbgen.User.GetById(followedId) + if err != nil { + return other.Error("webaction", "failed to get follower from db", err) + } + followed, err := dbgen.User.GetById(followedId) + if err != nil { + return err + } + tx := dbgen.Q.Begin() + _, err = u2u.Where(u2u.ID.Eq(followRequestRelationId)). + UpdateColumn(u2u.Relation, models.RelationFollow) + if err != nil { + _ = tx.Rollback() + return err + } + acceptActivity := models.Activity{ + Id: shared.NewId(), + Type: string(models.ActivityAccept), + ObjectId: followRequestActivity.Id, + ObjectType: uint32(models.ActivitystreamsActivityTargetActivity), + } + err = tx.Activity.Create(&acceptActivity) + if err != nil { + _ = tx.Rollback() + return err + } + err = tx.Commit() + if err != nil { + return err + } + webAccept, err := AcceptFromStorage(ctx, acceptActivity.Id) + if err != nil { + return err + } + webAccept.Context = activitypub.BaseLdContext + body, err := json.Marshal(webAccept) + if err != nil { + return err + } + res, _, err := webshared.RequestSigned( + "POST", + follower.RemoteInfo.InboxLink, + body, + followed, + ) + if err != nil { + return err + } + if res.StatusCode >= 400 { + body, _ = io.ReadAll(res.Body) + log.Error(). + Int("status-code", res.StatusCode). + Bytes("body", body). + Msg("Post of accept failed") + } + return nil } diff --git a/web/public/api/activitypub/inboxUndo.go b/web/public/api/activitypub/inboxUndo.go index 95f611d..9eb05f1 100644 --- a/web/public/api/activitypub/inboxUndo.go +++ b/web/public/api/activitypub/inboxUndo.go @@ -12,7 +12,7 @@ import ( "git.mstar.dev/mstar/linstrom/storage-new/dbgen" ) -func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) { +func handleUndo(w http.ResponseWriter, r *http.Request, object map[string]any) bool { log := hlog.FromRequest(r) _ = object["id"].(string) _, ok := object["actor"].(string)