about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-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
9 files changed, 215 insertions, 109 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