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:
Eugen Rochko 2020-11-23 17:50:16 +01:00 committed by GitHub
parent a2da02626e
commit f844386809
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 87 additions and 37 deletions

View File

@ -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,

View 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

View File

@ -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)

View File

@ -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|