summaryrefslogtreecommitdiff
path: root/internal/federation
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2022-09-12 13:03:23 +0200
committerLibravatar GitHub <noreply@github.com>2022-09-12 13:03:23 +0200
commit268f252e0d517f2693b30d03fb8a68a0764a43bc (patch)
tree95920c06bcdfc0ca11486aa08a547d85ca35f8ce /internal/federation
parent[docs] unbreak standard css (#818) (diff)
downloadgotosocial-268f252e0d517f2693b30d03fb8a68a0764a43bc.tar.xz
[feature] Fetch + display custom emoji in statuses from remote instances (#807)
* start implementing remote emoji fetcher * update status where pk * aaa * tidy up a little * check size limits for emojis * thank you linter, i love you <3 * update swagger docs * add emoji dereference test * make emoji max sizes configurable * normalize db.ErrAlreadyExists
Diffstat (limited to 'internal/federation')
-rw-r--r--internal/federation/dereferencing/dereferencer.go1
-rw-r--r--internal/federation/dereferencing/emoji.go51
-rw-r--r--internal/federation/dereferencing/emoji_test.go95
-rw-r--r--internal/federation/dereferencing/status.go76
-rw-r--r--internal/federation/federatingdb/create.go3
5 files changed, 211 insertions, 15 deletions
diff --git a/internal/federation/dereferencing/dereferencer.go b/internal/federation/dereferencing/dereferencer.go
index 4f7559be3..0fad2405e 100644
--- a/internal/federation/dereferencing/dereferencer.go
+++ b/internal/federation/dereferencing/dereferencer.go
@@ -41,6 +41,7 @@ type Dereferencer interface {
GetRemoteInstance(ctx context.Context, username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error)
GetRemoteMedia(ctx context.Context, requestingUsername string, accountID string, remoteURL string, ai *media.AdditionalMediaInfo) (*media.ProcessingMedia, error)
+ GetRemoteEmoji(ctx context.Context, requestingUsername string, remoteURL string, shortcode string, id string, emojiURI string, ai *media.AdditionalEmojiInfo) (*media.ProcessingEmoji, error)
DereferenceAnnounce(ctx context.Context, announce *gtsmodel.Status, requestingUsername string) error
DereferenceThread(ctx context.Context, username string, statusIRI *url.URL) error
diff --git a/internal/federation/dereferencing/emoji.go b/internal/federation/dereferencing/emoji.go
new file mode 100644
index 000000000..49811b131
--- /dev/null
+++ b/internal/federation/dereferencing/emoji.go
@@ -0,0 +1,51 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package dereferencing
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net/url"
+
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+)
+
+func (d *deref) GetRemoteEmoji(ctx context.Context, requestingUsername string, remoteURL string, shortcode string, id string, emojiURI string, ai *media.AdditionalEmojiInfo) (*media.ProcessingEmoji, error) {
+ t, err := d.transportController.NewTransportForUsername(ctx, requestingUsername)
+ if err != nil {
+ return nil, fmt.Errorf("GetRemoteEmoji: error creating transport: %s", err)
+ }
+
+ derefURI, err := url.Parse(remoteURL)
+ if err != nil {
+ return nil, fmt.Errorf("GetRemoteEmoji: error parsing url: %s", err)
+ }
+
+ dataFunc := func(innerCtx context.Context) (io.Reader, int, error) {
+ return t.DereferenceMedia(innerCtx, derefURI)
+ }
+
+ processingMedia, err := d.mediaManager.ProcessEmoji(ctx, dataFunc, nil, shortcode, id, emojiURI, ai)
+ if err != nil {
+ return nil, fmt.Errorf("GetRemoteEmoji: error processing emoji: %s", err)
+ }
+
+ return processingMedia, nil
+}
diff --git a/internal/federation/dereferencing/emoji_test.go b/internal/federation/dereferencing/emoji_test.go
new file mode 100644
index 000000000..b03d839ce
--- /dev/null
+++ b/internal/federation/dereferencing/emoji_test.go
@@ -0,0 +1,95 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package dereferencing_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+)
+
+type EmojiTestSuite struct {
+ DereferencerStandardTestSuite
+}
+
+func (suite *EmojiTestSuite) TestDereferenceEmojiBlocking() {
+ ctx := context.Background()
+ fetchingAccount := suite.testAccounts["local_account_1"]
+ emojiImageRemoteURL := "http://example.org/media/emojis/1781772.gif"
+ emojiImageStaticRemoteURL := "http://example.org/media/emojis/1781772.gif"
+ emojiURI := "http://example.org/emojis/1781772"
+ emojiShortcode := "peglin"
+ emojiID := "01GCBMGNZBKMEE1KTZ6PMJEW5D"
+ emojiDomain := "example.org"
+ emojiDisabled := false
+ emojiVisibleInPicker := false
+
+ ai := &media.AdditionalEmojiInfo{
+ Domain: &emojiDomain,
+ ImageRemoteURL: &emojiImageRemoteURL,
+ ImageStaticRemoteURL: &emojiImageStaticRemoteURL,
+ Disabled: &emojiDisabled,
+ VisibleInPicker: &emojiVisibleInPicker,
+ }
+
+ processingEmoji, err := suite.dereferencer.GetRemoteEmoji(ctx, fetchingAccount.Username, emojiImageRemoteURL, emojiShortcode, emojiID, emojiURI, ai)
+ suite.NoError(err)
+
+ // make a blocking call to load the emoji from the in-process media
+ emoji, err := processingEmoji.LoadEmoji(ctx)
+ suite.NoError(err)
+ suite.NotNil(emoji)
+
+ suite.Equal(emojiID, emoji.ID)
+ suite.WithinDuration(time.Now(), emoji.CreatedAt, 10*time.Second)
+ suite.WithinDuration(time.Now(), emoji.UpdatedAt, 10*time.Second)
+ suite.Equal(emojiShortcode, emoji.Shortcode)
+ suite.Equal(emojiDomain, emoji.Domain)
+ suite.Equal(emojiImageRemoteURL, emoji.ImageRemoteURL)
+ suite.Equal(emojiImageStaticRemoteURL, emoji.ImageStaticRemoteURL)
+ suite.Contains(emoji.ImageURL, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif")
+ suite.Contains(emoji.ImageStaticURL, "emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png")
+ suite.Contains(emoji.ImagePath, "/emoji/original/01GCBMGNZBKMEE1KTZ6PMJEW5D.gif")
+ suite.Contains(emoji.ImageStaticPath, "/emoji/static/01GCBMGNZBKMEE1KTZ6PMJEW5D.png")
+ suite.Equal("image/gif", emoji.ImageContentType)
+ suite.Equal("image/png", emoji.ImageStaticContentType)
+ suite.Equal(37796, emoji.ImageFileSize)
+ suite.Equal(7951, emoji.ImageStaticFileSize)
+ suite.WithinDuration(time.Now(), emoji.ImageUpdatedAt, 10*time.Second)
+ suite.False(*emoji.Disabled)
+ suite.Equal(emojiURI, emoji.URI)
+ suite.False(*emoji.VisibleInPicker)
+ suite.Empty(emoji.CategoryID)
+
+ // ensure that emoji is now in storage
+ stored, err := suite.storage.Get(ctx, emoji.ImagePath)
+ suite.NoError(err)
+ suite.Len(stored, emoji.ImageFileSize)
+
+ storedStatic, err := suite.storage.Get(ctx, emoji.ImageStaticPath)
+ suite.NoError(err)
+ suite.Len(storedStatic, emoji.ImageStaticFileSize)
+}
+
+func TestEmojiTestSuite(t *testing.T) {
+ suite.Run(t, new(EmojiTestSuite))
+}
diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go
index e6e03646c..f3b7ee96e 100644
--- a/internal/federation/dereferencing/status.go
+++ b/internal/federation/dereferencing/status.go
@@ -26,10 +26,10 @@ import (
"net/url"
"strings"
- "codeberg.org/gruf/go-kv"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
@@ -46,11 +46,7 @@ func (d *deref) EnrichRemoteStatus(ctx context.Context, username string, status
return nil, err
}
- if err := d.db.UpdateByPrimaryKey(ctx, status); err != nil {
- return nil, fmt.Errorf("EnrichRemoteStatus: error updating status: %s", err)
- }
-
- return status, nil
+ return d.db.UpdateStatus(ctx, status)
}
// GetRemoteStatus completely dereferences a remote status, converts it to a GtS model status,
@@ -225,12 +221,6 @@ func (d *deref) dereferenceStatusable(ctx context.Context, username string, remo
// and attach them to the status. The status itself will not be added to the database yet,
// that's up the caller to do.
func (d *deref) populateStatusFields(ctx context.Context, status *gtsmodel.Status, requestingUsername string, includeParent bool) error {
- l := log.WithFields(kv.Fields{
-
- {"status", status},
- }...)
- l.Debug("entering function")
-
statusIRI, err := url.Parse(status.URI)
if err != nil {
return fmt.Errorf("populateStatusFields: couldn't parse status URI %s: %s", status.URI, err)
@@ -262,7 +252,9 @@ func (d *deref) populateStatusFields(ctx context.Context, status *gtsmodel.Statu
// TODO
// 3. Emojis
- // TODO
+ if err := d.populateStatusEmojis(ctx, status, requestingUsername); err != nil {
+ return fmt.Errorf("populateStatusFields: error populating status emojis: %s", err)
+ }
// 4. Mentions
// TODO: do we need to handle removing empty mention objects and just using mention IDs slice?
@@ -413,6 +405,64 @@ func (d *deref) populateStatusAttachments(ctx context.Context, status *gtsmodel.
return nil
}
+func (d *deref) populateStatusEmojis(ctx context.Context, status *gtsmodel.Status, requestingUsername string) error {
+ // At this point we should know:
+ // * the AP uri of the emoji
+ // * the domain of the emoji
+ // * the shortcode of the emoji
+ // * the remote URL of the image
+ // This should be enough to dereference the emoji
+
+ gotEmojis := make([]*gtsmodel.Emoji, 0, len(status.Emojis))
+ emojiIDs := make([]string, 0, len(status.Emojis))
+
+ for _, e := range status.Emojis {
+ var gotEmoji *gtsmodel.Emoji
+ var err error
+
+ // check if we've already got this emoji in the db
+ if gotEmoji, err = d.db.GetEmojiByURI(ctx, e.URI); err != nil && err != db.ErrNoEntries {
+ log.Errorf("populateStatusEmojis: error checking database for emoji %s: %s", e.URI, err)
+ continue
+ }
+
+ if gotEmoji == nil {
+ // it's new! go get it!
+ newEmojiID, err := id.NewRandomULID()
+ if err != nil {
+ log.Errorf("populateStatusEmojis: error generating id for remote emoji %s: %s", e.URI, err)
+ continue
+ }
+
+ processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, newEmojiID, e.URI, &media.AdditionalEmojiInfo{
+ Domain: &e.Domain,
+ ImageRemoteURL: &e.ImageRemoteURL,
+ ImageStaticRemoteURL: &e.ImageRemoteURL,
+ Disabled: e.Disabled,
+ VisibleInPicker: e.VisibleInPicker,
+ })
+
+ if err != nil {
+ log.Errorf("populateStatusEmojis: couldn't get remote emoji %s: %s", e.URI, err)
+ continue
+ }
+
+ if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
+ log.Errorf("populateStatusEmojis: couldn't load remote emoji %s: %s", e.URI, err)
+ continue
+ }
+ }
+
+ // if we get here, we either had the emoji already or we successfully fetched it
+ gotEmojis = append(gotEmojis, gotEmoji)
+ emojiIDs = append(emojiIDs, gotEmoji.ID)
+ }
+
+ status.Emojis = gotEmojis
+ status.EmojiIDs = emojiIDs
+ return nil
+}
+
func (d *deref) populateStatusRepliedTo(ctx context.Context, status *gtsmodel.Status, requestingUsername string) error {
if status.InReplyToURI != "" && status.InReplyToID == "" {
statusURI, err := url.Parse(status.InReplyToURI)
diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go
index a6e55f2ad..25e961bc3 100644
--- a/internal/federation/federatingdb/create.go
+++ b/internal/federation/federatingdb/create.go
@@ -226,8 +226,7 @@ func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStream
status.ID = statusID
if err := f.db.PutStatus(ctx, status); err != nil {
- var alreadyExistsError *db.ErrAlreadyExists
- if errors.As(err, &alreadyExistsError) {
+ if errors.Is(err, db.ErrAlreadyExists) {
// the status already exists in the database, which means we've already handled everything else,
// so we can just return nil here and be done with it.
return nil