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

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 {
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,
})
}

View file

@ -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"`

View file

@ -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"`

View file

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

View file

@ -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")

View file

@ -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 &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 INoteToFeedDo interface {

View file

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

View file

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

View file

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

View file

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