diff options
Diffstat (limited to 'internal')
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 = >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 <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 // |