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.

306 lines
8.8 KiB

  1. # frozen_string_literal: true
  2. class DeleteAccountService < BaseService
  3. include Payloadable
  4. ASSOCIATIONS_ON_SUSPEND = %w(
  5. account_pins
  6. active_relationships
  7. aliases
  8. block_relationships
  9. blocked_by_relationships
  10. conversation_mutes
  11. conversations
  12. custom_filters
  13. devices
  14. domain_blocks
  15. featured_tags
  16. follow_requests
  17. identity_proofs
  18. list_accounts
  19. migrations
  20. mute_relationships
  21. muted_by_relationships
  22. notifications
  23. owned_lists
  24. passive_relationships
  25. report_notes
  26. scheduled_statuses
  27. status_pins
  28. ).freeze
  29. # The following associations have no important side-effects
  30. # in callbacks and all of their own associations are secured
  31. # by foreign keys, making them safe to delete without loading
  32. # into memory
  33. ASSOCIATIONS_WITHOUT_SIDE_EFFECTS = %w(
  34. account_pins
  35. aliases
  36. conversation_mutes
  37. conversations
  38. custom_filters
  39. devices
  40. domain_blocks
  41. featured_tags
  42. follow_requests
  43. identity_proofs
  44. list_accounts
  45. migrations
  46. mute_relationships
  47. muted_by_relationships
  48. notifications
  49. owned_lists
  50. scheduled_statuses
  51. status_pins
  52. )
  53. ASSOCIATIONS_ON_DESTROY = %w(
  54. reports
  55. targeted_moderation_notes
  56. targeted_reports
  57. ).freeze
  58. # Suspend or remove an account and remove as much of its data
  59. # as possible. If it's a local account and it has not been confirmed
  60. # or never been approved, then side effects are skipped and both
  61. # the user and account records are removed fully. Otherwise,
  62. # it is controlled by options.
  63. # @param [Account]
  64. # @param [Hash] options
  65. # @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
  66. # @option [Boolean] :reserve_username Keep account record
  67. # @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
  68. # @option [Boolean] :skip_activitypub Skip sending ActivityPub payloads. Implied by :skip_side_effects
  69. # @option [Time] :suspended_at Only applicable when :reserve_username is true
  70. def call(account, **options)
  71. @account = account
  72. @options = { reserve_username: true, reserve_email: true }.merge(options)
  73. if @account.local? && @account.user_unconfirmed_or_pending?
  74. @options[:reserve_email] = false
  75. @options[:reserve_username] = false
  76. @options[:skip_side_effects] = true
  77. end
  78. @options[:skip_activitypub] = true if @options[:skip_side_effects]
  79. distribute_activities!
  80. purge_content!
  81. fulfill_deletion_request!
  82. end
  83. private
  84. def distribute_activities!
  85. return if skip_activitypub?
  86. if @account.local?
  87. delete_actor!
  88. elsif @account.activitypub?
  89. reject_follows!
  90. undo_follows!
  91. end
  92. end
  93. def reject_follows!
  94. # When deleting a remote account, the account obviously doesn't
  95. # actually become deleted on its origin server, i.e. unlike a
  96. # locally deleted account it continues to have access to its home
  97. # feed and other content. To prevent it from being able to continue
  98. # to access toots it would receive because it follows local accounts,
  99. # we have to force it to unfollow them.
  100. ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
  101. [Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer)), follow.target_account_id, @account.inbox_url]
  102. end
  103. end
  104. def undo_follows!
  105. # When deleting a remote account, the account obviously doesn't
  106. # actually become deleted on its origin server, but following relationships
  107. # are severed on our end. Therefore, make the remote server aware that the
  108. # follow relationships are severed to avoid confusion and potential issues
  109. # if the remote account gets un-suspended.
  110. ActivityPub::DeliveryWorker.push_bulk(Follow.where(target_account: @account)) do |follow|
  111. [Oj.dump(serialize_payload(follow, ActivityPub::UndoFollowSerializer)), follow.account_id, @account.inbox_url]
  112. end
  113. end
  114. def purge_user!
  115. return if !@account.local? || @account.user.nil?
  116. if keep_user_record?
  117. @account.user.disable!
  118. @account.user.invites.where(uses: 0).destroy_all
  119. else
  120. @account.user.destroy
  121. end
  122. end
  123. def purge_content!
  124. purge_user!
  125. purge_profile!
  126. purge_statuses!
  127. purge_mentions!
  128. purge_media_attachments!
  129. purge_polls!
  130. purge_generated_notifications!
  131. purge_favourites!
  132. purge_bookmarks!
  133. purge_feeds!
  134. purge_other_associations!
  135. @account.destroy unless keep_account_record?
  136. end
  137. def purge_statuses!
  138. @account.statuses.reorder(nil).where.not(id: reported_status_ids).in_batches do |statuses|
  139. BatchedRemoveStatusService.new.call(statuses, skip_side_effects: skip_side_effects?)
  140. end
  141. end
  142. def purge_mentions!
  143. @account.mentions.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
  144. end
  145. def purge_media_attachments!
  146. @account.media_attachments.reorder(nil).find_each do |media_attachment|
  147. next if keep_account_record? && reported_status_ids.include?(media_attachment.status_id)
  148. media_attachment.destroy
  149. end
  150. end
  151. def purge_polls!
  152. @account.polls.reorder(nil).where.not(status_id: reported_status_ids).in_batches.delete_all
  153. end
  154. def purge_generated_notifications!
  155. # By deleting polls and statuses without callbacks, we've left behind
  156. # polymorphically associated notifications generated by this account
  157. Notification.where(from_account: @account).in_batches.delete_all
  158. end
  159. def purge_favourites!
  160. @account.favourites.in_batches do |favourites|
  161. ids = favourites.pluck(:status_id)
  162. StatusStat.where(status_id: ids).update_all('favourites_count = GREATEST(0, favourites_count - 1)')
  163. Chewy.strategy.current.update(StatusesIndex::Status, ids) if Chewy.enabled?
  164. Rails.cache.delete_multi(ids.map { |id| "statuses/#{id}" })
  165. favourites.delete_all
  166. end
  167. end
  168. def purge_bookmarks!
  169. @account.bookmarks.in_batches do |bookmarks|
  170. Chewy.strategy.current.update(StatusesIndex::Status, bookmarks.pluck(:status_id)) if Chewy.enabled?
  171. bookmarks.delete_all
  172. end
  173. end
  174. def purge_other_associations!
  175. associations_for_destruction.each do |association_name|
  176. purge_association(association_name)
  177. end
  178. end
  179. def purge_feeds!
  180. return unless @account.local?
  181. FeedManager.instance.clean_feeds!(:home, [@account.id])
  182. FeedManager.instance.clean_feeds!(:list, @account.owned_lists.pluck(:id))
  183. end
  184. def purge_profile!
  185. # If the account is going to be destroyed
  186. # there is no point wasting time updating
  187. # its values first
  188. return unless keep_account_record?
  189. @account.silenced_at = nil
  190. @account.suspended_at = @options[:suspended_at] || Time.now.utc
  191. @account.suspension_origin = :local
  192. @account.locked = false
  193. @account.memorial = false
  194. @account.discoverable = false
  195. @account.display_name = ''
  196. @account.note = ''
  197. @account.fields = []
  198. @account.statuses_count = 0
  199. @account.followers_count = 0
  200. @account.following_count = 0
  201. @account.moved_to_account = nil
  202. @account.also_known_as = []
  203. @account.trust_level = :untrusted
  204. @account.avatar.destroy
  205. @account.header.destroy
  206. @account.save!
  207. end
  208. def fulfill_deletion_request!
  209. @account.deletion_request&.destroy
  210. end
  211. def purge_association(association_name)
  212. association = @account.public_send(association_name)
  213. if ASSOCIATIONS_WITHOUT_SIDE_EFFECTS.include?(association_name)
  214. association.in_batches.delete_all
  215. else
  216. association.in_batches.destroy_all
  217. end
  218. end
  219. def delete_actor!
  220. ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
  221. [delete_actor_json, @account.id, inbox_url]
  222. end
  223. ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
  224. [delete_actor_json, @account.id, inbox_url]
  225. end
  226. end
  227. def delete_actor_json
  228. @delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
  229. end
  230. def delivery_inboxes
  231. @delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
  232. end
  233. def low_priority_delivery_inboxes
  234. Account.inboxes - delivery_inboxes
  235. end
  236. def reported_status_ids
  237. @reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
  238. end
  239. def associations_for_destruction
  240. if keep_account_record?
  241. ASSOCIATIONS_ON_SUSPEND
  242. else
  243. ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
  244. end
  245. end
  246. def keep_user_record?
  247. @options[:reserve_email]
  248. end
  249. def keep_account_record?
  250. @options[:reserve_username]
  251. end
  252. def skip_side_effects?
  253. @options[:skip_side_effects]
  254. end
  255. def skip_activitypub?
  256. @options[:skip_activitypub]
  257. end
  258. end