Skip to content

Commit

Permalink
Record account suspend/silence time and keep track of domain blocks (m…
Browse files Browse the repository at this point in the history
…astodon#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 a015c65.

* 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
  • Loading branch information
ClearlyClaire authored and hiyuki2578 committed Oct 2, 2019
1 parent 4b6f590 commit f661255
Show file tree
Hide file tree
Showing 30 changed files with 226 additions and 115 deletions.
8 changes: 2 additions & 6 deletions app/controllers/admin/domain_blocks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def show

def destroy
authorize @domain_block, :destroy?
UnblockDomainService.new.call(@domain_block, retroactive_unblock?)
UnblockDomainService.new.call(@domain_block)
log_action :destroy, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.destroyed_msg')
end
Expand All @@ -53,11 +53,7 @@ def set_domain_block
end

def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :retroactive)
end

def retroactive_unblock?
ActiveRecord::Type.lookup(:boolean).cast(resource_params[:retroactive])
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
end
end
end
2 changes: 1 addition & 1 deletion app/controllers/home_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def default_redirect_path
if request.path.start_with?('/web')
new_user_session_path
elsif single_user_mode?
short_account_path(Account.local.where(suspended: false).first)
short_account_path(Account.local.without_suspended.first)
else
about_path
end
Expand Down
40 changes: 25 additions & 15 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
# header_updated_at :datetime
# avatar_remote_url :string
# subscription_expires_at :datetime
# silenced :boolean default(FALSE), not null
# suspended :boolean default(FALSE), not null
# locked :boolean default(FALSE), not null
# header_remote_url :string default(""), not null
# last_webfingered_at :datetime
Expand All @@ -46,6 +44,8 @@
# cat :boolean default(FALSE), not null
# discoverable :boolean
# also_known_as :string is an Array
# silenced_at :datetime
# suspended_at :datetime
#

class Account < ApplicationRecord
Expand Down Expand Up @@ -83,10 +83,10 @@ class Account < ApplicationRecord
scope :local, -> { where(domain: nil) }
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 :silenced, -> { where(silenced: true) }
scope :suspended, -> { where(suspended: true) }
scope :without_suspended, -> { where(suspended: false) }
scope :without_silenced, -> { where(silenced: false) }
scope :silenced, -> { where.not(silenced_at: nil) }
scope :suspended, -> { where.not(suspended_at: nil) }
scope :without_suspended, -> { where(suspended_at: nil) }
scope :without_silenced, -> { where(silenced_at: nil) }
scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
Expand Down Expand Up @@ -166,25 +166,35 @@ def refresh!
ResolveAccountService.new.call(acct)
end

def silence!
update!(silenced: true)
def silenced?
silenced_at.present?
end

def silence!(date = nil)
date ||= Time.now.utc
update!(silenced_at: date)
end

def unsilence!
update!(silenced: false)
update!(silenced_at: nil)
end

def suspended?
suspended_at.present?
end

def suspend!
def suspend!(date = nil)
date ||= Time.now.utc
transaction do
user&.disable! if local?
update!(suspended: true)
update!(suspended_at: date)
end
end

def unsuspend!
transaction do
user&.enable! if local?
update!(suspended: false)
update!(suspended_at: nil)
end
end

Expand Down Expand Up @@ -400,7 +410,7 @@ def search_for(terms, limit = 10, offset = 0)
ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
FROM accounts
WHERE #{query} @@ #{textsearch}
AND accounts.suspended = false
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
ORDER BY rank DESC
LIMIT ? OFFSET ?
Expand Down Expand Up @@ -428,7 +438,7 @@ def advanced_search_for(terms, account, limit = 10, following = false, offset =
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)
AND #{query} @@ #{textsearch}
AND accounts.suspended = false
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
GROUP BY accounts.id
ORDER BY rank DESC
Expand All @@ -444,7 +454,7 @@ def advanced_search_for(terms, account, limit = 10, following = false, offset =
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 = ?)
WHERE #{query} @@ #{textsearch}
AND accounts.suspended = false
AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL
GROUP BY accounts.id
ORDER BY rank DESC
Expand Down
2 changes: 1 addition & 1 deletion app/models/concerns/account_finder_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def find_remote!(username, domain)
end

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

def find_local(username)
Expand Down
7 changes: 5 additions & 2 deletions app/models/domain_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ class DomainBlock < ApplicationRecord

enum severity: [:silence, :suspend, :noop]

attr_accessor :retroactive

validates :domain, presence: true, uniqueness: true

has_many :accounts, foreign_key: :domain, primary_key: :domain
Expand All @@ -36,4 +34,9 @@ def stricter_than?(other_block)
return false if other_block.silence? && noop?
(reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports)
end

def affected_accounts_count
scope = suspend? ? accounts.where(suspended_at: created_at) : accounts.where(silenced_at: created_at)
scope.count
end
end
4 changes: 2 additions & 2 deletions app/models/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ class Status < ApplicationRecord
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
scope :with_public_visibility, -> { where(visibility: :public) }
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 :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) }
scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) }
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_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) {
Expand Down
2 changes: 1 addition & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class User < ApplicationRecord
scope :confirmed, -> { where.not(confirmed_at: nil) }
scope :enabled, -> { where(disabled: false) }
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 :emailable, -> { confirmed.enabled.joins(:account).merge(Account.searchable) }

Expand Down
12 changes: 6 additions & 6 deletions app/services/activitypub/process_account_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ def call(username, domain, json, options = {})

def create_account
@account = Account.new
@account.protocol = :activitypub
@account.username = @username
@account.domain = @domain
@account.suspended = true if auto_suspend?
@account.silenced = true if auto_silence?
@account.private_key = nil
@account.protocol = :activitypub
@account.username = @username
@account.domain = @domain
@account.private_key = nil
@account.suspended_at = domain_block.created_at if auto_suspend?
@account.silenced_at = domain_block.created_at if auto_silence?
end

def update_account
Expand Down
6 changes: 3 additions & 3 deletions app/services/block_domain_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def invalidate_association_caches!
end

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

def clear_media!
Expand All @@ -43,9 +43,9 @@ def clear_media!
end

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?
SuspendAccountService.new.call(account)
SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at)
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/services/post_status_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def call(account, options = {})
def preprocess_attributes!
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@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 = nil if scheduled_in_the_past?
rescue ArgumentError
Expand Down
2 changes: 1 addition & 1 deletion app/services/process_mentions_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def call(status)
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)

Expand Down
6 changes: 3 additions & 3 deletions app/services/resolve_account_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ def create_account
Rails.logger.debug "Creating new remote account for #{@username}@#{@domain}"

@account = Account.new(username: @username, domain: @domain)
@account.suspended = true if auto_suspend?
@account.silenced = true if auto_silence?
@account.private_key = nil
@account.suspended_at = domain_block.created_at if auto_suspend?
@account.silenced_at = domain_block.created_at if auto_silence?
@account.private_key = nil
end

def update_account
Expand Down
2 changes: 1 addition & 1 deletion app/services/subscribe_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def subscription_params
end

def some_local_account
@some_local_account ||= Account.local.where(suspended: false).first
@some_local_account ||= Account.local.without_suspended.first
end

# Any response in the 3xx or 4xx range, except for 429 (rate limit)
Expand Down
4 changes: 2 additions & 2 deletions app/services/suspend_account_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ def purge_profile!

return if @options[:destroy]

@account.silenced = false
@account.suspended = true
@account.silenced_at = nil
@account.suspended_at = @options[:suspended_at] || Time.now.utc
@account.locked = false
@account.display_name = ''
@account.note = ''
Expand Down
15 changes: 10 additions & 5 deletions app/services/unblock_domain_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
class UnblockDomainService < BaseService
attr_accessor :domain_block

def call(domain_block, retroactive)
def call(domain_block)
@domain_block = domain_block
process_retroactive_updates if retroactive
process_retroactive_updates
domain_block.destroy
end

Expand All @@ -14,14 +14,19 @@ def process_retroactive_updates
end

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

def update_options
{ domain_block_impact => false }
{ domain_block_impact => nil }
end

def domain_block_impact
domain_block.silence? ? :silenced : :suspended
domain_block.silence? ? :silenced_at : :suspended_at
end
end
17 changes: 5 additions & 12 deletions app/views/admin/domain_blocks/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,11 @@

= simple_form_for @domain_block, url: admin_domain_block_path(@domain_block), method: :delete do |f|

- if (@domain_block.noop?)
= f.input :retroactive,
as: :hidden,
input_html: { :value => "0" }
- else
= 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)
- unless (@domain_block.noop?)
%p= t(".retroactive.#{@domain_block.severity}")
%p.hint= t(:affected_accounts,
scope: [:admin, :domain_blocks, :show],
count: @domain_block.affected_accounts_count)

.actions
= f.button :button, t('.undo'), type: :submit
4 changes: 2 additions & 2 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,8 @@ en:
one: One account in the database affected
other: "%{count} accounts in the database affected"
retroactive:
silence: Unsilence all existing accounts from this domain
suspend: Unsuspend all existing accounts from this domain
silence: Unsilence existing affected accounts from this domain
suspend: Unsuspend existing affected accounts from this domain
title: Undo domain block for %{domain}
undo: Undo
undo: Undo domain block
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit f661255

Please sign in to comment.