[chore] media and emoji refactoring (#3000)

* start updating media manager interface ready for storing attachments / emoji right away

* store emoji and media as uncached immediately, then (re-)cache on Processing{}.Load()

* remove now unused media workers

* fix tests and issues

* fix another test!

* fix emoji activitypub uri setting behaviour, fix remainder of test compilation issues

* fix more tests

* fix (most of) remaining tests, add debouncing to repeatedly failing media / emojis

* whoops, rebase issue

* remove kim's whacky experiments

* do some reshuffling, ensure emoji uri gets set

* ensure marked as not cached on cleanup

* tweaks to media / emoji processing to handle context canceled better

* ensure newly fetched emojis actually get set in returned slice

* use different varnames to be a bit more obvious

* move emoji refresh rate limiting to dereferencer

* add exported dereferencer functions for remote media, use these for recaching in processor

* add check for nil attachment in updateAttachment()

* remove unused emoji and media fields + columns

* see previous commit

* fix old migrations expecting image_updated_at to exists (from copies of old models)

* remove freshness checking code (seems to be broken...)

* fix error arg causing nil ptr exception

* finish documentating functions with comments, slight tweaks to media / emoji deref error logic

* remove some extra unneeded boolean checking

* finish writing documentation (code comments) for exported media manager methods

* undo changes to migration snapshot gtsmodels, updated failing migration to have its own snapshot

* move doesColumnExist() to util.go in migrations package
This commit is contained in:
kim 2024-06-26 15:01:16 +00:00 committed by GitHub
commit 21bb324156
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
48 changed files with 2578 additions and 1926 deletions

View file

@ -33,7 +33,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@ -536,12 +535,12 @@ func (d *Dereferencer) enrichStatus(
}
// Ensure the status' media attachments are populated, passing in existing to check for changes.
if err := d.fetchStatusAttachments(ctx, tsport, status, latestStatus); err != nil {
if err := d.fetchStatusAttachments(ctx, requestUser, status, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating attachments for status %s: %w", uri, err)
}
// Ensure the status' emoji attachments are populated, (changes are expected / okay).
if err := d.fetchStatusEmojis(ctx, requestUser, latestStatus); err != nil {
// Ensure the status' emoji attachments are populated, passing in existing to check for changes.
if err := d.fetchStatusEmojis(ctx, status, latestStatus); err != nil {
return nil, nil, gtserror.Newf("error populating emojis for status %s: %w", uri, err)
}
@ -643,79 +642,12 @@ func (d *Dereferencer) isPermittedStatus(
return onFail()
}
// populateMentionTarget tries to populate the given
// mention with the correct TargetAccount and (if not
// yet set) TargetAccountURI, returning the populated
// mention.
//
// Will check on the existing status if the mention
// is already there and populated; if so, existing
// mention will be returned along with `true`.
//
// Otherwise, this function will try to parse first
// the Href of the mention, and then the namestring,
// to see who it targets, and go fetch that account.
func (d *Dereferencer) populateMentionTarget(
func (d *Dereferencer) fetchStatusMentions(
ctx context.Context,
mention *gtsmodel.Mention,
requestUser string,
existing, status *gtsmodel.Status,
) (
*gtsmodel.Mention,
bool, // True if mention already exists in the DB.
error,
) {
// Mentions can be created using Name or Href.
// Prefer Href (TargetAccountURI), fall back to Name.
if mention.TargetAccountURI != "" {
// Look for existing mention with this URI.
// If we already have it we can return early.
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
if ok && existingMention.ID != "" {
return existingMention, true, nil
}
// Ensure that mention account URI is parseable.
accountURI, err := url.Parse(mention.TargetAccountURI)
if err != nil {
err = gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
return nil, false, err
}
// Ensure we have the account of the mention target dereferenced.
mention.TargetAccount, _, err = d.getAccountByURI(ctx, requestUser, accountURI)
if err != nil {
err = gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
return nil, false, err
}
} else {
// Href wasn't set. Find the target account using namestring.
username, domain, err := util.ExtractNamestringParts(mention.NameString)
if err != nil {
err = gtserror.Newf("failed to parse namestring %s: %w", mention.NameString, err)
return nil, false, err
}
mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx, requestUser, username, domain)
if err != nil {
err = gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
return nil, false, err
}
// Look for existing mention with this URI.
mention.TargetAccountURI = mention.TargetAccount.URI
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
if ok && existingMention.ID != "" {
return existingMention, true, nil
}
}
// At this point, mention.TargetAccountURI
// and mention.TargetAccount must be set.
return mention, false, nil
}
func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser string, existing, status *gtsmodel.Status) error {
existing *gtsmodel.Status,
status *gtsmodel.Status,
) error {
// Allocate new slice to take the yet-to-be created mention IDs.
status.MentionIDs = make([]string, len(status.Mentions))
@ -728,10 +660,10 @@ func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser stri
mention, alreadyExists, err = d.populateMentionTarget(
ctx,
mention,
requestUser,
existing,
status,
mention,
)
if err != nil {
log.Errorf(ctx, "failed to derive mention: %v", err)
@ -845,7 +777,11 @@ func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status
return nil
}
func (d *Dereferencer) fetchStatusTags(ctx context.Context, existing, status *gtsmodel.Status) error {
func (d *Dereferencer) fetchStatusTags(
ctx context.Context,
existing *gtsmodel.Status,
status *gtsmodel.Status,
) error {
// Allocate new slice to take the yet-to-be determined tag IDs.
status.TagIDs = make([]string, len(status.Tags))
@ -900,7 +836,11 @@ func (d *Dereferencer) fetchStatusTags(ctx context.Context, existing, status *gt
return nil
}
func (d *Dereferencer) fetchStatusPoll(ctx context.Context, existing, status *gtsmodel.Status) error {
func (d *Dereferencer) fetchStatusPoll(
ctx context.Context,
existing *gtsmodel.Status,
status *gtsmodel.Status,
) error {
var (
// insertStatusPoll generates ID and inserts the poll attached to status into the database.
insertStatusPoll = func(ctx context.Context, status *gtsmodel.Status) error {
@ -990,19 +930,24 @@ func (d *Dereferencer) fetchStatusPoll(ctx context.Context, existing, status *gt
}
}
func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transport.Transport, existing, status *gtsmodel.Status) error {
func (d *Dereferencer) fetchStatusAttachments(
ctx context.Context,
requestUser string,
existing *gtsmodel.Status,
status *gtsmodel.Status,
) error {
// Allocate new slice to take the yet-to-be fetched attachment IDs.
status.AttachmentIDs = make([]string, len(status.Attachments))
for i := range status.Attachments {
attachment := status.Attachments[i]
placeholder := status.Attachments[i]
// Look for existing media attachment with remote URL first.
existing, ok := existing.GetAttachmentByRemoteURL(attachment.RemoteURL)
existing, ok := existing.GetAttachmentByRemoteURL(placeholder.RemoteURL)
if ok && existing.ID != "" {
// Ensure the existing media attachment is up-to-date and cached.
existing, err := d.updateAttachment(ctx, tsport, existing, attachment)
existing, err := d.updateAttachment(ctx, requestUser, existing, placeholder)
if err != nil {
log.Errorf(ctx, "error updating existing attachment: %v", err)
@ -1019,25 +964,25 @@ func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transp
}
// Load this new media attachment.
attachment, err := d.loadAttachment(
attachment, err := d.GetMedia(
ctx,
tsport,
requestUser,
status.AccountID,
attachment.RemoteURL,
&media.AdditionalMediaInfo{
placeholder.RemoteURL,
media.AdditionalMediaInfo{
StatusID: &status.ID,
RemoteURL: &attachment.RemoteURL,
Description: &attachment.Description,
Blurhash: &attachment.Blurhash,
RemoteURL: &placeholder.RemoteURL,
Description: &placeholder.Description,
Blurhash: &placeholder.Blurhash,
},
)
if err != nil && attachment == nil {
log.Errorf(ctx, "error loading attachment: %v", err)
continue
}
if err != nil {
// A non-fatal error occurred during loading.
if attachment == nil {
log.Errorf(ctx, "error loading attachment %s: %v", placeholder.RemoteURL, err)
continue
}
// non-fatal error occurred during loading, still use it.
log.Warnf(ctx, "partially loaded attachment: %v", err)
}
@ -1061,22 +1006,108 @@ func (d *Dereferencer) fetchStatusAttachments(ctx context.Context, tsport transp
return nil
}
func (d *Dereferencer) fetchStatusEmojis(ctx context.Context, requestUser string, status *gtsmodel.Status) error {
// Fetch the full-fleshed-out emoji objects for our status.
emojis, err := d.populateEmojis(ctx, status.Emojis, requestUser)
func (d *Dereferencer) fetchStatusEmojis(
ctx context.Context,
existing *gtsmodel.Status,
status *gtsmodel.Status,
) error {
// Fetch the updated emojis for our status.
emojis, changed, err := d.fetchEmojis(ctx,
existing.Emojis,
status.Emojis,
)
if err != nil {
return gtserror.Newf("failed to populate emojis: %w", err)
return gtserror.Newf("error fetching emojis: %w", err)
}
// Iterate over and get their IDs.
emojiIDs := make([]string, 0, len(emojis))
for _, e := range emojis {
emojiIDs = append(emojiIDs, e.ID)
if !changed {
// Use existing status emoji objects.
status.EmojiIDs = existing.EmojiIDs
status.Emojis = existing.Emojis
return nil
}
// Set known emoji details.
// Set latest emojis.
status.Emojis = emojis
status.EmojiIDs = emojiIDs
// Iterate over and set changed emoji IDs.
status.EmojiIDs = make([]string, len(emojis))
for i, emoji := range emojis {
status.EmojiIDs[i] = emoji.ID
}
return nil
}
// populateMentionTarget tries to populate the given
// mention with the correct TargetAccount and (if not
// yet set) TargetAccountURI, returning the populated
// mention.
//
// Will check on the existing status if the mention
// is already there and populated; if so, existing
// mention will be returned along with `true`.
//
// Otherwise, this function will try to parse first
// the Href of the mention, and then the namestring,
// to see who it targets, and go fetch that account.
func (d *Dereferencer) populateMentionTarget(
ctx context.Context,
requestUser string,
existing *gtsmodel.Status,
status *gtsmodel.Status,
mention *gtsmodel.Mention,
) (
*gtsmodel.Mention,
bool, // True if mention already exists in the DB.
error,
) {
// Mentions can be created using Name or Href.
// Prefer Href (TargetAccountURI), fall back to Name.
if mention.TargetAccountURI != "" {
// Look for existing mention with this URI.
// If we already have it we can return early.
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
if ok && existingMention.ID != "" {
return existingMention, true, nil
}
// Ensure that mention account URI is parseable.
accountURI, err := url.Parse(mention.TargetAccountURI)
if err != nil {
err = gtserror.Newf("invalid account uri %q: %w", mention.TargetAccountURI, err)
return nil, false, err
}
// Ensure we have the account of the mention target dereferenced.
mention.TargetAccount, _, err = d.getAccountByURI(ctx, requestUser, accountURI)
if err != nil {
err = gtserror.Newf("failed to dereference account %s: %w", accountURI, err)
return nil, false, err
}
} else {
// Href wasn't set. Find the target account using namestring.
username, domain, err := util.ExtractNamestringParts(mention.NameString)
if err != nil {
err = gtserror.Newf("failed to parse namestring %s: %w", mention.NameString, err)
return nil, false, err
}
mention.TargetAccount, _, err = d.getAccountByUsernameDomain(ctx, requestUser, username, domain)
if err != nil {
err = gtserror.Newf("failed to dereference account %s: %w", mention.NameString, err)
return nil, false, err
}
// Look for existing mention with this URI.
mention.TargetAccountURI = mention.TargetAccount.URI
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
if ok && existingMention.ID != "" {
return existingMention, true, nil
}
}
// At this point, mention.TargetAccountURI
// and mention.TargetAccount must be set.
return mention, false, nil
}