about summary refs log tree commit diff
diff options
context:
space:
mode:
authormultiple creatures <dev@multiple-creature.party>2019-08-08 09:59:14 -0500
committermultiple creatures <dev@multiple-creature.party>2019-08-08 12:46:17 -0500
commit4dfc40324b1f3b20550982621501e162d2ed3bed (patch)
tree1578cba8809b7db27dc82f4d439471194d5fad31
parentd019e55b7bc496d3c4d942fb4ffe65bb7e149249 (diff)
add new `reject unknown` policy option to prevent spam & harassment from large/undermoderated servers
-rw-r--r--app/controllers/admin/accounts_controller.rb16
-rw-r--r--app/controllers/admin/domain_blocks_controller.rb4
-rw-r--r--app/helpers/log_helper.rb12
-rw-r--r--app/helpers/moderation_helper.rb17
-rw-r--r--app/lib/activitypub/activity.rb24
-rw-r--r--app/lib/activitypub/activity/announce.rb2
-rw-r--r--app/lib/activitypub/activity/create.rb8
-rw-r--r--app/lib/activitypub/activity/follow.rb2
-rw-r--r--app/models/account.rb14
-rw-r--r--app/models/account_warning.rb2
-rw-r--r--app/models/admin/account_action.rb9
-rw-r--r--app/models/domain_block.rb2
-rw-r--r--app/policies/account_policy.rb8
-rw-r--r--app/services/activitypub/fetch_remote_status_service.rb4
-rw-r--r--app/services/block_domain_service.rb51
-rw-r--r--app/services/favourite_service.rb2
-rw-r--r--app/services/fetch_remote_status_service.rb5
-rw-r--r--app/services/follow_service.rb2
-rw-r--r--app/services/post_status_service.rb5
-rw-r--r--app/services/reblog_service.rb2
-rw-r--r--app/services/resolve_url_service.rb2
-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/locales/simple_form.en.yml1
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20190807171841_add_known_to_accounts.rb5
-rw-r--r--db/migrate/20190807172051_mark_known_accounts.rb30
-rw-r--r--db/migrate/20190807221924_add_reject_unknown_to_domain_blocks.rb7
-rw-r--r--db/schema.rb4
31 files changed, 222 insertions, 37 deletions
diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb
index d486a97ba..9054a8aca 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, :allow_public, :allow_nonsensitive, :unsilence, :unsuspend, :memorialize, :approve, :reject]
+    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]
     before_action :require_remote_account!, only: [:redownload]
     before_action :require_local_account!, only: [:enable, :memorialize, :approve, :reject]
 
@@ -45,6 +45,20 @@ module Admin
       redirect_to admin_accounts_path(pending: '1')
     end
 
+    def mark_unknown
+      authorize @account, :mark_unknown?
+      @account.mark_unknown!
+      log_action :mark_unknown, @account
+      redirect_to admin_account_path(@account.id)
+    end
+
+    def mark_known
+      authorize @account, :mark_known?
+      @account.mark_known!
+      log_action :mark_known, @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 afba387cc..587c2c8c2 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -50,7 +50,7 @@ module Admin
       @domain_block.update(resource_params.except(:domain, :undo))
       changed = @domain_block.changed
       if @domain_block.save
-        DomainBlockWorker.perform_async(@domain_block.id) if (changed & %w(severity force_sensitive reject_media)).any?
+        DomainBlockWorker.perform_async(@domain_block.id) if (changed & %w(severity force_sensitive reject_media reject_unknown)).any?
         log_action :update, @domain_block
         flash[:notice] = I18n.t('admin.domain_blocks.updated_msg')
       else
@@ -66,7 +66,7 @@ module Admin
     end
 
     def resource_params
-      params.require(:domain_block).permit(:domain, :severity, :force_sensitive, :reject_media, :reject_reports, :reason, :undo)
+      params.require(:domain_block).permit(:domain, :severity, :force_sensitive, :reject_media, :reject_reports, :reject_unknown, :reason, :undo)
     end
   end
 end
diff --git a/app/helpers/log_helper.rb b/app/helpers/log_helper.rb
index 550ed2c15..6d0fdac13 100644
--- a/app/helpers/log_helper.rb
+++ b/app/helpers/log_helper.rb
@@ -5,7 +5,7 @@ module LogHelper
     case action
     when :create
       if target.is_a? DomainBlock
-        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" : ''} policy on https://#{target.domain}\u200b.\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" : ''} policy on https://#{target.domain}\u200b.\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
       elsif target.is_a? EmailDomainBlock
         LogWorker.perform_async("\u26d4 <#{source}> added a registration block on email domain '#{target.domain}'.")
       elsif target.is_a? CustomEmoji
@@ -26,7 +26,7 @@ module LogHelper
 
     when :update
       if target.is_a? DomainBlock
-        LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> changed the policy on https://#{target.domain} to #{target.severity}#{target.force_sensitive? ? " and force sensitive media" : ''}#{target.reject_media? ? " and reject media" : ''}.\n\n#{target.reason? ? "Comment: #{target.reason}" : ''}")
+        LogWorker.perform_async("\xf0\x9f\x9a\xab <#{source}> changed the policy on https://#{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\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
@@ -46,17 +46,21 @@ module LogHelper
         LogWorker.perform_async("\u26d4 <#{source}> disabled the '#{target.shortcode}' emoji.")
       end
 
+    when :mark_unknown
+      LogWorker.perform_async("\u2753 <#{source}> marked <#{target.acct}> as an unknown account.\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
       LogWorker.perform_async("\xf0\x9f\x94\x89 <#{source}> forced the posts of <#{target.acct}> to be unlisted.\n\n#{reason ? "Comment: #{reason}" : ''}")
     when :silence
-      LogWorker.perform_async("\xf0\x9f\x94\x87 <#{source}> silenced <#{target.acct}>'.\n\n#{reason ? "Comment: #{reason}" : ''}")
+      LogWorker.perform_async("\xf0\x9f\x94\x87 <#{source}> silenced <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
     when :suspend
       LogWorker.perform_async("\u26d4 <#{source}> suspended <#{target.acct}>.\n\n#{reason ? "Comment: #{reason}" : ''}")
 
+    when :mark_known
+      LogWorker.perform_async("\u2705 <#{source}> marked <#{target.acct}> as a known account.\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.")
+      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
       LogWorker.perform_async("\xf0\x9f\x86\x8a <#{source}> allowed <#{target.acct}> to post with public visibility.")
     when :unsilence
diff --git a/app/helpers/moderation_helper.rb b/app/helpers/moderation_helper.rb
index fda553c37..2f1561772 100644
--- a/app/helpers/moderation_helper.rb
+++ b/app/helpers/moderation_helper.rb
@@ -1,7 +1,7 @@
 module ModerationHelper
   include LogHelper
 
-  POLICIES = %w(silence unsilence suspend unsuspend force_unlisted allow_public force_sensitive allow_nonsensitive reset)
+  POLICIES = %w(silence unsilence suspend unsuspend force_unlisted mark_known mark_unknown reject_unknown allow_public force_sensitive allow_nonsensitive reset)
   EXCLUDED_DOMAINS = %w(tailma.ws monsterpit.net monsterpit.cloud monsterpit.gallery monsterpit.blog)
 
   def janitor_account
@@ -30,6 +30,10 @@ module ModerationHelper
     end
 
     case policy
+    when 'mark_unknown', 'reject_unknown'
+      acct.mark_unknown!
+    when 'mark_known'
+      acct.mark_known!
     when 'silence'
       acct.silence!
     when 'unsilence'
@@ -52,6 +56,7 @@ module ModerationHelper
       acct.unsilence!
       acct.allow_public!
       acct.allow_nonsensitive!
+      acct.mark_known!
     end
 
     acct.save
@@ -77,7 +82,7 @@ module ModerationHelper
     true
   end
 
-  def domain_policy(domain, policy, reason = nil, force_sensitive = false, reject_media = false, reject_reports = false)
+  def domain_policy(domain, policy, reason = nil, force_sensitive: false, reject_unknown: false, reject_media: false, reject_reports: false)
     return if policy.blank?
     policy = policy.to_s
     return false unless policy.in?(POLICIES)
@@ -87,10 +92,9 @@ module ModerationHelper
 
     return false if domain.in?(EXCLUDED_DOMAINS)
 
-    if policy == 'force_sensitive'
-      policy = 'noop'
-      force_sensitive = true
-    end
+    policy = 'noop' if policy == 'force_sensitive' || policy == 'reject_unknown'
+    force_sensitive = true if policy == 'force_sensitive'
+    reject_unknown = true if policy == 'reject_unknown'
 
     if policy.in? %w(silence suspend force_unlisted)
       return false unless domain_exists?(domain)
@@ -98,6 +102,7 @@ module ModerationHelper
       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.reject_media = reject_media
       domain_block.reject_reports = reject_reports
       domain_block.reason = reason.strip if reason && !reason.strip.blank?
diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb
index fee8fcd25..a708925a6 100644
--- a/app/lib/activitypub/activity.rb
+++ b/app/lib/activitypub/activity.rb
@@ -137,7 +137,7 @@ class ActivityPub::Activity
     redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
   end
 
-  def status_from_object
+  def status_from_object(announced_by: nil)
     # If the status is already known, return it
     status = status_from_uri(object_uri)
 
@@ -148,19 +148,20 @@ class ActivityPub::Activity
       actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
 
       if actor_id == @account.uri
-        return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
+        return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account, announced_by: announced_by).perform
       end
     end
 
-    fetch_remote_original_status
+    fetch_remote_original_status(announced_by: announced_by)
   end
 
-  def fetch_remote_original_status
+  def fetch_remote_original_status(announced_by: nil)
     if object_uri.start_with?('http')
       return if ActivityPub::TagManager.instance.local_uri?(object_uri)
-      ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first)
+      ActivityPub::FetchRemoteStatusService.new.call(object_uri, id: true, on_behalf_of: @account.followers.local.first, announced_by: announced_by)
     elsif @object['url'].present?
-      ::FetchRemoteStatusService.new.call(@object['url'])
+      options[:id] = true
+      ::FetchRemoteStatusService.new.call(@object['url'], announced_by: announced_by)
     end
   end
 
@@ -182,6 +183,17 @@ class ActivityPub::Activity
     @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
   end
 
+  def rejecting_unknown?(account = nil)
+    account = @account if account.nil?
+    DomainBlock.where(domain: account.domain, reject_unknown: true).exists?
+  end
+
+  def known?(account = nil)
+    account = @account if account.nil?
+    return true if account.known?
+    account.passive_relationships.exists?
+  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/announce.rb b/app/lib/activitypub/activity/announce.rb
index ada46b378..245126123 100644
--- a/app/lib/activitypub/activity/announce.rb
+++ b/app/lib/activitypub/activity/announce.rb
@@ -5,7 +5,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
     return if autoreject?
     return reject_payload! if !@options[:imported] && (delete_arrived_first?(@json['id']) || !related_to_local_activity?)
 
-    original_status = status_from_object
+    original_status = status_from_object(announced_by: @account)
 
     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 140226481..37aac4d59 100644
--- a/app/lib/activitypub/activity/create.rb
+++ b/app/lib/activitypub/activity/create.rb
@@ -5,6 +5,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
     return if autoreject?
     return reject_payload! if unsupported_object_type? || !@options[:imported] && (invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?)
 
+    unless known?
+      if @options[:announced_by].nil?
+        return reject_payload! if !@options[:requested] && rejecting_unknown?
+      else
+        @account.mark_known! if known?(@options[:announced_by])
+      end
+    end
+
     RedisLock.acquire(lock_options) do |lock|
       if lock.acquired?
         return if !@options[:imported] && (delete_arrived_first?(object_uri) || poll_vote?)
diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb
index 84041ec8d..bde507dcf 100644
--- a/app/lib/activitypub/activity/follow.rb
+++ b/app/lib/activitypub/activity/follow.rb
@@ -7,7 +7,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 target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved?
+    if (rejecting_unknown? && !known?) || target_account.blocking?(@account) || target_account.domain_blocking?(@account.domain) || target_account.moved?
       reject_follow_request!(target_account)
       return
     end
diff --git a/app/models/account.rb b/app/models/account.rb
index 6528a7ae9..97b0e93e4 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -51,6 +51,7 @@
 #  gently                  :boolean          default(FALSE), not null
 #  kobold                  :boolean          default(FALSE), not null
 #  froze                   :boolean
+#  known                   :boolean          default(FALSE), not null
 #
 
 class Account < ApplicationRecord
@@ -213,6 +214,14 @@ class Account < ApplicationRecord
     ResolveAccountService.new.call(acct)
   end
 
+  def mark_unknown!
+    update!(known: false)
+  end
+
+  def mark_known!
+    update!(known: true)
+  end
+
   def force_unlisted!
     transaction do
       update!(force_unlisted: true)
@@ -557,6 +566,7 @@ class Account < ApplicationRecord
 
   before_create :generate_keys
   before_create :set_domain_from_inbox_url
+  before_create :set_known, if: :local?
   before_validation :prepare_contents, if: :local?
   before_validation :prepare_username, on: :create
   before_destroy :clean_feed_manager
@@ -579,6 +589,10 @@ class Account < ApplicationRecord
     nil
   end
 
+  def set_known
+    self.known = true
+  end
+
   def generate_keys
     return unless local? && !Rails.env.test?
 
diff --git a/app/models/account_warning.rb b/app/models/account_warning.rb
index 4e06cf3d0..ddc3ff96e 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 force_sensitive force_unlisted silence suspend), _suffix: :action
+  enum action: %i(none disable force_sensitive force_unlisted silence suspend mark_unknown), _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 1ed464423..2195dc6a9 100644
--- a/app/models/admin/account_action.rb
+++ b/app/models/admin/account_action.rb
@@ -12,6 +12,7 @@ class Admin::AccountAction
     force_unlisted
     silence
     suspend
+    mark_unknown
   ).freeze
 
   attr_accessor :target_account,
@@ -66,6 +67,8 @@ class Admin::AccountAction
       handle_silence!
     when 'suspend'
       handle_suspend!
+    when 'mark_unknown'
+      handle_mark_unknown!
     end
   end
 
@@ -128,6 +131,12 @@ class Admin::AccountAction
     queue_suspension_worker!
   end
 
+  def handle_mark_unknown!
+    authorize(target_account, :mark_unknown?)
+    log_action(:mark_unknown, target_account.user)
+    target_account.mark_unknown!
+  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 e4baee5f0..7d3b65720 100644
--- a/app/models/domain_block.rb
+++ b/app/models/domain_block.rb
@@ -12,6 +12,7 @@
 #  reject_reports  :boolean          default(FALSE), not null
 #  force_sensitive :boolean          default(FALSE), not null
 #  reason          :text
+#  reject_unknown  :boolean          default(FALSE), not null
 #
 
 class DomainBlock < ApplicationRecord
@@ -52,6 +53,7 @@ class DomainBlock < ApplicationRecord
     additionals << "force sensitive media" if force_sensitive?
     additionals << "reject media" if reject_media?
     additionals << "reject reports" if reject_reports?
+    additionals << "reject unknown accounts" if reject_unknown?
     additionals
   end
 
diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb
index f3bda83db..35f9b98b3 100644
--- a/app/policies/account_policy.rb
+++ b/app/policies/account_policy.rb
@@ -13,6 +13,14 @@ class AccountPolicy < ApplicationPolicy
     staff? && !record.user&.staff?
   end
 
+  def mark_known?
+    staff?
+  end
+
+  def mark_unknown?
+    staff?
+  end
+
   def suspend?
     staff? && !record.user&.staff?
   end
diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb
index 42280ad74..423c7bc9a 100644
--- a/app/services/activitypub/fetch_remote_status_service.rb
+++ b/app/services/activitypub/fetch_remote_status_service.rb
@@ -5,7 +5,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
   include AutorejectHelper
 
   # Should be called when uri has already been checked for locality
-  def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil)
+  def call(uri, id: true, prefetched_body: nil, on_behalf_of: nil, announced_by: nil, requested: false)
     return if autoreject?(uri)
 
     @json = if prefetched_body.nil?
@@ -24,7 +24,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
 
     return if actor.nil? || actor.suspended?
 
-    ActivityPub::Activity.factory(activity_json, actor).perform
+    ActivityPub::Activity.factory(activity_json, actor, announced_by: announced_by, requested: requested).perform
   end
 
   private
diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb
index 908deacf4..5a6f4a6ce 100644
--- a/app/services/block_domain_service.rb
+++ b/app/services/block_domain_service.rb
@@ -5,8 +5,11 @@ class BlockDomainService < BaseService
 
   def call(domain_block)
     @domain_block = domain_block
+    @affected_status_ids = []
+
     remove_existing_block!
     process_domain_block!
+    invalidate_association_caches!
   end
 
   private
@@ -16,8 +19,9 @@ class BlockDomainService < BaseService
   end
 
   def process_domain_block!
-    clear_media! if domain_block.reject_media?
+    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?
 
     if domain_block.force_unlisted?
       force_accounts_unlisted!
@@ -39,17 +43,22 @@ class BlockDomainService < BaseService
   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|
+      blocked_domain_accounts.find_each do |account|
+        @affected_status_ids |= account.statuses.where(sensitive: false).pluck(:id)
         account.statuses.where(sensitive: false).in_batches.update_all(sensitive: true)
       end
     end
-    invalidate_association_caches! unless @affected_status_ids.blank?
+  end
+
+  def mark_unknown_accounts!
+    unknown_accounts.in_batches.update_all(known: false)
   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|
+      blocked_domain_accounts.find_each do |account|
+        @affected_status_ids |= account.statuses.with_public_visibility.pluck(:id)
         account.statuses.with_public_visibility.in_batches.update_all(visibility: :unlisted)
       end
     end
@@ -60,23 +69,21 @@ class BlockDomainService < BaseService
   end
 
   def clear_media!
-    @affected_status_ids = []
 
     clear_account_images!
     clear_account_attachments!
     clear_emojos!
 
-    invalidate_association_caches!
   end
 
   def suspend_accounts!
-    blocked_domain_accounts.without_suspended.reorder(nil).find_each do |account|
+    blocked_domain_accounts.without_suspended.find_each do |account|
       SuspendAccountService.new.call(account, suspended_at: @domain_block.created_at)
     end
   end
 
   def clear_account_images!
-    blocked_domain_accounts.reorder(nil).find_each do |account|
+    blocked_domain_accounts.find_each do |account|
       account.avatar.destroy if account.avatar.exists?
       account.header.destroy if account.header.exists?
       account.save
@@ -84,7 +91,7 @@ class BlockDomainService < BaseService
   end
 
   def clear_account_attachments!
-    media_from_blocked_domain.reorder(nil).find_each do |attachment|
+    media_from_blocked_domain.find_each do |attachment|
       @affected_status_ids << attachment.status_id if attachment.status_id.present?
 
       attachment.file.destroy if attachment.file.exists?
@@ -102,7 +109,7 @@ class BlockDomainService < BaseService
   end
 
   def blocked_domain_accounts
-    Account.where(domain: blocked_domain)
+    Account.where(domain: blocked_domain).reorder(nil)
   end
 
   def media_from_blocked_domain
@@ -112,4 +119,28 @@ class BlockDomainService < BaseService
   def emojis_from_blocked_domains
     CustomEmoji.where(domain: blocked_domain)
   end
+
+  def unknown_accounts
+    Account.where(id: blocked_domain_accounts.pluck(:id) - known_account_ids).reorder(nil)
+  end
+
+  def known_account_ids
+    local_accounts | packmates | boosted_authors | faved_authors
+  end
+
+  def boosted_authors
+    Status.where(id: Status.local.reblogs.reorder(nil).select(:reblog_of_id)).reorder(nil).pluck(:account_id)
+  end
+
+  def faved_authors
+    Status.where(id: Favourite.select(:status_id)).reorder(nil).pluck(:account_id)
+  end
+
+  def local_accounts
+    Account.local.pluck(:id)
+  end
+
+  def packmates
+    Account.local.flat_map { |account| account.following_ids | account.follower_ids }
+  end
 end
diff --git a/app/services/favourite_service.rb b/app/services/favourite_service.rb
index ee3248877..ce13b92ed 100644
--- a/app/services/favourite_service.rb
+++ b/app/services/favourite_service.rb
@@ -16,6 +16,8 @@ class FavouriteService < BaseService
 
     favourite = Favourite.create!(account: account, status: status)
 
+    status.account.mark_known! unless status.account.known?
+
     curate_status(status)
     create_notification(favourite) unless skip_notify
     bump_potential_friendship(account, status)
diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb
index 2c6dbb58d..4fde2b395 100644
--- a/app/services/fetch_remote_status_service.rb
+++ b/app/services/fetch_remote_status_service.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class FetchRemoteStatusService < BaseService
-  def call(url, prefetched_body = nil)
+  def call(url, prefetched_body = nil, announced_by: nil, requested: false)
     if prefetched_body.nil?
       resource_url, resource_options = FetchAtomService.new.call(url)
     else
@@ -9,6 +9,9 @@ class FetchRemoteStatusService < BaseService
       resource_options = { prefetched_body: prefetched_body }
     end
 
+    resource_options[:announced_by] = announced_by unless announced_by.nil?
+    resource_options[:requested] = true if requested
+
     return if resource_url.blank?
     ActivityPub::FetchRemoteStatusService.new.call(resource_url, **resource_options)
   end
diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb
index 3494dce99..776c4cb9b 100644
--- a/app/services/follow_service.rb
+++ b/app/services/follow_service.rb
@@ -14,6 +14,8 @@ class FollowService < BaseService
     raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
     raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account) || target_account.moved?
 
+    target_account.mark_known! unless target_account.known?
+
     if source_account.following?(target_account)
       # We're already following this account, but we'll call follow! again to
       # make sure the reblogs status is set correctly.
diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb
index bd076eaf4..6377248e5 100644
--- a/app/services/post_status_service.rb
+++ b/app/services/post_status_service.rb
@@ -94,6 +94,10 @@ class PostStatusService < BaseService
     @in_reply_to.present? && @in_reply_to.reject_replies && @in_reply_to.account_id != @account.id
   end
 
+  def mark_recipient_known
+    @in_reply_to.account.mark_known! unless @in_reply_to.account.known?
+  end
+
   def set_footer_from_i_am
     return if @footer.present? || @options[:no_footer]
     name = @account.user.vars['_they:are']
@@ -153,6 +157,7 @@ class PostStatusService < BaseService
     limit_visibility_if_silenced
 
     unless @in_reply_to.nil?
+      mark_recipient_known
       inherit_reply_rejection
       limit_visibility_to_reply
       unfilter_thread_on_reply
diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb
index 7d72357f9..1e7d4f3ca 100644
--- a/app/services/reblog_service.rb
+++ b/app/services/reblog_service.rb
@@ -17,6 +17,8 @@ class ReblogService < BaseService
     new_reblog = reblog.nil?
 
     if new_reblog
+      reblogged_status.account.mark_known! unless reblogged_status.account.known?
+
       visibility = options[:visibility] || account.user&.setting_default_privacy
       visibility = reblogged_status.visibility if reblogged_status.hidden?
       reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb
index 89a31c9ed..0c265b0db 100644
--- a/app/services/resolve_url_service.rb
+++ b/app/services/resolve_url_service.rb
@@ -21,7 +21,7 @@ class ResolveURLService < BaseService
     if equals_or_includes_any?(type, %w(Application Group Organization Person Service))
       FetchRemoteAccountService.new.call(atom_url, body)
     elsif equals_or_includes_any?(type, %w(Note Article Image Video Page Question))
-      FetchRemoteStatusService.new.call(atom_url, body)
+      FetchRemoteStatusService.new.call(atom_url, body, requested: true)
     end
   end
 
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index 0066ed8e7..f89b72422 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -144,6 +144,11 @@
     - 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.known?
+      = link_to t('admin.accounts.unknown'), mark_unknown_admin_account_path(@account.id), method: :post, class: 'button' if can?(:mark_unknown, @account)
+    - 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.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 d925edd64..588e512d4 100644
--- a/app/views/admin/domain_blocks/new.html.haml
+++ b/app/views/admin/domain_blocks/new.html.haml
@@ -15,6 +15,9 @@
     = 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_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 :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 02c73c268..ac13d57ab 100644
--- a/app/views/admin/domain_blocks/show.html.haml
+++ b/app/views/admin/domain_blocks/show.html.haml
@@ -15,6 +15,9 @@
     = 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_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 :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 95b20571a..6f34635f3 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -196,6 +196,8 @@ en:
       unsubscribe: Unsubscribe
       username: Username
       warn: Warn
+      known: Mark known
+      unknown: Mark unknown
       web: Web
     action_logs:
       actions:
@@ -235,6 +237,8 @@ en:
         update_custom_emoji: "%{name} updated emoji %{target}"
         update_domain_block: "%{name} updated policy for %{target}"
         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"
       deleted_status: "(deleted roar)"
       title: Audit log
     custom_emojis:
@@ -305,6 +309,8 @@ en:
         title: New domain policy
       force_sensitive: Mark media sensitive
       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.
       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/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 28750bcda..809c5dee8 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -70,6 +70,7 @@ en:
           none: Do nothing
           force_sensitive: Force sensitive
           force_unlisted: Force unlisted
+          mark_unknown: Mark unknown
           silence: Silence
           suspend: Suspend and irreversibly delete account data
         warning_preset_id: Use a warning preset
diff --git a/config/routes.rb b/config/routes.rb
index e716ab383..97a6821b3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -189,6 +189,8 @@ Rails.application.routes.draw do
         post :subscribe
         post :unsubscribe
         post :enable
+        post :mark_known
+        post :mark_unknown
         post :force_sensitive
         post :force_unlisted
         post :allow_public
diff --git a/db/migrate/20190807171841_add_known_to_accounts.rb b/db/migrate/20190807171841_add_known_to_accounts.rb
new file mode 100644
index 000000000..ff43f2a32
--- /dev/null
+++ b/db/migrate/20190807171841_add_known_to_accounts.rb
@@ -0,0 +1,5 @@
+class AddKnownToAccounts < ActiveRecord::Migration[5.2]
+  def change
+    add_column :accounts, :known, :boolean, null: false, default: false
+  end
+end
diff --git a/db/migrate/20190807172051_mark_known_accounts.rb b/db/migrate/20190807172051_mark_known_accounts.rb
new file mode 100644
index 000000000..95f9fe99c
--- /dev/null
+++ b/db/migrate/20190807172051_mark_known_accounts.rb
@@ -0,0 +1,30 @@
+class MarkKnownAccounts < ActiveRecord::Migration[5.2]
+  def up
+    Rails.logger.info("Marking known accounts:")
+    known_accounts = local_accounts | packmates | boosted_authors | faved_authors
+    Rails.logger.info("  Updating account flags...")
+    Account.where(id: known_accounts).in_batches.update_all(known: true)
+  end
+
+  private
+
+  def boosted_authors
+    Rails.logger.info("  Gathering boosted accounts...")
+    Status.where(id: Status.local.reblogs.reorder(nil).select(:reblog_of_id)).reorder(nil).pluck(:account_id)
+  end
+
+  def faved_authors
+    Rails.logger.info("  Gathering favourited accounts...")
+    Status.where(id: Favourite.select(:status_id)).reorder(nil).pluck(:account_id)
+  end
+
+  def local_accounts
+    Rails.logger.info("  Gathering local accounts...")
+    Account.local.pluck(:id)
+  end
+
+  def packmates
+    Rails.logger.info("  Gathering packmate accounts...")
+    Account.local.flat_map { |account| account.following_ids | account.follower_ids }
+  end
+end
diff --git a/db/migrate/20190807221924_add_reject_unknown_to_domain_blocks.rb b/db/migrate/20190807221924_add_reject_unknown_to_domain_blocks.rb
new file mode 100644
index 000000000..110d1fb79
--- /dev/null
+++ b/db/migrate/20190807221924_add_reject_unknown_to_domain_blocks.rb
@@ -0,0 +1,7 @@
+class AddRejectUnknownToDomainBlocks < ActiveRecord::Migration[5.2]
+  def change
+    safety_assured {
+      add_column :domain_blocks, :reject_unknown, :boolean, null: false, default: false
+    }
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a57ac2fd3..7f7d31bd2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2019_08_06_195913) do
+ActiveRecord::Schema.define(version: 2019_08_07_221924) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -154,6 +154,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_195913) do
     t.boolean "gently", default: false, null: false
     t.boolean "kobold", default: false, null: false
     t.boolean "froze"
+    t.boolean "known", 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"
@@ -274,6 +275,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_195913) do
     t.boolean "reject_reports", default: false, null: false
     t.boolean "force_sensitive", default: false, null: false
     t.text "reason"
+    t.boolean "reject_unknown", default: false, null: false
     t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true
   end