mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 10:42:24 -05:00 
			
		
		
		
	
		
			
	
	
		
			241 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			241 lines
		
	
	
	
		
			7.4 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
|  | // 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 | ||
|  | } |