summaryrefslogtreecommitdiff
path: root/internal/federation/dereferencing/emoji.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/federation/dereferencing/emoji.go')
-rw-r--r--internal/federation/dereferencing/emoji.go383
1 files changed, 258 insertions, 125 deletions
diff --git a/internal/federation/dereferencing/emoji.go b/internal/federation/dereferencing/emoji.go
index e81737d04..16f5acf25 100644
--- a/internal/federation/dereferencing/emoji.go
+++ b/internal/federation/dereferencing/emoji.go
@@ -19,29 +19,190 @@ package dereferencing
import (
"context"
- "fmt"
+ "errors"
"io"
"net/url"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
-func (d *Dereferencer) GetRemoteEmoji(ctx context.Context, requestUser string, remoteURL string, shortcode string, domain string, id string, emojiURI string, ai *media.AdditionalEmojiInfo, refresh bool) (*media.ProcessingEmoji, error) {
- var shortcodeDomain = shortcode + "@" + domain
+// GetEmoji fetches the emoji with given shortcode,
+// domain and remote URL to dereference it by. This
+// handles the case of existing emojis by passing them
+// to RefreshEmoji(), which in the case of a local
+// emoji will be a no-op. If the emoji does not yet
+// exist it will be newly inserted into the database
+// followed by dereferencing the actual media file.
+//
+// Please note that even if an error is returned,
+// an emoji model may still be returned if the error
+// was only encountered during actual dereferencing.
+// In this case, it will act as a placeholder.
+func (d *Dereferencer) GetEmoji(
+ ctx context.Context,
+ shortcode string,
+ domain string,
+ remoteURL string,
+ info media.AdditionalEmojiInfo,
+ refresh bool,
+) (
+ *gtsmodel.Emoji,
+ error,
+) {
+ // Look for an existing emoji with shortcode domain.
+ emoji, err := d.state.DB.GetEmojiByShortcodeDomain(ctx,
+ shortcode,
+ domain,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return nil, gtserror.Newf("error fetching emoji from db: %w", err)
+ }
+
+ if emoji != nil {
+ // This was an existing emoji, pass to refresh func.
+ return d.RefreshEmoji(ctx, emoji, info, refresh)
+ }
+
+ if domain == "" {
+ // failed local lookup, will be db.ErrNoEntries.
+ return nil, gtserror.SetUnretrievable(err)
+ }
+
+ // Generate shortcode domain for locks + logging.
+ shortcodeDomain := shortcode + "@" + domain
+
+ // Ensure we have a valid remote URL.
+ url, err := url.Parse(remoteURL)
+ if err != nil {
+ err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", remoteURL, shortcodeDomain, err)
+ return nil, err
+ }
+
+ // Acquire new instance account transport for emoji dereferencing.
+ tsport, err := d.transportController.NewTransportForUsername(ctx, "")
+ if err != nil {
+ err := gtserror.Newf("error getting instance transport: %w", err)
+ return nil, err
+ }
+
+ // Prepare data function to dereference remote emoji media.
+ data := func(context.Context) (io.ReadCloser, int64, error) {
+ return tsport.DereferenceMedia(ctx, url)
+ }
+
+ // Pass along for safe processing.
+ return d.processEmojiSafely(ctx,
+ shortcodeDomain,
+ func() (*media.ProcessingEmoji, error) {
+ return d.mediaManager.CreateEmoji(ctx,
+ shortcode,
+ domain,
+ data,
+ info,
+ )
+ },
+ )
+}
+
+// RefreshEmoji ensures that the given emoji is
+// up-to-date, both in terms of being cached in
+// in local instance storage, and compared to extra
+// information provided in media.AdditionEmojiInfo{}.
+// (note that is a no-op to pass in a local emoji).
+//
+// Please note that even if an error is returned,
+// an emoji model may still be returned if the error
+// was only encountered during actual dereferencing.
+// In this case, it will act as a placeholder.
+func (d *Dereferencer) RefreshEmoji(
+ ctx context.Context,
+ emoji *gtsmodel.Emoji,
+ info media.AdditionalEmojiInfo,
+ force bool,
+) (
+ *gtsmodel.Emoji,
+ error,
+) {
+ // Can't refresh local.
+ if emoji.IsLocal() {
+ return emoji, nil
+ }
+
+ // Check emoji is up-to-date
+ // with provided extra info.
+ switch {
+ case info.URI != nil &&
+ *info.URI != emoji.URI:
+ force = true
+ case info.ImageRemoteURL != nil &&
+ *info.ImageRemoteURL != emoji.ImageRemoteURL:
+ force = true
+ case info.ImageStaticRemoteURL != nil &&
+ *info.ImageStaticRemoteURL != emoji.ImageStaticRemoteURL:
+ force = true
+ }
+
+ // Check if needs updating.
+ if !force && *emoji.Cached {
+ return emoji, nil
+ }
+
+ // TODO: more finegrained freshness checks.
- // Ensure we have been passed a valid URL.
- derefURI, err := url.Parse(remoteURL)
+ // Generate shortcode domain for locks + logging.
+ shortcodeDomain := emoji.Shortcode + "@" + emoji.Domain
+
+ // Ensure we have a valid image remote URL.
+ url, err := url.Parse(emoji.ImageRemoteURL)
if err != nil {
- return nil, fmt.Errorf("GetRemoteEmoji: error parsing url for emoji %s: %s", shortcodeDomain, err)
+ err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", emoji.ImageRemoteURL, shortcodeDomain, err)
+ return nil, err
}
- // Acquire derefs lock.
+ // Acquire new instance account transport for emoji dereferencing.
+ tsport, err := d.transportController.NewTransportForUsername(ctx, "")
+ if err != nil {
+ err := gtserror.Newf("error getting instance transport: %w", err)
+ return nil, err
+ }
+
+ // Prepare data function to dereference remote emoji media.
+ data := func(context.Context) (io.ReadCloser, int64, error) {
+ return tsport.DereferenceMedia(ctx, url)
+ }
+
+ // Pass along for safe processing.
+ return d.processEmojiSafely(ctx,
+ shortcodeDomain,
+ func() (*media.ProcessingEmoji, error) {
+ return d.mediaManager.RefreshEmoji(ctx,
+ emoji,
+ data,
+ info,
+ )
+ },
+ )
+}
+
+// processingEmojiSafely provides concurrency-safe processing of
+// an emoji with given shortcode+domain. if a copy of the emoji is
+// not already being processed, the given 'process' callback will
+// be used to generate new *media.ProcessingEmoji{} instance.
+func (d *Dereferencer) processEmojiSafely(
+ ctx context.Context,
+ shortcodeDomain string,
+ process func() (*media.ProcessingEmoji, error),
+) (
+ emoji *gtsmodel.Emoji,
+ err error,
+) {
+
+ // Acquire map lock.
d.derefEmojisMu.Lock()
// Ensure unlock only done once.
@@ -53,146 +214,118 @@ func (d *Dereferencer) GetRemoteEmoji(ctx context.Context, requestUser string, r
processing, ok := d.derefEmojis[shortcodeDomain]
if !ok {
- // Fetch a transport for current request user in order to perform request.
- tsport, err := d.transportController.NewTransportForUsername(ctx, requestUser)
+ // Start new processing emoji.
+ processing, err = process()
if err != nil {
- return nil, gtserror.Newf("couldn't create transport: %w", err)
+ return nil, err
}
-
- // Set the media data function to dereference emoji from URI.
- data := func(ctx context.Context) (io.ReadCloser, int64, error) {
- return tsport.DereferenceMedia(ctx, derefURI)
- }
-
- // Create new emoji processing request from the media manager.
- processing, err = d.mediaManager.PreProcessEmoji(ctx, data,
- shortcode,
- id,
- emojiURI,
- ai,
- refresh,
- )
- if err != nil {
- return nil, gtserror.Newf("error preprocessing emoji %s: %s", shortcodeDomain, err)
- }
-
- // Store media in map to mark as processing.
- d.derefEmojis[shortcodeDomain] = processing
-
- defer func() {
- // On exit safely remove emoji from map.
- d.derefEmojisMu.Lock()
- delete(d.derefEmojis, shortcodeDomain)
- d.derefEmojisMu.Unlock()
- }()
}
// Unlock map.
unlock()
- // Start emoji attachment loading (blocking call).
- if _, err := processing.LoadEmoji(ctx); err != nil {
- return nil, err
+ // Perform emoji load operation.
+ emoji, err = processing.Load(ctx)
+ if err != nil {
+ err = gtserror.Newf("error loading emoji %s: %w", shortcodeDomain, err)
+
+ // TODO: in time we should return checkable flags by gtserror.Is___()
+ // which can determine if loading error should allow remaining placeholder.
}
- return processing, nil
+ // Return a COPY of emoji.
+ emoji2 := new(gtsmodel.Emoji)
+ *emoji2 = *emoji
+ return emoji2, err
}
-func (d *Dereferencer) populateEmojis(ctx context.Context, rawEmojis []*gtsmodel.Emoji, requestingUsername string) ([]*gtsmodel.Emoji, 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(rawEmojis))
-
- for _, e := range rawEmojis {
- var gotEmoji *gtsmodel.Emoji
- var err error
- shortcodeDomain := e.Shortcode + "@" + e.Domain
-
- // check if we already know this emoji
- if e.ID != "" {
- // we had an ID for this emoji already, which means
- // it should be fleshed out already and we won't
- // have to get it from the database again
- gotEmoji = e
- } else if gotEmoji, err = d.state.DB.GetEmojiByShortcodeDomain(ctx, e.Shortcode, e.Domain); err != nil && err != db.ErrNoEntries {
- log.Errorf(ctx, "error checking database for emoji %s: %s", shortcodeDomain, err)
- continue
- }
-
- var refresh bool
+func (d *Dereferencer) fetchEmojis(
+ ctx context.Context,
+ existing []*gtsmodel.Emoji,
+ emojis []*gtsmodel.Emoji, // newly dereferenced
+) (
+ []*gtsmodel.Emoji,
+ bool, // any changes?
+ error,
+) {
+ // Track any changes.
+ changed := false
- if gotEmoji != nil {
- // we had the emoji already, but refresh it if necessary
- if e.UpdatedAt.Unix() > gotEmoji.ImageUpdatedAt.Unix() {
- log.Tracef(ctx, "emoji %s was updated since we last saw it, will refresh", shortcodeDomain)
- refresh = true
- }
+ for i, placeholder := range emojis {
+ // Look for an existing emoji with shortcode + domain.
+ existing, ok := getEmojiByShortcodeDomain(existing,
+ placeholder.Shortcode,
+ placeholder.Domain,
+ )
+ if ok && existing.ID != "" {
- if !refresh && (e.URI != gotEmoji.URI) {
- log.Tracef(ctx, "emoji %s changed URI since we last saw it, will refresh", shortcodeDomain)
- refresh = true
- }
+ // Check for any emoji changes that
+ // indicate we should force a refresh.
+ force := emojiChanged(existing, placeholder)
- if !refresh && (e.ImageRemoteURL != gotEmoji.ImageRemoteURL) {
- log.Tracef(ctx, "emoji %s changed image URL since we last saw it, will refresh", shortcodeDomain)
- refresh = true
- }
+ // Ensure that the existing emoji model is up-to-date and cached.
+ existing, err := d.RefreshEmoji(ctx, existing, media.AdditionalEmojiInfo{
- if !refresh {
- log.Tracef(ctx, "emoji %s is up to date, will not refresh", shortcodeDomain)
- } else {
- log.Tracef(ctx, "refreshing emoji %s", shortcodeDomain)
- emojiID := gotEmoji.ID // use existing ID
- processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, e.Domain, emojiID, e.URI, &media.AdditionalEmojiInfo{
- Domain: &e.Domain,
- ImageRemoteURL: &e.ImageRemoteURL,
- ImageStaticRemoteURL: &e.ImageStaticRemoteURL,
- Disabled: gotEmoji.Disabled,
- VisibleInPicker: gotEmoji.VisibleInPicker,
- }, refresh)
- if err != nil {
- log.Errorf(ctx, "couldn't refresh remote emoji %s: %s", shortcodeDomain, err)
- continue
- }
-
- if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
- log.Errorf(ctx, "couldn't load refreshed remote emoji %s: %s", shortcodeDomain, err)
- continue
- }
- }
- } else {
- // it's new! go get it!
- newEmojiID, err := id.NewRandomULID()
+ // Set latest values from placeholder.
+ URI: &placeholder.URI,
+ ImageRemoteURL: &placeholder.ImageRemoteURL,
+ ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
+ }, force)
if err != nil {
- log.Errorf(ctx, "error generating id for remote emoji %s: %s", shortcodeDomain, err)
- continue
- }
+ log.Errorf(ctx, "error refreshing emoji: %v", err)
- processingEmoji, err := d.GetRemoteEmoji(ctx, requestingUsername, e.ImageRemoteURL, e.Shortcode, e.Domain, newEmojiID, e.URI, &media.AdditionalEmojiInfo{
- Domain: &e.Domain,
- ImageRemoteURL: &e.ImageRemoteURL,
- ImageStaticRemoteURL: &e.ImageStaticRemoteURL,
- Disabled: e.Disabled,
- VisibleInPicker: e.VisibleInPicker,
- }, refresh)
- if err != nil {
- log.Errorf(ctx, "couldn't get remote emoji %s: %s", shortcodeDomain, err)
- continue
+ // specifically do NOT continue here,
+ // we already have a model, we don't
+ // want to drop it from the slice, just
+ // log that an update for it failed.
}
- if gotEmoji, err = processingEmoji.LoadEmoji(ctx); err != nil {
- log.Errorf(ctx, "couldn't load remote emoji %s: %s", shortcodeDomain, err)
+ // Set existing emoji.
+ emojis[i] = existing
+ continue
+ }
+
+ // Emojis changed!
+ changed = true
+
+ // Fetch this newly added emoji,
+ // this function handles the case
+ // of existing cached emojis and
+ // new ones requiring dereference.
+ emoji, err := d.GetEmoji(ctx,
+ placeholder.Shortcode,
+ placeholder.Domain,
+ placeholder.ImageRemoteURL,
+ media.AdditionalEmojiInfo{
+ URI: &placeholder.URI,
+ ImageRemoteURL: &placeholder.ImageRemoteURL,
+ ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL,
+ },
+ false,
+ )
+ if err != nil {
+ if emoji == nil {
+ log.Errorf(ctx, "error loading emoji %s: %v", placeholder.ImageRemoteURL, err)
continue
}
+
+ // non-fatal error occurred during loading, still use it.
+ log.Warnf(ctx, "partially loaded emoji: %v", err)
}
- // if we get here, we either had the emoji already or we successfully fetched it
- gotEmojis = append(gotEmojis, gotEmoji)
+ // Set updated emoji.
+ emojis[i] = emoji
+ }
+
+ for i := 0; i < len(emojis); {
+ if emojis[i].ID == "" {
+ // Remove failed emoji populations.
+ copy(emojis[i:], emojis[i+1:])
+ emojis = emojis[:len(emojis)-1]
+ continue
+ }
+ i++
}
- return gotEmojis, nil
+ return emojis, changed, nil
}