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
							
								
									a2da02626e
								
							
						
					
					
						commit
						f844386809
					
				| @ -67,6 +67,7 @@ class Account < ApplicationRecord | |||||||
|   include Paginable |   include Paginable | ||||||
|   include AccountCounters |   include AccountCounters | ||||||
|   include DomainNormalizable |   include DomainNormalizable | ||||||
|  |   include AccountMerging | ||||||
| 
 | 
 | ||||||
|   TRUST_LEVELS = { |   TRUST_LEVELS = { | ||||||
|     untrusted: 0, |     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) |       say('OK', :green) | ||||||
|     end |     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' |     desc 'backup USERNAME', 'Request a backup for a user' | ||||||
|     long_desc <<-LONG_DESC |     long_desc <<-LONG_DESC | ||||||
|       Request a new backup for an account with a given USERNAME. |       Request a new backup for an account with a given USERNAME. | ||||||
| @ -335,7 +375,8 @@ module Mastodon | |||||||
|     option :verbose, type: :boolean, aliases: [:v] |     option :verbose, type: :boolean, aliases: [:v] | ||||||
|     desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT' |     desc 'unfollow ACCT', 'Make all local accounts unfollow account specified by ACCT' | ||||||
|     def unfollow(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? |       if target_account.nil? | ||||||
|         say('No such account', :red) |         say('No such account', :red) | ||||||
|  | |||||||
| @ -476,48 +476,13 @@ module Mastodon | |||||||
|         if other_account.public_key == reference_account.public_key |         if other_account.public_key == reference_account.public_key | ||||||
|           # The accounts definitely point to the same resource, so |           # The accounts definitely point to the same resource, so | ||||||
|           # it's safe to re-attribute content and relationships |           # it's safe to re-attribute content and relationships | ||||||
|           merge_accounts!(reference_account, other_account) |           reference_account.merge_with!(other_account) | ||||||
|         end |         end | ||||||
| 
 | 
 | ||||||
|         other_account.destroy |         other_account.destroy | ||||||
|       end |       end | ||||||
|     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) |     def merge_conversations!(main_conv, duplicate_conv) | ||||||
|       owned_classes = [ConversationMute, AccountConversation] |       owned_classes = [ConversationMute, AccountConversation] | ||||||
|       owned_classes.each do |klass| |       owned_classes.each do |klass| | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user