diff options
Diffstat (limited to 'internal/processing/account/move.go')
-rw-r--r-- | internal/processing/account/move.go | 241 |
1 files changed, 203 insertions, 38 deletions
diff --git a/internal/processing/account/move.go b/internal/processing/account/move.go index cd5c577c6..ca8dd4dea 100644 --- a/internal/processing/account/move.go +++ b/internal/processing/account/move.go @@ -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()) } + // 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( + "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. - account = authed.Account + originAcct = authed.Account // Target account to which // the move is taking place. - targetAccount *gtsmodel.Account + targetAcct *gtsmodel.Account + + // AP representation of target. + targetAcctable ap.Accountable ) - 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. - err := fmt.Errorf( - "account %s is already Moved to %s, cannot also Move to %s", - account.URI, account.MovedToURI, form.MovedToURI, - ) - return gtserror.NewErrorUnprocessableEntity(err, err.Error()) - } + // 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 |