You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

92 lines
3.8 KiB

  1. class FixAccountsUniqueIndex < ActiveRecord::Migration[5.2]
  2. disable_ddl_transaction!
  3. def up
  4. say ''
  5. say 'WARNING: This migration may take a *long* time for large instances'
  6. say 'It will *not* lock tables for any significant time, but it may run'
  7. say 'for a very long time. We will pause for 10 seconds to allow you to'
  8. say 'interrupt this migration if you are not ready.'
  9. say ''
  10. say 'This migration will irreversibly delete user accounts with duplicate'
  11. say 'usernames. You may use the `rake mastodon:maintenance:find_duplicate_usernames`'
  12. say 'task to manually deal with such accounts before running this migration.'
  13. 10.downto(1) do |i|
  14. say "Continuing in #{i} second#{i == 1 ? '' : 's'}...", true
  15. sleep 1
  16. end
  17. duplicates = Account.connection.select_all('SELECT string_agg(id::text, \',\') AS ids FROM accounts GROUP BY lower(username), lower(domain) HAVING count(*) > 1').to_hash
  18. duplicates.each do |row|
  19. deduplicate_account!(row['ids'].split(','))
  20. end
  21. remove_index :accounts, name: 'index_accounts_on_username_and_domain_lower' if index_name_exists?(:accounts, 'index_accounts_on_username_and_domain_lower')
  22. safety_assured { execute 'CREATE UNIQUE INDEX CONCURRENTLY index_accounts_on_username_and_domain_lower ON accounts (lower(username), lower(domain))' }
  23. remove_index :accounts, name: 'index_accounts_on_username_and_domain' if index_name_exists?(:accounts, 'index_accounts_on_username_and_domain')
  24. end
  25. def down
  26. raise ActiveRecord::IrreversibleMigration
  27. end
  28. private
  29. def deduplicate_account!(account_ids)
  30. accounts = Account.where(id: account_ids).to_a
  31. accounts = accounts.first.local? ? accounts.sort_by(&:created_at) : accounts.sort_by(&:updated_at).reverse
  32. reference_account = accounts.shift
  33. say_with_time "Deduplicating @#{reference_account.acct} (#{accounts.size} duplicates)..." do
  34. accounts.each do |other_account|
  35. if other_account.public_key == reference_account.public_key
  36. # The accounts definitely point to the same resource, so
  37. # it's safe to re-attribute content and relationships
  38. merge_accounts!(reference_account, other_account)
  39. elsif other_account.local?
  40. # Since domain is in the GROUP BY clause, both accounts
  41. # are always either going to be local or not local, so only
  42. # one check is needed. Since we cannot support two users with
  43. # the same username locally, one has to go. 😢
  44. other_account.user&.destroy
  45. end
  46. other_account.destroy
  47. end
  48. end
  49. end
  50. def merge_accounts!(main_account, duplicate_account)
  51. [Status, Mention, StatusPin, StreamEntry].each do |klass|
  52. klass.where(account_id: duplicate_account.id).in_batches.update_all(account_id: main_account.id)
  53. end
  54. # Since it's the same remote resource, the remote resource likely
  55. # already believes we are following/blocking, so it's safe to
  56. # re-attribute the relationships too. However, during the presence
  57. # of the index bug users could have *also* followed the reference
  58. # account already, therefore mass update will not work and we need
  59. # to check for (and skip past) uniqueness errors
  60. [Favourite, Follow, FollowRequest, Block, Mute].each do |klass|
  61. klass.where(account_id: duplicate_account.id).find_each do |record|
  62. begin
  63. record.update_attribute(:account_id, main_account.id)
  64. rescue ActiveRecord::RecordNotUnique
  65. next
  66. end
  67. end
  68. end
  69. [Follow, FollowRequest, Block, Mute].each do |klass|
  70. klass.where(target_account_id: duplicate_account.id).find_each do |record|
  71. begin
  72. record.update_attribute(:target_account_id, main_account.id)
  73. rescue ActiveRecord::RecordNotUnique
  74. next
  75. end
  76. end
  77. end
  78. end
  79. end