diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go
index 8d237c841..01538f5ab 100644
--- a/internal/federation/dereferencing/status.go
+++ b/internal/federation/dereferencing/status.go
@@ -670,9 +670,9 @@ func (d *Dereferencer) fetchStatusMentions(
alreadyExists bool
)
- // Search existing status for a mention already stored,
+ // Search existing status + db for a mention already stored,
// else ensure new mention's target account is populated.
- mention, alreadyExists, err = d.populateMentionTarget(ctx,
+ mention, alreadyExists, err = d.newOrExistingMention(ctx,
requestUser,
existing,
mention,
@@ -683,8 +683,8 @@ func (d *Dereferencer) fetchStatusMentions(
}
if alreadyExists {
- // This mention was already attached
- // to the status, use it and continue.
+ // This mention was already
+ // stored, use it and continue.
status.Mentions[i] = mention
status.MentionIDs[i] = mention.ID
continue
@@ -1294,14 +1294,15 @@ func (d *Dereferencer) handleStatusEdit(
return cols, nil
}
-// populateMentionTarget tries to populate the given
+// newOrExistingMention 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`.
+// Will check on the existing status and in the db
+// if the mention is already there and populated;
+// if so, existing mention will be returned along
+// with `true` to indicate that it already existed.
//
// Otherwise, this function will try to parse first
// the Href of the mention, and then the namestring,
@@ -1312,7 +1313,7 @@ func (d *Dereferencer) handleStatusEdit(
// rather than a URI, but because some remotes do
// silly things like only provide `@username` instead
// of `@username@domain`, we try by URI first.
-func (d *Dereferencer) populateMentionTarget(
+func (d *Dereferencer) newOrExistingMention(
ctx context.Context,
requestUser string,
existing *gtsmodel.Status,
@@ -1325,11 +1326,13 @@ func (d *Dereferencer) populateMentionTarget(
// Mentions can be created using `name` or `href`.
//
// Prefer `href` (TargetAccountURI), fall back to Name.
- if mention.TargetAccountURI != "" {
-
- // Look for existing mention with target account's URI, if so use this.
+ switch {
+ case mention.TargetAccountURI != "":
+ // Look on the status for existing mention with target account's URI.
existingMention, ok := existing.GetMentionByTargetURI(mention.TargetAccountURI)
if ok && existingMention.ID != "" {
+ // Already populated
+ // mention, use this.
return existingMention, true, nil
}
@@ -1354,8 +1357,25 @@ func (d *Dereferencer) populateMentionTarget(
err := gtserror.Newf("failed to dereference account %s: %w", targetAccountURI, err)
return nil, false, err
}
- } else {
+ // Look in the db for this existing mention.
+ existingMention, err = d.state.DB.GetMentionByTargetAcctStatus(
+ ctx,
+ mention.TargetAccount.ID,
+ existing.ID,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error looking for existing mention: %w", err)
+ return nil, false, err
+ }
+
+ if existingMention != nil {
+ // Already had stored
+ // mention, use this.
+ return existingMention, true, nil
+ }
+
+ case mention.NameString != "":
// Href wasn't set, extract the username and domain parts from namestring.
username, domain, err := util.ExtractNamestringParts(mention.NameString)
if err != nil {
@@ -1363,9 +1383,11 @@ func (d *Dereferencer) populateMentionTarget(
return nil, false, err
}
- // Look for existing mention with username domain target, if so use this.
+ // Look on the status for existing mention with username domain target.
existingMention, ok := existing.GetMentionByUsernameDomain(username, domain)
if ok && existingMention.ID != "" {
+ // Already populated
+ // mention, use this.
return existingMention, true, nil
}
@@ -1395,11 +1417,34 @@ func (d *Dereferencer) populateMentionTarget(
return nil, false, err
}
- // Look for existing mention with target account's URI, if so use this.
+ // Look on the status for existing mention with target account's URI.
existingMention, ok = existing.GetMentionByTargetURI(mention.TargetAccountURI)
if ok && existingMention.ID != "" {
+ // Already populated
+ // mention, use this.
return existingMention, true, nil
}
+
+ // Look in the db for this existing mention.
+ existingMention, err = d.state.DB.GetMentionByTargetAcctStatus(
+ ctx,
+ mention.TargetAccount.ID,
+ existing.ID,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error looking for existing mention: %w", err)
+ return nil, false, err
+ }
+
+ if existingMention != nil {
+ // Already had stored
+ // mention, use this.
+ return existingMention, true, nil
+ }
+
+ default:
+ const errText = "neither target uri nor namestring were set on mention, cannot process it"
+ return nil, false, gtserror.New(errText)
}
// At this point, mention.TargetAccountURI
diff --git a/internal/federation/federatingdb/update_test.go b/internal/federation/federatingdb/update_test.go
new file mode 100644
index 000000000..3c2d54fc7
--- /dev/null
+++ b/internal/federation/federatingdb/update_test.go
@@ -0,0 +1,76 @@
+// 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
hi @admin here's some media for ya, @the_mighty_zork you might like this too
`, + "some unknown media included
", + URLMustParse("http://example.org/users/Some_User"), + []*url.URL{ + ap.PublicURI(), + }, + []*url.URL{ + URLMustParse("http://example.org/users/Some_User/followers"), + URLMustParse("http://localhost:8080/users/admin"), + URLMustParse("http://localhost:8080/users/the_mighty_zork"), + }, + true, + []vocab.ActivityStreamsMention{ + newAPMention( + URLMustParse("http://localhost:8080/users/admin"), + "@admin@localhost:8080", + ), + newAPMention( + URLMustParse("http://localhost:8080/users/the_mighty_zork"), + "@the_mighty_zork@localhost:8080", + ), + }, + nil, + nil, + ) + update := WrapAPNoteInUpdate( + URLMustParse("http://example.org/users/Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5/update1"), + URLMustParse("http://example.org/users/Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"), + URLMustParse("http://example.org/users/Some_User"), + TimeMustParse("2023-11-02T12:46:25+02:00"), + remoteAccount2Status1Updated, + ) + updateSig, updateDigest, updateDate := GetSignatureForActivity( + update, + accounts["remote_account_2"].PublicKeyURI, + accounts["remote_account_2"].PrivateKey, + URLMustParse(accounts["local_account_1"].InboxURI), + ) + return map[string]ActivityWithSignature{ "dm_for_zork": { Activity: createDmForZork, @@ -3563,6 +3606,12 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit DigestHeader: deleteForRemoteAccount3Digest, DateHeader: deleteForRemoteAccount3Date, }, + "remote_account_2_status_1_update": { + Activity: update, + SignatureHeader: updateSig, + DigestHeader: updateDigest, + DateHeader: updateDate, + }, } } @@ -5186,6 +5235,27 @@ func WrapAPNoteInCreate(createID *url.URL, createActor *url.URL, createPublished return create } +func WrapAPNoteInUpdate( + updateID *url.URL, + updateTarget *url.URL, + updateActor *url.URL, + updatePublished time.Time, + updateNote vocab.ActivityStreamsNote, +) vocab.ActivityStreamsUpdate { + update := streams.NewActivityStreamsUpdate() + + ap.SetJSONLDId(update, updateID) + ap.AppendTargetIRIs(update, updateTarget) + ap.AppendActorIRIs(update, updateActor) + ap.SetPublished(update, updatePublished) + + objectProp := streams.NewActivityStreamsObjectProperty() + objectProp.AppendActivityStreamsNote(updateNote) + update.SetActivityStreamsObject(objectProp) + + return update +} + func newAPAnnounce(announceID *url.URL, announceActor *url.URL, announcePublished time.Time, announceTo *url.URL, announceNote vocab.ActivityStreamsNote) vocab.ActivityStreamsAnnounce { announce := streams.NewActivityStreamsAnnounce()