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.

105 lines
4.1 KiB

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