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>gh/stable
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,
|
||||||
|
|
|
@ -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