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]
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
@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
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
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
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
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
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
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
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
@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)
@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)
@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)
@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)
@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)
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
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
@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,
@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')
@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')
@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')
@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')
@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')
@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')
@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')
@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')
@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')
@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')
@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')
@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')
@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,
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?
--- /dev/null
+# 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
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?
# 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
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
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
end
def active_for_authentication?
- true
+ !account.memorial?
end
def suspicious_sign_in?(ip)
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?
staff? && !record.user&.staff?
end
+ def destroy?
+ record.suspended? && record.deletion_request.present? && admin?
+ end
+
def unsuspend?
staff?
end
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
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
--- /dev/null
+# 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
# 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?
-
- ActivityPub::DeliveryWorker.push_bulk(Follow.where(account: @account)) do |follow|
- [build_reject_json(follow), follow.target_account_id, follow.account.inbox_url]
- end
+ def suspend!
+ @account.suspend! unless @account.suspended?
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_home_timelines!
+ @account.followers_for_local_distribution.find_each do |follower|
+ FeedManager.instance.unmerge_from_timeline(@account, follower)
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])
+ def unmerge_from_list_timelines!
+ @account.lists_for_local_distribution.find_each do |list|
+ FeedManager.instance.unmerge_from_list(@account, list)
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]
+ def privatize_media_attachments!
+ attachment_names = MediaAttachment.attachment_definitions.keys
- @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
+ @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
- 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
--- /dev/null
+# 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
= 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?
= 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
%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)
+
+ - if @account.suspended?
+ %hr.spacer/
+
+ %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')
+
+ = link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
- %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.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.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)
-
- - unless @account.local?
- - if DomainBlock.rule_for(@account.domain)
- = link_to t('admin.domain_blocks.view'), admin_instance_path(@account.domain), class: 'button'
+ - 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)
+
+ - 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/
--- /dev/null
+# 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
--- /dev/null
+# 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
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
--- /dev/null
+# 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
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
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
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
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
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
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
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:'
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
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
end
namespace :admin do
- resources :accounts, only: [:index, :show] do
+ resources :accounts, only: [:index, :show, :destroy] do
member do
post :enable
post :unsilence
--- /dev/null
+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
#
# 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"
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
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
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
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
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]
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
describe ApplicationController, type: :controller do
controller do
include ExportControllerConcern
+
def index
send_export_file
end
--- /dev/null
+Fabricator(:account_deletion_request) do
+ account
+end
--- /dev/null
+require 'rails_helper'
+
+RSpec.describe AccountDeletionRequest, type: :model do
+end
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
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)