226 lines
6.5 KiB
Ruby
226 lines
6.5 KiB
Ruby
# 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, uniqueness: true
|
|
|
|
after_create :lock_old_user!
|
|
|
|
attr_accessor :old_private_key
|
|
|
|
def receive(*)
|
|
perform!
|
|
end
|
|
|
|
def public?
|
|
true
|
|
end
|
|
|
|
def sender
|
|
@sender ||= old_user || ephemeral_sender
|
|
end
|
|
|
|
# executes a migration plan according to this AccountMigration object
|
|
def perform!
|
|
raise "already performed" if performed?
|
|
validate_sender if locally_initiated?
|
|
|
|
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
|
|
|
|
dispatch if locally_initiated?
|
|
dispatch_contacts if remotely_initiated?
|
|
update(completed_at: Time.zone.now)
|
|
end
|
|
|
|
def performed?
|
|
!completed_at.nil?
|
|
end
|
|
|
|
# We assume that migration message subscribers are people that are subscribed to a new user profile updates.
|
|
# Since during the migration we update contact references, this includes all the contacts of the old person.
|
|
# 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?
|
|
end
|
|
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 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
|
|
|
|
# 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
|
|
new_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 ephemeral_sender
|
|
raise "can't build sender without old private key defined" if old_private_key.nil?
|
|
EphemeralUser.new(old_person.diaspora_handle, old_private_key)
|
|
end
|
|
|
|
def validate_sender
|
|
sender # sender method raises exception when sender can't be instantiated
|
|
end
|
|
|
|
def update_all_references
|
|
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].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=#{new_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=#{new_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=#{new_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=#{new_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=#{new_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=#{new_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=#{new_user.id})")
|
|
.destroy_all
|
|
end
|
|
|
|
def update_person_references
|
|
logger.debug "Updating references from person id=#{old_person.id} to person id=#{new_person.id}"
|
|
eliminate_person_duplicates
|
|
update_references(person_references, old_person, new_person.id)
|
|
end
|
|
|
|
def update_user_references
|
|
logger.debug "Updating references from user id=#{old_user.id} to user id=#{new_user.id}"
|
|
eliminate_user_duplicates
|
|
update_references(user_references, old_user, new_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
|