summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/federation/dereferencing/status.go75
-rw-r--r--internal/federation/federatingdb/update_test.go76
-rw-r--r--internal/processing/workers/fromfediapi.go10
-rw-r--r--internal/processing/workers/fromfediapi_test.go46
-rw-r--r--testrig/testmodels.go70
5 files changed, 257 insertions, 20 deletions
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 <http://www.gnu.org/licenses/>.
+
+package federatingdb_test
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+ "time"
+
+ "code.superseriousbusiness.org/gotosocial/internal/ap"
+ "code.superseriousbusiness.org/gotosocial/internal/gtscontext"
+ "github.com/stretchr/testify/suite"
+)
+
+type UpdateTestSuite struct {
+ FederatingDBTestSuite
+}
+
+func (suite *UpdateTestSuite) TestUpdateNewMention() {
+ var (
+ ctx = context.Background()
+ update = suite.testActivities["remote_account_2_status_1_update"]
+ receivingAcct = suite.testAccounts["local_account_1"]
+ requestingAcct = suite.testAccounts["remote_account_2"]
+ )
+
+ ctx = gtscontext.SetReceivingAccount(ctx, receivingAcct)
+ ctx = gtscontext.SetRequestingAccount(ctx, requestingAcct)
+
+ m, err := ap.Serialize(update.Activity)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ b, err := json.MarshalIndent(&m, "", " ")
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.T().Logf("Update:\n%s\n", string(b))
+
+ note := update.Activity.GetActivityStreamsObject().At(0).GetActivityStreamsNote()
+ if err := suite.federatingDB.Update(ctx, note); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Should be a message heading to the processor.
+ msg, ok := suite.getFederatorMsg(5 * time.Second)
+ if !ok {
+ suite.FailNow("no federator message after 5s")
+ }
+
+ suite.Equal(ap.ObjectNote, msg.APObjectType)
+ suite.Equal(ap.ActivityUpdate, msg.APActivityType)
+ suite.NotNil(msg.APObject)
+}
+
+func TestUpdateTestSuite(t *testing.T) {
+ suite.Run(t, new(UpdateTestSuite))
+}
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 93b12d89c..86d868530 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -991,11 +991,6 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
}
}
- // Push message that the status has been edited to streams.
- if err := p.surface.timelineStatusUpdate(ctx, status); err != nil {
- log.Errorf(ctx, "error streaming status edit: %v", err)
- }
-
// Notify any *new* mentions added
// to this status by the editor.
for _, mention := range status.Mentions {
@@ -1015,6 +1010,11 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
}
}
+ // Push message that the status has been edited to streams.
+ if err := p.surface.timelineStatusUpdate(ctx, status); err != nil {
+ log.Errorf(ctx, "error streaming status edit: %v", err)
+ }
+
// Status representation changed, uncache from timelines.
p.surface.invalidateStatusFromTimelines(status.ID)
diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go
index 9fe5cb0f4..203863e12 100644
--- a/internal/processing/workers/fromfediapi_test.go
+++ b/internal/processing/workers/fromfediapi_test.go
@@ -735,6 +735,52 @@ func (suite *FromFediAPITestSuite) TestUndoAnnounce() {
}
}
+func (suite *FromFediAPITestSuite) TestUpdateNote() {
+ var (
+ ctx = context.Background()
+ testStructs = testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ requestingAcct = suite.testAccounts["remote_account_2"]
+ receivingAcct = suite.testAccounts["local_account_1"]
+ )
+ defer testrig.TearDownTestStructs(testStructs)
+
+ update := testrig.NewTestActivities(suite.testAccounts)["remote_account_2_status_1_update"]
+ statusable := update.Activity.GetActivityStreamsObject().At(0).GetActivityStreamsNote()
+ noteURI := ap.GetJSONLDId(statusable)
+
+ // Get the OG status.
+ status, err := testStructs.State.DB.GetStatusByURI(ctx, noteURI.String())
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the Update.
+ err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityUpdate,
+ GTSModel: status, // original status
+ APObject: (ap.Statusable)(statusable),
+ Receiving: receivingAcct,
+ Requesting: requestingAcct,
+ })
+ suite.NoError(err)
+
+ // Wait for side effects to trigger:
+ // zork should have a mention notif.
+ if !testrig.WaitFor(func() bool {
+ _, err := testStructs.State.DB.GetNotification(
+ gtscontext.SetBarebones(ctx),
+ gtsmodel.NotificationMention,
+ receivingAcct.ID,
+ requestingAcct.ID,
+ status.ID,
+ )
+ return err == nil
+ }) {
+ suite.FailNow("timed out waiting for mention notif")
+ }
+}
+
func TestFromFederatorTestSuite(t *testing.T) {
suite.Run(t, &FromFediAPITestSuite{})
}
diff --git a/testrig/testmodels.go b/testrig/testmodels.go
index 37d4ecf84..5f0c2f032 100644
--- a/testrig/testmodels.go
+++ b/testrig/testmodels.go
@@ -3514,6 +3514,49 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit
keyToSignDelete := accounts["remote_account_1"].PrivateKey
deleteForRemoteAccount3Sig, deleteForRemoteAccount3Digest, deleteForRemoteAccount3Date := GetSignatureForActivity(deleteForRemoteAccount3, "https://somewhere.mysterious/users/rest_in_piss#main-key", keyToSignDelete, URLMustParse(accounts["local_account_1"].InboxURI))
+ remoteAccount2Status1Updated := NewAPNote(
+ URLMustParse("http://example.org/users/Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"),
+ URLMustParse("http://example.org/@Some_User/statuses/01HE7XJ1CG84TBKH5V9XKBVGF5"),
+ TimeMustParse("2023-11-02T12:44:25+02:00"),
+ `<p>hi <span class="h-card"><a href="http://localhost:8080/@admin" class="u-url mention" rel="nofollow noreferrer noopener" target="_blank">@<span>admin</span></a></span> here's some media for ya, <span class="h-card"><a href="http://localhost:8080/@the_mighty_zork" class="u-url mention" rel="nofollow noreferrer noopener" target="_blank">@<span>the_mighty_zork</span></a></span> you might like this too</p>`,
+ "<p>some unknown media included</p>",
+ 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()