diff options
author | 2022-05-23 17:40:03 +0200 | |
---|---|---|
committer | 2022-05-23 16:40:03 +0100 | |
commit | f0c9f4169be6c5c7dd913f348cdd294a19038d63 (patch) | |
tree | 7d06da6edc0ed92c78b79938a20c7e82f784fac4 /internal/federation | |
parent | [docs] document a checklist for how to go about a release (#592) (diff) | |
download | gotosocial-f0c9f4169be6c5c7dd913f348cdd294a19038d63.tar.xz |
[bugfix] Fix multiple dereferences of boosted status causing media duplication (#589)
* add some announces to test models
* start on announce test logic
* test federatingDB.Announce
* change signature of GetRemoteStatus
* remove 'refresh' logic and replace it with refetch
* go fmt
* remove timeline manager from processor test
* make zork created at determinate
* test get account statuses
* test get + serialize zork
* make account keys determinate
* make admin accountCreate time determinate
* test account to as
* init test config before test log
* test status to frontend
* remove daft Within check
* hack around a bit
* use index of slice
Diffstat (limited to 'internal/federation')
-rw-r--r-- | internal/federation/dereference.go | 4 | ||||
-rw-r--r-- | internal/federation/dereferencing/announce.go | 2 | ||||
-rw-r--r-- | internal/federation/dereferencing/dereferencer.go | 2 | ||||
-rw-r--r-- | internal/federation/dereferencing/status.go | 85 | ||||
-rw-r--r-- | internal/federation/dereferencing/status_test.go | 12 | ||||
-rw-r--r-- | internal/federation/dereferencing/thread.go | 13 | ||||
-rw-r--r-- | internal/federation/federatingdb/announce_test.go | 93 | ||||
-rw-r--r-- | internal/federation/federatingdb/federatingdb_test.go | 3 | ||||
-rw-r--r-- | internal/federation/federator.go | 2 |
9 files changed, 141 insertions, 75 deletions
diff --git a/internal/federation/dereference.go b/internal/federation/dereference.go index 8cb23a91f..8efa0cc7e 100644 --- a/internal/federation/dereference.go +++ b/internal/federation/dereference.go @@ -30,8 +30,8 @@ func (f *federator) GetRemoteAccount(ctx context.Context, username string, remot return f.dereferencer.GetRemoteAccount(ctx, username, remoteAccountID, blocking, refresh) } -func (f *federator) GetRemoteStatus(ctx context.Context, username string, remoteStatusID *url.URL, refresh, includeParent bool) (*gtsmodel.Status, ap.Statusable, bool, error) { - return f.dereferencer.GetRemoteStatus(ctx, username, remoteStatusID, refresh, includeParent) +func (f *federator) GetRemoteStatus(ctx context.Context, username string, remoteStatusID *url.URL, refetch, includeParent bool) (*gtsmodel.Status, ap.Statusable, error) { + return f.dereferencer.GetRemoteStatus(ctx, username, remoteStatusID, refetch, includeParent) } func (f *federator) EnrichRemoteStatus(ctx context.Context, username string, status *gtsmodel.Status, includeParent bool) (*gtsmodel.Status, error) { diff --git a/internal/federation/dereferencing/announce.go b/internal/federation/dereferencing/announce.go index 7bde7d2ce..c740bb20a 100644 --- a/internal/federation/dereferencing/announce.go +++ b/internal/federation/dereferencing/announce.go @@ -46,7 +46,7 @@ func (d *deref) DereferenceAnnounce(ctx context.Context, announce *gtsmodel.Stat return fmt.Errorf("DereferenceAnnounce: error dereferencing thread of boosted status: %s", err) } - boostedStatus, _, _, err := d.GetRemoteStatus(ctx, requestingUsername, boostedStatusURI, false, true) + boostedStatus, _, err := d.GetRemoteStatus(ctx, requestingUsername, boostedStatusURI, false, true) if err != nil { return fmt.Errorf("DereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.BoostOf.URI, err) } diff --git a/internal/federation/dereferencing/dereferencer.go b/internal/federation/dereferencing/dereferencer.go index 855c4baf8..cae24d0fd 100644 --- a/internal/federation/dereferencing/dereferencer.go +++ b/internal/federation/dereferencing/dereferencer.go @@ -35,7 +35,7 @@ import ( type Dereferencer interface { GetRemoteAccount(ctx context.Context, username string, remoteAccountID *url.URL, blocking bool, refresh bool) (*gtsmodel.Account, error) - GetRemoteStatus(ctx context.Context, username string, remoteStatusID *url.URL, refresh, includeParent bool) (*gtsmodel.Status, ap.Statusable, bool, error) + GetRemoteStatus(ctx context.Context, username string, remoteStatusID *url.URL, refetch, includeParent bool) (*gtsmodel.Status, ap.Statusable, error) EnrichRemoteStatus(ctx context.Context, username string, status *gtsmodel.Status, includeParent bool) (*gtsmodel.Status, error) GetRemoteInstance(ctx context.Context, username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error) diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go index cacca91b2..7c4d588bb 100644 --- a/internal/federation/dereferencing/status.go +++ b/internal/federation/dereferencing/status.go @@ -53,79 +53,63 @@ func (d *deref) EnrichRemoteStatus(ctx context.Context, username string, status } // GetRemoteStatus completely dereferences a remote status, converts it to a GtS model status, -// puts it in the database, and returns it to a caller. The boolean indicates whether the status is new -// to us or not. If we haven't seen the status before, bool will be true. If we have seen the status before, -// it will be false. +// puts it in the database, and returns it to a caller. // -// If refresh is true, then even if we have the status in our database already, it will be dereferenced from its -// remote representation, as will its owner. +// If refetch is true, then regardless of whether we have the original status in the database or not, +// the ap.Statusable representation of the status will be dereferenced and returned. // -// If a dereference was performed, then the function also returns the ap.Statusable representation for further processing. +// If refetch is false, the ap.Statusable will only be returned if this is a new status, so callers +// should check whether or not this is nil. // // SIDE EFFECTS: remote status will be stored in the database, and the remote status owner will also be stored. -func (d *deref) GetRemoteStatus(ctx context.Context, username string, remoteStatusID *url.URL, refresh, includeParent bool) (*gtsmodel.Status, ap.Statusable, bool, error) { - new := true - - // check if we already have the status in our db +func (d *deref) GetRemoteStatus(ctx context.Context, username string, remoteStatusID *url.URL, refetch, includeParent bool) (*gtsmodel.Status, ap.Statusable, error) { maybeStatus, err := d.db.GetStatusByURI(ctx, remoteStatusID.String()) - if err == nil { - // we've seen this status before so it's not new - new = false - - // if we're not being asked to refresh, we can just return the maybeStatus as-is and avoid doing any external calls - if !refresh { - return maybeStatus, nil, new, nil - } + if err == nil && !refetch { + // we already had the status and we aren't being asked to refetch the AP representation + return maybeStatus, nil, nil } statusable, err := d.dereferenceStatusable(ctx, username, remoteStatusID) if err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error dereferencing statusable: %s", err) + return nil, nil, fmt.Errorf("GetRemoteStatus: error dereferencing statusable: %s", err) + } + + if maybeStatus != nil && refetch { + // we already had the status and we've successfully fetched the AP representation as requested + return maybeStatus, statusable, nil } + // from here on out we can consider this to be a 'new' status because we didn't have the status in the db already accountURI, err := ap.ExtractAttributedTo(statusable) if err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error extracting attributedTo: %s", err) + return nil, nil, fmt.Errorf("GetRemoteStatus: error extracting attributedTo: %s", err) } - // do this so we know we have the remote account of the status in the db _, err = d.GetRemoteAccount(ctx, username, accountURI, true, false) if err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: couldn't derive status author: %s", err) + return nil, nil, fmt.Errorf("GetRemoteStatus: couldn't get status author: %s", err) } gtsStatus, err := d.typeConverter.ASStatusToStatus(ctx, statusable) if err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error converting statusable to status: %s", err) + return nil, statusable, fmt.Errorf("GetRemoteStatus: error converting statusable to status: %s", err) } - if new { - ulid, err := id.NewULIDFromTime(gtsStatus.CreatedAt) - if err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error generating new id for status: %s", err) - } - gtsStatus.ID = ulid - - if err := d.populateStatusFields(ctx, gtsStatus, username, includeParent); err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error populating status fields: %s", err) - } - - if err := d.db.PutStatus(ctx, gtsStatus); err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error putting new status: %s", err) - } - } else { - gtsStatus.ID = maybeStatus.ID + ulid, err := id.NewULIDFromTime(gtsStatus.CreatedAt) + if err != nil { + return nil, nil, fmt.Errorf("GetRemoteStatus: error generating new id for status: %s", err) + } + gtsStatus.ID = ulid - if err := d.populateStatusFields(ctx, gtsStatus, username, includeParent); err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error populating status fields: %s", err) - } + if err := d.populateStatusFields(ctx, gtsStatus, username, includeParent); err != nil { + return nil, nil, fmt.Errorf("GetRemoteStatus: error populating status fields: %s", err) + } - if err := d.db.UpdateByPrimaryKey(ctx, gtsStatus); err != nil { - return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error updating status: %s", err) - } + if err := d.db.PutStatus(ctx, gtsStatus); err != nil { + return nil, nil, fmt.Errorf("GetRemoteStatus: error putting new status: %s", err) } - return gtsStatus, statusable, new, nil + return gtsStatus, statusable, nil } func (d *deref) dereferenceStatusable(ctx context.Context, username string, remoteStatusID *url.URL) (ap.Statusable, error) { @@ -429,14 +413,9 @@ func (d *deref) populateStatusRepliedTo(ctx context.Context, status *gtsmodel.St return err } - // see if we have the status in our db already - replyToStatus, err := d.db.GetStatusByURI(ctx, status.InReplyToURI) + replyToStatus, _, err := d.GetRemoteStatus(ctx, requestingUsername, statusURI, false, false) if err != nil { - // Status was not in the DB, try fetch - replyToStatus, _, _, err = d.GetRemoteStatus(ctx, requestingUsername, statusURI, false, false) - if err != nil { - return fmt.Errorf("populateStatusRepliedTo: couldn't get reply to status with uri %s: %s", status.InReplyToURI, err) - } + return fmt.Errorf("populateStatusRepliedTo: couldn't get reply to status with uri %s: %s", status.InReplyToURI, err) } // we have the status diff --git a/internal/federation/dereferencing/status_test.go b/internal/federation/dereferencing/status_test.go index 08363f580..ab6efad38 100644 --- a/internal/federation/dereferencing/status_test.go +++ b/internal/federation/dereferencing/status_test.go @@ -38,11 +38,9 @@ func (suite *StatusTestSuite) TestDereferenceSimpleStatus() { fetchingAccount := suite.testAccounts["local_account_1"] statusURL := testrig.URLMustParse("https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839") - status, statusable, new, err := suite.dereferencer.GetRemoteStatus(context.Background(), fetchingAccount.Username, statusURL, false, false) + status, _, err := suite.dereferencer.GetRemoteStatus(context.Background(), fetchingAccount.Username, statusURL, false, false) suite.NoError(err) suite.NotNil(status) - suite.NotNil(statusable) - suite.True(new) // status values should be set suite.Equal("https://unknown-instance.com/users/brand_new_person/statuses/01FE4NTHKWW7THT67EF10EB839", status.URI) @@ -80,11 +78,9 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithMention() { fetchingAccount := suite.testAccounts["local_account_1"] statusURL := testrig.URLMustParse("https://unknown-instance.com/users/brand_new_person/statuses/01FE5Y30E3W4P7TRE0R98KAYQV") - status, statusable, new, err := suite.dereferencer.GetRemoteStatus(context.Background(), fetchingAccount.Username, statusURL, false, false) + status, _, err := suite.dereferencer.GetRemoteStatus(context.Background(), fetchingAccount.Username, statusURL, false, false) suite.NoError(err) suite.NotNil(status) - suite.NotNil(statusable) - suite.True(new) // status values should be set suite.Equal("https://unknown-instance.com/users/brand_new_person/statuses/01FE5Y30E3W4P7TRE0R98KAYQV", status.URI) @@ -135,11 +131,9 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithImageAndNoContent() { fetchingAccount := suite.testAccounts["local_account_1"] statusURL := testrig.URLMustParse("https://turnip.farm/users/turniplover6969/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042") - status, statusable, new, err := suite.dereferencer.GetRemoteStatus(context.Background(), fetchingAccount.Username, statusURL, false, false) + status, _, err := suite.dereferencer.GetRemoteStatus(context.Background(), fetchingAccount.Username, statusURL, false, false) suite.NoError(err) suite.NotNil(status) - suite.NotNil(statusable) - suite.True(new) // status values should be set suite.Equal("https://turnip.farm/users/turniplover6969/statuses/70c53e54-3146-42d5-a630-83c8b6c7c042", status.URI) diff --git a/internal/federation/dereferencing/thread.go b/internal/federation/dereferencing/thread.go index f527b99cd..469defd5e 100644 --- a/internal/federation/dereferencing/thread.go +++ b/internal/federation/dereferencing/thread.go @@ -52,9 +52,9 @@ func (d *deref) DereferenceThread(ctx context.Context, username string, statusIR } // first make sure we have this status in our db - _, statusable, _, err := d.GetRemoteStatus(ctx, username, statusIRI, true, false) + _, statusable, err := d.GetRemoteStatus(ctx, username, statusIRI, true, false) if err != nil { - return fmt.Errorf("DereferenceThread: error getting status with id %s: %s", statusIRI.String(), err) + return fmt.Errorf("DereferenceThread: error getting initial status with id %s: %s", statusIRI.String(), err) } // first iterate up through ancestors, dereferencing if necessary as we go @@ -106,9 +106,8 @@ func (d *deref) iterateAncestors(ctx context.Context, username string, statusIRI return d.iterateAncestors(ctx, username, *nextIRI) } - // If we reach here, we're looking at a remote status -- make sure we have it in our db by calling GetRemoteStatus - // We call it with refresh to true because we want the statusable representation to parse inReplyTo from. - _, statusable, _, err := d.GetRemoteStatus(ctx, username, &statusIRI, true, false) + // If we reach here, we're looking at a remote status + _, statusable, err := d.GetRemoteStatus(ctx, username, &statusIRI, true, false) if err != nil { l.Debugf("error getting remote status: %s", err) return nil @@ -220,8 +219,8 @@ pageLoop: foundReplies++ // get the remote statusable and put it in the db - _, statusable, new, err := d.GetRemoteStatus(ctx, username, itemURI, false, false) - if new && err == nil && statusable != nil { + _, statusable, err := d.GetRemoteStatus(ctx, username, itemURI, false, false) + if err == nil { // now iterate descendants of *that* status if err := d.iterateDescendants(ctx, username, *itemURI, statusable); err != nil { continue diff --git a/internal/federation/federatingdb/announce_test.go b/internal/federation/federatingdb/announce_test.go new file mode 100644 index 000000000..f63910298 --- /dev/null +++ b/internal/federation/federatingdb/announce_test.go @@ -0,0 +1,93 @@ +/* + GoToSocial + Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org + + 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 ( + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type AnnounceTestSuite struct { + FederatingDBTestSuite +} + +func (suite *AnnounceTestSuite) TestNewAnnounce() { + receivingAccount1 := suite.testAccounts["local_account_1"] + announcingAccount := suite.testAccounts["remote_account_1"] + + ctx := createTestContext(receivingAccount1, announcingAccount) + announce1 := suite.testActivities["announce_forwarded_1_zork"] + + err := suite.federatingDB.Announce(ctx, announce1.Activity.(vocab.ActivityStreamsAnnounce)) + suite.NoError(err) + + // should be a message heading to the processor now, which we can intercept here + msg := <-suite.fromFederator + suite.Equal(ap.ActivityAnnounce, msg.APObjectType) + suite.Equal(ap.ActivityCreate, msg.APActivityType) + + boost, ok := msg.GTSModel.(*gtsmodel.Status) + suite.True(ok) + suite.Equal(announcingAccount.ID, boost.AccountID) + + // only the URI will be set on the boosted status because it still needs to be dereferenced + suite.NotEmpty(boost.BoostOf.URI) +} + +func (suite *AnnounceTestSuite) TestAnnounceTwice() { + receivingAccount1 := suite.testAccounts["local_account_1"] + receivingAccount2 := suite.testAccounts["local_account_2"] + + announcingAccount := suite.testAccounts["remote_account_1"] + + ctx1 := createTestContext(receivingAccount1, announcingAccount) + announce1 := suite.testActivities["announce_forwarded_1_zork"] + + err := suite.federatingDB.Announce(ctx1, announce1.Activity.(vocab.ActivityStreamsAnnounce)) + suite.NoError(err) + + // should be a message heading to the processor now, which we can intercept here + msg := <-suite.fromFederator + suite.Equal(ap.ActivityAnnounce, msg.APObjectType) + suite.Equal(ap.ActivityCreate, msg.APActivityType) + boost, ok := msg.GTSModel.(*gtsmodel.Status) + suite.True(ok) + suite.Equal(announcingAccount.ID, boost.AccountID) + + // only the URI will be set on the boosted status because it still needs to be dereferenced + suite.NotEmpty(boost.BoostOf.URI) + + ctx2 := createTestContext(receivingAccount2, announcingAccount) + announce2 := suite.testActivities["announce_forwarded_1_turtle"] + + err = suite.federatingDB.Announce(ctx2, announce2.Activity.(vocab.ActivityStreamsAnnounce)) + suite.NoError(err) + + // since this is a repeat announce with the same URI, just delivered to a different inbox, + // we should have nothing in the messages channel... + suite.Empty(suite.fromFederator) +} + +func TestAnnounceTestSuite(t *testing.T) { + suite.Run(t, &AnnounceTestSuite{}) +} diff --git a/internal/federation/federatingdb/federatingdb_test.go b/internal/federation/federatingdb/federatingdb_test.go index 8e6c1802d..ded817c98 100644 --- a/internal/federation/federatingdb/federatingdb_test.go +++ b/internal/federation/federatingdb/federatingdb_test.go @@ -63,8 +63,9 @@ func (suite *FederatingDBTestSuite) SetupSuite() { } func (suite *FederatingDBTestSuite) SetupTest() { - testrig.InitTestLog() testrig.InitTestConfig() + testrig.InitTestLog() + suite.fedWorker = concurrency.NewWorkerPool[messages.FromFederator](-1, -1) suite.fromFederator = make(chan messages.FromFederator, 10) suite.fedWorker.SetProcessor(func(ctx context.Context, msg messages.FromFederator) error { diff --git a/internal/federation/federator.go b/internal/federation/federator.go index 61d7f3e17..6412c9ee1 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -62,7 +62,7 @@ type Federator interface { GetRemoteAccount(ctx context.Context, username string, remoteAccountID *url.URL, blocking bool, refresh bool) (*gtsmodel.Account, error) - GetRemoteStatus(ctx context.Context, username string, remoteStatusID *url.URL, refresh, includeParent bool) (*gtsmodel.Status, ap.Statusable, bool, error) + GetRemoteStatus(ctx context.Context, username string, remoteStatusID *url.URL, refetch, includeParent bool) (*gtsmodel.Status, ap.Statusable, error) EnrichRemoteStatus(ctx context.Context, username string, status *gtsmodel.Status, includeParent bool) (*gtsmodel.Status, error) GetRemoteInstance(ctx context.Context, username string, remoteInstanceURI *url.URL) (*gtsmodel.Instance, error) |