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
This commit is contained in:
tobi 2021-10-10 12:39:25 +02:00 committed by GitHub
parent 3dc7644ae6
commit 367bdca250
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 765 additions and 383 deletions

View file

@ -48,12 +48,9 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
l.Debug("entering Accept") l.Debug("entering Accept")
} }
targetAcct, fromFederatorChan, err := extractFromCtx(ctx) receivingAccount, _, fromFederatorChan := extractFromCtx(ctx)
if err != nil { if receivingAccount == nil || fromFederatorChan == nil {
return err // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass
}
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
// through the API, but came from inside GtS as the result of another activity on this instance. That being so, // 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. // we can safely just ignore this activity, since we know we've already processed it elsewhere.
return nil 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 // 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") return errors.New("ACCEPT: follow object account and inbox account were not the same")
} }
follow, err := f.db.AcceptFollowRequest(ctx, gtsFollowRequest.AccountID, gtsFollowRequest.TargetAccountID) 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, APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept, APActivityType: ap.ActivityAccept,
GTSModel: follow, GTSModel: follow,
ReceivingAccount: targetAcct, ReceivingAccount: receivingAccount,
} }
return nil 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) 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 // 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") return errors.New("ACCEPT: follow object account and inbox account were not the same")
} }
follow, err := f.db.AcceptFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID) 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, APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept, APActivityType: ap.ActivityAccept,
GTSModel: follow, GTSModel: follow,
ReceivingAccount: targetAcct, ReceivingAccount: receivingAccount,
} }
return nil return nil

View file

@ -44,12 +44,9 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre
l.Debug("entering Announce") l.Debug("entering Announce")
} }
targetAcct, fromFederatorChan, err := extractFromCtx(ctx) receivingAccount, _, fromFederatorChan := extractFromCtx(ctx)
if err != nil { if receivingAccount == nil || fromFederatorChan == nil {
return err // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass
}
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
// through the API, but came from inside GtS as the result of another activity on this instance. That being so, // 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. // we can safely just ignore this activity, since we know we've already processed it elsewhere.
return nil return nil
@ -57,7 +54,7 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre
boost, isNew, err := f.typeConverter.ASAnnounceToStatus(ctx, announce) boost, isNew, err := f.typeConverter.ASAnnounceToStatus(ctx, announce)
if err != nil { 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 { if !isNew {
@ -70,7 +67,7 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre
APObjectType: ap.ActivityAnnounce, APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate, APActivityType: ap.ActivityCreate,
GTSModel: boost, GTSModel: boost,
ReceivingAccount: targetAcct, ReceivingAccount: receivingAccount,
} }
return nil return nil

View file

@ -22,11 +22,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"github.com/go-fed/activity/streams/vocab" "github.com/go-fed/activity/streams/vocab"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/messages" "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") l.Debug("entering Create")
} }
targetAcct, fromFederatorChan, err := extractFromCtx(ctx) receivingAccount, requestingAccount, fromFederatorChan := extractFromCtx(ctx)
if err != nil { if receivingAccount == nil || fromFederatorChan == nil {
return err // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass
}
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
// through the API, but came from inside GtS as the result of another activity on this instance. That being so, // 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. // we can safely just ignore this activity, since we know we've already processed it elsewhere.
return nil return nil
} }
switch asType.GetTypeName() { 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: case ap.ActivityBlock:
// BLOCK SOMETHING // BLOCK SOMETHING
blockable, ok := asType.(vocab.ActivityStreamsBlock) return f.activityBlock(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan)
if !ok { case ap.ActivityCreate:
return errors.New("CREATE: could not convert type to block") // CREATE SOMETHING
} return f.activityCreate(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan)
case ap.ActivityFollow:
block, err := f.typeConverter.ASBlockToBlock(ctx, blockable) // FOLLOW SOMETHING
if err != nil { return f.activityFollow(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan)
return fmt.Errorf("CREATE: could not convert Block to gts model block") case ap.ActivityLike:
} // LIKE SOMETHING
return f.activityLike(ctx, asType, receivingAccount, requestingAccount, fromFederatorChan)
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 nil 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
}

View file

@ -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{})
}

View file

@ -44,12 +44,9 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
) )
l.Debug("entering Delete") l.Debug("entering Delete")
targetAcct, fromFederatorChan, err := extractFromCtx(ctx) receivingAccount, _, fromFederatorChan := extractFromCtx(ctx)
if err != nil { if receivingAccount == nil || fromFederatorChan == nil {
return err // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass
}
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
// through the API, but came from inside GtS as the result of another activity on this instance. That being so, // 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. // we can safely just ignore this activity, since we know we've already processed it elsewhere.
return nil return nil
@ -68,7 +65,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
APObjectType: ap.ObjectNote, APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityDelete, APActivityType: ap.ActivityDelete,
GTSModel: s, GTSModel: s,
ReceivingAccount: targetAcct, ReceivingAccount: receivingAccount,
} }
} }
@ -80,7 +77,7 @@ func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
APObjectType: ap.ObjectProfile, APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityDelete, APActivityType: ap.ActivityDelete,
GTSModel: a, GTSModel: a,
ReceivingAccount: targetAcct, ReceivingAccount: receivingAccount,
} }
} }

View file

@ -19,13 +19,17 @@
package federatingdb_test package federatingdb_test
import ( import (
"context"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig" "github.com/superseriousbusiness/gotosocial/testrig"
) )
@ -45,6 +49,7 @@ type FederatingDBTestSuite struct {
testAttachments map[string]*gtsmodel.MediaAttachment testAttachments map[string]*gtsmodel.MediaAttachment
testStatuses map[string]*gtsmodel.Status testStatuses map[string]*gtsmodel.Status
testBlocks map[string]*gtsmodel.Block testBlocks map[string]*gtsmodel.Block
testActivities map[string]testrig.ActivityWithSignature
} }
func (suite *FederatingDBTestSuite) SetupSuite() { func (suite *FederatingDBTestSuite) SetupSuite() {
@ -56,6 +61,7 @@ func (suite *FederatingDBTestSuite) SetupSuite() {
suite.testAttachments = testrig.NewTestAttachments() suite.testAttachments = testrig.NewTestAttachments()
suite.testStatuses = testrig.NewTestStatuses() suite.testStatuses = testrig.NewTestStatuses()
suite.testBlocks = testrig.NewTestBlocks() suite.testBlocks = testrig.NewTestBlocks()
suite.testActivities = testrig.NewTestActivities(suite.testAccounts)
} }
func (suite *FederatingDBTestSuite) SetupTest() { func (suite *FederatingDBTestSuite) SetupTest() {
@ -70,3 +76,11 @@ func (suite *FederatingDBTestSuite) SetupTest() {
func (suite *FederatingDBTestSuite) TearDownTest() { func (suite *FederatingDBTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db) 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
}

View file

@ -46,12 +46,9 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)
l.Debug("entering Undo") l.Debug("entering Undo")
} }
targetAcct, fromFederatorChan, err := extractFromCtx(ctx) receivingAccount, _, fromFederatorChan := extractFromCtx(ctx)
if err != nil { if receivingAccount == nil || fromFederatorChan == nil {
return err // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass
}
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
// through the API, but came from inside GtS as the result of another activity on this instance. That being so, // 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. // we can safely just ignore this activity, since we know we've already processed it elsewhere.
return nil 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) 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 // 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") return errors.New("UNDO: follow object account and inbox account were not the same")
} }
// delete any existing FOLLOW // 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) 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 // 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") return errors.New("UNDO: block object account and inbox account were not the same")
} }
// delete any existing BLOCK // delete any existing BLOCK

View file

@ -56,12 +56,9 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
l.Debug("entering Update") l.Debug("entering Update")
} }
targetAcct, fromFederatorChan, err := extractFromCtx(ctx) receivingAccount, _, fromFederatorChan := extractFromCtx(ctx)
if err != nil { if receivingAccount == nil || fromFederatorChan == nil {
return err // If the receiving account or federator channel wasn't set on the context, that means this request didn't pass
}
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
// through the API, but came from inside GtS as the result of another activity on this instance. That being so, // 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. // we can safely just ignore this activity, since we know we've already processed it elsewhere.
return nil return nil
@ -153,7 +150,7 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
APObjectType: ap.ObjectProfile, APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityUpdate, APActivityType: ap.ActivityUpdate,
GTSModel: updatedAcct, GTSModel: updatedAcct,
ReceivingAccount: targetAcct, ReceivingAccount: receivingAccount,
} }
} }

View file

@ -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: // 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 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. // - A channel that messages for the processor can be placed into.
func extractFromCtx(ctx context.Context) (*gtsmodel.Account, chan messages.FromFederator, error) { // If a value is not present, nil will be returned for it. It's up to the caller to check this and respond appropriately.
var targetAcct *gtsmodel.Account func extractFromCtx(ctx context.Context) (receivingAccount, requestingAccount *gtsmodel.Account, fromFederatorChan chan messages.FromFederator) {
targetAcctI := ctx.Value(util.APAccount) receivingAccountI := ctx.Value(util.APReceivingAccount)
if targetAcctI != nil { if receivingAccountI != nil {
var ok bool receivingAccount = receivingAccountI.(*gtsmodel.Account)
targetAcct, ok = targetAcctI.(*gtsmodel.Account) }
if !ok {
return nil, nil, errors.New("extractFromCtx: account value in context not parseable") requestingAcctI := ctx.Value(util.APRequestingAccount)
} if requestingAcctI != nil {
requestingAccount = requestingAcctI.(*gtsmodel.Account)
} }
var fromFederatorChan chan messages.FromFederator
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey) fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
if fromFederatorChanI != nil { if fromFederatorChanI != nil {
var ok bool fromFederatorChan = fromFederatorChanI.(chan messages.FromFederator)
fromFederatorChan, ok = fromFederatorChanI.(chan messages.FromFederator)
if !ok {
return nil, nil, errors.New("extractFromCtx: fromFederatorChan value in context not parseable")
}
} }
return targetAcct, fromFederatorChan, nil return
} }
func marshalItem(item vocab.Type) (string, error) { func marshalItem(item vocab.Type) (string, error) {

View file

@ -113,12 +113,12 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
return nil, false, errors.New("username was empty") 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 { 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 { if err != nil {
l.Debugf("request not authenticated: %s", err) l.Debugf("request not authenticated: %s", err)
return ctx, false, 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) requestingAccount, _, err := f.GetRemoteAccount(ctx, username, publicKeyOwnerURI, false)
if err != nil { 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) withRequesting := context.WithValue(ctx, util.APRequestingAccount, requestingAccount)
withRequested := context.WithValue(withRequester, util.APAccount, requestedAccount) withReceiving := context.WithValue(withRequesting, util.APReceivingAccount, receivingAccount)
return withRequested, true, nil return withReceiving, true, nil
} }
// Blocked should determine whether to permit a set of actors given by // 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) l.Debugf("entering BLOCKED function with IRI list: %+v", actorIRIs)
requestedAccountI := ctx.Value(util.APAccount) receivingAccountI := ctx.Value(util.APReceivingAccount)
requestedAccount, ok := requestedAccountI.(*gtsmodel.Account) receivingAccount, ok := receivingAccountI.(*gtsmodel.Account)
if !ok { if !ok {
f.log.Errorf("requested account not set on request context") f.log.Errorf("receiving account not set on request context")
return false, errors.New("requested account not set on request context, so couldn't determine blocks") return false, errors.New("receiving account not set on request context, so couldn't determine blocks")
} }
blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs) 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) 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 { if err != nil {
return false, fmt.Errorf("error checking account block: %s", err) return false, fmt.Errorf("error checking account block: %s", err)
} }
if blocked { 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 return true, nil
} }
} }

View file

@ -125,7 +125,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
ctx := context.Background() ctx := context.Background()
// by the time AuthenticatePostInbox is called, PostInboxRequestBodyHook should have already been called, // 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: // 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) ctxWithActivity := context.WithValue(ctxWithAccount, util.APActivity, activity)
ctxWithVerifier := context.WithValue(ctxWithActivity, util.APRequestingPublicKeyVerifier, verifier) ctxWithVerifier := context.WithValue(ctxWithActivity, util.APRequestingPublicKeyVerifier, verifier)
ctxWithSignature := context.WithValue(ctxWithVerifier, util.APRequestingPublicKeySignature, activity.SignatureHeader) ctxWithSignature := context.WithValue(ctxWithVerifier, util.APRequestingPublicKeySignature, activity.SignatureHeader)

View file

@ -18,7 +18,11 @@
package messages 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. // FromClientAPI wraps a message that travels from the client API into the processor.
type FromClientAPI struct { type FromClientAPI struct {
@ -31,8 +35,9 @@ type FromClientAPI struct {
// FromFederator wraps a message that travels from the federator into the processor. // FromFederator wraps a message that travels from the federator into the processor.
type FromFederator struct { type FromFederator struct {
APObjectType string APObjectType string // what is the object type of this message? eg., Note, Profile etc.
APActivityType string APActivityType string // what is the activity type of this message? eg., Create, Follow etc.
GTSModel interface{} APIri *url.URL // what is the IRI ID of this activity?
ReceivingAccount *gtsmodel.Account 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?
} }

View file

@ -31,203 +31,264 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/messages" "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 { func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
l := p.log.WithFields(logrus.Fields{ l := p.log.WithFields(logrus.Fields{
"func": "processFromFederator", "func": "processFromFederator",
"federatorMsg": fmt.Sprintf("%+v", federatorMsg), "APActivityType": federatorMsg.APActivityType,
"APObjectType": federatorMsg.APObjectType,
}) })
l.Trace("processing message from federator")
l.Trace("entering function PROCESS FROM FEDERATOR")
switch federatorMsg.APActivityType { switch federatorMsg.APActivityType {
case ap.ActivityCreate: case ap.ActivityCreate:
// CREATE // CREATE SOMETHING
switch federatorMsg.APObjectType { switch federatorMsg.APObjectType {
case ap.ObjectNote: case ap.ObjectNote:
// CREATE A STATUS // CREATE A STATUS
incomingStatus, ok := federatorMsg.GTSModel.(*gtsmodel.Status) return p.processCreateStatusFromFederator(ctx, federatorMsg)
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
case ap.ActivityLike: case ap.ActivityLike:
// CREATE A FAVE // CREATE A FAVE
incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave) return p.processCreateFaveFromFederator(ctx, federatorMsg)
if !ok {
return errors.New("like was not parseable as *gtsmodel.StatusFave")
}
if err := p.notifyFave(ctx, incomingFave); err != nil {
return err
}
case ap.ActivityFollow: case ap.ActivityFollow:
// CREATE A FOLLOW REQUEST // CREATE A FOLLOW REQUEST
followRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest) return p.processCreateFollowRequestFromFederator(ctx, federatorMsg)
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)
case ap.ActivityAnnounce: case ap.ActivityAnnounce:
// CREATE AN ANNOUNCE // CREATE AN ANNOUNCE
incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status) return p.processCreateAnnounceFromFederator(ctx, federatorMsg)
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
}
case ap.ActivityBlock: case ap.ActivityBlock:
// CREATE A BLOCK // CREATE A BLOCK
block, ok := federatorMsg.GTSModel.(*gtsmodel.Block) return p.processCreateBlockFromFederator(ctx, federatorMsg)
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
} }
case ap.ActivityUpdate: case ap.ActivityUpdate:
// UPDATE // UPDATE SOMETHING
switch federatorMsg.APObjectType { switch federatorMsg.APObjectType {
case ap.ObjectProfile: case ap.ObjectProfile:
// UPDATE AN ACCOUNT // UPDATE AN ACCOUNT
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) return p.processUpdateAccountFromFederator(ctx, federatorMsg)
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)
}
} }
case ap.ActivityDelete: case ap.ActivityDelete:
// DELETE // DELETE SOMETHING
switch federatorMsg.APObjectType { switch federatorMsg.APObjectType {
case ap.ObjectNote: case ap.ObjectNote:
// DELETE A STATUS // DELETE A STATUS
// TODO: handle side effects of status deletion here: return p.processDeleteStatusFromFederator(ctx, federatorMsg)
// 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, &gtsmodel.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)
case ap.ObjectProfile: case ap.ObjectProfile:
// DELETE A PROFILE/ACCOUNT // DELETE A PROFILE/ACCOUNT
// handle side effects of account deletion here: delete all objects, statuses, media etc associated with account return p.processDeleteAccountFromFederator(ctx, federatorMsg)
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) // 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 var err error
switch federatorMsg.APObjectType { status, err = p.federator.EnrichRemoteStatus(ctx, federatorMsg.ReceivingAccount.Username, status, true)
case ap.ActivityFollow: if err != nil {
// ACCEPT A FOLLOW return err
// nothing to do here
} }
} 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 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, &gtsmodel.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)
}

View file

@ -32,6 +32,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/testrig"
) )
type FromFederatorTestSuite struct { type FromFederatorTestSuite struct {
@ -486,6 +487,28 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() {
suite.Equal("Accept", accept.Type) 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) { func TestFromFederatorTestSuite(t *testing.T) {
suite.Run(t, &FromFederatorTestSuite{}) suite.Run(t, &FromFederatorTestSuite{})
} }

View file

@ -69,6 +69,7 @@ type ProcessingStandardTestSuite struct {
testMentions map[string]*gtsmodel.Mention testMentions map[string]*gtsmodel.Mention
testAutheds map[string]*oauth.Auth testAutheds map[string]*oauth.Auth
testBlocks map[string]*gtsmodel.Block testBlocks map[string]*gtsmodel.Block
testActivities map[string]testrig.ActivityWithSignature
sentHTTPRequests map[string][]byte sentHTTPRequests map[string][]byte
@ -92,6 +93,7 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() {
Account: suite.testAccounts["local_account_1"], Account: suite.testAccounts["local_account_1"],
}, },
} }
suite.testActivities = testrig.NewTestActivities(suite.testAccounts)
suite.testBlocks = testrig.NewTestBlocks() suite.testBlocks = testrig.NewTestBlocks()
} }
@ -149,6 +151,32 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
return response, nil 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{})) r := ioutil.NopCloser(bytes.NewReader([]byte{}))
return &http.Response{ return &http.Response{
StatusCode: 200, StatusCode: 200,

View file

@ -55,11 +55,11 @@ Text`
replaceMentionsWithLinkString = `Another test @foss_satan@fossbros-anonymous.io 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> 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 replaceMentionsWithLinkSelfString = `Mentioning myself: @the_mighty_zork

View file

@ -62,8 +62,8 @@ type APContextKey string
const ( const (
// APActivity can be used to set and retrieve the actual go-fed pub.Activity within a context. // APActivity can be used to set and retrieve the actual go-fed pub.Activity within a context.
APActivity APContextKey = "activity" APActivity APContextKey = "activity"
// APAccount can be used the set and retrieve the account being interacted with // APReceivingAccount can be used the set and retrieve the account being interacted with / receiving an activity in their inbox.
APAccount APContextKey = "account" APReceivingAccount APContextKey = "account"
// APRequestingAccount can be used to set and retrieve the account of an incoming federation request. // 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. // This will often be the actor of the instance that's posting the request.
APRequestingAccount APContextKey = "requestingAccount" APRequestingAccount APContextKey = "requestingAccount"

View file

@ -446,6 +446,41 @@ func NewTestAccounts() map[string]*gtsmodel.Account {
HideCollections: false, HideCollections: false,
SuspensionOrigin: "", 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 // generate keys for each account
@ -1268,29 +1303,53 @@ type ActivityWithSignature struct {
// their requesting signatures. // their requesting signatures.
func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature { func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]ActivityWithSignature {
dmForZork := newNote( dmForZork := newNote(
URLMustParse("https://fossbros-anonymous.io/users/foss_satan/statuses/5424b153-4553-4f30-9358-7b92f7cd42f6"), URLMustParse("http://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/@foss_satan/5424b153-4553-4f30-9358-7b92f7cd42f6"),
time.Now(), time.Now(),
"hey zork here's a new private note for you", "hey zork here's a new private note for you",
"new note for zork", "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")}, []*url.URL{URLMustParse("http://localhost:8080/users/the_mighty_zork")},
nil, nil,
true, true,
[]vocab.ActivityStreamsMention{}) []vocab.ActivityStreamsMention{})
createDmForZork := wrapNoteInCreate( createDmForZork := wrapNoteInCreate(
URLMustParse("https://fossbros-anonymous.io/users/foss_satan/statuses/5424b153-4553-4f30-9358-7b92f7cd42f6/activity"), URLMustParse("http://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"),
time.Now(), time.Now(),
dmForZork) 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{ return map[string]ActivityWithSignature{
"dm_for_zork": { "dm_for_zork": {
Activity: createDmForZork, Activity: createDmForZork,
SignatureHeader: sig, SignatureHeader: createDmForZorkSig,
DigestHeader: digest, DigestHeader: createDmForZorkDigest,
DateHeader: date, DateHeader: creatDmForZorkDate,
},
"forwarded_message": {
Activity: createForwardedMessage,
SignatureHeader: createForwardedMessageSig,
DigestHeader: createForwardedMessageDigest,
DateHeader: createForwardedMessageDate,
}, },
} }
} }