mirror of https://github.com/mastodon/mastodon
Add `deleted_at` attribute for deleted-but-not-suspended accounts
This commit is contained in:
parent
96fb6e491f
commit
f1cd809f72
|
@ -13,4 +13,8 @@ class ActivityPub::BaseController < Api::BaseController
|
|||
def skip_temporary_suspension_response?
|
||||
false
|
||||
end
|
||||
|
||||
def skip_pending_deletion_response?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -35,6 +35,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
true
|
||||
end
|
||||
|
||||
def skip_pending_deletion_response?
|
||||
true
|
||||
end
|
||||
|
||||
def body
|
||||
return @body if defined?(@body)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
|||
before_action :require_user!
|
||||
|
||||
def index
|
||||
@accounts = Account.where(id: account_ids).select(:id, :domain)
|
||||
@accounts = Account.without_deleted.where(id: account_ids).select(:id, :domain)
|
||||
@accounts.merge!(Account.without_suspended) unless truthy_param?(:with_suspended)
|
||||
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
|
|
@ -36,7 +36,7 @@ module AccountOwnedConcern
|
|||
def check_account_suspension
|
||||
if @account.permanently_unavailable?
|
||||
permanent_unavailability_response
|
||||
elsif @account.suspended? && !skip_temporary_suspension_response?
|
||||
elsif (@account.suspended? && !skip_temporary_suspension_response?) || (@account.deleted? && !skip_pending_deletion_response?)
|
||||
temporary_suspension_response
|
||||
end
|
||||
end
|
||||
|
@ -45,6 +45,10 @@ module AccountOwnedConcern
|
|||
false
|
||||
end
|
||||
|
||||
def skip_pending_deletion_response?
|
||||
false
|
||||
end
|
||||
|
||||
def permanent_unavailability_response
|
||||
expires_in(3.minutes, public: true)
|
||||
gone
|
||||
|
|
|
@ -37,7 +37,7 @@ class Settings::DeletesController < Settings::BaseController
|
|||
end
|
||||
|
||||
def destroy_account!
|
||||
current_account.suspend!(origin: :local, block_email: false)
|
||||
current_account.mark_deleted!
|
||||
AccountDeletionWorker.perform_async(current_user.account_id)
|
||||
sign_out
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
|
|||
target_statuses = target_statuses_by_account[target_account.id]
|
||||
replied_to_accounts = target_statuses.nil? ? [] : Account.local.where(id: target_statuses.filter_map(&:in_reply_to_account_id))
|
||||
|
||||
next if target_account.suspended? || (!target_account.local? && replied_to_accounts.none?)
|
||||
next if target_account.suspended? || target_account.permanently_deleted? || (!target_account.local? && replied_to_accounts.none?)
|
||||
|
||||
ReportService.new.call(
|
||||
@account,
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
# reviewed_at :datetime
|
||||
# requested_review_at :datetime
|
||||
# indexable :boolean default(FALSE), not null
|
||||
# deleted_at :datetime
|
||||
#
|
||||
|
||||
class Account < ApplicationRecord
|
||||
|
@ -122,7 +123,8 @@ class Account < ApplicationRecord
|
|||
scope :silenced, -> { where.not(silenced_at: nil) }
|
||||
scope :suspended, -> { where.not(suspended_at: nil) }
|
||||
scope :sensitized, -> { where.not(sensitized_at: nil) }
|
||||
scope :without_suspended, -> { where(suspended_at: nil) }
|
||||
scope :without_deleted, -> { where(deleted_at: nil) }
|
||||
scope :without_suspended, -> { without_deleted.where(suspended_at: nil) }
|
||||
scope :without_silenced, -> { where(silenced_at: nil) }
|
||||
scope :without_instance_actor, -> { where.not(id: INSTANCE_ACTOR_ID) }
|
||||
scope :recent, -> { reorder(id: :desc) }
|
||||
|
@ -247,16 +249,25 @@ class Account < ApplicationRecord
|
|||
suspended_at.present? && !instance_actor?
|
||||
end
|
||||
|
||||
def suspended_permanently?
|
||||
suspended? && deletion_request.nil?
|
||||
end
|
||||
|
||||
def suspended_temporarily?
|
||||
suspended? && deletion_request.present?
|
||||
end
|
||||
|
||||
alias unavailable? suspended?
|
||||
alias permanently_unavailable? suspended_permanently?
|
||||
def deleted?
|
||||
deleted_at.present? && !instance_actor?
|
||||
end
|
||||
|
||||
def permanently_deleted?
|
||||
deleted? && deletion_request.nil?
|
||||
end
|
||||
|
||||
def unavailable?
|
||||
deleted? || suspended?
|
||||
end
|
||||
|
||||
def permanently_unavailable?
|
||||
unavailable? && deletion_request.nil?
|
||||
end
|
||||
|
||||
def suspend!(date: Time.now.utc, origin: :local, block_email: true)
|
||||
transaction do
|
||||
|
@ -266,6 +277,13 @@ class Account < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def mark_deleted!(date: Time.now.utc)
|
||||
transaction do
|
||||
create_deletion_request!
|
||||
update!(deleted_at: date)
|
||||
end
|
||||
end
|
||||
|
||||
def unsuspend!
|
||||
transaction do
|
||||
deletion_request&.destroy!
|
||||
|
|
|
@ -57,7 +57,7 @@ module Account::Search
|
|||
LEFT JOIN users ON accounts.id = users.account_id
|
||||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||||
WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||||
AND accounts.suspended_at IS NULL
|
||||
AND accounts.suspended_at IS NULL AND accounts.deleted_at IS NULL
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
|
||||
ORDER BY rank DESC
|
||||
|
@ -80,7 +80,7 @@ module Account::Search
|
|||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||||
WHERE accounts.id IN (SELECT * FROM first_degree)
|
||||
AND to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||||
AND accounts.suspended_at IS NULL
|
||||
AND accounts.suspended_at IS NULL AND accounts.deleted_at IS NULL
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
GROUP BY accounts.id, s.id
|
||||
ORDER BY rank DESC
|
||||
|
@ -98,7 +98,7 @@ module Account::Search
|
|||
LEFT JOIN users ON accounts.id = users.account_id
|
||||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||||
WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||||
AND accounts.suspended_at IS NULL
|
||||
AND accounts.suspended_at IS NULL AND accounts.deleted_at IS NULL
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
|
||||
GROUP BY accounts.id, s.id
|
||||
|
|
|
@ -112,14 +112,14 @@ class User < ApplicationRecord
|
|||
validates :confirm_password, absence: true, on: :create
|
||||
validate :validate_role_elevation
|
||||
|
||||
scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) }
|
||||
scope :account_available, -> { joins(:account).merge(Account.without_suspended.without_deleted) }
|
||||
scope :recent, -> { order(id: :desc) }
|
||||
scope :pending, -> { where(approved: false) }
|
||||
scope :approved, -> { where(approved: true) }
|
||||
scope :confirmed, -> { where.not(confirmed_at: nil) }
|
||||
scope :enabled, -> { where(disabled: false) }
|
||||
scope :disabled, -> { where(disabled: true) }
|
||||
scope :active, -> { confirmed.signed_in_recently.account_not_suspended }
|
||||
scope :active, -> { confirmed.signed_in_recently.account_available }
|
||||
scope :signed_in_recently, -> { where(current_sign_in_at: ACTIVE_DURATION.ago..) }
|
||||
scope :not_signed_in_recently, -> { where(current_sign_in_at: ...ACTIVE_DURATION.ago) }
|
||||
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
|
||||
|
|
|
@ -225,9 +225,14 @@ class DeleteAccountService < BaseService
|
|||
|
||||
return unless keep_account_record?
|
||||
|
||||
if @options[:suspended_at]
|
||||
@account.suspended_at = @options[:suspended_at]
|
||||
@account.suspension_origin = :local
|
||||
else
|
||||
@account.deleted_at = Time.now.utc
|
||||
end
|
||||
|
||||
@account.silenced_at = nil
|
||||
@account.suspended_at = @options[:suspended_at] || Time.now.utc
|
||||
@account.suspension_origin = :local
|
||||
@account.locked = false
|
||||
@account.memorial = false
|
||||
@account.discoverable = false
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(account.id), method: :post, class: 'button' if can?(:redownload, account) && account.suspension_origin_remote?
|
||||
- if deletion_request.present? && can?(:destroy, account)
|
||||
= link_to t('admin.accounts.delete'), admin_account_path(account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') }
|
||||
- elsif account.deleted?
|
||||
%hr.spacer/
|
||||
|
||||
%p.muted-hint= deletion_request.present? ? t('admin.accounts.deletion_reversible_hint_html', date: content_tag(:strong, l(deletion_request.due_at.to_date))) : t('admin.accounts.deletion_irreversible')
|
||||
- else
|
||||
.action-buttons
|
||||
%div
|
||||
|
|
|
@ -22,12 +22,12 @@
|
|||
%div
|
||||
= link_to admin_action_logs_path(target_account_id: account.id) do
|
||||
.dashboard__counters__text
|
||||
- if account.local? && account.user.nil?
|
||||
- if account.suspended?
|
||||
= t('admin.accounts.suspended')
|
||||
- elsif account.deleted? || (account.local? && account.user.nil?)
|
||||
= t('admin.accounts.deleted')
|
||||
- elsif account.memorial?
|
||||
= t('admin.accounts.memorialized')
|
||||
- elsif account.suspended?
|
||||
= t('admin.accounts.suspended')
|
||||
- elsif account.silenced?
|
||||
= t('admin.accounts.silenced')
|
||||
- elsif account.local? && account.user&.disabled?
|
||||
|
|
|
@ -37,6 +37,8 @@
|
|||
%br/
|
||||
- if target_account.suspended?
|
||||
%span.red= t('admin.accounts.suspended')
|
||||
- elsif target_account.deleted?
|
||||
%span.red= t('admin.accounts.deleted')
|
||||
- elsif target_account.silenced?
|
||||
%span.red= t('admin.accounts.silenced')
|
||||
- elsif target_account.user&.disabled?
|
||||
|
|
|
@ -35,7 +35,7 @@ class Scheduler::SelfDestructScheduler
|
|||
# deletion request.
|
||||
|
||||
# This targets accounts that have not been deleted nor marked for deletion yet
|
||||
Account.local.without_suspended.reorder(id: :asc).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
|
||||
Account.local.without_deleted.reorder(id: :asc).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
|
||||
delete_account!(account)
|
||||
end
|
||||
|
||||
|
@ -43,9 +43,8 @@ class Scheduler::SelfDestructScheduler
|
|||
|
||||
# This targets accounts that have been marked for deletion but have not been
|
||||
# deleted yet
|
||||
Account.local.suspended.joins(:deletion_request).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
|
||||
Account.local.joins(:deletion_request).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
|
||||
delete_account!(account)
|
||||
account.deletion_request&.destroy
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -66,7 +65,8 @@ class Scheduler::SelfDestructScheduler
|
|||
[json, account.id, inbox_url]
|
||||
end
|
||||
|
||||
# Do not call `Account#suspend!` because we don't want to issue a deletion request
|
||||
account.update!(suspended_at: Time.now.utc, suspension_origin: :local)
|
||||
# Do not call `Account#mark_deleted!` because we don't want to issue a deletion request
|
||||
account.update!(deleted_at: Time.now.utc)
|
||||
account.deletion_request&.destroy
|
||||
end
|
||||
end
|
||||
|
|
|
@ -55,6 +55,8 @@ en:
|
|||
custom: Custom
|
||||
delete: Delete data
|
||||
deleted: Deleted
|
||||
deletion_irreversible: The data of this account has been irreversibly deleted.
|
||||
deletion_reversible_hint_html: This account is pending deletion, and the data will be fully removed on %{date}.
|
||||
demote: Demote
|
||||
destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
|
||||
disable: Freeze
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDeletedAtToAccounts < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :accounts, :deleted_at, :datetime
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_03_22_161611) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_03_27_124057) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
@ -200,6 +200,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_03_22_161611) do
|
|||
t.datetime "reviewed_at", precision: nil
|
||||
t.datetime "requested_review_at", precision: nil
|
||||
t.boolean "indexable", default: false, null: false
|
||||
t.datetime "deleted_at", precision: nil
|
||||
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
|
||||
t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
|
||||
t.index ["domain", "id"], name: "index_accounts_on_domain_and_id"
|
||||
|
|
|
@ -53,7 +53,7 @@ describe Settings::DeletesController do
|
|||
it 'removes user record and redirects', :aggregate_failures, :sidekiq_inline do
|
||||
expect(response).to redirect_to '/auth/sign_in'
|
||||
expect(User.find_by(id: user.id)).to be_nil
|
||||
expect(user.account.reload).to be_suspended
|
||||
expect(user.account.reload).to be_deleted
|
||||
expect(CanonicalEmailBlock.block?(user.email)).to be false
|
||||
end
|
||||
|
||||
|
|
|
@ -138,16 +138,19 @@ RSpec.describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'account_not_suspended' do
|
||||
it 'returns with linked accounts that are not suspended' do
|
||||
describe 'account_available' do
|
||||
it 'returns with linked accounts that are not suspended or deleted' do
|
||||
suspended_account = Fabricate(:account, suspended_at: 10.days.ago)
|
||||
non_suspended_account = Fabricate(:account, suspended_at: nil)
|
||||
suspended_user = Fabricate(:user, account: suspended_account)
|
||||
non_suspended_user = Fabricate(:user, account: non_suspended_account)
|
||||
deleted_account = Fabricate(:account, deleted_at: 10.days.ago)
|
||||
deleted_user = Fabricate(:user, account: deleted_account)
|
||||
|
||||
expect(described_class.account_not_suspended)
|
||||
expect(described_class.account_available)
|
||||
.to include(non_suspended_user)
|
||||
.and not_include(suspended_user)
|
||||
.and not_include(deleted_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -39,19 +39,19 @@ describe Scheduler::SelfDestructScheduler do
|
|||
end
|
||||
|
||||
context 'when sidekiq is operational' do
|
||||
it 'suspends local non-suspended accounts' do
|
||||
it 'deletes local non-deleted accounts' do
|
||||
worker.perform
|
||||
|
||||
expect(account.reload.suspended_at).to_not be_nil
|
||||
expect(account.reload.deleted_at).to_not be_nil
|
||||
end
|
||||
|
||||
it 'suspends local suspended accounts marked for deletion' do
|
||||
account.update(suspended_at: 10.days.ago)
|
||||
it 'deletes local accounts marked for deletion' do
|
||||
account.update(deleted_at: 10.days.ago)
|
||||
deletion_request = Fabricate(:account_deletion_request, account: account)
|
||||
|
||||
worker.perform
|
||||
|
||||
expect(account.reload.suspended_at).to be > 1.day.ago
|
||||
expect(account.reload.deleted_at).to be > 1.day.ago
|
||||
expect { deletion_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue