Files
redux-scraper/app/controllers/domain/posts_controller.rb
2025-08-13 08:20:32 +00:00

288 lines
8.0 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 = @user.faved_posts
@post_favs =
Domain::UserPostFav.where(user: @user, post: @posts).index_by(&:post_id)
# Apply pagination through posts_relation
@posts = posts_relation(@posts, skip_ordering: true)
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: 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
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)
before = Time.now
similar_fingerprints =
helpers.find_similar_fingerprints(
fingerprint_value: @uploaded_hash_value,
fingerprint_detail_value: @uploaded_detail_hash_value,
).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
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
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