diff --git a/internal/db/db.go b/internal/db/db.go index ed2e0b990..89cdae53b 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -113,6 +113,7 @@ type DB interface { // CreateInstanceAccount creates an account in the database with the same username as the instance host value. // Ie., if the instance is hosted at 'example.org' the instance user will have a username of 'example.org'. + // This is needed for things like serving files that belong to the instance and not an individual user/account. CreateInstanceAccount() error // GetAccountByUserID is a shortcut for the common action of fetching an account corresponding to a user ID. diff --git a/internal/db/gtsmodel/emoji.go b/internal/db/gtsmodel/emoji.go index 213d84251..fbd5aedf9 100644 --- a/internal/db/gtsmodel/emoji.go +++ b/internal/db/gtsmodel/emoji.go @@ -36,18 +36,31 @@ type Emoji struct { // For remote emojis, it'll be something like: // https://hackers.town/system/custom_emojis/images/000/049/842/original/1b74481204feabfd.png ImageRemoteURL string + // Where can a static / non-animated version of this emoji be retrieved remotely? Null for local emojis. + // For remote emojis, it'll be something like: + // https://hackers.town/system/custom_emojis/images/000/049/842/static/1b74481204feabfd.png + ImageStaticRemoteURL string // Where can this emoji be retrieved from the local server? Null for remote emojis. // Assuming our server is hosted at 'example.org', this will be something like: - // 'https://example.org/fileserver/emojis/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' + // 'https://example.org/fileserver/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/original/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' ImageURL string - // Path of the emoji image in the server storage system. - // Will be something like '/gotosocial/storage/emojis/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' + // Where can a static version of this emoji be retrieved from the local server? Null for remote emojis. + // Assuming our server is hosted at 'example.org', this will be something like: + // 'https://example.org/fileserver/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/small/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' + ImageStaticURL string + // Path of the emoji image in the server storage system. Will be something like: + // '/gotosocial/storage/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/original/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' ImagePath string `pg:",notnull"` + // Path of a static version of the emoji image in the server storage system. Will be something like: + // '/gotosocial/storage/6339820e-ef65-4166-a262-5a9f46adb1a7/emoji/small/bfa6c9c5-6c25-4ea4-98b4-d78b8126fb52.png' + ImageStaticPath string `pg:",notnull"` // MIME content type of the emoji image // Probably "image/png" ImageContentType string `pg:",notnull"` // Size of the emoji image file in bytes, for serving purposes. ImageFileSize int `pg:",notnull"` + // Size of the static version of the emoji image file in bytes, for serving purposes. + ImageStaticFileSize int `pg:",notnull"` // When was the emoji image last updated? ImageUpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Has a moderation action disabled this emoji from being shown? diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 2a327a8c3..ee336a249 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -51,6 +51,10 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr return fmt.Errorf("error creating dbservice: %s", err) } + if err := dbService.CreateInstanceAccount(); err != nil { + return fmt.Errorf("error creating instance account: %s", err) + } + router, err := router.New(c, log) if err != nil { return fmt.Errorf("error creating router: %s", err) diff --git a/internal/media/media.go b/internal/media/media.go index 4f73f6da3..dc27ecf28 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -144,6 +144,56 @@ func (mh *mediaHandler) ProcessAttachment(attachment []byte, accountID string) ( return nil, fmt.Errorf("content type %s not (yet) supported", contentType) } +func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (*gtsmodel.Emoji, error) { + contentType, err := parseContentType(emojiBytes) + if err != nil { + return nil, err + } + + if !supportedEmojiType(contentType) { + return nil, fmt.Errorf("content type %s not supported for emojis", contentType) + } + + newEmojiID := uuid.NewString() + instanceAccount := >smodel.Account{} + if err := mh.db.GetWhere("username", mh.config.Host, instanceAccount); err != nil { + return nil, fmt.Errorf("error fetching instance account: %s", err) + } + instanceAccountID := instanceAccount.ID + extension := strings.Split(contentType, "/")[1] + + URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath) + emojiURI := fmt.Sprintf("%s://%s/%s/%s", mh.config.Protocol, mh.config.Host, MediaEmoji, newEmojiID) + emojiURL := fmt.Sprintf("%s/%s/%s/%s/%s.%s", URLbase, instanceAccountID, MediaEmoji, MediaOriginal, newEmojiID, extension) + emojiPath := fmt.Sprintf("%s/%s/%s/%s/%s.%s", mh.config.StorageConfig.BasePath, instanceAccountID, MediaEmoji, MediaOriginal, newEmojiID, extension) + if err := mh.storage.StoreFileAt(emojiPath, emojiBytes); err != nil { + return nil, fmt.Errorf("storage error: %s", err) + } + + e := >smodel.Emoji{ + ID: newEmojiID, + Shortcode: shortcode, + Domain: "", // empty because this is a local emoji + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + ImageRemoteURL: "", // empty because this is a local emoji + ImageStaticRemoteURL: "", + ImageURL: emojiURL, + ImageStaticURL: "", + ImagePath: emojiPath, + ImageStaticPath: "", + ImageContentType: contentType, + ImageFileSize: 0, + ImageStaticFileSize: 0, + ImageUpdatedAt: time.Now(), + Disabled: false, + URI: emojiURI, + VisibleInPicker: true, + CategoryID: "", // empty because this is a new emoji -- no category yet + } + return e, nil +} + /* HELPER FUNCTIONS */ @@ -177,7 +227,7 @@ func (mh *mediaHandler) processImage(data []byte, accountID string, contentType return nil, errors.New("media type unrecognized") } - small, err = deriveThumbnail(clean, contentType) + small, err = deriveThumbnail(clean, contentType, 256, 256) if err != nil { return nil, fmt.Errorf("error deriving thumbnail: %s", err) } @@ -287,7 +337,7 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string return nil, fmt.Errorf("error parsing image: %s", err) } - small, err := deriveThumbnail(clean, contentType) + small, err := deriveThumbnail(clean, contentType, 256, 256) if err != nil { return nil, fmt.Errorf("error deriving thumbnail: %s", err) } diff --git a/internal/media/util.go b/internal/media/util.go index 518da1db8..25aeeadd4 100644 --- a/internal/media/util.go +++ b/internal/media/util.go @@ -86,6 +86,11 @@ func supportedVideoType(mimeType string) bool { return false } +// supportedEmojiType checks that the content type is image/png -- the only type supported for emoji. +func supportedEmojiType(mimeType string) bool { + return mimeType == "image/png" +} + // purgeExif is a little wrapper for the action of removing exif data from an image. // Only pass pngs or jpegs to this function. func purgeExif(b []byte) ([]byte, error) { @@ -191,7 +196,7 @@ func deriveImage(b []byte, extension string) (*imageAndMeta, error) { // // Note that the aspect ratio of the image will be retained, // so it will not necessarily be a square. -func deriveThumbnail(b []byte, extension string) (*imageAndMeta, error) { +func deriveThumbnail(b []byte, extension string, x uint, y uint) (*imageAndMeta, error) { var i image.Image var err error @@ -215,7 +220,7 @@ func deriveThumbnail(b []byte, extension string) (*imageAndMeta, error) { return nil, fmt.Errorf("extension %s not recognised", extension) } - thumb := resize.Thumbnail(256, 256, i, resize.NearestNeighbor) + thumb := resize.Thumbnail(x, y, i, resize.NearestNeighbor) width := thumb.Bounds().Size().X height := thumb.Bounds().Size().Y size := width * height diff --git a/internal/media/util_test.go b/internal/media/util_test.go index f24c1660f..be617a256 100644 --- a/internal/media/util_test.go +++ b/internal/media/util_test.go @@ -121,7 +121,7 @@ func (suite *MediaUtilTestSuite) TestDeriveThumbnailFromJPEG() { assert.Nil(suite.T(), err) // clean it up and validate the clean version - imageAndMeta, err := deriveThumbnail(b, "image/jpeg") + imageAndMeta, err := deriveThumbnail(b, "image/jpeg", 256, 256) assert.Nil(suite.T(), err) assert.Equal(suite.T(), 256, imageAndMeta.width) diff --git a/testrig/db.go b/testrig/db.go index c62ca925c..4d5bb6f18 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -58,10 +58,6 @@ func NewTestDB() db.DB { // StandardDBSetup populates a given db with all the necessary tables/models for perfoming tests. func StandardDBSetup(db db.DB) { - if err := db.CreateInstanceAccount(); err != nil { - panic(err) - } - for _, m := range testModels { if err := db.CreateTable(m); err != nil { panic(err) @@ -109,6 +105,10 @@ func StandardDBSetup(db db.DB) { panic(err) } } + + if err := db.CreateInstanceAccount(); err != nil { + panic(err) + } } // StandardDBTeardown drops all the standard testing tables/models from the database to ensure it's clean for the next test.