Add tootctl accounts merge (#15201)
				
					
				
			* Add `tootctl accounts merge` * Update lib/mastodon/accounts_cli.rb Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh> Co-authored-by: Yamagishi Kazutoshi <ykzts@desire.sh>
This commit is contained in:
		
							parent
							
								
									144e606ec9
								
							
						
					
					
						commit
						876df5bb18
					
				
					 4 changed files with 87 additions and 37 deletions
				
			
		|  | @ -67,6 +67,7 @@ class Account < ApplicationRecord | |||
|   include Paginable | ||||
|   include AccountCounters | ||||
|   include DomainNormalizable | ||||
|   include AccountMerging | ||||
| 
 | ||||
|   TRUST_LEVELS = { | ||||
|     untrusted: 0, | ||||
|  |  | |||
							
								
								
									
										43
									
								
								app/models/concerns/account_merging.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/models/concerns/account_merging.rb
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,43 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| module AccountMerging | ||||
|   extend ActiveSupport::Concern | ||||
| 
 | ||||
|   def merge_with!(other_account) | ||||
|     # Since it's the same remote resource, the remote resource likely | ||||
|     # already believes we are following/blocking, so it's safe to | ||||
|     # re-attribute the relationships too. However, during the presence | ||||
|     # of the index bug users could have *also* followed the reference | ||||
|     # account already, therefore mass update will not work and we need | ||||
|     # to check for (and skip past) uniqueness errors | ||||
| 
 | ||||
|     owned_classes = [ | ||||
|       Status, StatusPin, MediaAttachment, Poll, Report, Tombstone, Favourite, | ||||
|       Follow, FollowRequest, Block, Mute, AccountIdentityProof, | ||||
|       AccountModerationNote, AccountPin, AccountStat, ListAccount, | ||||
|       PollVote, Mention | ||||
|     ] | ||||
| 
 | ||||
|     owned_classes.each do |klass| | ||||
|       klass.where(account_id: other_account.id).find_each do |record| | ||||
|         begin | ||||
|           record.update_attribute(:account_id, id) | ||||
|         rescue ActiveRecord::RecordNotUnique | ||||
|           next | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     target_classes = [Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin] | ||||
| 
 | ||||
|     target_classes.each do |klass| | ||||
|       klass.where(target_account_id: other_account.id).find_each do |record| | ||||
|         begin | ||||
|           record.update_attribute(:target_account_id, id) | ||||
|         rescue ActiveRecord::RecordNotUnique | ||||
|           next | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -196,6 +196,46 @@ module Mastodon | |||
|       say('OK', :green) | ||||
|     end | ||||
| 
 | ||||
|     option :force, type: :boolean, aliases: [:f], description: 'Override public key check' | ||||
|     desc 'merge FROM TO', 'Merge two remote accounts into one' | ||||
|     long_desc <<-LONG_DESC | ||||
|       Merge two remote accounts specified by their username@domain | ||||
|       into one, whereby the TO account is the one being merged into | ||||
|       and kept, while the FROM one is removed. It is primarily meant | ||||
|       to fix duplicates caused by other servers changing their domain. | ||||
| 
 | ||||
|       The command by default only works if both accounts have the same | ||||
|       public key to prevent mistakes. To override this, use the --force. | ||||
|     LONG_DESC | ||||
|     def merge(from_acct, to_acct) | ||||
|       username, domain = from_acct.split('@') | ||||
|       from_account = Account.find_remote(username, domain) | ||||
| 
 | ||||
|       if from_account.nil? || from_account.local? | ||||
|         say("No such account (#{from_acct})", :red) | ||||
|         exit(1) | ||||
|       end | ||||
| 
 | ||||
|       username, domain = to_acct.split('@') | ||||
|       to_account = Account.find_remote(username, domain) | ||||
| 
 | ||||
|       if to_account.nil? || to_account.local? | ||||
|         say("No such account (#{to_acct})", :red) | ||||
|         exit(1) | ||||
|       end | ||||
| 
 | ||||
|       if from_account.public_key != to_account.public_key && !options[:force] | ||||
|         say("Accounts don't have the same public key, might not be duplicates!", :red) | ||||
|         say('Override with --force', :red) | ||||
|         exit(1) | ||||
|       end | ||||
| 
 | ||||
|       to_account.merge_with!(from_account) | ||||
|       from_account.destroy | ||||
| 
 | ||||
|       say('OK', :green) | ||||
|     end | ||||
| 
 | ||||
|     desc 'backup USERNAME', 'Request a backup for a user' | ||||
|     long_desc <<-LONG_DESC | ||||
|       Request a new backup for an account with a given USERNAME. | ||||
|  | @ -335,7 +375,8 @@ module Mastodon | |||
|     option :verbose, type: :boolean, aliases: [:v] | ||||
|     desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT' | ||||
|     def unfollow(acct) | ||||
|       target_account = Account.find_remote(*acct.split('@')) | ||||
|       username, domain = acct.split('@') | ||||
|       target_account = Account.find_remote(username, domain) | ||||
| 
 | ||||
|       if target_account.nil? | ||||
|         say('No such account', :red) | ||||
|  |  | |||
|  | @ -476,48 +476,13 @@ module Mastodon | |||
|         if other_account.public_key == reference_account.public_key | ||||
|           # The accounts definitely point to the same resource, so | ||||
|           # it's safe to re-attribute content and relationships | ||||
|           merge_accounts!(reference_account, other_account) | ||||
|           reference_account.merge_with!(other_account) | ||||
|         end | ||||
| 
 | ||||
|         other_account.destroy | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def merge_accounts!(main_account, duplicate_account) | ||||
|       # Since it's the same remote resource, the remote resource likely | ||||
|       # already believes we are following/blocking, so it's safe to | ||||
|       # re-attribute the relationships too. However, during the presence | ||||
|       # of the index bug users could have *also* followed the reference | ||||
|       # account already, therefore mass update will not work and we need | ||||
|       # to check for (and skip past) uniqueness errors | ||||
|       owned_classes = [ | ||||
|         Status, StatusPin, MediaAttachment, Poll, Report, Tombstone, Favourite, | ||||
|         Follow, FollowRequest, Block, Mute, AccountIdentityProof, | ||||
|         AccountModerationNote, AccountPin, AccountStat, ListAccount, | ||||
|         PollVote, Mention | ||||
|       ] | ||||
|       owned_classes.each do |klass| | ||||
|         klass.where(account_id: duplicate_account.id).find_each do |record| | ||||
|           begin | ||||
|             record.update_attribute(:account_id, main_account.id) | ||||
|           rescue ActiveRecord::RecordNotUnique | ||||
|             next | ||||
|           end | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       target_classes = [Follow, FollowRequest, Block, Mute, AccountModerationNote, AccountPin] | ||||
|       target_classes.each do |klass| | ||||
|         klass.where(target_account_id: duplicate_account.id).find_each do |record| | ||||
|           begin | ||||
|             record.update_attribute(:target_account_id, main_account.id) | ||||
|           rescue ActiveRecord::RecordNotUnique | ||||
|             next | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     def merge_conversations!(main_conv, duplicate_conv) | ||||
|       owned_classes = [ConversationMute, AccountConversation] | ||||
|       owned_classes.each do |klass| | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue