Files
redux-scraper/app/models/domain/post/fa_post.rb
2025-07-25 00:41:28 +00:00

226 lines
6.0 KiB
Ruby

# typed: strict
class Domain::Post::FaPost < Domain::Post
include AttrJsonRecordAliases
attr_json :title, :string
attr_json :state, :string
attr_json :fa_id, :integer
attr_json :category, :string
attr_json :theme, :string
attr_json :species, :string
attr_json :gender, :string
attr_json :description, :string
attr_json :keywords, :string, array: true, default: []
attr_json :num_favorites, :integer
attr_json :num_comments, :integer
attr_json :num_views, :integer
attr_json :scanned_at, ActiveModelUtcTimeValue.new
attr_json :scan_file_error, :string
attr_json :last_user_page_id, :integer
attr_json :first_browse_page_id, :integer
attr_json :first_gallery_page_id, :integer
attr_json :first_seen_entry_id, :integer
attr_json :fuzzysearch_checked_at, ActiveModelUtcTimeValue.new
attr_json :fuzzysearch_json, ActiveModel::Type::Value.new
attr_json :fuzzysearch_entry_id, :integer
# TODO - convert `file` to Domain::PostFile::FaPostFile and
# move this to Domain::PostFile::FaPostFile
attr_json :tried_from_fur_archiver, :boolean, default: false
attr_json :tried_from_tor, :boolean, default: false
belongs_to :last_user_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_browse_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_gallery_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_seen_entry, class_name: "::HttpLogEntry", optional: true
belongs_to :fuzzysearch_entry, class_name: "::HttpLogEntry", optional: true
has_single_file!
has_single_creator! Domain::User::FaUser
has_faving_users! Domain::User::FaUser
after_initialize { self.state ||= "ok" }
enum :state,
{
ok: "ok",
removed: "removed",
scan_error: "scan_error",
file_error: "file_error",
},
prefix: "state"
validates :state, presence: true
validates :fa_id, presence: true
sig { override.returns([String, Symbol]) }
def self.param_prefix_and_attribute
["fa", :fa_id]
end
sig { override.returns(String) }
def self.view_prefix
"fa"
end
sig { override.returns(Symbol) }
def self.post_order_attribute
:fa_id
end
sig { override.returns(Domain::DomainType) }
def self.domain_type
Domain::DomainType::Fa
end
sig { override.returns(T.nilable(String)) }
def title
super
end
sig { override.returns(T.nilable(Domain::User)) }
def primary_creator_for_view
self.creator
end
sig { override.returns(T.nilable(T.any(String, Integer))) }
def domain_id_for_view
self.fa_id
end
sig { override.returns(T.nilable(Addressable::URI)) }
def external_url_for_view
if self.fa_id.present?
Addressable::URI.parse("https://www.furaffinity.net/view/#{self.fa_id}")
end
end
sig { override.returns(T.nilable(Domain::PostFile)) }
def primary_file_for_view
self.file
end
sig { override.returns(T.nilable(String)) }
def description_html_for_view
description
end
sig { override.returns(String) }
def description_html_base_domain
"furaffinity.net"
end
sig { override.returns(T.nilable(HttpLogEntry)) }
def guess_last_submission_log_entry
@guessed_last_submission_log_entry ||=
T.let(
[
"https://www.furaffinity.net/view/#{self.fa_id}",
"https://www.furaffinity.net/view/#{self.fa_id}/",
].lazy
.map { |uri_str| HttpLogEntry.find_by_uri_host_path(uri_str) }
.find(&:present?),
T.nilable(HttpLogEntry),
)
end
sig { override.returns(T.nilable(String)) }
def description_for_view
self.description
end
sig { override.returns(T.nilable(Integer)) }
def num_favorites_for_view
num_favorites
end
sig { override.returns(T.nilable(HttpLogEntry)) }
def scanned_post_log_entry_for_view
self.last_submission_log_entry || self.guess_last_submission_log_entry
end
sig { returns(T.nilable(String)) }
def status_for_view
case self.state
when "ok"
"OK"
when "removed"
"Removed"
when "scan_error"
"Scan error"
when "file_error"
"File error"
end
end
sig do
params(
submission: Domain::Fa::Parser::ListedSubmissionParserHelper,
first_seen_log_entry: T.nilable(HttpLogEntry),
).returns(Domain::Post::FaPost)
end
def self.find_or_initialize_by_submission_parser(
submission,
first_seen_log_entry: nil
)
creator =
Domain::User::FaUser.find_or_create_by!(
{ url_name: submission.artist_url_name },
) { |user| user.name = submission.artist }
post =
Domain::Post::FaPost.find_or_initialize_by(fa_id: submission.id) do |post|
post.first_seen_entry = first_seen_log_entry
end
post.creator ||= creator
post.title ||= submission.title
post
end
sig { override.returns(T.nilable(ActiveSupport::TimeWithZone)) }
def posted_at
pa = super
return pa unless pa.nil?
for file_url_str in files.map(&:url_str).compact
parsed = FaUriHelper.parse_fa_media_url(file_url_str)
return parsed.original_file_posted_at.in_time_zone("UTC") if parsed
end
log_entry = last_submission_log_entry || guess_last_submission_log_entry
if log_entry&.response_bytes
parser =
Domain::Fa::Parser::Page.from_log_entry(
log_entry,
require_logged_in: false,
)
if parser.probably_submission?
parser.submission.posted_date&.in_time_zone("UTC")
end
end
end
sig { override.returns(T.nilable(T::Array[TagForView])) }
def tags_for_view
keywords.map { |value| TagForView.new(category: :general, value:) }
end
sig { returns(T.nilable(Domain::PostFile)) }
def fur_archiver_post_file
files.to_a.find do |file|
uri = Addressable::URI.parse(file.url_str)
uri.host == "furarchiver.net"
end
end
sig { returns(T.nilable(Domain::PostFile)) }
def tor_post_file
files.to_a.find do |file|
uri = Addressable::URI.parse(file.url_str)
uri.host ==
"g6jy5jkx466lrqojcngbnksugrcfxsl562bzuikrka5rv7srgguqbjid.onion"
end
end
end