# frozen_string_literal: true class AccountMigration < ApplicationRecord include Diaspora::Federated::Base belongs_to :old_person, class_name: "Person" belongs_to :new_person, class_name: "Person" validates :old_person, uniqueness: true validates :new_person, presence: true after_create :lock_old_user! attr_accessor :old_private_key attr_writer :old_person_diaspora_id attr_accessor :archive_contacts def receive(*) perform! end def public? true end def sender @sender ||= old_user || ephemeral_sender end def perform! raise "already performed" if performed? validate_sender if locally_initiated? tombstone_old_user_and_update_all_references if old_person dispatch if locally_initiated? dispatch_contacts update(completed_at: Time.zone.now) end def performed? !completed_at.nil? end # Send migration to all imported contacts, but also send it to all contacts from the archive which weren't imported, # but maybe share with the old account, so they can update contact information and resend the contact message. # In case when a user migrated to our pod from a remote one, we include remote person to subscribers so that # the new pod is informed about the migration as well. def subscribers new_user.profile.subscribers.remote.to_a.tap do |subscribers| subscribers.push(old_person) if old_person&.remote? archive_contacts&.each do |contact| diaspora_id = contact.fetch("account_id") next if subscribers.any? {|s| s.diaspora_handle == diaspora_id } person = Person.by_account_identifier(diaspora_id) subscribers.push(person) if person&.remote? end end end # This method finds the newest user person profile in the migration chain. # If person migrated multiple times then #new_person may point to a closed account. # In this case in order to find open account we have to delegate new_person call to the next account_migration # instance in the chain. def newest_person return new_person if new_person.account_migration.nil? new_person.account_migration.newest_person end private # Normally pod initiates migration locally when the new user is local. Then the pod creates AccountMigration object # itself. If new user is remote, then AccountMigration object is normally received via the federation and this is # remote initiation then. def remotely_initiated? new_person.remote? end def locally_initiated? !remotely_initiated? end def old_user old_person&.owner end def new_user new_person.owner end def newest_user newest_person.owner end def lock_old_user! old_user&.lock_access! end def user_left_our_pod? old_user && !new_user end def user_changed_id_locally? old_user && new_user end def includes_photo_migration? remote_photo_path.present? end def tombstone_old_user_and_update_all_references ActiveRecord::Base.transaction do account_deleter.tombstone_person_and_profile account_deleter.close_user if user_left_our_pod? account_deleter.tombstone_user if user_changed_id_locally? update_all_references end end # We need to resend contacts of users of our pod for the remote new person so that the remote pod received this # contact information from the authoritative source. def dispatch_contacts newest_person.contacts.sharing.each do |contact| Diaspora::Federation::Dispatcher.defer_dispatch(contact.user, contact) end end def dispatch Diaspora::Federation::Dispatcher.build(sender, self).dispatch end EphemeralUser = Struct.new(:diaspora_handle, :serialized_private_key) do def id diaspora_handle end def encryption_key OpenSSL::PKey::RSA.new(serialized_private_key) end end def old_person_diaspora_id old_person&.diaspora_handle || @old_person_diaspora_id end def ephemeral_sender if old_private_key.nil? || old_person_diaspora_id.nil? raise "can't build sender without old private key and diaspora ID defined" end EphemeralUser.new(old_person_diaspora_id, old_private_key) end def validate_sender sender # sender method raises exception when sender can't be instantiated end def update_all_references update_remote_photo_path if remotely_initiated? && includes_photo_migration? update_person_references update_user_references if user_changed_id_locally? end def person_references references = Person.reflections.reject {|key, _| %w[profile owner notifications pod account_deletion account_migration].include?(key) } references.map {|key, value| {value.foreign_key => key} } end def user_references references = User.reflections.reject {|key, _| %w[ person profile auto_follow_back_aspect invited_by aspect_memberships contact_people followed_tags ignored_people conversation_visibilities pairwise_pseudonymous_identifiers conversations o_auth_applications ].include?(key) } references.map {|key, value| {value.foreign_key => key} } end def eliminate_person_duplicates duplicate_person_contacts.destroy_all duplicate_person_likes.destroy_all duplicate_person_participations.destroy_all duplicate_person_poll_participations.destroy_all end def duplicate_person_contacts Contact .joins("INNER JOIN contacts as c2 ON (contacts.user_id = c2.user_id AND contacts.person_id=#{old_person.id} AND"\ " c2.person_id=#{newest_person.id})") end def duplicate_person_likes Like .joins("INNER JOIN likes as l2 ON (likes.target_id = l2.target_id "\ "AND likes.target_type = l2.target_type "\ "AND likes.author_id=#{old_person.id} AND"\ " l2.author_id=#{newest_person.id})") end def duplicate_person_participations Participation .joins("INNER JOIN participations as p2 ON (participations.target_id = p2.target_id "\ "AND participations.target_type = p2.target_type "\ "AND participations.author_id=#{old_person.id} AND"\ " p2.author_id=#{newest_person.id})") end def duplicate_person_poll_participations PollParticipation .joins("INNER JOIN poll_participations as p2 ON (poll_participations.poll_id = p2.poll_id "\ "AND poll_participations.author_id=#{old_person.id} AND"\ " p2.author_id=#{newest_person.id})") end def eliminate_user_duplicates Aspect .joins("INNER JOIN aspects as a2 ON (aspects.name = a2.name AND aspects.user_id=#{old_user.id} AND a2.user_id=#{newest_user.id})") .destroy_all Contact .joins("INNER JOIN contacts as c2 ON (contacts.person_id = c2.person_id AND contacts.user_id=#{old_user.id} AND"\ " c2.user_id=#{newest_user.id})") .destroy_all TagFollowing .joins("INNER JOIN tag_followings as t2 ON (tag_followings.tag_id = t2.tag_id AND"\ " tag_followings.user_id=#{old_user.id} AND t2.user_id=#{newest_user.id})") .destroy_all end def update_remote_photo_path Photo.where(author: old_person) .update_all(remote_photo_path: remote_photo_path) # rubocop:disable Rails/SkipsModelValidations return unless user_left_our_pod? Photo.where(author: old_person).find_in_batches do |batch| batch.each do |photo| photo.processed_image = nil photo.unprocessed_image = nil logger.warn "Error cleaning up photo #{photo.id}" unless photo.save end end end def update_person_references logger.debug "Updating references from person id=#{old_person.id} to person id=#{newest_person.id}" eliminate_person_duplicates update_references(person_references, old_person, newest_person.id) end def update_user_references logger.debug "Updating references from user id=#{old_user.id} to user id=#{newest_user.id}" eliminate_user_duplicates update_references(user_references, old_user, newest_user.id) end def update_references(references, object, new_id) references.each do |pair| key_id = pair.flatten[0] association = pair.flatten[1] object.send(association).update_all(key_id => new_id) end end def account_deleter @account_deleter ||= AccountDeleter.new(old_person) end end