Account domain blocks (#2381)

* Add <ostatus:conversation /> tag to Atom input/output

Only uses ref attribute (not href) because href would be
the alternate link that's always included also.

Creates new conversation for every non-reply status. Carries
over conversation for every reply. Keeps remote URIs verbatim,
generates local URIs on the fly like the rest of them.

* Conversation muting - prevents notifications that reference a conversation
(including replies, favourites, reblogs) from being created. API endpoints
/api/v1/statuses/:id/mute and /api/v1/statuses/:id/unmute

Currently no way to tell when a status/conversation is muted, so the web UI
only has a "disable notifications" button, doesn't work as a toggle

* Display "Dismiss notifications" on all statuses in notifications column, not just own

* Add "muted" as a boolean attribute on statuses JSON

For now always false on contained reblogs, since it's only relevant for
statuses returned from the notifications endpoint, which are not nested

Remove "Disable notifications" from detailed status view, since it's
only relevant in the notifications column

* Up max class length

* Remove pending test for conversation mute

* Add tests, clean up

* Rename to "mute conversation" and "unmute conversation"

* Raise validation error when trying to mute/unmute status without conversation

* Adding account domain blocks that filter notifications and public timelines

* Add tests for domain blocks in notifications, public timelines
Filter reblogs of blocked domains from home

* Add API for listing and creating account domain blocks

* API for creating/deleting domain blocks, tests for Status#ancestors
and Status#descendants, filter domain blocks from them

* Filter domains in streaming API

* Update account_domain_block_spec.rb
rebase/4.0.0rc2
Eugen Rochko 2017-05-19 01:14:30 +02:00 committed by GitHub
parent 8ec8410651
commit 620d0d8029
20 changed files with 420 additions and 124 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -151,6 +151,7 @@ Rails.application.routes.draw do
resources :reports, only: [:index, :create]
resource :instance, only: [:show]
resource :domain_blocks, only: [:show, :create, :destroy]
resources :follow_requests, only: [:index] do
member do

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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') }

View File

@ -0,0 +1,4 @@
Fabricator(:account_domain_block) do
account_id 1
domain "MyString"
end

View File

@ -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

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe AccountDomainBlock, type: :model do
end

View File

@ -180,6 +180,46 @@ 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)
second_status = Fabricate(:status, thread: first_status, account: alice)
@ -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])

View File

@ -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)

View File

@ -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 {