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:
		@@ -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|
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user