Prep for reactions, some fixes and improvements
Some checks failed
/ docker (push) Failing after 1m35s

This commit is contained in:
Melody Becker 2025-06-16 08:13:11 +02:00
parent 4b62c32247
commit 8947d97825
Signed by: mstar
SSH key fingerprint: SHA256:vkXfS9FG2pVNVfvDrzd1VW9n8VJzqqdKQGljxxX8uK8
13 changed files with 262 additions and 43 deletions

View file

@ -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
}

View file

@ -1,5 +1,14 @@
package translators 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 { type ActivityLikeOut struct {
Context any `json:"@context"` Context any `json:"@context"`
Type string `json:"type"` Type string `json:"type"`
@ -10,3 +19,37 @@ type ActivityLikeOut struct {
MkReaction *string `json:"_misskey_reaction,omitempty"` MkReaction *string `json:"_misskey_reaction,omitempty"`
Tags []Tag `json:"tag,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
}

View file

@ -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
}

View file

@ -103,16 +103,17 @@ func NoteFromStorage(ctx context.Context, id string) (*ObjectNote, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
pubUrl := webshared.UserPublicUrl(target.ID)
data.Tags = append(data.Tags, Tag{ data.Tags = append(data.Tags, Tag{
Type: "Mention", Type: "Mention",
Href: webshared.UserPublicUrl(target.ID), Href: &pubUrl,
Name: target.Username, Name: target.Username,
}) })
} }
for _, tag := range note.Tags { for _, tag := range note.Tags {
data.Tags = append(data.Tags, Tag{ data.Tags = append(data.Tags, Tag{
Type: "Hashtag", Type: "Hashtag",
Href: tag.TagUrl, Href: &tag.TagUrl,
Name: tag.Tag, Name: tag.Tag,
}) })
} }

View file

@ -21,11 +21,7 @@ type UserKey struct {
Owner string `json:"owner"` Owner string `json:"owner"`
Pem string `json:"publicKeyPem"` Pem string `json:"publicKeyPem"`
} }
type Media struct {
Type string `json:"type"`
Url string `json:"url"`
MediaType string `json:"mediaType"`
}
type User struct { type User struct {
Context []any `json:"@context"` Context []any `json:"@context"`
Id string `json:"id"` Id string `json:"id"`

View file

@ -65,14 +65,6 @@ type ConfigStorage struct {
Port int `toml:"port"` Port int `toml:"port"`
SslMode *string `toml:"ssl_mode"` SslMode *string `toml:"ssl_mode"`
TimeZone *string `toml:"time_zone"` 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 // Key used for encrypting sensitive information in the db
// DO NOT CHANGE THIS AFTER SETUP // DO NOT CHANGE THIS AFTER SETUP
EncryptionKey string `toml:"encryption_key"` EncryptionKey string `toml:"encryption_key"`

View file

@ -14,6 +14,14 @@ type Server struct {
client *minio.Client 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 ( var (
ErrNoBucketAccess = errors.New("can't access configured bucket") ErrNoBucketAccess = errors.New("can't access configured bucket")
) )
@ -50,7 +58,7 @@ func NewServer() (*Server, error) {
return &Server{client: client}, nil 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 // 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 { func UsernameFilename(userId, filename string) string {
return userId + "//" + filename return userId + "//" + filename

View file

@ -27,7 +27,7 @@ func newEmote(db *gorm.DB, opts ...gen.DOOption) emote {
tableName := _emote.emoteDo.TableName() tableName := _emote.emoteDo.TableName()
_emote.ALL = field.NewAsterisk(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.CreatedAt = field.NewTime(tableName, "created_at")
_emote.UpdatedAt = field.NewTime(tableName, "updated_at") _emote.UpdatedAt = field.NewTime(tableName, "updated_at")
_emote.DeletedAt = field.NewField(tableName, "deleted_at") _emote.DeletedAt = field.NewField(tableName, "deleted_at")
@ -73,7 +73,7 @@ type emote struct {
emoteDo emoteDo
ALL field.Asterisk ALL field.Asterisk
ID field.Uint ID field.String
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
DeletedAt field.Field DeletedAt field.Field
@ -99,7 +99,7 @@ func (e emote) As(alias string) *emote {
func (e *emote) updateTableName(table string) *emote { func (e *emote) updateTableName(table string) *emote {
e.ALL = field.NewAsterisk(table) 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.CreatedAt = field.NewTime(table, "created_at")
e.UpdatedAt = field.NewTime(table, "updated_at") e.UpdatedAt = field.NewTime(table, "updated_at")
e.DeletedAt = field.NewField(table, "deleted_at") e.DeletedAt = field.NewField(table, "deleted_at")

View file

@ -30,6 +30,8 @@ func newNoteToFeed(db *gorm.DB, opts ...gen.DOOption) noteToFeed {
_noteToFeed.ID = field.NewUint64(tableName, "id") _noteToFeed.ID = field.NewUint64(tableName, "id")
_noteToFeed.CreatedAt = field.NewTime(tableName, "created_at") _noteToFeed.CreatedAt = field.NewTime(tableName, "created_at")
_noteToFeed.NoteId = field.NewString(tableName, "note_id") _noteToFeed.NoteId = field.NewString(tableName, "note_id")
_noteToFeed.FeedId = field.NewUint64(tableName, "feed_id")
_noteToFeed.Reason = field.NewString(tableName, "reason")
_noteToFeed.Note = noteToFeedBelongsToNote{ _noteToFeed.Note = noteToFeedBelongsToNote{
db: db.Session(&gorm.Session{}), 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() _noteToFeed.fillFieldMap()
return _noteToFeed return _noteToFeed
@ -411,8 +424,12 @@ type noteToFeed struct {
ID field.Uint64 ID field.Uint64
CreatedAt field.Time CreatedAt field.Time
NoteId field.String NoteId field.String
FeedId field.Uint64
Reason field.String
Note noteToFeedBelongsToNote Note noteToFeedBelongsToNote
Feed noteToFeedBelongsToFeed
fieldMap map[string]field.Expr fieldMap map[string]field.Expr
} }
@ -431,6 +448,8 @@ func (n *noteToFeed) updateTableName(table string) *noteToFeed {
n.ID = field.NewUint64(table, "id") n.ID = field.NewUint64(table, "id")
n.CreatedAt = field.NewTime(table, "created_at") n.CreatedAt = field.NewTime(table, "created_at")
n.NoteId = field.NewString(table, "note_id") n.NoteId = field.NewString(table, "note_id")
n.FeedId = field.NewUint64(table, "feed_id")
n.Reason = field.NewString(table, "reason")
n.fillFieldMap() n.fillFieldMap()
@ -447,10 +466,12 @@ func (n *noteToFeed) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (n *noteToFeed) fillFieldMap() { 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["id"] = n.ID
n.fieldMap["created_at"] = n.CreatedAt n.fieldMap["created_at"] = n.CreatedAt
n.fieldMap["note_id"] = n.NoteId 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.noteToFeedDo.ReplaceConnPool(db.Statement.ConnPool)
n.Note.db = db.Session(&gorm.Session{Initialized: true}) n.Note.db = db.Session(&gorm.Session{Initialized: true})
n.Note.db.Statement.ConnPool = db.Statement.ConnPool 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 return n
} }
func (n noteToFeed) replaceDB(db *gorm.DB) noteToFeed { func (n noteToFeed) replaceDB(db *gorm.DB) noteToFeed {
n.noteToFeedDo.ReplaceDB(db) n.noteToFeedDo.ReplaceDB(db)
n.Note.db = db.Session(&gorm.Session{}) n.Note.db = db.Session(&gorm.Session{})
n.Feed.db = db.Session(&gorm.Session{})
return n return n
} }
@ -675,6 +699,91 @@ func (a noteToFeedBelongsToNoteTx) Unscoped() *noteToFeedBelongsToNoteTx {
return &a 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 &noteToFeedBelongsToFeedTx{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 noteToFeedDo struct{ gen.DO }
type INoteToFeedDo interface { type INoteToFeedDo interface {

View file

@ -27,13 +27,14 @@ func newReaction(db *gorm.DB, opts ...gen.DOOption) reaction {
tableName := _reaction.reactionDo.TableName() tableName := _reaction.reactionDo.TableName()
_reaction.ALL = field.NewAsterisk(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.CreatedAt = field.NewTime(tableName, "created_at")
_reaction.UpdatedAt = field.NewTime(tableName, "updated_at") _reaction.UpdatedAt = field.NewTime(tableName, "updated_at")
_reaction.DeletedAt = field.NewField(tableName, "deleted_at") _reaction.DeletedAt = field.NewField(tableName, "deleted_at")
_reaction.NoteId = field.NewString(tableName, "note_id") _reaction.NoteId = field.NewString(tableName, "note_id")
_reaction.ReactorId = field.NewString(tableName, "reactor_id") _reaction.ReactorId = field.NewString(tableName, "reactor_id")
_reaction.EmoteId = field.NewField(tableName, "emote_id") _reaction.EmoteId = field.NewField(tableName, "emote_id")
_reaction.Content = field.NewField(tableName, "content")
_reaction.Note = reactionBelongsToNote{ _reaction.Note = reactionBelongsToNote{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@ -424,13 +425,14 @@ type reaction struct {
reactionDo reactionDo
ALL field.Asterisk ALL field.Asterisk
ID field.Uint ID field.String
CreatedAt field.Time CreatedAt field.Time
UpdatedAt field.Time UpdatedAt field.Time
DeletedAt field.Field DeletedAt field.Field
NoteId field.String NoteId field.String
ReactorId field.String ReactorId field.String
EmoteId field.Field EmoteId field.Field
Content field.Field
Note reactionBelongsToNote Note reactionBelongsToNote
Reactor reactionBelongsToReactor Reactor reactionBelongsToReactor
@ -452,13 +454,14 @@ func (r reaction) As(alias string) *reaction {
func (r *reaction) updateTableName(table string) *reaction { func (r *reaction) updateTableName(table string) *reaction {
r.ALL = field.NewAsterisk(table) 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.CreatedAt = field.NewTime(table, "created_at")
r.UpdatedAt = field.NewTime(table, "updated_at") r.UpdatedAt = field.NewTime(table, "updated_at")
r.DeletedAt = field.NewField(table, "deleted_at") r.DeletedAt = field.NewField(table, "deleted_at")
r.NoteId = field.NewString(table, "note_id") r.NoteId = field.NewString(table, "note_id")
r.ReactorId = field.NewString(table, "reactor_id") r.ReactorId = field.NewString(table, "reactor_id")
r.EmoteId = field.NewField(table, "emote_id") r.EmoteId = field.NewField(table, "emote_id")
r.Content = field.NewField(table, "content")
r.fillFieldMap() r.fillFieldMap()
@ -475,7 +478,7 @@ func (r *reaction) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (r *reaction) fillFieldMap() { 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["id"] = r.ID
r.fieldMap["created_at"] = r.CreatedAt r.fieldMap["created_at"] = r.CreatedAt
r.fieldMap["updated_at"] = r.UpdatedAt r.fieldMap["updated_at"] = r.UpdatedAt
@ -483,6 +486,7 @@ func (r *reaction) fillFieldMap() {
r.fieldMap["note_id"] = r.NoteId r.fieldMap["note_id"] = r.NoteId
r.fieldMap["reactor_id"] = r.ReactorId r.fieldMap["reactor_id"] = r.ReactorId
r.fieldMap["emote_id"] = r.EmoteId r.fieldMap["emote_id"] = r.EmoteId
r.fieldMap["content"] = r.Content
} }

View file

@ -1,15 +1,21 @@
package models package models
import "gorm.io/gorm" import (
"gorm.io/gorm"
"time"
)
// Emotes are combinations of a name, the server it originated from // 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 // TODO: Include the case of unicode icons being used as emote
type Emote struct { type Emote struct {
gorm.Model // Standard gorm model for id and timestamps ID string `gorm:"primarykey"`
Metadata MediaMetadata // The media used by this emote CreatedAt time.Time
MetadataId string // Id of the media information, primarily for gorm 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 of the emote. Also the text for using it in a message (ex. :bob:)
Name string Name string

View file

@ -2,6 +2,7 @@ package models
import ( import (
"database/sql" "database/sql"
"time"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -10,12 +11,15 @@ import (
// A reaction may contain some content text. If yes, this is the reaction. // 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 // 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 { type Reaction struct {
gorm.Model ID string `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
Note Note Note Note
NoteId string NoteId string
Reactor User Reactor User
ReactorId string ReactorId string
Emote *Emote // Emote is optional. If not set, use the default emote of the server 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 Content sql.NullString // Content/text of the reaction. Used for example in cases where a unicode character is sent as emote reaction
} }

View file

@ -6,17 +6,17 @@ import (
func BuildActivitypubRouter() http.Handler { func BuildActivitypubRouter() http.Handler {
router := http.NewServeMux() router := http.NewServeMux()
router.HandleFunc("/user/{id}", users) router.HandleFunc("GET /user/{id}", users)
router.HandleFunc("/user/{id}/inbox", userInbox) router.HandleFunc("POST /user/{id}/inbox", userInbox)
router.HandleFunc("/user/{id}/followers", userFollowers) router.HandleFunc("GET /user/{id}/followers", userFollowers)
router.HandleFunc("/user/{id}/following", userFollowing) router.HandleFunc("GET /user/{id}/following", userFollowing)
router.HandleFunc("/activity/accept/{id}", activityAccept) router.HandleFunc("GET /activity/accept/{id}", activityAccept)
router.HandleFunc("/activity/create/{id}", activityCreate) router.HandleFunc("GET /activity/create/{id}", activityCreate)
router.HandleFunc("/activity/delete/{id}", activityDelete) router.HandleFunc("GET /activity/delete/{id}", activityDelete)
router.HandleFunc("/activity/reject/{id}", activityReject) router.HandleFunc("GET /activity/reject/{id}", activityReject)
router.HandleFunc("/activity/update/{id}", activityUpdate) router.HandleFunc("GET /activity/update/{id}", activityUpdate)
router.HandleFunc("/activity/follow/{id}", activityFollow) router.HandleFunc("GET /activity/follow/{id}", activityFollow)
router.HandleFunc("/note/{id}", objectNote) router.HandleFunc("GET /note/{id}", objectNote)
router.HandleFunc("/note/{id}/reactions", noteReactions) router.HandleFunc("GET /note/{id}/reactions", noteReactions)
return router return router
} }