diff options
| author | 2021-10-10 12:39:25 +0200 | |
|---|---|---|
| committer | 2021-10-10 12:39:25 +0200 | |
| commit | 367bdca25093ee76b36506d8a5e6733b0aa2e2bb (patch) | |
| tree | 37b1b82ae6e9fad7e6d95b8abbb58bdb42049707 | |
| parent | Derive visibility fixes (#271) (diff) | |
| download | gotosocial-367bdca25093ee76b36506d8a5e6733b0aa2e2bb.tar.xz | |
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
| -rw-r--r-- | internal/federation/federatingdb/accept.go | 17 | ||||
| -rw-r--r-- | internal/federation/federatingdb/announce.go | 13 | ||||
| -rw-r--r-- | internal/federation/federatingdb/create.go | 337 | ||||
| -rw-r--r-- | internal/federation/federatingdb/create_test.go | 91 | ||||
| -rw-r--r-- | internal/federation/federatingdb/delete.go | 13 | ||||
| -rw-r--r-- | internal/federation/federatingdb/federatingdb_test.go | 14 | ||||
| -rw-r--r-- | internal/federation/federatingdb/undo.go | 13 | ||||
| -rw-r--r-- | internal/federation/federatingdb/update.go | 11 | ||||
| -rw-r--r-- | internal/federation/federatingdb/util.go | 29 | ||||
| -rw-r--r-- | internal/federation/federatingprotocol.go | 26 | ||||
| -rw-r--r-- | internal/federation/federator_test.go | 2 | ||||
| -rw-r--r-- | internal/messages/messages.go | 15 | ||||
| -rw-r--r-- | internal/processing/fromfederator.go | 389 | ||||
| -rw-r--r-- | internal/processing/fromfederator_test.go | 23 | ||||
| -rw-r--r-- | internal/processing/processor_test.go | 28 | ||||
| -rw-r--r-- | internal/text/common_test.go | 4 | ||||
| -rw-r--r-- | internal/util/uri.go | 4 | ||||
| -rw-r--r-- | testrig/testmodels.go | 77 | 
18 files changed, 744 insertions, 362 deletions
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.ActivityBlock: +		// BLOCK SOMETHING +		return f.activityBlock(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan)  	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, -				} -			} -		} +		return f.activityCreate(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan)  	case ap.ActivityFollow:  		// FOLLOW SOMETHING -		follow, ok := asType.(vocab.ActivityStreamsFollow) -		if !ok { -			return errors.New("CREATE: could not convert type to follow") -		} +		return f.activityFollow(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan) +	case ap.ActivityLike: +		// LIKE SOMETHING +		return f.activityLike(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan) +	} +	return nil +} -		followRequest, err := f.typeConverter.ASFollowToFollowRequest(ctx, follow) -		if err != nil { -			return fmt.Errorf("CREATE: could not convert Follow to follow request: %s", err) -		} +/* +	BLOCK HANDLERS +*/ -		newID, err := id.NewULID() -		if err != nil { -			return err -		} -		followRequest.ID = newID +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") +	} -		if err := f.db.Put(ctx, followRequest); err != nil { -			return fmt.Errorf("CREATE: database error inserting follow request: %s", err) -		} +	block, err := f.typeConverter.ASBlockToBlock(ctx, blockable) +	if err != nil { +		return fmt.Errorf("activityBlock: could not convert Block to gts model block") +	} -		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") -		} +	newID, err := id.NewULID() +	if err != nil { +		return err +	} +	block.ID = newID -		fave, err := f.typeConverter.ASLikeToFave(ctx, like) -		if err != nil { -			return fmt.Errorf("CREATE: could not convert Like to fave: %s", err) +	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  		} -		newID, err := id.NewULID() -		if err != nil { -			return err +		// 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()))  		} -		fave.ID = newID +	} + +	if len(errs) != 0 { +		return fmt.Errorf("activityCreate: one or more errors while processing activity: %s", strings.Join(errs, "; ")) +	} -		if err := f.db.Put(ctx, fave); err != nil { -			return fmt.Errorf("CREATE: database error inserting fave: %s", err) +	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.ActivityLike, +			APObjectType:     ap.ObjectNote,  			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") +			APIri:            id.GetIRI(), +			GTSModel:         nil, +			ReceivingAccount: receivingAccount,  		} +		return nil +	} -		block, err := f.typeConverter.ASBlockToBlock(ctx, blockable) -		if err != nil { -			return fmt.Errorf("CREATE: could not convert Block to gts model block") -		} +	// if we reach this point, we know it's not a forwarded status, so proceed with processing it as normal -		newID, err := id.NewULID() -		if err != nil { -			return err -		} -		block.ID = newID +	status, err := f.typeConverter.ASStatusToStatus(ctx, note) +	if err != nil { +		return fmt.Errorf("createNote: error converting note to status: %s", err) +	} -		if err := f.db.Put(ctx, block); err != nil { -			return fmt.Errorf("CREATE: database error inserting block: %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 -		fromFederatorChan <- messages.FromFederator{ -			APObjectType:     ap.ActivityBlock, -			APActivityType:   ap.ActivityCreate, -			GTSModel:         block, -			ReceivingAccount: targetAcct, +	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 <http://www.gnu.org/licenses/>. +*/ + +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 <span class="h-card"><a href="http://fossbros-anonymous.io/@foss_satan" class="u-url mention">@<span>foss_satan</span></a></span> -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,  		},  	}  }  | 
