diff --git a/docs/user_guide/settings.md b/docs/user_guide/settings.md
index 89bc0a8bc..d82b2c084 100644
--- a/docs/user_guide/settings.md
+++ b/docs/user_guide/settings.md
@@ -137,6 +137,49 @@ You can use the Password Change section of the User Settings Panel to set a new
For more information on the way GoToSocial manages passwords, please see the [Password management document](./password_management.md).
+## Migration
+
+In the migration section you can manage settings related to aliasing and/or migrating your account to another account.
+
+!!! tip
+ Depending on the software that a target account is hosted on, target account URIs for both aliasing and moves should look something like `https://mastodon.example.org/users/account_you_are_moving_to`. If you are unsure what format to use, check with the admin of the instance you are moving or aliasing to.
+
+### Alias Account
+
+You can use this section to create an alias from your GoToSocial account to other accounts elsewhere, indicating that you are also known as those accounts.
+
+**Not implemented yet**: Alias information for accounts you enter here will be shown on the web view of your profile, but only if the target accounts are also aliased back to your account first. This is to prevent accounts from claiming to be aliased to other accounts that they don't actually control.
+
+### Move Account
+
+Using the move account settings, you can trigger the migration of your current account to the given target account URI.
+
+In order for the move to be successful, the target account (the account you are moving to) must be aliased back to your current account (the account you are moving from). The target account must also be reachable from your current account, ie., not blocked by you, not suspended by your current instance, and not on a domain that is blocked by your current instance. The target account does not have to be on a GoToSocial instance.
+
+GoToSocial uses an account move cooldown of 7 days. If either your current account or the target account have recently been involved in a move, you will not be able to trigger a move to the target account until seven days have passed.
+
+Moving your account will send a message out from your current account, to your current followers, indicating that they should follow the target account instead. Depending on the server software used by your followers, they may then automatically send a follow (request) to the target account, and unfollow your current account.
+
+Currently, **only your followers will be carried over to the new account**. Other things like your following list, statuses, media, bookmarks, faves, blocks, etc, will not be carried over.
+
+Once your account has moved, the web view of your current (now old) account will show a notice that you have moved, and to where.
+
+Your old statuses and media will still be visible on the web view of the account you've moved from, unless you delete them manually. If you prefer, you can ask the admin of the instance you've moved from to suspend/delete your account after the move has gone through.
+
+If necessary, you can retry an account move using the same target account URI. This will send the move message out again.
+
+!!! danger "Moving your account is an irreversible, permanent action!"
+
+ From the moment you trigger an account move, you will have only basic read- and delete-level permissions on the account you've moved from.
+
+ You will still be able to log in to your old account and see your own posts, faves, bookmarks, blocks, and lists.
+
+ You will also be able to edit your profile, delete and/or unpin your own posts, and unboost, unfave, and unbookmark posts.
+
+ However, you will not be able to take any action that involves creating something, such as writing, boosting, bookmarking, or faving a post, following someone, uploading media, creating a list, etc.
+
+ Additionally, you will not be able to view any timelines (home, tag, public, list), or use the search functionality.
+
## Admins
If your account has been promoted to admin, this interface will also show sections related to admin actions, see [Admin Settings](../admin/settings.md).
diff --git a/internal/ap/properties.go b/internal/ap/properties.go
index b77d20a02..1bd8c303e 100644
--- a/internal/ap/properties.go
+++ b/internal/ap/properties.go
@@ -192,7 +192,7 @@ func GetObjectIRIs(with WithObject) []*url.URL {
}
// AppendObjectIRIs appends the given IRIs to the Object property of 'with'.
-func AppendObjectIRIs(with WithObject) {
+func AppendObjectIRIs(with WithObject, object ...*url.URL) {
appendIRIs(func() Property[vocab.ActivityStreamsObjectPropertyIterator] {
objectProp := with.GetActivityStreamsObject()
if objectProp == nil {
@@ -200,7 +200,7 @@ func AppendObjectIRIs(with WithObject) {
with.SetActivityStreamsObject(objectProp)
}
return objectProp
- })
+ }, object...)
}
// GetTargetIRIs returns the IRIs contained in the Target property of 'with'.
@@ -210,7 +210,7 @@ func GetTargetIRIs(with WithTarget) []*url.URL {
}
// AppendTargetIRIs appends the given IRIs to the Target property of 'with'.
-func AppendTargetIRIs(with WithTarget) {
+func AppendTargetIRIs(with WithTarget, target ...*url.URL) {
appendIRIs(func() Property[vocab.ActivityStreamsTargetPropertyIterator] {
targetProp := with.GetActivityStreamsTarget()
if targetProp == nil {
@@ -218,7 +218,7 @@ func AppendTargetIRIs(with WithTarget) {
with.SetActivityStreamsTarget(targetProp)
}
return targetProp
- })
+ }, target...)
}
// GetAttributedTo returns the IRIs contained in the AttributedTo property of 'with'.
diff --git a/internal/api/client/accounts/follow.go b/internal/api/client/accounts/follow.go
index 2e6e79964..8a6e99744 100644
--- a/internal/api/client/accounts/follow.go
+++ b/internal/api/client/accounts/follow.go
@@ -97,6 +97,11 @@ func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/accounts/lookup.go b/internal/api/client/accounts/lookup.go
index f6bd97657..d2a8e76be 100644
--- a/internal/api/client/accounts/lookup.go
+++ b/internal/api/client/accounts/lookup.go
@@ -72,6 +72,13 @@ func (m *Module) AccountLookupGETHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ // For moving/moved accounts, just return
+ // empty to avoid breaking client apps.
+ apiutil.NotFoundAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/accounts/note.go b/internal/api/client/accounts/note.go
index 29ea01c9a..bcfd232ae 100644
--- a/internal/api/client/accounts/note.go
+++ b/internal/api/client/accounts/note.go
@@ -81,6 +81,11 @@ func (m *Module) AccountNotePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/accounts/search.go b/internal/api/client/accounts/search.go
index 183fc1347..13c135601 100644
--- a/internal/api/client/accounts/search.go
+++ b/internal/api/client/accounts/search.go
@@ -113,6 +113,13 @@ func (m *Module) AccountSearchGETHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ // For moving/moved accounts, just return
+ // empty to avoid breaking client apps.
+ apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/accounts/statuses.go b/internal/api/client/accounts/statuses.go
index cd93cb74e..7dd4cbe37 100644
--- a/internal/api/client/accounts/statuses.go
+++ b/internal/api/client/accounts/statuses.go
@@ -152,6 +152,13 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() && targetAcctID != authed.Account.ID {
+ // For moving/moved accounts, allow the
+ // account to view its own statuses only.
+ apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray)
+ return
+ }
+
limit := 30
limitString := c.Query(LimitKey)
if limitString != "" {
diff --git a/internal/api/client/admin/accountaction.go b/internal/api/client/admin/accountaction.go
index 89bcf644e..7d74e8530 100644
--- a/internal/api/client/admin/accountaction.go
+++ b/internal/api/client/admin/accountaction.go
@@ -99,6 +99,11 @@ func (m *Module) AccountActionPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
form := &apimodel.AdminActionRequest{}
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
diff --git a/internal/api/client/admin/domainkeysexpire.go b/internal/api/client/admin/domainkeysexpire.go
index 4990d879f..0926519f5 100644
--- a/internal/api/client/admin/domainkeysexpire.go
+++ b/internal/api/client/admin/domainkeysexpire.go
@@ -107,6 +107,11 @@ func (m *Module) DomainKeysExpirePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/domainpermission.go b/internal/api/client/admin/domainpermission.go
index 05319086f..90c0eb4c0 100644
--- a/internal/api/client/admin/domainpermission.go
+++ b/internal/api/client/admin/domainpermission.go
@@ -75,6 +75,11 @@ func (m *Module) createDomainPermissions(
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
@@ -178,6 +183,11 @@ func (m *Module) deleteDomainPermission(
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/emailtest.go b/internal/api/client/admin/emailtest.go
index 8f274e226..42b405ce7 100644
--- a/internal/api/client/admin/emailtest.go
+++ b/internal/api/client/admin/emailtest.go
@@ -93,6 +93,11 @@ func (m *Module) EmailTestPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go
index 9086b27e0..75661f1c3 100644
--- a/internal/api/client/admin/emojicreate.go
+++ b/internal/api/client/admin/emojicreate.go
@@ -110,6 +110,11 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/emojidelete.go b/internal/api/client/admin/emojidelete.go
index b5cf72daf..47248a1b9 100644
--- a/internal/api/client/admin/emojidelete.go
+++ b/internal/api/client/admin/emojidelete.go
@@ -87,6 +87,11 @@ func (m *Module) EmojiDELETEHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/emojiupdate.go b/internal/api/client/admin/emojiupdate.go
index ffde2d597..1d41dd545 100644
--- a/internal/api/client/admin/emojiupdate.go
+++ b/internal/api/client/admin/emojiupdate.go
@@ -137,6 +137,11 @@ func (m *Module) EmojiPATCHHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/headerfilter.go b/internal/api/client/admin/headerfilter.go
index 7b1a85c86..01bcaca16 100644
--- a/internal/api/client/admin/headerfilter.go
+++ b/internal/api/client/admin/headerfilter.go
@@ -114,6 +114,11 @@ func (m *Module) createHeaderFilter(c *gin.Context, create func(context.Context,
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
errWithCode := gtserror.NewErrorNotAcceptable(err, err.Error())
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
@@ -157,6 +162,11 @@ func (m *Module) deleteHeaderFilter(c *gin.Context, delete func(context.Context,
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
filterID, errWithCode := apiutil.ParseID(c.Param("ID"))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
diff --git a/internal/api/client/admin/mediacleanup.go b/internal/api/client/admin/mediacleanup.go
index 7a0ee4bd6..661a8ff15 100644
--- a/internal/api/client/admin/mediacleanup.go
+++ b/internal/api/client/admin/mediacleanup.go
@@ -81,6 +81,11 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
form := &apimodel.MediaCleanupRequest{}
if err := c.ShouldBind(form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
diff --git a/internal/api/client/admin/mediarefetch.go b/internal/api/client/admin/mediarefetch.go
index 1c0da6dea..b2b0516ba 100644
--- a/internal/api/client/admin/mediarefetch.go
+++ b/internal/api/client/admin/mediarefetch.go
@@ -83,6 +83,11 @@ func (m *Module) MediaRefetchPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if errWithCode := m.processor.Admin().MediaRefetch(c.Request.Context(), authed.Account, c.Query(DomainQueryKey)); errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/reportresolve.go b/internal/api/client/admin/reportresolve.go
index 2ad979b0b..51c268a2d 100644
--- a/internal/api/client/admin/reportresolve.go
+++ b/internal/api/client/admin/reportresolve.go
@@ -97,6 +97,11 @@ func (m *Module) ReportResolvePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/rulecreate.go b/internal/api/client/admin/rulecreate.go
index 155c69db0..8728940c5 100644
--- a/internal/api/client/admin/rulecreate.go
+++ b/internal/api/client/admin/rulecreate.go
@@ -77,6 +77,11 @@ func (m *Module) RulePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/ruledelete.go b/internal/api/client/admin/ruledelete.go
index 834149978..ead219e34 100644
--- a/internal/api/client/admin/ruledelete.go
+++ b/internal/api/client/admin/ruledelete.go
@@ -85,6 +85,11 @@ func (m *Module) RuleDELETEHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/admin/ruleupdate.go b/internal/api/client/admin/ruleupdate.go
index 2ba31485e..bf838f7ae 100644
--- a/internal/api/client/admin/ruleupdate.go
+++ b/internal/api/client/admin/ruleupdate.go
@@ -77,6 +77,11 @@ func (m *Module) RulePATCHHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/filters/v1/filterpost.go b/internal/api/client/filters/v1/filterpost.go
index 4c71eeddb..2d19f69cf 100644
--- a/internal/api/client/filters/v1/filterpost.go
+++ b/internal/api/client/filters/v1/filterpost.go
@@ -131,6 +131,11 @@ func (m *Module) FilterPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/filters/v1/filterput.go b/internal/api/client/filters/v1/filterput.go
index b7164936b..bb9fa809f 100644
--- a/internal/api/client/filters/v1/filterput.go
+++ b/internal/api/client/filters/v1/filterput.go
@@ -137,6 +137,11 @@ func (m *Module) FilterPUTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/followrequests/authorize.go b/internal/api/client/followrequests/authorize.go
index 406b54179..6a6f0dc81 100644
--- a/internal/api/client/followrequests/authorize.go
+++ b/internal/api/client/followrequests/authorize.go
@@ -75,6 +75,11 @@ func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go
index 58549a866..afddc5a50 100644
--- a/internal/api/client/instance/instancepatch.go
+++ b/internal/api/client/instance/instancepatch.go
@@ -144,6 +144,11 @@ func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
form := &apimodel.InstanceSettingsUpdateRequest{}
if err := c.ShouldBind(&form); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
diff --git a/internal/api/client/lists/listaccountsadd.go b/internal/api/client/lists/listaccountsadd.go
index 6fb5eab3c..e20056502 100644
--- a/internal/api/client/lists/listaccountsadd.go
+++ b/internal/api/client/lists/listaccountsadd.go
@@ -87,6 +87,11 @@ func (m *Module) ListAccountsPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/lists/listcreate.go b/internal/api/client/lists/listcreate.go
index 4228e5fff..9046ce34d 100644
--- a/internal/api/client/lists/listcreate.go
+++ b/internal/api/client/lists/listcreate.go
@@ -74,6 +74,11 @@ func (m *Module) ListCreatePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/lists/listupdate.go b/internal/api/client/lists/listupdate.go
index 966de4098..312aa9ec7 100644
--- a/internal/api/client/lists/listupdate.go
+++ b/internal/api/client/lists/listupdate.go
@@ -104,6 +104,11 @@ func (m *Module) ListUpdatePUTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go
index daa2e5bb7..eef945d21 100644
--- a/internal/api/client/media/mediacreate.go
+++ b/internal/api/client/media/mediacreate.go
@@ -108,6 +108,11 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/media/mediaupdate.go b/internal/api/client/media/mediaupdate.go
index 8378502e8..0a9ce4eb8 100644
--- a/internal/api/client/media/mediaupdate.go
+++ b/internal/api/client/media/mediaupdate.go
@@ -112,6 +112,11 @@ func (m *Module) MediaPUTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/polls/polls_vote.go b/internal/api/client/polls/polls_vote.go
index 0ab5ac20c..c5344326f 100644
--- a/internal/api/client/polls/polls_vote.go
+++ b/internal/api/client/polls/polls_vote.go
@@ -87,6 +87,11 @@ func (m *Module) PollVotePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
errWithCode := gtserror.NewErrorNotAcceptable(err, err.Error())
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
diff --git a/internal/api/client/reports/reportcreate.go b/internal/api/client/reports/reportcreate.go
index a34b8d52e..a303cf20a 100644
--- a/internal/api/client/reports/reportcreate.go
+++ b/internal/api/client/reports/reportcreate.go
@@ -72,6 +72,11 @@ func (m *Module) ReportPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/search/searchget.go b/internal/api/client/search/searchget.go
index 909c14f24..76cb929bf 100644
--- a/internal/api/client/search/searchget.go
+++ b/internal/api/client/search/searchget.go
@@ -175,6 +175,18 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ // For moving/moved accounts, just return
+ // empty to avoid breaking client apps.
+ results := &apimodel.SearchResult{
+ Accounts: make([]*apimodel.Account, 0),
+ Statuses: make([]*apimodel.Status, 0),
+ Hashtags: make([]any, 0),
+ }
+ apiutil.JSON(c, http.StatusOK, results)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/statuses/statusbookmark.go b/internal/api/client/statuses/statusbookmark.go
index cd1dd1c72..9dbc0f56e 100644
--- a/internal/api/client/statuses/statusbookmark.go
+++ b/internal/api/client/statuses/statusbookmark.go
@@ -75,6 +75,11 @@ func (m *Module) StatusBookmarkPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/statuses/statusboost.go b/internal/api/client/statuses/statusboost.go
index 1a3ca0eb2..035ee8747 100644
--- a/internal/api/client/statuses/statusboost.go
+++ b/internal/api/client/statuses/statusboost.go
@@ -78,6 +78,11 @@ func (m *Module) StatusBoostPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go
index efbe79223..5a9654195 100644
--- a/internal/api/client/statuses/statuscreate.go
+++ b/internal/api/client/statuses/statuscreate.go
@@ -218,6 +218,11 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/statuses/statusfave.go b/internal/api/client/statuses/statusfave.go
index 947760af3..41d45c6b8 100644
--- a/internal/api/client/statuses/statusfave.go
+++ b/internal/api/client/statuses/statusfave.go
@@ -74,6 +74,11 @@ func (m *Module) StatusFavePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/statuses/statusmute.go b/internal/api/client/statuses/statusmute.go
index 95ada8939..58d14a8bf 100644
--- a/internal/api/client/statuses/statusmute.go
+++ b/internal/api/client/statuses/statusmute.go
@@ -78,6 +78,11 @@ func (m *Module) StatusMutePOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/statuses/statuspin.go b/internal/api/client/statuses/statuspin.go
index 4c58eb1a5..e5879f715 100644
--- a/internal/api/client/statuses/statuspin.go
+++ b/internal/api/client/statuses/statuspin.go
@@ -80,6 +80,11 @@ func (m *Module) StatusPinPOSTHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go
index 8df4e9e76..e39c780b6 100644
--- a/internal/api/client/streaming/stream.go
+++ b/internal/api/client/streaming/stream.go
@@ -185,6 +185,13 @@ func (m *Module) StreamGETHandler(c *gin.Context) {
account = authed.Account
}
+ if account.IsMoving() {
+ // Moving accounts can't
+ // use streaming endpoints.
+ apiutil.NotFoundAfterMove(c)
+ return
+ }
+
// Get the initial requested stream type, if there is one.
streamType := c.Query(StreamQueryKey)
diff --git a/internal/api/client/timelines/home.go b/internal/api/client/timelines/home.go
index a7e7717da..55928dd3a 100644
--- a/internal/api/client/timelines/home.go
+++ b/internal/api/client/timelines/home.go
@@ -113,6 +113,13 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ // For moving/moved accounts, just return
+ // empty to avoid breaking client apps.
+ apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/timelines/list.go b/internal/api/client/timelines/list.go
index dc5f21424..25695bf0e 100644
--- a/internal/api/client/timelines/list.go
+++ b/internal/api/client/timelines/list.go
@@ -112,6 +112,13 @@ func (m *Module) ListTimelineGETHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ // For moving/moved accounts, just return
+ // empty to avoid breaking client apps.
+ apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/timelines/public.go b/internal/api/client/timelines/public.go
index 8eb34edc7..c4ffbc6c8 100644
--- a/internal/api/client/timelines/public.go
+++ b/internal/api/client/timelines/public.go
@@ -124,6 +124,13 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
return
}
+ if authed.Account != nil && authed.Account.IsMoving() {
+ // For moving/moved accounts, just return
+ // empty to avoid breaking client apps.
+ apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/client/timelines/tag.go b/internal/api/client/timelines/tag.go
index e66955a73..258184355 100644
--- a/internal/api/client/timelines/tag.go
+++ b/internal/api/client/timelines/tag.go
@@ -114,6 +114,13 @@ func (m *Module) TagTimelineGETHandler(c *gin.Context) {
return
}
+ if authed.Account.IsMoving() {
+ // For moving/moved accounts, just return
+ // empty to avoid breaking client apps.
+ apiutil.Data(c, http.StatusOK, apiutil.AppJSON, apiutil.EmptyJSONArray)
+ return
+ }
+
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
return
diff --git a/internal/api/util/errorhandling.go b/internal/api/util/errorhandling.go
index 848beff5b..d2b9171c8 100644
--- a/internal/api/util/errorhandling.go
+++ b/internal/api/util/errorhandling.go
@@ -184,3 +184,21 @@ func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) {
"error_description": errWithCode.Safe(),
})
}
+
+// NotFoundAfterMove returns code 404 to the caller and writes a helpful error message.
+// Specifically used for accounts trying to access endpoints they cannot use while moving.
+func NotFoundAfterMove(c *gin.Context) {
+ const errMsg = "your account has Moved or is currently Moving; you cannot use this endpoint"
+ JSON(c, http.StatusForbidden, map[string]string{
+ "error": errMsg,
+ })
+}
+
+// ForbiddenAfterMove returns code 403 to the caller and writes a helpful error message.
+// Specifically used for accounts trying to take actions on endpoints they cannot do while moving.
+func ForbiddenAfterMove(c *gin.Context) {
+ const errMsg = "your account has Moved or is currently Moving; you cannot take create or update type actions"
+ JSON(c, http.StatusForbidden, map[string]string{
+ "error": errMsg,
+ })
+}
diff --git a/internal/db/bundb/move.go b/internal/db/bundb/move.go
index a66b9dea5..220874630 100644
--- a/internal/db/bundb/move.go
+++ b/internal/db/bundb/move.go
@@ -177,21 +177,31 @@ func (m *moveDB) getMove(
}
// Populate the Move by parsing out the URIs.
+ if err := m.PopulateMove(ctx, move); err != nil {
+ return nil, err
+ }
+
+ return move, nil
+}
+
+func (m *moveDB) PopulateMove(ctx context.Context, move *gtsmodel.Move) error {
if move.Origin == nil {
+ var err error
move.Origin, err = url.Parse(move.OriginURI)
if err != nil {
- return nil, fmt.Errorf("error parsing Move originURI: %w", err)
+ return fmt.Errorf("error parsing Move originURI: %w", err)
}
}
if move.Target == nil {
+ var err error
move.Target, err = url.Parse(move.TargetURI)
if err != nil {
- return nil, fmt.Errorf("error parsing Move originURI: %w", err)
+ return fmt.Errorf("error parsing Move targetURI: %w", err)
}
}
- return move, nil
+ return nil
}
func (m *moveDB) PutMove(ctx context.Context, move *gtsmodel.Move) error {
diff --git a/internal/db/move.go b/internal/db/move.go
index 5bce781a3..42357627b 100644
--- a/internal/db/move.go
+++ b/internal/db/move.go
@@ -34,6 +34,9 @@ type Move interface {
// GetMoveByOriginTarget gets one move with the given originURI and targetURI.
GetMoveByOriginTarget(ctx context.Context, originURI string, targetURI string) (*gtsmodel.Move, error)
+ // PopulateMove parses out the origin and target URIs on the move.
+ PopulateMove(ctx context.Context, move *gtsmodel.Move) error
+
// GetLatestMoveSuccessInvolvingURIs gets the time of
// the latest successfully-processed Move that includes
// either uri1 or uri2 in target or origin positions.
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 @@
"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
diff --git a/internal/processing/account/move_test.go b/internal/processing/account/move_test.go
new file mode 100644
index 000000000..dfa0ea4e4
--- /dev/null
+++ b/internal/processing/account/move_test.go
@@ -0,0 +1,175 @@
+// 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