summaryrefslogtreecommitdiff
path: root/internal/federation/federatingdb
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-03-12 15:34:08 +0100
committerLibravatar GitHub <noreply@github.com>2024-03-12 14:34:08 +0000
commit1bcdf1da3bb10d564a6a56a89af5afa53e5cd78f (patch)
tree83716cea30d236c48e1655193c3adfc232e5bc75 /internal/federation/federatingdb
parent[chore] Update usage of OTEL libraries (#2725) (diff)
downloadgotosocial-1bcdf1da3bb10d564a6a56a89af5afa53e5cd78f.tar.xz
[feature] Process incoming `Move` activity (#2724)
* [feature] Process incoming account Move activity * fix targetAcct typo * put move origin account on fMsg * shift more move functionality back to the worker fn * simplify error logic
Diffstat (limited to 'internal/federation/federatingdb')
-rw-r--r--internal/federation/federatingdb/accept.go6
-rw-r--r--internal/federation/federatingdb/announce.go6
-rw-r--r--internal/federation/federatingdb/create.go6
-rw-r--r--internal/federation/federatingdb/db.go7
-rw-r--r--internal/federation/federatingdb/move.go182
-rw-r--r--internal/federation/federatingdb/move_test.go201
6 files changed, 408 insertions, 0 deletions
diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go
index e1d754f2e..7ec9346e0 100644
--- a/internal/federation/federatingdb/accept.go
+++ b/internal/federation/federatingdb/accept.go
@@ -49,6 +49,12 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
requestingAcct := activityContext.requestingAcct
receivingAcct := activityContext.receivingAcct
+ if requestingAcct.IsMoving() {
+ // A Moving account
+ // can't do this.
+ return nil
+ }
+
// Iterate all provided objects in the activity.
for _, object := range ap.ExtractObjects(accept) {
diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go
index 2ce6d1c59..e13e212da 100644
--- a/internal/federation/federatingdb/announce.go
+++ b/internal/federation/federatingdb/announce.go
@@ -49,6 +49,12 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre
requestingAcct := activityContext.requestingAcct
receivingAcct := activityContext.receivingAcct
+ if requestingAcct.IsMoving() {
+ // A Moving account
+ // can't do this.
+ return nil
+ }
+
// Ensure requestingAccount is among
// the Actors doing the Announce.
//
diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go
index cfb0f319b..cacaf07cf 100644
--- a/internal/federation/federatingdb/create.go
+++ b/internal/federation/federatingdb/create.go
@@ -68,6 +68,12 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
requestingAcct := activityContext.requestingAcct
receivingAcct := activityContext.receivingAcct
+ if requestingAcct.IsMoving() {
+ // A Moving account
+ // can't do this.
+ return nil
+ }
+
switch asType.GetTypeName() {
case ap.ActivityBlock:
// BLOCK SOMETHING
diff --git a/internal/federation/federatingdb/db.go b/internal/federation/federatingdb/db.go
index 2174a8003..12bd5a376 100644
--- a/internal/federation/federatingdb/db.go
+++ b/internal/federation/federatingdb/db.go
@@ -31,11 +31,18 @@ import (
// DB wraps the pub.Database interface with
// a couple of custom functions for GoToSocial.
type DB interface {
+ // Default functionality.
pub.Database
+
+ /*
+ Overridden functionality for calling from federatingProtocol.
+ */
+
Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error
Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error
Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error
Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error
+ Move(ctx context.Context, move vocab.ActivityStreamsMove) error
}
// FederatingDB uses the given state interface
diff --git a/internal/federation/federatingdb/move.go b/internal/federation/federatingdb/move.go
new file mode 100644
index 000000000..2e8049e08
--- /dev/null
+++ b/internal/federation/federatingdb/move.go
@@ -0,0 +1,182 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 gtsmodel contains types used *internally* by GoToSocial and added/removed/selected from the database.
+// These types should never be serialized and/or sent out via public APIs, as they contain sensitive information.
+// The annotation used on these structs is for handling them via the bun-db ORM.
+// See here for more info on bun model annotations: https://bun.uptrace.dev/guide/models.html
+
+package federatingdb
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "codeberg.org/gruf/go-logger/v2/level"
+ "github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+)
+
+func (f *federatingDB) Move(ctx context.Context, move vocab.ActivityStreamsMove) error {
+ if log.Level() >= level.DEBUG {
+ i, err := marshalItem(move)
+ if err != nil {
+ return err
+ }
+ l := log.WithContext(ctx).
+ WithField("move", i)
+ l.Debug("entering Move")
+ }
+
+ activityContext := getActivityContext(ctx)
+ if activityContext.internal {
+ // Already processed.
+ return nil
+ }
+
+ requestingAcct := activityContext.requestingAcct
+ receivingAcct := activityContext.receivingAcct
+
+ if requestingAcct.IsLocal() {
+ // We should not be processing
+ // a Move sent from our own
+ // instance in the federatingDB.
+ return nil
+ }
+
+ // Basic Move requirements we can
+ // check at this point already:
+ //
+ // - Move must have ID/URI set.
+ // - Move `object` and `actor` must
+ // be set, and must be the same
+ // as requesting account.
+ // - Move `target` must be set, and
+ // must *not* be the same as
+ // requesting account.
+ // - Move `target` and `object` must
+ // not have been involved in a
+ // successful Move within the
+ // last 7 days.
+ //
+ // If the Move looks OK at this point,
+ // additional requirements and checks
+ // will be processed in FromFediAPI.
+
+ // Ensure ID/URI set.
+ moveURI := ap.GetJSONLDId(move)
+ if moveURI == nil {
+ err := errors.New("Move ID/URI was nil")
+ return gtserror.SetMalformed(err)
+ }
+ moveURIStr := moveURI.String()
+
+ // Check `object` property.
+ objects := ap.GetObjectIRIs(move)
+ if l := len(objects); l != 1 {
+ err := fmt.Errorf("Move requires exactly 1 object, had %d", l)
+ return gtserror.SetMalformed(err)
+ }
+ object := objects[0]
+ objectStr := object.String()
+
+ if objectStr != requestingAcct.URI {
+ err := fmt.Errorf(
+ "Move was signed by %s but object was %s",
+ requestingAcct.URI, objectStr,
+ )
+ return gtserror.SetMalformed(err)
+ }
+
+ // Check `actor` property.
+ actors := ap.GetActorIRIs(move)
+ if l := len(actors); l != 1 {
+ err := fmt.Errorf("Move requires exactly 1 actor, had %d", l)
+ return gtserror.SetMalformed(err)
+ }
+ actor := actors[0]
+ actorStr := actor.String()
+
+ if actorStr != requestingAcct.URI {
+ err := fmt.Errorf(
+ "Move was signed by %s but actor was %s",
+ requestingAcct.URI, actorStr,
+ )
+ return gtserror.SetMalformed(err)
+ }
+
+ // Check `target` property.
+ targets := ap.GetTargetIRIs(move)
+ if l := len(targets); l != 1 {
+ err := fmt.Errorf("Move requires exactly 1 target, had %d", l)
+ return gtserror.SetMalformed(err)
+ }
+ target := targets[0]
+ targetStr := target.String()
+
+ if targetStr == requestingAcct.URI {
+ err := fmt.Errorf(
+ "Move target and origin were the same (%s)",
+ targetStr,
+ )
+ return gtserror.SetMalformed(err)
+ }
+
+ // If movedToURI is set on requestingAcct,
+ // make sure it points to the intended target.
+ //
+ // If it's not set, that's fine, we don't
+ // need it right now. We know by now that the
+ // Move was really sent to us by requestingAcct.
+ movedToURI := receivingAcct.MovedToURI
+ if movedToURI != "" &&
+ movedToURI != targetStr {
+ err := fmt.Errorf(
+ "origin account movedTo is set to %s, which differs from Move target; will not process Move",
+ movedToURI,
+ )
+ return gtserror.SetMalformed(err)
+ }
+
+ // Create a stub *gtsmodel.Move with relevant
+ // values. This will be updated / stored by the
+ // fedi api worker as necessary.
+ stubMove := &gtsmodel.Move{
+ OriginURI: objectStr,
+ Origin: object,
+ TargetURI: targetStr,
+ Target: target,
+ URI: moveURIStr,
+ }
+
+ // We had a Move already or stored a new Move.
+ // Pass back to a worker for async processing.
+ f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
+ APObjectType: ap.ObjectProfile,
+ APActivityType: ap.ActivityMove,
+ GTSModel: stubMove,
+ RequestingAccount: requestingAcct,
+ ReceivingAccount: receivingAcct,
+ })
+
+ return nil
+}
diff --git a/internal/federation/federatingdb/move_test.go b/internal/federation/federatingdb/move_test.go
new file mode 100644
index 000000000..006dcf0dc
--- /dev/null
+++ b/internal/federation/federatingdb/move_test.go
@@ -0,0 +1,201 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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 (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+)
+
+type MoveTestSuite struct {
+ FederatingDBTestSuite
+}
+
+func (suite *MoveTestSuite) move(
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+ moveStr string,
+) error {
+ ctx := createTestContext(receivingAcct, requestingAcct)
+
+ rawMove := make(map[string]interface{})
+ if err := json.Unmarshal([]byte(moveStr), &rawMove); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ t, err := streams.ToType(ctx, rawMove)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ move, ok := t.(vocab.ActivityStreamsMove)
+ if !ok {
+ suite.FailNow("", "couldn't cast %T to Move", t)
+ }
+
+ return suite.federatingDB.Move(ctx, move)
+}
+
+func (suite *MoveTestSuite) TestMove() {
+ var (
+ receivingAcct = suite.testAccounts["local_account_1"]
+ requestingAcct = suite.testAccounts["remote_account_1"]
+ moveStr1 = `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9",
+ "actor": "http://fossbros-anonymous.io/users/foss_satan",
+ "type": "Move",
+ "object": "http://fossbros-anonymous.io/users/foss_satan",
+ "target": "https://turnip.farm/users/turniplover6969",
+ "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
+}`
+ )
+
+ // Trigger the move.
+ suite.move(receivingAcct, requestingAcct, moveStr1)
+
+ // Should be a message heading to the processor.
+ var msg messages.FromFediAPI
+ select {
+ case msg = <-suite.fromFederator:
+ // Fine.
+ case <-time.After(5 * time.Second):
+ suite.FailNow("", "timeout waiting for suite.fromFederator")
+ }
+ suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActivityMove, msg.APActivityType)
+
+ // Stub Move should be on the message.
+ move, ok := msg.GTSModel.(*gtsmodel.Move)
+ if !ok {
+ suite.FailNow("", "could not cast %T to *gtsmodel.Move", msg.GTSModel)
+ }
+ suite.Equal("http://fossbros-anonymous.io/users/foss_satan", move.OriginURI)
+ suite.Equal("https://turnip.farm/users/turniplover6969", move.TargetURI)
+
+ // Trigger the same move again.
+ suite.move(receivingAcct, requestingAcct, moveStr1)
+
+ // Should be a message heading to the processor
+ // since this is just a straight up retry.
+ select {
+ case msg = <-suite.fromFederator:
+ // Fine.
+ case <-time.After(5 * time.Second):
+ suite.FailNow("", "timeout waiting for suite.fromFederator")
+ }
+ suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActivityMove, msg.APActivityType)
+
+ // Same as the first Move, but with a different ID.
+ moveStr2 := `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9XWDD25CKXHW82MYD1GDAR",
+ "actor": "http://fossbros-anonymous.io/users/foss_satan",
+ "type": "Move",
+ "object": "http://fossbros-anonymous.io/users/foss_satan",
+ "target": "https://turnip.farm/users/turniplover6969",
+ "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
+}`
+
+ // Trigger the move.
+ suite.move(receivingAcct, requestingAcct, moveStr2)
+
+ // Should be a message heading to the processor
+ // since this is just a retry with a different ID.
+ select {
+ case msg = <-suite.fromFederator:
+ // Fine.
+ case <-time.After(5 * time.Second):
+ suite.FailNow("", "timeout waiting for suite.fromFederator")
+ }
+ suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActivityMove, msg.APActivityType)
+}
+
+func (suite *MoveTestSuite) TestBadMoves() {
+ var (
+ receivingAcct = suite.testAccounts["local_account_1"]
+ requestingAcct = suite.testAccounts["remote_account_1"]
+ )
+
+ type testStruct struct {
+ moveStr string
+ err string
+ }
+
+ for _, t := range []testStruct{
+ {
+ // Move signed by someone else.
+ moveStr: `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9",
+ "actor": "http://fossbros-anonymous.io/users/someone_else",
+ "type": "Move",
+ "object": "http://fossbros-anonymous.io/users/foss_satan",
+ "target": "https://turnip.farm/users/turniplover6969",
+ "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
+}`,
+ err: "Move was signed by http://fossbros-anonymous.io/users/foss_satan but actor was http://fossbros-anonymous.io/users/someone_else",
+ },
+ {
+ // Actor and object not the same.
+ moveStr: `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9",
+ "actor": "http://fossbros-anonymous.io/users/foss_satan",
+ "type": "Move",
+ "object": "http://fossbros-anonymous.io/users/someone_else",
+ "target": "https://turnip.farm/users/turniplover6969",
+ "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
+}`,
+ err: "Move was signed by http://fossbros-anonymous.io/users/foss_satan but object was http://fossbros-anonymous.io/users/someone_else",
+ },
+ {
+ // Object and target the same.
+ moveStr: `{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9",
+ "actor": "http://fossbros-anonymous.io/users/foss_satan",
+ "type": "Move",
+ "object": "http://fossbros-anonymous.io/users/foss_satan",
+ "target": "http://fossbros-anonymous.io/users/foss_satan",
+ "to": "http://fossbros-anonymous.io/users/foss_satan/followers"
+}`,
+ err: "Move target and origin were the same (http://fossbros-anonymous.io/users/foss_satan)",
+ },
+ } {
+ // Trigger the move.
+ err := suite.move(receivingAcct, requestingAcct, t.moveStr)
+ if t.err != "" {
+ suite.EqualError(err, t.err)
+ }
+ }
+}
+
+func TestMoveTestSuite(t *testing.T) {
+ suite.Run(t, &MoveTestSuite{})
+}