297 lines
8.4 KiB
Ruby
297 lines
8.4 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_favorite_posts 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_favorite_posts
|
|
@posts_index_view_config =
|
|
PostsIndexViewConfig.new(
|
|
show_domain_filters: false,
|
|
show_creator_links: true,
|
|
index_type_header: "user_favorites",
|
|
)
|
|
|
|
@user = T.must(@user)
|
|
authorize @user
|
|
@posts = posts_relation(@user.faved_posts)
|
|
authorize @posts
|
|
render :index
|
|
end
|
|
|
|
sig(:final) { void }
|
|
def user_created_posts
|
|
@posts_index_view_config =
|
|
PostsIndexViewConfig.new(
|
|
show_domain_filters: false,
|
|
show_creator_links: true,
|
|
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
|
|
image_result = process_image_input
|
|
return unless image_result
|
|
|
|
image_path, content_type = image_result
|
|
|
|
# Create thumbnail for the view if possible
|
|
@uploaded_image_data_uri = create_thumbnail(image_path, content_type)
|
|
@uploaded_hash_value = generate_fingerprint(image_path)
|
|
@uploaded_detail_hash_value = generate_detail_fingerprint(image_path)
|
|
@post_file_fingerprints =
|
|
find_similar_fingerprints(@uploaded_hash_value).to_a
|
|
@post_file_fingerprints.sort! do |a, b|
|
|
helpers.calculate_similarity_percentage(
|
|
b.fingerprint_detail_value,
|
|
@uploaded_detail_hash_value,
|
|
) <=>
|
|
helpers.calculate_similarity_percentage(
|
|
a.fingerprint_detail_value,
|
|
@uploaded_detail_hash_value,
|
|
)
|
|
end
|
|
@post_file_fingerprints = @post_file_fingerprints.take(10)
|
|
@posts = @post_file_fingerprints.map(&:post_file).compact.map(&:post)
|
|
ensure
|
|
# Clean up any temporary files
|
|
if @temp_file
|
|
@temp_file.unlink
|
|
@temp_file = nil
|
|
end
|
|
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
|
|
|
|
# Create a thumbnail from the image and return the data URI
|
|
sig do
|
|
params(image_path: String, content_type: String).returns(T.nilable(String))
|
|
end
|
|
def create_thumbnail(image_path, content_type)
|
|
helpers.create_image_thumbnail_data_uri(image_path, content_type)
|
|
end
|
|
|
|
# Generate a fingerprint from the image path
|
|
sig { params(image_path: String).returns(String) }
|
|
def generate_fingerprint(image_path)
|
|
# Use the new from_file_path method to create a fingerprint
|
|
Domain::PostFile::BitFingerprint.from_file_path(image_path)
|
|
end
|
|
|
|
# Generate a detail fingerprint from the image path
|
|
sig { params(image_path: String).returns(String) }
|
|
def generate_detail_fingerprint(image_path)
|
|
Domain::PostFile::BitFingerprint.detail_from_file_path(image_path)
|
|
end
|
|
|
|
# Find similar images based on the fingerprint
|
|
sig { params(fingerprint_value: String).returns(ActiveRecord::Relation) }
|
|
def find_similar_fingerprints(fingerprint_value)
|
|
# Use the model's similar_to_fingerprint method directly
|
|
|
|
subquery = <<~SQL
|
|
(
|
|
select distinct on (post_file_id) *, (fingerprint_value <~> '#{ActiveRecord::Base.connection.quote_string(fingerprint_value)}') as distance
|
|
from #{Domain::PostFile::BitFingerprint.table_name}
|
|
order by post_file_id, distance asc
|
|
) subquery
|
|
SQL
|
|
|
|
Domain::PostFile::BitFingerprint
|
|
.select("*")
|
|
.from(subquery)
|
|
.order("distance ASC")
|
|
.limit(32)
|
|
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).returns(
|
|
T.all(ActiveRecord::Relation, Kaminari::ActiveRecordRelationMethods),
|
|
)
|
|
end
|
|
def posts_relation(starting_relation)
|
|
relation = starting_relation
|
|
relation = T.unsafe(policy_scope(relation)).page(params[:page]).per(50)
|
|
relation = relation.order(relation.klass.post_order_attribute => :desc)
|
|
relation
|
|
end
|
|
end
|