From d86ad370df4393af9873d54fcd114141551a0b92 Mon Sep 17 00:00:00 2001 From: mstar Date: Fri, 13 Jun 2025 13:42:56 +0200 Subject: [PATCH] Move translators db->ap to separate module --- activitypub/translators/accept.go | 48 +++++++ activitypub/translators/collection.go | 23 +++ activitypub/translators/create.go | 58 ++++++++ activitypub/translators/follow.go | 61 ++++++++ activitypub/translators/note.go | 113 +++++++++++++++ activitypub/translators/user.go | 126 ++++++++++++++++ storage-new/models/0allTypes.go | 1 + web/debug/posts.go | 4 +- web/debug/users.go | 4 +- web/public/api/activitypub/activityAccept.go | 45 +----- web/public/api/activitypub/activityCreate.go | 56 +------- web/public/api/activitypub/activityFollow.go | 58 +------- web/public/api/activitypub/collection.go | 22 --- web/public/api/activitypub/inbox.go | 7 +- web/public/api/activitypub/note.go | 110 +------------- web/public/api/activitypub/user.go | 143 ++----------------- 16 files changed, 456 insertions(+), 423 deletions(-) create mode 100644 activitypub/translators/accept.go create mode 100644 activitypub/translators/collection.go create mode 100644 activitypub/translators/create.go create mode 100644 activitypub/translators/follow.go create mode 100644 activitypub/translators/note.go create mode 100644 activitypub/translators/user.go diff --git a/activitypub/translators/accept.go b/activitypub/translators/accept.go new file mode 100644 index 0000000..2a99199 --- /dev/null +++ b/activitypub/translators/accept.go @@ -0,0 +1,48 @@ +package translators + +import ( + "context" + "fmt" + "strings" + + "git.mstar.dev/mstar/linstrom/config" + "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 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 + } + var outId string + if strings.HasPrefix(id, "http") { + outId = id + } else { + outId = fmt.Sprintf("%s/api/activitypub/activity/accept/%s", config.GlobalConfig.General.GetFullPublicUrl(), id) + } + return &ActivityAcceptOut{ + Id: outId, + Actor: follow.Object.(string), + Type: "Accept", + Object: follow, + }, nil +} diff --git a/activitypub/translators/collection.go b/activitypub/translators/collection.go new file mode 100644 index 0000000..2fb9ca6 --- /dev/null +++ b/activitypub/translators/collection.go @@ -0,0 +1,23 @@ +package translators + +// Used for both unordered and ordered +type CollectionOut struct { + Context any `json:"@context,omitempty"` + Summary string `json:"summary,omitempty"` + Type string `json:"type"` + Items []any `json:"items,omitempty"` + Id string `json:"id"` + TotalItems int `json:"totalItems"` + First string `json:"first"` +} + +// Used for both unordered and ordered +type CollectionPageOut struct { + Context any `json:"@context,omitempty"` + Type string `json:"type"` + Id string `json:"id"` + PartOf string `json:"partOf"` + Next string `json:"next,omitempty"` + Previous string `json:"prev,omitempty"` + Items []any `json:"items"` +} diff --git a/activitypub/translators/create.go b/activitypub/translators/create.go new file mode 100644 index 0000000..f6a1d7f --- /dev/null +++ b/activitypub/translators/create.go @@ -0,0 +1,58 @@ +package translators + +import ( + "context" + + "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" + "git.mstar.dev/mstar/linstrom/storage-new/models" +) + +type ActivityCreate struct { + Context any `json:"@context,omitempty"` + Id string `json:"id"` + Type string `json:"type"` + Actor string `json:"actor"` + Object any `json:"object"` +} + +// Find a create activity from the db and format it for activitypub reads +// 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) (*ActivityCreate, error) { + // log := log.Ctx(ctx) + a := dbgen.Activity + activity, err := a.Where(a.Type.Eq(string(models.ActivityCreate))). + Where(a.Id.Eq(id)). + First() + if err != nil { + return nil, err + } + switch models.ActivitystreamsActivityTargetType(activity.ObjectType) { + case models.ActivitystreamsActivityTargetNote: + note, err := NoteFromStorage(ctx, activity.ObjectId) + if err != nil { + return nil, err + } + out := ActivityCreate{ + Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/create/" + id, + Type: "Create", + Actor: note.AttributedTo, + Object: note, + } + return &out, nil + case models.ActivitystreamsActivityTargetBoost: + panic("Not implemented") + case models.ActivitystreamsActivityTargetReaction: + panic("Not implemented") + case models.ActivitystreamsActivityTargetActivity: + panic("Not implemented") + case models.ActivitystreamsActivityTargetUser: + panic("Not implemented") + case models.ActivitystreamsActivityTargetUnknown: + panic("Not implemented") + default: + panic("Not implemented") + } +} diff --git a/activitypub/translators/follow.go b/activitypub/translators/follow.go new file mode 100644 index 0000000..834ebb0 --- /dev/null +++ b/activitypub/translators/follow.go @@ -0,0 +1,61 @@ +package translators + +import ( + "context" + "strconv" + + "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/config" + "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 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: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/activity/follow/" + id, + Type: "Follow", + } + if follower.RemoteInfo != nil { + out.Actor = follower.RemoteInfo.ApLink + } else { + out.Actor = activitypub.UserIdToApUrl(follower.ID) + } + if followed.RemoteInfo != nil { + out.Object = followed.RemoteInfo.ApLink + } else { + out.Object = activitypub.UserIdToApUrl(followed.ID) + } + return &out, nil +} diff --git a/activitypub/translators/note.go b/activitypub/translators/note.go new file mode 100644 index 0000000..a7b3bae --- /dev/null +++ b/activitypub/translators/note.go @@ -0,0 +1,113 @@ +package translators + +import ( + "context" + "fmt" + "time" + + "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" + "git.mstar.dev/mstar/linstrom/storage-new/models" + webshared "git.mstar.dev/mstar/linstrom/web/shared" +) + +type Tag struct { + Type string `json:"type"` + Href string `json:"href"` + Name string `json:"name"` +} + +type ObjectNote struct { + // Context should be set, if needed, by the endpoint handler + Context any `json:"@context,omitempty"` + + // Attributes below set from storage + + Id string `json:"id"` + Type string `json:"type"` + Summary *string `json:"summary"` + InReplyTo *string `json:"inReplyTo"` + Published time.Time `json:"published"` + Url string `json:"url"` + AttributedTo string `json:"attributedTo"` + To []string `json:"to"` + CC []string `json:"cc"` + Sensitive bool `json:"sensitive"` + AtomUri string `json:"atomUri"` + InReplyToAtomUri *string `json:"inReplyToAtomUri"` + // Conversation string `json:"conversation"` // FIXME: Uncomment once understood what this field wants + Content string `json:"content"` + // ContentMap map[string]string `json:"content_map"` // TODO: Uncomment once/if support for multiple languages available + // Attachments []string `json:"attachments"` // FIXME: Change this to document type + Tags []Tag `json:"tag"` + // Replies any `json:"replies"` // FIXME: Change this to collection type embedding first page + // Likes any `json:"likes"` // FIXME: Change this to collection + // Shares any `json:"shares"` // FIXME: Change this to collection, is boosts +} + +func NoteFromStorage(ctx context.Context, id string) (*ObjectNote, error) { + note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(id)). + Preload(dbgen.Note.Creator). + Preload(dbgen.Note.PingRelations). + Preload(dbgen.Note.Tags). + First() + if err != nil { + return nil, err + } + // TODO: Check access level, requires acting user to be included in function signature + publicUrlPrefix := config.GlobalConfig.General.GetFullPublicUrl() + data := &ObjectNote{ + Id: publicUrlPrefix + "/api/activitypub/note/" + id, + Type: "Note", + Published: note.CreatedAt, + AttributedTo: publicUrlPrefix + "/api/activitypub/user/" + note.CreatorId, + Content: note.RawContent, // FIXME: Escape content + Url: publicUrlPrefix + "/@" + note.Creator.Username + "/" + id, + AtomUri: publicUrlPrefix + "/api/activitypub/object/" + id, + Tags: []Tag{}, + } + switch note.AccessLevel { + case models.NOTE_TARGET_PUBLIC: + data.To = []string{ + "https://www.w3.org/ns/activitystreams#Public", + } + data.CC = []string{ + fmt.Sprintf("%s/api/activitypub/user/%s/followers", publicUrlPrefix, note.CreatorId), + } + case models.NOTE_TARGET_HOME: + return nil, fmt.Errorf("home access level not implemented") + case models.NOTE_TARGET_FOLLOWERS: + return nil, fmt.Errorf("followers access level not implemented") + case models.NOTE_TARGET_DM: + return nil, fmt.Errorf("dm access level not implemented") + default: + return nil, fmt.Errorf("unknown access level %v", note.AccessLevel) + } + if note.RepliesTo.Valid { + data.InReplyTo = ¬e.RepliesTo.String + data.InReplyToAtomUri = ¬e.RepliesTo.String + } + if note.ContentWarning.Valid { + data.Summary = ¬e.ContentWarning.String + data.Sensitive = true + } + for _, ping := range note.PingRelations { + target, err := dbgen.User.GetById(ping.PingTargetId) + if err != nil { + return nil, err + } + data.Tags = append(data.Tags, Tag{ + Type: "Mention", + Href: webshared.UserPublicUrl(target.ID), + Name: target.Username, + }) + } + for _, tag := range note.Tags { + data.Tags = append(data.Tags, Tag{ + Type: "Hashtag", + Href: tag.TagUrl, + Name: tag.Tag, + }) + } + return data, nil +} diff --git a/activitypub/translators/user.go b/activitypub/translators/user.go new file mode 100644 index 0000000..7d2fe8f --- /dev/null +++ b/activitypub/translators/user.go @@ -0,0 +1,126 @@ +package translators + +import ( + "context" + "time" + + "git.mstar.dev/mstar/goutils/sliceutils" + "github.com/rs/zerolog/log" + + "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/config" + "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" + webshared "git.mstar.dev/mstar/linstrom/web/shared" +) + +type UserKey struct { + Id string `json:"id"` + Owner string `json:"owner"` + Pem string `json:"publicKeyPem"` +} +type Media struct { + Type string `json:"type"` + Url string `json:"url"` + MediaType string `json:"mediaType"` +} +type User struct { + Context []any `json:"@context"` + Id string `json:"id"` + Type string `json:"type"` + PreferredUsername string `json:"preferredUsername"` + Inbox string `json:"inbox"` + Outboux string `json:"outbox"` + PublicKey UserKey `json:"publicKey"` + Published time.Time `json:"published"` + DisplayName string `json:"name"` + Description *string `json:"summary,omitempty"` + PublicUrl string `json:"url"` + Icon *Media `json:"icon,omitempty"` + Banner *Media `json:"image,omitempty"` + Discoverable bool `json:"discoverable"` + Location *string `json:"vcard:Address,omitempty"` + Birthday *string `json:"vcard:bday,omitempty"` + SpeakAsCat bool `json:"speakAsCat"` + IsCat bool `json:"isCat"` + RestrictedFollow bool `json:"manuallyApprovesFollowers"` + Following string `json:"following"` + Followers string `json:"followers"` +} + +func UserFromStorage(ctx context.Context, id string) (*User, error) { + user, err := dbgen.User.Where(dbgen.User.ID.Eq(id)). + Preload(dbgen.User.Icon).Preload(dbgen.User.Banner). + Preload(dbgen.User.BeingTypes). + First() + + err = storage.EnsureLocalUserIdHasLinks(id) + if err != nil { + log.Warn().Err(err).Msg("Failed to create links for local user") + } + apUrl := activitypub.UserIdToApUrl(user.ID) + var keyBytes string + if config.GlobalConfig.Experimental.UseEd25519Keys { + keyBytes = shared.KeyBytesToPem(user.PublicKeyEd, true) + } else { + keyBytes = shared.KeyBytesToPem(user.PublicKeyRsa, false) + } + data := User{ + Id: apUrl, + Type: "Person", + PreferredUsername: user.Username, + Inbox: apUrl + "/inbox", + Outboux: apUrl + "/outbox", + PublicKey: UserKey{ + Id: apUrl + "#main-key", + Owner: apUrl, + Pem: keyBytes, + }, + Published: user.CreatedAt, + DisplayName: user.DisplayName, + PublicUrl: config.GlobalConfig.General.GetFullPublicUrl() + "/user/" + user.Username, + Discoverable: user.Indexable, + RestrictedFollow: user.RestrictedFollow, + Following: apUrl + "/following", + Followers: apUrl + "/followers", + } + if user.Description != "" { + data.Description = &user.Description + } + if user.Icon != nil { + log.Debug().Msg("icon found") + data.Icon = &Media{ + Type: "Image", + Url: config.GlobalConfig.General.GetFullPublicUrl() + webshared.EnsurePublicUrl( + user.Icon.Location, + ), + MediaType: user.Icon.Type, + } + } + if user.Banner != nil { + log.Debug().Msg("icon banner") + data.Banner = &Media{ + Type: "Image", + Url: config.GlobalConfig.General.GetFullPublicUrl() + webshared.EnsurePublicUrl( + user.Banner.Location, + ), + MediaType: user.Banner.Type, + } + } + if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { + return t.Being == string(models.BEING_CAT) + }) { + data.IsCat = true + // data.SpeakAsCat = true // TODO: Move to check of separate field in db model + } + if user.Location.Valid { + data.Location = &user.Location.String + } + if user.Birthday.Valid { + data.Birthday = &user.Birthday.String + // data.Birthday = other.IntoPointer(user.Birthday.Time.Format("2006-Jan-02")) //YYYY-Month-DD + } + return &data, nil +} diff --git a/storage-new/models/0allTypes.go b/storage-new/models/0allTypes.go index 45caf62..19871d1 100644 --- a/storage-new/models/0allTypes.go +++ b/storage-new/models/0allTypes.go @@ -5,6 +5,7 @@ var AllTypes = []any{ &Activity{}, &Collection{}, &Emote{}, + &FailedOutboundRequest{}, &Feed{}, &MediaMetadata{}, &Note{}, diff --git a/web/debug/posts.go b/web/debug/posts.go index 3c7da9a..75f0bff 100644 --- a/web/debug/posts.go +++ b/web/debug/posts.go @@ -13,11 +13,11 @@ import ( "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/activitypub/translators" "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" ) @@ -148,7 +148,7 @@ func postAs(w http.ResponseWriter, r *http.Request) { return } log.Debug().Strs("links", links).Send() - act, err := webap.CreateFromStorage(r.Context(), activity.Id) + act, err := translators.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) diff --git a/web/debug/users.go b/web/debug/users.go index 99592a8..34dea0b 100644 --- a/web/debug/users.go +++ b/web/debug/users.go @@ -19,11 +19,11 @@ import ( "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/activitypub/translators" "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" ) @@ -377,7 +377,7 @@ func requestFollow(w http.ResponseWriter, r *http.Request) { log.Error().Err(err).Msg("Failed to get target user with remote links") return } - activity, err := webap.FollowFromStorage(context.Background(), activity.Id) + activity, err := translators.FollowFromStorage(context.Background(), activity.Id) if err != nil { log.Error().Err(err).Msg("Failed to retrieve and format follow request") return diff --git a/web/public/api/activitypub/activityAccept.go b/web/public/api/activitypub/activityAccept.go index 08694c2..1959476 100644 --- a/web/public/api/activitypub/activityAccept.go +++ b/web/public/api/activitypub/activityAccept.go @@ -1,35 +1,23 @@ package activitypub import ( - "context" "encoding/json" "fmt" "net/http" - "strings" 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/config" + "git.mstar.dev/mstar/linstrom/activitypub/translators" "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) + activity, err := translators.CreateFromStorage(r.Context(), id) switch err { case gorm.ErrRecordNotFound: _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) @@ -52,32 +40,3 @@ func activityAccept(w http.ResponseWriter, r *http.Request) { _ = 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 - } - var outId string - if strings.HasPrefix(id, "http") { - outId = id - } else { - outId = fmt.Sprintf("%s/api/activitypub/activity/accept/%s", config.GlobalConfig.General.GetFullPublicUrl(), id) - } - return &ActivityAcceptOut{ - Id: outId, - 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 be95b2d..dd206a6 100644 --- a/web/public/api/activitypub/activityCreate.go +++ b/web/public/api/activitypub/activityCreate.go @@ -1,7 +1,6 @@ package activitypub import ( - "context" "encoding/json" "fmt" "net/http" @@ -11,24 +10,14 @@ import ( "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" - "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/activitypub/translators" "git.mstar.dev/mstar/linstrom/storage-new" - "git.mstar.dev/mstar/linstrom/storage-new/dbgen" - "git.mstar.dev/mstar/linstrom/storage-new/models" ) -type ActivityCreate struct { - Context any `json:"@context,omitempty"` - Id string `json:"id"` - Type string `json:"type"` - Actor string `json:"actor"` - Object any `json:"object"` -} - func activityCreate(w http.ResponseWriter, r *http.Request) { log := hlog.FromRequest(r) id := r.PathValue("id") - activity, err := CreateFromStorage(r.Context(), id) + activity, err := translators.CreateFromStorage(r.Context(), id) switch err { case gorm.ErrRecordNotFound: _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) @@ -51,44 +40,3 @@ func activityCreate(w http.ResponseWriter, r *http.Request) { _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } } - -// Find a create activity from the db and format it for activitypub reads -// 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) (*ActivityCreate, error) { - // log := log.Ctx(ctx) - a := dbgen.Activity - activity, err := a.Where(a.Type.Eq(string(models.ActivityCreate))). - Where(a.Id.Eq(id)). - First() - if err != nil { - return nil, err - } - switch models.ActivitystreamsActivityTargetType(activity.ObjectType) { - case models.ActivitystreamsActivityTargetNote: - note, err := NoteFromStorage(ctx, activity.ObjectId) - if err != nil { - return nil, err - } - out := ActivityCreate{ - Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/create/" + id, - Type: "Create", - Actor: note.AttributedTo, - Object: note, - } - return &out, nil - case models.ActivitystreamsActivityTargetBoost: - panic("Not implemented") - case models.ActivitystreamsActivityTargetReaction: - panic("Not implemented") - case models.ActivitystreamsActivityTargetActivity: - panic("Not implemented") - case models.ActivitystreamsActivityTargetUser: - panic("Not implemented") - case models.ActivitystreamsActivityTargetUnknown: - panic("Not implemented") - default: - panic("Not implemented") - } -} diff --git a/web/public/api/activitypub/activityFollow.go b/web/public/api/activitypub/activityFollow.go index 0b6eac4..41fca5a 100644 --- a/web/public/api/activitypub/activityFollow.go +++ b/web/public/api/activitypub/activityFollow.go @@ -1,35 +1,23 @@ 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/config" + "git.mstar.dev/mstar/linstrom/activitypub/translators" "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) + activity, err := translators.FollowFromStorage(r.Context(), id) switch err { case gorm.ErrRecordNotFound: _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) @@ -52,45 +40,3 @@ func activityFollow(w http.ResponseWriter, r *http.Request) { _ = 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: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/activity/follow/" + id, - Type: "Follow", - } - if follower.RemoteInfo != nil { - out.Actor = follower.RemoteInfo.ApLink - } else { - out.Actor = activitypub.UserIdToApUrl(follower.ID) - } - if followed.RemoteInfo != nil { - out.Object = followed.RemoteInfo.ApLink - } else { - out.Object = activitypub.UserIdToApUrl(followed.ID) - } - return &out, nil -} diff --git a/web/public/api/activitypub/collection.go b/web/public/api/activitypub/collection.go index bbc463f..7520576 100644 --- a/web/public/api/activitypub/collection.go +++ b/web/public/api/activitypub/collection.go @@ -2,28 +2,6 @@ package activitypub import "net/http" -// Used for both unordered and ordered -type collectionOut struct { - Context any `json:"@context,omitempty"` - Summary string `json:"summary,omitempty"` - Type string `json:"type"` - Items []any `json:"items,omitempty"` - Id string `json:"id"` - TotalItems int `json:"totalItems"` - First string `json:"first"` -} - -// Used for both unordered and ordered -type collectionPageOut struct { - Context any `json:"@context,omitempty"` - Type string `json:"type"` - Id string `json:"id"` - PartOf string `json:"partOf"` - Next string `json:"next,omitempty"` - Previous string `json:"prev,omitempty"` - Items []any `json:"items"` -} - // Unordered collections handler func collections(w http.ResponseWriter, r *http.Request) {} diff --git a/web/public/api/activitypub/inbox.go b/web/public/api/activitypub/inbox.go index a0fde7b..13e3b62 100644 --- a/web/public/api/activitypub/inbox.go +++ b/web/public/api/activitypub/inbox.go @@ -20,6 +20,7 @@ import ( "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/activitypub/translators" "git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/shared" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" @@ -682,7 +683,7 @@ func handleReject(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{} + activity := translators.ActivityCreate{} err := mapstructure.Decode(object, &activity) if err != nil { log.Error(). @@ -735,7 +736,7 @@ func handleCreate(w http.ResponseWriter, r *http.Request, object map[string]any) } else { obj["published"] = tmpTime } - objectNote := ObjectNote{} + objectNote := translators.ObjectNote{} err = mapstructure.Decode(obj, &objectNote) if err != nil { log.Error(). @@ -850,7 +851,7 @@ func AcceptFollow( if err != nil { return err } - webAccept, err := AcceptFromStorage(ctx, acceptActivity.Id) + webAccept, err := translators.AcceptFromStorage(ctx, acceptActivity.Id) if err != nil { return err } diff --git a/web/public/api/activitypub/note.go b/web/public/api/activitypub/note.go index 29b9c7a..e0741f7 100644 --- a/web/public/api/activitypub/note.go +++ b/web/public/api/activitypub/note.go @@ -1,62 +1,23 @@ package activitypub import ( - "context" "encoding/json" "fmt" "net/http" - "time" 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/config" + "git.mstar.dev/mstar/linstrom/activitypub/translators" "git.mstar.dev/mstar/linstrom/storage-new" - "git.mstar.dev/mstar/linstrom/storage-new/dbgen" - "git.mstar.dev/mstar/linstrom/storage-new/models" - webshared "git.mstar.dev/mstar/linstrom/web/shared" ) -type Tag struct { - Type string `json:"type"` - Href string `json:"href"` - Name string `json:"name"` -} - -type ObjectNote struct { - // Context should be set, if needed, by the endpoint handler - Context any `json:"@context,omitempty"` - - // Attributes below set from storage - - Id string `json:"id"` - Type string `json:"type"` - Summary *string `json:"summary"` - InReplyTo *string `json:"inReplyTo"` - Published time.Time `json:"published"` - Url string `json:"url"` - AttributedTo string `json:"attributedTo"` - To []string `json:"to"` - CC []string `json:"cc"` - Sensitive bool `json:"sensitive"` - AtomUri string `json:"atomUri"` - InReplyToAtomUri *string `json:"inReplyToAtomUri"` - // Conversation string `json:"conversation"` // FIXME: Uncomment once understood what this field wants - Content string `json:"content"` - // ContentMap map[string]string `json:"content_map"` // TODO: Uncomment once/if support for multiple languages available - // Attachments []string `json:"attachments"` // FIXME: Change this to document type - Tags []Tag `json:"tag"` - // Replies any `json:"replies"` // FIXME: Change this to collection type embedding first page - // Likes any `json:"likes"` // FIXME: Change this to collection - // Shares any `json:"shares"` // FIXME: Change this to collection, is boosts -} - func objectNote(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") log := hlog.FromRequest(r) - note, err := NoteFromStorage(r.Context(), id) + note, err := translators.NoteFromStorage(r.Context(), id) switch err { case gorm.ErrRecordNotFound: _ = webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) @@ -80,70 +41,3 @@ func objectNote(w http.ResponseWriter, r *http.Request) { _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } } - -func NoteFromStorage(ctx context.Context, id string) (*ObjectNote, error) { - note, err := dbgen.Note.Where(dbgen.Note.ID.Eq(id)). - Preload(dbgen.Note.Creator). - Preload(dbgen.Note.PingRelations). - Preload(dbgen.Note.Tags). - First() - if err != nil { - return nil, err - } - // TODO: Check access level, requires acting user to be included in function signature - publicUrlPrefix := config.GlobalConfig.General.GetFullPublicUrl() - data := &ObjectNote{ - Id: publicUrlPrefix + "/api/activitypub/note/" + id, - Type: "Note", - Published: note.CreatedAt, - AttributedTo: publicUrlPrefix + "/api/activitypub/user/" + note.CreatorId, - Content: note.RawContent, // FIXME: Escape content - Url: publicUrlPrefix + "/@" + note.Creator.Username + "/" + id, - AtomUri: publicUrlPrefix + "/api/activitypub/object/" + id, - Tags: []Tag{}, - } - switch note.AccessLevel { - case models.NOTE_TARGET_PUBLIC: - data.To = []string{ - "https://www.w3.org/ns/activitystreams#Public", - } - data.CC = []string{ - fmt.Sprintf("%s/api/activitypub/user/%s/followers", publicUrlPrefix, note.CreatorId), - } - case models.NOTE_TARGET_HOME: - return nil, fmt.Errorf("home access level not implemented") - case models.NOTE_TARGET_FOLLOWERS: - return nil, fmt.Errorf("followers access level not implemented") - case models.NOTE_TARGET_DM: - return nil, fmt.Errorf("dm access level not implemented") - default: - return nil, fmt.Errorf("unknown access level %v", note.AccessLevel) - } - if note.RepliesTo.Valid { - data.InReplyTo = ¬e.RepliesTo.String - data.InReplyToAtomUri = ¬e.RepliesTo.String - } - if note.ContentWarning.Valid { - data.Summary = ¬e.ContentWarning.String - data.Sensitive = true - } - for _, ping := range note.PingRelations { - target, err := dbgen.User.GetById(ping.PingTargetId) - if err != nil { - return nil, err - } - data.Tags = append(data.Tags, Tag{ - Type: "Mention", - Href: webshared.UserPublicUrl(target.ID), - Name: target.Username, - }) - } - for _, tag := range note.Tags { - data.Tags = append(data.Tags, Tag{ - Type: "Hashtag", - Href: tag.TagUrl, - Name: tag.Tag, - }) - } - return data, nil -} diff --git a/web/public/api/activitypub/user.go b/web/public/api/activitypub/user.go index 7d509ce..bfd6bbd 100644 --- a/web/public/api/activitypub/user.go +++ b/web/public/api/activitypub/user.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "strconv" - "time" webutils "git.mstar.dev/mstar/goutils/http" "git.mstar.dev/mstar/goutils/other" @@ -14,143 +13,21 @@ import ( "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/activitypub" - "git.mstar.dev/mstar/linstrom/config" - "git.mstar.dev/mstar/linstrom/shared" - "git.mstar.dev/mstar/linstrom/storage-new" + "git.mstar.dev/mstar/linstrom/activitypub/translators" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" - "git.mstar.dev/mstar/linstrom/storage-new/models" - webshared "git.mstar.dev/mstar/linstrom/web/shared" ) func users(w http.ResponseWriter, r *http.Request) { - type OutboundKey struct { - Id string `json:"id"` - Owner string `json:"owner"` - Pem string `json:"publicKeyPem"` - } - type OutboundMedia struct { - Type string `json:"type"` - Url string `json:"url"` - MediaType string `json:"mediaType"` - } - type Outbound struct { - Context []any `json:"@context"` - Id string `json:"id"` - Type string `json:"type"` - PreferredUsername string `json:"preferredUsername"` - Inbox string `json:"inbox"` - Outboux string `json:"outbox"` - PublicKey OutboundKey `json:"publicKey"` - Published time.Time `json:"published"` - DisplayName string `json:"name"` - Description *string `json:"summary,omitempty"` - PublicUrl string `json:"url"` - Icon *OutboundMedia `json:"icon,omitempty"` - Banner *OutboundMedia `json:"image,omitempty"` - Discoverable bool `json:"discoverable"` - Location *string `json:"vcard:Address,omitempty"` - Birthday *string `json:"vcard:bday,omitempty"` - SpeakAsCat bool `json:"speakAsCat"` - IsCat bool `json:"isCat"` - RestrictedFollow bool `json:"manuallyApprovesFollowers"` - Following string `json:"following"` - Followers string `json:"followers"` - } log := hlog.FromRequest(r) userId := r.PathValue("id") - user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)). - Preload(dbgen.User.Icon).Preload(dbgen.User.Banner). - Preload(dbgen.User.BeingTypes). - First() + + user, err := translators.UserFromStorage(r.Context(), userId) if err != nil { - _ = webutils.ProblemDetails( - w, - 500, - "/errors/db-failure", - "internal database failure", - nil, - nil, - ) - if storage.HandleReconnectError(err) { - log.Warn().Msg("Connection to db lost. Reconnect attempt started") - } else { - log.Error().Err(err).Msg("Failed to get total user count from db") - } + log.Error().Err(err).Msg("Failed to get user from db") + _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } - // FIXME: Remove this later - // (or rather move to dedicated module in storage for old migration stuff), - // temporary fix for old data. User creation locations are fixed already - err = storage.EnsureLocalUserIdHasLinks(userId) - if err != nil { - log.Warn().Err(err).Msg("Failed to create links for local user") - } - - apUrl := activitypub.UserIdToApUrl(user.ID) - var keyBytes string - if config.GlobalConfig.Experimental.UseEd25519Keys { - keyBytes = shared.KeyBytesToPem(user.PublicKeyEd, true) - } else { - keyBytes = shared.KeyBytesToPem(user.PublicKeyRsa, false) - } - data := Outbound{ - Context: activitypub.BaseLdContext, - Id: apUrl, - Type: "Person", - PreferredUsername: user.Username, - Inbox: apUrl + "/inbox", - Outboux: apUrl + "/outbox", - PublicKey: OutboundKey{ - Id: apUrl + "#main-key", - Owner: apUrl, - Pem: keyBytes, - }, - Published: user.CreatedAt, - DisplayName: user.DisplayName, - PublicUrl: config.GlobalConfig.General.GetFullPublicUrl() + "/user/" + user.Username, - Discoverable: user.Indexable, - RestrictedFollow: user.RestrictedFollow, - Following: apUrl + "/following", - Followers: apUrl + "/followers", - } - if user.Description != "" { - data.Description = &user.Description - } - if user.Icon != nil { - log.Debug().Msg("icon found") - data.Icon = &OutboundMedia{ - Type: "Image", - Url: config.GlobalConfig.General.GetFullPublicUrl() + webshared.EnsurePublicUrl( - user.Icon.Location, - ), - MediaType: user.Icon.Type, - } - } - if user.Banner != nil { - log.Debug().Msg("icon banner") - data.Banner = &OutboundMedia{ - Type: "Image", - Url: config.GlobalConfig.General.GetFullPublicUrl() + webshared.EnsurePublicUrl( - user.Banner.Location, - ), - MediaType: user.Banner.Type, - } - } - if sliceutils.ContainsFunc(user.BeingTypes, func(t models.UserToBeing) bool { - return t.Being == string(models.BEING_CAT) - }) { - data.IsCat = true - // data.SpeakAsCat = true // TODO: Move to check of separate field in db model - } - if user.Location.Valid { - data.Location = &user.Location.String - } - if user.Birthday.Valid { - data.Birthday = &user.Birthday.String - // data.Birthday = other.IntoPointer(user.Birthday.Time.Format("2006-Jan-02")) //YYYY-Month-DD - } - - encoded, err := json.Marshal(data) + encoded, err := json.Marshal(user) if err != nil { log.Error().Err(err).Msg("Failed to marshal response") _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) @@ -183,7 +60,7 @@ func userFollowing(w http.ResponseWriter, r *http.Request) { return } if pageNrStr == "" { - col := collectionOut{ + col := translators.CollectionOut{ Context: "https://www.w3.org/ns/activitystreams", Type: "OrderedCollection", Id: apUrl + "/following", @@ -222,7 +99,7 @@ func userFollowing(w http.ResponseWriter, r *http.Request) { _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } - page := collectionPageOut{ + page := translators.CollectionPageOut{ Context: "https://www.w3.org/ns/activitystreams", Type: "OrderedCollectionPage", Id: fmt.Sprintf("%s/following?page=%d", apUrl, pageNr), @@ -270,7 +147,7 @@ func userFollowers(w http.ResponseWriter, r *http.Request) { return } if pageNrStr == "" { - col := collectionOut{ + col := translators.CollectionOut{ Context: activitypub.BaseLdContext, Type: "OrderedCollection", Id: apUrl + "/followers", @@ -310,7 +187,7 @@ func userFollowers(w http.ResponseWriter, r *http.Request) { _ = webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } - page := collectionPageOut{ + page := translators.CollectionPageOut{ Context: activitypub.BaseLdContext, Type: "OrderedCollectionPage", Id: fmt.Sprintf("%s/followers?page=%d", apUrl, pageNr),