From 3b06175e8f5cb9d688e8ec376dbfd88abf5f3278 Mon Sep 17 00:00:00 2001 From: multiple creatures Date: Fri, 10 May 2019 03:48:11 -0500 Subject: [PATCH] Moderation: add `force sensitive` and `force unlisted` actions. Accounts: add federatable `adult content` tag. Handle from remote accounts as well. --- app/controllers/admin/accounts_controller.rb | 30 +++++++- .../admin/domain_blocks_controller.rb | 2 +- .../settings/profiles_controller.rb | 2 +- app/helpers/admin/action_logs_helper.rb | 6 +- app/helpers/stream_entries_helper.rb | 1 + .../features/account/components/header.js | 7 +- app/javascript/mastodon/locales/en.json | 1 + app/lib/activitypub/activity/create.rb | 3 + app/lib/activitypub/adapter.rb | 1 + app/models/account.rb | 26 +++++++ app/models/account_warning.rb | 2 +- app/models/admin/account_action.rb | 18 +++++ app/models/domain_block.rb | 26 ++++--- app/models/status.rb | 5 -- app/models/user.rb | 4 + app/policies/account_policy.rb | 16 ++++ .../activitypub/actor_serializer.rb | 12 ++- app/serializers/rest/account_serializer.rb | 3 +- .../activitypub/process_account_service.rb | 21 +++-- app/services/block_domain_service.rb | 24 +++++- app/services/post_status_service.rb | 7 +- app/services/unblock_domain_service.rb | 9 ++- app/views/admin/accounts/show.html.haml | 77 +++++++++++-------- app/views/admin/domain_blocks/new.html.haml | 3 + app/views/settings/profiles/show.html.haml | 3 + config/locales/en.yml | 41 +++++++--- config/locales/simple_form.en.yml | 4 + config/routes.rb | 4 + ...11_add_force_sensitive_to_domain_blocks.rb | 16 ++++ ...509185038_add_network_index_to_statuses.rb | 6 ++ ...90505_update_domain_block_severity_enum.rb | 19 +++++ ...509201242_add_force_options_to_accounts.rb | 8 ++ ...90509201451_add_adults_only_to_accounts.rb | 5 ++ ...1027_update_account_warning_action_enum.rb | 19 +++++ db/schema.rb | 5 ++ 35 files changed, 356 insertions(+), 80 deletions(-) create mode 100644 db/migrate/20190509183411_add_force_sensitive_to_domain_blocks.rb create mode 100644 db/migrate/20190509185038_add_network_index_to_statuses.rb create mode 100644 db/migrate/20190509190505_update_domain_block_severity_enum.rb create mode 100644 db/migrate/20190509201242_add_force_options_to_accounts.rb create mode 100644 db/migrate/20190509201451_add_adults_only_to_accounts.rb create mode 100644 db/migrate/20190510071027_update_account_warning_action_enum.rb diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index 86bc3c8a2..d486a97ba 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -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! diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 71597763b..47c2daa7a 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -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 diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index ac6635aea..e30079a0f 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -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 diff --git a/app/helpers/admin/action_logs_helper.rb b/app/helpers/admin/action_logs_helper.rb index e5fbb1500..93ce447a1 100644 --- a/app/helpers/admin/action_logs_helper.rb +++ b/app/helpers/admin/action_logs_helper.rb @@ -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' diff --git a/app/helpers/stream_entries_helper.rb b/app/helpers/stream_entries_helper.rb index f3848f3be..8757518b4 100644 --- a/app/helpers/stream_entries_helper.rb +++ b/app/helpers/stream_entries_helper.rb @@ -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 diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 43c4f0d32..ef5915382 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -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') ? (
) : null; + const badge_bot = account.get('bot') ? (
) : null; + const badge_ao = account.get('adults_only') ? (
) : 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 {

- {badge} + {badge_ao}{badge_bot} @{acct} {lockedIcon}

@@ -243,7 +244,7 @@ class Header extends ImmutablePureComponent { {fields.map((pair, i) => (
- +
{pair.get('verified_at') && }
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index d85322223..d61dc27ad 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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", diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 5514d9a6e..f24cfffa8 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -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) diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 9d940e4ef..4c0231ad7 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -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 diff --git a/app/models/account.rb b/app/models/account.rb index 6e7cf3773..5f88a951f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -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 diff --git a/app/models/account_warning.rb b/app/models/account_warning.rb index 157e6c04d..4e06cf3d0 100644 --- a/app/models/account_warning.rb +++ b/app/models/account_warning.rb @@ -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 diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb index 84c3f880d..1ed464423 100644 --- a/app/models/admin/account_action.rb +++ b/app/models/admin/account_action.rb @@ -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) diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index 84c08c158..c62ca3d8c 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -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 diff --git a/app/models/status.rb b/app/models/status.rb index 0b26e4605..3c98369b1 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 5d67dc0d9..2bd039958 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index 9c145979d..f3bda83db 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -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 diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 0644219fb..44dbc5ccb 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -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 diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index 574ccfc85..04df81225 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -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 diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f36ab7d61..ee24718e1 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -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) diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 497f0394b..154d00427 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -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 diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index d54f9295e..5a73b541f 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -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, diff --git a/app/services/unblock_domain_service.rb b/app/services/unblock_domain_service.rb index 9b8526fbe..d9b96edfe 100644 --- a/app/services/unblock_domain_service.rb +++ b/app/services/unblock_domain_service.rb @@ -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 diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 7494c9fa2..0066ed8e7 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -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/ diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml index 3a4963489..2517b2714 100644 --- a/app/views/admin/domain_blocks/new.html.haml +++ b/app/views/admin/domain_blocks/new.html.haml @@ -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') diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 43d436cb1..8a7ccfd37 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -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') diff --git a/config/locales/en.yml b/config/locales/en.yml index 36ee3110f..47b71893d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: "Silence will make the account's roars invisible to anyone who isn't following them. Suspend will remove all of the account's content, media, and profile data. Use None if you just want to reject media files." + desc_html: "Force Unlisted will force the account's roars to a maximum of unlisted visibility. Silence will make the account's roars invisible to anyone who isn't following them as well as disable notification. Suspend will remove all of the account's content, media, and profile data. Use None 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 diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index f93bb2cff..ebad93f93 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index aaa2802a9..4167fe4db 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20190509183411_add_force_sensitive_to_domain_blocks.rb b/db/migrate/20190509183411_add_force_sensitive_to_domain_blocks.rb new file mode 100644 index 000000000..c27fb0385 --- /dev/null +++ b/db/migrate/20190509183411_add_force_sensitive_to_domain_blocks.rb @@ -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 diff --git a/db/migrate/20190509185038_add_network_index_to_statuses.rb b/db/migrate/20190509185038_add_network_index_to_statuses.rb new file mode 100644 index 000000000..761cebfc8 --- /dev/null +++ b/db/migrate/20190509185038_add_network_index_to_statuses.rb @@ -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 diff --git a/db/migrate/20190509190505_update_domain_block_severity_enum.rb b/db/migrate/20190509190505_update_domain_block_severity_enum.rb new file mode 100644 index 000000000..90fbb7ebf --- /dev/null +++ b/db/migrate/20190509190505_update_domain_block_severity_enum.rb @@ -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 diff --git a/db/migrate/20190509201242_add_force_options_to_accounts.rb b/db/migrate/20190509201242_add_force_options_to_accounts.rb new file mode 100644 index 000000000..709efafaf --- /dev/null +++ b/db/migrate/20190509201242_add_force_options_to_accounts.rb @@ -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 diff --git a/db/migrate/20190509201451_add_adults_only_to_accounts.rb b/db/migrate/20190509201451_add_adults_only_to_accounts.rb new file mode 100644 index 000000000..179d28ef4 --- /dev/null +++ b/db/migrate/20190509201451_add_adults_only_to_accounts.rb @@ -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 diff --git a/db/migrate/20190510071027_update_account_warning_action_enum.rb b/db/migrate/20190510071027_update_account_warning_action_enum.rb new file mode 100644 index 000000000..b89fb60d2 --- /dev/null +++ b/db/migrate/20190510071027_update_account_warning_action_enum.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 3f0d3ce80..19725e8a1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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