From ff6a730e5813c2af5e8d9596ab41fac2f921c48b Mon Sep 17 00:00:00 2001 From: mStar Date: Sat, 10 May 2025 11:18:28 +0200 Subject: [PATCH] Follow accept works and messags are pushed as expected --- cmd/model-gen/main.go | 1 + .../dbgen/user_to_user_relations.gen.go | 25 ++ storage-new/models/UserToUserRelation.go | 11 + temp.toml | 4 +- web/debug/posts.go | 51 +++- web/public/api/activitypub/activityAccept.go | 74 +++++- web/public/api/activitypub/activityCreate.go | 10 +- web/public/api/activitypub/activityFollow.go | 94 ++++++++ web/public/api/activitypub/inbox.go | 223 +++++++++++++++++- web/public/api/activitypub/objectNote.go | 8 +- 10 files changed, 482 insertions(+), 19 deletions(-) diff --git a/cmd/model-gen/main.go b/cmd/model-gen/main.go index 829ede7..af1261b 100644 --- a/cmd/model-gen/main.go +++ b/cmd/model-gen/main.go @@ -50,6 +50,7 @@ func main() { g.ApplyInterface(func(models.IUser) {}, models.User{}) g.ApplyInterface(func(models.IAccessToken) {}, models.AccessToken{}) g.ApplyInterface(func(models.INote) {}, models.Note{}) + g.ApplyInterface(func(models.IUserToUserRelation) {}, models.UserToUserRelation{}) log.Info().Msg("Extra features applied, starting generation") g.Execute() diff --git a/storage-new/dbgen/user_to_user_relations.gen.go b/storage-new/dbgen/user_to_user_relations.gen.go index 1989e93..e600a27 100644 --- a/storage-new/dbgen/user_to_user_relations.gen.go +++ b/storage-new/dbgen/user_to_user_relations.gen.go @@ -7,6 +7,7 @@ package dbgen import ( "context" "database/sql" + "strings" "git.mstar.dev/mstar/linstrom/storage-new/models" "gorm.io/gorm" @@ -587,6 +588,30 @@ type IUserToUserRelationDo interface { Returning(value interface{}, columns ...string) IUserToUserRelationDo UnderlyingDB() *gorm.DB schema.Tabler + + GetFollowersForId(id string) (result []string, err error) +} + +// SELECT u.inbox_link +// FROM user_to_user_relations r +// LEFT JOIN user_remote_links u +// ON r.user_id = u.user_id +// WHERE +// +// r.target_user_id = @id AND +// r.relation = 'follow' +func (u userToUserRelationDo) GetFollowersForId(id string) (result []string, err error) { + var params []interface{} + + var generateSQL strings.Builder + params = append(params, id) + generateSQL.WriteString("SELECT u.inbox_link FROM user_to_user_relations r LEFT JOIN user_remote_links u ON r.user_id = u.user_id WHERE r.target_user_id = ? AND r.relation = 'follow' ") + + var executeSQL *gorm.DB + executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params...).Find(&result) // ignore_security_alert + err = executeSQL.Error + + return } func (u userToUserRelationDo) Debug() IUserToUserRelationDo { diff --git a/storage-new/models/UserToUserRelation.go b/storage-new/models/UserToUserRelation.go index 86343ed..95eabd2 100644 --- a/storage-new/models/UserToUserRelation.go +++ b/storage-new/models/UserToUserRelation.go @@ -11,3 +11,14 @@ type UserToUserRelation struct { TargetUserId string Relation string // `gorm:"type:relation_type"` } + +type IUserToUserRelation interface { + // SELECT u.inbox_link + // FROM user_to_user_relations r + // LEFT JOIN user_remote_links u + // ON r.user_id = u.user_id + // WHERE + // r.target_user_id = @id AND + // r.relation = 'follow' + GetFollowersForId(id string) ([]string, error) +} diff --git a/temp.toml b/temp.toml index 2f0fe77..828a979 100644 --- a/temp.toml +++ b/temp.toml @@ -1,7 +1,7 @@ [general] protocol = "https" - domain = "serveo.net" - subdomain = "b2f4e7c5596220d4c4957b24f6954220" + domain = "lhr.life" + subdomain = "d0deb4c7b3ee95" private_port = 8080 public_port = 443 diff --git a/web/debug/posts.go b/web/debug/posts.go index 8e4db0f..9aed01b 100644 --- a/web/debug/posts.go +++ b/web/debug/posts.go @@ -4,6 +4,7 @@ import ( "database/sql" "encoding/json" "errors" + "io" "net/http" webutils "git.mstar.dev/mstar/goutils/http" @@ -11,10 +12,12 @@ import ( "github.com/rs/zerolog/hlog" "gorm.io/gorm" + "git.mstar.dev/mstar/linstrom/activitypub" "git.mstar.dev/mstar/linstrom/shared" "git.mstar.dev/mstar/linstrom/storage-new" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" + webap "git.mstar.dev/mstar/linstrom/web/public/api/activitypub" webshared "git.mstar.dev/mstar/linstrom/web/shared" ) @@ -66,7 +69,8 @@ func postAs(w http.ResponseWriter, r *http.Request) { AccessLevel: models.NOTE_TARGET_PUBLIC, OriginId: 1, } - err = n.Select( + tx := dbgen.Q.Begin() + err = tx.Note.Select( n.ID, n.CreatorId, n.RawContent, @@ -78,6 +82,7 @@ func postAs(w http.ResponseWriter, r *http.Request) { n.OriginId, ).Create(¬e) if err != nil { + _ = tx.Rollback() log.Error(). Err(err). Str("username", data.Username). @@ -92,11 +97,51 @@ func postAs(w http.ResponseWriter, r *http.Request) { ObjectId: note.ID, ObjectType: uint32(models.ActivitystreamsActivityTargetNote), } - a := dbgen.Activity - err = a.Create(&activity) + err = tx.Activity.Create(&activity) if err != nil { + _ = tx.Rollback() log.Error().Err(err).Msg("Failed to create activity for new note") } + err = tx.Commit() + if err != nil { + log.Error().Err(err).Msg("Failed to commit note creation") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + u2u := dbgen.UserToUserRelation + links, err := u2u.GetFollowersForId(user.ID) + if err != nil { + log.Error().Err(err).Msg("Failed to get follower inbox links for user") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + log.Debug().Strs("links", links).Send() + act, err := webap.CreateFromStorage(r.Context(), activity.Id) + if err != nil { + log.Error().Err(err).Msg("Failed to fetch and format new note") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + act.Context = activitypub.BaseLdContext + outData, err := json.Marshal(act) + if err != nil { + log.Error().Err(err).Msg("Failed to marshal new note") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + for _, link := range links { + log.Debug().Str("target-inbox", link).Msg("Sending message to") + go func() { + res, err := webshared.RequestSignedCavage("POST", link, outData, user) + if err != nil { + log.Warn().Err(err).Str("link", link).Msg("Failed to send create to target inbox") + } + if res.StatusCode >= 400 { + body, _ := io.ReadAll(res.Body) + log.Warn().Int("status-code", res.StatusCode).Bytes("body", body).Msg("Bad reply") + } + }() + } } func notesFrom(w http.ResponseWriter, r *http.Request) { diff --git a/web/public/api/activitypub/activityAccept.go b/web/public/api/activitypub/activityAccept.go index e920807..43c6a33 100644 --- a/web/public/api/activitypub/activityAccept.go +++ b/web/public/api/activitypub/activityAccept.go @@ -1,5 +1,75 @@ package activitypub -import "net/http" +import ( + "context" + "encoding/json" + "fmt" + "net/http" -func activityAccept(w http.ResponseWriter, r *http.Request) {} + webutils "git.mstar.dev/mstar/goutils/http" + "github.com/rs/zerolog/hlog" + "gorm.io/gorm" + + "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/storage-new" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" + "git.mstar.dev/mstar/linstrom/storage-new/models" +) + +type ActivityAcceptOut struct { + Context any `json:"@context,omitempty"` + Id string `json:"id"` + Type string `json:"type"` + Actor string `json:"actor"` + Object any `json:"object"` +} + +func activityAccept(w http.ResponseWriter, r *http.Request) { + log := hlog.FromRequest(r) + id := r.PathValue("id") + activity, err := CreateFromStorage(r.Context(), id) + switch err { + case gorm.ErrRecordNotFound: + webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) + case nil: + activity.Context = activitypub.BaseLdContext + data, err := json.Marshal(activity) + if err != nil { + log.Error().Err(err).Any("activity", activity).Msg("Failed to marshal create activity") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + w.Header().Add("Content-Type", "application/activity+json") + fmt.Fprint(w, string(data)) + default: + if storage.HandleReconnectError(err) { + log.Error().Err(err).Msg("Connection failed, restart attempt started") + } else { + log.Error().Err(err).Msg("Failed to get create activity from db") + } + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + } +} + +func AcceptFromStorage(ctx context.Context, id string) (*ActivityAcceptOut, error) { + a := dbgen.Activity + activity, err := a.Where(a.Id.Eq(id), a.Type.Eq(string(models.ActivityAccept))).First() + if err != nil { + return nil, err + } + // switch activity.ObjectType { + // case models.ActivitystreamsActivityTargetFollow: + // default: + // return nil, errors.New("unknown activity target type") + // } + follow, err := FollowFromStorage(ctx, activity.ObjectId) + if err != nil { + return nil, err + } + return &ActivityAcceptOut{ + Id: id, + Actor: follow.Object.(string), + Type: "Accept", + Object: follow, + }, nil +} diff --git a/web/public/api/activitypub/activityCreate.go b/web/public/api/activitypub/activityCreate.go index 2285e24..76f56b9 100644 --- a/web/public/api/activitypub/activityCreate.go +++ b/web/public/api/activitypub/activityCreate.go @@ -17,7 +17,7 @@ import ( "git.mstar.dev/mstar/linstrom/storage-new/models" ) -type activityCreateOut struct { +type ActivityCreateOut struct { Context any `json:"@context,omitempty"` Id string `json:"id"` Type string `json:"type"` @@ -28,7 +28,7 @@ type activityCreateOut struct { func activityCreate(w http.ResponseWriter, r *http.Request) { log := hlog.FromRequest(r) id := r.PathValue("id") - activity, err := createFromStorage(r.Context(), id) + activity, err := CreateFromStorage(r.Context(), id) switch err { case gorm.ErrRecordNotFound: webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) @@ -56,7 +56,7 @@ func activityCreate(w http.ResponseWriter, r *http.Request) { // Does not set the context for the activity, in case the activity is embedded // in another activity or object. That's the responsibility of the handler // getting the final result -func createFromStorage(ctx context.Context, id string) (*activityCreateOut, error) { +func CreateFromStorage(ctx context.Context, id string) (*ActivityCreateOut, error) { // log := log.Ctx(ctx) a := dbgen.Activity activity, err := a.Where(a.Type.Eq(string(models.ActivityCreate))). @@ -67,11 +67,11 @@ func createFromStorage(ctx context.Context, id string) (*activityCreateOut, erro } switch models.ActivitystreamsActivityTargetType(activity.ObjectType) { case models.ActivitystreamsActivityTargetNote: - note, err := noteFromStorage(ctx, activity.ObjectId) + note, err := NoteFromStorage(ctx, activity.ObjectId) if err != nil { return nil, err } - out := activityCreateOut{ + out := ActivityCreateOut{ Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/create/" + id, Type: "Create", Actor: note.AttributedTo, diff --git a/web/public/api/activitypub/activityFollow.go b/web/public/api/activitypub/activityFollow.go index 7c30024..4cb1e36 100644 --- a/web/public/api/activitypub/activityFollow.go +++ b/web/public/api/activitypub/activityFollow.go @@ -1 +1,95 @@ package activitypub + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strconv" + + webutils "git.mstar.dev/mstar/goutils/http" + "github.com/rs/zerolog/hlog" + "gorm.io/gorm" + + "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/storage-new" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" + "git.mstar.dev/mstar/linstrom/storage-new/models" +) + +type ActivityFollowOut struct { + Context any `json:"@context,omitempty"` + Id string `json:"id"` + Type string `json:"type"` + Actor string `json:"actor"` + Object any `json:"object"` +} + +func activityFollow(w http.ResponseWriter, r *http.Request) { + log := hlog.FromRequest(r) + id := r.PathValue("id") + activity, err := FollowFromStorage(r.Context(), id) + switch err { + case gorm.ErrRecordNotFound: + webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) + case nil: + activity.Context = activitypub.BaseLdContext + data, err := json.Marshal(activity) + if err != nil { + log.Error().Err(err).Any("activity", activity).Msg("Failed to marshal create activity") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + w.Header().Add("Content-Type", "application/activity+json") + fmt.Fprint(w, string(data)) + default: + if storage.HandleReconnectError(err) { + log.Error().Err(err).Msg("Connection failed, restart attempt started") + } else { + log.Error().Err(err).Msg("Failed to get create activity from db") + } + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + } +} + +func FollowFromStorage(ctx context.Context, id string) (*ActivityFollowOut, error) { + ac := dbgen.Activity + u2u := dbgen.UserToUserRelation + u := dbgen.User + // log := log.Ctx(ctx) + activity, err := ac.Where(ac.Id.Eq(id), ac.Type.Eq(string(models.ActivityFollow))).First() + if err != nil { + return nil, err + } + followId, err := strconv.ParseUint(activity.ObjectId, 10, 64) + if err != nil { + return nil, err + } + relation, err := u2u.Where(u2u.ID.Eq(followId)).First() + if err != nil { + return nil, err + } + follower, err := u.Where(u.ID.Eq(relation.UserId)).Preload(u.RemoteInfo).First() + if err != nil { + return nil, err + } + followed, err := u.Where(u.ID.Eq(relation.TargetUserId)).Preload(u.RemoteInfo).First() + if err != nil { + return nil, err + } + out := ActivityFollowOut{ + Id: id, + Type: "Follow", + } + if follower.RemoteInfo != nil { + out.Actor = follower.RemoteInfo.ApLink + } else { + out.Actor = userIdToApUrl(follower.ID) + } + if followed.RemoteInfo != nil { + out.Object = followed.RemoteInfo.ApLink + } else { + out.Object = userIdToApUrl(followed.ID) + } + return &out, nil +} diff --git a/web/public/api/activitypub/inbox.go b/web/public/api/activitypub/inbox.go index db8dac1..c304450 100644 --- a/web/public/api/activitypub/inbox.go +++ b/web/public/api/activitypub/inbox.go @@ -8,6 +8,7 @@ import ( "net/http" "regexp" "strconv" + "time" webutils "git.mstar.dev/mstar/goutils/http" "git.mstar.dev/mstar/goutils/other" @@ -15,8 +16,10 @@ 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" + webshared "git.mstar.dev/mstar/linstrom/web/shared" ) var objectIdRegex = regexp.MustCompile( @@ -316,8 +319,54 @@ func handleFollow(w http.ResponseWriter, r *http.Request, object map[string]any) if err != nil { log.Error().Err(err).Msg("Failed to commit follow activity transaction") } + if !followed.RestrictedFollow { + // FIXME: Handle errors + tx = dbgen.Q.Begin() + _, err = u2u.Where(u2u.ID.Eq(req.ID)).UpdateColumn(u2u.Relation, models.RelationFollow) + acceptActivity := models.Activity{ + Id: shared.NewId(), + Type: string(models.ActivityAccept), + ObjectId: activity.Id, + ObjectType: uint32(models.ActivitystreamsActivityTargetActivity), + } + err = tx.Activity.Create(&acceptActivity) + tx.Commit() + go func() { + // FIXME: Clean this entire mess up + time.Sleep(time.Millisecond * 20) + webAccept, err := AcceptFromStorage(r.Context(), acceptActivity.Id) + 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.RequestSignedCavage( + "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") + } + }() + } } +// WARN: Untested as can't send follow activities yet func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) { log := hlog.FromRequest(r) rawTarget, ok := object["object"] @@ -404,9 +453,124 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) } relationId := other.Must(strconv.ParseUint(followActivity.ObjectId, 10, 64)) dbrel := dbgen.UserToUserRelation - _, err = dbrel.Where(dbrel.ID.Eq(relationId)). + tx := dbgen.Q.Begin() + _, err = tx.UserToUserRelation.Where(dbrel.ID.Eq(relationId)). UpdateColumn(dbrel.Relation, models.RelationFollow) switch err { + case gorm.ErrRecordNotFound: + // No need to rollback, nothing was done + webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) + return + case nil: + default: + _ = tx.Rollback() + log.Error(). + Err(err). + Str("target-id", internalId). + Msg("Failed to update follow status to confirmed follow") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + activity := models.Activity{ + Id: object["id"].(string), + Type: string(models.ActivityAccept), + ObjectType: uint32(models.ActivitystreamsActivityTargetActivity), + ObjectId: followActivity.Id, + } + err = tx.Activity.Create(&activity) + if err != nil { + err = tx.Rollback() + log.Error(). + Err(err). + Str("target-id", internalId). + Msg("Failed to store accept activity in db") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + err = tx.Commit() + if err != nil { + log.Error(). + Err(err). + Str("target-id", internalId). + Msg("Failed to commit accept transaction") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } +} + +// WARN: Untested as can't send follow activities yet +func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) { + log := hlog.FromRequest(r) + 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 + // 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) + // TODO: Ensure accept is only used for follows + if !ok || objType != "Follow" { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Target object type must be a string with value "Follow"`), + nil, + ) + return + } + 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 + } + internalIdMatch := objectIdRegex.FindStringSubmatch(targetObjectId) + if len(internalIdMatch) != 2 { + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "Bad activity data", + other.IntoPointer(`Request data target object is not internal id`), + nil, + ) + return + } + 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 @@ -419,6 +583,59 @@ func handleAccept(w http.ResponseWriter, r *http.Request, object map[string]any) webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } + relationId := other.Must(strconv.ParseUint(followActivity.ObjectId, 10, 64)) + dbrel := dbgen.UserToUserRelation + tx := dbgen.Q.Begin() + _, err = tx.UserToUserRelation.Where(dbrel.ID.Eq(relationId)).Delete() + switch err { + case gorm.ErrRecordNotFound: + // No need to rollback, nothing was done + webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) + return + case nil: + default: + _ = tx.Rollback() + log.Error(). + Err(err). + Str("target-id", internalId). + Msg("Failed to delete follow status") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + _, err = tx.Activity.Where( + dbgen.Activity.ObjectId.Eq(followActivity.Id), + dbgen.Activity.Type.Eq("Accept"), + ). + Delete() + if err != nil { + _ = tx.Rollback() + log.Error().Err(err).Msg("Failed to delete accept for later rejected follow") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + activity := models.Activity{ + Id: object["id"].(string), + Type: string(models.ActivityAccept), + ObjectType: uint32(models.ActivitystreamsActivityTargetActivity), + ObjectId: followActivity.Id, + } + err = tx.Activity.Create(&activity) + if err != nil { + err = tx.Rollback() + log.Error(). + Err(err). + Str("target-id", internalId). + Msg("Failed to store accept activity in db") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + err = tx.Commit() + if err != nil { + log.Error(). + Err(err). + Str("target-id", internalId). + Msg("Failed to commit accept transaction") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } } - -func handleReject(w http.ResponseWriter, r *http.Request, object map[string]any) {} diff --git a/web/public/api/activitypub/objectNote.go b/web/public/api/activitypub/objectNote.go index 9ca67c7..faec61a 100644 --- a/web/public/api/activitypub/objectNote.go +++ b/web/public/api/activitypub/objectNote.go @@ -17,7 +17,7 @@ import ( "git.mstar.dev/mstar/linstrom/storage-new/dbgen" ) -type objectNoteOut struct { +type ObjectNoteOut struct { // Context should be set, if needed, by the endpoint handler Context any `json:"@context,omitempty"` @@ -48,7 +48,7 @@ type objectNoteOut struct { func objectNote(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") log := hlog.FromRequest(r) - note, err := noteFromStorage(r.Context(), id) + note, err := NoteFromStorage(r.Context(), id) switch err { case gorm.ErrRecordNotFound: webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) @@ -73,12 +73,12 @@ func objectNote(w http.ResponseWriter, r *http.Request) { } } -func noteFromStorage(ctx context.Context, id string) (*objectNoteOut, error) { +func NoteFromStorage(ctx context.Context, id string) (*ObjectNoteOut, error) { note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(id)).Preload(dbgen.Note.Creator).First() if err != nil { return nil, err } - data := &objectNoteOut{ + data := &ObjectNoteOut{ Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/note/" + id, Type: "Note", Published: note.CreatedAt,