diff options
Diffstat (limited to 'internal/federation/dereferencing/emoji.go')
-rw-r--r-- | internal/federation/dereferencing/emoji.go | 383 |
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 } |