[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
This commit is contained in:
tobi
2024-03-12 15:34:08 +01:00
committed by GitHub
parent 5e871e81a8
commit 1bcdf1da3b
16 changed files with 1149 additions and 16 deletions

View File

@@ -64,8 +64,8 @@ func accountFresh(
return true
}
if !account.SuspendedAt.IsZero() {
// Can't refresh
if account.IsSuspended() {
// Can't/won't refresh
// suspended accounts.
return true
}
@@ -388,8 +388,9 @@ func (d *Dereferencer) enrichAccountSafely(
account *gtsmodel.Account,
accountable ap.Accountable,
) (*gtsmodel.Account, ap.Accountable, error) {
// Noop if account has been suspended.
if !account.SuspendedAt.IsZero() {
// Noop if account suspended;
// we don't want to deref it.
if account.IsSuspended() {
return account, nil, nil
}

View File

@@ -64,6 +64,16 @@ var (
// This is tuned to be quite fresh without
// causing loads of dereferencing calls.
Fresh = util.Ptr(FreshnessWindow(5 * time.Minute))
// 10 seconds.
//
// Freshest is useful when you want an
// immediately up to date model of something
// that's even fresher than Fresh.
//
// Be careful using this one; it can cause
// lots of unnecessary traffic if used unwisely.
Freshest = util.Ptr(FreshnessWindow(10 * time.Second))
)
// Dereferencer wraps logic and functionality for doing dereferencing

View File

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

View File

@@ -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.
//

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -450,7 +450,11 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
//
// Applications are not expected to handle every single ActivityStreams
// type and extension. The unhandled ones are passed to DefaultCallback.
func (f *Federator) FederatingCallbacks(ctx context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) {
func (f *Federator) FederatingCallbacks(ctx context.Context) (
wrapped pub.FederatingWrappedCallbacks,
other []any,
err error,
) {
wrapped = pub.FederatingWrappedCallbacks{
// OnFollow determines what action to take for this
// particular callback if a Follow Activity is handled.
@@ -461,7 +465,7 @@ func (f *Federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
}
// Override some default behaviors to trigger our own side effects.
other = []interface{}{
other = []any{
func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
return f.FederatingDB().Undo(ctx, undo)
},
@@ -476,6 +480,14 @@ func (f *Federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
},
}
// Define some of our own behaviors which are not
// overrides of the default pub.FederatingWrappedCallbacks.
other = append(other, []any{
func(ctx context.Context, move vocab.ActivityStreamsMove) error {
return f.FederatingDB().Move(ctx, move)
},
}...)
return
}