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 module Admin
class AccountsController < BaseController 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_remote_account!, only: [:redownload]
before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject] before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
@ -45,6 +45,34 @@ module Admin
redirect_to admin_accounts_path(pending: '1') redirect_to admin_accounts_path(pending: '1')
end 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 def unsilence
authorize @account, :unsilence? authorize @account, :unsilence?
@account.unsilence! @account.unsilence!

View File

@ -53,7 +53,7 @@ module Admin
end end
def resource_params 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 end
end end

View File

@ -25,7 +25,7 @@ class Settings::ProfilesController < Settings::BaseController
private private
def account_params 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 end
def set_account def set_account

View File

@ -19,7 +19,7 @@ module Admin::ActionLogsHelper
elsif log.target_type == 'User' && [:change_email].include?(log.action) elsif log.target_type == 'User' && [:change_email].include?(log.action)
log.recorded_changes.slice('email', 'unconfirmed_email') log.recorded_changes.slice('email', 'unconfirmed_email')
elsif log.target_type == 'DomainBlock' 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 elsif log.target_type == 'Status' && log.action == :update
log.recorded_changes.slice('sensitive') log.recorded_changes.slice('sensitive')
end end
@ -55,13 +55,13 @@ module Admin::ActionLogsHelper
def class_for_log_icon(log) def class_for_log_icon(log)
case log.action case log.action
when :enable, :unsuspend, :unsilence, :confirm, :promote, :resolve when :enable, :allow_public, :allow_nonsensitive, :unsuspend, :unsilence, :confirm, :promote, :resolve
'positive' 'positive'
when :create when :create
opposite_verbs?(log) ? 'negative' : 'positive' opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize, :change_email when :update, :reset_password, :disable_2fa, :memorialize, :change_email
'neutral' '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' 'negative'
when :destroy when :destroy
opposite_verbs?(log) ? 'positive' : 'negative' opposite_verbs?(log) ? 'positive' : 'negative'

View File

@ -38,6 +38,7 @@ module StreamEntriesHelper
content_tag(:div, class: 'roles') do content_tag(:div, class: 'roles') do
roles = [] roles = []
roles << content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot') if account.bot? 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.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 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 content = { __html: account.get('note_emojified') };
const displayNameHtml = { __html: account.get('display_name_html') }; const displayNameHtml = { __html: account.get('display_name_html') };
const fields = account.get('fields'); 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'); const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
return ( return (
@ -219,7 +220,7 @@ class Header extends ImmutablePureComponent {
<div className='account__header__tabs__name'> <div className='account__header__tabs__name'>
<h1> <h1>
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge} <span dangerouslySetInnerHTML={displayNameHtml} /> {badge_ao}{badge_bot}
<small>@{acct} {lockedIcon}</small> <small>@{acct} {lockedIcon}</small>
</h1> </h1>
</div> </div>
@ -243,7 +244,7 @@ class Header extends ImmutablePureComponent {
{fields.map((pair, i) => ( {fields.map((pair, i) => (
<dl key={i}> <dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> <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') }} /> {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> </dd>

View File

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

View File

@ -34,6 +34,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
process_tags process_tags
process_audience process_audience
@params[:visibility] = :unlisted if @params[:visibility] == :public && @account.force_unlisted?
@params[:sensitive] = true if @account.force_sensitive?
ApplicationRecord.transaction do ApplicationRecord.transaction do
@status = Status.create!(@params) @status = Status.create!(@params)
attach_tags(@status) 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' } }, focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' }, identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' }, blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
adults_only: { 'schema' => 'http://schema.org#', 'suggestedMinAge' => 'schema:suggestedMinAge' }
}.freeze }.freeze
def self.default_key_transform def self.default_key_transform

View File

@ -48,6 +48,9 @@
# vars :jsonb not null # vars :jsonb not null
# replies :boolean default(TRUE), not null # replies :boolean default(TRUE), not null
# unlisted :boolean default(FALSE), 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 class Account < ApplicationRecord
@ -120,6 +123,7 @@ class Account < ApplicationRecord
:moderator?, :moderator?,
:staff?, :staff?,
:locale, :locale,
:default_sensitive?,
:hides_network?, :hides_network?,
:shows_application?, :shows_application?,
:always_local?, :always_local?,
@ -185,6 +189,28 @@ class Account < ApplicationRecord
ResolveAccountService.new.call(acct) ResolveAccountService.new.call(acct)
end 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? def silenced?
silenced_at.present? silenced_at.present?
end end

View File

@ -13,7 +13,7 @@
# #
class AccountWarning < ApplicationRecord 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 :account, inverse_of: :account_warnings
belongs_to :target_account, class_name: 'Account', inverse_of: :targeted_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( TYPES = %w(
none none
disable disable
force_sensitive
force_unlisted
silence silence
suspend suspend
).freeze ).freeze
@ -56,6 +58,10 @@ class Admin::AccountAction
case type case type
when 'disable' when 'disable'
handle_disable! handle_disable!
when 'force_sensitive'
handle_force_sensitive!
when 'force_unlisted'
handle_force_unlisted!
when 'silence' when 'silence'
handle_silence! handle_silence!
when 'suspend' when 'suspend'
@ -97,6 +103,18 @@ class Admin::AccountAction
target_account.user&.disable! target_account.user&.disable!
end 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! def handle_silence!
authorize(target_account, :silence?) authorize(target_account, :silence?)
log_action(:silence, target_account) log_action(:silence, target_account)

View File

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

View File

@ -40,8 +40,6 @@ class Status < ApplicationRecord
# match both with and without U+FE0F (the emoji variation selector) # match both with and without U+FE0F (the emoji variation selector)
LOCAL_ONLY_TOKENS = /(?:#!|\u{1f441}\ufe0f?)\u200b?\z/ 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 # If `override_timestamps` is set at creation time, Snowflake ID creation
# will be based on current time instead of `created_at` # will be based on current time instead of `created_at`
@ -561,9 +559,6 @@ class Status < ApplicationRecord
def set_visibility def set_visibility
self.visibility = reblog.visibility if reblog? && visibility.nil? self.visibility = reblog.visibility if reblog? && visibility.nil?
self.visibility = (account.locked? ? :private : :public) if 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 end
def set_locality def set_locality

View File

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

View File

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

View File

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

View File

@ -5,7 +5,8 @@ class REST::AccountSerializer < ActiveModel::Serializer
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at, attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
:note, :url, :avatar, :avatar_static, :header, :header_static, :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_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
has_many :emojis, serializer: REST::CustomEmojiSerializer has_many :emojis, serializer: REST::CustomEmojiSerializer

View File

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

View File

@ -12,8 +12,11 @@ class BlockDomainService < BaseService
def process_domain_block! def process_domain_block!
clear_media! if domain_block.reject_media? 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! silence_accounts!
elsif domain_block.suspend? elsif domain_block.suspend?
suspend_accounts! suspend_accounts!
@ -28,6 +31,24 @@ class BlockDomainService < BaseService
@affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") } @affected_status_ids.each { |id| Rails.cache.delete_matched("statuses/#{id}-*") }
end 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! def silence_accounts!
blocked_domain_accounts.without_silenced.in_batches.update_all(silenced_at: @domain_block.created_at) blocked_domain_accounts.without_silenced.in_batches.update_all(silenced_at: @domain_block.created_at)
end end
@ -44,7 +65,6 @@ class BlockDomainService < BaseService
def suspend_accounts! def suspend_accounts!
blocked_domain_accounts.without_suspended.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, suspended_at: @domain_block.created_at) SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at)
end end
end end

View File

@ -30,6 +30,7 @@ class PostStatusService < BaseService
@in_reply_to = @options[:thread] @in_reply_to = @options[:thread]
@tags = @options[:tags] @tags = @options[:tags]
@local_only = @options[:local_only] @local_only = @options[:local_only]
@sensitive = (@account.force_sensitive? ? true : @options[:sensitive])
return idempotency_duplicate if idempotency_given? && idempotency_duplicate? return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
@ -58,7 +59,7 @@ class PostStatusService < BaseService
end end
@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.in?([nil, 'public']) && @account.silenced? || @account.force_unlisted
if @in_reply_to.present? && @in_reply_to.visibility.present? if @in_reply_to.present? && @in_reply_to.visibility.present?
v = %w(public unlisted private direct limited) 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 @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 = @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
@ -176,7 +179,7 @@ class PostStatusService < BaseService
media_attachments: @media || [], media_attachments: @media || [],
thread: @in_reply_to, thread: @in_reply_to,
poll_attributes: poll_attributes, 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] || '', spoiler_text: @options[:spoiler_text] || '',
visibility: @visibility, visibility: @visibility,
local_only: @local_only, local_only: @local_only,

View File

@ -27,6 +27,13 @@ class UnblockDomainService < BaseService
end end
def domain_block_impact 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
end end

View File

@ -141,42 +141,51 @@
= fa_icon DeliveryFailureTracker.unavailable?(@account.shared_inbox_url) ? 'times' : 'check' = fa_icon DeliveryFailureTracker.unavailable?(@account.shared_inbox_url) ? 'times' : 'check'
%div{ style: 'overflow: hidden' } %div{ style: 'overflow: hidden' }
%div{ style: 'float: right' } - if @account.local? && @account.user_approved?
- if @account.local? = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account)
= 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? - if @account.force_sensitive?
= 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) = link_to t('admin.accounts.allow_nonsensitive'), allow_nonsensitive_admin_account_path(@account.id), method: :post, class: 'button' if can?(:allow_nonsensitive, @account)
- if !@account.memorial? && @account.user_approved? - elsif !@account.local? || @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) = 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 - 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?
- if @account.local? && @account.user_approved? = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user)
= link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account) - if @account.user&.otp_required_for_login?
- if @account.silenced? = 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)
= link_to t('admin.accounts.undo_silenced'), unsilence_admin_account_path(@account.id), method: :post, class: 'button' if can?(:unsilence, @account) - if !@account.memorial? && @account.user_approved?
- elsif !@account.local? || @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)
= link_to t('admin.accounts.silence'), new_admin_account_action_path(@account.id, type: 'silence'), class: 'button button--destructive' if can?(:silence, @account) - else
= link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @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'
%hr.spacer/ %hr.spacer/

View File

@ -11,6 +11,9 @@
.fields-row__column.fields-row__column-6.fields-group .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') = 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 .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') = 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 :unlisted, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.unlisted')
= f.input :replies, as: :boolean, wrapper: :with_label = 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 .fields-group
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')

View File

@ -74,6 +74,7 @@ en:
moderator: Mod moderator: Mod
kobold: Gently the kobold kobold: Gently the kobold
gentlies_kobolds: Gentlies kobolds gentlies_kobolds: Gentlies kobolds
adults_only: 🔞 Adult content
unavailable: Profile unavailable unavailable: Profile unavailable
unfollow: Unfollow unfollow: Unfollow
admin: admin:
@ -134,6 +135,8 @@ en:
active: Active active: Active
all: All all: All
pending: Pending pending: Pending
force_sensitive: Sensitive
force_unlisted: Unlisted
silenced: Silenced silenced: Silenced
suspended: Suspended suspended: Suspended
title: Moderation title: Moderation
@ -175,6 +178,8 @@ en:
show: show:
created_reports: Made reports created_reports: Made reports
targeted_reports: Reported by others targeted_reports: Reported by others
force_unlisted: Force unlisted
force_sensitive: Force sensitive
silence: Silence silence: Silence
silenced: Silenced silenced: Silenced
statuses: Statuses statuses: Statuses
@ -182,6 +187,8 @@ en:
suspended: Suspended suspended: Suspended
title: Accounts title: Accounts
unconfirmed_email: Unconfirmed email unconfirmed_email: Unconfirmed email
allow_nonsensitive: Allow non-sensitive
allow_public: Allow public
undo_silenced: Undo silence undo_silenced: Undo silence
undo_suspension: Undo suspension undo_suspension: Undo suspension
unsubscribe: Unsubscribe unsubscribe: Unsubscribe
@ -213,9 +220,14 @@ en:
reopen_report: "%{name} reopened report %{target}" reopen_report: "%{name} reopened report %{target}"
reset_password_user: "%{name} reset password of creature %{target}" reset_password_user: "%{name} reset password of creature %{target}"
resolve_report: "%{name} resolved report %{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" silence_account: "%{name} silenced %{target}'s account"
suspend_account: "%{name} suspended %{target}'s account" suspend_account: "%{name} suspended %{target}'s account"
unassigned_report: "%{name} unassigned report %{target}" 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" unsilence_account: "%{name} unsilenced %{target}'s account"
unsuspend_account: "%{name} unsuspended %{target}'s account" unsuspend_account: "%{name} unsuspended %{target}'s account"
update_custom_emoji: "%{name} updated emoji %{target}" update_custom_emoji: "%{name} updated emoji %{target}"
@ -281,11 +293,14 @@ en:
create: Create block 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. 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: 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 noop: None
force_unlisted: Force unlisted
silence: Silence silence: Silence
suspend: Suspend suspend: Suspend
title: New domain block 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: Reject media files
reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions
reject_reports: Reject reports reject_reports: Reject reports
@ -293,15 +308,17 @@ en:
rejecting_media: rejecting media files rejecting_media: rejecting media files
rejecting_reports: rejecting reports rejecting_reports: rejecting reports
severity: severity:
force_unlisted: force unlisted
silence: silenced silence: silenced
suspend: suspended suspend: suspended
show: show:
affected_accounts: affected_accounts:
one: One account in the database affected one: One account affected
other: "%{count} accounts in the database affected" other: "%{count} accounts affected"
retroactive: retroactive:
silence: Unsilence existing affected accounts from this domain silence: Unsilence existing affected accounts from this domain
suspend: Unsuspend 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} title: Undo domain block for %{domain}
undo: Undo undo: Undo
undo: Undo domain block undo: Undo domain block
@ -1057,18 +1074,24 @@ en:
warning: warning:
explanation: explanation:
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked. 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. 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 review_server_policies: Review server policies
subject: subject:
disable: Your account %{acct} has been frozen disable: "%{acct}, you account has been frozen."
none: Warning for %{acct} none: "%{acct}, you've been given a moderation warning."
silence: Your account %{acct} has been limited force_sensitive: "%{acct}, your account's media visibility has been limited."
suspend: Your account %{acct} has been suspended 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: title:
disable: Account frozen disable: Account frozen
none: Warning none: Warning
silence: Account limited force_sensitive: Media visibility limited
force_unlisted: Roar visibility limited
silence: Account silenced
suspend: Account suspended suspend: Account suspended
welcome: welcome:
edit_profile_action: Setup profile 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 warning_preset_id: Optional. You can still add custom text to end of the preset
defaults: defaults:
hidden: Toggles whether your public profile is publicaly accessible 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 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 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 avatar: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
@ -66,11 +67,14 @@ en:
types: types:
disable: Disable disable: Disable
none: Do nothing none: Do nothing
force_sensitive: Force sensitive
force_unlisted: Force unlisted
silence: Silence silence: Silence
suspend: Suspend and irreversibly delete account data suspend: Suspend and irreversibly delete account data
warning_preset_id: Use a warning preset warning_preset_id: Use a warning preset
defaults: defaults:
hidden: Disable your public profile hidden: Disable your public profile
adults_only: Adult content
unlisted: Exclude from public interaction lists unlisted: Exclude from public interaction lists
replies: Show your public replies replies: Show your public replies
autofollow: Invite to join your pack autofollow: Invite to join your pack

View File

@ -188,6 +188,10 @@ Rails.application.routes.draw do
post :subscribe post :subscribe
post :unsubscribe post :unsubscribe
post :enable post :enable
post :force_sensitive
post :force_unlisted
post :allow_public
post :allow_nonsensitive
post :unsilence post :unsilence
post :unsuspend post :unsuspend
post :redownload 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.jsonb "vars", default: {}, null: false
t.boolean "replies", default: true, null: false t.boolean "replies", default: true, null: false
t.boolean "unlisted", default: false, 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 "(((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"
@ -258,6 +261,7 @@ ActiveRecord::Schema.define(version: 2019_05_19_130537) do
t.integer "severity", default: 0 t.integer "severity", default: 0
t.boolean "reject_media", default: false, null: false t.boolean "reject_media", default: false, null: false
t.boolean "reject_reports", 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 t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true
end 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 ["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_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 ["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 ["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 ["tsv"], name: "tsv_idx", using: :gin
t.index ["uri"], name: "index_statuses_on_uri", unique: true t.index ["uri"], name: "index_statuses_on_uri", unique: true