summaryrefslogtreecommitdiff
path: root/internal/media
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2022-10-13 15:16:24 +0200
committerLibravatar GitHub <noreply@github.com>2022-10-13 15:16:24 +0200
commit70d65b683fa963d2a8761182a2ddd2f4f9a86bb4 (patch)
tree9cbd8f6870569b2514683c0e8ff6ea32e6e81780 /internal/media
parent[frontend] Use new GET custom_emoji admin api (#908) (diff)
downloadgotosocial-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.go8
-rw-r--r--internal/media/manager_test.go101
-rw-r--r--internal/media/media_test.go2
-rw-r--r--internal/media/processingemoji.go144
-rw-r--r--internal/media/test/gts_pixellated-original.pngbin0 -> 10296 bytes
-rw-r--r--internal/media/test/gts_pixellated-static.pngbin0 -> 1010 bytes
-rw-r--r--internal/media/types.go4
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 := &gtsmodel.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 := &gtsmodel.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 = &gtsmodel.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
new file mode 100644
index 000000000..564ee76d8
--- /dev/null
+++ b/internal/media/test/gts_pixellated-original.png
Binary files differ
diff --git a/internal/media/test/gts_pixellated-static.png b/internal/media/test/gts_pixellated-static.png
new file mode 100644
index 000000000..c6dcb0f4a
--- /dev/null
+++ b/internal/media/test/gts_pixellated-static.png
Binary files differ
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