diff options
author | 2022-10-13 15:16:24 +0200 | |
---|---|---|
committer | 2022-10-13 15:16:24 +0200 | |
commit | 70d65b683fa963d2a8761182a2ddd2f4f9a86bb4 (patch) | |
tree | 9cbd8f6870569b2514683c0e8ff6ea32e6e81780 /internal/media | |
parent | [frontend] Use new GET custom_emoji admin api (#908) (diff) | |
download | gotosocial-70d65b683fa963d2a8761182a2ddd2f4f9a86bb4.tar.xz |
[feature] Refetch emojis when they change on remote instances (#905)
* select emoji using image_static_url
* use updated on AP emojis
* allow refetch of updated emojis
* cheeky workaround for test
* clean up old files for refreshed emoji
* check error for originalPostData
* shorten GetEmojiByStaticImageURL
* delete kirby (sorry nintendo)
Diffstat (limited to 'internal/media')
-rw-r--r-- | internal/media/manager.go | 8 | ||||
-rw-r--r-- | internal/media/manager_test.go | 101 | ||||
-rw-r--r-- | internal/media/media_test.go | 2 | ||||
-rw-r--r-- | internal/media/processingemoji.go | 144 | ||||
-rw-r--r-- | internal/media/test/gts_pixellated-original.png | bin | 0 -> 10296 bytes | |||
-rw-r--r-- | internal/media/test/gts_pixellated-static.png | bin | 0 -> 1010 bytes | |||
-rw-r--r-- | internal/media/types.go | 4 |
7 files changed, 220 insertions, 39 deletions
diff --git a/internal/media/manager.go b/internal/media/manager.go index 828aa033b..62998156e 100644 --- a/internal/media/manager.go +++ b/internal/media/manager.go @@ -69,7 +69,9 @@ type Manager interface { // uri is the ActivityPub URI/ID of the emoji. // // ai is optional and can be nil. Any additional information about the emoji provided will be put in the database. - ProcessEmoji(ctx context.Context, data DataFunc, postData PostDataCallbackFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) + // + // If refresh is true, this indicates that the emoji image has changed and should be updated. + ProcessEmoji(ctx context.Context, data DataFunc, postData PostDataCallbackFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo, refresh bool) (*ProcessingEmoji, error) // RecacheMedia refetches, reprocesses, and recaches an existing attachment that has been uncached via pruneRemote. RecacheMedia(ctx context.Context, data DataFunc, postData PostDataCallbackFunc, attachmentID string) (*ProcessingMedia, error) @@ -164,8 +166,8 @@ func (m *manager) ProcessMedia(ctx context.Context, data DataFunc, postData Post return processingMedia, nil } -func (m *manager) ProcessEmoji(ctx context.Context, data DataFunc, postData PostDataCallbackFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) { - processingEmoji, err := m.preProcessEmoji(ctx, data, postData, shortcode, id, uri, ai) +func (m *manager) ProcessEmoji(ctx context.Context, data DataFunc, postData PostDataCallbackFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo, refresh bool) (*ProcessingEmoji, error) { + processingEmoji, err := m.preProcessEmoji(ctx, data, postData, shortcode, id, uri, ai, refresh) if err != nil { return nil, err } diff --git a/internal/media/manager_test.go b/internal/media/manager_test.go index f9c96259d..e00cdd98d 100644 --- a/internal/media/manager_test.go +++ b/internal/media/manager_test.go @@ -55,7 +55,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() { emojiID := "01GDQ9G782X42BAMFASKP64343" emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343" - processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "rainbow_test", emojiID, emojiURI, nil) + processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "rainbow_test", emojiID, emojiURI, nil, false) suite.NoError(err) // do a blocking call to fetch the emoji @@ -101,6 +101,99 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlocking() { suite.Equal(processedStaticBytesExpected, processedStaticBytes) } +func (suite *ManagerTestSuite) TestEmojiProcessBlockingRefresh() { + ctx := context.Background() + + // we're going to 'refresh' the remote 'yell' emoji by changing the image url to the pixellated gts logo + originalEmoji := suite.testEmojis["yell"] + + emojiToUpdate := >smodel.Emoji{} + *emojiToUpdate = *originalEmoji + newImageRemoteURL := "http://fossbros-anonymous.io/some/image/path.png" + + oldEmojiImagePath := emojiToUpdate.ImagePath + oldEmojiImageStaticPath := emojiToUpdate.ImageStaticPath + + data := func(_ context.Context) (io.Reader, int64, error) { + b, err := os.ReadFile("./test/gts_pixellated-original.png") + if err != nil { + panic(err) + } + return bytes.NewBuffer(b), int64(len(b)), nil + } + + emojiID := emojiToUpdate.ID + emojiURI := emojiToUpdate.URI + + processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "yell", emojiID, emojiURI, &media.AdditionalEmojiInfo{ + CreatedAt: &emojiToUpdate.CreatedAt, + Domain: &emojiToUpdate.Domain, + ImageRemoteURL: &newImageRemoteURL, + }, true) + suite.NoError(err) + + // do a blocking call to fetch the emoji + emoji, err := processingEmoji.LoadEmoji(ctx) + suite.NoError(err) + suite.NotNil(emoji) + + // make sure it's got the stuff set on it that we expect + suite.Equal(emojiID, emoji.ID) + + // file meta should be correctly derived from the image + suite.Equal("image/png", emoji.ImageContentType) + suite.Equal("image/png", emoji.ImageStaticContentType) + suite.Equal(10296, emoji.ImageFileSize) + + // now make sure the emoji is in the database + dbEmoji, err := suite.db.GetEmojiByID(ctx, emojiID) + suite.NoError(err) + suite.NotNil(dbEmoji) + + // make sure the processed emoji file is in storage + processedFullBytes, err := suite.storage.Get(ctx, emoji.ImagePath) + suite.NoError(err) + suite.NotEmpty(processedFullBytes) + + // load the processed bytes from our test folder, to compare + processedFullBytesExpected, err := os.ReadFile("./test/gts_pixellated-original.png") + suite.NoError(err) + suite.NotEmpty(processedFullBytesExpected) + + // the bytes in storage should be what we expected + suite.Equal(processedFullBytesExpected, processedFullBytes) + + // now do the same for the thumbnail and make sure it's what we expected + processedStaticBytes, err := suite.storage.Get(ctx, emoji.ImageStaticPath) + suite.NoError(err) + suite.NotEmpty(processedStaticBytes) + + processedStaticBytesExpected, err := os.ReadFile("./test/gts_pixellated-static.png") + suite.NoError(err) + suite.NotEmpty(processedStaticBytesExpected) + + suite.Equal(processedStaticBytesExpected, processedStaticBytes) + + // most fields should be different on the emoji now from what they were before + suite.Equal(originalEmoji.ID, dbEmoji.ID) + suite.NotEqual(originalEmoji.ImageRemoteURL, dbEmoji.ImageRemoteURL) + suite.NotEqual(originalEmoji.ImageURL, dbEmoji.ImageURL) + suite.NotEqual(originalEmoji.ImageStaticURL, dbEmoji.ImageStaticURL) + suite.NotEqual(originalEmoji.ImageFileSize, dbEmoji.ImageFileSize) + suite.NotEqual(originalEmoji.ImageStaticFileSize, dbEmoji.ImageStaticFileSize) + suite.NotEqual(originalEmoji.ImagePath, dbEmoji.ImagePath) + suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath) + suite.NotEqual(originalEmoji.ImageStaticPath, dbEmoji.ImageStaticPath) + suite.NotEqual(originalEmoji.UpdatedAt, dbEmoji.UpdatedAt) + suite.NotEqual(originalEmoji.ImageUpdatedAt, dbEmoji.ImageUpdatedAt) + + // the old image files should no longer be in storage + _, err = suite.storage.Get(ctx, oldEmojiImagePath) + suite.ErrorIs(err, storage.ErrNotFound) + _, err = suite.storage.Get(ctx, oldEmojiImageStaticPath) + suite.ErrorIs(err, storage.ErrNotFound) +} + func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLarge() { ctx := context.Background() @@ -116,7 +209,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLarge() { emojiID := "01GDQ9G782X42BAMFASKP64343" emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343" - processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "big_panda", emojiID, emojiURI, nil) + processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "big_panda", emojiID, emojiURI, nil, false) suite.NoError(err) // do a blocking call to fetch the emoji @@ -140,7 +233,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingTooLargeNoSizeGiven() { emojiID := "01GDQ9G782X42BAMFASKP64343" emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343" - processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "big_panda", emojiID, emojiURI, nil) + processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "big_panda", emojiID, emojiURI, nil, false) suite.NoError(err) // do a blocking call to fetch the emoji @@ -165,7 +258,7 @@ func (suite *ManagerTestSuite) TestEmojiProcessBlockingNoFileSizeGiven() { emojiURI := "http://localhost:8080/emoji/01GDQ9G782X42BAMFASKP64343" // process the media with no additional info provided - processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "rainbow_test", emojiID, emojiURI, nil) + processingEmoji, err := suite.manager.ProcessEmoji(ctx, data, nil, "rainbow_test", emojiID, emojiURI, nil, false) suite.NoError(err) // do a blocking call to fetch the emoji diff --git a/internal/media/media_test.go b/internal/media/media_test.go index fda1963a7..e2c3914a3 100644 --- a/internal/media/media_test.go +++ b/internal/media/media_test.go @@ -35,6 +35,7 @@ type MediaStandardTestSuite struct { manager media.Manager testAttachments map[string]*gtsmodel.MediaAttachment testAccounts map[string]*gtsmodel.Account + testEmojis map[string]*gtsmodel.Emoji } func (suite *MediaStandardTestSuite) SetupSuite() { @@ -50,6 +51,7 @@ func (suite *MediaStandardTestSuite) SetupTest() { testrig.StandardDBSetup(suite.db, nil) suite.testAttachments = testrig.NewTestAttachments() suite.testAccounts = testrig.NewTestAccounts() + suite.testEmojis = testrig.NewTestEmojis() suite.manager = testrig.NewTestMediaManager(suite.db, suite.storage) } diff --git a/internal/media/processingemoji.go b/internal/media/processingemoji.go index 6495b991e..e1c6f2efb 100644 --- a/internal/media/processingemoji.go +++ b/internal/media/processingemoji.go @@ -21,6 +21,7 @@ package media import ( "bytes" "context" + "errors" "fmt" "io" "strings" @@ -28,9 +29,11 @@ import ( "sync/atomic" "time" + gostore "codeberg.org/gruf/go-store/storage" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/uris" @@ -71,6 +74,11 @@ type ProcessingEmoji struct { // track whether this emoji has already been put in the databse insertedInDB bool + + // is this a refresh of an existing emoji? + refresh bool + // if it is a refresh, which alternate ID should we use in the storage and URL paths? + newPathID string } // EmojiID returns the ID of the underlying emoji without blocking processing. @@ -94,8 +102,28 @@ func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error // store the result in the database before returning it if !p.insertedInDB { - if err := p.database.PutEmoji(ctx, p.emoji); err != nil { - return nil, err + if p.refresh { + columns := []string{ + "updated_at", + "image_remote_url", + "image_static_remote_url", + "image_url", + "image_static_url", + "image_path", + "image_static_path", + "image_content_type", + "image_file_size", + "image_static_file_size", + "image_updated_at", + "uri", + } + if _, err := p.database.UpdateEmoji(ctx, p.emoji, columns...); err != nil { + return nil, err + } + } else { + if err := p.database.PutEmoji(ctx, p.emoji); err != nil { + return nil, err + } } p.insertedInDB = true } @@ -203,8 +231,14 @@ func (p *ProcessingEmoji) store(ctx context.Context) error { // set some additional fields on the emoji now that // we know more about what the underlying image actually is - p.emoji.ImageURL = uris.GenerateURIForAttachment(p.instanceAccountID, string(TypeEmoji), string(SizeOriginal), p.emoji.ID, extension) - p.emoji.ImagePath = fmt.Sprintf("%s/%s/%s/%s.%s", p.instanceAccountID, TypeEmoji, SizeOriginal, p.emoji.ID, extension) + var pathID string + if p.refresh { + pathID = p.newPathID + } else { + pathID = p.emoji.ID + } + p.emoji.ImageURL = uris.GenerateURIForAttachment(p.instanceAccountID, string(TypeEmoji), string(SizeOriginal), pathID, extension) + p.emoji.ImagePath = fmt.Sprintf("%s/%s/%s/%s.%s", p.instanceAccountID, TypeEmoji, SizeOriginal, pathID, extension) p.emoji.ImageContentType = contentType // concatenate the first bytes with the existing bytes still in the reader (thanks Mara) @@ -251,39 +285,87 @@ func (p *ProcessingEmoji) store(ctx context.Context) error { return nil } -func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, postData PostDataCallbackFunc, shortcode string, id string, uri string, ai *AdditionalEmojiInfo) (*ProcessingEmoji, error) { +func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, postData PostDataCallbackFunc, shortcode string, emojiID string, uri string, ai *AdditionalEmojiInfo, refresh bool) (*ProcessingEmoji, error) { instanceAccount, err := m.db.GetInstanceAccount(ctx, "") if err != nil { return nil, fmt.Errorf("preProcessEmoji: error fetching this instance account from the db: %s", err) } - disabled := false - visibleInPicker := true - - // populate initial fields on the emoji -- some of these will be overwritten as we proceed - emoji := >smodel.Emoji{ - ID: id, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Shortcode: shortcode, - Domain: "", // assume our own domain unless told otherwise - ImageRemoteURL: "", - ImageStaticRemoteURL: "", - ImageURL: "", // we don't know yet - ImageStaticURL: uris.GenerateURIForAttachment(instanceAccount.ID, string(TypeEmoji), string(SizeStatic), id, mimePng), // all static emojis are encoded as png - ImagePath: "", // we don't know yet - ImageStaticPath: fmt.Sprintf("%s/%s/%s/%s.%s", instanceAccount.ID, TypeEmoji, SizeStatic, id, mimePng), // all static emojis are encoded as png - ImageContentType: "", // we don't know yet - ImageStaticContentType: mimeImagePng, // all static emojis are encoded as png - ImageFileSize: 0, - ImageStaticFileSize: 0, - ImageUpdatedAt: time.Now(), - Disabled: &disabled, - URI: uri, - VisibleInPicker: &visibleInPicker, - CategoryID: "", + var newPathID string + var emoji *gtsmodel.Emoji + if refresh { + emoji, err = m.db.GetEmojiByID(ctx, emojiID) + if err != nil { + return nil, fmt.Errorf("preProcessEmoji: error fetching emoji to refresh from the db: %s", err) + } + + // if this is a refresh, we will end up with new images + // stored for this emoji, so we can use the postData function + // to perform clean up of the old images from storage + originalPostData := postData + originalImagePath := emoji.ImagePath + originalImageStaticPath := emoji.ImageStaticPath + postData = func(ctx context.Context) error { + // trigger the original postData function if it was provided + if originalPostData != nil { + if err := originalPostData(ctx); err != nil { + return err + } + } + + l := log.WithField("shortcode@domain", emoji.Shortcode+"@"+emoji.Domain) + l.Debug("postData: cleaning up old emoji files for refreshed emoji") + if err := m.storage.Delete(ctx, originalImagePath); err != nil && !errors.Is(err, gostore.ErrNotFound) { + l.Errorf("postData: error cleaning up old emoji image at %s for refreshed emoji: %s", originalImagePath, err) + } + if err := m.storage.Delete(ctx, originalImageStaticPath); err != nil && !errors.Is(err, gostore.ErrNotFound) { + l.Errorf("postData: error cleaning up old emoji static image at %s for refreshed emoji: %s", originalImageStaticPath, err) + } + + return nil + } + + newPathID, err = id.NewRandomULID() + if err != nil { + return nil, fmt.Errorf("preProcessEmoji: error generating alternateID for emoji refresh: %s", err) + } + + // store + serve static image at new path ID + emoji.ImageStaticURL = uris.GenerateURIForAttachment(instanceAccount.ID, string(TypeEmoji), string(SizeStatic), newPathID, mimePng) + emoji.ImageStaticPath = fmt.Sprintf("%s/%s/%s/%s.%s", instanceAccount.ID, TypeEmoji, SizeStatic, newPathID, mimePng) + + // update these fields as we go + emoji.URI = uri + } else { + disabled := false + visibleInPicker := true + + // populate initial fields on the emoji -- some of these will be overwritten as we proceed + emoji = >smodel.Emoji{ + ID: emojiID, + CreatedAt: time.Now(), + Shortcode: shortcode, + Domain: "", // assume our own domain unless told otherwise + ImageRemoteURL: "", + ImageStaticRemoteURL: "", + ImageURL: "", // we don't know yet + ImageStaticURL: uris.GenerateURIForAttachment(instanceAccount.ID, string(TypeEmoji), string(SizeStatic), emojiID, mimePng), // all static emojis are encoded as png + ImagePath: "", // we don't know yet + ImageStaticPath: fmt.Sprintf("%s/%s/%s/%s.%s", instanceAccount.ID, TypeEmoji, SizeStatic, emojiID, mimePng), // all static emojis are encoded as png + ImageContentType: "", // we don't know yet + ImageStaticContentType: mimeImagePng, // all static emojis are encoded as png + ImageFileSize: 0, + ImageStaticFileSize: 0, + Disabled: &disabled, + URI: uri, + VisibleInPicker: &visibleInPicker, + CategoryID: "", + } } + emoji.ImageUpdatedAt = time.Now() + emoji.UpdatedAt = time.Now() + // check if we have additional info to add to the emoji, // and overwrite some of the emoji fields if so if ai != nil { @@ -324,6 +406,8 @@ func (m *manager) preProcessEmoji(ctx context.Context, data DataFunc, postData P staticState: int32(received), database: m.db, storage: m.storage, + refresh: refresh, + newPathID: newPathID, } return processingEmoji, nil diff --git a/internal/media/test/gts_pixellated-original.png b/internal/media/test/gts_pixellated-original.png Binary files differnew file mode 100644 index 000000000..564ee76d8 --- /dev/null +++ b/internal/media/test/gts_pixellated-original.png diff --git a/internal/media/test/gts_pixellated-static.png b/internal/media/test/gts_pixellated-static.png Binary files differnew file mode 100644 index 000000000..c6dcb0f4a --- /dev/null +++ b/internal/media/test/gts_pixellated-static.png diff --git a/internal/media/types.go b/internal/media/types.go index d71080658..3238916b8 100644 --- a/internal/media/types.go +++ b/internal/media/types.go @@ -98,8 +98,8 @@ type AdditionalMediaInfo struct { FocusY *float32 } -// AdditionalMediaInfo represents additional information -// that should be added to an emoji when processing it. +// AdditionalEmojiInfo represents additional information +// that should be taken into account when processing an emoji. type AdditionalEmojiInfo struct { // Time that this emoji was created; defaults to time.Now(). CreatedAt *time.Time |