// 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 . 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 }