Welcome to mirror list, hosted at ThFree Co, Russian Federation.

account_migration.rb « models « app - github.com/diaspora/diaspora.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: b971b881b701791f8fc9a51537ce0e5619068585 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# 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