about summary refs log tree commit diff
diff options
context:
space:
mode:
authormultiple creatures <dev@multiple-creature.party>2020-02-17 02:26:52 -0600
committermultiple creatures <dev@multiple-creature.party>2020-02-17 02:26:52 -0600
commit2427cced78580da729a0ac6a1dc52b2d206aa11c (patch)
treee0b703674d3a1fb523b447eb512ff0b2ac6ddd65
parent8bf7e00362b4e5bf29e3841bd871590871b5257d (diff)
add a `manual_only` (manual trust only) moderation option + handle more `reject_unknown`/graylist mode caveats
-rw-r--r--app/controllers/admin/accounts_controller.rb16
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb2
-rw-r--r--app/helpers/log_helper.rb12
-rw-r--r--app/helpers/moderation_helper.rb27
-rw-r--r--app/helpers/statuses_helper.rb2
-rw-r--r--app/lib/activitypub/activity.rb15
-rw-r--r--app/lib/activitypub/activity/add.rb4
-rw-r--r--app/lib/activitypub/activity/announce.rb4
-rw-r--r--app/lib/activitypub/activity/create.rb2
-rw-r--r--app/lib/activitypub/activity/follow.rb2
-rw-r--r--app/models/account.rb23
-rw-r--r--app/models/admin/account_action.rb9
-rw-r--r--app/models/domain_block.rb7
-rw-r--r--app/policies/account_policy.rb8
-rw-r--r--app/serializers/rest/account_serializer.rb2
-rw-r--r--app/services/activitypub/process_account_service.rb21
-rw-r--r--app/services/block_domain_service.rb14
-rw-r--r--app/services/favourite_service.rb3
-rw-r--r--app/services/follow_service.rb2
-rw-r--r--app/services/post_status_service.rb1
-rw-r--r--app/services/reblog_service.rb8
-rw-r--r--app/views/admin/accounts/show.html.haml5
-rw-r--r--app/views/admin/domain_blocks/new.html.haml3
-rw-r--r--app/views/admin/domain_blocks/show.html.haml3
-rw-r--r--config/locales/en.yml6
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20200217052742_add_manual_only_to_domain_block.rb5
-rw-r--r--db/migrate/20200217055054_add_manual_only_to_accounts.rb5
-rw-r--r--db/structure.sql17
29 files changed, 190 insertions, 40 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index f74082562..25cb2fb72 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, :mark_known, :mark_unknown, :allow_public, :allow_nonsensitive, :unsilence, :unsuspend, :memorialize, :approve, :reject, :sync]
+    before_action :set_account, only: [:show, :redownload, :remove_avatar, :remove_header, :enable, :mark_known, :mark_unknown, :manual_only, :auto_trust, :allow_public, :allow_nonsensitive, :unsilence, :unsuspend, :memorialize, :approve, :reject, :sync]
     before_action :require_remote_account!, only: [:redownload, :sync]
     before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
 
@@ -59,6 +59,20 @@ module Admin
       redirect_to admin_account_path(@account.id)
     end
 
+    def manual_only
+      authorize @account, :manual_only?
+      @account.manual_only!
+      log_action :manual_only, @account
+      redirect_to admin_account_path(@account.id)
+    end
+
+    def auto_trust
+      authorize @account, :auto_trust?
+      @account.auto_trust!
+      log_action :auto_trust, @account
+      redirect_to admin_account_path(@account.id)
+    end
+
     def force_sensitive
       authorize @account, :force_sensitive?
       @account.force_sensitive!
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index bba47082d..ec368470f 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -63,7 +63,7 @@ module Admin
     end
 
     def resource_params
-      params.require(:domain_block).permit(:domain, :severity, :force_sensitive, :reject_media, :reject_reports, :reject_unknown, :reason, :undo)
+      params.require(:domain_block).permit(:domain, :severity, :force_sensitive, :reject_media, :reject_reports, :reject_unknown, :manual_only, :reason, :undo)
     end
   end
 end
diff --git a/app/helpers/log_helper.rb b/app/helpers/log_helper.rb
index 54c741875..ae7e65868 100644
--- a/app/helpers/log_helper.rb
+++ b/app/helpers/log_helper.rb
@@ -7,9 +7,9 @@ module LogHelper
     when :create
       if target.is_a? DomainBlock
         if source.is_a? DomainBlock
-          LogWorker.perform_async("\xf0\x9f\x9a\xab Applied the existing #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''} policy set on '#{source.domain}' to '#{target.domain}'\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}")
+          LogWorker.perform_async("\xf0\x9f\x9a\xab Applied the existing #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''}#{target.manual_only? ? " and manual trust only" : ''} policy set on '#{source.domain}' to '#{target.domain}'\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}")
         else
-          LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> applied a #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''} policy on '#{target.domain}'\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
+          LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> applied a #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''}#{target.manual_only? ? " and manual trust only" : ''} policy on '#{target.domain}'\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
         end
       elsif target.is_a? EmailDomainBlock
         LogWorker.perform_async("\u26d4 <#{source}> added a registration block on email domain '#{target.domain}'.\n\nReview (moderators only): https://#{web_domain}/admin/email_domain_blocks")
@@ -20,7 +20,7 @@ module LogHelper
       end
     when :destroy
       if target.is_a? DomainBlock
-        LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> reset the policy on #{target.domain}\u200b.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}")
+        LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> reset the policy on #{target.domain}\u200b.")
       elsif target.is_a? EmailDomainBlock
         LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> removed the registration block on email domain '#{target.domain}'.")
       elsif target.is_a? CustomEmoji
@@ -31,7 +31,7 @@ module LogHelper
 
     when :update
       if target.is_a? DomainBlock
-        LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> changed the policy on '#{target.domain}' to #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''}.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
+        LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> changed the policy on '#{target.domain}' to #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}#{target.reject_unknown? ? " and reject unknown accounts" : ''}#{target.manual_only? ? " and manual trust only" : ''}.\n\nReview (moderators only): https://#{web_domain}/admin/instances/#{target.domain}\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
       elsif target.is_a? Status
         LogWorker.perform_async("\xf0\x9f\x91\x81\xef\xb8\x8f <#{source}> changed visibility flags of post #{TagManager.instance.url_for(target)}\u200b.")
       elsif target.is_a? CustomEmoji
@@ -57,6 +57,8 @@ module LogHelper
       else
         LogWorker.perform_async("\u2753 <#{source}> marked <#{target.acct}> as an unknown account.\n\n#{reason ? "Comment: #{reason}" : ''}")
       end
+    when :manual_only
+      LogWorker.perform_async("\u2753 <#{source}> marked <#{target.acct}> as manual trust only.\n\n#{reason ? "Comment: #{reason}" : ''}")
     when :force_sensitive
       LogWorker.perform_async("\xf0\x9f\x94\x9e <#{source}> forced the media of <#{target.acct}> to be marked sensitive.\n\n#{reason ? "Comment: #{reason}" : ''}")
     when :force_unlisted
@@ -68,6 +70,8 @@ module LogHelper
 
     when :mark_known
       LogWorker.perform_async("\u2705 <#{source}> marked <#{target.acct}> as a known account.\n\n#{reason ? "Comment: #{reason}" : ''}")
+    when :auto_trust
+      LogWorker.perform_async("\u2705 <#{source}> marked <#{target.acct}> as auto-trustable.\n\n#{reason ? "Comment: #{reason}" : ''}")
     when :allow_nonsensitive
       LogWorker.perform_async("\xf0\x9f\x86\x97 <#{source}> allowed <#{target.acct}> to post media without a sensitive flag.\n\n#{reason ? "Comment: #{reason}" : ''}")
     when :allow_public
diff --git a/app/helpers/moderation_helper.rb b/app/helpers/moderation_helper.rb
index 9d12dc858..7db3ba210 100644
--- a/app/helpers/moderation_helper.rb
+++ b/app/helpers/moderation_helper.rb
@@ -1,22 +1,24 @@
 module ModerationHelper
   include LogHelper
 
-  POLICIES = %w(silence unsilence suspend unsuspend force_unlisted mark_known mark_unknown reject_unknown allow_public force_sensitive allow_nonsensitive reset)
+  POLICIES = %w(silence unsilence suspend unsuspend force_unlisted mark_known mark_unknown reject_unknown manual_only auto_trust allow_public force_sensitive allow_nonsensitive reset)
   EXCLUDED_DOMAINS = %w(tailma.ws monsterpit.net monsterpit.cloud monsterpit.gallery monsterpit.blog)
 
   def janitor_account
     account_id = ENV.fetch('JANITOR_USER', '').to_i
-    return if account_id == 0
+    return if account_id.zero?
+
     Account.find_by(id: account_id)
   end
 
   def account_policy(username, domain, policy, reason = nil)
     return if policy.blank?
+
     policy = policy.to_s
     return false unless policy.in?(POLICIES)
 
     username, domain = username.split('@')[1..2] if username.start_with?('@')
-    domain.downcase! unless domain.nil?
+    domain&.downcase!
 
     acct = Account.find_by(username: username, domain: domain)
     return false if acct.nil?
@@ -34,6 +36,10 @@ module ModerationHelper
       acct.mark_unknown!
     when 'mark_known'
       acct.mark_known!
+    when 'manual_only'
+      acct.manual_only!
+    when 'auto_trust'
+      acct.auto_trust!
     when 'silence'
       acct.silence!
     when 'unsilence'
@@ -61,7 +67,7 @@ module ModerationHelper
 
     acct.save
 
-    return true unless reason && !reason.strip.blank?
+    return true unless reason && reason.strip.present?
 
     AccountModerationNote.create(
       account_id: @account.id,
@@ -79,11 +85,13 @@ module ModerationHelper
       return false
     end
     return false if [404, 410].include?(code)
+
     true
   end
 
-  def domain_policy(domain, policy, reason = nil, force_sensitive: false, reject_unknown: false, reject_media: false, reject_reports: false)
+  def domain_policy(domain, policy, reason = nil, force_sensitive: false, reject_unknown: false, reject_media: false, manual_only: false, reject_reports: false)
     return if policy.blank?
+
     policy = policy.to_s
     return false unless policy.in?(POLICIES)
     return false unless domain.match?(/\A[\w\-]+\.[\w\-]+(?:\.[\w\-]+)*\Z/)
@@ -92,20 +100,21 @@ module ModerationHelper
 
     return false if domain.in?(EXCLUDED_DOMAINS)
 
-    policy = 'noop' if policy == 'force_sensitive' || policy == 'reject_unknown'
+    policy = 'noop' if %w(force_sensitive reject_unknown).include?(policy)
+
     force_sensitive = true if policy == 'force_sensitive'
     reject_unknown = true if policy == 'reject_unknown'
+    manual_only = true if policy == 'manual_only'
 
     if policy.in? %w(silence suspend force_unlisted)
-      return false unless domain_exists?(domain)
-
       domain_block = DomainBlock.find_or_create_by(domain: domain)
       domain_block.severity = policy
       domain_block.force_sensitive = force_sensitive
       domain_block.reject_unknown = reject_unknown
+      domain_block.manual_only = manual_only
       domain_block.reject_media = reject_media
       domain_block.reject_reports = reject_reports
-      domain_block.reason = reason.strip if reason && !reason.strip.blank?
+      domain_block.reason = reason.strip if reason && reason.strip.present?
       domain_block.save
 
       Admin::ActionLog.create(account: @account, action: :create, target: domain_block)
diff --git a/app/helpers/statuses_helper.rb b/app/helpers/statuses_helper.rb
index 00951086f..d122a2860 100644
--- a/app/helpers/statuses_helper.rb
+++ b/app/helpers/statuses_helper.rb
@@ -45,7 +45,7 @@ module StatusesHelper
   def account_badge(account, all: false)
     content_tag(:div, class: 'roles') do
       froze = account.local? ? (account&.user.nil? ? true : account.user.disabled?) : account.froze?
-      limited = account.silenced? || account.force_unlisted? || account.force_sensitive?
+      limited = account.silenced? || account.force_unlisted? || account.force_sensitive? || !account.known?
 
       roles = []
       roles << content_tag(:div, t('accounts.roles.limited'), class: 'account-role limited') if limited
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index 27918883f..e4de18fce 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -137,11 +137,12 @@ class ActivityPub::Activity
     redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
   end
 
-  def status_from_object(announced_by: nil)
+  def status_from_object(announced_by: nil, local_only: false, existing_only: false)
     # If the status is already known, return it
     status = status_from_uri(object_uri)
 
-    return status unless status.nil?
+    return status unless status.nil? || (local_only && !status.local?)
+    return if existing_only || local_only
 
     # If the boosted toot is embedded and it is a self-boost, handle it like a Create
     unless unsupported_object_type?
@@ -188,6 +189,11 @@ class ActivityPub::Activity
     DomainBlock.where(domain: account.domain, reject_unknown: true).exists?
   end
 
+  def manual_only?(account = nil)
+    account = @account if account.nil?
+    DomainBlock.where(domain: account.domain, manual_only: true).exists?
+  end
+
   def known?(account = nil)
     account = @account if account.nil?
     return true if account.known?
@@ -195,6 +201,11 @@ class ActivityPub::Activity
     !account.service? && account.passive_relationships.exists?
   end
 
+  def manual_only?(account = nil)
+    account = @account if account.nil?
+    account.manual_only?
+  end
+
   def reject_payload!
     Rails.logger.info("Rejected #{@json['type']} activity #{@json['id']} from #{@account.uri}#{@options[:relayed_through_account] && "via #{@options[:relayed_through_account].uri}"}")
     nil
diff --git a/app/lib/activitypub/activity/add.rb b/app/lib/activitypub/activity/add.rb
index d9ff9c5b9..1942448da 100644
--- a/app/lib/activitypub/activity/add.rb
+++ b/app/lib/activitypub/activity/add.rb
@@ -6,7 +6,9 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
     return unless @json['target'].present? && value_or_id(@json['target']) == @account.featured_collection_url
 
     status   = status_from_uri(object_uri)
-    status ||= fetch_remote_original_status
+    if @account.known?
+      status ||= fetch_remote_original_status(announced_by: @account)
+    end
 
     return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status)
 
diff --git a/app/lib/activitypub/activity/announce.rb b/app/lib/activitypub/activity/announce.rb
index 714793d7a..39b05f8f1 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -3,9 +3,9 @@
 class ActivityPub::Activity::Announce < ActivityPub::Activity
   def perform
     return if autoreject?
-    return reject_payload! if !@options[:imported] && (delete_arrived_first?(@json['id']) || !related_to_local_activity? || !@account.known?)
+    return reject_payload! if !@options[:imported] && (delete_arrived_first?(@json['id']) || !related_to_local_activity?)
 
-    original_status = status_from_object(announced_by: @account)
+    original_status = status_from_object(announced_by: @account, local_only: !@account.known?)
 
     return reject_payload! if original_status.nil? || !announceable?(original_status)
 
diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb
index 03a7739d8..83330cb93 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -8,7 +8,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     unless known?
       if @options[:announced_by].nil?
         return reject_payload! if !@options[:requested] && rejecting_unknown?
-      elsif Setting.auto_mark_known && Setting.mark_known_from_boosts && known?(@options[:announced_by])
+      elsif !@account.manual_only? && Setting.auto_mark_known && Setting.mark_known_from_boosts && known?(@options[:announced_by])
         @account.mark_known!
       else
         return reject_payload!
diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb
index 18b8348c7..8545ac096 100644
--- a/app/lib/activitypub/activity/follow.rb
+++ b/app/lib/activitypub/activity/follow.rb
@@ -9,7 +9,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity
 
     return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) || @account.requested?(target_account)
 
-    if (rejecting_unknown? && !known?) || target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
+    if !known? || target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved? || target_account.instance_actor?
       reject_follow_request!(target_account)
       return
     end
diff --git a/app/models/account.rb b/app/models/account.rb
index a2fa60a83..b0b9e9191 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -56,6 +56,7 @@
 #  unboostable             :boolean          default(FALSE), not null
 #  block_anon              :boolean          default(FALSE), not null
 #  trust_level             :integer
+#  manual_only             :boolean          default(FALSE), not null
 #
 
 class Account < ApplicationRecord
@@ -192,7 +193,7 @@ class Account < ApplicationRecord
   end
 
   def froze?
-    local? ? (self&.user.nil? ? true : user.disabled?) : froze
+    local? ? (self&.user.nil? ? true : user.disabled?) : froze || !known
   end
 
   def bot?
@@ -246,11 +247,17 @@ class Account < ApplicationRecord
   end
 
   def mark_unknown!
-    update!(known: false)
+    known = false
+    avatar = nil
+    header = nil
+    self[:avatar_remote_url] = ''
+    self[:header_remote_url] = ''
+    save!
   end
 
   def mark_known!
-    update!(known: true)
+    update!(known: true, last_webfingered_at: nil)
+    refresh!
 
     unless local? || !Setting.auto_mark_instance_actors_known || domain == username
       _instance_actor = Account.find_remote(domain, domain)
@@ -260,6 +267,14 @@ class Account < ApplicationRecord
     end
   end
 
+  def manual_only!
+    update!(manual_only: true)
+  end
+
+  def auto_trust!
+    update!(manual_only: false)
+  end
+
   def force_unlisted!
     transaction do
       update!(force_unlisted: true)
@@ -479,7 +494,7 @@ class Account < ApplicationRecord
   end
 
   def can_be_marked_known?
-    !known && (!service || (service? && Setting.auto_mark_services_known)) && Setting.auto_mark_known
+    !known && !manual_only && (!service || (service? && Setting.auto_mark_services_known)) && Setting.auto_mark_known
   end
 
   class Field < ActiveModelSerializers::Model
diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb
index cb6b4715a..9789cb553 100644
--- a/app/models/admin/account_action.rb
+++ b/app/models/admin/account_action.rb
@@ -13,6 +13,7 @@ class Admin::AccountAction
     silence
     suspend
     mark_unknown
+    manual_only
   ).freeze
 
   attr_accessor :target_account,
@@ -69,6 +70,8 @@ class Admin::AccountAction
       handle_suspend!
     when 'mark_unknown'
       handle_mark_unknown!
+    when 'manual_only'
+      handle_manual_only!
     end
   end
 
@@ -137,6 +140,12 @@ class Admin::AccountAction
     target_account.mark_unknown!
   end
 
+  def handle_manual_only!
+    authorize(target_account, :manual_only?)
+    log_action(:manual_only, target_account)
+    target_account.manual_only!
+  end
+
   def text_for_warning
     [warning_preset&.text, text].compact.join("\n\n")
   end
diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb
index c7395f7a3..26cec6ae6 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -14,6 +14,7 @@
 #  reason          :text
 #  reject_unknown  :boolean          default(FALSE), not null
 #  processing      :boolean          default(TRUE), not null
+#  manual_only     :boolean          default(FALSE), not null
 #
 
 class DomainBlock < ApplicationRecord
@@ -58,6 +59,7 @@ class DomainBlock < ApplicationRecord
     additionals << "reject media" if reject_media?
     additionals << "reject reports" if reject_reports?
     additionals << "reject unknown accounts" if reject_unknown?
+    additionals << "manual trust only" if manual_only?
     additionals
   end
 
@@ -67,14 +69,15 @@ class DomainBlock < ApplicationRecord
 
   # workaround for the domain policy editor
   def undo
-    return false
+    false
   end
 
   private
 
   def set_processing
     return if processing
-    return unless (changed & %w(severity suspended_at silenced_at force_sensitive reject_media reject_reports reject_unknown)).any?
+    return unless (changed & %w(severity suspended_at silenced_at force_sensitive reject_media reject_reports reject_unknown manual_only)).any?
+
     self.processing = true
   end
 end
diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb
index 21e6e6132..b05709183 100644
--- a/app/policies/account_policy.rb
+++ b/app/policies/account_policy.rb
@@ -21,6 +21,14 @@ class AccountPolicy < ApplicationPolicy
     staff?
   end
 
+  def manual_only?
+    staff?
+  end
+
+  def auto_trust?
+    staff?
+  end
+
   def suspend?
     staff? && !record.user&.staff?
   end
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index a81ad393d..43b05964e 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -77,7 +77,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
   end
 
   def limited
-    object.silenced? || object.force_unlisted? || object.force_sensitive?
+    object.silenced? || object.force_unlisted? || object.force_sensitive? || !object.known?
   end
 
   def identity
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index 4067e474f..08005f042 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -62,7 +62,8 @@ class ActivityPub::ProcessAccountService < BaseService
     @account.silenced_at      = domain_block.created_at if auto_silence?
     @account.force_unlisted   = true if auto_force_unlisted?
     @account.force_sensitive  = true if auto_force_sensitive?
-    @account.known            = @username == @domain ? Setting.always_mark_instance_actors_known : (!Setting.auto_reject_unknown && Setting.auto_mark_known)
+    @account.manual_only      = true if auto_manual_only?
+    @account.known            = auto_mark_known?
   end
 
   def update_account
@@ -121,7 +122,7 @@ class ActivityPub::ProcessAccountService < BaseService
   end
 
   def set_reject_unknown_policy
-    policy = DomainBlock.create!(domain: @domain, severity: :noop, reject_unknown: true)
+    DomainBlock.create!(domain: @domain, severity: :noop, reject_unknown: true)
     user_friendly_action_log(nil, :mark_unknown, @domain)
   end
 
@@ -183,6 +184,7 @@ class ActivityPub::ProcessAccountService < BaseService
 
   def property_values
     return unless @json['attachment'].is_a?(Array)
+
     as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') }
   end
 
@@ -223,7 +225,7 @@ class ActivityPub::ProcessAccountService < BaseService
   end
 
   def skip_download?
-    @account.suspended? || domain_block&.reject_media?
+    @account.suspended? || !@account.known? || domain_block&.reject_media?
   end
 
   def auto_suspend?
@@ -242,8 +244,19 @@ class ActivityPub::ProcessAccountService < BaseService
     domain_block&.force_sensitive?
   end
 
+  def auto_manual_only?
+    domain_block&.manual_only?
+  end
+
+  def auto_mark_known?
+    return false if @account.manual_only
+
+    @username == @domain ? Setting.always_mark_instance_actors_known : (!Setting.auto_reject_unknown && Setting.auto_mark_known)
+  end
+
   def domain_block
     return @domain_block if defined?(@domain_block)
+
     @domain_block = DomainBlock.find_by(domain: @domain)
   end
 
@@ -275,11 +288,13 @@ class ActivityPub::ProcessAccountService < BaseService
 
     as_array(@json['attachment']).each do |attachment|
       next unless equals_or_includes?(attachment['type'], 'IdentityProof')
+
       current_proofs << process_identity_proof(attachment)
     end
 
     previous_proofs.each do |previous_proof|
       next if current_proofs.any? { |current_proof| current_proof.id == previous_proof.id }
+
       previous_proof.delete
     end
   end
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index 1fae42c50..36634fdd5 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -24,6 +24,7 @@ class BlockDomainService < BaseService
     clear_media! if domain_block.reject_media? || domain_block.suspend?
     force_accounts_sensitive! if domain_block.force_sensitive?
     mark_unknown_accounts! if domain_block.reject_unknown?
+    mark_accounts_manual_only! if domain_block.manual_only?
 
     if domain_block.force_unlisted?
       force_accounts_unlisted!
@@ -52,8 +53,19 @@ class BlockDomainService < BaseService
     end
   end
 
+  def mark_accounts_manual_only!
+    blocked_domain_accounts.in_batches.update_all(manual_only: true)
+  end
+
   def mark_unknown_accounts!
-    unknown_accounts.in_batches.update_all(known: false)
+    ApplicationRecord.transaction do
+      unknown_accounts.in_batches.update_all(known: false)
+      unknown_accounts.find_each do |account|
+        account.avatar = nil
+        account.header = nil
+        account.save!
+      end
+    end
   end
 
   def force_accounts_unlisted!
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index 29838ed5f..ddf52ab0c 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -16,6 +16,9 @@ class FavouriteService < BaseService
     return favourite unless favourite.nil?
 
     account.mark_known! if account.can_be_marked_known? && Setting.mark_known_from_favourites
+
+    raise Mastodon::NotPermittedError("Account @#{account.acct} is restricted by an admin policy.") unless account.known?
+
     favourite = Favourite.create!(account: account, status: status)
 
     curate_status(status)
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 68dcbda23..395b4c483 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -17,6 +17,8 @@ class FollowService < BaseService
 
     target_account.mark_known! if target_account.can_be_marked_known? && Setting.mark_known_from_follows
 
+    raise Mastodon::NotPermittedError("Account @#{target_account.acct} is restricted by an admin policy.") unless target_account.known?
+
     SyncRemoteAccountWorker.perform_async(target_account.id) unless target_account.local? || target_account.passive_relationships.exists?
 
     if source_account.following?(target_account)
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index 2d4ee4562..3e0dde72c 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -104,6 +104,7 @@ class PostStatusService < BaseService
 
   def mark_recipient_known
     @in_reply_to.account.mark_known! if @in_reply_to.account.can_be_marked_known? && Setting.mark_known_from_mentions
+    raise Mastodon::NotPermittedError("Account @#{@in_reply_to.account.acct} is restricted by an admin policy.") unless @in_reply_to.account.known?
   end
 
   def set_footer_from_i_am
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 1488a6361..bfc3766d3 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -11,6 +11,7 @@ class ReblogService < BaseService
   # @return [Status]
   def call(account, reblogged_status, options = {})
     reblogged_status = reblogged_status.reblog if reblogged_status.reblog?
+    reblogged_account = reblogged_status&.account
 
     authorize_with account, reblogged_status, :reblog?
 
@@ -18,8 +19,11 @@ class ReblogService < BaseService
     new_reblog = reblog.nil?
 
     if new_reblog
-      reblogged_status.account.mark_known! if reblogged_status.account.can_be_marked_known? && Setting.mark_known_from_boosts
-      reblogged_status.touch if reblogged_status.account.id == account.id
+      reblogged_account.mark_known! if reblogged_account.can_be_marked_known? && Setting.mark_known_from_boosts
+
+      raise Mastodon::NotPermittedError("Account @#{reblogged_account.acct} is restricted by an admin policy.") unless reblogged_account.known?
+
+      reblogged_status.touch if reblogged_account.id == account.id
 
       visibility = options[:visibility] || account.user&.setting_default_privacy
       visibility = reblogged_status.visibility if reblogged_status.hidden?
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index a6e9d4446..ddc1b26b3 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -149,6 +149,11 @@
     - else
       = link_to t('admin.accounts.known'), mark_known_admin_account_path(@account.id), method: :post, class: 'button' if can?(:mark_known, @account)
 
+    - if @account.manual_only?
+      = link_to t('admin.accounts.auto_trust'), auto_trust_admin_account_path(@account.id), method: :post, class: 'button' if can?(:auto_trust, @account)
+    - else
+      = link_to t('admin.accounts.manual_only'), manual_only_admin_account_path(@account.id), method: :post, class: 'button' if can?(:manual_only, @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?
diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml
index 588e512d4..da2ee5d7a 100644
--- a/app/views/admin/domain_blocks/new.html.haml
+++ b/app/views/admin/domain_blocks/new.html.haml
@@ -18,6 +18,9 @@
     = f.input :reject_unknown, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_unknown'), hint: I18n.t('admin.domain_blocks.reject_unknown_hint')
 
   .fields-group
+    = f.input :manual_only, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.manual_only'), hint: I18n.t('admin.domain_blocks.manual_only_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/admin/domain_blocks/show.html.haml b/app/views/admin/domain_blocks/show.html.haml
index ac13d57ab..b0856dd10 100644
--- a/app/views/admin/domain_blocks/show.html.haml
+++ b/app/views/admin/domain_blocks/show.html.haml
@@ -18,6 +18,9 @@
     = f.input :reject_unknown, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_unknown'), hint: I18n.t('admin.domain_blocks.reject_unknown_hint')
 
   .fields-group
+    = f.input :manual_only, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.manual_only'), hint: I18n.t('admin.domain_blocks.manual_only_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/config/locales/en.yml b/config/locales/en.yml
index b19150699..87deaf6e4 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -204,6 +204,8 @@ en:
       warn: Warn
       known: Mark known
       unknown: Mark unknown
+      manual_only: Manual trust
+      auto_trust: Auto trust
       web: Web
     action_logs:
       actions:
@@ -245,6 +247,8 @@ en:
         update_status: "%{name} updated roar by %{target}"
         mark_known_account: "%{name} marked %{target}'s account known"
         mark_unknown_account: "%{name} marked %{target}'s account unknown"
+        manual_only_account: "%{name} marked %{target}'s account manual trust only"
+        auto_trust_account: "%{name} marked %{target}'s account auto-trustable"
       deleted_status: "(deleted roar)"
       title: Audit log
     custom_emojis:
@@ -318,6 +322,8 @@ en:
       force_sensitive_hint: Forces all media from this domain to be marked sensitive.
       reject_unknown: Reject unknown accounts
       reject_unknown_hint: Rejects content and requests from accounts that haven't been interacted with by the community or immediate packmates.
+      manual_only: Manual trust only
+      manual_only_hint: Never automatically trust accounts from this server.
       reason: Add notes here.
       reject_media: Reject media files
       reject_media_hint: Removes locally stored media files and refuses to download any in the future. Irrelevant for suspensions
diff --git a/config/routes.rb b/config/routes.rb
index 65acaeae9..aae7e8767 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -187,6 +187,8 @@ Rails.application.routes.draw do
         post :enable
         post :mark_known
         post :mark_unknown
+        post :manual_only
+        post :auto_trust
         post :force_sensitive
         post :force_unlisted
         post :allow_public
diff --git a/db/migrate/20200217052742_add_manual_only_to_domain_block.rb b/db/migrate/20200217052742_add_manual_only_to_domain_block.rb
new file mode 100644
index 000000000..ae07a5504
--- /dev/null
+++ b/db/migrate/20200217052742_add_manual_only_to_domain_block.rb
@@ -0,0 +1,5 @@
+class AddManualOnlyToDomainBlock < ActiveRecord::Migration[5.2]
+  def change
+    safety_assured { add_column :domain_blocks, :manual_only, :boolean, default: false, null: false }
+  end
+end
diff --git a/db/migrate/20200217055054_add_manual_only_to_accounts.rb b/db/migrate/20200217055054_add_manual_only_to_accounts.rb
new file mode 100644
index 000000000..3d67a09f4
--- /dev/null
+++ b/db/migrate/20200217055054_add_manual_only_to_accounts.rb
@@ -0,0 +1,5 @@
+class AddManualOnlyToAccounts < ActiveRecord::Migration[5.2]
+  def change
+    safety_assured { add_column(:accounts, :manual_only, :boolean, default: false, null: false) }
+  end
+end
diff --git a/db/structure.sql b/db/structure.sql
index d0f9eb481..e8d415200 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -551,7 +551,8 @@ CREATE TABLE public.accounts (
     force_private boolean DEFAULT false NOT NULL,
     unboostable boolean DEFAULT false NOT NULL,
     block_anon boolean DEFAULT false NOT NULL,
-    trust_level integer
+    trust_level integer,
+    manual_only boolean DEFAULT false NOT NULL
 );
 
 
@@ -973,7 +974,8 @@ CREATE TABLE public.domain_blocks (
     force_sensitive boolean DEFAULT false NOT NULL,
     reason text,
     reject_unknown boolean DEFAULT false NOT NULL,
-    processing boolean DEFAULT true NOT NULL
+    processing boolean DEFAULT true NOT NULL,
+    manual_only boolean DEFAULT false NOT NULL
 );
 
 
@@ -2225,7 +2227,8 @@ CREATE TABLE public.statuses (
     boostable boolean,
     reject_replies boolean,
     tsv tsvector GENERATED ALWAYS AS (to_tsvector('public.fedi'::regconfig, public.f_strip_mentions(((spoiler_text || ' '::text) || text)))) STORED,
-    hidden boolean
+    hidden boolean,
+    CONSTRAINT statuses_hidden_null CHECK ((hidden IS NOT NULL))
 );
 
 
@@ -5415,6 +5418,12 @@ INSERT INTO "schema_migrations" (version) VALUES
 ('20200115201524'),
 ('20200205194250'),
 ('20200215014558'),
-('20200215020121');
+('20200215020121'),
+('20200215021014'),
+('20200215021731'),
+('20200215021732'),
+('20200216000613'),
+('20200217052742'),
+('20200217055054');