Moderation: add `force sensitive` and `force unlisted` actions. Accounts: add federatable `adult content` tag. Handle from remote accounts as well.

staging
multiple creatures 2019-05-10 03:48:11 -05:00
parent 5c59d1837f
commit 3b06175e8f
35 changed files with 356 additions and 80 deletions

View File

@ -2,7 +2,7 @@
module Admin
class AccountsController < BaseController
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :allow_public, :allow_nonsensitive, :unsilence, :unsuspend, :memorialize, :approve, :reject]
before_action :require_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
@ -45,6 +45,34 @@ module Admin
redirect_to admin_accounts_path(pending: '1')
end
def force_sensitive
authorize @account, :force_sensitive?
@account.force_sensitive!
log_action :force_sensitive, @account
redirect_to admin_account_path(@account.id)
end
def allow_nonsensitive
authorize @account, :allow_nonsensitive?
@account.allow_nonsensitive!
log_action :allow_nonsensitive, @account
redirect_to admin_account_path(@account.id)
end
def force_unlisted
authorize @account, :force_unlisted?
@account.force_unlisted!
log_action :force_unlisted, @account
redirect_to admin_account_path(@account.id)
end
def allow_public
authorize @account, :allow_public?
@account.allow_public!
log_action :allow_public, @account
redirect_to admin_account_path(@account.id)
end
def unsilence
authorize @account, :unsilence?
@account.unsilence!

View File

@ -53,7 +53,7 @@ module Admin
end
def resource_params
params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports)
params.require(:domain_block).permit(:domain, :severity, :force_sensitive, :reject_media, :reject_reports)
end
end
end

View File

@ -25,7 +25,7 @@ class Settings::ProfilesController < Settings::BaseController
private
def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :replies, :locked, :hidden, :unlisted, :bot, :discoverable, fields_attributes: [:name, :value])
params.require(:account).permit(:display_name, :note, :avatar, :header, :replies, :locked, :hidden, :unlisted, :adults_only, :bot, :discoverable, fields_attributes: [:name, :value])
end
def set_account

View File

@ -19,7 +19,7 @@ module Admin::ActionLogsHelper
elsif log.target_type == 'User' && [:change_email].include?(log.action)
log.recorded_changes.slice('email', 'unconfirmed_email')
elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media')
log.recorded_changes.slice('severity', 'reject_media', 'force_sensitive')
elsif log.target_type == 'Status' && log.action == :update
log.recorded_changes.slice('sensitive')
end
@ -55,13 +55,13 @@ module Admin::ActionLogsHelper
def class_for_log_icon(log)
case log.action
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve
when :enable, :allow_public, :allow_nonsensitive, :unsuspend, :unsilence, :confirm, :promote, :resolve
'positive'
when :create
opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize, :change_email
'neutral'
when :demote, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
when :demote, :force_sensitive, :force_unlisted, :silence, :disable, :suspend, :remove_avatar, :remove_header, :reopen
'negative'
when :destroy
opposite_verbs?(log) ? 'positive' : 'negative'

View File

@ -38,6 +38,7 @@ module StreamEntriesHelper
content_tag(:div, class: 'roles') do
roles = []
roles << content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot') if account.bot?
roles << content_tag(:div, t('accounts.roles.adults_only'), class: 'account-role adults-only') if account.adults_only?
roles << content_tag(:div, t('accounts.roles.gentlies_kobolds'), class: 'account-role gentlies') if account&.user&.setting_gently_kobolds
roles << content_tag(:div, t('accounts.roles.kobold'), class: 'account-role kobold') if account&.user&.setting_user_is_kobold

View File

@ -189,7 +189,8 @@ class Header extends ImmutablePureComponent {
const content = { __html: account.get('note_emojified') };
const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields');
const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
const badge_bot = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
const badge_ao = account.get('adults_only') ? (<div className='account-role adults-only'><FormattedMessage id='account.badges.adults_only' defaultMessage="🔞 Adult content" /></div>) : null;
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
return (
@ -219,7 +220,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__tabs__name'>
<h1>
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge}
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge_ao}{badge_bot}
<small>@{acct} {lockedIcon}</small>
</h1>
</div>
@ -243,7 +244,7 @@ class Header extends ImmutablePureComponent {
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
</dd>

View File

@ -1,6 +1,7 @@
{
"account.add_or_remove_from_list": "Add or Remove from lists",
"account.badges.bot": "Bot",
"account.badges.adults_only": "🔞 Adult content",
"account.block": "Block @{name}",
"account.block_domain": "Hide {domain}",
"account.blocked": "Blocked",

View File

@ -34,6 +34,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
process_tags
process_audience
@params[:visibility] = :unlisted if @params[:visibility] == :public && @account.force_unlisted?
@params[:sensitive] = true if @account.force_sensitive?
ApplicationRecord.transaction do
@status = Status.create!(@params)
attach_tags(@status)

View File

@ -19,6 +19,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
adults_only: { 'schema' => 'http://schema.org#', 'suggestedMinAge' => 'schema:suggestedMinAge' }
}.freeze
def self.default_key_transform

View File

@ -48,6 +48,9 @@
# vars :jsonb not null
# replies :boolean default(TRUE), not null
# unlisted :boolean default(FALSE), not null
# force_unlisted :boolean default(FALSE), not null
# force_sensitive :boolean default(FALSE), not null
# adults_only :boolean default(FALSE), not null
#
class Account < ApplicationRecord
@ -120,6 +123,7 @@ class Account < ApplicationRecord
:moderator?,
:staff?,
:locale,
:default_sensitive?,
:hides_network?,
:shows_application?,
:always_local?,
@ -185,6 +189,28 @@ class Account < ApplicationRecord
ResolveAccountService.new.call(acct)
end
def force_unlisted!
transaction do
update!(force_unlisted: true)
Status.where(account_id: id, visibility: :public).in_batches.update_all(visibility: :unlisted)
end
end
def force_sensitive!
transaction do
update!(force_sensitive: true)
Status.where(account_id: id, sensitive: false).in_batches.update_all(sensitive: true)
end
end
def allow_public!
update!(force_unlisted: false)
end
def allow_nonsensitive!
update!(force_sensitive: false)
end
def silenced?
silenced_at.present?
end

View File

@ -13,7 +13,7 @@
#
class AccountWarning < ApplicationRecord
enum action: %i(none disable silence suspend), _suffix: :action
enum action: %i(none disable force_sensitive force_unlisted silence suspend), _suffix: :action
belongs_to :account, inverse_of: :account_warnings
belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_account_warnings

View File

@ -8,6 +8,8 @@ class Admin::AccountAction
TYPES = %w(
none
disable
force_sensitive
force_unlisted
silence
suspend
).freeze
@ -56,6 +58,10 @@ class Admin::AccountAction
case type
when 'disable'
handle_disable!
when 'force_sensitive'
handle_force_sensitive!
when 'force_unlisted'
handle_force_unlisted!
when 'silence'
handle_silence!
when 'suspend'
@ -97,6 +103,18 @@ class Admin::AccountAction
target_account.user&.disable!
end
def handle_force_sensitive!
authorize(target_account, :force_sensitive?)
log_action(:force_sensitive, target_account.user)
target_account.force_sensitive!
end
def handle_force_unlisted!
authorize(target_account, :force_unlisted?)
log_action(:force_unlisted, target_account.user)
target_account.force_unlisted!
end
def handle_silence!
authorize(target_account, :silence?)
log_action(:silence, target_account)

View File

@ -3,19 +3,20 @@
#
# Table name: domain_blocks
#
# id :bigint(8) not null, primary key
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# severity :integer default("silence")
# reject_media :boolean default(FALSE), not null
# reject_reports :boolean default(FALSE), not null
# id :bigint(8) not null, primary key
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# severity :integer default("noop")
# reject_media :boolean default(FALSE), not null
# reject_reports :boolean default(FALSE), not null
# force_sensitive :boolean default(FALSE), not null
#
class DomainBlock < ApplicationRecord
include DomainNormalizable
enum severity: [:silence, :suspend, :noop]
enum severity: [:noop, :force_unlisted, :silence, :suspend]
validates :domain, presence: true, uniqueness: true
@ -28,10 +29,15 @@ class DomainBlock < ApplicationRecord
where(domain: domain, severity: :suspend).exists?
end
def self.force_unlisted?(domain)
where(domain: domain, severity: :force_unlisted).exists?
end
def stricter_than?(other_block)
return true if suspend?
return false if other_block.suspend? && (silence? || noop?)
return false if other_block.silence? && noop?
return false if other_block.suspend? && !suspend?
return false if other_block.silence? && (noop? || force_unlisted?)
return false if other_block.force_unlisted? && noop?
(reject_media || !other_block.reject_media) && (reject_reports || !other_block.reject_reports)
end

View File

@ -40,8 +40,6 @@ class Status < ApplicationRecord
# match both with and without U+FE0F (the emoji variation selector)
LOCAL_ONLY_TOKENS = /(?:#!|\u{1f441}\ufe0f?)\u200b?\z/
FORCE_SENSITIVE = ENV.fetch('FORCE_SENSITIVE', '').chomp.split(/\.?\s+/).freeze
FORCE_UNLISTED = ENV.fetch('FORCE_UNLISTED', '').chomp.split(/\.?\s+/).freeze
# If `override_timestamps` is set at creation time, Snowflake ID creation
# will be based on current time instead of `created_at`
@ -561,9 +559,6 @@ class Status < ApplicationRecord
def set_visibility
self.visibility = reblog.visibility if reblog? && visibility.nil?
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
self.visibility = :unlisted if visibility == :public && account.domain.in?(FORCE_UNLISTED)
self.sensitive = true if account.domain.in?(FORCE_SENSITIVE)
self.sensitive = false if sensitive.nil?
end
def set_locality

View File

@ -309,6 +309,10 @@ class User < ApplicationRecord
@hide_captions ||= (settings.hide_captions || false)
end
def default_sensitive?
@default_sensitive ||= settings.default_sensitive
end
def setting_default_privacy
settings.default_privacy || 'public'
end

View File

@ -29,6 +29,22 @@ class AccountPolicy < ApplicationPolicy
staff?
end
def force_unlisted?
staff?
end
def allow_public?
staff?
end
def force_sensitive?
staff?
end
def allow_nonsensitive?
staff?
end
def redownload?
admin?
end

View File

@ -6,7 +6,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
context :security
context_extensions :manually_approves_followers, :featured, :also_known_as,
:moved_to, :property_value, :hashtag, :emoji, :identity_proof
:moved_to, :property_value, :hashtag, :emoji, :identity_proof,
:adults_only
attributes :id, :type, :following, :followers,
:inbox, :outbox, :featured,
@ -20,6 +21,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
attribute :moved_to, if: :moved?
attribute :also_known_as, if: :also_known_as?
attribute :adults_only, if: :adults_only?
class EndpointsSerializer < ActivityPub::Serializer
include RoutingHelper
@ -66,6 +68,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
account_collection_url(object, :featured)
end
def adults_only
18
end
def endpoints
object
end
@ -126,6 +132,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
!object.also_known_as.empty?
end
def adults_only?
object.adults_only
end
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
end

View File

@ -5,7 +5,8 @@ class REST::AccountSerializer < ActiveModel::Serializer
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
:note, :url, :avatar, :avatar_static, :header, :header_static,
:followers_count, :following_count, :statuses_count, :replies
:followers_count, :following_count, :statuses_count, :replies,
:adults_only
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
has_many :emojis, serializer: REST::CustomEmojiSerializer

View File

@ -48,11 +48,13 @@ class ActivityPub::ProcessAccountService < BaseService
def create_account
@account = Account.new
@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?
@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?
@account.force_unlisted = true if force_unlisted?
@account.force_sensitive = true if force_sensitive?
end
def update_account
@ -75,6 +77,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.display_name = @json['name'] || ''
@account.note = @json['summary'] || ''
@account.locked = @json['manuallyApprovesFollowers'] || false
@account.adults_only = @json['suggestedMinAge'].to_i >= 18
@account.fields = property_values || {}
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
@account.actor_type = actor_type
@ -195,6 +198,14 @@ class ActivityPub::ProcessAccountService < BaseService
domain_block&.silence?
end
def auto_force_unlisted?
domain_block&.force_unlisted?
end
def auto_force_sensitive?
domain_block&.force_sensitive?
end
def domain_block
return @domain_block if defined?(@domain_block)
@domain_block = DomainBlock.find_by(domain: @domain)

View File

@ -12,8 +12,11 @@ class BlockDomainService < BaseService
def process_domain_block!
clear_media! if domain_block.reject_media?
force_accounts_sensitive! if domain_block.force_sensitive?
if domain_block.silence?
if domain_block.force_unlisted?
force_accounts_unlisted!
elsif domain_block.silence?
silence_accounts!
elsif domain_block.suspend?
suspend_accounts!
@ -28,6 +31,24 @@ class BlockDomainService < BaseService
@affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
end
def force_accounts_sensitive!
ApplicationRecord.transaction do
blocked_domain_accounts.in_batches.update_all(force_sensitive: true)
blocked_domain_accounts.reorder(nil).find_each do |account|
account.statuses.where(sensitive: false).in_batches.update_all(sensitive: true)
end
end
end
def force_accounts_unlisted!
ApplicationRecord.transaction do
blocked_domain_accounts.in_batches.update_all(force_unlisted: true)
blocked_domain_accounts.reorder(nil).find_each do |account|
account.statuses.with_public_visibility.in_batches.update_all(visibility: :unlisted)
end
end
end
def silence_accounts!
blocked_domain_accounts.without_silenced.in_batches.update_all(silenced_at: @domain_block.created_at)
end
@ -44,7 +65,6 @@ class BlockDomainService < BaseService
def suspend_accounts!
blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account|
UnsubscribeService.new.call(account) if account.subscribed?
SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at)
end
end

View File

@ -30,6 +30,7 @@ class PostStatusService < BaseService
@in_reply_to = @options[:thread]
@tags = @options[:tags]
@local_only = @options[:local_only]
@sensitive = (@account.force_sensitive? ? true : @options[:sensitive])
return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
@ -58,7 +59,7 @@ class PostStatusService < BaseService
end
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
@visibility = :unlisted if @visibility == :public && @account.silenced?
@visibility = :unlisted if @visibility.in?([nil, 'public']) && @account.silenced? || @account.force_unlisted
if @in_reply_to.present? && @in_reply_to.visibility.present?
v = %w(public unlisted private direct limited)
@ -67,6 +68,8 @@ class PostStatusService < BaseService
@local_only = true if @account.user_always_local? || @in_reply_to&.local_only
@sensitive = (@account.default_sensitive? || @options[:spoiler_text].present?) if @sensitive.nil?
@scheduled_at = @options[:scheduled_at]&.to_datetime
@scheduled_at = nil if scheduled_in_the_past?
rescue ArgumentError
@ -176,7 +179,7 @@ class PostStatusService < BaseService
media_attachments: @media || [],
thread: @in_reply_to,
poll_attributes: poll_attributes,
sensitive: (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?,
sensitive: @sensitive,
spoiler_text: @options[:spoiler_text] || '',
visibility: @visibility,
local_only: @local_only,

View File

@ -27,6 +27,13 @@ class UnblockDomainService < BaseService
end
def domain_block_impact
domain_block.silence? ? :silenced_at : :suspended_at
case domain_block.severity
when :force_unlisted
:force_unlisted
when :silence
:silenced_at
when :suspend
:suspended_at
end
end
end

View File

@ -141,42 +141,51 @@
= fa_icon DeliveryFailureTracker.unavailable?(@account.shared_inbox_url) ? 'times' : 'check'
%div{ style: 'overflow: hidden' }
%div{ style: 'float: right' }
- if @account.local?
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
- if @account.user&.otp_required_for_login?
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
- if !@account.memorial? && @account.user_approved?
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
- if @account.local? && @account.user_approved?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
- if @account.force_sensitive?
= link_to t('admin.accounts.allow_nonsensitive'), allow_nonsensitive_admin_account_path(@account.id), method: :post, class: 'button' if can?(:allow_nonsensitive, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.force_sensitive'), new_admin_account_action_path(@account.id, type: 'force_sensitive'), class: 'button button--destructive' if can?(:force_sensitive, @account)
- if @account.force_unlisted?
= link_to t('admin.accounts.allow_public'), allow_public_admin_account_path(@account.id), method: :post, class: 'button' if can?(:allow_public, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.force_unlisted'), new_admin_account_action_path(@account.id, type: 'force_unlisted'), class: 'button button--destructive' if can?(:force_unlisted, @account)
- if @account.silenced?
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button button--destructive' if can?(:silence, @account)
- if @account.local?
- if @account.user_pending?
= link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user)
= link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user)
- unless @account.user_confirmed?
= link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user)
- if @account.suspended?
= link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
- unless @account.local?
- if DomainBlock.where(domain: @account.domain).exists?
= link_to t('admin.domain_blocks.undo'), admin_instance_path(@account.domain), class: 'button'
- else
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
%div{ style: 'float: left' }
- if @account.local? && @account.user_approved?
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
- if @account.silenced?
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button button--destructive' if can?(:silence, @account)
- if @account.local?
- if @account.user_pending?
= link_to t('admin.accounts.approve'), approve_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' if can?(:approve, @account.user)
= link_to t('admin.accounts.reject'), reject_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:reject, @account.user)
- unless @account.user_confirmed?
= link_to t('admin.accounts.confirm'), admin_account_confirmation_path(@account.id), method: :post, class: 'button' if can?(:confirm, @account.user)
- if @account.suspended?
= link_to t('admin.accounts.undo_suspension'), unsuspend_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsuspend, @account)
- elsif !@account.local? || @account.user_approved?
= link_to t('admin.accounts.perform_full_suspension'), new_admin_account_action_path(@account.id, type: 'suspend'), class: 'button button--destructive' if can?(:suspend, @account)
- unless @account.local?
- if DomainBlock.where(domain: @account.domain).exists?
= link_to t('admin.domain_blocks.undo'), admin_instance_path(@account.domain), class: 'button'
- else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive'
- if @account.local?
= link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
- if @account.user&.otp_required_for_login?
= link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user)
- if !@account.memorial? && @account.user_approved?
= link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account)
- else
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account)
%hr.spacer/

View File

@ -11,6 +11,9 @@
.fields-row__column.fields-row__column-6.fields-group
= f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: lambda { |type| t(".severity.#{type}") }, hint: t('.severity.desc_html')
.fields-group
= f.input :force_sensitive, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.force_sensitive'), hint: I18n.t('admin.domain_blocks.force_sensitive_hint')
.fields-group
= f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint')

View File

@ -26,6 +26,9 @@
= f.input :unlisted, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.unlisted')
= f.input :replies, as: :boolean, wrapper: :with_label
.fields-group
= f.input :adults_only, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.adults_only')
.fields-group
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')

View File

@ -74,6 +74,7 @@ en:
moderator: Mod
kobold: Gently the kobold
gentlies_kobolds: Gentlies kobolds
adults_only: 🔞 Adult content
unavailable: Profile unavailable
unfollow: Unfollow
admin:
@ -134,6 +135,8 @@ en:
active: Active
all: All
pending: Pending
force_sensitive: Sensitive
force_unlisted: Unlisted
silenced: Silenced
suspended: Suspended
title: Moderation
@ -175,6 +178,8 @@ en:
show:
created_reports: Made reports
targeted_reports: Reported by others
force_unlisted: Force unlisted
force_sensitive: Force sensitive
silence: Silence
silenced: Silenced
statuses: Statuses
@ -182,6 +187,8 @@ en:
suspended: Suspended
title: Accounts
unconfirmed_email: Unconfirmed email
allow_nonsensitive: Allow non-sensitive
allow_public: Allow public
undo_silenced: Undo silence
undo_suspension: Undo suspension
unsubscribe: Unsubscribe
@ -213,9 +220,14 @@ en:
reopen_report: "%{name} reopened report %{target}"
reset_password_user: "%{name} reset password of creature %{target}"
resolve_report: "%{name} resolved report %{target}"
force_sensitive_user: "%{name} forced %{target}'s media to be marked sensitive"
force_sensitive_account: "%{name} forced %{target}'s media to be marked sensitive"
force_unlisted_account: "%{name} forced %{target}'s roars to be marked unlisted"
silence_account: "%{name} silenced %{target}'s account"
suspend_account: "%{name} suspended %{target}'s account"
unassigned_report: "%{name} unassigned report %{target}"
allow_nonsensitive_account: "%{name} allowed non-sensitive media from %{target}'s account"
allow_public_account: "%{name} allowed public roars from %{target}'s account"
unsilence_account: "%{name} unsilenced %{target}'s account"
unsuspend_account: "%{name} unsuspended %{target}'s account"
update_custom_emoji: "%{name} updated emoji %{target}"
@ -281,11 +293,14 @@ en:
create: Create block
hint: The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts.
severity:
desc_html: "<strong>Silence</strong> will make the account's roars invisible to anyone who isn't following them. <strong>Suspend</strong> will remove all of the account's content, media, and profile data. Use <strong>None</strong> if you just want to reject media files."
desc_html: "<strong>Force Unlisted</strong> will force the account's roars to a maximum of unlisted visibility. <strong>Silence</strong> will make the account's roars invisible to anyone who isn't following them as well as disable notification. <strong>Suspend</strong> will remove all of the account's content, media, and profile data. Use <strong>None</strong> if you just want to reject media files."
noop: None
force_unlisted: Force unlisted
silence: Silence
suspend: Suspend
title: New domain block
force_sensitive: Mark media sensitive
force_sensitive_hint: Forces all media from this domain to be marked sensitive.
reject_media: Reject media files
reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions
reject_reports: Reject reports
@ -293,15 +308,17 @@ en:
rejecting_media: rejecting media files
rejecting_reports: rejecting reports
severity:
force_unlisted: force unlisted
silence: silenced
suspend: suspended
show:
affected_accounts:
one: One account in the database affected
other: "%{count} accounts in the database affected"
one: One account affected
other: "%{count} accounts affected"
retroactive:
silence: Unsilence existing affected accounts from this domain
suspend: Unsuspend existing affected accounts from this domain
force_unlisted: Allow public roars on all existing accounts from this domain
title: Undo domain block for %{domain}
undo: Undo
undo: Undo domain block
@ -1057,18 +1074,24 @@ en:
warning:
explanation:
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
force_sensitive: Your account's media has been forced to sensitive visibility until this limit is removed by a moderator.
force_unlisted: Your account's roars have been forced to unlisted visibility until this limit is removed by a moderator.
silence: While your account is limited, only monsters who are already following you will see your roars on this server, and you may be excluded from various public listings. However, others may still manually join your pack.
suspend: Your account has been suspended, and all of your roars and your uploaded media files have been irreversibly removed from this server, and servers where you had packmates.
suspend: Your account has been suspended. All of your roars and your uploaded media files have been irreversibly removed from this server, and servers where you had packmates.
review_server_policies: Review server policies
subject:
disable: Your account %{acct} has been frozen
none: Warning for %{acct}
silence: Your account %{acct} has been limited
suspend: Your account %{acct} has been suspended
disable: "%{acct}, you account has been frozen."
none: "%{acct}, you've been given a moderation warning."
force_sensitive: "%{acct}, your account's media visibility has been limited."
force_unlisted: "%{acct}, your account's roar visibility has been limited."
silence: "%{acct}, your account has been silenced."
suspend: "%{acct}, your account has been suspended."
title:
disable: Account frozen
none: Warning
silence: Account limited
force_sensitive: Media visibility limited
force_unlisted: Roar visibility limited
silence: Account silenced
suspend: Account suspended
welcome:
edit_profile_action: Setup profile

View File

@ -11,6 +11,7 @@ en:
warning_preset_id: Optional. You can still add custom text to end of the preset
defaults:
hidden: Toggles whether your public profile is publicaly accessible
adults_only: This account may contain mature or sensitive content
unlisted: Excludes you from public repeated/admired by lists of *local* monsters
autofollow: People who sign up through the invite will automatically join your pack
avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
@ -66,11 +67,14 @@ en:
types:
disable: Disable
none: Do nothing
force_sensitive: Force sensitive
force_unlisted: Force unlisted
silence: Silence
suspend: Suspend and irreversibly delete account data
warning_preset_id: Use a warning preset
defaults:
hidden: Disable your public profile
adults_only: Adult content
unlisted: Exclude from public interaction lists
replies: Show your public replies
autofollow: Invite to join your pack

View File

@ -188,6 +188,10 @@ Rails.application.routes.draw do
post :subscribe
post :unsubscribe
post :enable
post :force_sensitive
post :force_unlisted
post :allow_public
post :allow_nonsensitive
post :unsilence
post :unsuspend
post :redownload

View File

@ -0,0 +1,16 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class AddForceSensitiveToDomainBlocks < ActiveRecord::Migration[5.2]
include Mastodon::MigrationHelpers
disable_ddl_transaction!
def up
safety_assured do
add_column_with_default :domain_blocks, :force_sensitive, :boolean, default: false, allow_null: false
end
end
def down
remove_column :domain_blocks, :force_sensitive
end
end

View File

@ -0,0 +1,6 @@
class AddNetworkIndexToStatuses < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
add_index :statuses, :network, where: :network, algorithm: :concurrently
end
end

View File

@ -0,0 +1,19 @@
class UpdateDomainBlockSeverityEnum < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
DomainBlock.where(severity: :force_unlisted).each do |block|
block.severity = :suspend
block.save
end
DomainBlock.where(severity: :noop).each do |block|
block.severity = :silence
block.save
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -0,0 +1,8 @@
class AddForceOptionsToAccounts < ActiveRecord::Migration[5.2]
def change
safety_assured {
add_column :accounts, :force_unlisted, :boolean, null: false, default: false
add_column :accounts, :force_sensitive, :boolean, null: false, default: false
}
end
end

View File

@ -0,0 +1,5 @@
class AddAdultsOnlyToAccounts < ActiveRecord::Migration[5.2]
def change
safety_assured { add_column :accounts, :adults_only, :boolean, null: false, default: false }
end
end

View File

@ -0,0 +1,19 @@
class UpdateAccountWarningActionEnum < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def up
AccountWarning.where(action: :force_unlisted).each do |warning|
warning.severity = :suspend
warning.save
end
AccountWarning.where(action: :force_sensitive).each do |warning|
warning.severity = :silence
warning.save
end
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -151,6 +151,9 @@ ActiveRecord::Schema.define(version: 2019_05_19_130537) do
t.jsonb "vars", default: {}, null: false
t.boolean "replies", default: true, null: false
t.boolean "unlisted", default: false, null: false
t.boolean "force_unlisted", default: false, null: false
t.boolean "force_sensitive", default: false, null: false
t.boolean "adults_only", default: false, null: false
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 ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
@ -258,6 +261,7 @@ ActiveRecord::Schema.define(version: 2019_05_19_130537) do
t.integer "severity", default: 0
t.boolean "reject_media", default: false, null: false
t.boolean "reject_reports", default: false, null: false
t.boolean "force_sensitive", default: false, null: false
t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true
end
@ -651,6 +655,7 @@ ActiveRecord::Schema.define(version: 2019_05_19_130537) do
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc }
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
t.index ["network"], name: "index_statuses_on_network", where: "network"
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
t.index ["tsv"], name: "tsv_idx", using: :gin
t.index ["uri"], name: "index_statuses_on_uri", unique: true