summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/ap/properties.go8
-rw-r--r--internal/api/client/accounts/follow.go5
-rw-r--r--internal/api/client/accounts/lookup.go7
-rw-r--r--internal/api/client/accounts/note.go5
-rw-r--r--internal/api/client/accounts/search.go7
-rw-r--r--internal/api/client/accounts/statuses.go7
-rw-r--r--internal/api/client/admin/accountaction.go5
-rw-r--r--internal/api/client/admin/domainkeysexpire.go5
-rw-r--r--internal/api/client/admin/domainpermission.go10
-rw-r--r--internal/api/client/admin/emailtest.go5
-rw-r--r--internal/api/client/admin/emojicreate.go5
-rw-r--r--internal/api/client/admin/emojidelete.go5
-rw-r--r--internal/api/client/admin/emojiupdate.go5
-rw-r--r--internal/api/client/admin/headerfilter.go10
-rw-r--r--internal/api/client/admin/mediacleanup.go5
-rw-r--r--internal/api/client/admin/mediarefetch.go5
-rw-r--r--internal/api/client/admin/reportresolve.go5
-rw-r--r--internal/api/client/admin/rulecreate.go5
-rw-r--r--internal/api/client/admin/ruledelete.go5
-rw-r--r--internal/api/client/admin/ruleupdate.go5
-rw-r--r--internal/api/client/filters/v1/filterpost.go5
-rw-r--r--internal/api/client/filters/v1/filterput.go5
-rw-r--r--internal/api/client/followrequests/authorize.go5
-rw-r--r--internal/api/client/instance/instancepatch.go5
-rw-r--r--internal/api/client/lists/listaccountsadd.go5
-rw-r--r--internal/api/client/lists/listcreate.go5
-rw-r--r--internal/api/client/lists/listupdate.go5
-rw-r--r--internal/api/client/media/mediacreate.go5
-rw-r--r--internal/api/client/media/mediaupdate.go5
-rw-r--r--internal/api/client/polls/polls_vote.go5
-rw-r--r--internal/api/client/reports/reportcreate.go5
-rw-r--r--internal/api/client/search/searchget.go12
-rw-r--r--internal/api/client/statuses/statusbookmark.go5
-rw-r--r--internal/api/client/statuses/statusboost.go5
-rw-r--r--internal/api/client/statuses/statuscreate.go5
-rw-r--r--internal/api/client/statuses/statusfave.go5
-rw-r--r--internal/api/client/statuses/statusmute.go5
-rw-r--r--internal/api/client/statuses/statuspin.go5
-rw-r--r--internal/api/client/streaming/stream.go7
-rw-r--r--internal/api/client/timelines/home.go7
-rw-r--r--internal/api/client/timelines/list.go7
-rw-r--r--internal/api/client/timelines/public.go7
-rw-r--r--internal/api/client/timelines/tag.go7
-rw-r--r--internal/api/util/errorhandling.go18
-rw-r--r--internal/db/bundb/move.go16
-rw-r--r--internal/db/move.go3
-rw-r--r--internal/processing/account/move.go241
-rw-r--r--internal/processing/account/move_test.go175
-rw-r--r--internal/processing/workers/federate.go66
-rw-r--r--internal/processing/workers/fromclientapi.go53
-rw-r--r--internal/processing/workers/fromfediapi.go12
-rw-r--r--internal/processing/workers/fromfediapi_move.go95
-rw-r--r--internal/processing/workers/util.go240
-rw-r--r--internal/processing/workers/wipestatus.go135
-rw-r--r--internal/processing/workers/workers.go36
-rw-r--r--internal/state/state.go6
-rw-r--r--internal/uris/uri.go9
57 files changed, 1051 insertions, 305 deletions
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 @@ 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
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 <http://www.gnu.org/licenses/>.
+
+package account_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+type MoveTestSuite struct {
+ AccountStandardTestSuite
+}
+
+func (suite *MoveTestSuite) TestMoveAccountOK() {
+ ctx := context.Background()
+
+ // Copy zork.
+ requestingAcct := new(gtsmodel.Account)
+ *requestingAcct = *suite.testAccounts["local_account_1"]
+
+ // Copy admin.
+ targetAcct := new(gtsmodel.Account)
+ *targetAcct = *suite.testAccounts["admin_account"]
+
+ // Update admin to alias back to zork.
+ targetAcct.AlsoKnownAsURIs = []string{requestingAcct.URI}
+ if err := suite.state.DB.UpdateAccount(
+ ctx,
+ targetAcct,
+ "also_known_as_uris",
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Trigger move from zork to admin.
+ if err := suite.accountProcessor.MoveSelf(
+ ctx,
+ &oauth.Auth{
+ Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
+ Application: suite.testApplications["local_account_1"],
+ User: suite.testUsers["local_account_1"],
+ Account: requestingAcct,
+ },
+ &apimodel.AccountMoveRequest{
+ Password: "password",
+ MovedToURI: targetAcct.URI,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // There should be a msg heading back to fromClientAPI.
+ select {
+ case msg := <-suite.fromClientAPIChan:
+ move, ok := msg.GTSModel.(*gtsmodel.Move)
+ if !ok {
+ suite.FailNow("", "could not cast %T to *gtsmodel.Move", move)
+ }
+
+ now := time.Now()
+ suite.WithinDuration(now, move.CreatedAt, 5*time.Second)
+ suite.WithinDuration(now, move.UpdatedAt, 5*time.Second)
+ suite.WithinDuration(now, move.AttemptedAt, 5*time.Second)
+ suite.Zero(move.SucceededAt)
+ suite.NotZero(move.ID)
+ suite.Equal(requestingAcct.URI, move.OriginURI)
+ suite.NotNil(move.Origin)
+ suite.Equal(targetAcct.URI, move.TargetURI)
+ suite.NotNil(move.Target)
+ suite.NotZero(move.URI)
+
+ case <-time.After(5 * time.Second):
+ suite.FailNow("time out waiting for message")
+ }
+
+ // Move should be in the database now.
+ move, err := suite.state.DB.GetMoveByOriginTarget(
+ ctx,
+ requestingAcct.URI,
+ targetAcct.URI,
+ )
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.NotNil(move)
+
+ // Origin account should have move ID and move to URI set.
+ suite.Equal(move.ID, requestingAcct.MoveID)
+ suite.Equal(targetAcct.URI, requestingAcct.MovedToURI)
+}
+
+func (suite *MoveTestSuite) TestMoveAccountNotAliased() {
+ ctx := context.Background()
+
+ // Copy zork.
+ requestingAcct := new(gtsmodel.Account)
+ *requestingAcct = *suite.testAccounts["local_account_1"]
+
+ // Don't copy admin.
+ targetAcct := suite.testAccounts["admin_account"]
+
+ // Trigger move from zork to admin.
+ //
+ // Move should fail since admin is
+ // not aliased back to zork.
+ err := suite.accountProcessor.MoveSelf(
+ ctx,
+ &oauth.Auth{
+ Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
+ Application: suite.testApplications["local_account_1"],
+ User: suite.testUsers["local_account_1"],
+ Account: requestingAcct,
+ },
+ &apimodel.AccountMoveRequest{
+ Password: "password",
+ MovedToURI: targetAcct.URI,
+ },
+ )
+ suite.EqualError(err, "target account http://localhost:8080/users/admin is not aliased to this account via alsoKnownAs; if you just changed it, please wait a few minutes and try the Move again")
+}
+
+func (suite *MoveTestSuite) TestMoveAccountBadPassword() {
+ ctx := context.Background()
+
+ // Copy zork.
+ requestingAcct := new(gtsmodel.Account)
+ *requestingAcct = *suite.testAccounts["local_account_1"]
+
+ // Don't copy admin.
+ targetAcct := suite.testAccounts["admin_account"]
+
+ // Trigger move from zork to admin.
+ //
+ // Move should fail since admin is
+ // not aliased back to zork.
+ err := suite.accountProcessor.MoveSelf(
+ ctx,
+ &oauth.Auth{
+ Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
+ Application: suite.testApplications["local_account_1"],
+ User: suite.testUsers["local_account_1"],
+ Account: requestingAcct,
+ },
+ &apimodel.AccountMoveRequest{
+ Password: "boobies",
+ MovedToURI: targetAcct.URI,
+ },
+ )
+ suite.EqualError(err, "invalid password provided in account Move request")
+}
+
+func TestMoveTestSuite(t *testing.T) {
+ suite.Run(t, new(MoveTestSuite))
+}
diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go
index aacb8dcc8..9fdb8f662 100644
--- a/internal/processing/workers/federate.go
+++ b/internal/processing/workers/federate.go
@@ -23,6 +23,7 @@ import (
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@@ -954,3 +955,68 @@ func (f *federate) Flag(ctx context.Context, report *gtsmodel.Report) error {
return nil
}
+
+func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) error {
+ // Do nothing if it's not our
+ // account that's been moved.
+ if !account.IsLocal() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(account.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ // Actor doing the Move.
+ actorIRI := account.Move.Origin
+
+ // Destination Actor of the Move.
+ targetIRI := account.Move.Target
+
+ followersIRI, err := parseURI(account.FollowersURI)
+ if err != nil {
+ return err
+ }
+
+ publicIRI, err := parseURI(pub.PublicActivityPubIRI)
+ if err != nil {
+ return err
+ }
+
+ // Create a new move.
+ move := streams.NewActivityStreamsMove()
+
+ // Set the Move ID.
+ if err := ap.SetJSONLDIdStr(move, account.Move.URI); err != nil {
+ return err
+ }
+
+ // Set the Actor for the Move.
+ ap.AppendActorIRIs(move, actorIRI)
+
+ // Set the account's IRI as the 'object' property.
+ ap.AppendObjectIRIs(move, actorIRI)
+
+ // Set the target's IRI as the 'target' property.
+ ap.AppendTargetIRIs(move, targetIRI)
+
+ // Address the move To followers.
+ ap.AppendTo(move, followersIRI)
+
+ // Address the move CC public.
+ ap.AppendCc(move, publicIRI)
+
+ // Send the Move via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, move,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T via outbox %s: %w",
+ move, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
index 05b9acc1f..c7e78fee2 100644
--- a/internal/processing/workers/fromclientapi.go
+++ b/internal/processing/workers/fromclientapi.go
@@ -39,12 +39,12 @@ import (
// specifically for messages originating
// from the client/REST API.
type clientAPI struct {
- state *state.State
- converter *typeutils.Converter
- surface *surface
- federate *federate
- wipeStatus wipeStatus
- account *account.Processor
+ state *state.State
+ converter *typeutils.Converter
+ surface *surface
+ federate *federate
+ account *account.Processor
+ utilF *utilF
}
func (p *Processor) EnqueueClientAPI(cctx context.Context, msgs ...messages.FromClientAPI) {
@@ -194,6 +194,15 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.From
case ap.ObjectProfile:
return p.clientAPI.ReportAccount(ctx, cMsg)
}
+
+ // MOVE SOMETHING
+ case ap.ActivityMove:
+ switch cMsg.APObjectType { //nolint:gocritic
+
+ // MOVE PROFILE/ACCOUNT
+ case ap.ObjectProfile, ap.ActorPerson:
+ return p.clientAPI.MoveAccount(ctx, cMsg)
+ }
}
return gtserror.Newf("unhandled: %s %s", cMsg.APActivityType, cMsg.APObjectType)
@@ -576,7 +585,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAP
return gtserror.Newf("db error populating status: %w", err)
}
- if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
+ if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil {
log.Errorf(ctx, "error wiping status: %v", err)
}
@@ -641,3 +650,33 @@ func (p *clientAPI) ReportAccount(ctx context.Context, cMsg messages.FromClientA
return nil
}
+
+func (p *clientAPI) MoveAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
+ // Redirect each local follower of
+ // OriginAccount to follow move target.
+ p.utilF.redirectFollowers(ctx, cMsg.OriginAccount, cMsg.TargetAccount)
+
+ // At this point, we know OriginAccount has the
+ // Move set on it. Just make sure it's populated.
+ if err := p.state.DB.PopulateMove(ctx, cMsg.OriginAccount.Move); err != nil {
+ return gtserror.Newf("error populating Move: %w", err)
+ }
+
+ // Now send the Move message out to
+ // OriginAccount's (remote) followers.
+ if err := p.federate.MoveAccount(ctx, cMsg.OriginAccount); err != nil {
+ return gtserror.Newf("error federating account move: %w", err)
+ }
+
+ // Mark the move attempt as successful.
+ cMsg.OriginAccount.Move.SucceededAt = cMsg.OriginAccount.Move.AttemptedAt
+ if err := p.state.DB.UpdateMove(
+ ctx,
+ cMsg.OriginAccount.Move,
+ "succeeded_at",
+ ); err != nil {
+ return gtserror.Newf("error marking move as successful: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 62cb58c83..2fc3b4b26 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -39,11 +39,11 @@ import (
// specifically for messages originating
// from the federation/ActivityPub API.
type fediAPI struct {
- state *state.State
- surface *surface
- federate *federate
- wipeStatus wipeStatus
- account *account.Processor
+ state *state.State
+ surface *surface
+ federate *federate
+ account *account.Processor
+ utilF *utilF
}
func (p *Processor) EnqueueFediAPI(cctx context.Context, msgs ...messages.FromFediAPI) {
@@ -563,7 +563,7 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) e
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
}
- if err := p.wipeStatus(ctx, status, deleteAttachments); err != nil {
+ if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil {
log.Errorf(ctx, "error wiping status: %v", err)
}
diff --git a/internal/processing/workers/fromfediapi_move.go b/internal/processing/workers/fromfediapi_move.go
index 2223a21f5..0188a5d14 100644
--- a/internal/processing/workers/fromfediapi_move.go
+++ b/internal/processing/workers/fromfediapi_move.go
@@ -22,7 +22,6 @@ import (
"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"
@@ -380,7 +379,7 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er
// Transfer originAcct's followers
// on this instance to targetAcct.
- redirectOK := p.RedirectAccountFollowers(
+ redirectOK := p.utilF.redirectFollowers(
ctx,
originAcct,
targetAcct,
@@ -422,98 +421,6 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er
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.
//
diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go
new file mode 100644
index 000000000..a38ecd336
--- /dev/null
+++ b/internal/processing/workers/util.go
@@ -0,0 +1,240 @@
+// 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"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/account"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/media"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+)
+
+// utilF wraps util functions used by both
+// the fromClientAPI and fromFediAPI functions.
+type utilF struct {
+ state *state.State
+ media *media.Processor
+ account *account.Processor
+ surface *surface
+}
+
+// wipeStatus encapsulates common logic
+// used to totally delete a status + all
+// its attachments, notifications, boosts,
+// and timeline entries.
+func (u *utilF) wipeStatus(
+ ctx context.Context,
+ statusToDelete *gtsmodel.Status,
+ deleteAttachments bool,
+) error {
+ var errs gtserror.MultiError
+
+ // Either delete all attachments for this status,
+ // or simply unattach + clean them separately later.
+ //
+ // Reason to unattach rather than delete is that
+ // the poster might want to reattach them to another
+ // status immediately (in case of delete + redraft)
+ if deleteAttachments {
+ // todo:u.state.DB.DeleteAttachmentsForStatus
+ for _, id := range statusToDelete.AttachmentIDs {
+ if err := u.media.Delete(ctx, id); err != nil {
+ errs.Appendf("error deleting media: %w", err)
+ }
+ }
+ } else {
+ // todo:u.state.DB.UnattachAttachmentsForStatus
+ for _, id := range statusToDelete.AttachmentIDs {
+ if _, err := u.media.Unattach(ctx, statusToDelete.Account, id); err != nil {
+ errs.Appendf("error unattaching media: %w", err)
+ }
+ }
+ }
+
+ // delete all mention entries generated by this status
+ // todo:u.state.DB.DeleteMentionsForStatus
+ for _, id := range statusToDelete.MentionIDs {
+ if err := u.state.DB.DeleteMentionByID(ctx, id); err != nil {
+ errs.Appendf("error deleting status mention: %w", err)
+ }
+ }
+
+ // delete all notification entries generated by this status
+ if err := u.state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status notifications: %w", err)
+ }
+
+ // delete all bookmarks that point to this status
+ if err := u.state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status bookmarks: %w", err)
+ }
+
+ // delete all faves of this status
+ if err := u.state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status faves: %w", err)
+ }
+
+ if pollID := statusToDelete.PollID; pollID != "" {
+ // Delete this poll by ID from the database.
+ if err := u.state.DB.DeletePollByID(ctx, pollID); err != nil {
+ errs.Appendf("error deleting status poll: %w", err)
+ }
+
+ // Delete any poll votes pointing to this poll ID.
+ if err := u.state.DB.DeletePollVotes(ctx, pollID); err != nil {
+ errs.Appendf("error deleting status poll votes: %w", err)
+ }
+
+ // Cancel any scheduled expiry task for poll.
+ _ = u.state.Workers.Scheduler.Cancel(pollID)
+ }
+
+ // delete all boosts for this status + remove them from timelines
+ boosts, err := u.state.DB.GetStatusBoosts(
+ // we MUST set a barebones context here,
+ // as depending on where it came from the
+ // original BoostOf may already be gone.
+ gtscontext.SetBarebones(ctx),
+ statusToDelete.ID)
+ if err != nil {
+ errs.Appendf("error fetching status boosts: %w", err)
+ }
+
+ for _, boost := range boosts {
+ if err := u.surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
+ errs.Appendf("error deleting boost from timelines: %w", err)
+ }
+ if err := u.state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
+ errs.Appendf("error deleting boost: %w", err)
+ }
+ }
+
+ // delete this status from any and all timelines
+ if err := u.surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status from timelines: %w", err)
+ }
+
+ // finally, delete the status itself
+ if err := u.state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
+ errs.Appendf("error deleting status: %w", err)
+ }
+
+ return errs.Combine()
+}
+
+// redirectFollowers redirects all local
+// followers of originAcct to targetAcct.
+//
+// Both accounts must be fully dereferenced
+// already, and the Move must be valid.
+//
+// Return bool will be true if all goes OK.
+func (u *utilF) redirectFollowers(
+ 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 := u.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 = u.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.
+ //
+ // Also, ensure new follow wouldn't be a
+ // self follow, since that will error.
+ if follow.AccountID != targetAcct.ID {
+ if _, err := u.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 := u.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
+}
diff --git a/internal/processing/workers/wipestatus.go b/internal/processing/workers/wipestatus.go
deleted file mode 100644
index 90a037928..000000000
--- a/internal/processing/workers/wipestatus.go
+++ /dev/null
@@ -1,135 +0,0 @@
-// 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"
-
- "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/processing/media"
- "github.com/superseriousbusiness/gotosocial/internal/state"
-)
-
-// wipeStatus encapsulates common logic used to totally delete a status
-// + all its attachments, notifications, boosts, and timeline entries.
-type wipeStatus func(context.Context, *gtsmodel.Status, bool) error
-
-// wipeStatusF returns a wipeStatus util function.
-func wipeStatusF(state *state.State, media *media.Processor, surface *surface) wipeStatus {
- return func(
- ctx context.Context,
- statusToDelete *gtsmodel.Status,
- deleteAttachments bool,
- ) error {
- var errs gtserror.MultiError
-
- // Either delete all attachments for this status,
- // or simply unattach + clean them separately later.
- //
- // Reason to unattach rather than delete is that
- // the poster might want to reattach them to another
- // status immediately (in case of delete + redraft)
- if deleteAttachments {
- // todo:state.DB.DeleteAttachmentsForStatus
- for _, id := range statusToDelete.AttachmentIDs {
- if err := media.Delete(ctx, id); err != nil {
- errs.Appendf("error deleting media: %w", err)
- }
- }
- } else {
- // todo:state.DB.UnattachAttachmentsForStatus
- for _, id := range statusToDelete.AttachmentIDs {
- if _, err := media.Unattach(ctx, statusToDelete.Account, id); err != nil {
- errs.Appendf("error unattaching media: %w", err)
- }
- }
- }
-
- // delete all mention entries generated by this status
- // todo:state.DB.DeleteMentionsForStatus
- for _, id := range statusToDelete.MentionIDs {
- if err := state.DB.DeleteMentionByID(ctx, id); err != nil {
- errs.Appendf("error deleting status mention: %w", err)
- }
- }
-
- // delete all notification entries generated by this status
- if err := state.DB.DeleteNotificationsForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status notifications: %w", err)
- }
-
- // delete all bookmarks that point to this status
- if err := state.DB.DeleteStatusBookmarksForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status bookmarks: %w", err)
- }
-
- // delete all faves of this status
- if err := state.DB.DeleteStatusFavesForStatus(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status faves: %w", err)
- }
-
- if pollID := statusToDelete.PollID; pollID != "" {
- // Delete this poll by ID from the database.
- if err := state.DB.DeletePollByID(ctx, pollID); err != nil {
- errs.Appendf("error deleting status poll: %w", err)
- }
-
- // Delete any poll votes pointing to this poll ID.
- if err := state.DB.DeletePollVotes(ctx, pollID); err != nil {
- errs.Appendf("error deleting status poll votes: %w", err)
- }
-
- // Cancel any scheduled expiry task for poll.
- _ = state.Workers.Scheduler.Cancel(pollID)
- }
-
- // delete all boosts for this status + remove them from timelines
- boosts, err := state.DB.GetStatusBoosts(
- // we MUST set a barebones context here,
- // as depending on where it came from the
- // original BoostOf may already be gone.
- gtscontext.SetBarebones(ctx),
- statusToDelete.ID)
- if err != nil {
- errs.Appendf("error fetching status boosts: %w", err)
- }
-
- for _, boost := range boosts {
- if err := surface.deleteStatusFromTimelines(ctx, boost.ID); err != nil {
- errs.Appendf("error deleting boost from timelines: %w", err)
- }
- if err := state.DB.DeleteStatusByID(ctx, boost.ID); err != nil {
- errs.Appendf("error deleting boost: %w", err)
- }
- }
-
- // delete this status from any and all timelines
- if err := surface.deleteStatusFromTimelines(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status from timelines: %w", err)
- }
-
- // finally, delete the status itself
- if err := state.DB.DeleteStatusByID(ctx, statusToDelete.ID); err != nil {
- errs.Appendf("error deleting status: %w", err)
- }
-
- return errs.Combine()
- }
-}
diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go
index c0612de27..8488e501c 100644
--- a/internal/processing/workers/workers.go
+++ b/internal/processing/workers/workers.go
@@ -63,30 +63,30 @@ func New(
converter: converter,
}
- // Init shared logic wipe
- // status util func.
- wipeStatus := wipeStatusF(
- state,
- media,
- surface,
- )
+ // Init shared util funcs.
+ utilF := &utilF{
+ state: state,
+ media: media,
+ account: account,
+ surface: surface,
+ }
return Processor{
workers: &state.Workers,
clientAPI: &clientAPI{
- state: state,
- converter: converter,
- surface: surface,
- federate: federate,
- wipeStatus: wipeStatus,
- account: account,
+ state: state,
+ converter: converter,
+ surface: surface,
+ federate: federate,
+ account: account,
+ utilF: utilF,
},
fediAPI: &fediAPI{
- state: state,
- surface: surface,
- federate: federate,
- wipeStatus: wipeStatus,
- account: account,
+ state: state,
+ surface: surface,
+ federate: federate,
+ account: account,
+ utilF: utilF,
},
}
}
diff --git a/internal/state/state.go b/internal/state/state.go
index 5dfe83271..6120515b9 100644
--- a/internal/state/state.go
+++ b/internal/state/state.go
@@ -50,6 +50,12 @@ type State struct {
// functions, and by the go-fed/activity library.
FedLocks mutexes.MutexMap
+ // ClientLocks provides access to this state's
+ // mutex map of per URI client locks.
+ //
+ // Used during account migration actions.
+ ClientLocks mutexes.MutexMap
+
// Storage provides access to the storage driver.
Storage *storage.Driver
diff --git a/internal/uris/uri.go b/internal/uris/uri.go
index d12e24fea..335461d84 100644
--- a/internal/uris/uri.go
+++ b/internal/uris/uri.go
@@ -40,6 +40,7 @@ const (
FollowPath = "follow" // FollowPath used to generate the URI for an individual follow or follow request
UpdatePath = "updates" // UpdatePath is used to generate the URI for an account update
BlocksPath = "blocks" // BlocksPath is used to generate the URI for a block
+ MovesPath = "moves" // MovesPath is used to generate the URI for a move
ReportsPath = "reports" // ReportsPath is used to generate the URI for a report/flag
ConfirmEmailPath = "confirm_email" // ConfirmEmailPath is used to generate the URI for an email confirmation link
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
@@ -108,6 +109,14 @@ func GenerateURIForBlock(username string, thisBlockID string) string {
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, BlocksPath, thisBlockID)
}
+// GenerateURIForMove returns the AP URI for a new Move activity -- something like:
+// https://example.org/users/whatever_user/moves/01F7XTH1QGBAPMGF49WJZ91XGC
+func GenerateURIForMove(username string, thisMoveID string) string {
+ protocol := config.GetProtocol()
+ host := config.GetHost()
+ return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, MovesPath, thisMoveID)
+}
+
// GenerateURIForReport returns the API URI for a new Flag activity -- something like:
// https://example.org/reports/01GP3AWY4CRDVRNZKW0TEAMB5R
//