Record account suspend/silence time and keep track of domain blocks (#10660)

* Record account suspend/silence time and keep track of domain blocks

* Also unblock users who were suspended/silenced before dates were recorded

* Add tests

* Keep track of suspending date for users suspended through the CLI

* Show accurate number of accounts that would be affected by unsuspending an instance

* Change migration to set silenced_at and suspended_at

* Revert "Also unblock users who were suspended/silenced before dates were recorded"

This reverts commit a015c65d2d1e28c7b7cfab8b3f8cd5fb48b8b71c.

* Switch from using suspended and silenced to suspended_at and silenced_at

* Add post-deployment migration script to remove `suspended` and `silenced` columns

* Use Account#silence! and Account#suspend! instead of updating the underlying property

* Add silenced_at and suspended_at migration to post-migration

* Change account fabricator to translate suspended and silenced attributes

* Minor fixes

* Make unblocking domains always retroactive
This commit is contained in:
ThibG 2019-05-14 19:05:02 +02:00 committed by Eugen Rochko
parent 564106c5d6
commit 14f6ce2885
30 changed files with 226 additions and 115 deletions

View file

@ -41,7 +41,7 @@ module Admin
def destroy def destroy
authorize @domain_block, :destroy? authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?) UnblockDomainService.new.call(@domain_block)
log_action :destroy, @domain_block log_action :destroy, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg') redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
end end
@ -53,11 +53,7 @@ module Admin
end end
def resource_params def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :retroactive) params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
end
def retroactive_unblock?
ActiveRecord::Type.lookup(:boolean).cast(resource_params[:retroactive])
end end
end end
end end

View file

@ -58,7 +58,7 @@ class HomeController < ApplicationController
if request.path.start_with?('/web') if request.path.start_with?('/web')
new_user_session_path new_user_session_path
elsif single_user_mode? elsif single_user_mode?
short_account_path(Account.local.where(suspended: false).first) short_account_path(Account.local.without_suspended.first)
else else
about_path about_path
end end

View file

@ -28,8 +28,6 @@
# header_updated_at :datetime # header_updated_at :datetime
# avatar_remote_url :string # avatar_remote_url :string
# subscription_expires_at :datetime # subscription_expires_at :datetime
# silenced :boolean default(FALSE), not null
# suspended :boolean default(FALSE), not null
# locked :boolean default(FALSE), not null # locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null # header_remote_url :string default(""), not null
# last_webfingered_at :datetime # last_webfingered_at :datetime
@ -45,6 +43,8 @@
# actor_type :string # actor_type :string
# discoverable :boolean # discoverable :boolean
# also_known_as :string is an Array # also_known_as :string is an Array
# silenced_at :datetime
# suspended_at :datetime
# #
class Account < ApplicationRecord class Account < ApplicationRecord
@ -82,10 +82,10 @@ class Account < ApplicationRecord
scope :local, -> { where(domain: nil) } scope :local, -> { where(domain: nil) }
scope :expiring, ->(time) { remote.where.not(subscription_expires_at: nil).where('subscription_expires_at < ?', time) } scope :expiring, ->(time) { remote.where.not(subscription_expires_at: nil).where('subscription_expires_at < ?', time) }
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) } scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }
scope :silenced, -> { where(silenced: true) } scope :silenced, -> { where.not(silenced_at: nil) }
scope :suspended, -> { where(suspended: true) } scope :suspended, -> { where.not(suspended_at: nil) }
scope :without_suspended, -> { where(suspended: false) } scope :without_suspended, -> { where(suspended_at: nil) }
scope :without_silenced, -> { where(silenced: false) } scope :without_silenced, -> { where(silenced_at: nil) }
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) } scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :alphabetic, -> { order(domain: :asc, username: :asc) } scope :alphabetic, -> { order(domain: :asc, username: :asc) }
@ -165,25 +165,35 @@ class Account < ApplicationRecord
ResolveAccountService.new.call(acct) ResolveAccountService.new.call(acct)
end end
def silence! def silenced?
update!(silenced: true) silenced_at.present?
end
def silence!(date = nil)
date ||= Time.now.utc
update!(silenced_at: date)
end end
def unsilence! def unsilence!
update!(silenced: false) update!(silenced_at: nil)
end end
def suspend! def suspended?
suspended_at.present?
end
def suspend!(date = nil)
date ||= Time.now.utc
transaction do transaction do
user&.disable! if local? user&.disable! if local?
update!(suspended: true) update!(suspended_at: date)
end end
end end
def unsuspend! def unsuspend!
transaction do transaction do
user&.enable! if local? user&.enable! if local?
update!(suspended: false) update!(suspended_at: nil)
end end
end end
@ -399,7 +409,7 @@ class Account < ApplicationRecord
ts_rank_cd(#{textsearch}, #{query}, 32) AS rank ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
FROM accounts FROM accounts
WHERE #{query} @@ #{textsearch} WHERE #{query} @@ #{textsearch}
AND accounts.suspended = false AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL AND accounts.moved_to_account_id IS NULL
ORDER BY rank DESC ORDER BY rank DESC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
@ -427,7 +437,7 @@ class Account < ApplicationRecord
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?) LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
WHERE accounts.id IN (SELECT * FROM first_degree) WHERE accounts.id IN (SELECT * FROM first_degree)
AND #{query} @@ #{textsearch} AND #{query} @@ #{textsearch}
AND accounts.suspended = false AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL AND accounts.moved_to_account_id IS NULL
GROUP BY accounts.id GROUP BY accounts.id
ORDER BY rank DESC ORDER BY rank DESC
@ -443,7 +453,7 @@ class Account < ApplicationRecord
FROM accounts FROM accounts
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?) LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
WHERE #{query} @@ #{textsearch} WHERE #{query} @@ #{textsearch}
AND accounts.suspended = false AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL AND accounts.moved_to_account_id IS NULL
GROUP BY accounts.id GROUP BY accounts.id
ORDER BY rank DESC ORDER BY rank DESC

View file

@ -13,7 +13,7 @@ module AccountFinderConcern
end end
def representative def representative
find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) || Account.local.find_by(suspended: false) find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) || Account.local.without_suspended.first
end end
def find_local(username) def find_local(username)

View file

@ -17,8 +17,6 @@ class DomainBlock < ApplicationRecord
enum severity: [:silence, :suspend, :noop] enum severity: [:silence, :suspend, :noop]
attr_accessor :retroactive
validates :domain, presence: true, uniqueness: true validates :domain, presence: true, uniqueness: true
has_many :accounts, foreign_key: :domain, primary_key: :domain has_many :accounts, foreign_key: :domain, primary_key: :domain
@ -36,4 +34,9 @@ class DomainBlock < ApplicationRecord
return false if other_block.silence? && noop? return false if other_block.silence? && noop?
(reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports) (reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports)
end end
def affected_accounts_count
scope = suspend? ? accounts.where(suspended_at: created_at) : accounts.where(silenced_at: created_at)
scope.count
end
end end

View file

@ -84,8 +84,8 @@ class Status < ApplicationRecord
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
scope :with_public_visibility, -> { where(visibility: :public) } scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) } scope :tagged_with, ->(tag) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag }) }
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: false }) } scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) } scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) }
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) } scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) } scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
scope :tagged_with_all, ->(tags) { scope :tagged_with_all, ->(tags) {

View file

@ -88,7 +88,7 @@ class User < ApplicationRecord
scope :confirmed, -> { where.not(confirmed_at: nil) } scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :enabled, -> { where(disabled: false) } scope :enabled, -> { where(disabled: false) }
scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) } scope :inactive, -> { where(arel_table[:current_sign_in_at].lt(ACTIVE_DURATION.ago)) }
scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where(accounts: { suspended: false }) } scope :active, -> { confirmed.where(arel_table[:current_sign_in_at].gteq(ACTIVE_DURATION.ago)).joins(:account).where.not(accounts: { suspended_at: nil }) }
scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) } scope :matches_email, ->(value) { where(arel_table[:email].matches("#{value}%")) }
scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) } scope :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }

View file

@ -50,12 +50,12 @@ class ActivityPub::ProcessAccountService < BaseService
def create_account def create_account
@account = Account.new @account = Account.new
@account.protocol = :activitypub @account.protocol = :activitypub
@account.username = @username @account.username = @username
@account.domain = @domain @account.domain = @domain
@account.suspended = true if auto_suspend? @account.private_key = nil
@account.silenced = true if auto_silence? @account.suspended_at = domain_block.created_at if auto_suspend?
@account.private_key = nil @account.silenced_at = domain_block.created_at if auto_silence?
end end
def update_account def update_account

View file

@ -29,7 +29,7 @@ class BlockDomainService < BaseService
end end
def silence_accounts! def silence_accounts!
blocked_domain_accounts.in_batches.update_all(silenced: true) blocked_domain_accounts.without_silenced.in_batches.update_all(silenced_at: @domain_block.created_at)
end end
def clear_media! def clear_media!
@ -43,9 +43,9 @@ class BlockDomainService < BaseService
end end
def suspend_accounts! def suspend_accounts!
blocked_domain_accounts.where(suspended: false).reorder(nil).find_each do |account| blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account|
UnsubscribeService.new.call(account) if account.subscribed? UnsubscribeService.new.call(account) if account.subscribed?
SuspendAccountService.new.call(account) SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at)
end end
end end

View file

@ -49,7 +49,7 @@ class PostStatusService < BaseService
def preprocess_attributes! def preprocess_attributes!
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@visibility = @options[:visibility] || @account.user&.setting_default_privacy @visibility = @options[:visibility] || @account.user&.setting_default_privacy
@visibility = :unlisted if @visibility == :public && @account.silenced @visibility = :unlisted if @visibility == :public && @account.silenced?
@scheduled_at = @options[:scheduled_at]&.to_datetime @scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past? @scheduled_at = nil if scheduled_in_the_past?
rescue ArgumentError rescue ArgumentError

View file

@ -25,7 +25,7 @@ class ProcessMentionsService < BaseService
end end
end end
next match if mention_undeliverable?(mentioned_account) || mentioned_account&.suspended next match if mention_undeliverable?(mentioned_account) || mentioned_account&.suspended?
mentions << mentioned_account.mentions.where(status: status).first_or_create(status: status) mentions << mentioned_account.mentions.where(status: status).first_or_create(status: status)

View file

@ -119,9 +119,9 @@ class ResolveAccountService < BaseService
Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}" Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"
@account = Account.new(username: @username, domain: @domain) @account = Account.new(username: @username, domain: @domain)
@account.suspended = true if auto_suspend? @account.suspended_at = domain_block.created_at if auto_suspend?
@account.silenced = true if auto_silence? @account.silenced_at = domain_block.created_at if auto_silence?
@account.private_key = nil @account.private_key = nil
end end
def update_account def update_account

View file

@ -43,7 +43,7 @@ class SubscribeService < BaseService
end end
def some_local_account def some_local_account
@some_local_account ||= Account.local.where(suspended: false).first @some_local_account ||= Account.local.without_suspended.first
end end
# Any response in the 3xx or 4xx range, except for 429 (rate limit) # Any response in the 3xx or 4xx range, except for 429 (rate limit)

View file

@ -88,8 +88,8 @@ class SuspendAccountService < BaseService
return if @options[:destroy] return if @options[:destroy]
@account.silenced = false @account.silenced_at = nil
@account.suspended = true @account.suspended_at = @options[:suspended_at] || Time.now.utc
@account.locked = false @account.locked = false
@account.display_name = '' @account.display_name = ''
@account.note = '' @account.note = ''

View file

@ -3,9 +3,9 @@
class UnblockDomainService < BaseService class UnblockDomainService < BaseService
attr_accessor :domain_block attr_accessor :domain_block
def call(domain_block, retroactive) def call(domain_block)
@domain_block = domain_block @domain_block = domain_block
process_retroactive_updates if retroactive process_retroactive_updates
domain_block.destroy domain_block.destroy
end end
@ -14,14 +14,19 @@ class UnblockDomainService < BaseService
end end
def blocked_accounts def blocked_accounts
Account.where(domain: domain_block.domain) scope = Account.where(domain: domain_block.domain)
if domain_block.silence?
scope.where(silenced_at: @domain_block.created_at)
else
scope.where(suspended_at: @domain_block.created_at)
end
end end
def update_options def update_options
{ domain_block_impact => false } { domain_block_impact => nil }
end end
def domain_block_impact def domain_block_impact
domain_block.silence? ? :silenced : :suspended domain_block.silence? ? :silenced_at : :suspended_at
end end
end end

View file

@ -3,18 +3,11 @@
= simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f| = simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f|
- if (@domain_block.noop?) - unless (@domain_block.noop?)
= f.input :retroactive, %p= t(".retroactive.#{@domain_block.severity}")
as: :hidden, %p.hint= t(:affected_accounts,
input_html: { :value => "0" } scope: [:admin, :domain_blocks, :show],
- else count: @domain_block.affected_accounts_count)
= f.input :retroactive,
as: :boolean,
wrapper: :with_label,
label: t(".retroactive.#{@domain_block.severity}"),
hint: t(:affected_accounts,
scope: [:admin, :domain_blocks, :show],
count: @domain_block.accounts_count)
.actions .actions
= f.button :button, t('.undo'), type: :submit = f.button :button, t('.undo'), type: :submit

View file

@ -293,8 +293,8 @@ en:
one: One account in the database affected one: One account in the database affected
other: "%{count} accounts in the database affected" other: "%{count} accounts in the database affected"
retroactive: retroactive:
silence: Unsilence all existing accounts from this domain silence: Unsilence existing affected accounts from this domain
suspend: Unsuspend all existing accounts from this domain suspend: Unsuspend existing affected accounts from this domain
title: Undo domain block for %{domain} title: Undo domain block for %{domain}
undo: Undo undo: Undo
undo: Undo domain block undo: Undo domain block

View file

@ -0,0 +1,41 @@
class AddSilencedAtSuspendedAtToAccounts < ActiveRecord::Migration[5.2]
class Account < ApplicationRecord
# Dummy class, to make migration possible across version changes
end
class DomainBlock < ApplicationRecord
# Dummy class, to make migration possible across version changes
enum severity: [:silence, :suspend, :noop]
has_many :accounts, foreign_key: :domain, primary_key: :domain
end
def up
add_column :accounts, :silenced_at, :datetime
add_column :accounts, :suspended_at, :datetime
# Record suspend date of blocks and silences for users whose limitations match
# a domain block
DomainBlock.where(severity: [:silence, :suspend]).find_each do |block|
scope = block.accounts
if block.suspend?
block.accounts.where(suspended: true).in_batches.update_all(suspended_at: block.created_at)
else
block.accounts.where(silenced: true).in_batches.update_all(silenced_at: block.created_at)
end
end
# Set dates for accounts which have limitations not related to a domain block
Account.where(suspended: true, suspended_at: nil).in_batches.update_all(suspended_at: Time.now.utc)
Account.where(silenced: true, silenced_at: nil).in_batches.update_all(silenced_at: Time.now.utc)
end
def down
# Block or silence accounts that have a date set
Account.where(suspended: false).where.not(suspended_at: nil).in_batches.update_all(suspended: true)
Account.where(silenced: false).where.not(silenced_at: nil).in_batches.update_all(silenced: true)
remove_column :accounts, :silenced_at
remove_column :accounts, :suspended_at
end
end

View file

@ -0,0 +1,45 @@
# frozen_string_literal: true
class RemoveSuspendedSilencedAccountFields < ActiveRecord::Migration[5.2]
class Account < ApplicationRecord
# Dummy class, to make migration possible across version changes
end
class DomainBlock < ApplicationRecord
# Dummy class, to make migration possible across version changes
enum severity: [:silence, :suspend, :noop]
has_many :accounts, foreign_key: :domain, primary_key: :domain
end
disable_ddl_transaction!
def up
# Record suspend date of blocks and silences for users whose limitations match
# a domain block
DomainBlock.where(severity: [:silence, :suspend]).find_each do |block|
scope = block.accounts
if block.suspend?
block.accounts.where(suspended: true).in_batches.update_all(suspended_at: block.created_at)
else
block.accounts.where(silenced: true).in_batches.update_all(silenced_at: block.created_at)
end
end
# Set dates for accounts which have limitations not related to a domain block
Account.where(suspended: true, suspended_at: nil).in_batches.update_all(suspended_at: Time.now.utc)
Account.where(silenced: true, silenced_at: nil).in_batches.update_all(silenced_at: Time.now.utc)
safety_assured do
remove_column :accounts, :suspended, :boolean, null: false, default: false
remove_column :accounts, :silenced, :boolean, null: false, default: false
end
end
def down
safety_assured do
add_column :accounts, :suspended, :boolean, null: false, default: false
add_column :accounts, :silenced, :boolean, null: false, default: false
end
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_05_09_164208) do ActiveRecord::Schema.define(version: 2019_05_11_152737) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -131,8 +131,6 @@ ActiveRecord::Schema.define(version: 2019_05_09_164208) do
t.datetime "header_updated_at" t.datetime "header_updated_at"
t.string "avatar_remote_url" t.string "avatar_remote_url"
t.datetime "subscription_expires_at" t.datetime "subscription_expires_at"
t.boolean "silenced", default: false, null: false
t.boolean "suspended", default: false, null: false
t.boolean "locked", default: false, null: false t.boolean "locked", default: false, null: false
t.string "header_remote_url", default: "", null: false t.string "header_remote_url", default: "", null: false
t.datetime "last_webfingered_at" t.datetime "last_webfingered_at"
@ -148,6 +146,8 @@ ActiveRecord::Schema.define(version: 2019_05_09_164208) do
t.string "actor_type" t.string "actor_type"
t.boolean "discoverable" t.boolean "discoverable"
t.string "also_known_as", array: true t.string "also_known_as", array: true
t.datetime "silenced_at"
t.datetime "suspended_at"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id" t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"

View file

@ -106,7 +106,7 @@ module Mastodon
[json, account.id, inbox_url] [json, account.id, inbox_url]
end end
account.update_column(:suspended, true) account.suspend!
end end
processed += 1 processed += 1

View file

@ -87,8 +87,8 @@ module Mastodon
end end
end end
account.suspended = false account.suspended_at = nil
user.account = account user.account = account
if user.save if user.save
if options[:confirmed] if options[:confirmed]

View file

@ -63,9 +63,9 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do
service = double(call: true) service = double(call: true)
allow(UnblockDomainService).to receive(:new).and_return(service) allow(UnblockDomainService).to receive(:new).and_return(service)
domain_block = Fabricate(:domain_block) domain_block = Fabricate(:domain_block)
delete :destroy, params: { id: domain_block.id, domain_block: { retroactive: '1' } } delete :destroy, params: { id: domain_block.id }
expect(service).to have_received(:call).with(domain_block, true) expect(service).to have_received(:call).with(domain_block)
expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.destroyed_msg') expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.destroyed_msg')
expect(response).to redirect_to(admin_instances_path(limited: '1')) expect(response).to redirect_to(admin_instances_path(limited: '1'))
end end

View file

@ -3,8 +3,11 @@ public_key = keypair.public_key.to_pem
private_key = keypair.to_pem private_key = keypair.to_pem
Fabricator(:account) do Fabricator(:account) do
transient :suspended, :silenced
username { sequence(:username) { |i| "#{Faker::Internet.user_name(nil, %w(_))}#{i}" } } username { sequence(:username) { |i| "#{Faker::Internet.user_name(nil, %w(_))}#{i}" } }
last_webfingered_at { Time.now.utc } last_webfingered_at { Time.now.utc }
public_key { public_key } public_key { public_key }
private_key { private_key } private_key { private_key }
suspended_at { |attrs| attrs[:suspended] ? Time.now.utc : nil }
silenced_at { |attrs| attrs[:silenced] ? Time.now.utc : nil }
end end

View file

@ -168,13 +168,13 @@ RSpec.describe FeedManager do
it 'returns true for status by silenced account who recipient is not following' do it 'returns true for status by silenced account who recipient is not following' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
alice.update(silenced: true) alice.silence!
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be true
end end
it 'returns false for status by followed silenced account' do it 'returns false for status by followed silenced account' do
status = Fabricate(:status, text: 'Hello world', account: alice) status = Fabricate(:status, text: 'Hello world', account: alice)
alice.update(silenced: true) alice.silence!
bob.follow!(alice) bob.follow!(alice)
expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false expect(FeedManager.instance.filter?(:mentions, status, bob.id)).to be false
end end

View file

@ -15,7 +15,7 @@ describe StatusFilter do
context 'when status account is silenced' do context 'when status account is silenced' do
before do before do
status.account.update(silenced: true) status.account.silence!
end end
it { is_expected.to be_filtered } it { is_expected.to be_filtered }
@ -65,7 +65,7 @@ describe StatusFilter do
context 'when status account is silenced' do context 'when status account is silenced' do
before do before do
status.account.update(silenced: true) status.account.silence!
end end
it { is_expected.to be_filtered } it { is_expected.to be_filtered }

View file

@ -35,7 +35,7 @@ describe StatusThreadingConcern do
end end
it 'does not return conversation history from silenced and not followed users' do it 'does not return conversation history from silenced and not followed users' do
jeff.update(silenced: true) jeff.silence!
expect(reply3.ancestors(4, viewer)).to_not include(reply1) expect(reply3.ancestors(4, viewer)).to_not include(reply1)
end end
@ -110,7 +110,7 @@ describe StatusThreadingConcern do
end end
it 'does not return replies from silenced and not followed users' do it 'does not return replies from silenced and not followed users' do
jeff.update(silenced: true) jeff.silence!
expect(status.descendants(4, viewer)).to_not include(reply3) expect(status.descendants(4, viewer)).to_not include(reply3)
end end

View file

@ -1,20 +1,14 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe BlockDomainService, type: :service do RSpec.describe BlockDomainService, type: :service do
let(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') } let!(:bad_account) { Fabricate(:account, username: 'badguy666', domain: 'evil.org') }
let(:bad_status1) { Fabricate(:status, account: bad_account, text: 'You suck') } let!(:bad_status1) { Fabricate(:status, account: bad_account, text: 'You suck') }
let(:bad_status2) { Fabricate(:status, account: bad_account, text: 'Hahaha') } let!(:bad_status2) { Fabricate(:status, account: bad_account, text: 'Hahaha') }
let(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status2, file: attachment_fixture('attachment.jpg')) } let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status2, file: attachment_fixture('attachment.jpg')) }
let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) }
subject { BlockDomainService.new } subject { BlockDomainService.new }
before do
bad_account
bad_status1
bad_status2
bad_attachment
end
describe 'for a suspension' do describe 'for a suspension' do
before do before do
subject.call(DomainBlock.create!(domain: 'evil.org', severity: :suspend)) subject.call(DomainBlock.create!(domain: 'evil.org', severity: :suspend))
@ -28,6 +22,18 @@ RSpec.describe BlockDomainService, type: :service do
expect(Account.find_remote('badguy666', 'evil.org').suspended?).to be true expect(Account.find_remote('badguy666', 'evil.org').suspended?).to be true
end end
it 'records suspension date appropriately' do
expect(Account.find_remote('badguy666', 'evil.org').suspended_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
end
it 'keeps already-banned accounts banned' do
expect(Account.find_remote('badguy', 'evil.org').suspended?).to be true
end
it 'does not overwrite suspension date of already-banned accounts' do
expect(Account.find_remote('badguy', 'evil.org').suspended_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
end
it 'removes the remote accounts\'s statuses and media attachments' do it 'removes the remote accounts\'s statuses and media attachments' do
expect { bad_status1.reload }.to raise_exception ActiveRecord::RecordNotFound expect { bad_status1.reload }.to raise_exception ActiveRecord::RecordNotFound
expect { bad_status2.reload }.to raise_exception ActiveRecord::RecordNotFound expect { bad_status2.reload }.to raise_exception ActiveRecord::RecordNotFound
@ -48,6 +54,18 @@ RSpec.describe BlockDomainService, type: :service do
expect(Account.find_remote('badguy666', 'evil.org').silenced?).to be true expect(Account.find_remote('badguy666', 'evil.org').silenced?).to be true
end end
it 'records suspension date appropriately' do
expect(Account.find_remote('badguy666', 'evil.org').silenced_at).to eq DomainBlock.find_by(domain: 'evil.org').created_at
end
it 'keeps already-banned accounts banned' do
expect(Account.find_remote('badguy', 'evil.org').silenced?).to be true
end
it 'does not overwrite suspension date of already-banned accounts' do
expect(Account.find_remote('badguy', 'evil.org').silenced_at).to_not eq DomainBlock.find_by(domain: 'evil.org').created_at
end
it 'leaves the domains status and attachements, but clears media' do it 'leaves the domains status and attachements, but clears media' do
expect { bad_status1.reload }.not_to raise_error expect { bad_status1.reload }.not_to raise_error
expect { bad_status2.reload }.not_to raise_error expect { bad_status2.reload }.not_to raise_error

View file

@ -39,12 +39,12 @@ RSpec.describe NotifyService, type: :service do
end end
it 'does not notify when sender is silenced and not followed' do it 'does not notify when sender is silenced and not followed' do
sender.update(silenced: true) sender.silence!
is_expected.to_not change(Notification, :count) is_expected.to_not change(Notification, :count)
end end
it 'does not notify when recipient is suspended' do it 'does not notify when recipient is suspended' do
recipient.update(suspended: true) recipient.suspend!
is_expected.to_not change(Notification, :count) is_expected.to_not change(Notification, :count)
end end

View file

@ -7,36 +7,33 @@ describe UnblockDomainService, type: :service do
describe 'call' do describe 'call' do
before do before do
@silenced = Fabricate(:account, domain: 'example.com', silenced: true) @independently_suspended = Fabricate(:account, domain: 'example.com', suspended_at: 1.hour.ago)
@suspended = Fabricate(:account, domain: 'example.com', suspended: true) @independently_silenced = Fabricate(:account, domain: 'example.com', silenced_at: 1.hour.ago)
@domain_block = Fabricate(:domain_block, domain: 'example.com') @domain_block = Fabricate(:domain_block, domain: 'example.com')
@silenced = Fabricate(:account, domain: 'example.com', silenced_at: @domain_block.created_at)
@suspended = Fabricate(:account, domain: 'example.com', suspended_at: @domain_block.created_at)
end end
context 'without retroactive' do it 'unsilences accounts and removes block' do
it 'removes the domain block' do @domain_block.update(severity: :silence)
subject.call(@domain_block, false)
expect_deleted_domain_block subject.call(@domain_block)
end expect_deleted_domain_block
expect(@silenced.reload.silenced?).to be false
expect(@suspended.reload.suspended?).to be true
expect(@independently_suspended.reload.suspended?).to be true
expect(@independently_silenced.reload.silenced?).to be true
end end
context 'with retroactive' do it 'unsuspends accounts and removes block' do
it 'unsilences accounts and removes block' do @domain_block.update(severity: :suspend)
@domain_block.update(severity: :silence)
subject.call(@domain_block, true) subject.call(@domain_block)
expect_deleted_domain_block expect_deleted_domain_block
expect(@silenced.reload.silenced).to be false expect(@suspended.reload.suspended?).to be false
expect(@suspended.reload.suspended).to be true expect(@silenced.reload.silenced?).to be true
end expect(@independently_suspended.reload.suspended?).to be true
expect(@independently_silenced.reload.silenced?).to be true
it 'unsuspends accounts and removes block' do
@domain_block.update(severity: :suspend)
subject.call(@domain_block, true)
expect_deleted_domain_block
expect(@suspended.reload.suspended).to be false
expect(@silenced.reload.silenced).to be true
end
end end
end end