about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/models/account.rb1
-rw-r--r--app/models/concerns/account_merging.rb43
-rw-r--r--lib/mastodon/accounts_cli.rb43
-rw-r--r--lib/mastodon/maintenance_cli.rb37
4 files changed, 87 insertions, 37 deletions
diff --git a/app/models/account.rb b/app/models/account.rb
index bc9bcc72d..f794d8a29 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -67,6 +67,7 @@ class Account < ApplicationRecord
   include Paginable
   include AccountCounters
   include DomainNormalizable
+  include AccountMerging
 
   TRUST_LEVELS = {
     untrusted: 0,
diff --git a/app/models/concerns/account_merging.rb b/app/models/concerns/account_merging.rb
new file mode 100644
index 000000000..691d02e03
--- /dev/null
+++ b/app/models/concerns/account_merging.rb
@@ -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
diff --git a/lib/mastodon/accounts_cli.rb b/lib/mastodon/accounts_cli.rb
index 7565620cf..bef4093a8 100644
--- a/lib/mastodon/accounts_cli.rb
+++ b/lib/mastodon/accounts_cli.rb
@@ -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)
diff --git a/lib/mastodon/maintenance_cli.rb b/lib/mastodon/maintenance_cli.rb
index 191a3b03f..547238ec6 100644
--- a/lib/mastodon/maintenance_cli.rb
+++ b/lib/mastodon/maintenance_cli.rb
@@ -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|