Add new public status index (#26344)
Co-authored-by: Eugen Rochko <eugen@zeonfederated.com> Co-authored-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
		@@ -62,6 +62,6 @@ class AccountsIndex < Chewy::Index
 | 
			
		||||
    field(:last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at })
 | 
			
		||||
    field(:display_name, type: 'text', analyzer: 'verbatim') { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
 | 
			
		||||
    field(:username, type: 'text', analyzer: 'verbatim', value: ->(account) { [account.username, account.domain].compact.join('@') }) { field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'verbatim' }
 | 
			
		||||
    field(:text, type: 'text', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
 | 
			
		||||
    field(:text, type: 'text', analyzer: 'whitespace', value: ->(account) { account.searchable_text }) { field :stemmed, type: 'text', analyzer: 'natural' }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										50
									
								
								app/chewy/public_statuses_index.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								app/chewy/public_statuses_index.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class PublicStatusesIndex < Chewy::Index
 | 
			
		||||
  settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
 | 
			
		||||
    filter: {
 | 
			
		||||
      english_stop: {
 | 
			
		||||
        type: 'stop',
 | 
			
		||||
        stopwords: '_english_',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      english_stemmer: {
 | 
			
		||||
        type: 'stemmer',
 | 
			
		||||
        language: 'english',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      english_possessive_stemmer: {
 | 
			
		||||
        type: 'stemmer',
 | 
			
		||||
        language: 'possessive_english',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    analyzer: {
 | 
			
		||||
      content: {
 | 
			
		||||
        tokenizer: 'uax_url_email',
 | 
			
		||||
        filter: %w(
 | 
			
		||||
          english_possessive_stemmer
 | 
			
		||||
          lowercase
 | 
			
		||||
          asciifolding
 | 
			
		||||
          cjk_width
 | 
			
		||||
          english_stop
 | 
			
		||||
          english_stemmer
 | 
			
		||||
        ),
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  index_scope ::Status.unscoped
 | 
			
		||||
                      .kept
 | 
			
		||||
                      .indexable
 | 
			
		||||
                      .includes(:media_attachments, :preloadable_poll, :preview_cards)
 | 
			
		||||
 | 
			
		||||
  root date_detection: false do
 | 
			
		||||
    field(:id, type: 'keyword')
 | 
			
		||||
    field(:account_id, type: 'long')
 | 
			
		||||
    field(:text, type: 'text', analyzer: 'whitespace', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
 | 
			
		||||
    field(:language, type: 'keyword')
 | 
			
		||||
    field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
 | 
			
		||||
    field(:created_at, type: 'date')
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -1,23 +1,24 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class StatusesIndex < Chewy::Index
 | 
			
		||||
  include FormattingHelper
 | 
			
		||||
 | 
			
		||||
  settings index: index_preset(refresh_interval: '30s', number_of_shards: 5), analysis: {
 | 
			
		||||
    filter: {
 | 
			
		||||
      english_stop: {
 | 
			
		||||
        type: 'stop',
 | 
			
		||||
        stopwords: '_english_',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      english_stemmer: {
 | 
			
		||||
        type: 'stemmer',
 | 
			
		||||
        language: 'english',
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      english_possessive_stemmer: {
 | 
			
		||||
        type: 'stemmer',
 | 
			
		||||
        language: 'possessive_english',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    analyzer: {
 | 
			
		||||
      content: {
 | 
			
		||||
        tokenizer: 'uax_url_email',
 | 
			
		||||
@@ -35,7 +36,7 @@ class StatusesIndex < Chewy::Index
 | 
			
		||||
 | 
			
		||||
  # We do not use delete_if option here because it would call a method that we
 | 
			
		||||
  # expect to be called with crutches without crutches, causing n+1 queries
 | 
			
		||||
  index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll)
 | 
			
		||||
  index_scope ::Status.unscoped.kept.without_reblogs.includes(:media_attachments, :preloadable_poll, :preview_cards)
 | 
			
		||||
 | 
			
		||||
  crutch :mentions do |collection|
 | 
			
		||||
    data = ::Mention.where(status_id: collection.map(&:id)).where(account: Account.local, silent: false).pluck(:status_id, :account_id)
 | 
			
		||||
@@ -63,13 +64,12 @@ class StatusesIndex < Chewy::Index
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  root date_detection: false do
 | 
			
		||||
    field :id, type: 'long'
 | 
			
		||||
    field :account_id, type: 'long'
 | 
			
		||||
 | 
			
		||||
    field :text, type: 'text', value: ->(status) { status.searchable_text } do
 | 
			
		||||
      field :stemmed, type: 'text', analyzer: 'content'
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }
 | 
			
		||||
    field(:id, type: 'keyword')
 | 
			
		||||
    field(:account_id, type: 'long')
 | 
			
		||||
    field(:text, type: 'text', analyzer: 'whitespace', value: ->(status) { status.searchable_text }) { field(:stemmed, type: 'text', analyzer: 'content') }
 | 
			
		||||
    field(:searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) })
 | 
			
		||||
    field(:language, type: 'keyword')
 | 
			
		||||
    field(:properties, type: 'keyword', value: ->(status) { status.searchable_properties })
 | 
			
		||||
    field(:created_at, type: 'date')
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
 | 
			
		||||
      :bot,
 | 
			
		||||
      :discoverable,
 | 
			
		||||
      :hide_collections,
 | 
			
		||||
      :indexable,
 | 
			
		||||
      fields_attributes: [:name, :value]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def account_params
 | 
			
		||||
    params.require(:account).permit(:discoverable, :unlocked, :show_collections, settings: UserSettings.keys)
 | 
			
		||||
    params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def set_account
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								app/lib/importer/public_statuses_index_importer.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								app/lib/importer/public_statuses_index_importer.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class Importer::PublicStatusesIndexImporter < Importer::BaseImporter
 | 
			
		||||
  def import!
 | 
			
		||||
    indexable_statuses_scope.find_in_batches(batch_size: @batch_size) do |batch|
 | 
			
		||||
      in_work_unit(batch.map(&:status_id)) do |status_ids|
 | 
			
		||||
        bulk = ActiveRecord::Base.connection_pool.with_connection do
 | 
			
		||||
          Chewy::Index::Import::BulkBuilder.new(index, to_index: Status.includes(:media_attachments, :preloadable_poll).where(id: status_ids)).bulk_body
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        indexed = 0
 | 
			
		||||
        deleted = 0
 | 
			
		||||
 | 
			
		||||
        bulk.map! do |entry|
 | 
			
		||||
          if entry[:index]
 | 
			
		||||
            indexed += 1
 | 
			
		||||
          else
 | 
			
		||||
            deleted += 1
 | 
			
		||||
          end
 | 
			
		||||
          entry
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
        Chewy::Index::Import::BulkRequest.new(index).perform(bulk)
 | 
			
		||||
 | 
			
		||||
        [indexed, deleted]
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    wait!
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def index
 | 
			
		||||
    PublicStatusesIndex
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def indexable_statuses_scope
 | 
			
		||||
    Status.indexable.select('"statuses"."id", COALESCE("statuses"."reblog_of_id", "statuses"."id") AS status_id')
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -36,7 +36,7 @@ class SearchQueryTransformer < Parslet::Transform
 | 
			
		||||
    def clause_to_filter(clause)
 | 
			
		||||
      case clause
 | 
			
		||||
      when PrefixClause
 | 
			
		||||
        { term: { clause.filter => clause.term } }
 | 
			
		||||
        { clause.type => { clause.filter => clause.term } }
 | 
			
		||||
      else
 | 
			
		||||
        raise "Unexpected clause type: #{clause}"
 | 
			
		||||
      end
 | 
			
		||||
@@ -47,12 +47,10 @@ class SearchQueryTransformer < Parslet::Transform
 | 
			
		||||
    class << self
 | 
			
		||||
      def symbol(str)
 | 
			
		||||
        case str
 | 
			
		||||
        when '+'
 | 
			
		||||
        when '+', nil
 | 
			
		||||
          :must
 | 
			
		||||
        when '-'
 | 
			
		||||
          :must_not
 | 
			
		||||
        when nil
 | 
			
		||||
          :should
 | 
			
		||||
        else
 | 
			
		||||
          raise "Unknown operator: #{str}"
 | 
			
		||||
        end
 | 
			
		||||
@@ -81,23 +79,52 @@ class SearchQueryTransformer < Parslet::Transform
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  class PrefixClause
 | 
			
		||||
    attr_reader :filter, :operator, :term
 | 
			
		||||
    attr_reader :type, :filter, :operator, :term
 | 
			
		||||
 | 
			
		||||
    def initialize(prefix, term)
 | 
			
		||||
      @operator = :filter
 | 
			
		||||
 | 
			
		||||
      case prefix
 | 
			
		||||
      when 'has', 'is'
 | 
			
		||||
        @filter = :properties
 | 
			
		||||
        @type = :term
 | 
			
		||||
        @term = term
 | 
			
		||||
      when 'language'
 | 
			
		||||
        @filter = :language
 | 
			
		||||
        @type = :term
 | 
			
		||||
        @term = term
 | 
			
		||||
      when 'from'
 | 
			
		||||
        @filter = :account_id
 | 
			
		||||
 | 
			
		||||
        username, domain = term.gsub(/\A@/, '').split('@')
 | 
			
		||||
        domain           = nil if TagManager.instance.local_domain?(domain)
 | 
			
		||||
        account          = Account.find_remote!(username, domain)
 | 
			
		||||
 | 
			
		||||
        @term = account.id
 | 
			
		||||
        @type = :term
 | 
			
		||||
        @term = account_id_from_term(term)
 | 
			
		||||
      when 'before'
 | 
			
		||||
        @filter = :created_at
 | 
			
		||||
        @type = :range
 | 
			
		||||
        @term = { lt: term }
 | 
			
		||||
      when 'after'
 | 
			
		||||
        @filter = :created_at
 | 
			
		||||
        @type = :range
 | 
			
		||||
        @term = { gt: term }
 | 
			
		||||
      when 'during'
 | 
			
		||||
        @filter = :created_at
 | 
			
		||||
        @type = :range
 | 
			
		||||
        @term = { gte: term, lte: term }
 | 
			
		||||
      else
 | 
			
		||||
        raise Mastodon::SyntaxError
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    private
 | 
			
		||||
 | 
			
		||||
    def account_id_from_term(term)
 | 
			
		||||
      username, domain = term.gsub(/\A@/, '').split('@')
 | 
			
		||||
      domain = nil if TagManager.instance.local_domain?(domain)
 | 
			
		||||
      account = Account.find_remote(username, domain)
 | 
			
		||||
 | 
			
		||||
      # If the account is not found, we want to return empty results, so return
 | 
			
		||||
      # an ID that does not exist
 | 
			
		||||
      account&.id || -1
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  rule(clause: subtree(:clause)) do
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,10 @@ class Vacuum::StatusesVacuum
 | 
			
		||||
      statuses.direct_visibility
 | 
			
		||||
              .includes(mentions: :account)
 | 
			
		||||
              .find_each(&:unlink_from_conversations!)
 | 
			
		||||
      remove_from_search_index(statuses.ids) if Chewy.enabled?
 | 
			
		||||
      if Chewy.enabled?
 | 
			
		||||
        remove_from_index(statuses.ids, 'chewy:queue:StatusesIndex')
 | 
			
		||||
        remove_from_index(statuses.ids, 'chewy:queue:PublicStatusesIndex')
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      # Foreign keys take care of most associated records for us.
 | 
			
		||||
      # Media attachments will be orphaned.
 | 
			
		||||
@@ -38,7 +41,7 @@ class Vacuum::StatusesVacuum
 | 
			
		||||
    Mastodon::Snowflake.id_at(@retention_period.ago, with_random: false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def remove_from_search_index(status_ids)
 | 
			
		||||
    with_redis { |redis| redis.sadd('chewy:queue:StatusesIndex', status_ids) }
 | 
			
		||||
  def remove_from_index(status_ids, index)
 | 
			
		||||
    with_redis { |redis| redis.sadd(index, status_ids) }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -82,6 +82,7 @@ class Account < ApplicationRecord
 | 
			
		||||
  include DomainMaterializable
 | 
			
		||||
  include AccountMerging
 | 
			
		||||
  include AccountSearch
 | 
			
		||||
  include AccountStatusesSearch
 | 
			
		||||
 | 
			
		||||
  enum protocol: { ostatus: 0, activitypub: 1 }
 | 
			
		||||
  enum suspension_origin: { local: 0, remote: 1 }, _prefix: true
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								app/models/concerns/account_statuses_search.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								app/models/concerns/account_statuses_search.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module AccountStatusesSearch
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  included do
 | 
			
		||||
    after_update_commit :enqueue_update_public_statuses_index, if: :saved_change_to_indexable?
 | 
			
		||||
    after_destroy_commit :enqueue_remove_from_public_statuses_index, if: :indexable?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def enqueue_update_public_statuses_index
 | 
			
		||||
    if indexable?
 | 
			
		||||
      enqueue_add_to_public_statuses_index
 | 
			
		||||
    else
 | 
			
		||||
      enqueue_remove_from_public_statuses_index
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def enqueue_add_to_public_statuses_index
 | 
			
		||||
    return unless Chewy.enabled?
 | 
			
		||||
 | 
			
		||||
    AddToPublicStatusesIndexWorker.perform_async(id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def enqueue_remove_from_public_statuses_index
 | 
			
		||||
    return unless Chewy.enabled?
 | 
			
		||||
 | 
			
		||||
    RemoveFromPublicStatusesIndexWorker.perform_async(id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_to_public_statuses_index!
 | 
			
		||||
    return unless Chewy.enabled?
 | 
			
		||||
 | 
			
		||||
    statuses.indexable.find_in_batches do |batch|
 | 
			
		||||
      PublicStatusesIndex.import(query: batch)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def remove_from_public_statuses_index!
 | 
			
		||||
    return unless Chewy.enabled?
 | 
			
		||||
 | 
			
		||||
    PublicStatusesIndex.filter(term: { account_id: id }).delete_all
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										54
									
								
								app/models/concerns/status_search_concern.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								app/models/concerns/status_search_concern.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
module StatusSearchConcern
 | 
			
		||||
  extend ActiveSupport::Concern
 | 
			
		||||
 | 
			
		||||
  included do
 | 
			
		||||
    scope :indexable, -> { without_reblogs.where(visibility: :public).joins(:account).where(account: { indexable: true }) }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def searchable_by(preloaded = nil)
 | 
			
		||||
    ids = []
 | 
			
		||||
 | 
			
		||||
    ids << account_id if local?
 | 
			
		||||
 | 
			
		||||
    if preloaded.nil?
 | 
			
		||||
      ids += mentions.joins(:account).merge(Account.local).active.pluck(:account_id)
 | 
			
		||||
      ids += favourites.joins(:account).merge(Account.local).pluck(:account_id)
 | 
			
		||||
      ids += reblogs.joins(:account).merge(Account.local).pluck(:account_id)
 | 
			
		||||
      ids += bookmarks.joins(:account).merge(Account.local).pluck(:account_id)
 | 
			
		||||
      ids += poll.votes.joins(:account).merge(Account.local).pluck(:account_id) if poll.present?
 | 
			
		||||
    else
 | 
			
		||||
      ids += preloaded.mentions[id] || []
 | 
			
		||||
      ids += preloaded.favourites[id] || []
 | 
			
		||||
      ids += preloaded.reblogs[id] || []
 | 
			
		||||
      ids += preloaded.bookmarks[id] || []
 | 
			
		||||
      ids += preloaded.votes[id] || []
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    ids.uniq
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def searchable_text
 | 
			
		||||
    [
 | 
			
		||||
      spoiler_text,
 | 
			
		||||
      FormattingHelper.extract_status_plain_text(self),
 | 
			
		||||
      preloadable_poll&.options&.join("\n\n"),
 | 
			
		||||
      ordered_media_attachments.map(&:description).join("\n\n"),
 | 
			
		||||
    ].compact.join("\n\n")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def searchable_properties
 | 
			
		||||
    [].tap do |properties|
 | 
			
		||||
      properties << 'image' if ordered_media_attachments.any?(&:image?)
 | 
			
		||||
      properties << 'video' if ordered_media_attachments.any?(&:video?)
 | 
			
		||||
      properties << 'audio' if ordered_media_attachments.any?(&:audio?)
 | 
			
		||||
      properties << 'media' if with_media?
 | 
			
		||||
      properties << 'poll' if with_poll?
 | 
			
		||||
      properties << 'link' if with_preview_card?
 | 
			
		||||
      properties << 'embed' if preview_cards.any?(&:video?)
 | 
			
		||||
      properties << 'sensitive' if sensitive?
 | 
			
		||||
      properties << 'reply' if reply?
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -37,6 +37,7 @@ class Status < ApplicationRecord
 | 
			
		||||
  include StatusSnapshotConcern
 | 
			
		||||
  include RateLimitable
 | 
			
		||||
  include StatusSafeReblogInsert
 | 
			
		||||
  include StatusSearchConcern
 | 
			
		||||
 | 
			
		||||
  rate_limit by: :account, family: :statuses
 | 
			
		||||
 | 
			
		||||
@@ -47,6 +48,7 @@ class Status < ApplicationRecord
 | 
			
		||||
  attr_accessor :override_timestamps
 | 
			
		||||
 | 
			
		||||
  update_index('statuses', :proper)
 | 
			
		||||
  update_index('public_statuses', :proper)
 | 
			
		||||
 | 
			
		||||
  enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4 }, _suffix: :visibility
 | 
			
		||||
 | 
			
		||||
@@ -165,37 +167,6 @@ class Status < ApplicationRecord
 | 
			
		||||
    "v3:#{super}"
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def searchable_by(preloaded = nil)
 | 
			
		||||
    ids = []
 | 
			
		||||
 | 
			
		||||
    ids << account_id if local?
 | 
			
		||||
 | 
			
		||||
    if preloaded.nil?
 | 
			
		||||
      ids += mentions.joins(:account).merge(Account.local).active.pluck(:account_id)
 | 
			
		||||
      ids += favourites.joins(:account).merge(Account.local).pluck(:account_id)
 | 
			
		||||
      ids += reblogs.joins(:account).merge(Account.local).pluck(:account_id)
 | 
			
		||||
      ids += bookmarks.joins(:account).merge(Account.local).pluck(:account_id)
 | 
			
		||||
      ids += poll.votes.joins(:account).merge(Account.local).pluck(:account_id) if poll.present?
 | 
			
		||||
    else
 | 
			
		||||
      ids += preloaded.mentions[id] || []
 | 
			
		||||
      ids += preloaded.favourites[id] || []
 | 
			
		||||
      ids += preloaded.reblogs[id] || []
 | 
			
		||||
      ids += preloaded.bookmarks[id] || []
 | 
			
		||||
      ids += preloaded.votes[id] || []
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    ids.uniq
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def searchable_text
 | 
			
		||||
    [
 | 
			
		||||
      spoiler_text,
 | 
			
		||||
      FormattingHelper.extract_status_plain_text(self),
 | 
			
		||||
      preloadable_poll ? preloadable_poll.options.join("\n\n") : nil,
 | 
			
		||||
      ordered_media_attachments.map(&:description).join("\n\n"),
 | 
			
		||||
    ].compact.join("\n\n")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def to_log_human_identifier
 | 
			
		||||
    account.acct
 | 
			
		||||
  end
 | 
			
		||||
@@ -270,6 +241,10 @@ class Status < ApplicationRecord
 | 
			
		||||
    preview_cards.any?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def with_poll?
 | 
			
		||||
    preloadable_poll.present?
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def non_sensitive_with_media?
 | 
			
		||||
    !sensitive? && with_media?
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -8,13 +8,13 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
 | 
			
		||||
 | 
			
		||||
  context_extensions :manually_approves_followers, :featured, :also_known_as,
 | 
			
		||||
                     :moved_to, :property_value, :discoverable, :olm, :suspended,
 | 
			
		||||
                     :memorial
 | 
			
		||||
                     :memorial, :indexable
 | 
			
		||||
 | 
			
		||||
  attributes :id, :type, :following, :followers,
 | 
			
		||||
             :inbox, :outbox, :featured, :featured_tags,
 | 
			
		||||
             :preferred_username, :name, :summary,
 | 
			
		||||
             :url, :manually_approves_followers,
 | 
			
		||||
             :discoverable, :published, :memorial
 | 
			
		||||
             :discoverable, :indexable, :published, :memorial
 | 
			
		||||
 | 
			
		||||
  has_one :public_key, serializer: ActivityPub::PublicKeySerializer
 | 
			
		||||
 | 
			
		||||
@@ -99,6 +99,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
 | 
			
		||||
    object.suspended? ? false : (object.discoverable || false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def indexable
 | 
			
		||||
    object.suspended? ? false : (object.indexable || false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def name
 | 
			
		||||
    object.suspended? ? object.username : (object.display_name.presence || object.username)
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,10 @@ class BatchedRemoveStatusService < BaseService
 | 
			
		||||
 | 
			
		||||
    # Since we skipped all callbacks, we also need to manually
 | 
			
		||||
    # deindex the statuses
 | 
			
		||||
    Chewy.strategy.current.update(StatusesIndex, statuses_and_reblogs) if Chewy.enabled?
 | 
			
		||||
    if Chewy.enabled?
 | 
			
		||||
      Chewy.strategy.current.update(StatusesIndex, statuses_and_reblogs)
 | 
			
		||||
      Chewy.strategy.current.update(PublicStatusesIndex, statuses_and_reblogs)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    return if options[:skip_side_effects]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,25 +39,15 @@ class SearchService < BaseService
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def perform_statuses_search!
 | 
			
		||||
    definition = parsed_query.apply(StatusesIndex.filter(term: { searchable_by: @account.id }))
 | 
			
		||||
 | 
			
		||||
    definition = definition.filter(term: { account_id: @options[:account_id] }) if @options[:account_id].present?
 | 
			
		||||
 | 
			
		||||
    if @options[:min_id].present? || @options[:max_id].present?
 | 
			
		||||
      range      = {}
 | 
			
		||||
      range[:gt] = @options[:min_id].to_i if @options[:min_id].present?
 | 
			
		||||
      range[:lt] = @options[:max_id].to_i if @options[:max_id].present?
 | 
			
		||||
      definition = definition.filter(range: { id: range })
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    results             = definition.limit(@limit).offset(@offset).objects.compact
 | 
			
		||||
    account_ids         = results.map(&:account_id)
 | 
			
		||||
    account_domains     = results.map(&:account_domain)
 | 
			
		||||
    preloaded_relations = @account.relations_map(account_ids, account_domains)
 | 
			
		||||
 | 
			
		||||
    results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? }
 | 
			
		||||
  rescue Faraday::ConnectionFailed, Parslet::ParseFailed
 | 
			
		||||
    []
 | 
			
		||||
    StatusesSearchService.new.call(
 | 
			
		||||
      @query,
 | 
			
		||||
      @account,
 | 
			
		||||
      limit: @limit,
 | 
			
		||||
      offset: @offset,
 | 
			
		||||
      account_id: @options[:account_id],
 | 
			
		||||
      min_id: @options[:min_id],
 | 
			
		||||
      max_id: @options[:max_id]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def perform_hashtags_search!
 | 
			
		||||
@@ -114,8 +104,4 @@ class SearchService < BaseService
 | 
			
		||||
  def statuses_search?
 | 
			
		||||
    @options[:type].blank? || @options[:type] == 'statuses'
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def parsed_query
 | 
			
		||||
    SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								app/services/statuses_search_service.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								app/services/statuses_search_service.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class StatusesSearchService < BaseService
 | 
			
		||||
  def call(query, account = nil, options = {})
 | 
			
		||||
    @query   = query&.strip
 | 
			
		||||
    @account = account
 | 
			
		||||
    @options = options
 | 
			
		||||
    @limit   = options[:limit].to_i
 | 
			
		||||
    @offset  = options[:offset].to_i
 | 
			
		||||
 | 
			
		||||
    status_search_results
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  private
 | 
			
		||||
 | 
			
		||||
  def status_search_results
 | 
			
		||||
    definition = parsed_query.apply(
 | 
			
		||||
      StatusesIndex.filter(
 | 
			
		||||
        bool: {
 | 
			
		||||
          should: [
 | 
			
		||||
            publicly_searchable,
 | 
			
		||||
            non_publicly_searchable,
 | 
			
		||||
          ],
 | 
			
		||||
 | 
			
		||||
          minimum_should_match: 1,
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # This is the best way to submit identical queries to multi-indexes though chewy
 | 
			
		||||
    definition.instance_variable_get(:@parameters)[:indices].value[:indices] << PublicStatusesIndex
 | 
			
		||||
 | 
			
		||||
    results             = definition.collapse(field: :id).order(_id: { order: :desc }).limit(@limit).offset(@offset).objects.compact
 | 
			
		||||
    account_ids         = results.map(&:account_id)
 | 
			
		||||
    account_domains     = results.map(&:account_domain)
 | 
			
		||||
    preloaded_relations = @account.relations_map(account_ids, account_domains)
 | 
			
		||||
 | 
			
		||||
    results.reject { |status| StatusFilter.new(status, @account, preloaded_relations).filtered? }
 | 
			
		||||
  rescue Faraday::ConnectionFailed, Parslet::ParseFailed
 | 
			
		||||
    []
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def publicly_searchable
 | 
			
		||||
    {
 | 
			
		||||
      bool: {
 | 
			
		||||
        must_not: {
 | 
			
		||||
          exists: {
 | 
			
		||||
            field: 'searchable_by',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def non_publicly_searchable
 | 
			
		||||
    {
 | 
			
		||||
      bool: {
 | 
			
		||||
        must: [
 | 
			
		||||
          {
 | 
			
		||||
            exists: {
 | 
			
		||||
              field: 'searchable_by',
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            term: { searchable_by: @account.id },
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def parsed_query
 | 
			
		||||
    SearchQueryTransformer.new.apply(SearchQueryParser.new.parse(@query))
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -24,6 +24,9 @@
 | 
			
		||||
 | 
			
		||||
  %p.lead= t('privacy.search_hint_html')
 | 
			
		||||
 | 
			
		||||
  .fields-group
 | 
			
		||||
    = f.input :indexable, as: :boolean, wrapper: :with_label
 | 
			
		||||
 | 
			
		||||
  = f.simple_fields_for :settings, current_user.settings do |ff|
 | 
			
		||||
    .fields-group
 | 
			
		||||
      = ff.input :indexable, wrapper: :with_label
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								app/workers/add_to_public_statuses_index_worker.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/workers/add_to_public_statuses_index_worker.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class AddToPublicStatusesIndexWorker
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  def perform(account_id)
 | 
			
		||||
    account = Account.find(account_id)
 | 
			
		||||
 | 
			
		||||
    return unless account.indexable?
 | 
			
		||||
 | 
			
		||||
    account.add_to_public_statuses_index!
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										15
									
								
								app/workers/remove_from_public_statuses_index_worker.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/workers/remove_from_public_statuses_index_worker.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
class RemoveFromPublicStatusesIndexWorker
 | 
			
		||||
  include Sidekiq::Worker
 | 
			
		||||
 | 
			
		||||
  def perform(account_id)
 | 
			
		||||
    account = Account.find(account_id)
 | 
			
		||||
 | 
			
		||||
    return if account.indexable?
 | 
			
		||||
 | 
			
		||||
    account.remove_from_public_statuses_index!
 | 
			
		||||
  rescue ActiveRecord::RecordNotFound
 | 
			
		||||
    true
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -23,6 +23,6 @@ class Scheduler::IndexingScheduler
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def indexes
 | 
			
		||||
    [AccountsIndex, TagsIndex, StatusesIndex]
 | 
			
		||||
    [AccountsIndex, TagsIndex, PublicStatusesIndex, StatusesIndex]
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ en:
 | 
			
		||||
        discoverable: Your public posts and profile may be featured or recommended in various areas of Mastodon and your profile may be suggested to other users.
 | 
			
		||||
        display_name: Your full name or your fun name.
 | 
			
		||||
        fields: Your homepage, pronouns, age, anything you want.
 | 
			
		||||
        indexable: Your public posts may appear in search results on Mastodon. People who have interacted with your posts may be able to search them regardless.
 | 
			
		||||
        note: 'You can @mention other people or #hashtags.'
 | 
			
		||||
        show_collections: People will be able to browse through your follows and followers. People that you follow will see that you follow them regardless.
 | 
			
		||||
        unlocked: People will be able to follow you without requesting approval. Uncheck if you want to review follow requests and chose whether to accept or reject new followers.
 | 
			
		||||
@@ -143,6 +144,7 @@ en:
 | 
			
		||||
        fields:
 | 
			
		||||
          name: Label
 | 
			
		||||
          value: Content
 | 
			
		||||
        indexable: Include public posts in search results
 | 
			
		||||
        show_collections: Show follows and followers on profile
 | 
			
		||||
        unlocked: Automatically accept new followers
 | 
			
		||||
      account_alias:
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ module Mastodon::CLI
 | 
			
		||||
      InstancesIndex,
 | 
			
		||||
      AccountsIndex,
 | 
			
		||||
      TagsIndex,
 | 
			
		||||
      PublicStatusesIndex,
 | 
			
		||||
      StatusesIndex,
 | 
			
		||||
    ].freeze
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										31
									
								
								spec/chewy/public_statuses_index_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								spec/chewy/public_statuses_index_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe PublicStatusesIndex do
 | 
			
		||||
  describe 'Searching the index' do
 | 
			
		||||
    before do
 | 
			
		||||
      mock_elasticsearch_response(described_class, raw_response)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'returns results from a query' do
 | 
			
		||||
      results = described_class.query(match: { name: 'status' })
 | 
			
		||||
 | 
			
		||||
      expect(results).to eq []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def raw_response
 | 
			
		||||
    {
 | 
			
		||||
      took: 3,
 | 
			
		||||
      hits: {
 | 
			
		||||
        hits: [
 | 
			
		||||
          {
 | 
			
		||||
            _id: '0',
 | 
			
		||||
            _score: 1.6375021,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										16
									
								
								spec/lib/importer/public_statuses_index_importer_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								spec/lib/importer/public_statuses_index_importer_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe Importer::PublicStatusesIndexImporter do
 | 
			
		||||
  describe 'import!' do
 | 
			
		||||
    let(:pool) { Concurrent::FixedThreadPool.new(5) }
 | 
			
		||||
    let(:importer) { described_class.new(batch_size: 123, executor: pool) }
 | 
			
		||||
 | 
			
		||||
    before { Fabricate(:status, account: Fabricate(:account, indexable: true)) }
 | 
			
		||||
 | 
			
		||||
    it 'indexes relevant statuses' do
 | 
			
		||||
      expect { importer.import! }.to update_index(PublicStatusesIndex)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -9,8 +9,8 @@ describe SearchQueryTransformer do
 | 
			
		||||
    it 'sets attributes' do
 | 
			
		||||
      transformer = described_class.new.apply(parser)
 | 
			
		||||
 | 
			
		||||
      expect(transformer.should_clauses.first).to be_a(SearchQueryTransformer::TermClause)
 | 
			
		||||
      expect(transformer.must_clauses.first).to be_nil
 | 
			
		||||
      expect(transformer.should_clauses.first).to be_nil
 | 
			
		||||
      expect(transformer.must_clauses.first).to be_a(SearchQueryTransformer::TermClause)
 | 
			
		||||
      expect(transformer.must_not_clauses.first).to be_nil
 | 
			
		||||
      expect(transformer.filter_clauses.first).to be_nil
 | 
			
		||||
    end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										66
									
								
								spec/models/concerns/account_statuses_search_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								spec/models/concerns/account_statuses_search_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe AccountStatusesSearch do
 | 
			
		||||
  let(:account) { Fabricate(:account, indexable: indexable) }
 | 
			
		||||
 | 
			
		||||
  before do
 | 
			
		||||
    allow(Chewy).to receive(:enabled?).and_return(true)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#enqueue_update_public_statuses_index' do
 | 
			
		||||
    before do
 | 
			
		||||
      allow(account).to receive(:enqueue_add_to_public_statuses_index)
 | 
			
		||||
      allow(account).to receive(:enqueue_remove_from_public_statuses_index)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when account is indexable' do
 | 
			
		||||
      let(:indexable) { true }
 | 
			
		||||
 | 
			
		||||
      it 'enqueues add_to_public_statuses_index and not to remove_from_public_statuses_index' do
 | 
			
		||||
        account.enqueue_update_public_statuses_index
 | 
			
		||||
        expect(account).to have_received(:enqueue_add_to_public_statuses_index)
 | 
			
		||||
        expect(account).to_not have_received(:enqueue_remove_from_public_statuses_index)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when account is not indexable' do
 | 
			
		||||
      let(:indexable) { false }
 | 
			
		||||
 | 
			
		||||
      it 'enqueues remove_from_public_statuses_index and not to add_to_public_statuses_index' do
 | 
			
		||||
        account.enqueue_update_public_statuses_index
 | 
			
		||||
        expect(account).to have_received(:enqueue_remove_from_public_statuses_index)
 | 
			
		||||
        expect(account).to_not have_received(:enqueue_add_to_public_statuses_index)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#enqueue_add_to_public_statuses_index' do
 | 
			
		||||
    let(:indexable) { true }
 | 
			
		||||
    let(:worker) { AddToPublicStatusesIndexWorker }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      allow(worker).to receive(:perform_async)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'enqueues AddToPublicStatusesIndexWorker' do
 | 
			
		||||
      account.enqueue_add_to_public_statuses_index
 | 
			
		||||
      expect(worker).to have_received(:perform_async).with(account.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe '#enqueue_remove_from_public_statuses_index' do
 | 
			
		||||
    let(:indexable) { false }
 | 
			
		||||
    let(:worker) { RemoveFromPublicStatusesIndexWorker }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      allow(worker).to receive(:perform_async)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    it 'enqueues RemoveFromPublicStatusesIndexWorker' do
 | 
			
		||||
      account.enqueue_remove_from_public_statuses_index
 | 
			
		||||
      expect(worker).to have_received(:perform_async).with(account.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										42
									
								
								spec/workers/add_to_public_statuses_index_worker_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								spec/workers/add_to_public_statuses_index_worker_spec.rb
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe AddToPublicStatusesIndexWorker do
 | 
			
		||||
  describe '#perform' do
 | 
			
		||||
    let(:account) { Fabricate(:account, indexable: indexable) }
 | 
			
		||||
    let(:account_id) { account.id }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      allow(Account).to receive(:find).with(account_id).and_return(account) unless account.nil?
 | 
			
		||||
      allow(account).to receive(:add_to_public_statuses_index!) unless account.nil?
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when account is indexable' do
 | 
			
		||||
      let(:indexable) { true }
 | 
			
		||||
 | 
			
		||||
      it 'adds the account to the public statuses index' do
 | 
			
		||||
        subject.perform(account_id)
 | 
			
		||||
        expect(account).to have_received(:add_to_public_statuses_index!)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when account is not indexable' do
 | 
			
		||||
      let(:indexable) { false }
 | 
			
		||||
 | 
			
		||||
      it 'does not add the account to public statuses index' do
 | 
			
		||||
        subject.perform(account_id)
 | 
			
		||||
        expect(account).to_not have_received(:add_to_public_statuses_index!)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when account does not exist' do
 | 
			
		||||
      let(:account) { nil }
 | 
			
		||||
      let(:account_id) { 999 }
 | 
			
		||||
 | 
			
		||||
      it 'does not raise an error' do
 | 
			
		||||
        expect { subject.perform(account_id) }.to_not raise_error
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -0,0 +1,42 @@
 | 
			
		||||
# frozen_string_literal: true
 | 
			
		||||
 | 
			
		||||
require 'rails_helper'
 | 
			
		||||
 | 
			
		||||
describe RemoveFromPublicStatusesIndexWorker do
 | 
			
		||||
  describe '#perform' do
 | 
			
		||||
    let(:account) { Fabricate(:account, indexable: indexable) }
 | 
			
		||||
    let(:account_id) { account.id }
 | 
			
		||||
 | 
			
		||||
    before do
 | 
			
		||||
      allow(Account).to receive(:find).with(account_id).and_return(account) unless account.nil?
 | 
			
		||||
      allow(account).to receive(:remove_from_public_statuses_index!) unless account.nil?
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when account is not indexable' do
 | 
			
		||||
      let(:indexable) { false }
 | 
			
		||||
 | 
			
		||||
      it 'removes the account from public statuses index' do
 | 
			
		||||
        subject.perform(account_id)
 | 
			
		||||
        expect(account).to have_received(:remove_from_public_statuses_index!)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when account is indexable' do
 | 
			
		||||
      let(:indexable) { true }
 | 
			
		||||
 | 
			
		||||
      it 'does not remove the account from public statuses index' do
 | 
			
		||||
        subject.perform(account_id)
 | 
			
		||||
        expect(account).to_not have_received(:remove_from_public_statuses_index!)
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    context 'when account does not exist' do
 | 
			
		||||
      let(:account) { nil }
 | 
			
		||||
      let(:account_id) { 999 }
 | 
			
		||||
 | 
			
		||||
      it 'does not raise an error' do
 | 
			
		||||
        expect { subject.perform(account_id) }.to_not raise_error
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
		Reference in New Issue
	
	Block a user