# 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::Boolean) } def pending_scan? scanned_at.nil? 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