From 61141ac2324fd73221f8301ba3805c051bc99fe2 Mon Sep 17 00:00:00 2001
From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com>
Date: Tue, 28 Jan 2025 20:22:23 +0000
Subject: [PATCH] [chore] remove type switch in Create() and instead move to
FederatedCallbacks() (#3697)
* remove type switch in Create() and instead move to FederatedCallbacks()
* add missing (my bad!) federating wrapped callbacks behaviour
* add missing license header :innocent:
* fix create flag test to use correct function
---
internal/federation/authenticate.go | 3 +-
internal/federation/federatingdb/block.go | 87 +++++
internal/federation/federatingdb/create.go | 330 ++----------------
.../federation/federatingdb/create_test.go | 5 +-
internal/federation/federatingdb/db.go | 29 +-
internal/federation/federatingdb/flag.go | 91 +++++
internal/federation/federatingdb/follow.go | 84 +++++
internal/federation/federatingdb/like.go | 147 ++++++++
internal/federation/federatingdb/move.go | 3 +
internal/federation/federatingdb/update.go | 12 +-
internal/federation/federatingprotocol.go | 35 +-
internal/federation/federator.go | 56 ++-
internal/federation/transport.go | 2 +-
internal/gtserror/withcode.go | 159 +++++----
14 files changed, 610 insertions(+), 433 deletions(-)
create mode 100644 internal/federation/federatingdb/block.go
create mode 100644 internal/federation/federatingdb/flag.go
create mode 100644 internal/federation/federatingdb/follow.go
create mode 100644 internal/federation/federatingdb/like.go
diff --git a/internal/federation/authenticate.go b/internal/federation/authenticate.go
index e9263d43c..c06b8e72b 100644
--- a/internal/federation/authenticate.go
+++ b/internal/federation/authenticate.go
@@ -404,7 +404,8 @@ func (f *Federator) callForPubKey(
pubKeyID *url.URL,
) ([]byte, gtserror.WithCode) {
// Use a transport to dereference the remote.
- trans, err := f.transportController.NewTransportForUsername(
+ trans, err := f.transport.NewTransportForUsername(
+
// We're on a hot path: don't retry if req fails.
gtscontext.SetFastFail(ctx),
requestedUsername,
diff --git a/internal/federation/federatingdb/block.go b/internal/federation/federatingdb/block.go
new file mode 100644
index 000000000..461a8748e
--- /dev/null
+++ b/internal/federation/federatingdb/block.go
@@ -0,0 +1,87 @@
+// 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 .
+
+package federatingdb
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+)
+
+func (f *federatingDB) Block(ctx context.Context, blockable vocab.ActivityStreamsBlock) error {
+ log.DebugKV(ctx, "block", serialize{blockable})
+
+ // Extract relevant values from passed ctx.
+ activityContext := getActivityContext(ctx)
+ if activityContext.internal {
+ return nil // Already processed.
+ }
+
+ requesting := activityContext.requestingAcct
+ receiving := activityContext.receivingAcct
+
+ if receiving.IsMoving() {
+ // A Moving account
+ // can't do this.
+ return nil
+ }
+
+ // Convert received AS block type to internal model.
+ block, err := f.converter.ASBlockToBlock(ctx, blockable)
+ if err != nil {
+ err := gtserror.Newf("error converting from AS type: %w", err)
+ return gtserror.WrapWithCode(http.StatusBadRequest, err)
+ }
+
+ // Ensure block enacted by correct account.
+ if block.AccountID != requesting.ID {
+ return gtserror.NewfWithCode(http.StatusForbidden, "requester %s is not expected actor %s",
+ requesting.URI, block.Account.URI)
+ }
+
+ // Ensure block received by correct account.
+ if block.TargetAccountID != receiving.ID {
+ return gtserror.NewfWithCode(http.StatusForbidden, "receiver %s is not expected object %s",
+ receiving.URI, block.TargetAccount.URI)
+ }
+
+ // Generate new ID for block.
+ block.ID = id.NewULID()
+
+ // Insert the new validated block into the database.
+ if err := f.state.DB.PutBlock(ctx, block); err != nil {
+ return gtserror.Newf("error inserting %s into db: %w", block.URI, err)
+ }
+
+ // Push message to worker queue to handle block side-effects.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: ap.ActivityBlock,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: block,
+ Receiving: receiving,
+ Requesting: requesting,
+ })
+
+ return nil
+}
diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go
index 11030b16b..d9834b144 100644
--- a/internal/federation/federatingdb/create.go
+++ b/internal/federation/federatingdb/create.go
@@ -20,9 +20,7 @@ package federatingdb
import (
"context"
"errors"
- "fmt"
- "github.com/miekg/dns"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
@@ -49,115 +47,36 @@ import (
func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
log.DebugKV(ctx, "create", serialize{asType})
+ // Cache entry for this activity type's ID for later
+ // checks in the Exist() function if we see it again.
+ f.activityIDs.Set(ap.GetJSONLDId(asType).String(), struct{}{})
+
+ // Extract relevant values from passed ctx.
activityContext := getActivityContext(ctx)
if activityContext.internal {
return nil // Already processed.
}
- requestingAcct := activityContext.requestingAcct
- receivingAcct := activityContext.receivingAcct
+ requesting := activityContext.requestingAcct
+ receiving := activityContext.receivingAcct
- if requestingAcct.IsMoving() {
+ if requesting.IsMoving() {
// A Moving account
// can't do this.
return nil
}
- // Cache entry for this create activity ID for later
- // checks in the Exist() function if we see it again.
- f.activityIDs.Set(ap.GetJSONLDId(asType).String(), struct{}{})
-
- switch name := asType.GetTypeName(); name {
- case ap.ActivityBlock:
- // BLOCK SOMETHING
- return f.activityBlock(ctx, asType, receivingAcct, requestingAcct)
- case ap.ActivityCreate:
- // CREATE SOMETHING
- return f.activityCreate(ctx, asType, receivingAcct, requestingAcct)
- case ap.ActivityFollow:
- // FOLLOW SOMETHING
- return f.activityFollow(ctx, asType, receivingAcct, requestingAcct)
- case ap.ActivityLike:
- // LIKE SOMETHING
- return f.activityLike(ctx, asType, receivingAcct, requestingAcct)
- case ap.ActivityFlag:
- // FLAG / REPORT SOMETHING
- return f.activityFlag(ctx, asType, receivingAcct, requestingAcct)
- default:
- log.Debugf(ctx, "unhandled object type: %s", name)
- }
-
- return nil
-}
-
-/*
- BLOCK HANDLERS
-*/
-
-func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, receiving *gtsmodel.Account, requesting *gtsmodel.Account) error {
- blockable, ok := asType.(vocab.ActivityStreamsBlock)
+ // Cast to the expected types we handle in this func.
+ creatable, ok := asType.(vocab.ActivityStreamsCreate)
if !ok {
- return errors.New("activityBlock: could not convert type to block")
- }
-
- block, err := f.converter.ASBlockToBlock(ctx, blockable)
- if err != nil {
- return fmt.Errorf("activityBlock: could not convert Block to gts model block")
- }
-
- if block.AccountID != requesting.ID {
- return fmt.Errorf(
- "activityBlock: requestingAccount %s is not Block actor account %s",
- requesting.URI, block.Account.URI,
- )
- }
-
- if block.TargetAccountID != receiving.ID {
- return fmt.Errorf(
- "activityBlock: inbox account %s is not Block object account %s",
- receiving.URI, block.TargetAccount.URI,
- )
- }
-
- block.ID = id.NewULID()
-
- if err := f.state.DB.PutBlock(ctx, block); err != nil {
- return fmt.Errorf("activityBlock: database error inserting block: %s", err)
- }
-
- f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
- APObjectType: ap.ActivityBlock,
- APActivityType: ap.ActivityCreate,
- GTSModel: block,
- Receiving: receiving,
- Requesting: requesting,
- })
-
- return nil
-}
-
-/*
- CREATE HANDLERS
-*/
-
-// activityCreate handles asType Create by checking
-// the Object entries of the Create and calling other
-// handlers as appropriate.
-func (f *federatingDB) activityCreate(
- ctx context.Context,
- asType vocab.Type,
- receivingAccount *gtsmodel.Account,
- requestingAccount *gtsmodel.Account,
-) error {
- create, ok := asType.(vocab.ActivityStreamsCreate)
- if !ok {
- return gtserror.Newf("could not convert asType %T to ActivityStreamsCreate", asType)
+ log.Debugf(ctx, "unhandled object type: %s", asType.GetTypeName())
+ return nil
}
var errs gtserror.MultiError
// Extract objects from create activity.
- objects := ap.ExtractObjects(create)
+ objects := ap.ExtractObjects(creatable)
// Extract PollOptionables (votes!) from objects slice.
optionables, objects := ap.ExtractPollOptionables(objects)
@@ -166,8 +85,8 @@ func (f *federatingDB) activityCreate(
// Handle provided poll vote(s) creation, this can
// be for single or multiple votes in the same poll.
err := f.createPollOptionables(ctx,
- receivingAccount,
- requestingAccount,
+ receiving,
+ requesting,
optionables,
)
if err != nil {
@@ -182,12 +101,12 @@ func (f *federatingDB) activityCreate(
for _, statusable := range statusables {
// Check if this is a forwarded object, i.e. did
// the account making the request also create this?
- forwarded := !isSender(statusable, requestingAccount)
+ forwarded := !isSender(statusable, requesting)
// Handle create event for this statusable.
if err := f.createStatusable(ctx,
- receivingAccount,
- requestingAccount,
+ receiving,
+ requesting,
statusable,
forwarded,
); err != nil {
@@ -340,8 +259,7 @@ func (f *federatingDB) createStatusable(
//
// It does this to try to ensure thread completion, but
// we have our own thread fetching mechanism anyway.
- log.Debugf(ctx,
- "status %s is not relevant to receiver (%v); dropping it",
+ log.Debugf(ctx, "status %s is not relevant to receiver (%v); dropping it",
ap.GetJSONLDId(statusable), err,
)
return nil
@@ -351,8 +269,7 @@ func (f *federatingDB) createStatusable(
// gauge how much spam is being sent to them.
//
// TODO: add Prometheus metrics for this.
- log.Infof(ctx,
- "status %s looked like spam (%v); dropping it",
+ log.Infof(ctx, "status %s looked like spam (%v); dropping it",
ap.GetJSONLDId(statusable), err,
)
return nil
@@ -398,210 +315,3 @@ func (f *federatingDB) createStatusable(
return nil
}
-
-/*
- FOLLOW HANDLERS
-*/
-
-func (f *federatingDB) activityFollow(ctx context.Context, asType vocab.Type, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error {
- follow, ok := asType.(vocab.ActivityStreamsFollow)
- if !ok {
- return errors.New("activityFollow: could not convert type to follow")
- }
-
- followRequest, err := f.converter.ASFollowToFollowRequest(ctx, follow)
- if err != nil {
- return fmt.Errorf("activityFollow: could not convert Follow to follow request: %s", err)
- }
-
- if followRequest.AccountID != requestingAccount.ID {
- return fmt.Errorf(
- "activityFollow: requestingAccount %s is not Follow actor account %s",
- requestingAccount.URI, followRequest.Account.URI,
- )
- }
-
- if followRequest.TargetAccountID != receivingAccount.ID {
- return fmt.Errorf(
- "activityFollow: inbox account %s is not Follow object account %s",
- receivingAccount.URI, followRequest.TargetAccount.URI,
- )
- }
-
- followRequest.ID = id.NewULID()
-
- if err := f.state.DB.PutFollowRequest(ctx, followRequest); err != nil {
- return fmt.Errorf("activityFollow: database error inserting follow request: %s", err)
- }
-
- f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
- APObjectType: ap.ActivityFollow,
- APActivityType: ap.ActivityCreate,
- GTSModel: followRequest,
- Receiving: receivingAccount,
- Requesting: requestingAccount,
- })
-
- return nil
-}
-
-/*
- LIKE HANDLERS
-*/
-
-func (f *federatingDB) activityLike(
- ctx context.Context,
- asType vocab.Type,
- receivingAcct *gtsmodel.Account,
- requestingAcct *gtsmodel.Account,
-) error {
- like, ok := asType.(vocab.ActivityStreamsLike)
- if !ok {
- err := gtserror.Newf("could not convert asType %T to ActivityStreamsLike", asType)
- return gtserror.SetMalformed(err)
- }
-
- fave, err := f.converter.ASLikeToFave(ctx, like)
- if err != nil {
- return gtserror.Newf("could not convert Like to fave: %w", err)
- }
-
- // Ensure requester not trying to
- // Like on someone else's behalf.
- if fave.AccountID != requestingAcct.ID {
- text := fmt.Sprintf(
- "requestingAcct %s is not Like actor account %s",
- requestingAcct.URI, fave.Account.URI,
- )
- return gtserror.NewErrorForbidden(errors.New(text), text)
- }
-
- if !*fave.Status.Local {
- // Only process likes of local statuses.
- // TODO: process for remote statuses as well.
- return nil
- }
-
- // Ensure valid Like target for requester.
- policyResult, err := f.intFilter.StatusLikeable(ctx,
- requestingAcct,
- fave.Status,
- )
- if err != nil {
- err := gtserror.Newf("error seeing if status %s is likeable: %w", fave.Status.ID, err)
- return gtserror.NewErrorInternalError(err)
- }
-
- if policyResult.Forbidden() {
- const errText = "requester does not have permission to Like this status"
- err := gtserror.New(errText)
- return gtserror.NewErrorForbidden(err, errText)
- }
-
- // Derive pendingApproval
- // and preapproved status.
- var (
- pendingApproval bool
- preApproved bool
- )
-
- switch {
- case policyResult.WithApproval():
- // Requester allowed to do
- // this pending approval.
- pendingApproval = true
-
- case policyResult.MatchedOnCollection():
- // Requester allowed to do this,
- // but matched on collection.
- // Preapprove Like and have the
- // processor send out an Accept.
- pendingApproval = true
- preApproved = true
-
- case policyResult.Permitted():
- // Requester straight up
- // permitted to do this,
- // no need for Accept.
- pendingApproval = false
- }
-
- // Set appropriate fields
- // on fave and store it.
- fave.ID = id.NewULID()
- fave.PendingApproval = &pendingApproval
- fave.PreApproved = preApproved
-
- if err := f.state.DB.PutStatusFave(ctx, fave); err != nil {
- if errors.Is(err, db.ErrAlreadyExists) {
- // The fave already exists in the
- // database, which means we've already
- // handled side effects. We can just
- // return nil here and be done with it.
- return nil
- }
- return gtserror.Newf("db error inserting fave: %w", err)
- }
-
- f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
- APObjectType: ap.ActivityLike,
- APActivityType: ap.ActivityCreate,
- GTSModel: fave,
- Receiving: receivingAcct,
- Requesting: requestingAcct,
- })
-
- return nil
-}
-
-/*
- FLAG HANDLERS
-*/
-
-func (f *federatingDB) activityFlag(ctx context.Context, asType vocab.Type, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error {
- flag, ok := asType.(vocab.ActivityStreamsFlag)
- if !ok {
- return errors.New("activityFlag: could not convert type to flag")
- }
-
- report, err := f.converter.ASFlagToReport(ctx, flag)
- if err != nil {
- return fmt.Errorf("activityFlag: could not convert Flag to report: %w", err)
- }
-
- // Requesting account must have at
- // least two domains from the right
- // in common with reporting account.
- if dns.CompareDomainName(
- requestingAccount.Domain,
- report.Account.Domain,
- ) < 2 {
- return fmt.Errorf(
- "activityFlag: requesting account %s does not share a domain with Flag Actor account %s",
- requestingAccount.URI, report.Account.URI,
- )
- }
-
- if report.TargetAccountID != receivingAccount.ID {
- return fmt.Errorf(
- "activityFlag: inbox account %s is not Flag object account %s",
- receivingAccount.URI, report.TargetAccount.URI,
- )
- }
-
- report.ID = id.NewULID()
-
- if err := f.state.DB.PutReport(ctx, report); err != nil {
- return fmt.Errorf("activityFlag: database error inserting report: %w", err)
- }
-
- f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
- APObjectType: ap.ActivityFlag,
- APActivityType: ap.ActivityCreate,
- GTSModel: report,
- Receiving: receivingAccount,
- Requesting: requestingAccount,
- })
-
- return nil
-}
diff --git a/internal/federation/federatingdb/create_test.go b/internal/federation/federatingdb/create_test.go
index fffee1432..51f1f3ed7 100644
--- a/internal/federation/federatingdb/create_test.go
+++ b/internal/federation/federatingdb/create_test.go
@@ -25,6 +25,7 @@ import (
"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/id"
@@ -115,8 +116,10 @@ func (suite *CreateTestSuite) TestCreateFlag1() {
suite.FailNow(err.Error())
}
+ flag := t.(vocab.ActivityStreamsFlag)
+
ctx := createTestContext(reportedAccount, reportingAccount)
- if err := suite.federatingDB.Create(ctx, t); err != nil {
+ if err := suite.federatingDB.Flag(ctx, flag); err != nil {
suite.FailNow(err.Error())
}
diff --git a/internal/federation/federatingdb/db.go b/internal/federation/federatingdb/db.go
index 230098073..d76e5a42c 100644
--- a/internal/federation/federatingdb/db.go
+++ b/internal/federation/federatingdb/db.go
@@ -24,6 +24,7 @@ import (
"codeberg.org/gruf/go-cache/v3/simple"
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
"github.com/superseriousbusiness/gotosocial/internal/filter/spam"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
@@ -34,18 +35,20 @@ import (
// DB wraps the pub.Database interface with
// a couple of custom functions for GoToSocial.
type DB interface {
- // Default functionality.
+ // 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
+ // Federating protocol overridden callback functionality.
+ Like(context.Context, vocab.ActivityStreamsLike) error
+ Block(context.Context, vocab.ActivityStreamsBlock) error
+ Follow(context.Context, vocab.ActivityStreamsFollow) error
+ Undo(context.Context, vocab.ActivityStreamsUndo) error
+ Accept(context.Context, vocab.ActivityStreamsAccept) error
+ Reject(context.Context, vocab.ActivityStreamsReject) error
+ Announce(context.Context, vocab.ActivityStreamsAnnounce) error
+ Move(context.Context, vocab.ActivityStreamsMove) error
+ Flag(context.Context, vocab.ActivityStreamsFlag) error
/*
Extra/convenience functionality.
@@ -87,3 +90,9 @@ func New(
fdb.activityIDs.Init(0, 2048)
return &fdb
}
+
+// storeActivityID stores an entry in the .activityIDs cache for this
+// type's JSON-LD ID, for later checks in Exist() to mark it as seen.
+func (f *federatingDB) storeActivityID(asType vocab.Type) {
+ f.activityIDs.Set(ap.GetJSONLDId(asType).String(), struct{}{})
+}
diff --git a/internal/federation/federatingdb/flag.go b/internal/federation/federatingdb/flag.go
new file mode 100644
index 000000000..583a7c186
--- /dev/null
+++ b/internal/federation/federatingdb/flag.go
@@ -0,0 +1,91 @@
+// 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 .
+
+package federatingdb
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/miekg/dns"
+ "github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+)
+
+func (f *federatingDB) Flag(ctx context.Context, flaggable vocab.ActivityStreamsFlag) error {
+ log.DebugKV(ctx, "flag", serialize{flaggable})
+
+ // Mark activity as handled.
+ f.storeActivityID(flaggable)
+
+ // Extract relevant values from passed ctx.
+ activityContext := getActivityContext(ctx)
+ if activityContext.internal {
+ return nil // Already processed.
+ }
+
+ requesting := activityContext.requestingAcct
+ receiving := activityContext.receivingAcct
+
+ // Convert received AS flag type to internal report model.
+ report, err := f.converter.ASFlagToReport(ctx, flaggable)
+ if err != nil {
+ err := gtserror.Newf("error converting from AS type: %w", err)
+ return gtserror.WrapWithCode(http.StatusBadRequest, err)
+ }
+
+ // Requesting acc's domain must be at
+ // least a subdomain of the reporting
+ // account. i.e. if they're using a
+ // different account domain to host.
+ if dns.CompareDomainName(
+ requesting.Domain,
+ report.Account.Domain,
+ ) < 2 {
+ return gtserror.NewfWithCode(http.StatusForbidden, "requester %s does not share a domain with Flag Actor account %s",
+ requesting.URI, report.Account.URI)
+ }
+
+ // Ensure report received by correct account.
+ if report.TargetAccountID != receiving.ID {
+ return gtserror.NewfWithCode(http.StatusForbidden, "receiver %s is not expected object %s",
+ receiving.URI, report.TargetAccount.URI)
+ }
+
+ // Generate new ID for report.
+ report.ID = id.NewULID()
+
+ // Insert the new validated reported into the database.
+ if err := f.state.DB.PutReport(ctx, report); err != nil {
+ return gtserror.Newf("error inserting %s into db: %w", report.URI, err)
+ }
+
+ // Push message to worker queue to handle report side-effects.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: ap.ActivityFlag,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: report,
+ Receiving: receiving,
+ Requesting: requesting,
+ })
+
+ return nil
+}
diff --git a/internal/federation/federatingdb/follow.go b/internal/federation/federatingdb/follow.go
new file mode 100644
index 000000000..33c5ad856
--- /dev/null
+++ b/internal/federation/federatingdb/follow.go
@@ -0,0 +1,84 @@
+// 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 .
+
+package federatingdb
+
+import (
+ "context"
+ "net/http"
+
+ "github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+)
+
+func (f *federatingDB) Follow(ctx context.Context, followable vocab.ActivityStreamsFollow) error {
+ log.DebugKV(ctx, "follow", serialize{followable})
+
+ // Mark activity as handled.
+ f.storeActivityID(followable)
+
+ // Extract relevant values from passed ctx.
+ activityContext := getActivityContext(ctx)
+ if activityContext.internal {
+ return nil // Already processed.
+ }
+
+ requesting := activityContext.requestingAcct
+ receiving := activityContext.receivingAcct
+
+ // Convert received AS block type to internal follow request model.
+ followreq, err := f.converter.ASFollowToFollowRequest(ctx, followable)
+ if err != nil {
+ err := gtserror.Newf("error converting from AS type: %w", err)
+ return gtserror.WrapWithCode(http.StatusBadRequest, err)
+ }
+
+ // Ensure follow enacted by correct account.
+ if followreq.AccountID != requesting.ID {
+ return gtserror.NewfWithCode(http.StatusForbidden, "requester %s is not expected actor %s",
+ requesting.URI, followreq.Account.URI)
+ }
+
+ // Ensure follow received by correct account.
+ if followreq.TargetAccountID != receiving.ID {
+ return gtserror.NewfWithCode(http.StatusForbidden, "receiver %s is not expected object %s",
+ receiving.URI, followreq.TargetAccount.URI)
+ }
+
+ // Generate new ID for followreq.
+ followreq.ID = id.NewULID()
+
+ // Insert the new validate follow request into the database.
+ if err := f.state.DB.PutFollowRequest(ctx, followreq); err != nil {
+ return gtserror.Newf("error inserting %s into db: %w", followreq.URI, err)
+ }
+
+ // Push message to worker queue to handle followreq side-effects.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: ap.ActivityFollow,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: followreq,
+ Receiving: receiving,
+ Requesting: requesting,
+ })
+
+ return nil
+}
diff --git a/internal/federation/federatingdb/like.go b/internal/federation/federatingdb/like.go
new file mode 100644
index 000000000..37c7f988d
--- /dev/null
+++ b/internal/federation/federatingdb/like.go
@@ -0,0 +1,147 @@
+// 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 .
+
+package federatingdb
+
+import (
+ "context"
+ "errors"
+ "net/http"
+
+ "github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+)
+
+func (f *federatingDB) Like(ctx context.Context, likeable vocab.ActivityStreamsLike) error {
+ log.DebugKV(ctx, "like", serialize{likeable})
+
+ // Mark activity as handled.
+ f.storeActivityID(likeable)
+
+ // Extract relevant values from passed ctx.
+ activityContext := getActivityContext(ctx)
+ if activityContext.internal {
+ return nil // Already processed.
+ }
+
+ requesting := activityContext.requestingAcct
+ receiving := activityContext.receivingAcct
+
+ if receiving.IsMoving() {
+ // A Moving account
+ // can't do this.
+ return nil
+ }
+
+ // Convert received AS like type to internal fave model.
+ fave, err := f.converter.ASLikeToFave(ctx, likeable)
+ if err != nil {
+ err := gtserror.Newf("error converting from AS type: %w", err)
+ return gtserror.WrapWithCode(http.StatusBadRequest, err)
+ }
+
+ // Ensure fave enacted by correct account.
+ if fave.AccountID != requesting.ID {
+ return gtserror.NewfWithCode(http.StatusForbidden, "requester %s is not expected actor %s",
+ requesting.URI, fave.Account.URI)
+ }
+
+ // Ensure fave received by correct account.
+ if fave.TargetAccountID != receiving.ID {
+ return gtserror.NewfWithCode(http.StatusForbidden, "receiver %s is not expected object %s",
+ receiving.URI, fave.TargetAccount.URI)
+ }
+
+ if !*fave.Status.Local {
+ // Only process likes of local statuses.
+ // TODO: process for remote statuses as well.
+ return nil
+ }
+
+ // Ensure valid Like target for requester.
+ policyResult, err := f.intFilter.StatusLikeable(ctx,
+ requesting,
+ fave.Status,
+ )
+ if err != nil {
+ return gtserror.Newf("error seeing if status %s is likeable: %w", fave.Status.URI, err)
+ }
+
+ if policyResult.Forbidden() {
+ return gtserror.NewWithCode(http.StatusForbidden, "requester does not have permission to Like status")
+ }
+
+ // Derive pendingApproval
+ // and preapproved status.
+ var (
+ pendingApproval bool
+ preApproved bool
+ )
+
+ switch {
+ case policyResult.WithApproval():
+ // Requester allowed to do
+ // this pending approval.
+ pendingApproval = true
+
+ case policyResult.MatchedOnCollection():
+ // Requester allowed to do this,
+ // but matched on collection.
+ // Preapprove Like and have the
+ // processor send out an Accept.
+ pendingApproval = true
+ preApproved = true
+
+ case policyResult.Permitted():
+ // Requester straight up
+ // permitted to do this,
+ // no need for Accept.
+ pendingApproval = false
+ }
+
+ // Set appropriate fields
+ // on fave and store it.
+ fave.ID = id.NewULID()
+ fave.PendingApproval = &pendingApproval
+ fave.PreApproved = preApproved
+
+ if err := f.state.DB.PutStatusFave(ctx, fave); err != nil {
+ if errors.Is(err, db.ErrAlreadyExists) {
+ // The fave already exists in the
+ // database, which means we've already
+ // handled side effects. We can just
+ // return nil here and be done with it.
+ return nil
+ }
+ return gtserror.Newf("error inserting %s into db: %w", fave.URI, err)
+ }
+
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: fave,
+ Receiving: receiving,
+ Requesting: requesting,
+ })
+
+ return nil
+}
diff --git a/internal/federation/federatingdb/move.go b/internal/federation/federatingdb/move.go
index 6ae299e30..cfdd0f651 100644
--- a/internal/federation/federatingdb/move.go
+++ b/internal/federation/federatingdb/move.go
@@ -38,6 +38,9 @@ import (
func (f *federatingDB) Move(ctx context.Context, move vocab.ActivityStreamsMove) error {
log.DebugKV(ctx, "move", serialize{move})
+ // Mark activity as handled.
+ f.storeActivityID(move)
+
activityContext := getActivityContext(ctx)
if activityContext.internal {
// Already processed.
diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go
index a2a9777d1..803c476d6 100644
--- a/internal/federation/federatingdb/update.go
+++ b/internal/federation/federatingdb/update.go
@@ -43,20 +43,24 @@ import (
func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
log.DebugKV(ctx, "update", serialize{asType})
+ // Mark activity as handled.
+ f.storeActivityID(asType)
+
+ // Extract relevant values from passed ctx.
activityContext := getActivityContext(ctx)
if activityContext.internal {
return nil // Already processed.
}
- requestingAcct := activityContext.requestingAcct
- receivingAcct := activityContext.receivingAcct
+ requesting := activityContext.requestingAcct
+ receiving := activityContext.receivingAcct
if accountable, ok := ap.ToAccountable(asType); ok {
- return f.updateAccountable(ctx, receivingAcct, requestingAcct, accountable)
+ return f.updateAccountable(ctx, receiving, requesting, accountable)
}
if statusable, ok := ap.ToStatusable(asType); ok {
- return f.updateStatusable(ctx, receivingAcct, requestingAcct, statusable)
+ return f.updateStatusable(ctx, receiving, requesting, statusable)
}
log.Debugf(ctx, "unhandled object type: %T", asType)
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index a953701f8..2bf934161 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -456,39 +456,8 @@ func (f *Federator) FederatingCallbacks(ctx context.Context) (
other []any,
err error,
) {
- wrapped = pub.FederatingWrappedCallbacks{
- // OnFollow determines what action to take for this
- // particular callback if a Follow Activity is handled.
- //
- // For our implementation, we always want to do nothing
- // because we have internal logic for handling follows.
- OnFollow: pub.OnFollowDoNothing,
- }
-
- // Override some default behaviors to trigger our own side effects.
- other = []any{
- func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
- return f.FederatingDB().Undo(ctx, undo)
- },
- func(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
- return f.FederatingDB().Accept(ctx, accept)
- },
- func(ctx context.Context, reject vocab.ActivityStreamsReject) error {
- return f.FederatingDB().Reject(ctx, reject)
- },
- func(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error {
- return f.FederatingDB().Announce(ctx, announce)
- },
- }
-
- // 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)
- },
- }...)
-
+ wrapped = f.wrapped
+ other = f.callback
return
}
diff --git a/internal/federation/federator.go b/internal/federation/federator.go
index b6f54906b..8f3efb302 100644
--- a/internal/federation/federator.go
+++ b/internal/federation/federator.go
@@ -36,14 +36,19 @@ var _ interface {
} = (*Federator)(nil)
type Federator struct {
- db db.DB
- federatingDB federatingdb.DB
- clock pub.Clock
- converter *typeutils.Converter
- transportController transport.Controller
- mediaManager *media.Manager
- actor pub.FederatingActor
+ db db.DB
+ federatingDB federatingdb.DB
+ clock pub.Clock
+ converter *typeutils.Converter
+ transport transport.Controller
+ mediaManager *media.Manager
+ actor pub.FederatingActor
dereferencing.Dereferencer
+
+ // store result of FederatingCallbacks() ahead
+ // of time since it's called in every PostInbox().
+ wrapped pub.FederatingWrappedCallbacks
+ callback []any
}
// NewFederator returns a new federator instance.
@@ -58,12 +63,13 @@ func NewFederator(
) *Federator {
clock := &Clock{}
f := &Federator{
- db: state.DB,
- federatingDB: federatingDB,
- clock: clock,
- converter: converter,
- transportController: transportController,
- mediaManager: mediaManager,
+ db: state.DB,
+ federatingDB: federatingDB,
+ clock: clock,
+ converter: converter,
+ transport: transportController,
+ mediaManager: mediaManager,
+
Dereferencer: dereferencing.NewDereferencer(
state,
converter,
@@ -72,6 +78,28 @@ func NewFederator(
intFilter,
mediaManager,
),
+
+ // prepared response to FederatingCallbacks()
+ wrapped: pub.FederatingWrappedCallbacks{
+
+ // OnFollow determines what action to take for this
+ // particular callback if a Follow Activity is handled.
+ //
+ // For our implementation, we always want to do nothing
+ // because we have internal logic for handling follows.
+ OnFollow: pub.OnFollowDoNothing,
+ },
+ callback: []any{
+ federatingDB.Like,
+ federatingDB.Block,
+ federatingDB.Follow,
+ federatingDB.Undo,
+ federatingDB.Accept,
+ federatingDB.Reject,
+ federatingDB.Announce,
+ federatingDB.Move,
+ federatingDB.Flag,
+ },
}
actor := newFederatingActor(f, f, federatingDB, clock)
f.actor = actor
@@ -90,5 +118,5 @@ func (f *Federator) FederatingDB() federatingdb.DB {
// TransportController returns the underlying transport controller.
func (f *Federator) TransportController() transport.Controller {
- return f.transportController
+ return f.transport
}
diff --git a/internal/federation/transport.go b/internal/federation/transport.go
index bab89eafc..688bb793f 100644
--- a/internal/federation/transport.go
+++ b/internal/federation/transport.go
@@ -68,5 +68,5 @@ func (f *Federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, _ st
return nil, fmt.Errorf("id %s was neither an inbox path nor an outbox path", actorBoxIRI.String())
}
- return f.transportController.NewTransportForUsername(ctx, username)
+ return f.transport.NewTransportForUsername(ctx, username)
}
diff --git a/internal/gtserror/withcode.go b/internal/gtserror/withcode.go
index 0878db7bc..e2059474d 100644
--- a/internal/gtserror/withcode.go
+++ b/internal/gtserror/withcode.go
@@ -18,7 +18,7 @@
package gtserror
import (
- "errors"
+ "fmt"
"net/http"
"strings"
)
@@ -53,37 +53,78 @@ type WithCode interface {
}
type withCode struct {
- original error
- safe error
- code int
+ err error
+ safe string
+ code int
}
-func (e withCode) Unwrap() error {
- return e.original
+func (e *withCode) Unwrap() error {
+ return e.err
}
-func (e withCode) Error() string {
- return e.original.Error()
+func (e *withCode) Error() string {
+ return e.err.Error()
}
-func (e withCode) Safe() string {
- return e.safe.Error()
+func (e *withCode) Safe() string {
+ return e.safe
}
-func (e withCode) Code() int {
+func (e *withCode) Code() int {
return e.code
}
+// NewWithCode returns a new gtserror.WithCode that implements the error interface
+// with given HTTP status code, providing status message of "${httpStatus}: ${msg}".
+func NewWithCode(code int, msg string) WithCode {
+ return &withCode{
+ err: newAt(3, msg),
+ safe: http.StatusText(code) + ": " + msg,
+ code: code,
+ }
+}
+
+// NewfWithCode returns a new formatted gtserror.WithCode that implements the error interface
+// with given HTTP status code, provided formatted status message of "${httpStatus}: ${msg}".
+func NewfWithCode(code int, msgf string, args ...any) WithCode {
+ msg := fmt.Sprintf(msgf, args...)
+ return &withCode{
+ err: newAt(3, msg),
+ safe: http.StatusText(code) + ": " + msg,
+ code: code,
+ }
+}
+
+// NewWithCodeSafe returns a new gtserror.WithCode wrapping error with given HTTP status
+// code, hiding error message externally, providing status message of "${httpStatus}: ${safe}".
+func NewWithCodeSafe(code int, err error, safe string) WithCode {
+ return &withCode{
+ err: err,
+ safe: http.StatusText(code) + ": " + safe,
+ code: code,
+ }
+}
+
+// WrapWithCode returns a new gtserror.WithCode wrapping error with given HTTP
+// status code, hiding error message externally, providing standard status message.
+func WrapWithCode(code int, err error) WithCode {
+ return &withCode{
+ err: err,
+ safe: http.StatusText(code),
+ code: code,
+ }
+}
+
// NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text.
func NewErrorBadRequest(original error, helpText ...string) WithCode {
safe := http.StatusText(http.StatusBadRequest)
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusBadRequest,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusBadRequest,
}
}
@@ -93,10 +134,10 @@ func NewErrorUnauthorized(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusUnauthorized,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusUnauthorized,
}
}
@@ -106,10 +147,10 @@ func NewErrorForbidden(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusForbidden,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusForbidden,
}
}
@@ -119,10 +160,10 @@ func NewErrorNotFound(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusNotFound,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusNotFound,
}
}
@@ -132,10 +173,10 @@ func NewErrorInternalError(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusInternalServerError,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusInternalServerError,
}
}
@@ -145,10 +186,10 @@ func NewErrorConflict(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusConflict,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusConflict,
}
}
@@ -158,10 +199,10 @@ func NewErrorNotAcceptable(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusNotAcceptable,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusNotAcceptable,
}
}
@@ -171,10 +212,10 @@ func NewErrorUnprocessableEntity(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusUnprocessableEntity,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusUnprocessableEntity,
}
}
@@ -184,10 +225,10 @@ func NewErrorGone(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusGone,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusGone,
}
}
@@ -197,10 +238,10 @@ func NewErrorNotImplemented(original error, helpText ...string) WithCode {
if helpText != nil {
safe = safe + ": " + strings.Join(helpText, ": ")
}
- return withCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusNotImplemented,
+ return &withCode{
+ err: original,
+ safe: safe,
+ code: http.StatusNotImplemented,
}
}
@@ -208,10 +249,10 @@ func NewErrorNotImplemented(original error, helpText ...string) WithCode {
// This error type should only be used when an http caller has already hung up their request.
// See: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#nginx
func NewErrorClientClosedRequest(original error) WithCode {
- return withCode{
- original: original,
- safe: errors.New(StatusTextClientClosedRequest),
- code: StatusClientClosedRequest,
+ return &withCode{
+ err: original,
+ safe: StatusTextClientClosedRequest,
+ code: StatusClientClosedRequest,
}
}
@@ -219,9 +260,9 @@ func NewErrorClientClosedRequest(original error) WithCode {
// This error type should only be used when the server has decided to hang up a client
// request after x amount of time, to avoid keeping extremely slow client requests open.
func NewErrorRequestTimeout(original error) WithCode {
- return withCode{
- original: original,
- safe: errors.New(http.StatusText(http.StatusRequestTimeout)),
- code: http.StatusRequestTimeout,
+ return &withCode{
+ err: original,
+ safe: http.StatusText(http.StatusRequestTimeout),
+ code: http.StatusRequestTimeout,
}
}