From 367bdca25093ee76b36506d8a5e6733b0aa2e2bb Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sun, 10 Oct 2021 12:39:25 +0200 Subject: [PATCH] Handle forwarded messages (#273) * correct path of foss_satan * add APIri and notes * test create forward note * rename target => receiving account * split up create into separate funcs * update extractFromCtx * tidy up from federator processing * foss satan => http not https * check if status in db * mock dereference of status from IRI * add forward message deref test * update test with activities * add remote_account_2 to test rig --- internal/federation/federatingdb/accept.go | 17 +- internal/federation/federatingdb/announce.go | 13 +- internal/federation/federatingdb/create.go | 379 +++++++++++------ .../federation/federatingdb/create_test.go | 91 ++++ internal/federation/federatingdb/delete.go | 13 +- .../federatingdb/federatingdb_test.go | 14 + internal/federation/federatingdb/undo.go | 13 +- internal/federation/federatingdb/update.go | 11 +- internal/federation/federatingdb/util.go | 29 +- internal/federation/federatingprotocol.go | 26 +- internal/federation/federator_test.go | 2 +- internal/messages/messages.go | 15 +- internal/processing/fromfederator.go | 389 ++++++++++-------- internal/processing/fromfederator_test.go | 23 ++ internal/processing/processor_test.go | 28 ++ internal/text/common_test.go | 4 +- internal/util/uri.go | 4 +- testrig/testmodels.go | 77 +++- 18 files changed, 765 insertions(+), 383 deletions(-) create mode 100644 internal/federation/federatingdb/create_test.go diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index 7ecf41ee4..0d2d3a270 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -48,12 +48,9 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA l.Debug("entering Accept") } - targetAcct, fromFederatorChan, err := extractFromCtx(ctx) - if err != nil { - return err - } - if targetAcct == nil || fromFederatorChan == nil { - // If the target account or federator channel wasn't set on the context, that means this request didn't pass + receivingAccount, _, fromFederatorChan := extractFromCtx(ctx) + if receivingAccount == nil || fromFederatorChan == nil { + // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass // through the API, but came from inside GtS as the result of another activity on this instance. That being so, // we can safely just ignore this activity, since we know we've already processed it elsewhere. return nil @@ -77,7 +74,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA } // make sure the addressee of the original follow is the same as whatever inbox this landed in - if gtsFollowRequest.AccountID != targetAcct.ID { + if gtsFollowRequest.AccountID != receivingAccount.ID { return errors.New("ACCEPT: follow object account and inbox account were not the same") } follow, err := f.db.AcceptFollowRequest(ctx, gtsFollowRequest.AccountID, gtsFollowRequest.TargetAccountID) @@ -89,7 +86,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA APObjectType: ap.ActivityFollow, APActivityType: ap.ActivityAccept, GTSModel: follow, - ReceivingAccount: targetAcct, + ReceivingAccount: receivingAccount, } return nil @@ -114,7 +111,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err) } // make sure the addressee of the original follow is the same as whatever inbox this landed in - if gtsFollow.AccountID != targetAcct.ID { + if gtsFollow.AccountID != receivingAccount.ID { return errors.New("ACCEPT: follow object account and inbox account were not the same") } follow, err := f.db.AcceptFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID) @@ -126,7 +123,7 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA APObjectType: ap.ActivityFollow, APActivityType: ap.ActivityAccept, GTSModel: follow, - ReceivingAccount: targetAcct, + ReceivingAccount: receivingAccount, } return nil diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go index 9a03ede92..49ec84509 100644 --- a/internal/federation/federatingdb/announce.go +++ b/internal/federation/federatingdb/announce.go @@ -44,12 +44,9 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre l.Debug("entering Announce") } - targetAcct, fromFederatorChan, err := extractFromCtx(ctx) - if err != nil { - return err - } - if targetAcct == nil || fromFederatorChan == nil { - // If the target account or federator channel wasn't set on the context, that means this request didn't pass + receivingAccount, _, fromFederatorChan := extractFromCtx(ctx) + if receivingAccount == nil || fromFederatorChan == nil { + // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass // through the API, but came from inside GtS as the result of another activity on this instance. That being so, // we can safely just ignore this activity, since we know we've already processed it elsewhere. return nil @@ -57,7 +54,7 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre boost, isNew, err := f.typeConverter.ASAnnounceToStatus(ctx, announce) if err != nil { - return fmt.Errorf("ANNOUNCE: error converting announce to boost: %s", err) + return fmt.Errorf("Announce: error converting announce to boost: %s", err) } if !isNew { @@ -70,7 +67,7 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre APObjectType: ap.ActivityAnnounce, APActivityType: ap.ActivityCreate, GTSModel: boost, - ReceivingAccount: targetAcct, + ReceivingAccount: receivingAccount, } return nil diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index 2a31108f2..4262dfcb4 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -22,11 +22,13 @@ import ( "context" "errors" "fmt" + "strings" "github.com/go-fed/activity/streams/vocab" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/messages" ) @@ -59,144 +61,261 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { l.Debug("entering Create") } - targetAcct, fromFederatorChan, err := extractFromCtx(ctx) - if err != nil { - return err - } - if targetAcct == nil || fromFederatorChan == nil { - // If the target account or federator channel wasn't set on the context, that means this request didn't pass + receivingAccount, requestingAccount, fromFederatorChan := extractFromCtx(ctx) + if receivingAccount == nil || fromFederatorChan == nil { + // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass // through the API, but came from inside GtS as the result of another activity on this instance. That being so, // we can safely just ignore this activity, since we know we've already processed it elsewhere. return nil } switch asType.GetTypeName() { - case ap.ActivityCreate: - // CREATE SOMETHING - create, ok := asType.(vocab.ActivityStreamsCreate) - if !ok { - return errors.New("CREATE: could not convert type to create") - } - object := create.GetActivityStreamsObject() - for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { - switch objectIter.GetType().GetTypeName() { - case ap.ObjectNote: - // CREATE A NOTE - note := objectIter.GetActivityStreamsNote() - status, err := f.typeConverter.ASStatusToStatus(ctx, note) - if err != nil { - return fmt.Errorf("CREATE: error converting note to status: %s", err) - } - - // id the status based on the time it was created - statusID, err := id.NewULIDFromTime(status.CreatedAt) - if err != nil { - return err - } - status.ID = statusID - - if err := f.db.PutStatus(ctx, status); err != nil { - if err == db.ErrAlreadyExists { - // the status already exists in the database, which means we've already handled everything else, - // so we can just return nil here and be done with it. - return nil - } - // an actual error has happened - return fmt.Errorf("CREATE: database error inserting status: %s", err) - } - - fromFederatorChan <- messages.FromFederator{ - APObjectType: ap.ObjectNote, - APActivityType: ap.ActivityCreate, - GTSModel: status, - ReceivingAccount: targetAcct, - } - } - } - case ap.ActivityFollow: - // FOLLOW SOMETHING - follow, ok := asType.(vocab.ActivityStreamsFollow) - if !ok { - return errors.New("CREATE: could not convert type to follow") - } - - followRequest, err := f.typeConverter.ASFollowToFollowRequest(ctx, follow) - if err != nil { - return fmt.Errorf("CREATE: could not convert Follow to follow request: %s", err) - } - - newID, err := id.NewULID() - if err != nil { - return err - } - followRequest.ID = newID - - if err := f.db.Put(ctx, followRequest); err != nil { - return fmt.Errorf("CREATE: database error inserting follow request: %s", err) - } - - fromFederatorChan <- messages.FromFederator{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityCreate, - GTSModel: followRequest, - ReceivingAccount: targetAcct, - } - case ap.ActivityLike: - // LIKE SOMETHING - like, ok := asType.(vocab.ActivityStreamsLike) - if !ok { - return errors.New("CREATE: could not convert type to like") - } - - fave, err := f.typeConverter.ASLikeToFave(ctx, like) - if err != nil { - return fmt.Errorf("CREATE: could not convert Like to fave: %s", err) - } - - newID, err := id.NewULID() - if err != nil { - return err - } - fave.ID = newID - - if err := f.db.Put(ctx, fave); err != nil { - return fmt.Errorf("CREATE: database error inserting fave: %s", err) - } - - fromFederatorChan <- messages.FromFederator{ - APObjectType: ap.ActivityLike, - APActivityType: ap.ActivityCreate, - GTSModel: fave, - ReceivingAccount: targetAcct, - } case ap.ActivityBlock: // BLOCK SOMETHING - blockable, ok := asType.(vocab.ActivityStreamsBlock) - if !ok { - return errors.New("CREATE: could not convert type to block") - } - - block, err := f.typeConverter.ASBlockToBlock(ctx, blockable) - if err != nil { - return fmt.Errorf("CREATE: could not convert Block to gts model block") - } - - newID, err := id.NewULID() - if err != nil { - return err - } - block.ID = newID - - if err := f.db.Put(ctx, block); err != nil { - return fmt.Errorf("CREATE: database error inserting block: %s", err) - } - - fromFederatorChan <- messages.FromFederator{ - APObjectType: ap.ActivityBlock, - APActivityType: ap.ActivityCreate, - GTSModel: block, - ReceivingAccount: targetAcct, - } + return f.activityBlock(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan) + case ap.ActivityCreate: + // CREATE SOMETHING + return f.activityCreate(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan) + case ap.ActivityFollow: + // FOLLOW SOMETHING + return f.activityFollow(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan) + case ap.ActivityLike: + // LIKE SOMETHING + return f.activityLike(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan) } return nil } + +/* + BLOCK HANDLERS +*/ + +func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, receiving *gtsmodel.Account, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) error { + blockable, ok := asType.(vocab.ActivityStreamsBlock) + if !ok { + return errors.New("activityBlock: could not convert type to block") + } + + block, err := f.typeConverter.ASBlockToBlock(ctx, blockable) + if err != nil { + return fmt.Errorf("activityBlock: could not convert Block to gts model block") + } + + newID, err := id.NewULID() + if err != nil { + return err + } + block.ID = newID + + if err := f.db.Put(ctx, block); err != nil { + return fmt.Errorf("activityBlock: database error inserting block: %s", err) + } + + fromFederatorChan <- messages.FromFederator{ + APObjectType: ap.ActivityBlock, + APActivityType: ap.ActivityCreate, + GTSModel: block, + ReceivingAccount: receiving, + } + return nil +} + +/* + CREATE HANDLERS +*/ + +func (f *federatingDB) activityCreate(ctx context.Context, asType vocab.Type, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) error { + create, ok := asType.(vocab.ActivityStreamsCreate) + if !ok { + return errors.New("activityCreate: could not convert type to create") + } + + // create should have an object + object := create.GetActivityStreamsObject() + if object == nil { + return errors.New("Create had no Object") + } + + errs := []string{} + // iterate through the object(s) to see what we're meant to be creating + for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() { + asObjectType := objectIter.GetType() + if asObjectType == nil { + // currently we can't do anything with just a Create of something that's not an Object with a type + // TODO: process a Create with an Object that's just a URI or something + errs = append(errs, "object of Create was not a Type") + continue + } + + // we have a type -- what is it? + asObjectTypeName := asObjectType.GetTypeName() + switch asObjectTypeName { + case ap.ObjectNote: + // CREATE A NOTE + if err := f.createNote(ctx, objectIter.GetActivityStreamsNote(), receivingAccount, requestingAccount, fromFederatorChan); err != nil { + errs = append(errs, err.Error()) + } + default: + errs = append(errs, fmt.Sprintf("received an object on a Create that we couldn't handle: %s", asObjectType.GetTypeName())) + } + } + + if len(errs) != 0 { + return fmt.Errorf("activityCreate: one or more errors while processing activity: %s", strings.Join(errs, "; ")) + } + + return nil +} + +// createNote handles a Create activity with a Note type. +func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStreamsNote, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) error { + l := f.log.WithFields(logrus.Fields{ + "func": "createNote", + "receivingAccount": receivingAccount.URI, + "requestingAccount": requestingAccount.URI, + }) + + // Check if we have a forward. + // In other words, was the note posted to our inbox by at least one actor who actually created the note, or are they just forwarding it? + forward := true + + // note should have an attributedTo + noteAttributedTo := note.GetActivityStreamsAttributedTo() + if noteAttributedTo == nil { + return errors.New("createNote: note had no attributedTo") + } + + // compare the attributedTo(s) with the actor who posted this to our inbox + for attributedToIter := noteAttributedTo.Begin(); attributedToIter != noteAttributedTo.End(); attributedToIter = attributedToIter.Next() { + if !attributedToIter.IsIRI() { + continue + } + iri := attributedToIter.GetIRI() + if requestingAccount.URI == iri.String() { + // at least one creator of the note, and the actor who posted the note to our inbox, are the same, so it's not a forward + forward = false + } + } + + // If we do have a forward, we should ignore the content for now and just dereference based on the URL/ID of the note instead, to get the note straight from the horse's mouth + if forward { + l.Trace("note is a forward") + id := note.GetJSONLDId() + if !id.IsIRI() { + // if the note id isn't an IRI, there's nothing we can do here + return nil + } + // pass the note iri into the processor and have it do the dereferencing instead of doing it here + fromFederatorChan <- messages.FromFederator{ + APObjectType: ap.ObjectNote, + APActivityType: ap.ActivityCreate, + APIri: id.GetIRI(), + GTSModel: nil, + ReceivingAccount: receivingAccount, + } + return nil + } + + // if we reach this point, we know it's not a forwarded status, so proceed with processing it as normal + + status, err := f.typeConverter.ASStatusToStatus(ctx, note) + if err != nil { + return fmt.Errorf("createNote: error converting note to status: %s", err) + } + + // id the status based on the time it was created + statusID, err := id.NewULIDFromTime(status.CreatedAt) + if err != nil { + return err + } + status.ID = statusID + + if err := f.db.PutStatus(ctx, status); err != nil { + if err == db.ErrAlreadyExists { + // the status already exists in the database, which means we've already handled everything else, + // so we can just return nil here and be done with it. + return nil + } + // an actual error has happened + return fmt.Errorf("createNote: database error inserting status: %s", err) + } + + fromFederatorChan <- messages.FromFederator{ + APObjectType: ap.ObjectNote, + APActivityType: ap.ActivityCreate, + GTSModel: status, + ReceivingAccount: receivingAccount, + } + + return nil +} + +/* + FOLLOW HANDLERS +*/ + +func (f *federatingDB) activityFollow(ctx context.Context, asType vocab.Type, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) error { + follow, ok := asType.(vocab.ActivityStreamsFollow) + if !ok { + return errors.New("activityFollow: could not convert type to follow") + } + + followRequest, err := f.typeConverter.ASFollowToFollowRequest(ctx, follow) + if err != nil { + return fmt.Errorf("activityFollow: could not convert Follow to follow request: %s", err) + } + + newID, err := id.NewULID() + if err != nil { + return err + } + followRequest.ID = newID + + if err := f.db.Put(ctx, followRequest); err != nil { + return fmt.Errorf("activityFollow: database error inserting follow request: %s", err) + } + + fromFederatorChan <- messages.FromFederator{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityCreate, + GTSModel: followRequest, + ReceivingAccount: receivingAccount, + } + + return nil +} + +/* + LIKE HANDLERS +*/ + +func (f *federatingDB) activityLike(ctx context.Context, asType vocab.Type, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) error { + like, ok := asType.(vocab.ActivityStreamsLike) + if !ok { + return errors.New("activityLike: could not convert type to like") + } + + fave, err := f.typeConverter.ASLikeToFave(ctx, like) + if err != nil { + return fmt.Errorf("activityLike: could not convert Like to fave: %s", err) + } + + newID, err := id.NewULID() + if err != nil { + return err + } + fave.ID = newID + + if err := f.db.Put(ctx, fave); err != nil { + return fmt.Errorf("activityLike: database error inserting fave: %s", err) + } + + fromFederatorChan <- messages.FromFederator{ + APObjectType: ap.ActivityLike, + APActivityType: ap.ActivityCreate, + GTSModel: fave, + ReceivingAccount: receivingAccount, + } + + return nil +} diff --git a/internal/federation/federatingdb/create_test.go b/internal/federation/federatingdb/create_test.go new file mode 100644 index 000000000..ee2194f9e --- /dev/null +++ b/internal/federation/federatingdb/create_test.go @@ -0,0 +1,91 @@ +/* + GoToSocial + Copyright (C) 2021 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 . +*/ + +package federatingdb_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/messages" +) + +type CreateTestSuite struct { + FederatingDBTestSuite +} + +func (suite *CreateTestSuite) TestCreateNote() { + receivingAccount := suite.testAccounts["local_account_1"] + requestingAccount := suite.testAccounts["remote_account_1"] + fromFederatorChan := make(chan messages.FromFederator, 10) + + ctx := createTestContext(receivingAccount, requestingAccount, fromFederatorChan) + + create := suite.testActivities["dm_for_zork"].Activity + + err := suite.federatingDB.Create(ctx, create) + suite.NoError(err) + + // should be a message heading to the processor now, which we can intercept here + msg := <-fromFederatorChan + suite.Equal(ap.ObjectNote, msg.APObjectType) + suite.Equal(ap.ActivityCreate, msg.APActivityType) + + // shiny new status should be defined on the message + suite.NotNil(msg.GTSModel) + status := msg.GTSModel.(*gtsmodel.Status) + + // status should have some expected values + suite.Equal(requestingAccount.ID, status.AccountID) + suite.Equal("hey zork here's a new private note for you", status.Content) + + // status should be in the database + _, err = suite.db.GetStatusByID(context.Background(), status.ID) + suite.NoError(err) +} + +func (suite *CreateTestSuite) TestCreateNoteForward() { + receivingAccount := suite.testAccounts["local_account_1"] + requestingAccount := suite.testAccounts["remote_account_1"] + fromFederatorChan := make(chan messages.FromFederator, 10) + + ctx := createTestContext(receivingAccount, requestingAccount, fromFederatorChan) + + create := suite.testActivities["forwarded_message"].Activity + + err := suite.federatingDB.Create(ctx, create) + suite.NoError(err) + + // should be a message heading to the processor now, which we can intercept here + msg := <-fromFederatorChan + suite.Equal(ap.ObjectNote, msg.APObjectType) + suite.Equal(ap.ActivityCreate, msg.APActivityType) + + // nothing should be set as the model since this is a forward + suite.Nil(msg.GTSModel) + + // but we should have a uri set + suite.Equal("http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1", msg.APIri.String()) +} + +func TestCreateTestSuite(t *testing.T) { + suite.Run(t, &CreateTestSuite{}) +} diff --git a/internal/federation/federatingdb/delete.go b/internal/federation/federatingdb/delete.go index fc77f8025..77c3f502b 100644 --- a/internal/federation/federatingdb/delete.go +++ b/internal/federation/federatingdb/delete.go @@ -44,12 +44,9 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { ) l.Debug("entering Delete") - targetAcct, fromFederatorChan, err := extractFromCtx(ctx) - if err != nil { - return err - } - if targetAcct == nil || fromFederatorChan == nil { - // If the target account or federator channel wasn't set on the context, that means this request didn't pass + receivingAccount, _, fromFederatorChan := extractFromCtx(ctx) + if receivingAccount == nil || fromFederatorChan == nil { + // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass // through the API, but came from inside GtS as the result of another activity on this instance. That being so, // we can safely just ignore this activity, since we know we've already processed it elsewhere. return nil @@ -68,7 +65,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { APObjectType: ap.ObjectNote, APActivityType: ap.ActivityDelete, GTSModel: s, - ReceivingAccount: targetAcct, + ReceivingAccount: receivingAccount, } } @@ -80,7 +77,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error { APObjectType: ap.ObjectProfile, APActivityType: ap.ActivityDelete, GTSModel: a, - ReceivingAccount: targetAcct, + ReceivingAccount: receivingAccount, } } diff --git a/internal/federation/federatingdb/federatingdb_test.go b/internal/federation/federatingdb/federatingdb_test.go index fc78540f2..3c22480f7 100644 --- a/internal/federation/federatingdb/federatingdb_test.go +++ b/internal/federation/federatingdb/federatingdb_test.go @@ -19,13 +19,17 @@ package federatingdb_test import ( + "context" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -45,6 +49,7 @@ type FederatingDBTestSuite struct { testAttachments map[string]*gtsmodel.MediaAttachment testStatuses map[string]*gtsmodel.Status testBlocks map[string]*gtsmodel.Block + testActivities map[string]testrig.ActivityWithSignature } func (suite *FederatingDBTestSuite) SetupSuite() { @@ -56,6 +61,7 @@ func (suite *FederatingDBTestSuite) SetupSuite() { suite.testAttachments = testrig.NewTestAttachments() suite.testStatuses = testrig.NewTestStatuses() suite.testBlocks = testrig.NewTestBlocks() + suite.testActivities = testrig.NewTestActivities(suite.testAccounts) } func (suite *FederatingDBTestSuite) SetupTest() { @@ -70,3 +76,11 @@ func (suite *FederatingDBTestSuite) SetupTest() { func (suite *FederatingDBTestSuite) TearDownTest() { testrig.StandardDBTeardown(suite.db) } + +func createTestContext(receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) context.Context { + ctx := context.Background() + ctx = context.WithValue(ctx, util.APReceivingAccount, receivingAccount) + ctx = context.WithValue(ctx, util.APRequestingAccount, requestingAccount) + ctx = context.WithValue(ctx, util.APFromFederatorChanKey, fromFederatorChan) + return ctx +} diff --git a/internal/federation/federatingdb/undo.go b/internal/federation/federatingdb/undo.go index 082d76e60..3d4cb1d53 100644 --- a/internal/federation/federatingdb/undo.go +++ b/internal/federation/federatingdb/undo.go @@ -46,12 +46,9 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) l.Debug("entering Undo") } - targetAcct, fromFederatorChan, err := extractFromCtx(ctx) - if err != nil { - return err - } - if targetAcct == nil || fromFederatorChan == nil { - // If the target account or federator channel wasn't set on the context, that means this request didn't pass + receivingAccount, _, fromFederatorChan := extractFromCtx(ctx) + if receivingAccount == nil || fromFederatorChan == nil { + // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass // through the API, but came from inside GtS as the result of another activity on this instance. That being so, // we can safely just ignore this activity, since we know we've already processed it elsewhere. return nil @@ -83,7 +80,7 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) return fmt.Errorf("UNDO: error converting asfollow to gtsfollow: %s", err) } // make sure the addressee of the original follow is the same as whatever inbox this landed in - if gtsFollow.TargetAccountID != targetAcct.ID { + if gtsFollow.TargetAccountID != receivingAccount.ID { return errors.New("UNDO: follow object account and inbox account were not the same") } // delete any existing FOLLOW @@ -116,7 +113,7 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) return fmt.Errorf("UNDO: error converting asblock to gtsblock: %s", err) } // make sure the addressee of the original block is the same as whatever inbox this landed in - if gtsBlock.TargetAccountID != targetAcct.ID { + if gtsBlock.TargetAccountID != receivingAccount.ID { return errors.New("UNDO: block object account and inbox account were not the same") } // delete any existing BLOCK diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go index 6ce08ccb6..b3a27b462 100644 --- a/internal/federation/federatingdb/update.go +++ b/internal/federation/federatingdb/update.go @@ -56,12 +56,9 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error { l.Debug("entering Update") } - targetAcct, fromFederatorChan, err := extractFromCtx(ctx) - if err != nil { - return err - } - if targetAcct == nil || fromFederatorChan == nil { - // If the target account or federator channel wasn't set on the context, that means this request didn't pass + receivingAccount, _, fromFederatorChan := extractFromCtx(ctx) + if receivingAccount == nil || fromFederatorChan == nil { + // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass // through the API, but came from inside GtS as the result of another activity on this instance. That being so, // we can safely just ignore this activity, since we know we've already processed it elsewhere. return nil @@ -153,7 +150,7 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error { APObjectType: ap.ObjectProfile, APActivityType: ap.ActivityUpdate, GTSModel: updatedAcct, - ReceivingAccount: targetAcct, + ReceivingAccount: receivingAccount, } } diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go index dfc998abb..49a95a449 100644 --- a/internal/federation/federatingdb/util.go +++ b/internal/federation/federatingdb/util.go @@ -289,29 +289,26 @@ func (f *federatingDB) collectIRIs(ctx context.Context, iris []*url.URL) (vocab. // extractFromCtx extracts some useful values from a context passed into the federatingDB via the API: // - The target account that owns the inbox or URI being interacted with. +// - The requesting account that posted to the inbox. // - A channel that messages for the processor can be placed into. -func extractFromCtx(ctx context.Context) (*gtsmodel.Account, chan messages.FromFederator, error) { - var targetAcct *gtsmodel.Account - targetAcctI := ctx.Value(util.APAccount) - if targetAcctI != nil { - var ok bool - targetAcct, ok = targetAcctI.(*gtsmodel.Account) - if !ok { - return nil, nil, errors.New("extractFromCtx: account value in context not parseable") - } +// If a value is not present, nil will be returned for it. It's up to the caller to check this and respond appropriately. +func extractFromCtx(ctx context.Context) (receivingAccount, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) { + receivingAccountI := ctx.Value(util.APReceivingAccount) + if receivingAccountI != nil { + receivingAccount = receivingAccountI.(*gtsmodel.Account) + } + + requestingAcctI := ctx.Value(util.APRequestingAccount) + if requestingAcctI != nil { + requestingAccount = requestingAcctI.(*gtsmodel.Account) } - var fromFederatorChan chan messages.FromFederator fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) if fromFederatorChanI != nil { - var ok bool - fromFederatorChan, ok = fromFederatorChanI.(chan messages.FromFederator) - if !ok { - return nil, nil, errors.New("extractFromCtx: fromFederatorChan value in context not parseable") - } + fromFederatorChan = fromFederatorChanI.(chan messages.FromFederator) } - return targetAcct, fromFederatorChan, nil + return } func marshalItem(item vocab.Type) (string, error) { diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index c0ed97d5c..afa763d98 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -113,12 +113,12 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr return nil, false, errors.New("username was empty") } - requestedAccount, err := f.db.GetLocalAccountByUsername(ctx, username) + receivingAccount, err := f.db.GetLocalAccountByUsername(ctx, username) if err != nil { - return nil, false, fmt.Errorf("could not fetch requested account with username %s: %s", username, err) + return nil, false, fmt.Errorf("could not fetch receiving account with username %s: %s", username, err) } - publicKeyOwnerURI, authenticated, err := f.AuthenticateFederatedRequest(ctx, requestedAccount.Username) + publicKeyOwnerURI, authenticated, err := f.AuthenticateFederatedRequest(ctx, receivingAccount.Username) if err != nil { l.Debugf("request not authenticated: %s", err) return ctx, false, err @@ -154,12 +154,12 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr requestingAccount, _, err := f.GetRemoteAccount(ctx, username, publicKeyOwnerURI, false) if err != nil { - return nil, false, fmt.Errorf("couldn't get remote account: %s", err) + return nil, false, fmt.Errorf("couldn't get requesting account %s: %s", publicKeyOwnerURI, err) } - withRequester := context.WithValue(ctx, util.APRequestingAccount, requestingAccount) - withRequested := context.WithValue(withRequester, util.APAccount, requestedAccount) - return withRequested, true, nil + withRequesting := context.WithValue(ctx, util.APRequestingAccount, requestingAccount) + withReceiving := context.WithValue(withRequesting, util.APReceivingAccount, receivingAccount) + return withReceiving, true, nil } // Blocked should determine whether to permit a set of actors given by @@ -182,11 +182,11 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er }) l.Debugf("entering BLOCKED function with IRI list: %+v", actorIRIs) - requestedAccountI := ctx.Value(util.APAccount) - requestedAccount, ok := requestedAccountI.(*gtsmodel.Account) + receivingAccountI := ctx.Value(util.APReceivingAccount) + receivingAccount, ok := receivingAccountI.(*gtsmodel.Account) if !ok { - f.log.Errorf("requested account not set on request context") - return false, errors.New("requested account not set on request context, so couldn't determine blocks") + f.log.Errorf("receiving account not set on request context") + return false, errors.New("receiving account not set on request context, so couldn't determine blocks") } blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs) @@ -209,12 +209,12 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err) } - blocked, err = f.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, false) + blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, requestingAccount.ID, false) if err != nil { return false, fmt.Errorf("error checking account block: %s", err) } if blocked { - l.Tracef("local account %s blocks account with uri %s", requestedAccount.Username, uri) + l.Tracef("local account %s blocks account with uri %s", receivingAccount.Username, uri) return true, nil } } diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go index 5a7056e6b..76e16a04f 100644 --- a/internal/federation/federator_test.go +++ b/internal/federation/federator_test.go @@ -125,7 +125,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() { ctx := context.Background() // by the time AuthenticatePostInbox is called, PostInboxRequestBodyHook should have already been called, // which should have set the account and username onto the request. We can replicate that behavior here: - ctxWithAccount := context.WithValue(ctx, util.APAccount, inboxAccount) + ctxWithAccount := context.WithValue(ctx, util.APReceivingAccount, inboxAccount) ctxWithActivity := context.WithValue(ctxWithAccount, util.APActivity, activity) ctxWithVerifier := context.WithValue(ctxWithActivity, util.APRequestingPublicKeyVerifier, verifier) ctxWithSignature := context.WithValue(ctxWithVerifier, util.APRequestingPublicKeySignature, activity.SignatureHeader) diff --git a/internal/messages/messages.go b/internal/messages/messages.go index 6cd2f466c..1a9396100 100644 --- a/internal/messages/messages.go +++ b/internal/messages/messages.go @@ -18,7 +18,11 @@ package messages -import "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +import ( + "net/url" + + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) // FromClientAPI wraps a message that travels from the client API into the processor. type FromClientAPI struct { @@ -31,8 +35,9 @@ type FromClientAPI struct { // FromFederator wraps a message that travels from the federator into the processor. type FromFederator struct { - APObjectType string - APActivityType string - GTSModel interface{} - ReceivingAccount *gtsmodel.Account + APObjectType string // what is the object type of this message? eg., Note, Profile etc. + APActivityType string // what is the activity type of this message? eg., Create, Follow etc. + APIri *url.URL // what is the IRI ID of this activity? + GTSModel interface{} // representation of this object if it's already been converted into our internal gts model + ReceivingAccount *gtsmodel.Account // which account owns the inbox that this activity was posted to? } diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 1ef29264e..449cc6f08 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -31,203 +31,264 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/messages" ) +// ProcessFromFederator reads the APActivityType and APObjectType of an incoming message from the federator, +// and directs the message into the appropriate side effect handler function, or simply does nothing if there's +// no handler function defined for the combination of Activity and Object. func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { l := p.log.WithFields(logrus.Fields{ - "func": "processFromFederator", - "federatorMsg": fmt.Sprintf("%+v", federatorMsg), + "func": "processFromFederator", + "APActivityType": federatorMsg.APActivityType, + "APObjectType": federatorMsg.APObjectType, }) - - l.Trace("entering function PROCESS FROM FEDERATOR") + l.Trace("processing message from federator") switch federatorMsg.APActivityType { case ap.ActivityCreate: - // CREATE + // CREATE SOMETHING switch federatorMsg.APObjectType { case ap.ObjectNote: // CREATE A STATUS - incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status) - if !ok { - return errors.New("note was not parseable as *gtsmodel.Status") - } - - status, err := p.federator.EnrichRemoteStatus(ctx, federatorMsg.ReceivingAccount.Username, incomingStatus, true) - if err != nil { - return err - } - - if err := p.timelineStatus(ctx, status); err != nil { - return err - } - - if err := p.notifyStatus(ctx, status); err != nil { - return err - } - case ap.ObjectProfile: - // CREATE AN ACCOUNT - // nothing to do here + return p.processCreateStatusFromFederator(ctx, federatorMsg) case ap.ActivityLike: // CREATE A FAVE - incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave) - if !ok { - return errors.New("like was not parseable as *gtsmodel.StatusFave") - } - - if err := p.notifyFave(ctx, incomingFave); err != nil { - return err - } + return p.processCreateFaveFromFederator(ctx, federatorMsg) case ap.ActivityFollow: // CREATE A FOLLOW REQUEST - followRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest) - if !ok { - return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest") - } - - if followRequest.TargetAccount == nil { - a, err := p.db.GetAccountByID(ctx, followRequest.TargetAccountID) - if err != nil { - return err - } - followRequest.TargetAccount = a - } - targetAccount := followRequest.TargetAccount - - if targetAccount.Locked { - // if the account is locked just notify the follow request and nothing else - return p.notifyFollowRequest(ctx, followRequest) - } - - if followRequest.Account == nil { - a, err := p.db.GetAccountByID(ctx, followRequest.AccountID) - if err != nil { - return err - } - followRequest.Account = a - } - originAccount := followRequest.Account - - // if the target account isn't locked, we should already accept the follow and notify about the new follower instead - follow, err := p.db.AcceptFollowRequest(ctx, followRequest.AccountID, followRequest.TargetAccountID) - if err != nil { - return err - } - - if err := p.federateAcceptFollowRequest(ctx, follow, originAccount, targetAccount); err != nil { - return err - } - - return p.notifyFollow(ctx, follow, targetAccount) + return p.processCreateFollowRequestFromFederator(ctx, federatorMsg) case ap.ActivityAnnounce: // CREATE AN ANNOUNCE - incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status) - if !ok { - return errors.New("announce was not parseable as *gtsmodel.Status") - } - - if err := p.federator.DereferenceAnnounce(ctx, incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil { - return fmt.Errorf("error dereferencing announce from federator: %s", err) - } - - incomingAnnounceID, err := id.NewULIDFromTime(incomingAnnounce.CreatedAt) - if err != nil { - return err - } - incomingAnnounce.ID = incomingAnnounceID - - if err := p.db.PutStatus(ctx, incomingAnnounce); err != nil { - return fmt.Errorf("error adding dereferenced announce to the db: %s", err) - } - - if err := p.timelineStatus(ctx, incomingAnnounce); err != nil { - return err - } - - if err := p.notifyAnnounce(ctx, incomingAnnounce); err != nil { - return err - } + return p.processCreateAnnounceFromFederator(ctx, federatorMsg) case ap.ActivityBlock: // CREATE A BLOCK - block, ok := federatorMsg.GTSModel.(*gtsmodel.Block) - if !ok { - return errors.New("block was not parseable as *gtsmodel.Block") - } - - // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa - if err := p.timelineManager.WipeStatusesFromAccountID(ctx, block.AccountID, block.TargetAccountID); err != nil { - return err - } - if err := p.timelineManager.WipeStatusesFromAccountID(ctx, block.TargetAccountID, block.AccountID); err != nil { - return err - } - // TODO: same with notifications - // TODO: same with bookmarks + return p.processCreateBlockFromFederator(ctx, federatorMsg) } case ap.ActivityUpdate: - // UPDATE + // UPDATE SOMETHING switch federatorMsg.APObjectType { case ap.ObjectProfile: // UPDATE AN ACCOUNT - incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) - if !ok { - return errors.New("profile was not parseable as *gtsmodel.Account") - } - - if _, err := p.federator.EnrichRemoteAccount(ctx, federatorMsg.ReceivingAccount.Username, incomingAccount); err != nil { - return fmt.Errorf("error enriching updated account from federator: %s", err) - } + return p.processUpdateAccountFromFederator(ctx, federatorMsg) } case ap.ActivityDelete: - // DELETE + // DELETE SOMETHING switch federatorMsg.APObjectType { case ap.ObjectNote: // DELETE A STATUS - // TODO: handle side effects of status deletion here: - // 1. delete all media associated with status - // 2. delete boosts of status - // 3. etc etc etc - statusToDelete, ok := federatorMsg.GTSModel.(*gtsmodel.Status) - if !ok { - return errors.New("note was not parseable as *gtsmodel.Status") - } - - // delete all attachments for this status - for _, a := range statusToDelete.AttachmentIDs { - if err := p.mediaProcessor.Delete(ctx, a); err != nil { - return err - } - } - - // delete all mentions for this status - for _, m := range statusToDelete.MentionIDs { - if err := p.db.DeleteByID(ctx, m, >smodel.Mention{}); err != nil { - return err - } - } - - // delete all notifications for this status - if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil { - return err - } - - // remove this status from any and all timelines - return p.deleteStatusFromTimelines(ctx, statusToDelete) + return p.processDeleteStatusFromFederator(ctx, federatorMsg) case ap.ObjectProfile: // DELETE A PROFILE/ACCOUNT - // handle side effects of account deletion here: delete all objects, statuses, media etc associated with account - account, ok := federatorMsg.GTSModel.(*gtsmodel.Account) - if !ok { - return errors.New("account delete was not parseable as *gtsmodel.Account") - } + return p.processDeleteAccountFromFederator(ctx, federatorMsg) + } + } - return p.accountProcessor.Delete(ctx, account, account.ID) + // not a combination we can/need to process + return nil +} + +// processCreateStatusFromFederator handles Activity Create and Object Note +func (p *processor) processCreateStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { + // check for either an IRI that we still need to dereference, OR an already dereferenced + // and converted status pinned to the message. + var status *gtsmodel.Status + + if federatorMsg.GTSModel != nil { + // there's a gts model already pinned to the message, it should be a status + var ok bool + if status, ok = federatorMsg.GTSModel.(*gtsmodel.Status); !ok { + return errors.New("ProcessFromFederator: note was not parseable as *gtsmodel.Status") } - case ap.ActivityAccept: - // ACCEPT - switch federatorMsg.APObjectType { - case ap.ActivityFollow: - // ACCEPT A FOLLOW - // nothing to do here + + var err error + status, err = p.federator.EnrichRemoteStatus(ctx, federatorMsg.ReceivingAccount.Username, status, true) + if err != nil { + return err } + } else { + // no model pinned, we need to dereference based on the IRI + if federatorMsg.APIri == nil { + return errors.New("ProcessFromFederator: status was not pinned to federatorMsg, and neither was an IRI for us to dereference") + } + var err error + status, _, _, err = p.federator.GetRemoteStatus(ctx, federatorMsg.ReceivingAccount.Username, federatorMsg.APIri, false, false) + if err != nil { + return err + } + } + + if err := p.timelineStatus(ctx, status); err != nil { + return err + } + + if err := p.notifyStatus(ctx, status); err != nil { + return err } return nil } + +// processCreateFaveFromFederator handles Activity Create and Object Like +func (p *processor) processCreateFaveFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { + incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave) + if !ok { + return errors.New("like was not parseable as *gtsmodel.StatusFave") + } + + if err := p.notifyFave(ctx, incomingFave); err != nil { + return err + } + + return nil +} + +// processCreateFollowRequestFromFederator handles Activity Create and Object Follow +func (p *processor) processCreateFollowRequestFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { + followRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest) + if !ok { + return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest") + } + + if followRequest.TargetAccount == nil { + a, err := p.db.GetAccountByID(ctx, followRequest.TargetAccountID) + if err != nil { + return err + } + followRequest.TargetAccount = a + } + targetAccount := followRequest.TargetAccount + + if targetAccount.Locked { + // if the account is locked just notify the follow request and nothing else + return p.notifyFollowRequest(ctx, followRequest) + } + + if followRequest.Account == nil { + a, err := p.db.GetAccountByID(ctx, followRequest.AccountID) + if err != nil { + return err + } + followRequest.Account = a + } + originAccount := followRequest.Account + + // if the target account isn't locked, we should already accept the follow and notify about the new follower instead + follow, err := p.db.AcceptFollowRequest(ctx, followRequest.AccountID, followRequest.TargetAccountID) + if err != nil { + return err + } + + if err := p.federateAcceptFollowRequest(ctx, follow, originAccount, targetAccount); err != nil { + return err + } + + return p.notifyFollow(ctx, follow, targetAccount) +} + +// processCreateAnnounceFromFederator handles Activity Create and Object Announce +func (p *processor) processCreateAnnounceFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { + incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("announce was not parseable as *gtsmodel.Status") + } + + if err := p.federator.DereferenceAnnounce(ctx, incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil { + return fmt.Errorf("error dereferencing announce from federator: %s", err) + } + + incomingAnnounceID, err := id.NewULIDFromTime(incomingAnnounce.CreatedAt) + if err != nil { + return err + } + incomingAnnounce.ID = incomingAnnounceID + + if err := p.db.PutStatus(ctx, incomingAnnounce); err != nil { + return fmt.Errorf("error adding dereferenced announce to the db: %s", err) + } + + if err := p.timelineStatus(ctx, incomingAnnounce); err != nil { + return err + } + + if err := p.notifyAnnounce(ctx, incomingAnnounce); err != nil { + return err + } + + return nil +} + +// processCreateBlockFromFederator handles Activity Create and Object Block +func (p *processor) processCreateBlockFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { + block, ok := federatorMsg.GTSModel.(*gtsmodel.Block) + if !ok { + return errors.New("block was not parseable as *gtsmodel.Block") + } + + // remove any of the blocking account's statuses from the blocked account's timeline, and vice versa + if err := p.timelineManager.WipeStatusesFromAccountID(ctx, block.AccountID, block.TargetAccountID); err != nil { + return err + } + if err := p.timelineManager.WipeStatusesFromAccountID(ctx, block.TargetAccountID, block.AccountID); err != nil { + return err + } + // TODO: same with notifications + // TODO: same with bookmarks + + return nil +} + +// processUpdateAccountFromFederator handles Activity Update and Object Profile +func (p *processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { + incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) + if !ok { + return errors.New("profile was not parseable as *gtsmodel.Account") + } + + if _, err := p.federator.EnrichRemoteAccount(ctx, federatorMsg.ReceivingAccount.Username, incomingAccount); err != nil { + return fmt.Errorf("error enriching updated account from federator: %s", err) + } + + return nil +} + +// processDeleteStatusFromFederator handles Activity Delete and Object Note +func (p *processor) processDeleteStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { + // TODO: handle side effects of status deletion here: + // 1. delete all media associated with status + // 2. delete boosts of status + // 3. etc etc etc + statusToDelete, ok := federatorMsg.GTSModel.(*gtsmodel.Status) + if !ok { + return errors.New("note was not parseable as *gtsmodel.Status") + } + + // delete all attachments for this status + for _, a := range statusToDelete.AttachmentIDs { + if err := p.mediaProcessor.Delete(ctx, a); err != nil { + return err + } + } + + // delete all mentions for this status + for _, m := range statusToDelete.MentionIDs { + if err := p.db.DeleteByID(ctx, m, >smodel.Mention{}); err != nil { + return err + } + } + + // delete all notifications for this status + if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: statusToDelete.ID}}, &[]*gtsmodel.Notification{}); err != nil { + return err + } + + // remove this status from any and all timelines + return p.deleteStatusFromTimelines(ctx, statusToDelete) +} + +// processDeleteAccountFromFederator handles Activity Delete and Object Profile +func (p *processor) processDeleteAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { + account, ok := federatorMsg.GTSModel.(*gtsmodel.Account) + if !ok { + return errors.New("account delete was not parseable as *gtsmodel.Account") + } + + return p.accountProcessor.Delete(ctx, account, account.ID) +} diff --git a/internal/processing/fromfederator_test.go b/internal/processing/fromfederator_test.go index 4f100d4cb..09519d1d3 100644 --- a/internal/processing/fromfederator_test.go +++ b/internal/processing/fromfederator_test.go @@ -32,6 +32,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/messages" + "github.com/superseriousbusiness/gotosocial/testrig" ) type FromFederatorTestSuite struct { @@ -486,6 +487,28 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() { suite.Equal("Accept", accept.Type) } +// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor. +func (suite *FromFederatorTestSuite) TestCreateStatusFromIRI() { + ctx := context.Background() + + receivingAccount := suite.testAccounts["local_account_1"] + statusCreator := suite.testAccounts["remote_account_2"] + + err := suite.processor.ProcessFromFederator(ctx, messages.FromFederator{ + APObjectType: ap.ObjectNote, + APActivityType: ap.ActivityCreate, + GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri + ReceivingAccount: receivingAccount, + APIri: testrig.URLMustParse("http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1"), + }) + suite.NoError(err) + + // status should now be in the database, attributed to remote_account_2 + s, err := suite.db.GetStatusByURI(context.Background(), "http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1") + suite.NoError(err) + suite.Equal(statusCreator.URI, s.AccountURI) +} + func TestFromFederatorTestSuite(t *testing.T) { suite.Run(t, &FromFederatorTestSuite{}) } diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go index 1c4dfb32f..7a1db442e 100644 --- a/internal/processing/processor_test.go +++ b/internal/processing/processor_test.go @@ -69,6 +69,7 @@ type ProcessingStandardTestSuite struct { testMentions map[string]*gtsmodel.Mention testAutheds map[string]*oauth.Auth testBlocks map[string]*gtsmodel.Block + testActivities map[string]testrig.ActivityWithSignature sentHTTPRequests map[string][]byte @@ -92,6 +93,7 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() { Account: suite.testAccounts["local_account_1"], }, } + suite.testActivities = testrig.NewTestActivities(suite.testAccounts) suite.testBlocks = testrig.NewTestBlocks() } @@ -149,6 +151,32 @@ func (suite *ProcessingStandardTestSuite) SetupTest() { return response, nil } + if req.URL.String() == "http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1" { + // the request is for the forwarded message + message := suite.testActivities["forwarded_message"].Activity.GetActivityStreamsObject().At(0).GetActivityStreamsNote() + messageI, err := streams.Serialize(message) + if err != nil { + panic(err) + } + messageJson, err := json.Marshal(messageI) + if err != nil { + panic(err) + } + responseType := "application/activity+json" + + reader := bytes.NewReader(messageJson) + readCloser := io.NopCloser(reader) + response := &http.Response{ + StatusCode: 200, + Body: readCloser, + ContentLength: int64(len(messageJson)), + Header: http.Header{ + "content-type": {responseType}, + }, + } + return response, nil + } + r := ioutil.NopCloser(bytes.NewReader([]byte{})) return &http.Response{ StatusCode: 200, diff --git a/internal/text/common_test.go b/internal/text/common_test.go index 6d21a0719..cb6d95c14 100644 --- a/internal/text/common_test.go +++ b/internal/text/common_test.go @@ -55,11 +55,11 @@ Text` replaceMentionsWithLinkString = `Another test @foss_satan@fossbros-anonymous.io -https://fossbros-anonymous.io/@foss_satan/statuses/6675ee73-fccc-4562-a46a-3e8cd9798060` +http://fossbros-anonymous.io/@foss_satan/statuses/6675ee73-fccc-4562-a46a-3e8cd9798060` replaceMentionsWithLinkStringExpected = `Another test @foss_satan -https://fossbros-anonymous.io/@foss_satan/statuses/6675ee73-fccc-4562-a46a-3e8cd9798060` +http://fossbros-anonymous.io/@foss_satan/statuses/6675ee73-fccc-4562-a46a-3e8cd9798060` replaceMentionsWithLinkSelfString = `Mentioning myself: @the_mighty_zork diff --git a/internal/util/uri.go b/internal/util/uri.go index 734becf13..5945c7bdd 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -62,8 +62,8 @@ type APContextKey string const ( // APActivity can be used to set and retrieve the actual go-fed pub.Activity within a context. APActivity APContextKey = "activity" - // APAccount can be used the set and retrieve the account being interacted with - APAccount APContextKey = "account" + // APReceivingAccount can be used the set and retrieve the account being interacted with / receiving an activity in their inbox. + APReceivingAccount APContextKey = "account" // APRequestingAccount can be used to set and retrieve the account of an incoming federation request. // This will often be the actor of the instance that's posting the request. APRequestingAccount APContextKey = "requestingAccount" diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 8ed5054d1..3fd542e27 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -446,6 +446,41 @@ func NewTestAccounts() map[string]*gtsmodel.Account { HideCollections: false, SuspensionOrigin: "", }, + "remote_account_2": { + ID: "01FHMQX3GAABWSM0S2VZEC2SWC", + Username: "some_user", + Domain: "example.org", + DisplayName: "some user", + Fields: []gtsmodel.Field{}, + Note: "i'm a real son of a gun", + Memorial: false, + MovedToAccountID: "", + CreatedAt: TimeMustParse("2020-08-10T14:13:28+02:00"), + UpdatedAt: time.Now().Add(-1 * time.Hour), + Bot: false, + Locked: true, + Discoverable: true, + Sensitive: false, + Language: "en", + URI: "http://example.org/users/some_user", + URL: "http://example.org/@some_user", + LastWebfingeredAt: time.Time{}, + InboxURI: "http://example.org/users/some_user/inbox", + OutboxURI: "http://example.org/users/some_user/outbox", + FollowersURI: "http://example.org/users/some_user/followers", + FollowingURI: "http://example.org/users/some_user/following", + FeaturedCollectionURI: "http://example.org/users/some_user/collections/featured", + ActorType: ap.ActorPerson, + AlsoKnownAs: "", + PrivateKey: &rsa.PrivateKey{}, + PublicKey: &rsa.PublicKey{}, + PublicKeyURI: "http://example.org/users/some_user#main-key", + SensitizedAt: time.Time{}, + SilencedAt: time.Time{}, + SuspendedAt: time.Time{}, + HideCollections: false, + SuspensionOrigin: "", + }, } // generate keys for each account @@ -1268,29 +1303,53 @@ type ActivityWithSignature struct { // their requesting signatures. func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature { dmForZork := newNote( - URLMustParse("https://fossbros-anonymous.io/users/foss_satan/statuses/5424b153-4553-4f30-9358-7b92f7cd42f6"), - URLMustParse("https://fossbros-anonymous.io/@foss_satan/5424b153-4553-4f30-9358-7b92f7cd42f6"), + URLMustParse("http://fossbros-anonymous.io/users/foss_satan/statuses/5424b153-4553-4f30-9358-7b92f7cd42f6"), + URLMustParse("http://fossbros-anonymous.io/@foss_satan/5424b153-4553-4f30-9358-7b92f7cd42f6"), time.Now(), "hey zork here's a new private note for you", "new note for zork", - URLMustParse("https://fossbros-anonymous.io/users/foss_satan"), + URLMustParse("http://fossbros-anonymous.io/users/foss_satan"), []*url.URL{URLMustParse("http://localhost:8080/users/the_mighty_zork")}, nil, true, []vocab.ActivityStreamsMention{}) createDmForZork := wrapNoteInCreate( - URLMustParse("https://fossbros-anonymous.io/users/foss_satan/statuses/5424b153-4553-4f30-9358-7b92f7cd42f6/activity"), - URLMustParse("https://fossbros-anonymous.io/users/foss_satan"), + URLMustParse("http://fossbros-anonymous.io/users/foss_satan/statuses/5424b153-4553-4f30-9358-7b92f7cd42f6/activity"), + URLMustParse("http://fossbros-anonymous.io/users/foss_satan"), time.Now(), dmForZork) - sig, digest, date := GetSignatureForActivity(createDmForZork, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].InboxURI)) + createDmForZorkSig, createDmForZorkDigest, creatDmForZorkDate := GetSignatureForActivity(createDmForZork, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].InboxURI)) + + forwardedMessage := newNote( + URLMustParse("http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1"), + URLMustParse("http://example.org/@some_user/afaba698-5740-4e32-a702-af61aa543bc1"), + time.Now(), + "this is a public status, please forward it!", + "", + URLMustParse("http://example.org/users/some_user"), + []*url.URL{URLMustParse(pub.PublicActivityPubIRI)}, + nil, + false, + []vocab.ActivityStreamsMention{}) + createForwardedMessage := wrapNoteInCreate( + URLMustParse("http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1/activity"), + URLMustParse("http://example.org/users/some_user"), + time.Now(), + forwardedMessage) + createForwardedMessageSig, createForwardedMessageDigest, createForwardedMessageDate := GetSignatureForActivity(createForwardedMessage, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].InboxURI)) return map[string]ActivityWithSignature{ "dm_for_zork": { Activity: createDmForZork, - SignatureHeader: sig, - DigestHeader: digest, - DateHeader: date, + SignatureHeader: createDmForZorkSig, + DigestHeader: createDmForZorkDigest, + DateHeader: creatDmForZorkDate, + }, + "forwarded_message": { + Activity: createForwardedMessage, + SignatureHeader: createForwardedMessageSig, + DigestHeader: createForwardedMessageDigest, + DateHeader: createForwardedMessageDate, }, } }