// GoToSocial // Copyright (C) GoToSocial Authors admin@gotosocial.org // SPDX-License-Identifier: AGPL-3.0-or-later // // 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 . package dereferencing import ( "context" "errors" "io" "net/url" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/util" ) // 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 // Pass along for safe processing. return d.processEmojiSafely(ctx, shortcodeDomain, func() (*media.ProcessingEmoji, error) { // 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 } // Get maximum supported remote emoji size. maxsz := int64(config.GetMediaEmojiRemoteMaxSize()) // #nosec G115 -- Already validated. // Prepare data function to dereference remote emoji media. data := func(context.Context) (io.ReadCloser, error) { return tsport.DereferenceMedia(ctx, url, maxsz) } // Create new emoji with prepared info. 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, ) { // Check emoji is up-to-date // with provided extra info. switch { case info.URI != nil && *info.URI != emoji.URI: emoji.URI = *info.URI force = true case info.ImageRemoteURL != nil && *info.ImageRemoteURL != emoji.ImageRemoteURL: emoji.ImageRemoteURL = *info.ImageRemoteURL force = true case info.ImageStaticRemoteURL != nil && *info.ImageStaticRemoteURL != emoji.ImageStaticRemoteURL: emoji.ImageStaticRemoteURL = *info.ImageStaticRemoteURL force = true } // Check if needs // force refresh. if !force { // We still want to make sure // the emoji is cached. Simply // check whether emoji is cached. return d.RecacheEmoji(ctx, emoji) } // Can't refresh local. if emoji.IsLocal() { return emoji, nil } // Get shortcode domain for locks + logging. shortcodeDomain := emoji.ShortcodeDomain() // Ensure we have a valid image remote URL. url, err := url.Parse(emoji.ImageRemoteURL) if err != nil { err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", emoji.ImageRemoteURL, shortcodeDomain, err) return nil, err } // Pass along for safe processing. return d.processEmojiSafely(ctx, shortcodeDomain, func() (*media.ProcessingEmoji, error) { // 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 } // Get maximum supported remote emoji size. maxsz := int64(config.GetMediaEmojiRemoteMaxSize()) // #nosec G115 -- Already validated. // Prepare data function to dereference remote emoji media. data := func(context.Context) (io.ReadCloser, error) { return tsport.DereferenceMedia(ctx, url, maxsz) } // Update emoji with prepared info. return d.mediaManager.UpdateEmoji(ctx, emoji, data, info, ) }, ) } // RecacheEmoji handles the simplest case which is that // of an existing emoji that only needs to be recached. // It handles the case of both local emojis, and those // already cached as no-ops. // // 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) RecacheEmoji( ctx context.Context, emoji *gtsmodel.Emoji, ) ( *gtsmodel.Emoji, error, ) { // Can't recache local. if emoji.IsLocal() { return emoji, nil } if *emoji.Cached { // Already cached. return emoji, nil } // Get shortcode domain for locks + logging. shortcodeDomain := emoji.ShortcodeDomain() // Ensure we have a valid image remote URL. url, err := url.Parse(emoji.ImageRemoteURL) if err != nil { err := gtserror.Newf("invalid image remote url %s for emoji %s: %w", emoji.ImageRemoteURL, shortcodeDomain, err) return nil, err } // Pass along for safe processing. return d.processEmojiSafely(ctx, shortcodeDomain, func() (*media.ProcessingEmoji, error) { // 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 } // Get maximum supported remote emoji size. maxsz := int64(config.GetMediaEmojiRemoteMaxSize()) // #nosec G115 -- Already validated. // Prepare data function to dereference remote emoji media. data := func(context.Context) (io.ReadCloser, error) { return tsport.DereferenceMedia(ctx, url, maxsz) } // Recache emoji with prepared info. return d.mediaManager.CacheEmoji(ctx, emoji, data, ) }, ) } // 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. unlock := d.derefEmojisMu.Unlock unlock = util.DoOnce(unlock) defer unlock() // Look for an existing dereference in progress. processing, ok := d.derefEmojis[shortcodeDomain] if !ok { // Start new processing emoji. processing, err = process() if err != nil { return nil, err } // Add processing emoji media to hash map. d.derefEmojis[shortcodeDomain] = processing defer func() { // Remove on finish. d.derefEmojisMu.Lock() delete(d.derefEmojis, shortcodeDomain) d.derefEmojisMu.Unlock() }() } // Unlock map. unlock() // 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 } 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 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 != "" { // Check for any emoji changes that // indicate we should force a refresh. force := emojiChanged(existing, placeholder) // Ensure that the existing emoji model is up-to-date and cached. existing, err := d.RefreshEmoji(ctx, existing, media.AdditionalEmojiInfo{ // Set latest values from placeholder. URI: &placeholder.URI, ImageRemoteURL: &placeholder.ImageRemoteURL, ImageStaticRemoteURL: &placeholder.ImageStaticRemoteURL, }, force) if err != nil { log.Errorf(ctx, "error refreshing emoji: %v", err) // 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. } // 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) } // 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 emojis, changed, nil }