575 lines
16 KiB
Go
575 lines
16 KiB
Go
|
// 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 workers
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"time"
|
||
|
|
||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||
|
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||
|
)
|
||
|
|
||
|
// ShouldProcessMove checks whether we should attempt
|
||
|
// to process a move with the given object and target,
|
||
|
// based on whether or not a move with those values
|
||
|
// was attempted or succeeded recently.
|
||
|
func (p *fediAPI) ShouldProcessMove(
|
||
|
ctx context.Context,
|
||
|
object string,
|
||
|
target string,
|
||
|
) (bool, error) {
|
||
|
// If a Move has been *attempted* within last 5m,
|
||
|
// that involved the origin and target in any way,
|
||
|
// then we shouldn't try to reprocess immediately.
|
||
|
//
|
||
|
// This avoids the potential DDOS vector of a given
|
||
|
// origin account spamming out moves to various
|
||
|
// target accounts, causing loads of dereferences.
|
||
|
latestMoveAttempt, err := p.state.DB.GetLatestMoveAttemptInvolvingURIs(
|
||
|
ctx, object, target,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return false, gtserror.Newf(
|
||
|
"error checking latest Move attempt involving object %s and target %s: %w",
|
||
|
object, target, err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if !latestMoveAttempt.IsZero() &&
|
||
|
time.Since(latestMoveAttempt) < 5*time.Minute {
|
||
|
log.Infof(ctx,
|
||
|
"object %s or target %s have been involved in a Move attempt within the last 5 minutes, will not process Move",
|
||
|
object, target,
|
||
|
)
|
||
|
return false, nil
|
||
|
}
|
||
|
|
||
|
// If a Move has *succeeded* within the last week
|
||
|
// that involved the origin and target in any way,
|
||
|
// then we shouldn't process again for a while.
|
||
|
latestMoveSuccess, err := p.state.DB.GetLatestMoveSuccessInvolvingURIs(
|
||
|
ctx, object, target,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return false, gtserror.Newf(
|
||
|
"error checking latest Move success involving object %s and target %s: %w",
|
||
|
object, target, err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if !latestMoveSuccess.IsZero() &&
|
||
|
time.Since(latestMoveSuccess) < 168*time.Hour {
|
||
|
log.Infof(ctx,
|
||
|
"object %s or target %s have been involved in a successful Move within the last 7 days, will not process Move",
|
||
|
object, target,
|
||
|
)
|
||
|
return false, nil
|
||
|
}
|
||
|
|
||
|
return true, nil
|
||
|
}
|
||
|
|
||
|
// GetOrCreateMove takes a stub move created by the
|
||
|
// requesting account, and either retrieves or creates
|
||
|
// a corresponding move in the database. If a move is
|
||
|
// created in this way, requestingAcct will be updated
|
||
|
// with the correct moveID.
|
||
|
func (p *fediAPI) GetOrCreateMove(
|
||
|
ctx context.Context,
|
||
|
requestingAcct *gtsmodel.Account,
|
||
|
stubMove *gtsmodel.Move,
|
||
|
) (*gtsmodel.Move, error) {
|
||
|
var (
|
||
|
moveURIStr = stubMove.URI
|
||
|
objectStr = stubMove.OriginURI
|
||
|
object = stubMove.Origin
|
||
|
targetStr = stubMove.TargetURI
|
||
|
target = stubMove.Target
|
||
|
|
||
|
move *gtsmodel.Move
|
||
|
err error
|
||
|
)
|
||
|
|
||
|
// See if we have a move with
|
||
|
// this ID/URI stored already.
|
||
|
move, err = p.state.DB.GetMoveByURI(ctx, moveURIStr)
|
||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||
|
return nil, gtserror.Newf(
|
||
|
"db error retrieving move with URI %s: %w",
|
||
|
moveURIStr, err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if move != nil {
|
||
|
// We had a Move with this ID/URI.
|
||
|
//
|
||
|
// Make sure the Move we already had
|
||
|
// stored has the same origin + target.
|
||
|
if move.OriginURI != objectStr ||
|
||
|
move.TargetURI != targetStr {
|
||
|
return nil, gtserror.Newf(
|
||
|
"Move object %s and/or target %s differ from stored object and target for this ID (%s)",
|
||
|
objectStr, targetStr, moveURIStr,
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If we didn't have a move stored for
|
||
|
// this ID/URI, then see if we have a
|
||
|
// Move with this origin and target
|
||
|
// already (but a different ID/URI).
|
||
|
if move == nil {
|
||
|
move, err = p.state.DB.GetMoveByOriginTarget(ctx, objectStr, targetStr)
|
||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||
|
return nil, gtserror.Newf(
|
||
|
"db error retrieving Move with object %s and target %s: %w",
|
||
|
objectStr, targetStr, err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if move != nil {
|
||
|
// We had a move for this object and
|
||
|
// target, but the ID/URI has changed.
|
||
|
// Update the Move's URI in the db to
|
||
|
// reflect that this is but the latest
|
||
|
// attempt with this origin + target.
|
||
|
//
|
||
|
// The remote may be trying to retry
|
||
|
// the Move but their server might
|
||
|
// not reuse the same Activity URIs,
|
||
|
// and we don't want to store a brand
|
||
|
// new Move for each attempt!
|
||
|
move.URI = moveURIStr
|
||
|
if err := p.state.DB.UpdateMove(ctx, move, "uri"); err != nil {
|
||
|
return nil, gtserror.Newf(
|
||
|
"db error updating Move with object %s and target %s: %w",
|
||
|
objectStr, targetStr, err,
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if move == nil {
|
||
|
// If Move is still nil then
|
||
|
// we didn't have this Move
|
||
|
// stored yet, so it's new.
|
||
|
// Store it now!
|
||
|
move = >smodel.Move{
|
||
|
ID: id.NewULID(),
|
||
|
AttemptedAt: time.Now(),
|
||
|
OriginURI: objectStr,
|
||
|
Origin: object,
|
||
|
TargetURI: targetStr,
|
||
|
Target: target,
|
||
|
URI: moveURIStr,
|
||
|
}
|
||
|
if err := p.state.DB.PutMove(ctx, move); err != nil {
|
||
|
return nil, gtserror.Newf(
|
||
|
"db error storing move %s: %w",
|
||
|
moveURIStr, err,
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If move_id isn't set on the requesting
|
||
|
// account yet, set it so other processes
|
||
|
// know there's a Move in progress.
|
||
|
if requestingAcct.MoveID != move.ID {
|
||
|
requestingAcct.Move = move
|
||
|
requestingAcct.MoveID = move.ID
|
||
|
if err := p.state.DB.UpdateAccount(ctx,
|
||
|
requestingAcct, "move_id",
|
||
|
); err != nil {
|
||
|
return nil, gtserror.Newf(
|
||
|
"db error updating move_id on account: %w",
|
||
|
err,
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return move, nil
|
||
|
}
|
||
|
|
||
|
// MoveAccount processes the given
|
||
|
// Move FromFediAPI message:
|
||
|
//
|
||
|
// APObjectType: "Profile"
|
||
|
// APActivityType: "Move"
|
||
|
// GTSModel: stub *gtsmodel.Move.
|
||
|
// ReceivingAccount: Account of inbox owner receiving the Move.
|
||
|
func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) error {
|
||
|
// The account who received the Move message.
|
||
|
receiver := fMsg.ReceivingAccount
|
||
|
|
||
|
// *gtsmodel.Move activity.
|
||
|
stubMove, ok := fMsg.GTSModel.(*gtsmodel.Move)
|
||
|
if !ok {
|
||
|
return gtserror.Newf(
|
||
|
"%T not parseable as *gtsmodel.Move",
|
||
|
fMsg.GTSModel,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// Move origin and target info.
|
||
|
var (
|
||
|
originAcctURIStr = stubMove.OriginURI
|
||
|
originAcct = fMsg.RequestingAccount
|
||
|
targetAcctURIStr = stubMove.TargetURI
|
||
|
targetAcctURI = stubMove.Target
|
||
|
)
|
||
|
|
||
|
// Assemble log context.
|
||
|
l := log.
|
||
|
WithContext(ctx).
|
||
|
WithField("originAcct", originAcctURIStr).
|
||
|
WithField("targetAcct", targetAcctURIStr)
|
||
|
|
||
|
// We can't/won't validate Move activities
|
||
|
// to domains we have blocked, so check this.
|
||
|
targetDomainBlocked, err := p.state.DB.IsDomainBlocked(ctx, targetAcctURI.Host)
|
||
|
if err != nil {
|
||
|
return gtserror.Newf(
|
||
|
"db error checking if target domain %s blocked: %w",
|
||
|
targetAcctURI.Host, err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if targetDomainBlocked {
|
||
|
l.Info("target domain is blocked, will not process Move")
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Next steps require making calls to remote +
|
||
|
// setting values that may be attempted by other
|
||
|
// in-process Moves. To avoid race conditions,
|
||
|
// ensure we're only trying to process this
|
||
|
// Move combo one attempt at a time.
|
||
|
//
|
||
|
// We use a custom lock because remotes might
|
||
|
// try to send the same Move several times with
|
||
|
// different IDs (you never know), but we only
|
||
|
// want to process them based on origin + target.
|
||
|
unlock := p.state.FedLocks.Lock(
|
||
|
"move:" + originAcctURIStr + ":" + targetAcctURIStr,
|
||
|
)
|
||
|
defer unlock()
|
||
|
|
||
|
// Check if Move is rate limited based
|
||
|
// on previous attempts / successes.
|
||
|
shouldProcess, err := p.ShouldProcessMove(ctx,
|
||
|
originAcctURIStr, targetAcctURIStr,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return gtserror.Newf(
|
||
|
"error checking if Move should be processed now: %w",
|
||
|
err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
if !shouldProcess {
|
||
|
// Move is rate limited, so don't process.
|
||
|
// Reason why should already be logged.
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Store new or retrieve existing Move. This will
|
||
|
// also update moveID on originAcct if necessary.
|
||
|
move, err := p.GetOrCreateMove(ctx, originAcct, stubMove)
|
||
|
if err != nil {
|
||
|
return gtserror.Newf(
|
||
|
"error refreshing target account %s: %w",
|
||
|
targetAcctURIStr, err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// Account to which the Move is taking place.
|
||
|
targetAcct, targetAcctable, err := p.federate.GetAccountByURI(
|
||
|
ctx,
|
||
|
receiver.Username,
|
||
|
targetAcctURI,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return gtserror.Newf(
|
||
|
"error getting target account %s: %w",
|
||
|
targetAcctURIStr, err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
// If target is suspended from this instance,
|
||
|
// then we can't/won't process any move side
|
||
|
// effects to that account, because:
|
||
|
//
|
||
|
// 1. We can't verify that it's aliased correctly
|
||
|
// back to originAcct without dereferencing it.
|
||
|
// 2. We can't/won't forward follows to a suspended
|
||
|
// account, since suspension would remove follows
|
||
|
// etc. targeting the new account anyways.
|
||
|
// 3. If someone is moving to a suspended account
|
||
|
// they probably totally suck ass (according to
|
||
|
// the moderators of this instance, anyway) so
|
||
|
// to hell with it.
|
||
|
if targetAcct.IsSuspended() {
|
||
|
l.Info("target account is suspended, will not process Move")
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if targetAcct.IsRemote() {
|
||
|
// Force refresh Move target account
|
||
|
// to ensure we have up-to-date version.
|
||
|
targetAcct, _, err = p.federate.RefreshAccount(ctx,
|
||
|
receiver.Username,
|
||
|
targetAcct,
|
||
|
targetAcctable,
|
||
|
dereferencing.Freshest,
|
||
|
)
|
||
|
if err != nil {
|
||
|
return gtserror.Newf(
|
||
|
"error refreshing target account %s: %w",
|
||
|
targetAcctURIStr, err,
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Target must not itself have moved somewhere.
|
||
|
// You can't move to an already-moved account.
|
||
|
targetAcctMovedTo := targetAcct.MovedToURI
|
||
|
if targetAcctMovedTo != "" {
|
||
|
l.Infof(
|
||
|
"target account has, itself, already moved to %s, will not process Move",
|
||
|
targetAcctMovedTo,
|
||
|
)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Target must be aliased back to origin account.
|
||
|
// Ie., its alsoKnownAs values must include the
|
||
|
// origin account, so we know it's for real.
|
||
|
if !targetAcct.IsAliasedTo(originAcctURIStr) {
|
||
|
l.Info("target account is not aliased back to origin account, will not process Move")
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
At this point we know that the move
|
||
|
looks valid and we should process it.
|
||
|
*/
|
||
|
|
||
|
// Transfer originAcct's followers
|
||
|
// on this instance to targetAcct.
|
||
|
redirectOK := p.RedirectAccountFollowers(
|
||
|
ctx,
|
||
|
originAcct,
|
||
|
targetAcct,
|
||
|
)
|
||
|
|
||
|
// Remove follows on this
|
||
|
// instance owned by originAcct.
|
||
|
removeFollowingOK := p.RemoveAccountFollowing(
|
||
|
ctx,
|
||
|
originAcct,
|
||
|
)
|
||
|
|
||
|
// Whatever happened above, error or
|
||
|
// not, we've just at least attempted
|
||
|
// the Move so we'll need to update it.
|
||
|
move.AttemptedAt = time.Now()
|
||
|
updateColumns := []string{"attempted_at"}
|
||
|
|
||
|
if redirectOK && removeFollowingOK {
|
||
|
// All OK means we can mark the
|
||
|
// Move as definitively succeeded.
|
||
|
//
|
||
|
// Take same time so SucceededAt
|
||
|
// isn't 0.0001s later or something.
|
||
|
move.SucceededAt = move.AttemptedAt
|
||
|
updateColumns = append(updateColumns, "succeeded_at")
|
||
|
}
|
||
|
|
||
|
// Update whatever columns we need to update.
|
||
|
if err := p.state.DB.UpdateMove(ctx,
|
||
|
move, updateColumns...,
|
||
|
); err != nil {
|
||
|
return gtserror.Newf(
|
||
|
"db error updating Move %s: %w",
|
||
|
move.URI, err,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// RedirectAccountFollowers redirects all local
|
||
|
// followers of originAcct to targetAcct.
|
||
|
//
|
||
|
// Both accounts must be fully dereferenced
|
||
|
// already, and the Move must be valid.
|
||
|
//
|
||
|
// Callers to this function MUST have obtained
|
||
|
// a lock already by calling FedLocks.Lock.
|
||
|
//
|
||
|
// Return bool will be true if all goes OK.
|
||
|
func (p *fediAPI) RedirectAccountFollowers(
|
||
|
ctx context.Context,
|
||
|
originAcct *gtsmodel.Account,
|
||
|
targetAcct *gtsmodel.Account,
|
||
|
) bool {
|
||
|
// Any local followers of originAcct should
|
||
|
// send follow requests to targetAcct instead,
|
||
|
// and have followers of originAcct removed.
|
||
|
//
|
||
|
// Select local followers with barebones, since
|
||
|
// we only need follow.Account and we can get
|
||
|
// that ourselves.
|
||
|
followers, err := p.state.DB.GetAccountLocalFollowers(
|
||
|
gtscontext.SetBarebones(ctx),
|
||
|
originAcct.ID,
|
||
|
)
|
||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||
|
log.Errorf(ctx,
|
||
|
"db error getting follows targeting originAcct: %v",
|
||
|
err,
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
for _, follow := range followers {
|
||
|
// Fetch the local account that
|
||
|
// owns the follow targeting originAcct.
|
||
|
if follow.Account, err = p.state.DB.GetAccountByID(
|
||
|
gtscontext.SetBarebones(ctx),
|
||
|
follow.AccountID,
|
||
|
); err != nil {
|
||
|
log.Errorf(ctx,
|
||
|
"db error getting follow account %s: %v",
|
||
|
follow.AccountID, err,
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Use the account processor FollowCreate
|
||
|
// function to send off the new follow,
|
||
|
// carrying over the Reblogs and Notify
|
||
|
// values from the old follow to the new.
|
||
|
//
|
||
|
// This will also handle cases where our
|
||
|
// account has already followed the target
|
||
|
// account, by just updating the existing
|
||
|
// follow of target account.
|
||
|
if _, err := p.account.FollowCreate(
|
||
|
ctx,
|
||
|
follow.Account,
|
||
|
&apimodel.AccountFollowRequest{
|
||
|
ID: targetAcct.ID,
|
||
|
Reblogs: follow.ShowReblogs,
|
||
|
Notify: follow.Notify,
|
||
|
},
|
||
|
); err != nil {
|
||
|
log.Errorf(ctx,
|
||
|
"error creating new follow for account %s: %v",
|
||
|
follow.AccountID, err,
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// New follow is in the process of
|
||
|
// sending, remove the existing follow.
|
||
|
// This will send out an Undo Activity for each Follow.
|
||
|
if _, err := p.account.FollowRemove(
|
||
|
ctx,
|
||
|
follow.Account,
|
||
|
follow.TargetAccountID,
|
||
|
); err != nil {
|
||
|
log.Errorf(ctx,
|
||
|
"error removing old follow for account %s: %v",
|
||
|
follow.AccountID, err,
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// RemoveAccountFollowing removes all
|
||
|
// follows owned by the move originAcct.
|
||
|
//
|
||
|
// originAcct must be fully dereferenced
|
||
|
// already, and the Move must be valid.
|
||
|
//
|
||
|
// Callers to this function MUST have obtained
|
||
|
// a lock already by calling FedLocks.Lock.
|
||
|
//
|
||
|
// Return bool will be true if all goes OK.
|
||
|
func (p *fediAPI) RemoveAccountFollowing(
|
||
|
ctx context.Context,
|
||
|
originAcct *gtsmodel.Account,
|
||
|
) bool {
|
||
|
// Any follows owned by originAcct which target
|
||
|
// accounts on our instance should be removed.
|
||
|
//
|
||
|
// We should rely on the target instance
|
||
|
// to send out new follows from targetAcct.
|
||
|
following, err := p.state.DB.GetAccountLocalFollows(
|
||
|
gtscontext.SetBarebones(ctx),
|
||
|
originAcct.ID,
|
||
|
)
|
||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||
|
log.Errorf(ctx,
|
||
|
"db error getting follows owned by originAcct: %v",
|
||
|
err,
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
for _, follow := range following {
|
||
|
// Ditch it. This is a one-way action
|
||
|
// from our side so we don't need to
|
||
|
// send any messages this time.
|
||
|
if err := p.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil {
|
||
|
log.Errorf(ctx,
|
||
|
"error removing old follow owned by account %s: %v",
|
||
|
follow.AccountID, err,
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Finally delete any follow requests
|
||
|
// owned by or targeting the originAcct.
|
||
|
if err := p.state.DB.DeleteAccountFollowRequests(
|
||
|
ctx, originAcct.ID,
|
||
|
); err != nil {
|
||
|
log.Errorf(ctx,
|
||
|
"db error deleting follow requests involving originAcct %s: %v",
|
||
|
originAcct.URI, err,
|
||
|
)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|