about summary refs log tree commit diff
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
parent5c59d1837f2d3152342ef45bf7827495183e62dd (diff)
Moderation: add `force sensitive` and `force unlisted` actions. Accounts: add federatable `adult content` tag. Handle from remote accounts as well.
-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
-rw-r--r--config/locales/en.yml41
-rw-r--r--config/locales/simple_form.en.yml4
-rw-r--r--config/routes.rb4
-rw-r--r--db/migrate/20190509183411_add_force_sensitive_to_domain_blocks.rb16
-rw-r--r--db/migrate/20190509185038_add_network_index_to_statuses.rb6
-rw-r--r--db/migrate/20190509190505_update_domain_block_severity_enum.rb19
-rw-r--r--db/migrate/20190509201242_add_force_options_to_accounts.rb8
-rw-r--r--db/migrate/20190509201451_add_adults_only_to_accounts.rb5
-rw-r--r--db/migrate/20190510071027_update_account_warning_action_enum.rb19
-rw-r--r--db/schema.rb5
35 files changed, 357 insertions, 81 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
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: "<strong>Silence</strong> will make the account's roars invisible to anyone who isn't following them. <strong>Suspend</strong> will remove all of the account's content, media, and profile data. Use <strong>None</strong> if you just want to reject media files."
+          desc_html: "<strong>Force Unlisted</strong> will force the account's roars to a maximum of unlisted visibility. <strong>Silence</strong> will make the account's roars invisible to anyone who isn't following them as well as disable notification. <strong>Suspend</strong> will remove all of the account's content, media, and profile data. Use <strong>None</strong> if you just want to reject media files."
           noop: None
+          force_unlisted: Force unlisted
           silence: Silence
           suspend: Suspend
         title: New domain block
+      force_sensitive: Mark media sensitive
+      force_sensitive_hint: Forces all media from this domain to be marked sensitive.
       reject_media: Reject media files
       reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions
       reject_reports: Reject reports
@@ -293,15 +308,17 @@ en:
       rejecting_media: rejecting media files
       rejecting_reports: rejecting reports
       severity:
+        force_unlisted: force unlisted
         silence: silenced
         suspend: suspended
       show:
         affected_accounts:
-          one: One account in the database affected
-          other: "%{count} accounts in the database affected"
+          one: One account affected
+          other: "%{count} accounts affected"
         retroactive:
           silence: Unsilence existing affected accounts from this domain
           suspend: Unsuspend existing affected accounts from this domain
+          force_unlisted: Allow public roars on all existing accounts from this domain
         title: Undo domain block for %{domain}
         undo: Undo
       undo: Undo domain block
@@ -1057,18 +1074,24 @@ en:
     warning:
       explanation:
         disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
+        force_sensitive: Your account's media has been forced to sensitive visibility until this limit is removed by a moderator.
+        force_unlisted: Your account's roars have been forced to unlisted visibility until this limit is removed by a moderator.
         silence: While your account is limited, only monsters who are already following you will see your roars on this server, and you may be excluded from various public listings. However, others may still manually join your pack.
-        suspend: Your account has been suspended, and all of your roars and your uploaded media files have been irreversibly removed from this server, and servers where you had packmates.
+        suspend: Your account has been suspended. All of your roars and your uploaded media files have been irreversibly removed from this server, and servers where you had packmates.
       review_server_policies: Review server policies
       subject:
-        disable: Your account %{acct} has been frozen
-        none: Warning for %{acct}
-        silence: Your account %{acct} has been limited
-        suspend: Your account %{acct} has been suspended
+        disable: "%{acct}, you account has been frozen."
+        none: "%{acct}, you've been given a moderation warning."
+        force_sensitive: "%{acct}, your account's media visibility has been limited."
+        force_unlisted: "%{acct}, your account's roar visibility has been limited."
+        silence: "%{acct}, your account has been silenced."
+        suspend: "%{acct}, your account has been suspended."
       title:
         disable: Account frozen
         none: Warning
-        silence: Account limited
+        force_sensitive: Media visibility limited
+        force_unlisted: Roar visibility limited
+        silence: Account silenced
         suspend: Account suspended
     welcome:
       edit_profile_action: Setup profile
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