Files
redux-scraper/app/controllers/domain/posts_controller.rb
2025-08-19 01:22:56 +00:00

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