summaryrefslogtreecommitdiff
path: root/internal/processing/account/move.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/account/move.go')
-rw-r--r--internal/processing/account/move.go241
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 = &gtsmodel.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