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 {