diff options
author | 2024-03-13 13:53:29 +0100 | |
---|---|---|
committer | 2024-03-13 13:53:29 +0100 | |
commit | ab2d063fcb04f241a3147c843a021491f5fc0a55 (patch) | |
tree | 3d2eff864e8b19d4d9a24f4f1fe92feda8ee4dac /internal/processing/workers/util.go | |
parent | [bugfix]: Add missing Link headers in Swagger spec (#2751) (diff) | |
download | gotosocial-ab2d063fcb04f241a3147c843a021491f5fc0a55.tar.xz |
[feature] Process outgoing Move from clientAPI (#2750)
* prevent moved accounts from taking create-type actions
* update move logic
* federate move out
* indicate on web profile when an account has moved
* [docs] Add migration docs section
* lock while checking + setting move state
* use redirectFollowers func for clientAPI as well
* comment typo
* linter? i barely know 'er!
* Update internal/uris/uri.go
Co-authored-by: Daenney <daenney@users.noreply.github.com>
* add a couple tests for move
* fix little mistake exposed by tests (thanks tests)
* ensure Move marked as successful
* attach shared util funcs to struct
* lock whole account when doing move
* move moving check to after error check
* replace repeated text with error func
* linterrrrrr!!!!
* catch self follow case
---------
Co-authored-by: Daenney <daenney@users.noreply.github.com>
Diffstat (limited to 'internal/processing/workers/util.go')
-rw-r--r-- | internal/processing/workers/util.go | 240 |
1 files changed, 240 insertions, 0 deletions
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 +} |