mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Process outgoing Move from clientAPI (#2750)
* prevent moved accounts from taking create-type actions * update move logic * federate move out * indicate on web profile when an account has moved * [docs] Add migration docs section * lock while checking + setting move state * use redirectFollowers func for clientAPI as well * comment typo * linter? i barely know 'er! * Update internal/uris/uri.go Co-authored-by: Daenney <daenney@users.noreply.github.com> * add a couple tests for move * fix little mistake exposed by tests (thanks tests) * ensure Move marked as successful * attach shared util funcs to struct * lock whole account when doing move * move moving check to after error check * replace repeated text with error func * linterrrrrr!!!! * catch self follow case --------- Co-authored-by: Daenney <daenney@users.noreply.github.com>
This commit is contained in:
@@ -23,14 +23,17 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -45,13 +48,14 @@ func (p *Processor) MoveSelf(
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
movedToURI, err := url.Parse(form.MovedToURI)
|
||||
targetAcctURIStr := form.MovedToURI
|
||||
targetAcctURI, err := url.Parse(form.MovedToURI)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("invalid moved_to_uri provided in account Move request: %w", err)
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
if movedToURI.Scheme != "https" && movedToURI.Scheme != "http" {
|
||||
if targetAcctURI.Scheme != "https" && targetAcctURI.Scheme != "http" {
|
||||
err := errors.New("invalid moved_to_uri provided in account Move request: uri scheme must be http or https")
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
@@ -70,83 +74,244 @@ func (p *Processor) MoveSelf(
|
||||
return gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
var (
|
||||
// Current account from which
|
||||
// the move is taking place.
|
||||
account = authed.Account
|
||||
|
||||
// Target account to which
|
||||
// the move is taking place.
|
||||
targetAccount *gtsmodel.Account
|
||||
)
|
||||
|
||||
switch {
|
||||
case account.MovedToURI == "":
|
||||
// No problemo.
|
||||
|
||||
case account.MovedToURI == form.MovedToURI:
|
||||
// Trying to move again to the same
|
||||
// destination, perhaps to reprocess
|
||||
// side effects. This is OK.
|
||||
log.Info(ctx,
|
||||
"reprocessing Move side effects from %s to %s",
|
||||
account.URI, form.MovedToURI,
|
||||
)
|
||||
|
||||
default:
|
||||
// Account already moved, and now
|
||||
// trying to move somewhere else.
|
||||
// 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 {
|
||||
err := fmt.Errorf(
|
||||
"account %s is already Moved to %s, cannot also Move to %s",
|
||||
account.URI, account.MovedToURI, form.MovedToURI,
|
||||
"db error checking if target domain %s blocked: %w",
|
||||
targetAcctURI.Host, err,
|
||||
)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if targetDomainBlocked {
|
||||
err := fmt.Errorf(
|
||||
"domain of %s is blocked from this instance; "+
|
||||
"you will not be able to Move to that account",
|
||||
targetAcctURIStr,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
var (
|
||||
// Current account from which
|
||||
// the move is taking place.
|
||||
originAcct = authed.Account
|
||||
|
||||
// Target account to which
|
||||
// the move is taking place.
|
||||
targetAcct *gtsmodel.Account
|
||||
|
||||
// AP representation of target.
|
||||
targetAcctable ap.Accountable
|
||||
)
|
||||
|
||||
// Next steps involve checking + setting
|
||||
// state that might get messed up if a
|
||||
// client triggers this function twice
|
||||
// in quick succession, so get a lock on
|
||||
// this account.
|
||||
lockKey := originAcct.URI
|
||||
unlock := p.state.ClientLocks.Lock(lockKey)
|
||||
defer unlock()
|
||||
|
||||
// Ensure we have a valid, up-to-date representation of the target account.
|
||||
targetAccount, _, err = p.federator.GetAccountByURI(ctx, account.Username, movedToURI)
|
||||
targetAcct, targetAcctable, err = p.federator.GetAccountByURI(
|
||||
ctx,
|
||||
originAcct.Username,
|
||||
targetAcctURI,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error dereferencing moved_to_uri account: %w", err)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
if !targetAccount.SuspendedAt.IsZero() {
|
||||
if !targetAcct.SuspendedAt.IsZero() {
|
||||
err := fmt.Errorf(
|
||||
"target account %s is suspended from this instance; "+
|
||||
"you will not be able to Move to that account",
|
||||
targetAccount.URI,
|
||||
targetAcct.URI,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
if targetAcct.IsRemote() {
|
||||
// Force refresh Move target account
|
||||
// to ensure we have up-to-date version.
|
||||
targetAcct, _, err = p.federator.RefreshAccount(ctx,
|
||||
originAcct.Username,
|
||||
targetAcct,
|
||||
targetAcctable,
|
||||
dereferencing.Freshest,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"error refreshing target account %s: %w",
|
||||
targetAcctURIStr, err,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Target account MUST be aliased to this
|
||||
// account for this to be a valid Move.
|
||||
if !slices.Contains(targetAccount.AlsoKnownAsURIs, account.URI) {
|
||||
if !slices.Contains(targetAcct.AlsoKnownAsURIs, originAcct.URI) {
|
||||
err := fmt.Errorf(
|
||||
"target account %s is not aliased to this account via alsoKnownAs; "+
|
||||
"if you just changed it, wait five minutes and try the Move again",
|
||||
targetAccount.URI,
|
||||
"if you just changed it, please wait a few minutes and try the Move again",
|
||||
targetAcct.URI,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// Target account cannot itself have
|
||||
// already Moved somewhere else.
|
||||
if targetAccount.MovedToURI != "" {
|
||||
if targetAcct.MovedToURI != "" {
|
||||
err := fmt.Errorf(
|
||||
"target account %s has already Moved somewhere else (%s); "+
|
||||
"you will not be able to Move to that account",
|
||||
targetAccount.URI, targetAccount.MovedToURI,
|
||||
targetAcct.URI, targetAcct.MovedToURI,
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// Everything seems OK, so process the Move.
|
||||
// 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.
|
||||
latestMoveAttempt, err := p.state.DB.GetLatestMoveAttemptInvolvingURIs(
|
||||
ctx, originAcct.URI, targetAcct.URI,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"error checking latest Move attempt involving origin %s and target %s: %w",
|
||||
originAcct.URI, targetAcct.URI, err,
|
||||
)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if !latestMoveAttempt.IsZero() &&
|
||||
time.Since(latestMoveAttempt) < 5*time.Minute {
|
||||
err := fmt.Errorf(
|
||||
"your account or target account have been involved in a Move attempt within "+
|
||||
"the last 5 minutes, will not process Move; please try again after %s",
|
||||
latestMoveAttempt.Add(5*time.Minute),
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// 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, originAcct.URI, targetAcct.URI,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf(
|
||||
"error checking latest Move success involving origin %s and target %s: %w",
|
||||
originAcct.URI, targetAcct.URI, err,
|
||||
)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if !latestMoveSuccess.IsZero() &&
|
||||
time.Since(latestMoveSuccess) < 168*time.Hour {
|
||||
err := fmt.Errorf(
|
||||
"your account or target account have been involved in a successful Move within "+
|
||||
"the last 7 days, will not process Move; please try again after %s",
|
||||
latestMoveSuccess.Add(168*time.Hour),
|
||||
)
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// See if we have a Move stored already
|
||||
// or if we need to create a new one.
|
||||
var move *gtsmodel.Move
|
||||
|
||||
if originAcct.MoveID != "" {
|
||||
// Move already stored, ensure it's
|
||||
// to the target and nothing weird is
|
||||
// happening with race conditions etc.
|
||||
move = originAcct.Move
|
||||
if move == nil {
|
||||
// This shouldn't happen...
|
||||
err := fmt.Errorf("nil move for id %s", originAcct.MoveID)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if move.OriginURI != originAcct.URI ||
|
||||
move.TargetURI != targetAcct.URI {
|
||||
// This is also weird...
|
||||
err := errors.New("a Move is already stored for your account but contains invalid fields")
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
if originAcct.MovedToURI != move.TargetURI {
|
||||
// Huh... I'll be damned.
|
||||
err := errors.New("stored Move target URI does not equal your moved_to_uri value")
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
} else {
|
||||
// Move not stored yet, create it.
|
||||
moveID := id.NewULID()
|
||||
moveURIStr := uris.GenerateURIForMove(originAcct.Username, moveID)
|
||||
|
||||
// We might have selected the target
|
||||
// using the URL and not the URI.
|
||||
// Ensure we continue with the URI!
|
||||
if targetAcctURIStr != targetAcct.URI {
|
||||
targetAcctURIStr = targetAcct.URI
|
||||
targetAcctURI, err = url.Parse(targetAcctURIStr)
|
||||
if err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse origin URI.
|
||||
originAcctURI, err := url.Parse(originAcct.URI)
|
||||
if err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Store the Move.
|
||||
move = >smodel.Move{
|
||||
ID: moveID,
|
||||
AttemptedAt: time.Now(),
|
||||
OriginURI: originAcct.URI,
|
||||
Origin: originAcctURI,
|
||||
TargetURI: targetAcctURIStr,
|
||||
Target: targetAcctURI,
|
||||
URI: moveURIStr,
|
||||
}
|
||||
if err := p.state.DB.PutMove(ctx, move); err != nil {
|
||||
err := fmt.Errorf("db error storing move %s: %w", moveURIStr, err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Update account with the new
|
||||
// Move, and set moved_to_uri.
|
||||
originAcct.MoveID = move.ID
|
||||
originAcct.Move = move
|
||||
originAcct.MovedToURI = targetAcct.URI
|
||||
originAcct.MovedTo = targetAcct
|
||||
if err := p.state.DB.UpdateAccount(
|
||||
ctx,
|
||||
originAcct,
|
||||
"move_id",
|
||||
"moved_to_uri",
|
||||
); err != nil {
|
||||
err := fmt.Errorf("db error updating account: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Everything seems OK, process Move side effects async.
|
||||
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
|
||||
APObjectType: ap.ActorPerson,
|
||||
APActivityType: ap.ActivityMove,
|
||||
OriginAccount: account,
|
||||
TargetAccount: targetAccount,
|
||||
GTSModel: move,
|
||||
OriginAccount: originAcct,
|
||||
TargetAccount: targetAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
|
Reference in New Issue
Block a user