[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 😇

* fix create flag test to use correct function
This commit is contained in:
kim 2025-01-28 20:22:23 +00:00 committed by GitHub
parent bfe8144fda
commit 61141ac232
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 610 additions and 433 deletions

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
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
}

View File

@ -20,9 +20,7 @@
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 @@
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
}

View File

@ -25,6 +25,7 @@
"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())
}

View File

@ -24,6 +24,7 @@
"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 @@
// 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{}{})
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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
}

View File

@ -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 <http://www.gnu.org/licenses/>.
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
}

View File

@ -38,6 +38,9 @@
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.

View File

@ -43,20 +43,24 @@
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)

View File

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

View File

@ -36,14 +36,19 @@
} = (*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
}

View File

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

View File

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