mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-30 17:13:32 -06:00
[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
This commit is contained in:
parent
1461adfd3a
commit
f0c9f4169b
18 changed files with 324 additions and 99 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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.db.PutStatus(ctx, gtsStatus); err != nil {
|
||||
return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error putting new status: %s", err)
|
||||
}
|
||||
} else {
|
||||
gtsStatus.ID = maybeStatus.ID
|
||||
|
||||
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.UpdateByPrimaryKey(ctx, gtsStatus); err != nil {
|
||||
return nil, statusable, new, fmt.Errorf("GetRemoteStatus: error updating status: %s", err)
|
||||
}
|
||||
if err := d.populateStatusFields(ctx, gtsStatus, username, includeParent); err != nil {
|
||||
return nil, nil, fmt.Errorf("GetRemoteStatus: error populating status fields: %s", err)
|
||||
}
|
||||
|
||||
return gtsStatus, statusable, new, nil
|
||||
if err := d.db.PutStatus(ctx, gtsStatus); err != nil {
|
||||
return nil, nil, fmt.Errorf("GetRemoteStatus: error putting new status: %s", err)
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
93
internal/federation/federatingdb/announce_test.go
Normal file
93
internal/federation/federatingdb/announce_test.go
Normal file
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue