about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/api/v1/domain_blocks_controller.rb38
-rw-r--r--app/lib/feed_manager.rb6
-rw-r--r--app/models/account.rb109
-rw-r--r--app/models/account_domain_block.rb26
-rw-r--r--app/models/concerns/account_avatar.rb2
-rw-r--r--app/models/concerns/account_header.rb2
-rw-r--r--app/models/concerns/account_interactions.rb127
-rw-r--r--app/models/status.rb13
-rw-r--r--app/services/notify_service.rb1
-rw-r--r--config/routes.rb3
-rw-r--r--db/migrate/20170424003227_create_account_domain_blocks.rb12
-rw-r--r--db/schema.rb10
-rw-r--r--spec/controllers/api/v1/domain_blocks_controller_spec.rb55
-rw-r--r--spec/controllers/api/v1/media_controller_spec.rb1
-rw-r--r--spec/fabricators/account_domain_block_fabricator.rb4
-rw-r--r--spec/lib/feed_manager_spec.rb10
-rw-r--r--spec/models/account_domain_block_spec.rb5
-rw-r--r--spec/models/status_spec.rb93
-rw-r--r--spec/services/notify_service_spec.rb7
-rw-r--r--streaming/index.js20
20 files changed, 420 insertions, 124 deletions
diff --git a/app/controllers/api/v1/domain_blocks_controller.rb b/app/controllers/api/v1/domain_blocks_controller.rb
new file mode 100644
index 000000000..e14547911
--- /dev/null
+++ b/app/controllers/api/v1/domain_blocks_controller.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class Api::V1::DomainBlocksController < ApiController
+  before_action -> { doorkeeper_authorize! :follow }
+  before_action :require_user!
+
+  respond_to :json
+
+  def show
+    @blocks = AccountDomainBlock.where(account: current_account).paginate_by_max_id(limit_param(100), params[:max_id], params[:since_id])
+
+    next_path = api_v1_domain_blocks_url(pagination_params(max_id: @blocks.last.id))    if @blocks.size == limit_param(100)
+    prev_path = api_v1_domain_blocks_url(pagination_params(since_id: @blocks.first.id)) unless @blocks.empty?
+
+    set_pagination_headers(next_path, prev_path)
+    render json: @blocks.map(&:domain)
+  end
+
+  def create
+    current_account.block_domain!(domain_block_params[:domain])
+    render_empty
+  end
+
+  def destroy
+    current_account.unblock_domain!(domain_block_params[:domain])
+    render_empty
+  end
+
+  private
+
+  def pagination_params(core_params)
+    params.permit(:limit).merge(core_params)
+  end
+
+  def domain_block_params
+    params.permit(:domain)
+  end
+end
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index aaff9acd3..c2d3a2e2c 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -98,7 +98,7 @@ class FeedManager
 
     return true if Mute.where(account_id: receiver_id, target_account_id: check_for_mutes).any?
 
-    check_for_blocks = status.mentions.map(&:account_id)
+    check_for_blocks = status.mentions.pluck(:account_id)
     check_for_blocks.concat([status.reblog.account_id]) if status.reblog?
 
     return true if Block.where(account_id: receiver_id, target_account_id: check_for_blocks).any?
@@ -109,7 +109,9 @@ class FeedManager
       should_filter &&= !(status.account_id == status.in_reply_to_account_id)                                            # and it's not a self-reply
       return should_filter
     elsif status.reblog?                                                                                                 # Filter out a reblog
-      return Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists?                   # or if the author of the reblogged status is blocking me
+      should_filter   = Block.where(account_id: status.reblog.account_id, target_account_id: receiver_id).exists?        # or if the author of the reblogged status is blocking me
+      should_filter ||= AccountDomainBlock.where(account_id: receiver_id, domain: status.reblog.account.domain).exists?  # or the author's domain is blocked
+      return should_filter
     end
 
     false
diff --git a/app/models/account.rb b/app/models/account.rb
index 03e7db398..f418a0f8b 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -43,6 +43,7 @@ class Account < ApplicationRecord
 
   include AccountAvatar
   include AccountHeader
+  include AccountInteractions
   include Attachmentable
   include Remotable
   include Targetable
@@ -67,26 +68,6 @@ class Account < ApplicationRecord
   has_many :mentions, inverse_of: :account, dependent: :destroy
   has_many :notifications, inverse_of: :account, dependent: :destroy
 
-  # Follow relations
-  has_many :follow_requests, dependent: :destroy
-
-  has_many :active_relationships,  class_name: 'Follow', foreign_key: 'account_id',        dependent: :destroy
-  has_many :passive_relationships, class_name: 'Follow', foreign_key: 'target_account_id', dependent: :destroy
-
-  has_many :following, -> { order('follows.id desc') }, through: :active_relationships,  source: :target_account
-  has_many :followers, -> { order('follows.id desc') }, through: :passive_relationships, source: :account
-
-  # Block relationships
-  has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy
-  has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account
-  has_many :blocked_by_relationships, class_name: 'Block', foreign_key: :target_account_id, dependent: :destroy
-  has_many :blocked_by, -> { order('blocks.id desc') }, through: :blocked_by_relationships, source: :account
-
-  # Mute relationships
-  has_many :mute_relationships, class_name: 'Mute', foreign_key: 'account_id', dependent: :destroy
-  has_many :muting, -> { order('mutes.id desc') }, through: :mute_relationships, source: :target_account
-  has_many :conversation_mutes
-
   # Media
   has_many :media_attachments, dependent: :destroy
 
@@ -120,62 +101,6 @@ class Account < ApplicationRecord
 
   delegate :allowed_languages, to: :user, prefix: false, allow_nil: true
 
-  def follow!(other_account)
-    active_relationships.find_or_create_by!(target_account: other_account)
-  end
-
-  def block!(other_account)
-    block_relationships.find_or_create_by!(target_account: other_account)
-  end
-
-  def mute!(other_account)
-    mute_relationships.find_or_create_by!(target_account: other_account)
-  end
-
-  def mute_conversation!(conversation)
-    conversation_mutes.find_or_create_by!(conversation: conversation)
-  end
-
-  def unfollow!(other_account)
-    follow = active_relationships.find_by(target_account: other_account)
-    follow&.destroy
-  end
-
-  def unblock!(other_account)
-    block = block_relationships.find_by(target_account: other_account)
-    block&.destroy
-  end
-
-  def unmute!(other_account)
-    mute = mute_relationships.find_by(target_account: other_account)
-    mute&.destroy
-  end
-
-  def unmute_conversation!(conversation)
-    mute = conversation_mutes.find_by(conversation: conversation)
-    mute&.destroy!
-  end
-
-  def following?(other_account)
-    following.include?(other_account)
-  end
-
-  def blocking?(other_account)
-    blocking.include?(other_account)
-  end
-
-  def muting?(other_account)
-    muting.include?(other_account)
-  end
-
-  def muting_conversation?(conversation)
-    conversation_mutes.where(conversation: conversation).exists?
-  end
-
-  def requested?(other_account)
-    follow_requests.where(target_account: other_account).exists?
-  end
-
   def local?
     domain.nil?
   end
@@ -200,14 +125,6 @@ class Account < ApplicationRecord
     followers.reorder(nil).pluck('distinct accounts.domain')
   end
 
-  def favourited?(status)
-    status.proper.favourites.where(account: self).exists?
-  end
-
-  def reblogged?(status)
-    status.proper.reblogs.where(account: self).exists?
-  end
-
   def keypair
     OpenSSL::PKey::RSA.new(private_key || public_key)
   end
@@ -238,6 +155,10 @@ class Account < ApplicationRecord
     Rails.cache.fetch("exclude_account_ids_for:#{id}") { blocking.pluck(:target_account_id) + blocked_by.pluck(:account_id) + muting.pluck(:target_account_id) }
   end
 
+  def excluded_from_timeline_domains
+    Rails.cache.fetch("exclude_domains_for:#{id}") { domain_blocks.pluck(:domain) }
+  end
+
   class << self
     def find_local!(username)
       find_remote!(username, nil)
@@ -321,26 +242,6 @@ class Account < ApplicationRecord
       find_by_sql([sql, account.id, account.id, limit])
     end
 
-    def following_map(target_account_ids, account_id)
-      follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
-    end
-
-    def followed_by_map(target_account_ids, account_id)
-      follow_mapping(Follow.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
-    end
-
-    def blocking_map(target_account_ids, account_id)
-      follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
-    end
-
-    def muting_map(target_account_ids, account_id)
-      follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
-    end
-
-    def requested_map(target_account_ids, account_id)
-      follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
-    end
-
     private
 
     def generate_query_for_search(terms)
diff --git a/app/models/account_domain_block.rb b/app/models/account_domain_block.rb
new file mode 100644
index 000000000..9241d9720
--- /dev/null
+++ b/app/models/account_domain_block.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+# == Schema Information
+#
+# Table name: account_domain_blocks
+#
+#  id         :integer          not null, primary key
+#  account_id :integer
+#  domain     :string
+#  created_at :datetime         not null
+#  updated_at :datetime         not null
+#
+
+class AccountDomainBlock < ApplicationRecord
+  include Paginable
+
+  belongs_to :account, required: true
+
+  after_create  :remove_blocking_cache
+  after_destroy :remove_blocking_cache
+
+  private
+
+  def remove_blocking_cache
+    Rails.cache.delete("exclude_domains_for:#{account_id}")
+  end
+end
diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb
index c664366ef..73507a328 100644
--- a/app/models/concerns/account_avatar.rb
+++ b/app/models/concerns/account_avatar.rb
@@ -2,6 +2,7 @@
 
 module AccountAvatar
   extend ActiveSupport::Concern
+
   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
 
   class_methods do
@@ -10,6 +11,7 @@ module AccountAvatar
       styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
       styles
     end
+
     private :avatar_styles
   end
 
diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb
index f1b0883ee..4d96e990a 100644
--- a/app/models/concerns/account_header.rb
+++ b/app/models/concerns/account_header.rb
@@ -2,6 +2,7 @@
 
 module AccountHeader
   extend ActiveSupport::Concern
+
   IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
 
   class_methods do
@@ -10,6 +11,7 @@ module AccountHeader
       styles[:static] = { format: 'png' } if file.content_type == 'image/gif'
       styles
     end
+
     private :header_styles
   end
 
diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb
new file mode 100644
index 000000000..d51e6643e
--- /dev/null
+++ b/app/models/concerns/account_interactions.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+module AccountInteractions
+  extend ActiveSupport::Concern
+
+  class_methods do
+    def following_map(target_account_ids, account_id)
+      follow_mapping(Follow.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
+    end
+
+    def followed_by_map(target_account_ids, account_id)
+      follow_mapping(Follow.where(account_id: target_account_ids, target_account_id: account_id), :account_id)
+    end
+
+    def blocking_map(target_account_ids, account_id)
+      follow_mapping(Block.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
+    end
+
+    def muting_map(target_account_ids, account_id)
+      follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
+    end
+
+    def requested_map(target_account_ids, account_id)
+      follow_mapping(FollowRequest.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
+    end
+  end
+
+  included do
+    # Follow relations
+    has_many :follow_requests, dependent: :destroy
+
+    has_many :active_relationships,  class_name: 'Follow', foreign_key: 'account_id',        dependent: :destroy
+    has_many :passive_relationships, class_name: 'Follow', foreign_key: 'target_account_id', dependent: :destroy
+
+    has_many :following, -> { order('follows.id desc') }, through: :active_relationships,  source: :target_account
+    has_many :followers, -> { order('follows.id desc') }, through: :passive_relationships, source: :account
+
+    # Block relationships
+    has_many :block_relationships, class_name: 'Block', foreign_key: 'account_id', dependent: :destroy
+    has_many :blocking, -> { order('blocks.id desc') }, through: :block_relationships, source: :target_account
+    has_many :blocked_by_relationships, class_name: 'Block', foreign_key: :target_account_id, dependent: :destroy
+    has_many :blocked_by, -> { order('blocks.id desc') }, through: :blocked_by_relationships, source: :account
+
+    # Mute relationships
+    has_many :mute_relationships, class_name: 'Mute', foreign_key: 'account_id', dependent: :destroy
+    has_many :muting, -> { order('mutes.id desc') }, through: :mute_relationships, source: :target_account
+    has_many :conversation_mutes, dependent: :destroy
+    has_many :domain_blocks, class_name: 'AccountDomainBlock', dependent: :destroy
+
+    def follow!(other_account)
+      active_relationships.find_or_create_by!(target_account: other_account)
+    end
+
+    def block!(other_account)
+      block_relationships.find_or_create_by!(target_account: other_account)
+    end
+
+    def mute!(other_account)
+      mute_relationships.find_or_create_by!(target_account: other_account)
+    end
+
+    def mute_conversation!(conversation)
+      conversation_mutes.find_or_create_by!(conversation: conversation)
+    end
+
+    def block_domain!(other_domain)
+      domain_blocks.find_or_create_by!(domain: other_domain)
+    end
+
+    def unfollow!(other_account)
+      follow = active_relationships.find_by(target_account: other_account)
+      follow&.destroy
+    end
+
+    def unblock!(other_account)
+      block = block_relationships.find_by(target_account: other_account)
+      block&.destroy
+    end
+
+    def unmute!(other_account)
+      mute = mute_relationships.find_by(target_account: other_account)
+      mute&.destroy
+    end
+
+    def unmute_conversation!(conversation)
+      mute = conversation_mutes.find_by(conversation: conversation)
+      mute&.destroy!
+    end
+
+    def unblock_domain!(other_domain)
+      block = domain_blocks.find_by(domain: other_domain)
+      block&.destroy
+    end
+
+    def following?(other_account)
+      active_relationships.where(target_account: other_account).exists?
+    end
+
+    def blocking?(other_account)
+      block_relationships.where(target_account: other_account).exists?
+    end
+
+    def domain_blocking?(other_domain)
+      domain_blocks.where(domain: other_domain).exists?
+    end
+
+    def muting?(other_account)
+      mute_relationships.where(target_account: other_account).exists?
+    end
+
+    def muting_conversation?(conversation)
+      conversation_mutes.where(conversation: conversation).exists?
+    end
+
+    def requested?(other_account)
+      follow_requests.where(target_account: other_account).exists?
+    end
+
+    def favourited?(status)
+      status.proper.favourites.where(account: self).exists?
+    end
+
+    def reblogged?(status)
+      status.proper.reblogs.where(account: self).exists?
+    end
+  end
+end
diff --git a/app/models/status.rb b/app/models/status.rb
index fd1049116..760ecc928 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -67,7 +67,7 @@ class Status < ApplicationRecord
   scope :local_only, -> { left_outer_joins(:account).where(accounts: { domain: nil }) }
   scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: false }) }
   scope :including_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced: true }) }
-  scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
+  scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids, accounts: { domain: account.excluded_from_timeline_domains }) }
 
   cache_associated :account, :application, :media_attachments, :tags, :stream_entry, mentions: :account, reblog: [:account, :application, :stream_entry, :tags, :media_attachments, mentions: :account], thread: :account
 
@@ -284,7 +284,9 @@ class Status < ApplicationRecord
   end
 
   def find_statuses_from_tree_path(ids, account)
-    statuses = Status.where(id: ids).to_a
+    statuses = Status.where(id: ids).includes(:account).to_a
+
+    # FIXME: n+1 bonanza
     statuses.reject! { |status| filter_from_context?(status, account) }
 
     # Order ancestors/descendants by tree path
@@ -292,6 +294,11 @@ class Status < ApplicationRecord
   end
 
   def filter_from_context?(status, account)
-    account&.blocking?(status.account_id) || account&.muting?(status.account_id) || (status.account.silenced? && !account&.following?(status.account_id)) || !status.permitted?(account)
+    should_filter   = account&.blocking?(status.account_id)
+    should_filter ||= account&.domain_blocking?(status.account.domain)
+    should_filter ||= account&.muting?(status.account_id)
+    should_filter ||= (status.account.silenced? && !account&.following?(status.account_id))
+    should_filter ||= !status.permitted?(account)
+    should_filter
   end
 end
diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb
index 7b377f6a8..150ffe6b2 100644
--- a/app/services/notify_service.rb
+++ b/app/services/notify_service.rb
@@ -39,6 +39,7 @@ class NotifyService < BaseService
   def blocked?
     blocked   = @recipient.suspended?                                                                                              # Skip if the recipient account is suspended anyway
     blocked ||= @recipient.id == @notification.from_account.id                                                                     # Skip for interactions with self
+    blocked ||= @recipient.domain_blocking?(@notification.from_account.domain)                                                     # Skip for domain blocked accounts
     blocked ||= @recipient.blocking?(@notification.from_account)                                                                   # Skip for blocked accounts
     blocked ||= (@notification.from_account.silenced? && !@recipient.following?(@notification.from_account))                       # Hellban
     blocked ||= (@recipient.user.settings.interactions['must_be_follower']  && !@notification.from_account.following?(@recipient)) # Options
diff --git a/config/routes.rb b/config/routes.rb
index 9ff6a13bb..e3c6ce156 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -150,7 +150,8 @@ Rails.application.routes.draw do
       resources :favourites, only: [:index]
       resources :reports,    only: [:index, :create]
 
-      resource :instance, only: [:show]
+      resource :instance,      only: [:show]
+      resource :domain_blocks, only: [:show, :create, :destroy]
 
       resources :follow_requests, only: [:index] do
         member do
diff --git a/db/migrate/20170424003227_create_account_domain_blocks.rb b/db/migrate/20170424003227_create_account_domain_blocks.rb
new file mode 100644
index 000000000..b9448aca4
--- /dev/null
+++ b/db/migrate/20170424003227_create_account_domain_blocks.rb
@@ -0,0 +1,12 @@
+class CreateAccountDomainBlocks < ActiveRecord::Migration[5.0]
+  def change
+    create_table :account_domain_blocks do |t|
+      t.integer :account_id
+      t.string :domain
+
+      t.timestamps
+    end
+
+    add_index :account_domain_blocks, [:account_id, :domain], unique: true
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 00016e7c1..8246e665a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,11 +10,19 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20170516072309) do
+ActiveRecord::Schema.define(version: 20170517205741) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
 
+  create_table "account_domain_blocks", force: :cascade do |t|
+    t.integer  "account_id"
+    t.string   "domain"
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+    t.index ["account_id", "domain"], name: "index_account_domain_blocks_on_account_id_and_domain", unique: true, using: :btree
+  end
+
   create_table "accounts", force: :cascade do |t|
     t.string   "username",                default: "",    null: false
     t.string   "domain"
diff --git a/spec/controllers/api/v1/domain_blocks_controller_spec.rb b/spec/controllers/api/v1/domain_blocks_controller_spec.rb
new file mode 100644
index 000000000..c3331744d
--- /dev/null
+++ b/spec/controllers/api/v1/domain_blocks_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+RSpec.describe Api::V1::DomainBlocksController, type: :controller do
+  render_views
+
+  let(:user)  { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
+  let(:token) { double acceptable?: true, resource_owner_id: user.id }
+
+  before do
+    user.account.block_domain!('example.com')
+    allow(controller).to receive(:doorkeeper_token) { token }
+  end
+
+  describe 'GET #show' do
+    before do
+      get :show
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'returns blocked domains' do
+      expect(body_as_json.first).to eq 'example.com'
+    end
+  end
+
+  describe 'POST #create' do
+    before do
+      post :create, params: { domain: 'example.org' }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'creates a domain block' do
+      expect(user.account.domain_blocking?('example.org')).to be true
+    end
+  end
+
+  describe 'DELETE #destroy' do
+    before do
+      delete :destroy, params: { domain: 'example.com' }
+    end
+
+    it 'returns http success' do
+      expect(response).to have_http_status(:success)
+    end
+
+    it 'deletes a domain block' do
+      expect(user.account.domain_blocking?('example.com')).to be false
+    end
+  end
+end
diff --git a/spec/controllers/api/v1/media_controller_spec.rb b/spec/controllers/api/v1/media_controller_spec.rb
index c2d333282..b1d9798ea 100644
--- a/spec/controllers/api/v1/media_controller_spec.rb
+++ b/spec/controllers/api/v1/media_controller_spec.rb
@@ -55,7 +55,6 @@ RSpec.describe Api::V1::MediaController, type: :controller do
       end
     end
 
-
     context 'video/webm' do
       before do
         post :create, params: { file: fixture_file_upload('files/attachment.webm', 'video/webm') }
diff --git a/spec/fabricators/account_domain_block_fabricator.rb b/spec/fabricators/account_domain_block_fabricator.rb
new file mode 100644
index 000000000..fbbddadd5
--- /dev/null
+++ b/spec/fabricators/account_domain_block_fabricator.rb
@@ -0,0 +1,4 @@
+Fabricator(:account_domain_block) do
+  account_id 1
+  domain     "MyString"
+end
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index 16b1e7377..bf474c354 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe FeedManager do
 
   describe '#filter?' do
     let(:alice) { Fabricate(:account, username: 'alice') }
-    let(:bob)   { Fabricate(:account, username: 'bob') }
+    let(:bob)   { Fabricate(:account, username: 'bob', domain: 'example.com') }
     let(:jeff)  { Fabricate(:account, username: 'jeff') }
 
     context 'for home feed' do
@@ -93,6 +93,14 @@ RSpec.describe FeedManager do
         status = PostStatusService.new.call(alice, 'Hey @jeff')
         expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true
       end
+
+      it 'returns true for reblog of a personally blocked domain' do
+        alice.block_domain!('example.com')
+        alice.follow!(jeff)
+        status = Fabricate(:status, text: 'Hello world', account: bob)
+        reblog = Fabricate(:status, reblog: status, account: jeff)
+        expect(FeedManager.instance.filter?(:home, reblog, alice.id)).to be true
+      end
     end
 
     context 'for mentions feed' do
diff --git a/spec/models/account_domain_block_spec.rb b/spec/models/account_domain_block_spec.rb
new file mode 100644
index 000000000..bd64e10fb
--- /dev/null
+++ b/spec/models/account_domain_block_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe AccountDomainBlock, type: :model do
+
+end
diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb
index d4f85b725..97ed94149 100644
--- a/spec/models/status_spec.rb
+++ b/spec/models/status_spec.rb
@@ -180,8 +180,48 @@ RSpec.describe Status, type: :model do
   end
 
   describe '#ancestors' do
+    let!(:alice)  { Fabricate(:account, username: 'alice') }
+    let!(:bob)    { Fabricate(:account, username: 'bob', domain: 'example.com') }
+    let!(:jeff)   { Fabricate(:account, username: 'jeff') }
+    let!(:status) { Fabricate(:status, account: alice) }
+    let!(:reply1) { Fabricate(:status, thread: status, account: jeff) }
+    let!(:reply2) { Fabricate(:status, thread: reply1, account: bob) }
+    let!(:reply3) { Fabricate(:status, thread: reply2, account: alice) }
+    let!(:viewer) { Fabricate(:account, username: 'viewer') }
+
+    it 'returns conversation history' do
+      expect(reply3.ancestors).to include(status, reply1, reply2)
+    end
+
+    it 'does not return conversation history user is not allowed to see' do
+      reply1.update(visibility: :private)
+      status.update(visibility: :direct)
+
+      expect(reply3.ancestors(viewer)).to_not include(reply1, status)
+    end
+
+    it 'does not return conversation history from blocked users' do
+      viewer.block!(jeff)
+      expect(reply3.ancestors(viewer)).to_not include(reply1)
+    end
+
+    it 'does not return conversation history from muted users' do
+      viewer.mute!(jeff)
+      expect(reply3.ancestors(viewer)).to_not include(reply1)
+    end
+
+    it 'does not return conversation history from silenced and not followed users' do
+      jeff.update(silenced: true)
+      expect(reply3.ancestors(viewer)).to_not include(reply1)
+    end
+
+    it 'does not return conversation history from blocked domains' do
+      viewer.block_domain!('example.com')
+      expect(reply3.ancestors(viewer)).to_not include(reply2)
+    end
+
     it 'ignores deleted records' do
-      first_status = Fabricate(:status, account: bob)
+      first_status  = Fabricate(:status, account: bob)
       second_status = Fabricate(:status, thread: first_status, account: alice)
 
       # Create cache and delete cached record
@@ -192,8 +232,46 @@ RSpec.describe Status, type: :model do
     end
   end
 
-  describe '#filter_from_context?' do
-    pending
+  describe '#descendants' do
+    let!(:alice)  { Fabricate(:account, username: 'alice') }
+    let!(:bob)    { Fabricate(:account, username: 'bob', domain: 'example.com') }
+    let!(:jeff)   { Fabricate(:account, username: 'jeff') }
+    let!(:status) { Fabricate(:status, account: alice) }
+    let!(:reply1) { Fabricate(:status, thread: status, account: alice) }
+    let!(:reply2) { Fabricate(:status, thread: status, account: bob) }
+    let!(:reply3) { Fabricate(:status, thread: reply1, account: jeff) }
+    let!(:viewer) { Fabricate(:account, username: 'viewer') }
+
+    it 'returns replies' do
+      expect(status.descendants).to include(reply1, reply2, reply3)
+    end
+
+    it 'does not return replies user is not allowed to see' do
+      reply1.update(visibility: :private)
+      reply3.update(visibility: :direct)
+
+      expect(status.descendants(viewer)).to_not include(reply1, reply3)
+    end
+
+    it 'does not return replies from blocked users' do
+      viewer.block!(jeff)
+      expect(status.descendants(viewer)).to_not include(reply3)
+    end
+
+    it 'does not return replies from muted users' do
+      viewer.mute!(jeff)
+      expect(status.descendants(viewer)).to_not include(reply3)
+    end
+
+    it 'does not return replies from silenced and not followed users' do
+      jeff.update(silenced: true)
+      expect(status.descendants(viewer)).to_not include(reply3)
+    end
+
+    it 'does not return replies from blocked domains' do
+      viewer.block_domain!('example.com')
+      expect(status.descendants(viewer)).to_not include(reply2)
+    end
   end
 
   describe '.mutes_map' do
@@ -368,6 +446,15 @@ RSpec.describe Status, type: :model do
         expect(results).not_to include(muted_status)
       end
 
+      it 'excludes statuses from accounts from personally blocked domains' do
+        blocked = Fabricate(:account, domain: 'example.com')
+        @account.block_domain!(blocked.domain)
+        blocked_status = Fabricate(:status, account: blocked)
+
+        results = Status.as_public_timeline(@account)
+        expect(results).not_to include(blocked_status)
+      end
+
       context 'with language preferences' do
         it 'excludes statuses in languages not allowed by the account user' do
           user = Fabricate(:user, allowed_languages: [:en, :es])
diff --git a/spec/services/notify_service_spec.rb b/spec/services/notify_service_spec.rb
index 032c37a28..29bd741aa 100644
--- a/spec/services/notify_service_spec.rb
+++ b/spec/services/notify_service_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe NotifyService do
 
   let(:user) { Fabricate(:user) }
   let(:recipient) { user.account }
-  let(:sender) { Fabricate(:account) }
+  let(:sender) { Fabricate(:account, domain: 'example.com') }
   let(:activity) { Fabricate(:follow, account: sender, target_account: recipient) }
 
   it { is_expected.to change(Notification, :count).by(1) }
@@ -17,6 +17,11 @@ RSpec.describe NotifyService do
     is_expected.to_not change(Notification, :count)
   end
 
+  it 'does not notify when sender\'s domain is blocked' do
+    recipient.block_domain!(sender.domain)
+    is_expected.to_not change(Notification, :count)
+  end
+
   it 'does not notify when sender is silenced and not followed' do
     sender.update(silenced: true)
     is_expected.to_not change(Notification, :count)
diff --git a/streaming/index.js b/streaming/index.js
index 0fd545a41..7f30b4e88 100644
--- a/streaming/index.js
+++ b/streaming/index.js
@@ -229,20 +229,26 @@ if (cluster.isMaster) {
 
           const unpackedPayload  = JSON.parse(payload)
           const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)).concat(unpackedPayload.reblog ? [unpackedPayload.reblog.account.id] : [])
+          const accountDomain    = unpackedPayload.account.acct.split('@')[1]
 
-          client.query(`SELECT target_account_id FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT target_account_id FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds), (err, result) => {
-            done()
+          const queries = [
+            client.query(`SELECT 1 FROM blocks WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)}) UNION SELECT 1 FROM mutes WHERE account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 1)})`, [req.accountId].concat(targetAccountIds)),
+          ]
 
-            if (err) {
-              log.error(err)
-              return
-            }
+          if (accountDomain) {
+            queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain]))
+          }
 
-            if (result.rows.length > 0) {
+          Promise.all(queries).then(values => {
+            done()
+
+            if (values[0].rows.length > 0 || (values.length > 1 && values[1].rows.length > 0)) {
               return
             }
 
             transmit()
+          }).catch(err => {
+            log.error(err)
           })
         })
       } else {