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