Change account suspensions to be reversible by default (#14726)
This commit is contained in:
parent
bbcbf12215
commit
ed099d8bdc
39 changed files with 526 additions and 279 deletions
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Admin
|
||||
class AccountsController < BaseController
|
||||
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
|
||||
before_action :set_account, except: [:index]
|
||||
before_action :require_remote_account!, only: [:redownload]
|
||||
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
|
||||
|
||||
|
@ -14,49 +14,58 @@ module Admin
|
|||
def show
|
||||
authorize @account, :show?
|
||||
|
||||
@deletion_request = @account.deletion_request
|
||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
||||
@warnings = @account.targeted_account_warnings.latest.custom
|
||||
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||
end
|
||||
|
||||
def memorialize
|
||||
authorize @account, :memorialize?
|
||||
@account.memorialize!
|
||||
log_action :memorialize, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.memorialized_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def enable
|
||||
authorize @account.user, :enable?
|
||||
@account.user.enable!
|
||||
log_action :enable, @account.user
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.enabled_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def approve
|
||||
authorize @account.user, :approve?
|
||||
@account.user.approve!
|
||||
redirect_to admin_pending_accounts_path
|
||||
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.approved_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def reject
|
||||
authorize @account.user, :reject?
|
||||
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
redirect_to admin_pending_accounts_path
|
||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
redirect_to admin_pending_accounts_path, notice: I18n.t('admin.accounts.rejected_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @account, :destroy?
|
||||
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.destroyed_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def unsilence
|
||||
authorize @account, :unsilence?
|
||||
@account.unsilence!
|
||||
log_action :unsilence, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsilenced_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def unsuspend
|
||||
authorize @account, :unsuspend?
|
||||
@account.unsuspend!
|
||||
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||
log_action :unsuspend, @account
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.unsuspended_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def redownload
|
||||
|
@ -65,7 +74,7 @@ module Admin
|
|||
@account.update!(last_webfingered_at: nil)
|
||||
ResolveAccountService.new.call(@account)
|
||||
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.redownloaded_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def remove_avatar
|
||||
|
@ -76,7 +85,7 @@ module Admin
|
|||
|
||||
log_action :remove_avatar, @account.user
|
||||
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_avatar_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
def remove_header
|
||||
|
@ -87,7 +96,7 @@ module Admin
|
|||
|
||||
log_action :remove_header, @account.user
|
||||
|
||||
redirect_to admin_account_path(@account.id)
|
||||
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.removed_header_msg', username: @account.acct)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -96,12 +96,12 @@ class Api::BaseController < ApplicationController
|
|||
def require_user!
|
||||
if !current_user
|
||||
render json: { error: 'This method requires an authenticated user' }, status: 422
|
||||
elsif current_user.disabled?
|
||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||
elsif !current_user.confirmed?
|
||||
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
|
||||
elsif !current_user.approved?
|
||||
render json: { error: 'Your login is currently pending approval' }, status: 403
|
||||
elsif !current_user.functional?
|
||||
render json: { error: 'Your login is currently disabled' }, status: 403
|
||||
else
|
||||
set_user_activity
|
||||
end
|
||||
|
|
|
@ -58,7 +58,13 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||
|
||||
def reject
|
||||
authorize @account.user, :reject?
|
||||
SuspendAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
DeleteAccountService.new.call(@account, reserve_email: false, reserve_username: false)
|
||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @account, :destroy?
|
||||
Admin::AccountDeletionWorker.perform_async(@account.id)
|
||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||
end
|
||||
|
||||
|
@ -72,6 +78,7 @@ class Api::V1::Admin::AccountsController < Api::BaseController
|
|||
def unsuspend
|
||||
authorize @account, :unsuspend?
|
||||
@account.unsuspend!
|
||||
Admin::UnsuspensionWorker.perform_async(@account.id)
|
||||
log_action :unsuspend, @account
|
||||
render json: @account, serializer: REST::Admin::AccountSerializer
|
||||
end
|
||||
|
|
|
@ -43,7 +43,7 @@ class Settings::DeletesController < Settings::BaseController
|
|||
|
||||
def destroy_account!
|
||||
current_account.suspend!
|
||||
Admin::SuspensionWorker.perform_async(current_user.account_id, true)
|
||||
AccountDeletionWorker.perform_async(current_user.account_id)
|
||||
sign_out
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
|||
|
||||
def delete_person
|
||||
lock_or_return("delete_in_progress:#{@account.id}") do
|
||||
SuspendAccountService.new.call(@account, reserve_username: false)
|
||||
DeleteAccountService.new.call(@account, reserve_username: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@me = recipient
|
||||
@status = notification.target_status
|
||||
|
||||
return if @me.user.disabled? || @status.nil?
|
||||
return unless @me.user.functional? && @status.present?
|
||||
|
||||
locale_for_account(@me) do
|
||||
thread_by_conversation(@status.conversation)
|
||||
|
@ -22,7 +22,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@me = recipient
|
||||
@account = notification.from_account
|
||||
|
||||
return if @me.user.disabled?
|
||||
return unless @me.user.functional?
|
||||
|
||||
locale_for_account(@me) do
|
||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow.subject', name: @account.acct)
|
||||
|
@ -34,7 +34,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@account = notification.from_account
|
||||
@status = notification.target_status
|
||||
|
||||
return if @me.user.disabled? || @status.nil?
|
||||
return unless @me.user.functional? && @status.present?
|
||||
|
||||
locale_for_account(@me) do
|
||||
thread_by_conversation(@status.conversation)
|
||||
|
@ -47,7 +47,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@account = notification.from_account
|
||||
@status = notification.target_status
|
||||
|
||||
return if @me.user.disabled? || @status.nil?
|
||||
return unless @me.user.functional? && @status.present?
|
||||
|
||||
locale_for_account(@me) do
|
||||
thread_by_conversation(@status.conversation)
|
||||
|
@ -59,7 +59,7 @@ class NotificationMailer < ApplicationMailer
|
|||
@me = recipient
|
||||
@account = notification.from_account
|
||||
|
||||
return if @me.user.disabled?
|
||||
return unless @me.user.functional?
|
||||
|
||||
locale_for_account(@me) do
|
||||
mail to: @me.user.email, subject: I18n.t('notification_mailer.follow_request.subject', name: @account.acct)
|
||||
|
@ -67,7 +67,7 @@ class NotificationMailer < ApplicationMailer
|
|||
end
|
||||
|
||||
def digest(recipient, **opts)
|
||||
return if recipient.user.disabled?
|
||||
return unless recipient.user.functional?
|
||||
|
||||
@me = recipient
|
||||
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
|
||||
|
@ -88,8 +88,10 @@ class NotificationMailer < ApplicationMailer
|
|||
|
||||
def thread_by_conversation(conversation)
|
||||
return if conversation.nil?
|
||||
|
||||
msg_id = "<conversation-#{conversation.id}.#{conversation.created_at.strftime('%Y-%m-%d')}@#{Rails.configuration.x.local_domain}>"
|
||||
|
||||
headers['In-Reply-To'] = msg_id
|
||||
headers['References'] = msg_id
|
||||
headers['References'] = msg_id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ class UserMailer < Devise::Mailer
|
|||
@token = token
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.unconfirmed_email.presence || @resource.email,
|
||||
|
@ -29,7 +29,7 @@ class UserMailer < Devise::Mailer
|
|||
@token = token
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.reset_password_instructions.subject')
|
||||
|
@ -40,7 +40,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.password_change.subject')
|
||||
|
@ -51,7 +51,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.email_changed.subject')
|
||||
|
@ -62,7 +62,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_enabled.subject')
|
||||
|
@ -73,7 +73,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_disabled.subject')
|
||||
|
@ -84,7 +84,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.two_factor_recovery_codes_changed.subject')
|
||||
|
@ -95,7 +95,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_enabled.subject')
|
||||
|
@ -106,7 +106,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_disabled.subject')
|
||||
|
@ -118,7 +118,7 @@ class UserMailer < Devise::Mailer
|
|||
@instance = Rails.configuration.x.local_domain
|
||||
@webauthn_credential = webauthn_credential
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.added.subject')
|
||||
|
@ -130,7 +130,7 @@ class UserMailer < Devise::Mailer
|
|||
@instance = Rails.configuration.x.local_domain
|
||||
@webauthn_credential = webauthn_credential
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('devise.mailer.webauthn_credential.deleted.subject')
|
||||
|
@ -141,7 +141,7 @@ class UserMailer < Devise::Mailer
|
|||
@resource = user
|
||||
@instance = Rails.configuration.x.local_domain
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.welcome.subject')
|
||||
|
@ -153,7 +153,7 @@ class UserMailer < Devise::Mailer
|
|||
@instance = Rails.configuration.x.local_domain
|
||||
@backup = backup
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email, subject: I18n.t('user_mailer.backup_ready.subject')
|
||||
|
@ -181,7 +181,7 @@ class UserMailer < Devise::Mailer
|
|||
@detection = Browser.new(user_agent)
|
||||
@timestamp = timestamp.to_time.utc
|
||||
|
||||
return if @resource.disabled?
|
||||
return unless @resource.active_for_authentication?
|
||||
|
||||
I18n.with_locale(@resource.locale || I18n.default_locale) do
|
||||
mail to: @resource.email,
|
||||
|
|
|
@ -222,23 +222,20 @@ class Account < ApplicationRecord
|
|||
|
||||
def suspend!(date = Time.now.utc)
|
||||
transaction do
|
||||
user&.disable! if local?
|
||||
create_deletion_request!
|
||||
update!(suspended_at: date)
|
||||
end
|
||||
end
|
||||
|
||||
def unsuspend!
|
||||
transaction do
|
||||
user&.enable! if local?
|
||||
deletion_request&.destroy!
|
||||
update!(suspended_at: nil)
|
||||
end
|
||||
end
|
||||
|
||||
def memorialize!
|
||||
transaction do
|
||||
user&.disable! if local?
|
||||
update!(memorial: true)
|
||||
end
|
||||
update!(memorial: true)
|
||||
end
|
||||
|
||||
def sign?
|
||||
|
|
20
app/models/account_deletion_request.rb
Normal file
20
app/models/account_deletion_request.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: account_deletion_requests
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# account_id :bigint(8)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
class AccountDeletionRequest < ApplicationRecord
|
||||
DELAY_TO_DELETION = 30.days.freeze
|
||||
|
||||
belongs_to :account
|
||||
|
||||
def due_at
|
||||
created_at + DELAY_TO_DELETION
|
||||
end
|
||||
end
|
|
@ -134,7 +134,7 @@ class Admin::AccountAction
|
|||
end
|
||||
|
||||
def process_email!
|
||||
UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable?
|
||||
UserMailer.warning(target_account.user, warning, status_ids).deliver_later! if warnable?
|
||||
end
|
||||
|
||||
def warnable?
|
||||
|
|
|
@ -60,5 +60,8 @@ module AccountAssociations
|
|||
# Hashtags
|
||||
has_and_belongs_to_many :tags
|
||||
has_many :featured_tags, -> { includes(:tag) }, dependent: :destroy, inverse_of: :account
|
||||
|
||||
# Account deletion requests
|
||||
has_one :deletion_request, class_name: 'AccountDeletionRequest', inverse_of: :account, dependent: :destroy
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,6 +69,6 @@ class Form::AccountBatch
|
|||
records = accounts.includes(:user)
|
||||
|
||||
records.each { |account| authorize(account.user, :reject?) }
|
||||
.each { |account| SuspendAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
||||
.each { |account| DeleteAccountService.new.call(account, reserve_email: false, reserve_username: false) }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,7 +28,7 @@ class Invite < ApplicationRecord
|
|||
before_validation :set_code
|
||||
|
||||
def valid_for_use?
|
||||
(max_uses.nil? || uses < max_uses) && !expired? && !(user.nil? || user.disabled?)
|
||||
(max_uses.nil? || uses < max_uses) && !expired? && user&.functional?
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -168,7 +168,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def active_for_authentication?
|
||||
true
|
||||
!account.memorial?
|
||||
end
|
||||
|
||||
def suspicious_sign_in?(ip)
|
||||
|
@ -176,7 +176,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def functional?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
|
||||
confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? && account.moved_to_account_id.nil?
|
||||
end
|
||||
|
||||
def unconfirmed_or_pending?
|
||||
|
|
|
@ -17,6 +17,10 @@ class AccountPolicy < ApplicationPolicy
|
|||
staff? && !record.user&.staff?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
record.suspended? && record.deletion_request.present? && admin?
|
||||
end
|
||||
|
||||
def unsuspend?
|
||||
staff?
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class AfterUnallowDomainService < BaseService
|
||||
def call(domain)
|
||||
Account.where(domain: domain).find_each do |account|
|
||||
SuspendAccountService.new.call(account, reserve_username: false)
|
||||
DeleteAccountService.new.call(account, reserve_username: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,7 +36,7 @@ class BlockDomainService < BaseService
|
|||
def suspend_accounts!
|
||||
blocked_domain_accounts.without_suspended.in_batches.update_all(suspended_at: @domain_block.created_at)
|
||||
blocked_domain_accounts.where(suspended_at: @domain_block.created_at).reorder(nil).find_each do |account|
|
||||
SuspendAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
|
||||
DeleteAccountService.new.call(account, reserve_username: true, suspended_at: @domain_block.created_at)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
180
app/services/delete_account_service.rb
Normal file
180
app/services/delete_account_service.rb
Normal file
|
@ -0,0 +1,180 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DeleteAccountService < BaseService
|
||||
include Payloadable
|
||||
|
||||
ASSOCIATIONS_ON_SUSPEND = %w(
|
||||
account_pins
|
||||
active_relationships
|
||||
block_relationships
|
||||
blocked_by_relationships
|
||||
conversation_mutes
|
||||
conversations
|
||||
custom_filters
|
||||
domain_blocks
|
||||
favourites
|
||||
follow_requests
|
||||
list_accounts
|
||||
mute_relationships
|
||||
muted_by_relationships
|
||||
notifications
|
||||
owned_lists
|
||||
passive_relationships
|
||||
report_notes
|
||||
scheduled_statuses
|
||||
status_pins
|
||||
).freeze
|
||||
|
||||
ASSOCIATIONS_ON_DESTROY = %w(
|
||||
reports
|
||||
targeted_moderation_notes
|
||||
targeted_reports
|
||||
).freeze
|
||||
|
||||
# Suspend or remove an account and remove as much of its data
|
||||
# as possible. If it's a local account and it has not been confirmed
|
||||
# or never been approved, then side effects are skipped and both
|
||||
# the user and account records are removed fully. Otherwise,
|
||||
# it is controlled by options.
|
||||
# @param [Account]
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
|
||||
# @option [Boolean] :reserve_username Keep account record
|
||||
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
|
||||
# @option [Time] :suspended_at Only applicable when :reserve_username is true
|
||||
def call(account, **options)
|
||||
@account = account
|
||||
@options = { reserve_username: true, reserve_email: true }.merge(options)
|
||||
|
||||
if @account.local? && @account.user_unconfirmed_or_pending?
|
||||
@options[:reserve_email] = false
|
||||
@options[:reserve_username] = false
|
||||
@options[:skip_side_effects] = true
|
||||
end
|
||||
|
||||
reject_follows!
|
||||
purge_user!
|
||||
purge_profile!
|
||||
purge_content!
|
||||
fulfill_deletion_request!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reject_follows!
|
||||
return if @account.local? || !@account.activitypub?
|
||||
|
||||
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
|
||||
[build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def purge_user!
|
||||
return if !@account.local? || @account.user.nil?
|
||||
|
||||
if @options[:reserve_email]
|
||||
@account.user.disable!
|
||||
@account.user.invites.where(uses: 0).destroy_all
|
||||
else
|
||||
@account.user.destroy
|
||||
end
|
||||
end
|
||||
|
||||
def purge_content!
|
||||
distribute_delete_actor! if @account.local? && !@options[:skip_side_effects]
|
||||
|
||||
@account.statuses.reorder(nil).find_in_batches do |statuses|
|
||||
statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username]
|
||||
BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects])
|
||||
end
|
||||
|
||||
@account.media_attachments.reorder(nil).find_each do |media_attachment|
|
||||
next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id)
|
||||
|
||||
media_attachment.destroy
|
||||
end
|
||||
|
||||
@account.polls.reorder(nil).find_each do |poll|
|
||||
next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id)
|
||||
|
||||
poll.destroy
|
||||
end
|
||||
|
||||
associations_for_destruction.each do |association_name|
|
||||
destroy_all(@account.public_send(association_name))
|
||||
end
|
||||
|
||||
@account.destroy unless @options[:reserve_username]
|
||||
end
|
||||
|
||||
def purge_profile!
|
||||
# If the account is going to be destroyed
|
||||
# there is no point wasting time updating
|
||||
# its values first
|
||||
|
||||
return unless @options[:reserve_username]
|
||||
|
||||
@account.silenced_at = nil
|
||||
@account.suspended_at = @options[:suspended_at] || Time.now.utc
|
||||
@account.locked = false
|
||||
@account.memorial = false
|
||||
@account.discoverable = false
|
||||
@account.display_name = ''
|
||||
@account.note = ''
|
||||
@account.fields = []
|
||||
@account.statuses_count = 0
|
||||
@account.followers_count = 0
|
||||
@account.following_count = 0
|
||||
@account.moved_to_account = nil
|
||||
@account.trust_level = :untrusted
|
||||
@account.avatar.destroy
|
||||
@account.header.destroy
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def fulfill_deletion_request!
|
||||
@account.deletion_request&.destroy
|
||||
end
|
||||
|
||||
def destroy_all(association)
|
||||
association.in_batches.destroy_all
|
||||
end
|
||||
|
||||
def distribute_delete_actor!
|
||||
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
|
||||
[delete_actor_json, @account.id, inbox_url]
|
||||
end
|
||||
|
||||
ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
|
||||
[delete_actor_json, @account.id, inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def delete_actor_json
|
||||
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
|
||||
end
|
||||
|
||||
def build_reject_json(follow)
|
||||
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
|
||||
end
|
||||
|
||||
def delivery_inboxes
|
||||
@delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
|
||||
end
|
||||
|
||||
def low_priority_delivery_inboxes
|
||||
Account.inboxes - delivery_inboxes
|
||||
end
|
||||
|
||||
def reported_status_ids
|
||||
@reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
|
||||
end
|
||||
|
||||
def associations_for_destruction
|
||||
if @options[:reserve_username]
|
||||
ASSOCIATIONS_ON_SUSPEND
|
||||
else
|
||||
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,175 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SuspendAccountService < BaseService
|
||||
include Payloadable
|
||||
|
||||
ASSOCIATIONS_ON_SUSPEND = %w(
|
||||
account_pins
|
||||
active_relationships
|
||||
block_relationships
|
||||
blocked_by_relationships
|
||||
conversation_mutes
|
||||
conversations
|
||||
custom_filters
|
||||
domain_blocks
|
||||
favourites
|
||||
follow_requests
|
||||
list_accounts
|
||||
mute_relationships
|
||||
muted_by_relationships
|
||||
notifications
|
||||
owned_lists
|
||||
passive_relationships
|
||||
report_notes
|
||||
scheduled_statuses
|
||||
status_pins
|
||||
).freeze
|
||||
|
||||
ASSOCIATIONS_ON_DESTROY = %w(
|
||||
reports
|
||||
targeted_moderation_notes
|
||||
targeted_reports
|
||||
).freeze
|
||||
|
||||
# Suspend or remove an account and remove as much of its data
|
||||
# as possible. If it's a local account and it has not been confirmed
|
||||
# or never been approved, then side effects are skipped and both
|
||||
# the user and account records are removed fully. Otherwise,
|
||||
# it is controlled by options.
|
||||
# @param [Account]
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :reserve_email Keep user record. Only applicable for local accounts
|
||||
# @option [Boolean] :reserve_username Keep account record
|
||||
# @option [Boolean] :skip_side_effects Side effects are ActivityPub and streaming API payloads
|
||||
# @option [Time] :suspended_at Only applicable when :reserve_username is true
|
||||
def call(account, **options)
|
||||
def call(account)
|
||||
@account = account
|
||||
@options = { reserve_username: true, reserve_email: true }.merge(options)
|
||||
|
||||
if @account.local? && @account.user_unconfirmed_or_pending?
|
||||
@options[:reserve_email] = false
|
||||
@options[:reserve_username] = false
|
||||
@options[:skip_side_effects] = true
|
||||
end
|
||||
|
||||
reject_follows!
|
||||
purge_user!
|
||||
purge_profile!
|
||||
purge_content!
|
||||
suspend!
|
||||
unmerge_from_home_timelines!
|
||||
unmerge_from_list_timelines!
|
||||
privatize_media_attachments!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def reject_follows!
|
||||
return if @account.local? || !@account.activitypub?
|
||||
def suspend!
|
||||
@account.suspend! unless @account.suspended?
|
||||
end
|
||||
|
||||
ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
|
||||
[build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
|
||||
def unmerge_from_home_timelines!
|
||||
@account.followers_for_local_distribution.find_each do |follower|
|
||||
FeedManager.instance.unmerge_from_timeline(@account, follower)
|
||||
end
|
||||
end
|
||||
|
||||
def purge_user!
|
||||
return if !@account.local? || @account.user.nil?
|
||||
|
||||
if @options[:reserve_email]
|
||||
@account.user.disable!
|
||||
@account.user.invites.where(uses: 0).destroy_all
|
||||
else
|
||||
@account.user.destroy
|
||||
def unmerge_from_list_timelines!
|
||||
@account.lists_for_local_distribution.find_each do |list|
|
||||
FeedManager.instance.unmerge_from_list(@account, list)
|
||||
end
|
||||
end
|
||||
|
||||
def purge_content!
|
||||
distribute_delete_actor! if @account.local? && !@options[:skip_side_effects]
|
||||
def privatize_media_attachments!
|
||||
attachment_names = MediaAttachment.attachment_definitions.keys
|
||||
|
||||
@account.statuses.reorder(nil).find_in_batches do |statuses|
|
||||
statuses.reject! { |status| reported_status_ids.include?(status.id) } if @options[:reserve_username]
|
||||
BatchedRemoveStatusService.new.call(statuses, skip_side_effects: @options[:skip_side_effects])
|
||||
end
|
||||
@account.media_attachments.find_each do |media_attachment|
|
||||
attachment_names.each do |attachment_name|
|
||||
attachment = media_attachment.public_send(attachment_name)
|
||||
styles = [:original] | attachment.styles.keys
|
||||
|
||||
@account.media_attachments.reorder(nil).find_each do |media_attachment|
|
||||
next if @options[:reserve_username] && reported_status_ids.include?(media_attachment.status_id)
|
||||
|
||||
media_attachment.destroy
|
||||
end
|
||||
|
||||
@account.polls.reorder(nil).find_each do |poll|
|
||||
next if @options[:reserve_username] && reported_status_ids.include?(poll.status_id)
|
||||
|
||||
poll.destroy
|
||||
end
|
||||
|
||||
associations_for_destruction.each do |association_name|
|
||||
destroy_all(@account.public_send(association_name))
|
||||
end
|
||||
|
||||
@account.destroy unless @options[:reserve_username]
|
||||
end
|
||||
|
||||
def purge_profile!
|
||||
# If the account is going to be destroyed
|
||||
# there is no point wasting time updating
|
||||
# its values first
|
||||
|
||||
return unless @options[:reserve_username]
|
||||
|
||||
@account.silenced_at = nil
|
||||
@account.suspended_at = @options[:suspended_at] || Time.now.utc
|
||||
@account.locked = false
|
||||
@account.memorial = false
|
||||
@account.discoverable = false
|
||||
@account.display_name = ''
|
||||
@account.note = ''
|
||||
@account.fields = []
|
||||
@account.statuses_count = 0
|
||||
@account.followers_count = 0
|
||||
@account.following_count = 0
|
||||
@account.moved_to_account = nil
|
||||
@account.trust_level = :untrusted
|
||||
@account.avatar.destroy
|
||||
@account.header.destroy
|
||||
@account.save!
|
||||
end
|
||||
|
||||
def destroy_all(association)
|
||||
association.in_batches.destroy_all
|
||||
end
|
||||
|
||||
def distribute_delete_actor!
|
||||
ActivityPub::DeliveryWorker.push_bulk(delivery_inboxes) do |inbox_url|
|
||||
[delete_actor_json, @account.id, inbox_url]
|
||||
end
|
||||
|
||||
ActivityPub::LowPriorityDeliveryWorker.push_bulk(low_priority_delivery_inboxes) do |inbox_url|
|
||||
[delete_actor_json, @account.id, inbox_url]
|
||||
end
|
||||
end
|
||||
|
||||
def delete_actor_json
|
||||
@delete_actor_json ||= Oj.dump(serialize_payload(@account, ActivityPub::DeleteActorSerializer, signer: @account))
|
||||
end
|
||||
|
||||
def build_reject_json(follow)
|
||||
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
|
||||
end
|
||||
|
||||
def delivery_inboxes
|
||||
@delivery_inboxes ||= @account.followers.inboxes + Relay.enabled.pluck(:inbox_url)
|
||||
end
|
||||
|
||||
def low_priority_delivery_inboxes
|
||||
Account.inboxes - delivery_inboxes
|
||||
end
|
||||
|
||||
def reported_status_ids
|
||||
@reported_status_ids ||= Report.where(target_account: @account).unresolved.pluck(:status_ids).flatten.uniq
|
||||
end
|
||||
|
||||
def associations_for_destruction
|
||||
if @options[:reserve_username]
|
||||
ASSOCIATIONS_ON_SUSPEND
|
||||
else
|
||||
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
|
||||
styles.each do |style|
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3
|
||||
attachment.s3_object(style).acl.put(:private)
|
||||
when :fog
|
||||
# Not supported
|
||||
when :filesystem
|
||||
FileUtils.chmod(0o600 & ~File.umask, attachment.path(style))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
52
app/services/unsuspend_account_service.rb
Normal file
52
app/services/unsuspend_account_service.rb
Normal file
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UnsuspendAccountService < BaseService
|
||||
def call(account)
|
||||
@account = account
|
||||
|
||||
unsuspend!
|
||||
merge_into_home_timelines!
|
||||
merge_into_list_timelines!
|
||||
publish_media_attachments!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unsuspend!
|
||||
@account.unsuspend! if @account.suspended?
|
||||
end
|
||||
|
||||
def merge_into_home_timelines!
|
||||
@account.followers_for_local_distribution.find_each do |follower|
|
||||
FeedManager.instance.merge_into_timeline(@account, follower)
|
||||
end
|
||||
end
|
||||
|
||||
def merge_into_list_timelines!
|
||||
@account.lists_for_local_distribution.find_each do |list|
|
||||
FeedManager.instance.merge_into_list(@account, list)
|
||||
end
|
||||
end
|
||||
|
||||
def publish_media_attachments!
|
||||
attachment_names = MediaAttachment.attachment_definitions.keys
|
||||
|
||||
@account.media_attachments.find_each do |media_attachment|
|
||||
attachment_names.each do |attachment_name|
|
||||
attachment = media_attachment.public_send(attachment_name)
|
||||
styles = [:original] | attachment.styles.keys
|
||||
|
||||
styles.each do |style|
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3
|
||||
attachment.s3_object(style).acl.put(Paperclip::Attachment.default_options[:s3_permissions])
|
||||
when :fog
|
||||
# Not supported
|
||||
when :filesystem
|
||||
FileUtils.chmod(0o666 & ~File.umask, attachment.path(style))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -56,19 +56,21 @@
|
|||
= link_to admin_action_logs_path(target_account_id: @account.id) do
|
||||
.dashboard__counters__text
|
||||
- if @account.local? && @account.user.nil?
|
||||
%span.neutral= t('admin.accounts.deleted')
|
||||
= t('admin.accounts.deleted')
|
||||
- elsif @account.memorial?
|
||||
= t('admin.accounts.memorialized')
|
||||
- elsif @account.suspended?
|
||||
%span.red= t('admin.accounts.suspended')
|
||||
= t('admin.accounts.suspended')
|
||||
- elsif @account.silenced?
|
||||
%span.red= t('admin.accounts.silenced')
|
||||
= t('admin.accounts.silenced')
|
||||
- elsif @account.local? && @account.user&.disabled?
|
||||
%span.red= t('admin.accounts.disabled')
|
||||
= t('admin.accounts.disabled')
|
||||
- elsif @account.local? && !@account.user&.confirmed?
|
||||
%span.neutral= t('admin.accounts.confirming')
|
||||
= t('admin.accounts.confirming')
|
||||
- elsif @account.local? && !@account.user_approved?
|
||||
%span.neutral= t('admin.accounts.pending')
|
||||
= t('admin.accounts.pending')
|
||||
- else
|
||||
%span.neutral= t('admin.accounts.no_limits_imposed')
|
||||
= t('admin.accounts.no_limits_imposed')
|
||||
.dashboard__counters__label= t 'admin.accounts.login_status'
|
||||
|
||||
- unless @account.local? && @account.user.nil?
|
||||
|
@ -122,19 +124,6 @@
|
|||
= t('admin.accounts.confirming')
|
||||
%td= table_link_to 'refresh', t('admin.accounts.resend_confirmation.send'), resend_admin_account_confirmation_path(@account.id), method: :post if can?(:confirm, @account.user)
|
||||
|
||||
%tr
|
||||
%th= t('admin.accounts.login_status')
|
||||
%td
|
||||
- if @account.user&.disabled?
|
||||
= t('admin.accounts.disabled')
|
||||
- else
|
||||
= t('admin.accounts.enabled')
|
||||
%td
|
||||
- if @account.user&.disabled?
|
||||
= table_link_to 'unlock', t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post if can?(:enable, @account.user)
|
||||
- elsif @account.user_approved?
|
||||
= table_link_to 'lock', t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable') if can?(:disable, @account.user)
|
||||
|
||||
%tr
|
||||
%th= t('simple_form.labels.defaults.locale')
|
||||
%td= @account.user_locale
|
||||
|
@ -172,49 +161,62 @@
|
|||
%td
|
||||
= @account.inbox_url
|
||||
= fa_icon DeliveryFailureTracker.available?(@account.inbox_url) ? 'check' : 'times'
|
||||
%td
|
||||
= table_link_to 'search', @domain_block.present? ? t('admin.domain_blocks.view') : t('admin.accounts.view_domain'), admin_instance_path(@account.domain)
|
||||
%tr
|
||||
%th= t('admin.accounts.shared_inbox_url')
|
||||
%td
|
||||
= @account.shared_inbox_url
|
||||
= fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times'
|
||||
%td
|
||||
- if @domain_block.nil?
|
||||
= table_link_to 'ban', t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain)
|
||||
|
||||
%div.action-buttons
|
||||
%div
|
||||
- if @account.local? && @account.user_approved?
|
||||
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
|
||||
- if @account.silenced?
|
||||
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
|
||||
- elsif !@account.local? || @account.user_approved?
|
||||
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button button--destructive' if can?(:silence, @account)
|
||||
- if @account.suspended?
|
||||
%hr.spacer/
|
||||
|
||||
- if @account.local?
|
||||
- if @account.user_pending?
|
||||
= link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user)
|
||||
= link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user)
|
||||
%p.muted-hint= @deletion_request.present? ? t('admin.accounts.suspension_reversible_hint_html', date: content_tag(:strong, l(@deletion_request.due_at.to_date))) : t('admin.accounts.suspension_irreversible')
|
||||
|
||||
- unless @account.user_confirmed?
|
||||
= link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user)
|
||||
= link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
|
||||
|
||||
- if @account.suspended?
|
||||
= link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
|
||||
- elsif !@account.local? || @account.user_approved?
|
||||
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
|
||||
- if @deletion_request.present?
|
||||
= link_to t('admin.accounts.delete'), admin_account_path(@account.id), method: :destroy, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, @account)
|
||||
- else
|
||||
%div.action-buttons
|
||||
%div
|
||||
- if @account.local? && @account.user_approved?
|
||||
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
|
||||
|
||||
- unless @account.local?
|
||||
- if DomainBlock.rule_for(@account.domain)
|
||||
= link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button'
|
||||
- if @account.user_disabled?
|
||||
= link_to t('admin.accounts.enable'), enable_admin_account_path(@account.id), method: :post, class: 'button' if can?(:enable, @account.user)
|
||||
- else
|
||||
= link_to t('admin.accounts.disable'), new_admin_account_action_path(@account.id, type: 'disable'), class: 'button' if can?(:disable, @account.user)
|
||||
|
||||
- if @account.silenced?
|
||||
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
|
||||
- elsif !@account.local? || @account.user_approved?
|
||||
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button' if can?(:silence, @account)
|
||||
|
||||
- if @account.local?
|
||||
- if @account.user_pending?
|
||||
= link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user)
|
||||
= link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user)
|
||||
|
||||
- unless @account.user_confirmed?
|
||||
= link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user)
|
||||
|
||||
- if !@account.local? || @account.user_approved?
|
||||
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button' if can?(:suspend, @account)
|
||||
|
||||
%div
|
||||
- if @account.local?
|
||||
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
|
||||
- if @account.user&.otp_required_for_login?
|
||||
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
|
||||
- if !@account.memorial? && @account.user_approved?
|
||||
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
|
||||
- else
|
||||
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
|
||||
|
||||
%div
|
||||
- if @account.local?
|
||||
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
|
||||
- if @account.user&.otp_required_for_login?
|
||||
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
|
||||
- if !@account.memorial? && @account.user_approved?
|
||||
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
|
||||
- else
|
||||
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
|
||||
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
|
|
13
app/workers/account_deletion_worker.rb
Normal file
13
app/workers/account_deletion_worker.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AccountDeletionWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id)
|
||||
DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: false)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
13
app/workers/admin/account_deletion_worker.rb
Normal file
13
app/workers/admin/account_deletion_worker.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::AccountDeletionWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id)
|
||||
DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: true)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -5,7 +5,9 @@ class Admin::SuspensionWorker
|
|||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id, remove_user = false)
|
||||
SuspendAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: !remove_user)
|
||||
def perform(account_id)
|
||||
SuspendAccountService.new.call(Account.find(account_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
13
app/workers/admin/unsuspension_worker.rb
Normal file
13
app/workers/admin/unsuspension_worker.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::UnsuspensionWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: 'pull'
|
||||
|
||||
def perform(account_id)
|
||||
UnsuspendAccountService.new.call(Account.find(account_id))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
end
|
|
@ -6,9 +6,22 @@ class Scheduler::UserCleanupScheduler
|
|||
sidekiq_options lock: :until_executed, retry: 0
|
||||
|
||||
def perform
|
||||
clean_unconfirmed_accounts!
|
||||
clean_suspended_accounts!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clean_unconfirmed_accounts!
|
||||
User.where('confirmed_at is NULL AND confirmation_sent_at <= ?', 2.days.ago).reorder(nil).find_in_batches do |batch|
|
||||
Account.where(id: batch.map(&:account_id)).delete_all
|
||||
User.where(id: batch.map(&:id)).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def clean_suspended_accounts!
|
||||
AccountDeletionRequest.where('created_at <= ?', AccountDeletionRequest::DELAY_TO_DELETION.ago).reorder(nil).find_each do |deletion_request|
|
||||
Admin::AccountDeletionWorker.perform_async(deletion_request.account_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,6 +98,7 @@ en:
|
|||
add_email_domain_block: Block e-mail domain
|
||||
approve: Approve
|
||||
approve_all: Approve all
|
||||
approved_msg: Successfully approved %{username}'s sign-up application
|
||||
are_you_sure: Are you sure?
|
||||
avatar: Avatar
|
||||
by_domain: Domain
|
||||
|
@ -111,18 +112,21 @@ en:
|
|||
confirm: Confirm
|
||||
confirmed: Confirmed
|
||||
confirming: Confirming
|
||||
delete: Delete data
|
||||
deleted: Deleted
|
||||
demote: Demote
|
||||
disable: Disable
|
||||
destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
|
||||
disable: Freeze
|
||||
disable_two_factor_authentication: Disable 2FA
|
||||
disabled: Disabled
|
||||
disabled: Frozen
|
||||
display_name: Display name
|
||||
domain: Domain
|
||||
edit: Edit
|
||||
email: Email
|
||||
email_status: Email status
|
||||
enable: Enable
|
||||
enable: Unfreeze
|
||||
enabled: Enabled
|
||||
enabled_msg: Successfully unfroze %{username}'s account
|
||||
followers: Followers
|
||||
follows: Follows
|
||||
header: Header
|
||||
|
@ -138,6 +142,8 @@ en:
|
|||
login_status: Login status
|
||||
media_attachments: Media attachments
|
||||
memorialize: Turn into memoriam
|
||||
memorialized: Memorialized
|
||||
memorialized_msg: Successfully turned %{username} into a memorial account
|
||||
moderation:
|
||||
active: Active
|
||||
all: All
|
||||
|
@ -158,10 +164,14 @@ en:
|
|||
public: Public
|
||||
push_subscription_expires: PuSH subscription expires
|
||||
redownload: Refresh profile
|
||||
redownloaded_msg: Successfully refreshed %{username}'s profile from origin
|
||||
reject: Reject
|
||||
reject_all: Reject all
|
||||
rejected_msg: Successfully rejected %{username}'s sign-up application
|
||||
remove_avatar: Remove avatar
|
||||
remove_header: Remove header
|
||||
removed_avatar_msg: Successfully removed %{username}'s avatar image
|
||||
removed_header_msg: Successfully removed %{username}'s header image
|
||||
resend_confirmation:
|
||||
already_confirmed: This user is already confirmed
|
||||
send: Resend confirmation email
|
||||
|
@ -182,18 +192,23 @@ en:
|
|||
show:
|
||||
created_reports: Made reports
|
||||
targeted_reports: Reported by others
|
||||
silence: Silence
|
||||
silenced: Silenced
|
||||
silence: Limit
|
||||
silenced: Limited
|
||||
statuses: Statuses
|
||||
subscribe: Subscribe
|
||||
suspended: Suspended
|
||||
suspension_irreversible: The data of this account has been irreversibly deleted. You can unsuspend the account to make it usable but it will not recover any data it previously had.
|
||||
suspension_reversible_hint_html: The account has been suspended, and the data will be fully removed on %{date}. Until then, the account can be restored without any ill effects. If you wish to remove all of the account's data immediately, you can do so below.
|
||||
time_in_queue: Waiting in queue %{time}
|
||||
title: Accounts
|
||||
unconfirmed_email: Unconfirmed email
|
||||
undo_silenced: Undo silence
|
||||
undo_suspension: Undo suspension
|
||||
unsilenced_msg: Successfully unlimited %{username}'s account
|
||||
unsubscribe: Unsubscribe
|
||||
unsuspended_msg: Successfully unsuspended %{username}'s account
|
||||
username: Username
|
||||
view_domain: View summary for domain
|
||||
warn: Warn
|
||||
web: Web
|
||||
whitelisted: Allowed for federation
|
||||
|
@ -1304,9 +1319,9 @@ en:
|
|||
title: Sign in attempt
|
||||
warning:
|
||||
explanation:
|
||||
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
|
||||
silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
|
||||
suspend: Your account has been suspended, and all of your toots and your uploaded media files have been irreversibly removed from this server, and servers where you had followers.
|
||||
disable: You can no longer login to your account or use it in any other way, but your profile and other data remains intact.
|
||||
silence: You can still use your account but only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
|
||||
suspend: You can no longer use your account, and your profile and other data are no longer accessible. You can still login to request a backup of your data until the data is fully removed, but we will retain some data to prevent you from evading the suspension.
|
||||
get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}.
|
||||
review_server_policies: Review server policies
|
||||
statuses: 'Specifically, for:'
|
||||
|
|
|
@ -90,10 +90,10 @@ en:
|
|||
text: Custom warning
|
||||
type: Action
|
||||
types:
|
||||
disable: Disable login
|
||||
none: Do nothing
|
||||
silence: Silence
|
||||
suspend: Suspend and irreversibly delete account data
|
||||
disable: Freeze
|
||||
none: Send a warning
|
||||
silence: Limit
|
||||
suspend: Suspend
|
||||
warning_preset_id: Use a warning preset
|
||||
announcement:
|
||||
all_day: All-day event
|
||||
|
|
|
@ -232,7 +232,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :report_notes, only: [:create, :destroy]
|
||||
|
||||
resources :accounts, only: [:index, :show] do
|
||||
resources :accounts, only: [:index, :show, :destroy] do
|
||||
member do
|
||||
post :enable
|
||||
post :unsilence
|
||||
|
@ -466,7 +466,7 @@ Rails.application.routes.draw do
|
|||
end
|
||||
|
||||
namespace :admin do
|
||||
resources :accounts, only: [:index, :show] do
|
||||
resources :accounts, only: [:index, :show, :destroy] do
|
||||
member do
|
||||
post :enable
|
||||
post :unsilence
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
class CreateAccountDeletionRequests < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :account_deletion_requests do |t|
|
||||
t.references :account, foreign_key: { on_delete: :cascade }
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
10
db/schema.rb
10
db/schema.rb
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2020_06_30_190544) do
|
||||
ActiveRecord::Schema.define(version: 2020_09_08_193330) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -36,6 +36,13 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
|
|||
t.index ["conversation_id"], name: "index_account_conversations_on_conversation_id"
|
||||
end
|
||||
|
||||
create_table "account_deletion_requests", force: :cascade do |t|
|
||||
t.bigint "account_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_account_deletion_requests_on_account_id"
|
||||
end
|
||||
|
||||
create_table "account_domain_blocks", force: :cascade do |t|
|
||||
t.string "domain"
|
||||
t.datetime "created_at", null: false
|
||||
|
@ -926,6 +933,7 @@ ActiveRecord::Schema.define(version: 2020_06_30_190544) do
|
|||
add_foreign_key "account_aliases", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_conversations", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_conversations", "conversations", on_delete: :cascade
|
||||
add_foreign_key "account_deletion_requests", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_domain_blocks", "accounts", name: "fk_206c6029bd", on_delete: :cascade
|
||||
add_foreign_key "account_identity_proofs", "accounts", on_delete: :cascade
|
||||
add_foreign_key "account_migrations", "accounts", column: "target_account_id", on_delete: :nullify
|
||||
|
|
|
@ -87,7 +87,7 @@ module Mastodon
|
|||
say('Use --force to reattach it anyway and delete the other user')
|
||||
return
|
||||
elsif account.user.present?
|
||||
account.user.destroy!
|
||||
DeleteAccountService.new.call(account, reserve_email: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -192,7 +192,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
say("Deleting user with #{account.statuses_count} statuses, this might take a while...")
|
||||
SuspendAccountService.new.call(account, reserve_email: false)
|
||||
DeleteAccountService.new.call(account, reserve_email: false)
|
||||
say('OK', :green)
|
||||
end
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
processed, = parallelize_with_progress(scope) do |account|
|
||||
SuspendAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run]
|
||||
DeleteAccountService.new.call(account, reserve_username: false, skip_side_effects: true) unless options[:dry_run]
|
||||
end
|
||||
|
||||
DomainBlock.where(domain: domains).destroy_all unless options[:dry_run]
|
||||
|
|
|
@ -199,9 +199,10 @@ RSpec.describe Auth::RegistrationsController, type: :controller do
|
|||
end
|
||||
|
||||
subject do
|
||||
inviter = Fabricate(:user, confirmed_at: 2.days.ago)
|
||||
Setting.registrations_mode = 'approved'
|
||||
request.headers["Accept-Language"] = accept_language
|
||||
invite = Fabricate(:invite, max_uses: nil, expires_at: 1.hour.from_now)
|
||||
invite = Fabricate(:invite, user: inviter, max_uses: nil, expires_at: 1.hour.from_now)
|
||||
post :create, params: { user: { account_attributes: { username: 'test' }, email: 'test@example.com', password: '12345678', password_confirmation: '12345678', 'invite_code': invite.code, agreement: 'true' } }
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ require 'rails_helper'
|
|||
describe ApplicationController, type: :controller do
|
||||
controller do
|
||||
include ExportControllerConcern
|
||||
|
||||
def index
|
||||
send_export_file
|
||||
end
|
||||
|
|
3
spec/fabricators/account_deletion_request_fabricator.rb
Normal file
3
spec/fabricators/account_deletion_request_fabricator.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
Fabricator(:account_deletion_request) do
|
||||
account
|
||||
end
|
4
spec/models/account_deletion_request_spec.rb
Normal file
4
spec/models/account_deletion_request_spec.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountDeletionRequest, type: :model do
|
||||
end
|
|
@ -29,7 +29,7 @@ RSpec.describe Invite, type: :model do
|
|||
|
||||
it 'returns false when invite creator has been disabled' do
|
||||
invite = Fabricate(:invite, max_uses: nil, expires_at: nil)
|
||||
SuspendAccountService.new.call(invite.user.account)
|
||||
invite.user.account.suspend!
|
||||
expect(invite.valid_for_use?).to be false
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe SuspendAccountService, type: :service do
|
||||
RSpec.describe DeleteAccountService, type: :service do
|
||||
describe '#call on local account' do
|
||||
before do
|
||||
stub_request(:post, "https://alice.com/inbox").to_return(status: 201)
|
Loading…
Reference in a new issue