255 lines
7.1 KiB
Ruby
255 lines
7.1 KiB
Ruby
# typed: true
|
|
|
|
require "open-uri"
|
|
require "tempfile"
|
|
require "base64"
|
|
|
|
class Domain::PostsController < DomainController
|
|
extend T::Sig
|
|
extend T::Helpers
|
|
|
|
skip_before_action :authenticate_user!,
|
|
only: %i[
|
|
show
|
|
index
|
|
user_favorite_posts
|
|
user_created_posts
|
|
visual_search
|
|
visual_results
|
|
]
|
|
before_action :set_post!, only: %i[show]
|
|
before_action :set_user!, only: %i[user_created_posts]
|
|
before_action :set_post_group!, only: %i[posts_in_group]
|
|
|
|
class PostsIndexViewConfig < T::ImmutableStruct
|
|
include T::Struct::ActsAsComparable
|
|
|
|
const :show_domain_filters, T::Boolean
|
|
const :show_creator_links, T::Boolean
|
|
const :index_type_header, String
|
|
end
|
|
|
|
sig { void }
|
|
def initialize
|
|
super
|
|
@posts_index_view_config =
|
|
PostsIndexViewConfig.new(
|
|
show_domain_filters: false,
|
|
show_creator_links: false,
|
|
index_type_header: "all_posts",
|
|
)
|
|
end
|
|
|
|
# GET /posts
|
|
sig(:final) { void }
|
|
def index
|
|
@posts_index_view_config =
|
|
PostsIndexViewConfig.new(
|
|
show_domain_filters: true,
|
|
show_creator_links: true,
|
|
index_type_header: "all_posts",
|
|
)
|
|
authorize Domain::Post
|
|
@posts = posts_relation(Domain::Post.all).without_count
|
|
active_sources = (params[:sources] || DomainSourceHelper.all_source_names)
|
|
unless DomainSourceHelper.has_all_sources?(active_sources)
|
|
postable_types =
|
|
DomainSourceHelper.source_names_to_class_names(active_sources)
|
|
@posts = @posts.where(type: postable_types) if postable_types.any?
|
|
end
|
|
end
|
|
|
|
# GET /posts/:id
|
|
sig(:final) { void }
|
|
def show
|
|
authorize @post
|
|
end
|
|
|
|
sig(:final) { void }
|
|
def user_created_posts
|
|
@posts_index_view_config =
|
|
PostsIndexViewConfig.new(
|
|
show_domain_filters: false,
|
|
show_creator_links: false,
|
|
index_type_header: "user_created",
|
|
)
|
|
|
|
@user = T.must(@user)
|
|
authorize @user
|
|
@posts = posts_relation(@user.posts)
|
|
authorize @posts
|
|
render :index
|
|
end
|
|
|
|
sig(:final) { void }
|
|
def posts_in_group
|
|
@posts_index_view_config =
|
|
PostsIndexViewConfig.new(
|
|
show_domain_filters: false,
|
|
show_creator_links: true,
|
|
index_type_header: "posts_in_group",
|
|
)
|
|
|
|
authorize @post_group
|
|
@posts = posts_relation(T.must(@post_group).posts)
|
|
render :index
|
|
end
|
|
|
|
# GET /posts/visual_search
|
|
sig(:final) { void }
|
|
def visual_search
|
|
authorize Domain::Post
|
|
end
|
|
|
|
sig { params(content_type: T.nilable(String)).returns(T::Boolean) }
|
|
def check_content_type!(content_type)
|
|
return false unless content_type
|
|
|
|
ret =
|
|
Domain::PostFile::Thumbnail::THUMBABLE_CONTENT_TYPES.any? do |type|
|
|
content_type.match?(type)
|
|
end
|
|
|
|
unless ret
|
|
flash.now[:error] = "The uploaded file is not a valid image format."
|
|
render :visual_search
|
|
end
|
|
|
|
ret
|
|
end
|
|
|
|
# POST /posts/visual_search
|
|
sig(:final) { void }
|
|
def visual_results
|
|
authorize Domain::Post
|
|
|
|
# Process the uploaded image or URL
|
|
file_result = process_image_input
|
|
return unless file_result
|
|
file_path, content_type = file_result
|
|
|
|
# Create thumbnail for the view if possible
|
|
tmp_dir = Dir.mktmpdir("visual-search")
|
|
thumbs_and_fingerprints =
|
|
helpers.generate_fingerprints(file_path, content_type, tmp_dir)
|
|
first_thumb_and_fingerprint = thumbs_and_fingerprints&.first
|
|
if thumbs_and_fingerprints.nil? || first_thumb_and_fingerprint.nil?
|
|
flash.now[:error] = "Error generating fingerprints"
|
|
render :visual_search
|
|
return
|
|
end
|
|
logger.info("generated #{thumbs_and_fingerprints.length} thumbs")
|
|
|
|
@uploaded_image_data_uri =
|
|
helpers.create_image_thumbnail_data_uri(
|
|
first_thumb_and_fingerprint.thumb_path,
|
|
"image/jpeg",
|
|
)
|
|
@uploaded_detail_hash_value = first_thumb_and_fingerprint.detail_fingerprint
|
|
before = Time.now
|
|
|
|
similar_fingerprints =
|
|
helpers.find_similar_fingerprints(
|
|
thumbs_and_fingerprints.map(&:to_fingerprint_and_detail),
|
|
).take(10)
|
|
|
|
@time_taken = Time.now - before
|
|
|
|
@matches = similar_fingerprints
|
|
@good_matches =
|
|
similar_fingerprints.select { |f| f.similarity_percentage >= 80 }
|
|
@bad_matches =
|
|
similar_fingerprints.select { |f| f.similarity_percentage < 80 }
|
|
|
|
@matches = @good_matches if @good_matches.any?
|
|
ensure
|
|
# Clean up any temporary files
|
|
FileUtils.rm_rf(tmp_dir) if tmp_dir
|
|
end
|
|
|
|
private
|
|
|
|
# Process the uploaded file or URL and return [image_path, content_type] or nil on failure
|
|
sig { returns(T.nilable([String, String])) }
|
|
def process_image_input
|
|
if params[:image_file].present?
|
|
process_uploaded_file
|
|
elsif params[:image_url].present?
|
|
process_image_url
|
|
else
|
|
flash.now[:error] = "Please upload an image or provide an image URL."
|
|
render :visual_search
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Process an uploaded file and return [image_path, content_type] or nil on failure
|
|
sig { returns(T.nilable([String, String])) }
|
|
def process_uploaded_file
|
|
image_file = params[:image_file]
|
|
content_type = T.must(image_file.content_type)
|
|
|
|
return nil unless check_content_type!(content_type)
|
|
|
|
image_path = T.must(image_file.tempfile.path)
|
|
[image_path, content_type]
|
|
end
|
|
|
|
# Process an image URL and return [image_path, content_type] or nil on failure
|
|
sig { returns(T.nilable([String, String])) }
|
|
def process_image_url
|
|
# Download the image to a temporary file
|
|
image_url = params[:image_url]
|
|
image_io = URI.open(image_url)
|
|
|
|
if image_io.nil?
|
|
flash.now[:error] = "The URL does not point to a valid image format."
|
|
render :visual_search
|
|
return nil
|
|
end
|
|
|
|
content_type = T.must(T.unsafe(image_io).content_type)
|
|
return nil unless check_content_type!(content_type)
|
|
|
|
# Save to temp file
|
|
extension = helpers.extension_for_content_type(content_type) || "jpg"
|
|
@temp_file = Tempfile.new(["image", ".#{extension}"])
|
|
@temp_file.binmode
|
|
image_data = image_io.read
|
|
@temp_file.write(image_data)
|
|
@temp_file.close
|
|
|
|
image_path = T.must(@temp_file.path)
|
|
[image_path, content_type]
|
|
rescue StandardError => e
|
|
Rails.logger.error("Error processing image URL: #{e.message}")
|
|
flash.now[:error] = "Error downloading search image"
|
|
render :visual_search
|
|
nil
|
|
end
|
|
|
|
sig { override.returns(DomainController::DomainParamConfig) }
|
|
def self.param_config
|
|
DomainController::DomainParamConfig.new(
|
|
post_id_param: :id,
|
|
user_id_param: :domain_user_id,
|
|
post_group_id_param: :domain_post_group_id,
|
|
)
|
|
end
|
|
|
|
sig(:final) do
|
|
params(
|
|
starting_relation: ActiveRecord::Relation,
|
|
skip_ordering: T::Boolean,
|
|
).returns(
|
|
T.all(ActiveRecord::Relation, Kaminari::ActiveRecordRelationMethods),
|
|
)
|
|
end
|
|
def posts_relation(starting_relation, skip_ordering: false)
|
|
relation = starting_relation
|
|
relation = T.unsafe(policy_scope(relation)).page(params[:page]).per(50)
|
|
relation = relation.order("posted_at DESC NULLS LAST") unless skip_ordering
|
|
relation
|
|
end
|
|
end
|