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