about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authormultiple creatures <dev@multiple-creature.party>2019-05-10 03:48:11 -0500
committermultiple creatures <dev@multiple-creature.party>2019-05-21 03:16:23 -0500
commit3b06175e8f5cb9d688e8ec376dbfd88abf5f3278 (patch)
tree160a6f6c97777ca022326bb93701f358fe689c99 /app
parent5c59d1837f2d3152342ef45bf7827495183e62dd (diff)
Moderation: add `force sensitive` and `force unlisted` actions. Accounts: add federatable `adult content` tag. Handle from remote accounts as well.
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/accounts_controller.rb30
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb2
-rw-r--r--app/controllers/settings/profiles_controller.rb2
-rw-r--r--app/helpers/admin/action_logs_helper.rb6
-rw-r--r--app/helpers/stream_entries_helper.rb1
-rw-r--r--app/javascript/flavours/glitch/features/account/components/header.js7
-rw-r--r--app/javascript/mastodon/locales/en.json1
-rw-r--r--app/lib/activitypub/activity/create.rb3
-rw-r--r--app/lib/activitypub/adapter.rb1
-rw-r--r--app/models/account.rb26
-rw-r--r--app/models/account_warning.rb2
-rw-r--r--app/models/admin/account_action.rb18
-rw-r--r--app/models/domain_block.rb26
-rw-r--r--app/models/status.rb5
-rw-r--r--app/models/user.rb4
-rw-r--r--app/policies/account_policy.rb16
-rw-r--r--app/serializers/activitypub/actor_serializer.rb12
-rw-r--r--app/serializers/rest/account_serializer.rb3
-rw-r--r--app/services/activitypub/process_account_service.rb21
-rw-r--r--app/services/block_domain_service.rb24
-rw-r--r--app/services/post_status_service.rb7
-rw-r--r--app/services/unblock_domain_service.rb9
-rw-r--r--app/views/admin/accounts/show.html.haml79
-rw-r--r--app/views/admin/domain_blocks/new.html.haml3
-rw-r--r--app/views/settings/profiles/show.html.haml3
25 files changed, 239 insertions, 72 deletions
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') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
+    const badge_bot       = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
+    const badge_ao        = account.get('adults_only') ? (<div className='account-role adults-only'><FormattedMessage id='account.badges.adults_only' defaultMessage="🔞 Adult content"  /></div>) : null;
     const acct            = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
 
     return (
@@ -219,7 +220,7 @@ class Header extends ImmutablePureComponent {
 
           <div className='account__header__tabs__name'>
             <h1>
-              <span dangerouslySetInnerHTML={displayNameHtml} /> {badge}
+              <span dangerouslySetInnerHTML={displayNameHtml} /> {badge_ao}{badge_bot}
               <small>@{acct} {lockedIcon}</small>
             </h1>
           </div>
@@ -243,7 +244,7 @@ class Header extends ImmutablePureComponent {
                   {fields.map((pair, i) => (
                     <dl key={i}>
                       <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
- 
+
                       <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
                         {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
                       </dd>
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)
-
-    %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'
+        = 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
@@ -12,6 +12,9 @@
       = 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')
 
   .fields-group
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
@@ -27,6 +27,9 @@
     = 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')
 
   - if Setting.profile_directory