2023-07-12 00:47:08 -07:00
# frozen_string_literal: true
2023-03-25 16:39:24 -07:00
require_relative '../../lib/mastodon/migration_warning'
2018-05-29 17:51:26 -07:00
class FixAccountsUniqueIndex < ActiveRecord :: Migration [ 5 . 2 ]
2023-03-25 16:39:24 -07:00
include Mastodon :: MigrationWarning
2018-08-11 09:00:41 -07:00
class Account < ApplicationRecord
# Dummy class, to make migration possible across version changes
has_one :user , inverse_of : :account
def local?
domain . nil?
end
2018-08-15 11:23:12 -07:00
def acct
local? ? username : " #{ username } @ #{ domain } "
end
2018-08-11 09:00:41 -07:00
end
2019-07-10 08:09:10 -07:00
class StreamEntry < ApplicationRecord
# Dummy class, to make migration possible across version changes
belongs_to :account , inverse_of : :stream_entries
end
2022-01-27 09:13:41 -08:00
class Status < ApplicationRecord
# Dummy class, to make migration possible across version changes
belongs_to :account
end
class Mention < ApplicationRecord
# Dummy class, to make migration possible across version changes
belongs_to :account
end
class StatusPin < ApplicationRecord
# Dummy class, to make migration possible across version changes
belongs_to :account
end
2018-05-29 17:51:26 -07:00
disable_ddl_transaction!
def up
2023-03-25 16:39:24 -07:00
migration_duration_warning ( << ~ EXPLANATION )
This migration will irreversibly delete user accounts with duplicate
usernames . You may use the ` rake mastodon:maintenance:find_duplicate_usernames `
task to manually deal with such accounts before running this migration .
EXPLANATION
2018-05-29 17:51:26 -07:00
2021-03-18 18:45:34 -07:00
duplicates = Account . connection . select_all ( 'SELECT string_agg(id::text, \',\') AS ids FROM accounts GROUP BY lower(username), lower(domain) HAVING count(*) > 1' ) . to_ary
2018-05-29 17:51:26 -07:00
duplicates . each do | row |
deduplicate_account! ( row [ 'ids' ] . split ( ',' ) )
end
remove_index :accounts , name : 'index_accounts_on_username_and_domain_lower' if index_name_exists? ( :accounts , 'index_accounts_on_username_and_domain_lower' )
safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_accounts_on_username_and_domain_lower ON accounts (lower(username), lower(domain))' }
remove_index :accounts , name : 'index_accounts_on_username_and_domain' if index_name_exists? ( :accounts , 'index_accounts_on_username_and_domain' )
end
def down
raise ActiveRecord :: IrreversibleMigration
end
private
def deduplicate_account! ( account_ids )
accounts = Account . where ( id : account_ids ) . to_a
2018-05-30 00:39:52 -07:00
accounts = accounts . first . local? ? accounts . sort_by ( & :created_at ) : accounts . sort_by ( & :updated_at ) . reverse
2018-05-29 17:51:26 -07:00
reference_account = accounts . shift
2018-05-31 08:09:09 -07:00
say_with_time " Deduplicating @ #{ reference_account . acct } ( #{ accounts . size } duplicates)... " do
accounts . each do | other_account |
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 )
elsif other_account . local?
# Since domain is in the GROUP BY clause, both accounts
# are always either going to be local or not local, so only
# one check is needed. Since we cannot support two users with
# the same username locally, one has to go. 😢
other_account . user & . destroy
end
2018-05-29 17:51:26 -07:00
2018-05-31 08:09:09 -07:00
other_account . destroy
end
2018-05-29 17:51:26 -07:00
end
end
def merge_accounts! ( main_account , duplicate_account )
2018-05-31 08:09:09 -07:00
[ Status , Mention , StatusPin , StreamEntry ] . each do | klass |
klass . where ( account_id : duplicate_account . id ) . in_batches . update_all ( account_id : main_account . id )
2018-05-29 17:51:26 -07:00
end
# 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
2018-05-31 08:09:09 -07:00
[ Favourite , Follow , FollowRequest , Block , Mute ] . each do | klass |
2018-05-29 17:51:26 -07:00
klass . where ( account_id : duplicate_account . id ) . find_each do | record |
2023-02-18 14:09:40 -08:00
record . update_attribute ( :account_id , main_account . id )
rescue ActiveRecord :: RecordNotUnique
next
2018-05-29 17:51:26 -07:00
end
2018-05-31 08:22:33 -07:00
end
2018-05-29 17:51:26 -07:00
2018-05-31 08:22:33 -07:00
[ Follow , FollowRequest , Block , Mute ] . each do | klass |
2018-05-29 17:51:26 -07:00
klass . where ( target_account_id : duplicate_account . id ) . find_each do | record |
2023-02-18 14:09:40 -08:00
record . update_attribute ( :target_account_id , main_account . id )
rescue ActiveRecord :: RecordNotUnique
next
2018-05-29 17:51:26 -07:00
end
end
end
end