mirror of
https://github.com/mastodon/mastodon.git
synced 2026-08-28 04:54:55 -05:00
Add requested_deletion_at attribute to Account for deleted-but-not-suspended accounts (#23617)
This commit is contained in:
4
.github/workflows/test-migrations.yml
vendored
4
.github/workflows/test-migrations.yml
vendored
@@ -104,8 +104,8 @@ jobs:
|
||||
bin/rails db:create
|
||||
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails tests:migrations:prepare_database
|
||||
|
||||
# Migrate up to v4.2.0 breakpoint
|
||||
bin/rails db:migrate VERSION=20230907150100
|
||||
# Migrate up to v4.3.0 breakpoint
|
||||
bin/rails db:migrate VERSION=20241007071624
|
||||
|
||||
# Migrate the rest
|
||||
SKIP_POST_DEPLOYMENT_MIGRATIONS=true bin/rails db:migrate
|
||||
|
||||
@@ -13,4 +13,8 @@ class ActivityPub::BaseController < Api::BaseController
|
||||
def skip_temporary_suspension_response?
|
||||
false
|
||||
end
|
||||
|
||||
def skip_pending_deletion_response?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,6 +40,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
||||
true
|
||||
end
|
||||
|
||||
def skip_pending_deletion_response?
|
||||
true
|
||||
end
|
||||
|
||||
def body
|
||||
return @body if defined?(@body)
|
||||
|
||||
|
||||
9
app/controllers/api/v1/accounts/base_controller.rb
Normal file
9
app/controllers/api/v1/accounts/base_controller.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::BaseController < Api::BaseController
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.without_requested_deletion.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::EndorsementsController < Api::BaseController
|
||||
class Api::V1::Accounts::EndorsementsController < Api::V1::Accounts::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :index
|
||||
@@ -28,10 +28,6 @@ class Api::V1::Accounts::EndorsementsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_endorsed_accounts
|
||||
@endorsed_accounts = @account.unavailable? ? [] : paginated_endorsed_accounts
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
|
||||
class Api::V1::Accounts::FeaturedTagsController < Api::V1::Accounts::BaseController
|
||||
before_action :set_account
|
||||
before_action :set_featured_tags
|
||||
|
||||
@@ -12,10 +12,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_featured_tags
|
||||
@featured_tags = @account.unavailable? ? [] : @account.featured_tags
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
||||
class Api::V1::Accounts::FollowerAccountsController < Api::V1::Accounts::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
|
||||
before_action :set_account
|
||||
after_action :insert_pagination_headers
|
||||
@@ -13,10 +13,6 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
return [] if hide_results?
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
||||
class Api::V1::Accounts::FollowingAccountsController < Api::V1::Accounts::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:accounts' }
|
||||
before_action :set_account
|
||||
after_action :insert_pagination_headers
|
||||
@@ -13,10 +13,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def load_accounts
|
||||
return [] if hide_results?
|
||||
|
||||
|
||||
@@ -6,15 +6,8 @@ class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
||||
deprecate_api '2022-03-30'
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
def index
|
||||
render json: []
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::ListsController < Api::BaseController
|
||||
class Api::V1::Accounts::ListsController < Api::V1::Accounts::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:lists' }
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
@@ -9,10 +9,4 @@ class Api::V1::Accounts::ListsController < Api::BaseController
|
||||
@lists = @account.suspended? ? [] : @account.lists.where(account: current_account)
|
||||
render json: @lists, each_serializer: REST::ListSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,6 +13,7 @@ class Api::V1::Accounts::LookupController < Api::BaseController
|
||||
|
||||
def set_account
|
||||
@account = ResolveAccountService.new.call(params[:acct], skip_webfinger: true) || raise(ActiveRecord::RecordNotFound)
|
||||
raise ActiveRecord::RecordNotFound if @account.deleted?
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
raise(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::NotesController < Api::BaseController
|
||||
class Api::V1::Accounts::NotesController < Api::V1::Accounts::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }
|
||||
@@ -20,10 +20,6 @@ class Api::V1::Accounts::NotesController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def relationships_presenter
|
||||
AccountRelationshipsPresenter.new([@account], current_user.account_id)
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController
|
||||
before_action :require_user!
|
||||
|
||||
def index
|
||||
@accounts = Account.where(id: account_ids).select(:id, :domain)
|
||||
@accounts = Account.without_requested_deletion.where(id: account_ids).select(:id, :domain)
|
||||
@accounts.merge!(Account.without_suspended) unless truthy_param?(:with_suspended)
|
||||
render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships
|
||||
end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
class Api::V1::Accounts::StatusesController < Api::V1::Accounts::BaseController
|
||||
before_action -> { authorize_if_got_token! :read, :'read:statuses' }
|
||||
before_action :set_account
|
||||
|
||||
@@ -14,10 +14,6 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
end
|
||||
|
||||
def load_statuses
|
||||
@account.unavailable? ? [] : preloaded_account_statuses
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:id])
|
||||
@account = Account.without_requested_deletion.find(params[:id])
|
||||
end
|
||||
|
||||
def set_accounts
|
||||
|
||||
@@ -50,7 +50,7 @@ class Api::V1::CollectionItemsController < Api::BaseController
|
||||
def set_account
|
||||
return render(json: { error: '`account_id` parameter is missing' }, status: 422) if params[:account_id].blank?
|
||||
|
||||
@account = Account.find(params[:account_id])
|
||||
@account = Account.without_requested_deletion.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_collection_item
|
||||
|
||||
@@ -69,7 +69,7 @@ class Api::V1::CollectionsController < Api::BaseController
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
@account = Account.without_requested_deletion.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_collections
|
||||
|
||||
@@ -25,7 +25,7 @@ class Api::V1::InCollectionsController < Api::BaseController
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find(params[:account_id])
|
||||
@account = Account.without_requested_deletion.find(params[:account_id])
|
||||
end
|
||||
|
||||
def set_collections
|
||||
|
||||
@@ -40,7 +40,7 @@ module AccountOwnedConcern
|
||||
def check_account_suspension
|
||||
if @account.permanently_unavailable?
|
||||
permanent_unavailability_response
|
||||
elsif @account.suspended? && !skip_temporary_suspension_response?
|
||||
elsif (@account.suspended? && !skip_temporary_suspension_response?) || (@account.deleted? && !skip_pending_deletion_response?)
|
||||
temporary_suspension_response
|
||||
end
|
||||
end
|
||||
@@ -49,6 +49,10 @@ module AccountOwnedConcern
|
||||
false
|
||||
end
|
||||
|
||||
def skip_pending_deletion_response?
|
||||
false
|
||||
end
|
||||
|
||||
def permanent_unavailability_response
|
||||
expires_in(3.minutes, public: true)
|
||||
gone
|
||||
|
||||
@@ -37,7 +37,7 @@ class Settings::DeletesController < Settings::BaseController
|
||||
end
|
||||
|
||||
def destroy_account!
|
||||
current_account.suspend!(origin: :local, block_email: false)
|
||||
current_account.mark_deleted!
|
||||
AccountDeletionWorker.perform_async(current_user.account_id)
|
||||
sign_out
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
|
||||
target_collections = target_collections_by_account.fetch(target_account.id, [])
|
||||
replied_to_accounts = target_statuses.nil? ? [] : Account.local.where(id: target_statuses.filter_map(&:in_reply_to_account_id))
|
||||
|
||||
next if target_account.suspended? || (!target_account.local? && replied_to_accounts.none?)
|
||||
next if target_account.suspended? || target_account.permanently_deleted? || (!target_account.local? && replied_to_accounts.none?)
|
||||
|
||||
ReportService.new.call(
|
||||
@account,
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
# private_key :text
|
||||
# protocol :integer default("ostatus"), not null
|
||||
# public_key :text default(""), not null
|
||||
# requested_deletion_at :datetime
|
||||
# requested_review_at :datetime
|
||||
# reviewed_at :datetime
|
||||
# sensitized_at :datetime
|
||||
@@ -285,6 +286,21 @@ class Account < ApplicationRecord
|
||||
ResolveAccountService.new.call(acct) unless local?
|
||||
end
|
||||
|
||||
def deleted?
|
||||
requested_deletion_at.present? && !instance_actor?
|
||||
end
|
||||
|
||||
def permanently_deleted?
|
||||
deleted? && deletion_request.nil?
|
||||
end
|
||||
|
||||
def mark_deleted!(date: Time.now.utc)
|
||||
transaction do
|
||||
create_deletion_request!
|
||||
update!(requested_deletion_at: date)
|
||||
end
|
||||
end
|
||||
|
||||
def memorialize!
|
||||
update!(memorial: true)
|
||||
end
|
||||
|
||||
@@ -57,7 +57,7 @@ module Account::Search
|
||||
LEFT JOIN users ON accounts.id = users.account_id
|
||||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||||
WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||||
AND accounts.suspended_at IS NULL
|
||||
AND accounts.suspended_at IS NULL AND accounts.requested_deletion_at IS NULL
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
|
||||
ORDER BY rank DESC
|
||||
@@ -80,7 +80,7 @@ module Account::Search
|
||||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||||
WHERE accounts.id IN (SELECT * FROM first_degree)
|
||||
AND to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||||
AND accounts.suspended_at IS NULL
|
||||
AND accounts.suspended_at IS NULL AND accounts.requested_deletion_at IS NULL
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
GROUP BY accounts.id, s.id
|
||||
ORDER BY rank DESC
|
||||
@@ -98,7 +98,7 @@ module Account::Search
|
||||
LEFT JOIN users ON accounts.id = users.account_id
|
||||
LEFT JOIN account_stats AS s ON accounts.id = s.account_id
|
||||
WHERE to_tsquery('simple', :tsquery) @@ #{TEXT_SEARCH_RANKS}
|
||||
AND accounts.suspended_at IS NULL
|
||||
AND accounts.suspended_at IS NULL AND accounts.requested_deletion_at IS NULL
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
AND (accounts.domain IS NOT NULL OR (users.approved = TRUE AND users.confirmed_at IS NOT NULL))
|
||||
GROUP BY accounts.id, s.id
|
||||
|
||||
@@ -5,13 +5,13 @@ module Account::Suspensions
|
||||
|
||||
included do
|
||||
scope :suspended, -> { where.not(suspended_at: nil) }
|
||||
scope :without_suspended, -> { where(suspended_at: nil) }
|
||||
scope :without_requested_deletion, -> { where(requested_deletion_at: nil) }
|
||||
scope :without_suspended, -> { without_requested_deletion.where(suspended_at: nil) }
|
||||
end
|
||||
|
||||
def suspended?
|
||||
suspended_at.present? && !instance_actor?
|
||||
end
|
||||
alias unavailable? suspended?
|
||||
|
||||
def suspended_locally?
|
||||
suspended? && suspension_origin_local?
|
||||
@@ -20,12 +20,19 @@ module Account::Suspensions
|
||||
def suspended_permanently?
|
||||
suspended? && deletion_request.nil?
|
||||
end
|
||||
alias permanently_unavailable? suspended_permanently?
|
||||
|
||||
def suspended_temporarily?
|
||||
suspended? && deletion_request.present?
|
||||
end
|
||||
|
||||
def unavailable?
|
||||
deleted? || suspended?
|
||||
end
|
||||
|
||||
def permanently_unavailable?
|
||||
unavailable? && deletion_request.nil?
|
||||
end
|
||||
|
||||
def suspend!(date: Time.now.utc, origin: :local, block_email: true)
|
||||
transaction do
|
||||
create_deletion_request!
|
||||
|
||||
@@ -102,13 +102,13 @@ class User < ApplicationRecord
|
||||
validates :date_of_birth, presence: true, date_of_birth: true, on: :create, if: -> { Setting.min_age.present? && !bypass_registration_checks? }
|
||||
validate :validate_role_elevation
|
||||
|
||||
scope :account_not_suspended, -> { joins(:account).merge(Account.without_suspended) }
|
||||
scope :account_available, -> { joins(:account).merge(Account.without_suspended.without_requested_deletion) }
|
||||
scope :recent, -> { order(id: :desc) }
|
||||
scope :pending, -> { where(approved: false) }
|
||||
scope :approved, -> { where(approved: true) }
|
||||
scope :enabled, -> { where(disabled: false) }
|
||||
scope :disabled, -> { where(disabled: true) }
|
||||
scope :active, -> { confirmed.signed_in_recently.account_not_suspended }
|
||||
scope :active, -> { confirmed.signed_in_recently.account_available }
|
||||
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
|
||||
scope :matches_ip, ->(value) { left_joins(:ips).merge(UserIp.contained_by(value)).group(users: [:id]) }
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
||||
|
||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||
|
||||
attribute :suspended, if: :suspended?
|
||||
attribute :suspended, if: :unavailable?
|
||||
attribute :silenced, key: :limited, if: :silenced?
|
||||
attribute :noindex, if: :local?
|
||||
|
||||
@@ -164,7 +164,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
|
||||
object.user_prefers_noindex?
|
||||
end
|
||||
|
||||
delegate :suspended?, :silenced?, :local?, :memorial?, to: :object
|
||||
delegate :unavailable?, :silenced?, :local?, :memorial?, to: :object
|
||||
|
||||
def moved_and_not_nested?
|
||||
object.moved?
|
||||
|
||||
@@ -226,9 +226,14 @@ class DeleteAccountService < BaseService
|
||||
|
||||
return unless keep_account_record?
|
||||
|
||||
if @options[:suspended_at]
|
||||
@account.suspended_at = @options[:suspended_at]
|
||||
@account.suspension_origin = :local
|
||||
else
|
||||
@account.requested_deletion_at ||= Time.now.utc
|
||||
end
|
||||
|
||||
@account.silenced_at = nil
|
||||
@account.suspended_at = @options[:suspended_at] || Time.now.utc
|
||||
@account.suspension_origin = :local
|
||||
@account.locked = false
|
||||
@account.memorial = false
|
||||
@account.discoverable = false
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(account.id), method: :post, class: 'button' if can?(:redownload, account) && account.suspension_origin_remote?
|
||||
- if deletion_request.present? && can?(:destroy, account)
|
||||
= link_to t('admin.accounts.delete'), admin_account_path(account.id), method: :delete, class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure') }
|
||||
- elsif account.deleted?
|
||||
%hr.spacer/
|
||||
|
||||
%p.muted-hint= deletion_request.present? ? t('admin.accounts.deletion_reversible_hint_html', date: content_tag(:strong, l(deletion_request.due_at.to_date))) : t('admin.accounts.deletion_irreversible')
|
||||
- else
|
||||
.action-buttons
|
||||
%div
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
%div
|
||||
= link_to admin_action_logs_path(target_account_id: account.id) do
|
||||
.dashboard__counters__text
|
||||
- if account.local? && account.user.nil?
|
||||
- if account.suspended?
|
||||
= t('admin.accounts.suspended')
|
||||
- elsif account.deleted? || (account.local? && account.user.nil?)
|
||||
= t('admin.accounts.deleted')
|
||||
- elsif account.memorial?
|
||||
= t('admin.accounts.memorialized')
|
||||
- elsif account.suspended?
|
||||
= t('admin.accounts.suspended')
|
||||
- elsif account.silenced?
|
||||
= t('admin.accounts.silenced')
|
||||
- elsif account.local? && account.user_disabled?
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
%br/
|
||||
- if target_account.suspended?
|
||||
%span.red= t('admin.accounts.suspended')
|
||||
- elsif target_account.deleted?
|
||||
%span.red= t('admin.accounts.deleted')
|
||||
- elsif target_account.silenced?
|
||||
%span.red= t('admin.accounts.silenced')
|
||||
- elsif target_account.user_disabled?
|
||||
|
||||
@@ -36,7 +36,7 @@ class Scheduler::SelfDestructScheduler
|
||||
# deletion request.
|
||||
|
||||
# This targets accounts that have not been deleted nor marked for deletion yet
|
||||
Account.local.without_suspended.reorder(id: :asc).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
|
||||
Account.local.without_requested_deletion.reorder(id: :asc).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
|
||||
delete_account!(account)
|
||||
end
|
||||
|
||||
@@ -44,9 +44,8 @@ class Scheduler::SelfDestructScheduler
|
||||
|
||||
# This targets accounts that have been marked for deletion but have not been
|
||||
# deleted yet
|
||||
Account.local.suspended.joins(:deletion_request).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
|
||||
Account.local.joins(:deletion_request).take(MAX_ACCOUNT_DELETIONS_PER_JOB).each do |account|
|
||||
delete_account!(account)
|
||||
account.deletion_request&.destroy
|
||||
end
|
||||
end
|
||||
|
||||
@@ -64,8 +63,9 @@ class Scheduler::SelfDestructScheduler
|
||||
[json, account.id, inbox_url]
|
||||
end
|
||||
|
||||
# Do not call `Account#suspend!` because we don't want to issue a deletion request
|
||||
account.update!(suspended_at: Time.now.utc, suspension_origin: :local)
|
||||
# Do not call `Account#mark_deleted!` because we don't want to issue a deletion request
|
||||
account.update!(requested_deletion_at: Time.now.utc)
|
||||
account.deletion_request&.destroy
|
||||
end
|
||||
|
||||
def deletion_payload(account)
|
||||
|
||||
@@ -64,6 +64,8 @@ en:
|
||||
custom: Custom
|
||||
delete: Delete data
|
||||
deleted: Deleted
|
||||
deletion_irreversible: The data of this account has been irreversibly deleted.
|
||||
deletion_reversible_hint_html: This account is pending deletion, and the data will be fully removed on %{date}.
|
||||
demote: Demote
|
||||
destroyed_msg: "%{username}'s data is now queued to be deleted imminently"
|
||||
disable: Freeze
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddRequestedDeletionAtToAccounts < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :accounts, :requested_deletion_at, :datetime
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UpdateAccountSummariesToVersion3 < ActiveRecord::Migration[8.0]
|
||||
def up
|
||||
reapplication_global_follow_recommendations_v1 do
|
||||
drop_view :account_summaries, materialized: true
|
||||
create_view :account_summaries, version: 3, materialized: { no_data: true }
|
||||
safety_assured { add_index :account_summaries, :account_id, unique: true }
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
reapplication_global_follow_recommendations_v1 do
|
||||
drop_view :account_summaries, materialized: true
|
||||
create_view :account_summaries, version: 2, materialized: { no_data: true }
|
||||
safety_assured { add_index :account_summaries, :account_id, unique: true }
|
||||
end
|
||||
end
|
||||
|
||||
def reapplication_global_follow_recommendations_v1
|
||||
drop_view :global_follow_recommendations, materialized: true
|
||||
yield
|
||||
create_view :global_follow_recommendations, version: 1, materialized: { no_data: true }
|
||||
safety_assured { add_index :global_follow_recommendations, :account_id, unique: true }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,29 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class BackfillAccountRequestedDeletionAt < ActiveRecord::Migration[8.1]
|
||||
def up
|
||||
safety_assured do
|
||||
execute <<~SQL
|
||||
UPDATE
|
||||
accounts
|
||||
SET
|
||||
requested_deletion_at = suspended_at, suspended_at = NULL, suspension_origin = NULL
|
||||
WHERE
|
||||
-- rule out remote accounts, as deleted remote accounts are deleted locally
|
||||
domain IS NULL
|
||||
-- only care about accounts marked as suspended (which prior to this migration applies to both suspended and self-deleted accounts)
|
||||
AND suspended_at IS NOT NULL
|
||||
-- rule accounts that were not self-deleted (might still include suspended but self-deleted accounts)
|
||||
AND NOT EXISTS (SELECT 1 FROM users WHERE account_id = accounts.id) AND NOT EXISTS (SELECT 1 FROM canonical_email_blocks WHERE reference_account_id = accounts.id)
|
||||
-- rule out accounts that have a canonical email block: sign they were suspended and not unsuspended
|
||||
AND NOT EXISTS (SELECT 1 FROM canonical_email_blocks WHERE reference_account_id = accounts.id)
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
safety_assured do
|
||||
execute 'UPDATE accounts SET suspended_at = requested_deletion_at WHERE domain is NULL AND suspended_at IS NULL AND requested_deletion_at IS NOT NULL'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_07_20_124731) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_07_28_145507) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
@@ -192,6 +192,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_20_124731) do
|
||||
t.text "private_key"
|
||||
t.integer "protocol", default: 0, null: false
|
||||
t.text "public_key", default: "", null: false
|
||||
t.datetime "requested_deletion_at"
|
||||
t.datetime "requested_review_at", precision: nil
|
||||
t.datetime "reviewed_at", precision: nil
|
||||
t.datetime "sensitized_at", precision: nil
|
||||
@@ -1632,10 +1633,9 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_20_124731) do
|
||||
WHERE ((statuses.account_id = accounts.id) AND (statuses.deleted_at IS NULL) AND (statuses.reblog_of_id IS NULL))
|
||||
ORDER BY statuses.id DESC
|
||||
LIMIT 20) t0)
|
||||
WHERE ((accounts.suspended_at IS NULL) AND (accounts.silenced_at IS NULL) AND (accounts.moved_to_account_id IS NULL) AND (accounts.discoverable = true) AND (accounts.locked = false))
|
||||
WHERE ((accounts.suspended_at IS NULL) AND (accounts.requested_deletion_at IS NULL) AND (accounts.silenced_at IS NULL) AND (accounts.moved_to_account_id IS NULL) AND (accounts.discoverable = true) AND (accounts.locked = false))
|
||||
GROUP BY accounts.id;
|
||||
SQL
|
||||
add_index "account_summaries", ["account_id", "language", "sensitive"], name: "idx_on_account_id_language_sensitive_250461e1eb"
|
||||
add_index "account_summaries", ["account_id"], name: "index_account_summaries_on_account_id", unique: true
|
||||
|
||||
create_view "global_follow_recommendations", materialized: true, sql_definition: <<-SQL
|
||||
|
||||
24
db/views/account_summaries_v03.sql
Normal file
24
db/views/account_summaries_v03.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
SELECT
|
||||
accounts.id AS account_id,
|
||||
mode() WITHIN GROUP (ORDER BY language ASC) AS language,
|
||||
mode() WITHIN GROUP (ORDER BY sensitive ASC) AS sensitive
|
||||
FROM accounts
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT
|
||||
statuses.account_id,
|
||||
statuses.language,
|
||||
statuses.sensitive
|
||||
FROM statuses
|
||||
WHERE statuses.account_id = accounts.id
|
||||
AND statuses.deleted_at IS NULL
|
||||
AND statuses.reblog_of_id IS NULL
|
||||
ORDER BY statuses.id DESC
|
||||
LIMIT 20
|
||||
) t0
|
||||
WHERE accounts.suspended_at IS NULL
|
||||
AND accounts.requested_deletion_at IS NULL
|
||||
AND accounts.silenced_at IS NULL
|
||||
AND accounts.moved_to_account_id IS NULL
|
||||
AND accounts.discoverable = 't'
|
||||
AND accounts.locked = 'f'
|
||||
GROUP BY accounts.id
|
||||
@@ -66,9 +66,9 @@ namespace :db do
|
||||
abort 'This version of Mastodon requires PostgreSQL 14.0 or newer. Please update PostgreSQL before updating Mastodon.' if pg_version < 140_000
|
||||
|
||||
schema_version = ActiveRecord::Migrator.current_version
|
||||
abort <<~MESSAGE if ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] && schema_version < 2023_09_07_150100
|
||||
Zero-downtime migrations from Mastodon versions earlier than 4.2.0 are not supported.
|
||||
Please update to Mastodon 4.2.x first or upgrade by stopping all services and running migrations without `SKIP_POST_DEPLOYMENT_MIGRATIONS`.
|
||||
abort <<~MESSAGE if ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] && schema_version < 2024_10_07_071624
|
||||
Zero-downtime migrations from Mastodon versions earlier than 4.3.0 are not supported.
|
||||
Please update to Mastodon 4.3.x first or upgrade by stopping all services and running migrations without `SKIP_POST_DEPLOYMENT_MIGRATIONS`.
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
|
||||
@@ -117,6 +117,21 @@ namespace :tests do
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless Account.find_by(username: 'suspended', domain: nil).suspended?
|
||||
puts 'Unexpected value for Account#suspended? for user @suspended'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if Account.find_by(username: 'deleted', domain: nil).suspended?
|
||||
puts 'Unexpected value for Account#suspended? for user @deleted'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless Account.find_by(username: 'deleted', domain: nil).deleted?
|
||||
puts 'Unexpected value for Account#deleted? for user @deleted'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless Identity.where(provider: 'foo', uid: 0).one?
|
||||
puts 'Identities not deduplicated as expected'
|
||||
exit(1)
|
||||
@@ -226,11 +241,11 @@ namespace :tests do
|
||||
INSERT INTO "accounts"
|
||||
(id, username, domain, uri, private_key, public_key, created_at, updated_at, last_webfingered_at)
|
||||
VALUES
|
||||
(12, 'alice', 'social.example.com', 'https://social.example.com/alice', NULL, #{user_public_key}, '2021-01-01'::date, now(), '2021-01-09'::date),
|
||||
(13, 'alice', 'example.com', 'https://social.example.com/alice', NULL, #{user_public_key}, '2021-01-10'::date, now(), '2021-01-11'::date),
|
||||
(14, 'bogus1', 'example.com', '', NULL, '', now(), now(), now()),
|
||||
(15, 'bogus2', 'example.com', '', NULL, '', now(), now(), now()),
|
||||
(16, 'bogus3', 'example.com', '', NULL, '', now(), now(), now());
|
||||
(14, 'alice', 'social.example.com', 'https://social.example.com/alice', NULL, #{user_public_key}, '2021-01-01'::date, now(), '2021-01-09'::date),
|
||||
(15, 'alice', 'example.com', 'https://social.example.com/alice', NULL, #{user_public_key}, '2021-01-10'::date, now(), '2021-01-11'::date),
|
||||
(16, 'bogus1', 'example.com', '', NULL, '', now(), now(), now()),
|
||||
(17, 'bogus2', 'example.com', '', NULL, '', now(), now(), now()),
|
||||
(18, 'bogus3', 'example.com', '', NULL, '', now(), now(), now());
|
||||
SQL
|
||||
end
|
||||
|
||||
@@ -291,20 +306,20 @@ namespace :tests do
|
||||
INSERT INTO "accounts"
|
||||
(id, username, domain, private_key, public_key, created_at, updated_at)
|
||||
VALUES
|
||||
(10, 'kmruser', NULL, #{user_private_key}, #{user_public_key}, now(), now()),
|
||||
(11, 'qcuser', NULL, #{user_private_key}, #{user_public_key}, now(), now());
|
||||
(12, 'kmruser', NULL, #{user_private_key}, #{user_public_key}, now(), now()),
|
||||
(13, 'qcuser', NULL, #{user_private_key}, #{user_public_key}, now(), now());
|
||||
|
||||
INSERT INTO "users"
|
||||
(id, account_id, email, created_at, updated_at, admin, locale, chosen_languages)
|
||||
VALUES
|
||||
(4, 10, 'kmruser@localhost', now(), now(), false, 'ku', '{en,kmr,ku,ckb}');
|
||||
(5, 12, 'kmruser@localhost', now(), now(), false, 'ku', '{en,kmr,ku,ckb}');
|
||||
|
||||
INSERT INTO "users"
|
||||
(id, account_id, email, created_at, updated_at, locale,
|
||||
encrypted_otp_secret, encrypted_otp_secret_iv, encrypted_otp_secret_salt,
|
||||
otp_required_for_login)
|
||||
VALUES
|
||||
(5, 11, 'qcuser@localhost', now(), now(), 'fr-QC',
|
||||
(6, 13, 'qcuser@localhost', now(), now(), 'fr-QC',
|
||||
E'Fttsy7QAa0edaDfdfSz094rRLAxc8cJweDQ4BsWH/zozcdVA8o9GLqcKhn2b\nGi/V\n',
|
||||
'rys3THICkr60BoWC',
|
||||
'_LMkAGvdg7a+sDIKjI3mR2Q==',
|
||||
@@ -313,7 +328,7 @@ namespace :tests do
|
||||
INSERT INTO "settings"
|
||||
(id, thing_type, thing_id, var, value, created_at, updated_at)
|
||||
VALUES
|
||||
(5, 'User', 4, 'default_language', E'--- kmr\n', now(), now()),
|
||||
(5, 'User', 5, 'default_language', E'--- kmr\n', now(), now()),
|
||||
(6, 'User', 1, 'interactions', E'--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nmust_be_follower: false\nmust_be_following: true\nmust_be_following_dm: false\n', now(), now());
|
||||
|
||||
INSERT INTO "identities"
|
||||
@@ -390,18 +405,25 @@ namespace :tests do
|
||||
'https://activitypub.com/users/evil/inbox', 'https://activitypub.com/users/evil/outbox',
|
||||
'https://activitypub.com/users/evil/followers', true);
|
||||
|
||||
INSERT INTO "accounts"
|
||||
(id, username, domain, private_key, public_key, created_at, updated_at, suspended)
|
||||
VALUES
|
||||
(10, 'suspended', NULL, #{admin_private_key}, #{admin_public_key}, now(), now(), true),
|
||||
(11, 'deleted', NULL, #{user_private_key}, #{user_public_key}, now(), now(), true);
|
||||
|
||||
-- users
|
||||
|
||||
INSERT INTO "users"
|
||||
(id, account_id, email, created_at, updated_at, admin)
|
||||
VALUES
|
||||
(1, 1, 'admin@localhost', now(), now(), true),
|
||||
(2, 2, 'user@localhost', now(), now(), false);
|
||||
(2, 2, 'user@localhost', now(), now(), false),
|
||||
(3, 10, 'suspended@localhost', now(), now(), false);
|
||||
|
||||
INSERT INTO "users"
|
||||
(id, account_id, email, created_at, updated_at, admin, locale)
|
||||
VALUES
|
||||
(3, 8, 'ptuser@localhost', now(), now(), false, 'pt');
|
||||
(4, 8, 'ptuser@localhost', now(), now(), false, 'pt');
|
||||
|
||||
-- conversations
|
||||
INSERT INTO "conversations" (id, created_at, updated_at) VALUES (1, now(), now());
|
||||
|
||||
@@ -54,7 +54,7 @@ RSpec.describe ApplicationController do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'before_action :check_suspension' do
|
||||
describe 'before_action :require_functional!' do
|
||||
before do
|
||||
routes.draw { get 'success' => 'anonymous#success' }
|
||||
end
|
||||
@@ -64,17 +64,29 @@ RSpec.describe ApplicationController do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'does nothing if user who signed in is not suspended' do
|
||||
it 'does nothing if user who signed in is functional' do
|
||||
sign_in(Fabricate(:account, suspended: false).user)
|
||||
get 'success'
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'redirects to account status page' do
|
||||
it 'redirects to account status page if the account is suspended' do
|
||||
sign_in(Fabricate(:account, suspended: true).user)
|
||||
get 'success'
|
||||
expect(response).to redirect_to(edit_user_registration_path)
|
||||
end
|
||||
|
||||
it 'redirects to account status page if the account is pending deletion' do
|
||||
sign_in(Fabricate(:account, requested_deletion: true).user)
|
||||
get 'success'
|
||||
expect(response).to redirect_to(edit_user_registration_path)
|
||||
end
|
||||
|
||||
it 'redirects to auth setup page if the account is unconfirmed' do
|
||||
sign_in(Fabricate(:user, confirmed_at: nil))
|
||||
get 'success'
|
||||
expect(response).to redirect_to(auth_setup_path)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'raise_not_found' do
|
||||
|
||||
@@ -141,6 +141,15 @@ RSpec.describe Auth::RegistrationsController do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when deleted' do
|
||||
let(:user) { Fabricate(:user, account_attributes: { username: 'test', requested_deletion_at: Time.now.utc }) }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
put :update
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
|
||||
@@ -48,6 +48,19 @@ RSpec.describe Auth::SessionsController do
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a deleted user' do
|
||||
before do
|
||||
user.account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'redirects to home after sign out' do
|
||||
sign_in(user, scope: :user)
|
||||
delete :destroy
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST #create' do
|
||||
|
||||
@@ -33,7 +33,7 @@ RSpec.describe AccountControllerConcern do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is suspended' do
|
||||
context 'when account is permanently suspended' do
|
||||
it 'returns http gone' do
|
||||
account = Fabricate(:account, suspended: true)
|
||||
get 'success', params: { account_username: account.username }
|
||||
@@ -41,14 +41,32 @@ RSpec.describe AccountControllerConcern do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is deleted by owner' do
|
||||
context 'when account is temporarily suspended' do
|
||||
it 'returns http forbidden' do
|
||||
account = Fabricate(:account)
|
||||
account.suspend!
|
||||
get 'success', params: { account_username: account.username }
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
it 'returns http gone' do
|
||||
account = Fabricate(:account, suspended: true, user: nil)
|
||||
account = Fabricate(:account, requested_deletion: true)
|
||||
get 'success', params: { account_username: account.username }
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
it 'returns http forbidden' do
|
||||
account = Fabricate(:account)
|
||||
account.mark_deleted!
|
||||
get 'success', params: { account_username: account.username }
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is not suspended' do
|
||||
let(:account) { Fabricate(:account, username: 'username') }
|
||||
|
||||
|
||||
@@ -36,6 +36,27 @@ RSpec.describe FollowingAccountsController do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
before do
|
||||
alice.mark_deleted!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
before do
|
||||
alice.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when format is json' do
|
||||
@@ -108,6 +129,27 @@ RSpec.describe FollowingAccountsController do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
before do
|
||||
alice.mark_deleted!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
before do
|
||||
alice.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'without page' do
|
||||
@@ -161,6 +203,27 @@ RSpec.describe FollowingAccountsController do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
before do
|
||||
alice.mark_deleted!
|
||||
alice.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
before do
|
||||
alice.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,17 +3,18 @@
|
||||
require_relative '../support/signing_keys_helpers'
|
||||
|
||||
Fabricator(:account) do
|
||||
transient :suspended, :silenced, :legacy_keypair
|
||||
username { sequence(:username) { |i| "#{Faker::Internet.user_name(separators: %w(_))}#{i}" } }
|
||||
last_webfingered_at { Time.now.utc }
|
||||
public_key { |attrs| attrs[:legacy_keypair] ? SigningKeysHelpers::PUBLIC_RSA_TEST_KEY : '' }
|
||||
private_key { |attrs| attrs[:legacy_keypair] && attrs[:domain].nil? ? SigningKeysHelpers::PRIVATE_RSA_TEST_KEY : nil }
|
||||
suspended_at { |attrs| attrs[:suspended] ? Time.now.utc : nil }
|
||||
silenced_at { |attrs| attrs[:silenced] ? Time.now.utc : nil }
|
||||
user { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil }
|
||||
uri { |attrs| attrs[:domain].nil? ? nil : "https://#{attrs[:domain]}/users/#{attrs[:username]}" }
|
||||
discoverable true
|
||||
indexable true
|
||||
transient :suspended, :silenced, :legacy_keypair, :requested_deletion
|
||||
username { sequence(:username) { |i| "#{Faker::Internet.user_name(separators: %w(_))}#{i}" } }
|
||||
last_webfingered_at { Time.now.utc }
|
||||
public_key { |attrs| attrs[:legacy_keypair] ? SigningKeysHelpers::PUBLIC_RSA_TEST_KEY : '' }
|
||||
private_key { |attrs| attrs[:legacy_keypair] && attrs[:domain].nil? ? SigningKeysHelpers::PRIVATE_RSA_TEST_KEY : nil }
|
||||
suspended_at { |attrs| attrs[:suspended] ? Time.now.utc : nil }
|
||||
silenced_at { |attrs| attrs[:silenced] ? Time.now.utc : nil }
|
||||
requested_deletion_at { |attrs| attrs[:requested_deletion] ? Time.now.utc : nil }
|
||||
user { |attrs| attrs[:domain].nil? ? Fabricate.build(:user, account: nil) : nil }
|
||||
uri { |attrs| attrs[:domain].nil? ? nil : "https://#{attrs[:domain]}/users/#{attrs[:username]}" }
|
||||
discoverable true
|
||||
indexable true
|
||||
|
||||
# This is not strictly needed but this avoids generating multiple keys
|
||||
# and, when `store_private_key` is passed, stores private keys for use in request specs
|
||||
|
||||
@@ -74,16 +74,19 @@ RSpec.describe User do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'account_not_suspended' do
|
||||
describe 'account_available' do
|
||||
it 'returns with linked accounts that are not suspended' do
|
||||
suspended_account = Fabricate(:account, suspended_at: 10.days.ago)
|
||||
non_suspended_account = Fabricate(:account, suspended_at: nil)
|
||||
suspended_user = Fabricate(:user, account: suspended_account)
|
||||
non_suspended_user = Fabricate(:user, account: non_suspended_account)
|
||||
deleted_account = Fabricate(:account, requested_deletion_at: 10.days.ago)
|
||||
deleted_user = Fabricate(:user, account: deleted_account)
|
||||
|
||||
expect(described_class.account_not_suspended)
|
||||
expect(described_class.account_available)
|
||||
.to include(non_suspended_user)
|
||||
.and not_include(suspended_user)
|
||||
.and not_include(deleted_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -62,6 +62,33 @@ RSpec.describe 'Accounts show response' do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'permanently deleted account check' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns appropriate http response code' do
|
||||
{ html: 410, json: 410, rss: 410 }.each do |format, code|
|
||||
get short_account_path(username: account.username), as: format
|
||||
|
||||
expect(response).to have_http_status(code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'pending deletion account check' do
|
||||
before { account.mark_deleted! }
|
||||
|
||||
it 'returns appropriate http response code' do
|
||||
{ html: 403, json: 403, rss: 403 }.each do |format, code|
|
||||
get short_account_path(username: account.username), as: format
|
||||
|
||||
expect(response).to have_http_status(code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET to short username paths' do
|
||||
context 'with existing statuses' do
|
||||
context 'with HTML' do
|
||||
|
||||
@@ -62,6 +62,33 @@ RSpec.describe 'ActivityPub Collections' do
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with signature' do
|
||||
|
||||
@@ -62,6 +62,33 @@ RSpec.describe 'ActivityPub Inboxes' do
|
||||
.to have_http_status(202)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http accepted' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(202)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -61,6 +61,33 @@ RSpec.describe 'ActivityPub Outboxes' do
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with page requested' do
|
||||
@@ -110,6 +137,33 @@ RSpec.describe 'ActivityPub Outboxes' do
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -77,6 +77,37 @@ RSpec.describe 'ActivityPub Replies' do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
before do
|
||||
status.account.mark_deleted!
|
||||
status.account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is pending deletion' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
before do
|
||||
status.account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
subject
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status is public' do
|
||||
let(:parent_visibility) { :public }
|
||||
|
||||
|
||||
@@ -66,5 +66,30 @@ RSpec.describe 'API V1 Accounts FollowerAccounts' do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when request account is permanently deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when request account is pending deletion' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get "/api/v1/accounts/#{account.id}/followers", params: { limit: 2 }, headers: headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,5 +66,30 @@ RSpec.describe 'API V1 Accounts FollowingAccounts' do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when request account is permanently deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when request account is pending deletion' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get "/api/v1/accounts/#{account.id}/following", params: { limit: 2 }, headers: headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,4 +22,29 @@ RSpec.describe 'Accounts Lists API' do
|
||||
.to start_with('application/json')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requested account is permanently deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get "/api/v1/accounts/#{account.id}/lists", headers: headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requested account is pending deletion' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get "/api/v1/accounts/#{account.id}/lists", headers: headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -152,5 +152,34 @@ RSpec.describe 'API V1 Accounts Statuses' do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requested account is permanently deleted' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get "/api/v1/accounts/#{account.id}/statuses", params: { limit: 2 }, headers: headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requested account is pending deletion' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
get "/api/v1/accounts/#{account.id}/statuses", params: { limit: 2 }, headers: headers
|
||||
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,6 +48,31 @@ RSpec.describe '/api/v1/accounts' do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requesting a permanently deleted account' do
|
||||
let(:other_account) { Fabricate(:account, requested_deletion: true) }
|
||||
|
||||
before do
|
||||
get "/api/v1/accounts/#{other_account.id}"
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requesting an account pending deletion' do
|
||||
let(:other_account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
other_account.mark_deleted!
|
||||
get "/api/v1/accounts/#{other_account.id}"
|
||||
end
|
||||
|
||||
it 'returns http not found' do
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when logged in' do
|
||||
subject do
|
||||
get "/api/v1/accounts/#{account.id}", headers: headers
|
||||
|
||||
@@ -26,6 +26,17 @@ RSpec.describe 'Settings Deletes' do
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when already deleted' do
|
||||
let(:user) { Fabricate(:user, account_attributes: { requested_deletion_at: Time.now.utc }) }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
delete settings_delete_path
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
@@ -54,6 +65,17 @@ RSpec.describe 'Settings Deletes' do
|
||||
.to include('private, no-store')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when already deleted' do
|
||||
let(:user) { Fabricate(:user, account_attributes: { requested_deletion_at: Time.now.utc }) }
|
||||
|
||||
it 'returns http forbidden' do
|
||||
get settings_delete_path
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not signed in' do
|
||||
|
||||
@@ -33,6 +33,33 @@ RSpec.describe 'Statuses' do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is permanently deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
account.deletion_request.destroy
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
get "/@#{account.username}/#{status.id}"
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is temporarily deleted' do
|
||||
before do
|
||||
account.mark_deleted!
|
||||
end
|
||||
|
||||
it 'returns http forbidden' do
|
||||
get "/@#{account.username}/#{status.id}"
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(403)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status is a reblog' do
|
||||
let(:original_account) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') }
|
||||
|
||||
@@ -53,7 +53,18 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
|
||||
it_behaves_like 'a successful response'
|
||||
end
|
||||
|
||||
context 'when an account is permanently suspended or deleted' do
|
||||
context 'when an account is pending deletion' do
|
||||
let(:resource) { alice.to_webfinger_s }
|
||||
|
||||
before do
|
||||
alice.mark_deleted!
|
||||
perform_request!
|
||||
end
|
||||
|
||||
it_behaves_like 'a successful response'
|
||||
end
|
||||
|
||||
context 'when an account is permanently suspended' do
|
||||
let(:resource) { alice.to_webfinger_s }
|
||||
|
||||
before do
|
||||
@@ -67,6 +78,20 @@ RSpec.describe 'The /.well-known/webfinger endpoint' do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an account is permanently deleted' do
|
||||
let(:resource) { alice.to_webfinger_s }
|
||||
|
||||
before do
|
||||
alice.mark_deleted!
|
||||
alice.deletion_request.destroy
|
||||
perform_request!
|
||||
end
|
||||
|
||||
it 'returns http gone' do
|
||||
expect(response).to have_http_status(410)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an account is not found' do
|
||||
let(:resource) { 'acct:not@existing.com' }
|
||||
|
||||
|
||||
@@ -78,6 +78,15 @@ RSpec.describe AccountSearchService do
|
||||
expect(results).to eq [partial]
|
||||
end
|
||||
|
||||
it 'returns the fuzzy match first, and does not return deleted exacts' do
|
||||
partial = Fabricate(:account, username: 'exactness')
|
||||
Fabricate(:account, username: 'exact', requested_deletion: true)
|
||||
results = subject.call('exact', nil, limit: 10)
|
||||
|
||||
expect(results.size).to eq 1
|
||||
expect(results).to eq [partial]
|
||||
end
|
||||
|
||||
it 'does not return suspended remote accounts' do
|
||||
Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', suspended: true)
|
||||
results = subject.call('a@example.com', nil, limit: 2)
|
||||
@@ -85,6 +94,14 @@ RSpec.describe AccountSearchService do
|
||||
expect(results.size).to eq 0
|
||||
expect(results).to eq []
|
||||
end
|
||||
|
||||
it 'does not return deleted remote accounts' do
|
||||
Fabricate(:account, username: 'a', domain: 'remote', display_name: 'e', requested_deletion: true)
|
||||
results = subject.call('a@example.com', nil, limit: 2)
|
||||
|
||||
expect(results.size).to eq 0
|
||||
expect(results).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context 'when elasticsearch is enabled', :search do
|
||||
|
||||
@@ -50,6 +50,11 @@ RSpec.describe NotifyService do
|
||||
expect { subject }.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
it 'does not notify when recipient is deleted' do
|
||||
recipient.mark_deleted!
|
||||
expect { subject }.to_not change(Notification, :count)
|
||||
end
|
||||
|
||||
describe 'reblogs' do
|
||||
let(:status) { Fabricate(:status, account: Fabricate(:account)) }
|
||||
let(:activity) { Fabricate(:status, account: sender, reblog: status) }
|
||||
|
||||
@@ -27,10 +27,8 @@ RSpec.describe 'Settings Deletes' do
|
||||
.to have_text(I18n.t('deletes.success_msg'))
|
||||
expect(page)
|
||||
.to have_title(I18n.t('auth.login'))
|
||||
expect(User.find_by(id: user.id))
|
||||
.to be_nil
|
||||
expect(user.account.reload)
|
||||
.to be_suspended
|
||||
.to be_deleted
|
||||
expect(CanonicalEmailBlock.block?(user.email))
|
||||
.to be(false)
|
||||
end
|
||||
|
||||
@@ -41,14 +41,14 @@ RSpec.describe Scheduler::SelfDestructScheduler do
|
||||
context 'when sidekiq is operational' do
|
||||
let!(:other_account) { Fabricate :account, inbox_url: 'https://host.example/inbox', domain: 'host.example', protocol: :activitypub }
|
||||
|
||||
it 'suspends local non-suspended accounts' do
|
||||
it 'deletes local non-deleted accounts' do
|
||||
worker.perform
|
||||
|
||||
expect(account.reload.suspended_at).to_not be_nil
|
||||
expect(account.reload.requested_deletion_at).to_not be_nil
|
||||
end
|
||||
|
||||
it 'suspends local suspended accounts marked for deletion' do
|
||||
account.update(suspended_at: 10.days.ago)
|
||||
it 'deletes local accounts marked for deletion' do
|
||||
account.update(requested_deletion_at: 10.days.ago)
|
||||
deletion_request = Fabricate(:account_deletion_request, account: account)
|
||||
|
||||
worker.perform
|
||||
@@ -56,7 +56,7 @@ RSpec.describe Scheduler::SelfDestructScheduler do
|
||||
expect(ActivityPub::DeliveryWorker)
|
||||
.to have_enqueued_sidekiq_job(match_json_values(type: 'Delete', signature: be_present), account.id, other_account.inbox_url)
|
||||
|
||||
expect(account.reload.suspended_at).to be > 1.day.ago
|
||||
expect(account.reload.requested_deletion_at).to be > 1.day.ago
|
||||
expect { deletion_request.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user