summaryrefslogtreecommitdiff
path: root/app/controllers/search_controller.rb
blob: 3a54ac1eb8438fab5fb01daef763abde973949f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# frozen_string_literal: true

require 'tempfile'
require 'fancy_searchable/parsing/search_parsing_error'
require 'booru/image_processing'

class SearchController < ApplicationController
  include ImageUpload
  include SortParams

  before_action :setup_pagination_and_tags
  skip_authorization_check

  def index
    set_image_filter

    @search_query = params[:q]

    unless @search_query.present?
      @title = 'Search'
      respond_to do |format|
        format.html { render }
        format.json { head :ok }
      end
      return
    end

    # JSON API kludge
    @include_deleted = params.key?(:deleted) if params[:format] == 'json'

    @title = "Searching for #{@search_query}"

    if @include_deleted == :only
      @title += ' (Deleted/Merged Only)'
    elsif @include_deleted
      @title += ' (Including Deleted/Merged)'
    end

    begin
      loader = PostLoader.new(default_image_filter_options.merge(sorts: parse_sort))
      @search = loader.search(@search_query)
    rescue FancySearchable::Parsing::SearchParsingError => ex
      @search_failed = ex
      respond_to do |format|
        format.html { render }
        format.json { render json: {}, status: :bad_request }
      end
    else
      # Rely on ES to filter out deleted images.
      begin
        if params[:random_image] && (random_image_id = find_random_image_id)
          return respond_to do |format|
            format.html { redirect_to post_path(random_image_id, scope_key) }
            format.json { render json: { id: random_image_id } }
          end
        else
          @posts = @search.records(includes: [:tags, :user])
          @image_ids = @search.results.map { |h| h[:_id].to_i }
          # A small hack for detecting single tag searches and candidates for unspoilering.
          search_body = @search.search.definition[:body][:query][:bool][:filter]
          # If wildcards are present, detect the single tags (so they don't get spoilered)
          search_body += @search.search.definition[:body][:query][:bool][:must] if @search.search.definition[:body][:query][:bool][:must].is_a?(Array)
          setup_ignoredtaglist search_body
          @interactions = PostQuery.interactions(@image_ids.compact.uniq, current_user.id) if current_user
        end
      rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
        puts e
        @posts = nil
        @search_failed = "Processing error. Please report this query if this error persists."
      end

      respond_to do |format|
        format.html { render }
        format.json { render json: { search: @posts.map(&:as_json), total: @search.total_count, interactions: (@interactions || []) } }
      end
    end
  end

  def dump
    authorize! :manage, Post
    @search_query = params[:q]

    unless @search_query.present?
      return render plain: 'No query specified', status: :bad_request
    end

    set_image_filter

    begin
      loader = PostLoader.new(default_image_filter_options.merge(sorts: parse_sort, size: 65535))
      @search = loader.search(@search_query)
    rescue FancySearchable::Parsing::SearchParsingError => ex
      render plain: ex, status: :internal_server_error
    else
      @posts = @search.records(includes: [:tags, :user])
      send_data @posts.as_json.to_json, type: 'application/json', disposition: :attachment, filename: "booru-dump-#{Time.now.to_i}.json"
    end
  end

  def reverse
    @title = 'Reverse Search'

    if request.post? || params[:scraper_url].present?
      scraped = scraped_media

      file = if scraped && scraped[:url] && scraped[:type] == 'image'
               Tempfile.new.tap do |t|
                 t.binmode
                 t.write Booru::Scrapers.fetch_image(scraped[:url])
                 t.close
               end
             elsif params[:image].present?
               params[:image]
             else
               flash[:error] = 'We could not find an image at the URL you provided'
               redirect_to search_reverse_path && return
             end

      begin
        processor = Booru::ImageProcessing.processor_for_file(file.path)
        processor.generate_rasterized
        intensities = ImageIntensities.file(processor.rasterized, type: (processor.is_a?(Booru::ImageProcessing::JpegProcessor) ? :jpeg : :png)) # FIXME: Hack!
      rescue StandardError => e
        Rails.logger.error "SearchController#reverse, except: #{e}"
        flash[:error] = 'The file you uploaded could not be analyzed!'
        redirect_to search_reverse_path
        return
      end

      dist = params[:fuzziness].to_f rescue 0.25
      dist = dist.clamp(0.1, 5.0)

      @matches = PostQuery.duplicates(intensities, dist)

      respond_to do |format|
        format.html
        format.json { render json: { search: @matches.map(&:as_json), total: @matches.count } }
      end
    end
  end

  def syntax
    @title = 'Search syntax documentation'
    respond_to do |format|
      format.html { render }
      format.json { head :ok }
    end
  end

  private

  def find_random_image_id
    loader = PostLoader.new(default_image_filter_options.merge(sample: true))
    @search = loader.search(@search_query)
    @search.results.empty? ? false : @search.results[0].id
  end

  # TODO: Clean up this mess of rescues and bizarre logic
  def setup_ignoredtaglist(search_body)
    # Okay, so this is not an "ignored tag list".
    # This is a "list of tags that the user explicitly searched for" - it is used so that they are not spoilered in
    # search results, as well as for displaying the tag info bar at the top.
    @ignoredtaglist = []

    @ignoredtaglist = search_body.map { |t| t.dig(:term, :'namespaced_tags.name') }.compact rescue []
    @ignoredtaglist = search_body.map { |t| t.dig(:bool, :must) }.compact.flatten.map { |t| t.dig(:term, :'namespaced_tags.name') } rescue [] if @ignoredtaglist.empty?
    @ignoredtaglist = @ignoredtaglist.map { |t| t.is_a?(Hash) ? t[:value] : t }
    @ignoredtaglist = Tag.includes(:implied_tags, :aliased_tag).where(name: @ignoredtaglist).map { |t| t.aliased_tag || t }
  end
end