From f844386809e894591dce3f52ace4beba0c977a8b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 23 Nov 2020 17:50:16 +0100 Subject: [PATCH] Add `tootctl accounts merge` (#15201) * Add `tootctl accounts merge` * Update lib/mastodon/accounts_cli.rb Co-authored-by: Yamagishi Kazutoshi Co-authored-by: Yamagishi Kazutoshi --- app/models/account.rb | 1 + app/models/concerns/account_merging.rb | 43 ++++++++++++++++++++++++++ lib/mastodon/accounts_cli.rb | 43 +++++++++++++++++++++++++- lib/mastodon/maintenance_cli.rb | 37 +--------------------- 4 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 app/models/concerns/account_merging.rb 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|