From 8947d97825a92d4f343c80b46cf42c6c5114ab40 Mon Sep 17 00:00:00 2001 From: mStar Date: Mon, 16 Jun 2025 08:13:11 +0200 Subject: [PATCH] Prep for reactions, some fixes and improvements --- activitypub/translators/emote.go | 30 +++++++ activitypub/translators/like.go | 43 ++++++++++ activitypub/translators/media.go | 26 ++++++ activitypub/translators/note.go | 5 +- activitypub/translators/user.go | 6 +- config/config.go | 8 -- media/media.go | 10 ++- storage-new/dbgen/emotes.gen.go | 6 +- storage-new/dbgen/note_to_feeds.gen.go | 111 ++++++++++++++++++++++++- storage-new/dbgen/reactions.gen.go | 12 ++- storage-new/models/Emote.go | 16 ++-- storage-new/models/Reaction.go | 8 +- web/public/api/activitypub/server.go | 24 +++--- 13 files changed, 262 insertions(+), 43 deletions(-) create mode 100644 activitypub/translators/emote.go create mode 100644 activitypub/translators/media.go diff --git a/activitypub/translators/emote.go b/activitypub/translators/emote.go new file mode 100644 index 0000000..473bed3 --- /dev/null +++ b/activitypub/translators/emote.go @@ -0,0 +1,30 @@ +package translators + +import ( + "context" + + "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" +) + +func EmoteTagFromStorage(ctx context.Context, id string) (*Tag, error) { + emote, err := dbgen.Emote.Where(dbgen.Emote.ID.Eq(id)).First() + if err != nil { + return nil, err + } + emoteId := config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/emote/" + id + emoteMedia, err := MediaFromStorage(ctx, emote.MetadataId) + if err != nil { + return nil, err + } + out := Tag{ + Type: "Emoji", + Name: emote.Name, + Href: nil, + Id: &emoteId, + Updated: &emote.UpdatedAt, + Icon: emoteMedia, + } + + return &out, nil +} diff --git a/activitypub/translators/like.go b/activitypub/translators/like.go index 227f313..4aa7483 100644 --- a/activitypub/translators/like.go +++ b/activitypub/translators/like.go @@ -1,5 +1,14 @@ package translators +import ( + "context" + "strings" + + "git.mstar.dev/mstar/linstrom/activitypub" + "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" +) + type ActivityLikeOut struct { Context any `json:"@context"` Type string `json:"type"` @@ -10,3 +19,37 @@ type ActivityLikeOut struct { MkReaction *string `json:"_misskey_reaction,omitempty"` Tags []Tag `json:"tag,omitempty"` } + +func LikeFromStorage(ctx context.Context, id string) (*ActivityLikeOut, error) { + reaction, err := dbgen.Reaction. + Where(dbgen.Reaction.ID.Eq(id)). + Preload(dbgen.Reaction.Emote). + First() + if err != nil { + return nil, err + } + like := &ActivityLikeOut{ + Type: "Like", + Id: config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/activity/like/" + id, + Actor: activitypub.UserIdToApUrl(reaction.ReactorId), + } + if strings.HasPrefix(reaction.NoteId, "http") { + like.Object = reaction.NoteId + } else { + like.Object = config.GlobalConfig.General.GetFullPublicUrl() + "/api/activitypub/note/" + reaction.NoteId + } + if reaction.Content.Valid { + like.Content = &reaction.Content.String + like.MkReaction = &reaction.Content.String + } + if reaction.Emote != nil { + like.Content = &reaction.Emote.Name + like.MkReaction = &reaction.Emote.Name + emote, err := EmoteTagFromStorage(ctx, reaction.Emote.ID) + if err != nil { + return nil, err + } + like.Tags = append(like.Tags, *emote) + } + return like, nil +} diff --git a/activitypub/translators/media.go b/activitypub/translators/media.go new file mode 100644 index 0000000..2ca177d --- /dev/null +++ b/activitypub/translators/media.go @@ -0,0 +1,26 @@ +package translators + +import ( + "context" + + "git.mstar.dev/mstar/linstrom/storage-new/dbgen" +) + +type Media struct { + Type string `json:"type"` + Url string `json:"url"` + MediaType string `json:"mediaType"` +} + +func MediaFromStorage(ctx context.Context, id string) (*Media, error) { + metadata, err := dbgen.MediaMetadata.Where(dbgen.MediaMetadata.ID.Eq(id)).First() + if err != nil { + return nil, err + } + data := Media{ + Type: "Image", // FIXME: Change this to a sort of dynamic detection based on mimetype + MediaType: metadata.Type, + Url: metadata.Location, + } + return &data, nil +} diff --git a/activitypub/translators/note.go b/activitypub/translators/note.go index 0dfb297..bbdfedb 100644 --- a/activitypub/translators/note.go +++ b/activitypub/translators/note.go @@ -103,16 +103,17 @@ func NoteFromStorage(ctx context.Context, id string) (*ObjectNote, error) { if err != nil { return nil, err } + pubUrl := webshared.UserPublicUrl(target.ID) data.Tags = append(data.Tags, Tag{ Type: "Mention", - Href: webshared.UserPublicUrl(target.ID), + Href: &pubUrl, Name: target.Username, }) } for _, tag := range note.Tags { data.Tags = append(data.Tags, Tag{ Type: "Hashtag", - Href: tag.TagUrl, + Href: &tag.TagUrl, Name: tag.Tag, }) } diff --git a/activitypub/translators/user.go b/activitypub/translators/user.go index 7d2fe8f..4083c7e 100644 --- a/activitypub/translators/user.go +++ b/activitypub/translators/user.go @@ -21,11 +21,7 @@ type UserKey struct { 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"` diff --git a/config/config.go b/config/config.go index f6cf846..6f0aa08 100644 --- a/config/config.go +++ b/config/config.go @@ -65,14 +65,6 @@ type ConfigStorage struct { Port int `toml:"port"` SslMode *string `toml:"ssl_mode"` TimeZone *string `toml:"time_zone"` - // Url to redis server. If empty, no redis is used - RedisUrl *string `toml:"redis_url,omitempty"` - // The maximum size of the in-memory cache in bytes - MaxInMemoryCacheSize int64 `toml:"max_in_memory_cache_size"` - // The time to live for in app in memory cache items, in seconds - MaxInMemoryCacheTTL int `toml:"max_in_memory_cache_ttl"` - // The time to live for items in redis, in seconds - MaxRedisCacheTTL *int `toml:"max_redis_cache_ttl"` // Key used for encrypting sensitive information in the db // DO NOT CHANGE THIS AFTER SETUP EncryptionKey string `toml:"encryption_key"` diff --git a/media/media.go b/media/media.go index 5b00ecb..3ee981b 100644 --- a/media/media.go +++ b/media/media.go @@ -14,6 +14,14 @@ type Server struct { client *minio.Client } +/* +TODO: Figure out an api for a microservice for transcoding media, see https://tech.lgbt/@lina/114682780787448797 +- Read endpoint from config +- Try to reach transcoder +- If transcoder is alive, use it for transcoding +- If not alive, store files as is +*/ + var ( ErrNoBucketAccess = errors.New("can't access configured bucket") ) @@ -50,7 +58,7 @@ func NewServer() (*Server, error) { return &Server{client: client}, nil } -// Convert a userId and filename into a proper filepath for s3. +// UsernameFilename converts a userId and filename into a proper filepath for s3. // Reason for this is that the userId for external users is a valid url which needs to be encoded func UsernameFilename(userId, filename string) string { return userId + "//" + filename diff --git a/storage-new/dbgen/emotes.gen.go b/storage-new/dbgen/emotes.gen.go index 052bf85..97b1440 100644 --- a/storage-new/dbgen/emotes.gen.go +++ b/storage-new/dbgen/emotes.gen.go @@ -27,7 +27,7 @@ func newEmote(db *gorm.DB, opts ...gen.DOOption) emote { tableName := _emote.emoteDo.TableName() _emote.ALL = field.NewAsterisk(tableName) - _emote.ID = field.NewUint(tableName, "id") + _emote.ID = field.NewString(tableName, "id") _emote.CreatedAt = field.NewTime(tableName, "created_at") _emote.UpdatedAt = field.NewTime(tableName, "updated_at") _emote.DeletedAt = field.NewField(tableName, "deleted_at") @@ -73,7 +73,7 @@ type emote struct { emoteDo ALL field.Asterisk - ID field.Uint + ID field.String CreatedAt field.Time UpdatedAt field.Time DeletedAt field.Field @@ -99,7 +99,7 @@ func (e emote) As(alias string) *emote { func (e *emote) updateTableName(table string) *emote { e.ALL = field.NewAsterisk(table) - e.ID = field.NewUint(table, "id") + e.ID = field.NewString(table, "id") e.CreatedAt = field.NewTime(table, "created_at") e.UpdatedAt = field.NewTime(table, "updated_at") e.DeletedAt = field.NewField(table, "deleted_at") diff --git a/storage-new/dbgen/note_to_feeds.gen.go b/storage-new/dbgen/note_to_feeds.gen.go index 5e5ade3..3fc9c46 100644 --- a/storage-new/dbgen/note_to_feeds.gen.go +++ b/storage-new/dbgen/note_to_feeds.gen.go @@ -30,6 +30,8 @@ func newNoteToFeed(db *gorm.DB, opts ...gen.DOOption) noteToFeed { _noteToFeed.ID = field.NewUint64(tableName, "id") _noteToFeed.CreatedAt = field.NewTime(tableName, "created_at") _noteToFeed.NoteId = field.NewString(tableName, "note_id") + _noteToFeed.FeedId = field.NewUint64(tableName, "feed_id") + _noteToFeed.Reason = field.NewString(tableName, "reason") _noteToFeed.Note = noteToFeedBelongsToNote{ db: db.Session(&gorm.Session{}), @@ -399,6 +401,17 @@ func newNoteToFeed(db *gorm.DB, opts ...gen.DOOption) noteToFeed { }, } + _noteToFeed.Feed = noteToFeedBelongsToFeed{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Feed", "models.Feed"), + Owner: struct { + field.RelationField + }{ + RelationField: field.NewRelation("Feed.Owner", "models.User"), + }, + } + _noteToFeed.fillFieldMap() return _noteToFeed @@ -411,8 +424,12 @@ type noteToFeed struct { ID field.Uint64 CreatedAt field.Time NoteId field.String + FeedId field.Uint64 + Reason field.String Note noteToFeedBelongsToNote + Feed noteToFeedBelongsToFeed + fieldMap map[string]field.Expr } @@ -431,6 +448,8 @@ func (n *noteToFeed) updateTableName(table string) *noteToFeed { n.ID = field.NewUint64(table, "id") n.CreatedAt = field.NewTime(table, "created_at") n.NoteId = field.NewString(table, "note_id") + n.FeedId = field.NewUint64(table, "feed_id") + n.Reason = field.NewString(table, "reason") n.fillFieldMap() @@ -447,10 +466,12 @@ func (n *noteToFeed) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (n *noteToFeed) fillFieldMap() { - n.fieldMap = make(map[string]field.Expr, 4) + n.fieldMap = make(map[string]field.Expr, 7) n.fieldMap["id"] = n.ID n.fieldMap["created_at"] = n.CreatedAt n.fieldMap["note_id"] = n.NoteId + n.fieldMap["feed_id"] = n.FeedId + n.fieldMap["reason"] = n.Reason } @@ -458,12 +479,15 @@ func (n noteToFeed) clone(db *gorm.DB) noteToFeed { n.noteToFeedDo.ReplaceConnPool(db.Statement.ConnPool) n.Note.db = db.Session(&gorm.Session{Initialized: true}) n.Note.db.Statement.ConnPool = db.Statement.ConnPool + n.Feed.db = db.Session(&gorm.Session{Initialized: true}) + n.Feed.db.Statement.ConnPool = db.Statement.ConnPool return n } func (n noteToFeed) replaceDB(db *gorm.DB) noteToFeed { n.noteToFeedDo.ReplaceDB(db) n.Note.db = db.Session(&gorm.Session{}) + n.Feed.db = db.Session(&gorm.Session{}) return n } @@ -675,6 +699,91 @@ func (a noteToFeedBelongsToNoteTx) Unscoped() *noteToFeedBelongsToNoteTx { return &a } +type noteToFeedBelongsToFeed struct { + db *gorm.DB + + field.RelationField + + Owner struct { + field.RelationField + } +} + +func (a noteToFeedBelongsToFeed) Where(conds ...field.Expr) *noteToFeedBelongsToFeed { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a noteToFeedBelongsToFeed) WithContext(ctx context.Context) *noteToFeedBelongsToFeed { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a noteToFeedBelongsToFeed) Session(session *gorm.Session) *noteToFeedBelongsToFeed { + a.db = a.db.Session(session) + return &a +} + +func (a noteToFeedBelongsToFeed) Model(m *models.NoteToFeed) *noteToFeedBelongsToFeedTx { + return ¬eToFeedBelongsToFeedTx{a.db.Model(m).Association(a.Name())} +} + +func (a noteToFeedBelongsToFeed) Unscoped() *noteToFeedBelongsToFeed { + a.db = a.db.Unscoped() + return &a +} + +type noteToFeedBelongsToFeedTx struct{ tx *gorm.Association } + +func (a noteToFeedBelongsToFeedTx) Find() (result *models.Feed, err error) { + return result, a.tx.Find(&result) +} + +func (a noteToFeedBelongsToFeedTx) Append(values ...*models.Feed) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a noteToFeedBelongsToFeedTx) Replace(values ...*models.Feed) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a noteToFeedBelongsToFeedTx) Delete(values ...*models.Feed) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a noteToFeedBelongsToFeedTx) Clear() error { + return a.tx.Clear() +} + +func (a noteToFeedBelongsToFeedTx) Count() int64 { + return a.tx.Count() +} + +func (a noteToFeedBelongsToFeedTx) Unscoped() *noteToFeedBelongsToFeedTx { + a.tx = a.tx.Unscoped() + return &a +} + type noteToFeedDo struct{ gen.DO } type INoteToFeedDo interface { diff --git a/storage-new/dbgen/reactions.gen.go b/storage-new/dbgen/reactions.gen.go index d984385..19a3096 100644 --- a/storage-new/dbgen/reactions.gen.go +++ b/storage-new/dbgen/reactions.gen.go @@ -27,13 +27,14 @@ func newReaction(db *gorm.DB, opts ...gen.DOOption) reaction { tableName := _reaction.reactionDo.TableName() _reaction.ALL = field.NewAsterisk(tableName) - _reaction.ID = field.NewUint(tableName, "id") + _reaction.ID = field.NewString(tableName, "id") _reaction.CreatedAt = field.NewTime(tableName, "created_at") _reaction.UpdatedAt = field.NewTime(tableName, "updated_at") _reaction.DeletedAt = field.NewField(tableName, "deleted_at") _reaction.NoteId = field.NewString(tableName, "note_id") _reaction.ReactorId = field.NewString(tableName, "reactor_id") _reaction.EmoteId = field.NewField(tableName, "emote_id") + _reaction.Content = field.NewField(tableName, "content") _reaction.Note = reactionBelongsToNote{ db: db.Session(&gorm.Session{}), @@ -424,13 +425,14 @@ type reaction struct { reactionDo ALL field.Asterisk - ID field.Uint + ID field.String CreatedAt field.Time UpdatedAt field.Time DeletedAt field.Field NoteId field.String ReactorId field.String EmoteId field.Field + Content field.Field Note reactionBelongsToNote Reactor reactionBelongsToReactor @@ -452,13 +454,14 @@ func (r reaction) As(alias string) *reaction { func (r *reaction) updateTableName(table string) *reaction { r.ALL = field.NewAsterisk(table) - r.ID = field.NewUint(table, "id") + r.ID = field.NewString(table, "id") r.CreatedAt = field.NewTime(table, "created_at") r.UpdatedAt = field.NewTime(table, "updated_at") r.DeletedAt = field.NewField(table, "deleted_at") r.NoteId = field.NewString(table, "note_id") r.ReactorId = field.NewString(table, "reactor_id") r.EmoteId = field.NewField(table, "emote_id") + r.Content = field.NewField(table, "content") r.fillFieldMap() @@ -475,7 +478,7 @@ func (r *reaction) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (r *reaction) fillFieldMap() { - r.fieldMap = make(map[string]field.Expr, 10) + r.fieldMap = make(map[string]field.Expr, 11) r.fieldMap["id"] = r.ID r.fieldMap["created_at"] = r.CreatedAt r.fieldMap["updated_at"] = r.UpdatedAt @@ -483,6 +486,7 @@ func (r *reaction) fillFieldMap() { r.fieldMap["note_id"] = r.NoteId r.fieldMap["reactor_id"] = r.ReactorId r.fieldMap["emote_id"] = r.EmoteId + r.fieldMap["content"] = r.Content } diff --git a/storage-new/models/Emote.go b/storage-new/models/Emote.go index b0142ef..58723f5 100644 --- a/storage-new/models/Emote.go +++ b/storage-new/models/Emote.go @@ -1,15 +1,21 @@ package models -import "gorm.io/gorm" +import ( + "gorm.io/gorm" + "time" +) // Emotes are combinations of a name, the server it originated from -// and the media for it +// and the media for it. Only represents custom emotes, not characters found in unicode // // TODO: Include the case of unicode icons being used as emote type Emote struct { - gorm.Model // Standard gorm model for id and timestamps - Metadata MediaMetadata // The media used by this emote - MetadataId string // Id of the media information, primarily for gorm + ID string `gorm:"primarykey"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` + Metadata MediaMetadata // The media used by this emote + MetadataId string // Id of the media information, primarily for gorm // Name of the emote. Also the text for using it in a message (ex. :bob:) Name string diff --git a/storage-new/models/Reaction.go b/storage-new/models/Reaction.go index e29605b..e2c376f 100644 --- a/storage-new/models/Reaction.go +++ b/storage-new/models/Reaction.go @@ -2,6 +2,7 @@ package models import ( "database/sql" + "time" "gorm.io/gorm" ) @@ -10,12 +11,15 @@ import ( // A reaction may contain some content text. If yes, this is the reaction. // It also may contain a specifically linked emote (via tag). If yes, this is the reaction and takes precedence over the content type Reaction struct { - gorm.Model + ID string `gorm:"primarykey"` + CreatedAt time.Time + UpdatedAt time.Time + DeletedAt gorm.DeletedAt `gorm:"index"` Note Note NoteId string Reactor User ReactorId string Emote *Emote // Emote is optional. If not set, use the default emote of the server - EmoteId sql.NullInt64 + EmoteId sql.NullString Content sql.NullString // Content/text of the reaction. Used for example in cases where a unicode character is sent as emote reaction } diff --git a/web/public/api/activitypub/server.go b/web/public/api/activitypub/server.go index ed00c98..bc9fbd4 100644 --- a/web/public/api/activitypub/server.go +++ b/web/public/api/activitypub/server.go @@ -6,17 +6,17 @@ import ( func BuildActivitypubRouter() http.Handler { router := http.NewServeMux() - router.HandleFunc("/user/{id}", users) - router.HandleFunc("/user/{id}/inbox", userInbox) - router.HandleFunc("/user/{id}/followers", userFollowers) - router.HandleFunc("/user/{id}/following", userFollowing) - router.HandleFunc("/activity/accept/{id}", activityAccept) - router.HandleFunc("/activity/create/{id}", activityCreate) - router.HandleFunc("/activity/delete/{id}", activityDelete) - router.HandleFunc("/activity/reject/{id}", activityReject) - router.HandleFunc("/activity/update/{id}", activityUpdate) - router.HandleFunc("/activity/follow/{id}", activityFollow) - router.HandleFunc("/note/{id}", objectNote) - router.HandleFunc("/note/{id}/reactions", noteReactions) + router.HandleFunc("GET /user/{id}", users) + router.HandleFunc("POST /user/{id}/inbox", userInbox) + router.HandleFunc("GET /user/{id}/followers", userFollowers) + router.HandleFunc("GET /user/{id}/following", userFollowing) + router.HandleFunc("GET /activity/accept/{id}", activityAccept) + router.HandleFunc("GET /activity/create/{id}", activityCreate) + router.HandleFunc("GET /activity/delete/{id}", activityDelete) + router.HandleFunc("GET /activity/reject/{id}", activityReject) + router.HandleFunc("GET /activity/update/{id}", activityUpdate) + router.HandleFunc("GET /activity/follow/{id}", activityFollow) + router.HandleFunc("GET /note/{id}", objectNote) + router.HandleFunc("GET /note/{id}/reactions", noteReactions) return router }