🔖

オープンソースプロジェクトにおける退会処理のパターン

2022/01/23に公開

Railsのプロジェクトから調査しました。購入履歴を消せないなどの理由でUserテーブルに状態をもたせて管理しているようなものをみたかったのですがなかったです。もしそういうものをご存知の方がいればコメント欄で教えていただきたいです

Redmine

コントローラーからは User.destroy を呼び出し、 before_destroyremove_references_before_destroy (redmine/user.rb at master · redmine/redmine)を呼んでいる。 remove_references_before_destroy では、当該ユーザーの関連レコードをすべて anonymousユーザーに紐付けたり、不要なレコードを削除したりしている。

ユーザーは完全に削除しつつ、そのユーザーが作成した投稿などは消えたら困るので1人の匿名ユーザーに紐付けするという方針

  # Removes references that are not handled by associations
  # Things that are not deleted are reassociated with the anonymous user
  def remove_references_before_destroy
    return if self.id.nil?

    substitute = User.anonymous
    Attachment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    Comment.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    Issue.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    Issue.where(['assigned_to_id = ?', id]).update_all('assigned_to_id = NULL')
    Journal.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
    JournalDetail.
      where(["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]).
      update_all(['old_value = ?', substitute.id.to_s])
    JournalDetail.
      where(["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]).
      update_all(['value = ?', substitute.id.to_s])
    Message.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    News.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    # Remove private queries and keep public ones
    ::Query.where('user_id = ? AND visibility = ?', id, ::Query::VISIBILITY_PRIVATE).delete_all
    ::Query.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
    TimeEntry.where(['user_id = ?', id]).update_all(['user_id = ?', substitute.id])
    Token.where('user_id = ?', id).delete_all
    Watcher.where('user_id = ?', id).delete_all
    WikiContent.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    WikiContentVersion.where(['author_id = ?', id]).update_all(['author_id = ?', substitute.id])
    user_custom_field_ids = CustomField.where(field_format: 'user').ids
    if user_custom_field_ids.any?
      CustomValue.where(custom_field_id: user_custom_field_ids, value: self.id.to_s).delete_all
    end
  end

Discourse(掲示板)

discourse/discourse: A platform for community discussion. Free, open, simple.

UserDestroyer(URL)にて処理。

  • 関連テーブルの削除
    • 特定のuser_idをもったログ情報は、記録のためにuser_idを備考欄にコピーしてから、user_idを消すというような処理もあった
  • 掲示板への投稿はシステムユーザーに置き換え
  • ログイン情報の削除の際は、ログイン用のメールアドレスだけは ScreenedEmailというモデルで保存している

ScreenedEmailはBANしたアカウントの管理にも使われているようだった。

  def destroy
    @user = fetch_user_from_params
    guardian.ensure_can_delete_user!(@user)

    UserDestroyer.new(current_user).destroy(@user, delete_posts: true, context: params[:context])

    render json: success_json
  end

ScreenedEmailというテーブルはこのような感じ。BANしたアカウントなどもこちらで管理しているようだ

class AddIpAddressToScreeningTables < ActiveRecord::Migration[4.2]
  def change
    add_column :screened_emails, :ip_address, :inet
    add_column :screened_urls,   :ip_address, :inet
  end
end

Gitlab

gitlabhq/destroy_service.rb at 76365117183054e95bbb2f3a6392ac333628eacf · gitlabhq/gitlabhq

  • global ghost user というものに関連レコードの所有者を置き換える
  • 関連レコードの削除
    • Railsは関連レコードを削除するとき、すべてメモリに読み込んでしまうので、バッチで削除するという工夫をしている

Mastodon

mastodon/delete_account_service.rb at 3a103cd317fd56aca27fca01e03647df44e3ffd2 · mastodon/mastodon

  • 関連レコードの削除
  • アクティビティーを記録
  • アンフォロー

その他にも私は詳しくないのだが、他のMastodonとの関連の処理がいろいろされているようだった。

  def destroy_account!
    current_account.suspend!(origin: :local, block_email: false)
    AccountDeletionWorker.perform_async(current_user.account_id)
    sign_out
  end

Discussion