more typing

This commit is contained in:
Dylan Knutson
2025-02-15 06:17:25 +00:00
parent 1d7a373d73
commit 3285e56c10
36 changed files with 706 additions and 117 deletions

105
Rakefile
View File

@@ -111,8 +111,8 @@ task migrate_to_domain: :environment do
end
if only_domains.include?("fa")
migrator.migrate_fa_users(only_user: only_user)
migrator.migrate_fa_posts(only_user: only_user)
# migrator.migrate_fa_users(only_user: only_user)
# migrator.migrate_fa_posts(only_user: only_user)
migrator.migrate_fa_users_favs(only_user: only_user)
migrator.migrate_fa_users_followed_users(only_user: only_user)
end
@@ -350,3 +350,104 @@ task perform_good_jobs: :environment do
end
end
end
task fix_removed_fa_posts: :environment do
colorize_state = ->(state) do
case state
when "ok"
"ok".green
when "removed"
"removed".red
else
state.to_s
end.bold
end
last_fa_id = ENV["start_at"]&.to_i
while true
query =
Domain::Post::FaPost
.where(state: "removed")
.where.not(title: nil)
.order(fa_id: :desc)
query = query.where(fa_id: ...last_fa_id) if last_fa_id
post = query.first
break unless post
last_fa_id = post.fa_id
puts "[before] [post.state: #{colorize_state.call(post.state)}] [post.file.id: #{post.file&.id}] [post.id: #{post.id}] [post.fa_id: #{post.fa_id}] [post.title: #{post.title}]"
Domain::Fa::Job::ScanPostJob.perform_now(post: post, force_scan: true)
post.reload
puts "[after] [post.state: #{colorize_state.call(post.state)}] [post.file.id: #{post.file&.id}] [post.id: #{post.id}] [post.fa_id: #{post.fa_id}] [post.title: #{post.title}]"
sleep 2
end
rescue => e
puts "error: #{e.message}"
binding.pry
end
task fix_fa_user_avatars: :environment do
new_users_missing_avatar =
Domain::User::FaUser.where.missing(:avatar).select(:url_name)
old_users_with_avatar =
Domain::Fa::User
.where(url_name: new_users_missing_avatar)
.includes(:avatar)
.filter(&:avatar)
old_users_with_avatar.each do |old_user|
old_avatar = old_user.avatar
new_user = Domain::User::FaUser.find_by(url_name: old_user.url_name)
if old_avatar.log_entry.nil?
puts "enqueue fresh download for #{old_user.url_name}"
new_avatar = Domain::UserAvatar.new
new_user.avatar = new_avatar
new_user.save!
Domain::Fa::Job::UserAvatarJob.perform_now(avatar: new_avatar)
new_avatar.reload
binding.pry
next
end
new_avatar = Domain::UserAvatar.new
new_avatar.log_entry_id = old_avatar.log_entry_id
new_avatar.last_log_entry_id = old_avatar.log_entry_id
new_avatar.url_str = old_avatar.file_url_str
new_avatar.downloaded_at = old_avatar.log_entry&.created_at
new_avatar.state =
case old_avatar.state
when "ok"
old_avatar.log_entry_id.present? ? "ok" : "pending"
when "file_not_found"
new_avatar.error_message = old_avatar.state
"file_404"
else
new_avatar.error_message = old_avatar.state
"http_error"
end
new_user.avatar = new_avatar
new_user.save!
puts "migrated #{old_user.url_name}"
rescue => e
puts "error: #{e.message}"
binding.pry
end
end
task run_fa_user_avatar_jobs: :environment do
avatars =
Domain::UserAvatar
.where(state: "pending")
.joins(:user)
.where(user: { type: Domain::User::FaUser.name })
puts "count: #{avatars.count}"
avatars.each do |avatar|
Domain::Fa::Job::UserAvatarJob.perform_now(avatar:)
avatar.reload
puts "perform avatar job for #{avatar.user.url_name} - #{avatar.state.bold}"
end
end

View File

@@ -76,14 +76,11 @@ module Domain::Fa::PostsHelper
nil
end
valid_type =
[URI::HTTP, URI::HTTPS, URI::Generic].any? do |klass|
uri.is_a?(klass)
end
valid_type = !uri.is_a?(URI::MailTo)
next { node_whitelist: [node] } if uri.nil? || !valid_type
uri.host ||= "www.furaffinity.net"
uri.scheme ||= "https"
path = uri.path
fa_host_matcher = /^(www\.)?furaffinity\.net$/

View File

@@ -116,7 +116,16 @@ module Domain::PostsHelper
# by default, assume the host is www.furaffinity.net
href = node["href"]&.downcase || ""
href = "//" + href if href.match?(/^(www\.)?furaffinity\.net/)
uri = URI.parse(href)
uri =
begin
URI.parse(href)
rescue URI::InvalidURIError
nil
end
valid_type = !uri.is_a?(URI::MailTo)
next { node_whitelist: [node] } if uri.nil? || !valid_type
uri.host ||= "www.furaffinity.net"
path = uri.path

View File

@@ -44,7 +44,6 @@ class Domain::Fa::Job::ScanPostJob < Domain::Fa::Job::Base
)
end
post.scanned_at = DateTime.current
logger.info format_tags("finished post scan")
ensure
post.save! if post
@@ -77,6 +76,7 @@ class Domain::Fa::Job::ScanPostJob < Domain::Fa::Job::Base
return
end
post.state = "ok"
fatal_error("submission page is not logged in") unless page.logged_in?
unless page.probably_submission?
@@ -93,7 +93,7 @@ class Domain::Fa::Job::ScanPostJob < Domain::Fa::Job::Base
submission = page.submission
unless submission.id.to_i == post.fa_id
raise("id mismatch: #{submission.id} != #{post.fa_id}")
fatal_error("id mismatch: #{submission.id} != #{post.fa_id}")
end
# save before any changes so post has an id for any files
@@ -111,7 +111,7 @@ class Domain::Fa::Job::ScanPostJob < Domain::Fa::Job::Base
invalid: :replace,
undef: :replace,
)
post.keywords = submission.keywords_array || []
post.keywords = submission.keywords_array
uri = Addressable::URI.parse(submission.full_res_img)
uri.scheme = "https" if uri.scheme.blank?
file = post.file || post.build_file

View File

@@ -15,10 +15,12 @@ class Domain::Fa::Job::UserAvatarJob < Domain::Fa::Job::Base
return
end
response =
http_client.get("https://a.furaffinity.net/0/#{user.url_name}.gif")
avatar_url_str =
avatar.url_str || "https://a.furaffinity.net/0/#{user.url_name}.gif"
response = http_client.get(avatar_url_str)
logger.push_tags(make_arg_tag(response.log_entry))
avatar.url_str = avatar_url_str
avatar.last_log_entry = response.log_entry
avatar.downloaded_at = response.log_entry.created_at

View File

@@ -130,7 +130,13 @@ class Domain::Fa::Job::UserGalleryJob < Domain::Fa::Job::Base
total_num_new_posts_seen += listing_page_stats.new_seen
total_num_posts_seen += listing_page_stats.total_seen
page.submission_folders.each { |sf| @folders.add?(sf) } if force_scan?
if force_scan?
page.submission_folders.each do |sf|
@folders.add?(
Folder.new(href: T.must(sf[:href]), title: T.must(sf[:title])),
)
end
end
page_number += 1
break if listing_page_stats.new_seen == 0 && !@go_until_end

View File

@@ -1,13 +1,28 @@
# typed: true
# typed: strict
class Domain::Fa::Parser::Base
extend T::Sig
sig { returns(Symbol) }
attr_reader :page_version
sig { params(page_version: Symbol).void }
def initialize(page_version)
@page_version = page_version
end
sig { returns(T.noreturn) }
def unimplemented_version!
raise("unimplemented page version: #{page_version}")
end
sig do
type_parameters(:Elem)
.params(
elem: T.all(T.type_parameter(:Elem), Nokogiri::XML::Node),
csses: T::Array[String],
)
.returns(T.nilable(T.type_parameter(:Elem)))
end
def first_matching_css(elem, csses)
for css in csses
e = elem.css(css).first

View File

@@ -1,17 +1,31 @@
# typed: true
# typed: strict
class Domain::Fa::Parser::ListedSubmissionParserHelper
extend T::Sig
sig { returns(T::Boolean) }
attr_accessor :debug
sig { params(elem: Nokogiri::XML::Node, page_version: Symbol).void }
def initialize(elem, page_version)
@new_parse_mode = !!elem.css("figcaption").first
@new_parse_mode = T.let(!!elem.css("figcaption").first, T::Boolean)
@elem = elem
@page_version = page_version
@debug = T.let(false, T::Boolean)
@id = T.let(nil, T.nilable(Integer))
@artist = T.let(nil, T.nilable(String))
@artist_user_page_path = T.let(nil, T.nilable(String))
@title = T.let(nil, T.nilable(String))
@view_path = T.let(nil, T.nilable(String))
@thumb_path = T.let(nil, T.nilable(String))
end
sig { returns(T.nilable(Integer)) }
def id
@id ||= %r{/view/(\d+)}.match(view_path).try(:[], 1).try(:to_i)
end
sig { returns(T.nilable(String)) }
def artist
@artist ||=
if !@new_parse_mode
@@ -21,6 +35,7 @@ class Domain::Fa::Parser::ListedSubmissionParserHelper
end
end
sig { returns(T.nilable(String)) }
def artist_user_page_path
@artist_user_page_path ||=
if !@new_parse_mode
@@ -30,10 +45,12 @@ class Domain::Fa::Parser::ListedSubmissionParserHelper
end
end
sig { returns(T.nilable(String)) }
def artist_url_name
artist_user_page_path.split("/").last
artist_user_page_path&.split("/")&.last
end
sig { returns(T.nilable(String)) }
def title
@title ||=
if !@new_parse_mode
@@ -43,6 +60,7 @@ class Domain::Fa::Parser::ListedSubmissionParserHelper
end
end
sig { returns(T.nilable(String)) }
def view_path
@view_path ||=
if !@new_parse_mode
@@ -52,6 +70,7 @@ class Domain::Fa::Parser::ListedSubmissionParserHelper
end
end
sig { returns(T.nilable(String)) }
def thumb_path
@thumb_path ||=
if !@new_parse_mode

View File

@@ -4,6 +4,8 @@
require "nokogiri"
class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
extend T::Sig
# old, old before legacy
VERSION_0 = :old_old
# legacy version
@@ -11,6 +13,12 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
# redux version
VERSION_2 = :redux
sig do
params(
page_html: T.any(String, Nokogiri::HTML4::Document),
require_logged_in: T::Boolean,
).void
end
def initialize(page_html, require_logged_in: true)
@page =
if page_html.is_a? Nokogiri::HTML::Document
@@ -18,9 +26,10 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
else
phtml = page_html.delete("\u0000")
@phtml = phtml
Nokogiri.HTML(phtml)
T.cast(Nokogiri.HTML(phtml), Nokogiri::HTML4::Document)
end
@page_version =
super(
if @page.css("link[href='/themes/beta/img/favicon.ico']").first
VERSION_2
elsif @page.css(".submission-list section").first ||
@@ -28,26 +37,29 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
VERSION_1
else
VERSION_0
end
end,
)
if require_logged_in && !submission_not_found?
raise Domain::Fa::Parser::NotLoggedInError unless logged_in?
end
end
sig { void }
def require_logged_in!
if !@page.css("img.loggedin_user_avatar")&.first.nil?
raise Domain::Fa::Parser::NotLoggedInError
end
end
sig { returns(T::Boolean) }
def submission_not_found?
# the username elem is never shown on a "not found" page
return false if logged_in_user_elem
not_found_text =
"The submission you are trying to find is not in our database"
case @page_version
!!case @page_version
when VERSION_2
@page.css("body .section-body")&.first&.text&.include?(not_found_text)
else
@@ -59,6 +71,7 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
end
end
sig { returns(T::Boolean) }
def logged_in?
logged_in_user_elem ? true : false
end
@@ -74,6 +87,7 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
end
end
sig { returns(T.nilable(String)) }
def favorites_next_button_id
next_regex = %r{/favorites/.+/(\d+)/next/?}
@@ -98,6 +112,7 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
end
end
sig { returns(T::Array[T::Hash[Symbol, String]]) }
def submission_folders
@submission_folders ||=
@page
@@ -107,6 +122,7 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
end
end
sig { returns(T::Array[Nokogiri::XML::Node]) }
def submission_elems
@submission_elems ||=
case @page_version
@@ -130,7 +146,7 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
].lazy.map { |css| @page.css(css) }.reject(&:empty?).to_a.flatten
else
unimplemented_version!
end
end.to_a
end
def logged_in_user
@@ -226,17 +242,29 @@ class Domain::Fa::Parser::Page < Domain::Fa::Parser::Base
def user_list
@user_list ||= Domain::Fa::Parser::UserListParserHelper.user_list(@page)
end
end
private
def elem_after_text_match(children, regex)
idx = elem_idx_after_text_match(children, regex)
return nil unless idx
children[idx + 1]
end
def elem_idx_after_text_match(children, regex)
children.find_index { |child| child.text.match(regex) }
private
sig do
params(
children: T.any(Nokogiri::XML::Node, Nokogiri::XML::NodeSet),
regex: Regexp,
).returns(T.nilable(Nokogiri::XML::Node))
end
def self.elem_after_text_match(children, regex)
idx = elem_idx_after_text_match(children, regex)
return nil unless idx
children[idx + 1]
end
sig do
params(
children: T.any(Nokogiri::XML::Node, Nokogiri::XML::NodeSet),
regex: Regexp,
).returns(T.nilable(Integer))
end
def self.elem_idx_after_text_match(children, regex)
children.find_index { |child| child.text.match(regex) }
end
end

View File

@@ -1,15 +1,42 @@
# typed: true
# typed: strict
class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
VERSION_0 = Domain::Fa::Parser::Page::VERSION_0
VERSION_1 = Domain::Fa::Parser::Page::VERSION_1
VERSION_2 = Domain::Fa::Parser::Page::VERSION_2
sig do
params(elem: Nokogiri::XML::Node, phtml: String, page_version: Symbol).void
end
def initialize(elem, phtml, page_version)
@elem = elem
@phtml = phtml
@page_version = page_version
@id = T.let(nil, T.nilable(Integer))
@small_img = T.let(nil, T.nilable(String))
@title = T.let(nil, T.nilable(String))
@artist = T.let(nil, T.nilable(String))
@artist_user_page_path = T.let(nil, T.nilable(String))
@artist_avatar_url = T.let(nil, T.nilable(String))
@description_html = T.let(nil, T.nilable(String))
@full_res_img = T.let(nil, T.nilable(String))
@posted_date = T.let(nil, T.nilable(ActiveSupport::TimeWithZone))
@rating = T.let(nil, T.nilable(Symbol))
@category = T.let(nil, T.nilable(String))
@theme = T.let(nil, T.nilable(String))
@category_full_str_redux = T.let(nil, T.nilable(String))
@species = T.let(nil, T.nilable(String))
@gender = T.let(nil, T.nilable(String))
@num_favorites = T.let(nil, T.nilable(Integer))
@num_comments = T.let(nil, T.nilable(Integer))
@num_views = T.let(nil, T.nilable(Integer))
@resolution_str = T.let(nil, T.nilable(String))
@keywords_array = T.let(nil, T.nilable(T::Array[String]))
@information_elem = T.let(nil, T.nilable(Nokogiri::XML::Node))
@stats_container_redux = T.let(nil, T.nilable(Nokogiri::XML::Node))
end
sig { returns(Integer) }
def id
# @elem.css("form[name=myform]").first['action'].split("/").last.to_i
@id ||=
@@ -19,10 +46,12 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def small_img
@elem.css("#submissionImg").first["src"].strip
end
sig { returns(String) }
def title
# r = @elem.css(".cat").first.text.strip
case @page_version
@@ -35,6 +64,7 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def artist
# @elem.css(".cat a").first.text.strip
@artist ||=
@@ -48,6 +78,7 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def artist_user_page_path
@artist_user_page_path ||=
case @page_version
@@ -60,10 +91,12 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def artist_url_name
artist_user_page_path.split("/").last
T.must(artist_user_page_path.split("/").last)
end
sig { returns(String) }
def artist_avatar_url
@artist_avatar_url ||=
case @page_version
@@ -76,6 +109,7 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def description_html
case @page_version
# when VERSION_0
@@ -89,6 +123,7 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def full_res_img
case @page_version
when VERSION_0
@@ -113,7 +148,12 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
@posted_date ||=
case @page_version
when VERSION_0, VERSION_1
idx = elem_idx_after_text_match(info_children, /Posted/)
idx =
Domain::Fa::Parser::Page.elem_idx_after_text_match(
info_children,
/Posted/,
)
idx = T.must(idx)
child = info_children[idx..idx + 5].find { |ic| ic.name == "span" }
date_str = child.try(:[], "title").try(:strip)
if date_str
@@ -131,6 +171,7 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end&.in_time_zone("UTC")
end
sig { returns(Symbol) }
def rating
case @page_version
when VERSION_2
@@ -148,11 +189,15 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(T.nilable(String)) }
def category
@category ||=
case @page_version
when VERSION_0, VERSION_1
elem_after_text_match(info_children, /Category/).text.strip
Domain::Fa::Parser::Page
.elem_after_text_match(info_children, /Category/)
&.text
&.strip
when VERSION_2
category_full_str_redux&.split(" / ")&.first&.strip
else
@@ -160,11 +205,15 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(T.nilable(String)) }
def theme
@theme ||=
case @page_version
when VERSION_0, VERSION_1
elem_after_text_match(info_children, /Theme/).text.strip
Domain::Fa::Parser::Page
.elem_after_text_match(info_children, /Theme/)
&.text
&.strip
when VERSION_2
category_full_str_redux&.split(" / ")&.last&.strip
else
@@ -173,6 +222,7 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
# FA started combining "Category / Theme" string into one
sig { returns(T.nilable(String)) }
def category_full_str_redux
@category_full_str_redux ||=
case @page_version
@@ -183,11 +233,15 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def species
@species ||=
case @page_version
when VERSION_0, VERSION_1
elem_after_text_match(info_children, /Species/).try(:text).try(:strip)
Domain::Fa::Parser::Page
.elem_after_text_match(info_children, /Species/)
&.text
&.strip
when VERSION_2
info_text_value_redux("Species")
else
@@ -195,11 +249,15 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def gender
@gender ||=
case @page_version
when VERSION_0, VERSION_1
elem_after_text_match(info_children, /Gender/).try(:text).try(:strip)
Domain::Fa::Parser::Page
.elem_after_text_match(info_children, /Gender/)
&.text
&.strip
when VERSION_2
info_text_value_redux("Gender")
else
@@ -207,11 +265,16 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(Integer) }
def num_favorites
@num_favorites ||=
case @page_version
when VERSION_0, VERSION_1
elem_after_text_match(info_children, /Favorites/).text.strip.to_i
Domain::Fa::Parser::Page
.elem_after_text_match(info_children, /Favorites/)
&.text
&.strip
&.to_i
when VERSION_2
stats_container_redux
.css(".favorites .font-large")
@@ -224,11 +287,16 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(Integer) }
def num_comments
@num_comments ||=
case @page_version
when VERSION_0, VERSION_1
elem_after_text_match(info_children, /Comments/).text.strip.to_i
Domain::Fa::Parser::Page
.elem_after_text_match(info_children, /Comments/)
&.text
&.strip
&.to_i
when VERSION_2
stats_container_redux.css(".comments .font-large").first.text.strip.to_i
else
@@ -236,11 +304,16 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(Integer) }
def num_views
@num_views ||=
case @page_version
when VERSION_0, VERSION_1
elem_after_text_match(info_children, /Views/).text.strip.to_i
Domain::Fa::Parser::Page
.elem_after_text_match(info_children, /Views/)
&.text
&.strip
&.to_i
when VERSION_2
stats_container_redux.css(".views .font-large").first.text.strip.to_i
else
@@ -248,24 +321,32 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(String) }
def resolution_str
@resolution_str ||=
case @page_version
when VERSION_0
elem_after_text_match(info_children, /Resolution/).try(:text).try(
:strip,
)
Domain::Fa::Parser::Page
.elem_after_text_match(info_children, /Resolution/)
&.text
&.strip
when VERSION_1
idx = elem_idx_after_text_match(info_children, /Resolution/)
idx =
Domain::Fa::Parser::Page.elem_idx_after_text_match(
info_children,
/Resolution/,
)
idx = T.must(idx)
info_children[idx + 1].try(:text).try(:strip)
when VERSION_2
parts = info_text_value_redux("Size").split(" ")
parts.first + "x" + parts.last
parts = T.must(info_text_value_redux("Size")&.split(" "))
"#{parts.first}x#{parts.last}"
else
unimplemented_version!
end
end
sig { returns(T::Array[String]) }
def keywords_array
@keywords_array ||=
case @page_version
@@ -275,19 +356,22 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
@elem.css(".tags-row .tags a").map(&:text).map(&:strip)
else
unimplemented_version!
end&.reject(&:empty?)
end&.reject(&:empty?) || []
end
private
sig { returns(Nokogiri::XML::NodeSet) }
def info_children
information_elem.children
end
sig { params(i: Integer).returns(Nokogiri::XML::Node) }
def info_child(i)
information_elem.children[i]
end
sig { returns(Nokogiri::XML::Node) }
def information_elem
@information_elem ||=
case @page_version
@@ -300,10 +384,12 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
end
end
sig { returns(Nokogiri::XML::Node) }
def info_text_elem_redux
@elem.css("section.info.text").first
end
sig { params(info_section: String).returns(T.nilable(String)) }
def info_text_value_redux(info_section)
info_text_elem_redux
.css(".highlight")
@@ -315,6 +401,7 @@ class Domain::Fa::Parser::SubmissionParserHelper < Domain::Fa::Parser::Base
&.strip
end
sig { returns(Nokogiri::XML::NodeSet) }
def stats_container_redux
@elem.css(".stats-container.text")
end

View File

@@ -1,7 +1,10 @@
# typed: true
# typed: strict
class Domain::Fa::Parser::UserListParserHelper
extend T::Sig
User = Struct.new(:name, :url_name, :href, keyword_init: true)
sig { params(page: Nokogiri::XML::Node).returns(T::Array[User]) }
def self.user_list(page)
page
.css(".watch-list .watch-list-items")
@@ -10,6 +13,7 @@ class Domain::Fa::Parser::UserListParserHelper
private
sig { params(elem: Nokogiri::XML::Node).returns(User) }
def self.watch_list_item_to_user_struct(elem)
link = elem.css("a").first
href = link["href"]

View File

@@ -1,10 +1,33 @@
# typed: true
# typed: strict
class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
extend T::Sig
VERSION_0 = Domain::Fa::Parser::Page::VERSION_0
VERSION_1 = Domain::Fa::Parser::Page::VERSION_1
VERSION_2 = Domain::Fa::Parser::Page::VERSION_2
sig { params(elem: Nokogiri::HTML::Document, page_version: Symbol).void }
def initialize(elem, page_version)
@name = T.let(nil, T.nilable(String))
@account_status = T.let(nil, T.nilable(Symbol))
@full_name = T.let(nil, T.nilable(String))
@artist_type = T.let(nil, T.nilable(String))
@profile_thumb_url = T.let(nil, T.nilable(String))
@registered_since = T.let(nil, T.nilable(ActiveSupport::TimeWithZone))
@mood = T.let(nil, T.nilable(String))
@profile_html = T.let(nil, T.nilable(String))
@num_pageviews = T.let(nil, T.nilable(Integer))
@num_submissions = T.let(nil, T.nilable(Integer))
@num_comments_recieved = T.let(nil, T.nilable(Integer))
@num_comments_given = T.let(nil, T.nilable(Integer))
@num_journals = T.let(nil, T.nilable(Integer))
@num_favorites = T.let(nil, T.nilable(Integer))
@recent_favs = T.let(nil, T.nilable(T::Array[Integer]))
@recent_watchers = T.let(nil, T.nilable(T::Array[RecentUser]))
@recent_watching = T.let(nil, T.nilable(T::Array[RecentUser]))
@statistics = T.let(nil, T.nilable(Nokogiri::XML::Element))
@main_about = T.let(nil, T.nilable(Nokogiri::XML::Element))
@elem = elem
@page_version = page_version
end
@@ -18,6 +41,7 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
"", # deceased
]
sig { returns(T.nilable(String)) }
def name
@name ||=
begin
@@ -55,6 +79,7 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
end
end
sig { returns(T.nilable(Symbol)) }
def account_status
@account_status ||=
begin
@@ -86,15 +111,25 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
end
end
sig { returns(T.nilable(String)) }
def full_name
@full_name ||= elem_after_text_match(main_about.children, /Full/).text.strip
@full_name ||=
Domain::Fa::Parser::Page
.elem_after_text_match(main_about.children, /Full/)
&.text
&.strip
end
sig { returns(T.nilable(String)) }
def artist_type
@artist_type ||=
elem_after_text_match(main_about.children, /Type/).try(:text).try(:strip)
Domain::Fa::Parser::Page
.elem_after_text_match(main_about.children, /Type/)
&.text
&.strip
end
sig { returns(T.nilable(String)) }
def profile_thumb_url
@profile_thumb_url ||=
case @page_version
@@ -107,11 +142,17 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
end
end
sig { returns(T.nilable(ActiveSupport::TimeWithZone)) }
def registered_since
@registered_since ||=
case @page_version
when VERSION_0, VERSION_1
elem_after_text_match(main_about.children, /Registered/).text.strip
Time.zone.parse(
Domain::Fa::Parser::Page
.elem_after_text_match(main_about.children, /Registered/)
&.text
&.strip,
)
when VERSION_2
date_str =
@elem
@@ -120,40 +161,52 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
&.next_sibling
&.text
&.strip
DateTime.parse(date_str) if date_str
Time.zone.parse(date_str) if date_str
else
unimplemented_version!
end
end
sig { returns(T.nilable(String)) }
def mood
@mood ||= elem_after_text_match(main_about.children, /mood/).text.strip
@mood ||=
Domain::Fa::Parser::Page
.elem_after_text_match(main_about.children, /mood/)
&.text
&.strip
end
sig { returns(String) }
def profile_html
@profile_html ||= main_about.inner_html.force_encoding("utf-8")
end
sig { returns(Integer) }
def num_pageviews
@num_pageviews ||= stat_value(:pvs, 0)
end
sig { returns(Integer) }
def num_submissions
@num_submissions ||= stat_value(:subs, 1)
end
sig { returns(Integer) }
def num_comments_recieved
@num_comments_recieved ||= stat_value(:crec, 3)
end
sig { returns(Integer) }
def num_comments_given
@num_comments_given ||= stat_value(:cgiv, 4)
end
sig { returns(Integer) }
def num_journals
@num_journals ||= stat_value(:njr, 5)
end
sig { returns(Integer) }
def num_favorites
@num_favorites ||= stat_value(:nfav, 2)
end
@@ -226,6 +279,7 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
end
end
sig { params(legacy_name: Symbol, redux_idx: Integer).returns(Integer) }
def stat_value(legacy_name, redux_idx)
legacy_map = # if false # old mode?
# { pvs: 2, subs: 5, crec: 8, cgiv: 11, njr: 14, nfav: 17 }
@@ -251,6 +305,7 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
end
end
sig { returns(Nokogiri::XML::Element) }
def statistics
@statistics ||=
case @page_version
@@ -267,6 +322,7 @@ class Domain::Fa::Parser::UserPageHelper < Domain::Fa::Parser::Base
end
end
sig { returns(Nokogiri::XML::Element) }
def main_about
@main_about ||=
case @page_version

View File

@@ -1,22 +1,34 @@
# typed: false
# typed: strict
class Domain::Fa::UserEnqueuer
extend T::Sig
extend T::Helpers
include HasBulkEnqueueJobs
include HasColorLogger
include HasMeasureDuration
include Domain::Fa::HasCountFailedInQueue
sig do
params(
start_at: Integer,
low_water_mark: Integer,
high_water_mark: Integer,
).void
end
def initialize(start_at:, low_water_mark:, high_water_mark:)
@low_water_mark = low_water_mark
@high_water_mark = high_water_mark
raise if @high_water_mark <= @low_water_mark
@user_iterator =
Enumerator.new do |e|
Domain::Fa::User
.where("id >= ?", start_at)
.find_each { |user| e << user }
end
T.let(
Enumerator.new do |e|
Domain::User::FaUser.find_each(start: start_at) { |user| e << user }
end,
T::Enumerator[Domain::User::FaUser],
)
end
sig { returns(T.nilable(Symbol)) }
def run_once
already_enqueued = enqueued_count
if already_enqueued <= @low_water_mark
@@ -25,12 +37,9 @@ class Domain::Fa::UserEnqueuer
"enqueuing #{to_enqueue.to_s.bold} more users - #{already_enqueued.to_s.bold} already enqueued",
)
rows =
measure(
proc do |p|
p && "gathered #{p.length.to_s.bold} users to enqueue" ||
"gathering users..."
end,
) { to_enqueue.times.map { @user_iterator.next } }
measure("gathered #{to_enqueue.to_s.bold} users to enqueue") do
to_enqueue.times.map { @user_iterator.next }
end
measure("enqueue jobs") do
rows.each do |user|
types = []
@@ -60,14 +69,14 @@ class Domain::Fa::UserEnqueuer
end
end
avatar = user.ensure_avatar!
if avatar.file.nil? && avatar.state == "ok"
Domain::Fa::Job::UserAvatarJob.perform_later({ user: user })
avatar = user.avatar || user.create_avatar!
if avatar.log_entry.nil? && avatar.state_pending?
Domain::Fa::Job::UserAvatarJob.perform_later({ avatar: avatar })
types << "avatar"
end
types = types.map { |t| t.bold }.join("|")
logger.info "#{types} - #{user.url_name.bold} - #{user.id.to_s.bold}"
logger.info "#{types} - #{user.url_name&.bold} - #{user.id.to_s.bold}"
end
end
throw StopIteration if rows.empty?
@@ -82,6 +91,7 @@ class Domain::Fa::UserEnqueuer
private
sig { returns(Integer) }
def enqueued_count
if Rails.env.test?
return SpecUtil.enqueued_jobs(Domain::Fa::Job::UserFollowsJob).count

View File

@@ -251,9 +251,11 @@ class Domain::MigrateToDomain
format: "%t: %c/%C %B %p%% %a %e",
output: @pb_sink,
)
query.find_each do |user|
ReduxApplicationRecord.transaction { migrate_fa_user_favs(user) }
pb.progress = [pb.progress + 1, pb.total].min
query.find_in_batches(batch_size: 5) do |batch|
ReduxApplicationRecord.transaction do
batch.each { |user| migrate_fa_user_favs(user) }
end
pb.progress = [pb.progress + batch.size, pb.total].min
end
end
@@ -744,15 +746,19 @@ class Domain::MigrateToDomain
def migrate_fa_user_favs(user)
user_url_name = user.url_name
old_user = Domain::Fa::User.find_by!(url_name: user_url_name)
old_post_fa_ids = old_user.fav_posts.pluck(:fa_id)
new_post_ids = Domain::Post::FaPost.where(fa_id: old_post_fa_ids).pluck(:id)
new_post_ids.each_slice(10_000) do |post_ids|
Domain::UserPostFav.upsert_all(
post_ids.map { |post_id| { user_id: user.id, post_id: } },
unique_by: %i[user_id post_id],
)
end
Domain::UserPostFav.connection.execute(<<~SQL)
INSERT INTO domain_user_post_favs (user_id, post_id)
SELECT #{user.id}, domain_posts.id
FROM domain_fa_posts old_posts
INNER JOIN domain_posts ON
(domain_posts.json_attributes->>'fa_id')::integer = old_posts.fa_id
AND domain_posts.type = 'Domain::Post::FaPost'
INNER JOIN domain_fa_favs ON
domain_fa_favs.post_id = old_posts.id
AND domain_fa_favs.user_id = #{old_user.id}
ON CONFLICT (user_id, post_id) DO NOTHING
SQL
if user.faved_posts.count != old_user.fav_posts.count
logger.error(

View File

@@ -76,9 +76,9 @@ class FaBackfillFavs
favs = T.must(user_favs[url_name])
page.submissions_parsed.each do |submission|
next unless submission.id
favs.add(submission.id)
fa_id = submission.id
next unless fa_id
favs.add(fa_id)
end
break if @limit && @total_log_entries_processed >= @limit

View File

@@ -20,6 +20,21 @@ module HasCompositeToParam
sig { abstract.returns([String, Symbol]) }
def param_prefix_and_attribute
end
sig(:final) { returns(String) }
def param_prefix
param_prefix_and_attribute[0]
end
sig(:final) { returns(Symbol) }
def param_attribute
param_prefix_and_attribute[1]
end
sig { overridable.returns(Symbol) }
def param_order_attribute
param_attribute
end
end
mixes_in_class_methods(ClassMethods)

View File

@@ -140,6 +140,11 @@ class Domain::Post < ReduxApplicationRecord
def title
end
sig { overridable.returns(String) }
def title_for_view
title || "(unknown)"
end
sig { abstract.returns(T.nilable(T.any(String, Integer))) }
def domain_id_for_view
end

View File

@@ -32,6 +32,14 @@ class Domain::Post::FaPost < Domain::Post
after_initialize { self.state ||= "ok" }
enum :state,
{
ok: "ok",
removed: "removed",
scan_error: "scan_error",
file_error: "file_error",
},
prefix: "state"
validates :state, inclusion: { in: %w[ok removed scan_error file_error] }
validates :fa_id, presence: true
@@ -47,7 +55,7 @@ class Domain::Post::FaPost < Domain::Post
sig { override.returns(T.nilable(String)) }
def title
super || "(unknown)"
super
end
sig { override.returns(T.nilable(Domain::User)) }

View File

@@ -22,12 +22,13 @@ class Domain::PostFile < ReduxApplicationRecord
ok: "ok",
retryable_error: "retryable_error",
terminal_error: "terminal_error",
removed: "removed",
},
prefix: "state"
validates :state,
inclusion: {
in: %w[pending ok retryable_error terminal_error],
in: %w[pending ok removed retryable_error terminal_error],
}
after_initialize do

View File

@@ -107,6 +107,7 @@ class Domain::User < ReduxApplicationRecord
def self.has_created_posts!(klass)
self.class_has_created_posts = klass
has_many :posts,
-> { order(klass.param_order_attribute => :desc) },
through: :user_post_creations,
source: :post,
class_name: klass.name
@@ -116,6 +117,7 @@ class Domain::User < ReduxApplicationRecord
def self.has_faved_posts!(klass)
self.class_has_faved_posts = klass
has_many :faved_posts,
-> { order(klass.param_order_attribute => :desc) },
through: :user_post_favs,
source: :post,
class_name: klass.name

View File

@@ -126,7 +126,7 @@ class Domain::User::FaUser < Domain::User
(contents = response.contents)
parser =
Domain::Fa::Parser::Page.new(contents, require_logged_in: false)
parser.user_page.account_status if parser.probably_user_page?
parser.user_page.account_status.to_s if parser.probably_user_page?
end
end || "unknown"
end

View File

@@ -14,6 +14,15 @@ class Domain::UserAvatar < ReduxApplicationRecord
belongs_to :last_log_entry, class_name: "::HttpLogEntry", optional: true
belongs_to :log_entry, class_name: "::HttpLogEntry", optional: true
enum :state,
{
pending: "pending",
ok: "ok",
file_404: "file_404",
http_error: "http_error",
},
prefix: "state"
validates :state,
presence: true,
inclusion: {

View File

@@ -29,7 +29,7 @@
),
class:
"max-h-[300px] max-w-[300px] rounded-md border border-slate-300 object-contain shadow-md",
alt: post.title %>
alt: post.title_for_view %>
<% end %>
<% else %>
<span>No file available</span>
@@ -37,7 +37,7 @@
</div>
<div class="border-t border-slate-300">
<h2 class="p-4 text-center text-lg">
<%= link_to post.title, domain_post_path(post), class: "sky-link" %>
<%= link_to post.title_for_view, domain_post_path(post), class: "sky-link" %>
</h2>
<div class="px-4 pb-4 text-sm text-slate-500">
<div class="flex justify-between gap-2">

View File

@@ -3,7 +3,7 @@
<div class="flex min-w-0 items-center gap-1">
<%= image_tag domain_post_domain_icon_path(post), class: "w-6 h-6" %>
<span class="truncate text-lg font-medium">
<%= link_to post.title,
<%= link_to post.title_for_view,
post.domain_url_for_view.to_s,
class: "text-blue-600 hover:underline",
target: "_blank" %>

View File

@@ -5,7 +5,7 @@
<%= link_to "#{user.posts.count} total", domain_user_posts_path(user), class: "sky-link" %>
</span>
</h2>
<% recent_posts = user.posts.order(user.class.post_order_attribute => :desc).limit(5).to_a %>
<% recent_posts = user.posts.limit(5).to_a %>
<% if recent_posts.any? %>
<% recent_posts.each do |post| %>
<div class="flex items-center px-4 py-2">

View File

@@ -1,4 +1,5 @@
<% fav_posts = user.faved_posts.limit(5).to_a %>
<%# annoying postgres optimizer bug that causes an extremly bad plan if the limit is below 50 %>
<% fav_posts = user.faved_posts.includes(:creator).limit(60).to_a[..5] %>
<section class="animated-shadow-sky sky-section">
<h2 class="section-header">
<span class="font-medium text-slate-900">Favorited Posts</span>

View File

@@ -8,6 +8,7 @@
class Domain::Post::FaPost
include GeneratedAssociationMethods
include GeneratedAttributeMethods
include EnumMethodsModule
extend CommonRelationMethods
extend GeneratedRelationMethods
@@ -51,6 +52,9 @@ class Domain::Post::FaPost
).returns(::Domain::Post::FaPost)
end
def new(attributes = nil, &block); end
sig { returns(T::Hash[T.any(String, Symbol), String]) }
def states; end
end
module CommonRelationMethods
@@ -442,6 +446,32 @@ class Domain::Post::FaPost
def third_to_last!; end
end
module EnumMethodsModule
sig { void }
def state_file_error!; end
sig { returns(T::Boolean) }
def state_file_error?; end
sig { void }
def state_ok!; end
sig { returns(T::Boolean) }
def state_ok?; end
sig { void }
def state_removed!; end
sig { returns(T::Boolean) }
def state_removed?; end
sig { void }
def state_scan_error!; end
sig { returns(T::Boolean) }
def state_scan_error?; end
end
module GeneratedAssociationMethods
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::FaUser) }
def build_creator(*args, &blk); end
@@ -765,6 +795,18 @@ class Domain::Post::FaPost
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def none(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_file_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_ok(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_scan_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def null_relation?(*args, &blk); end
@@ -824,6 +866,18 @@ class Domain::Post::FaPost
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def select(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_file_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_ok(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_scan_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def strict_loading(*args, &blk); end
@@ -2068,7 +2122,7 @@ class Domain::Post::FaPost
sig { returns(T.nilable(::String)) }
def state; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
sig { params(value: T.nilable(T.any(::String, ::Symbol))).returns(T.nilable(T.any(::String, ::Symbol))) }
def state=(value); end
sig { returns(T::Boolean) }
@@ -2089,7 +2143,12 @@ class Domain::Post::FaPost
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def state_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
sig do
params(
from: T.nilable(T.any(::String, ::Symbol)),
to: T.nilable(T.any(::String, ::Symbol))
).returns(T::Boolean)
end
def state_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
@@ -2098,7 +2157,12 @@ class Domain::Post::FaPost
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def state_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
sig do
params(
from: T.nilable(T.any(::String, ::Symbol)),
to: T.nilable(T.any(::String, ::Symbol))
).returns(T::Boolean)
end
def state_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
@@ -2452,6 +2516,18 @@ class Domain::Post::FaPost
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def none(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_file_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_ok(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_scan_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def null_relation?(*args, &blk); end
@@ -2511,6 +2587,18 @@ class Domain::Post::FaPost
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def select(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_file_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_ok(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_scan_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def strict_loading(*args, &blk); end

View File

@@ -427,6 +427,12 @@ class Domain::PostFile
sig { returns(T::Boolean) }
def state_pending?; end
sig { void }
def state_removed!; end
sig { returns(T::Boolean) }
def state_removed?; end
sig { void }
def state_retryable_error!; end
@@ -602,6 +608,9 @@ class Domain::PostFile
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_retryable_error(*args, &blk); end
@@ -673,6 +682,9 @@ class Domain::PostFile
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_retryable_error(*args, &blk); end
@@ -1619,6 +1631,9 @@ class Domain::PostFile
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_retryable_error(*args, &blk); end
@@ -1690,6 +1705,9 @@ class Domain::PostFile
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_retryable_error(*args, &blk); end

View File

@@ -597,6 +597,9 @@ class Domain::PostFile::InkbunnyPostFile
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_retryable_error(*args, &blk); end
@@ -668,6 +671,9 @@ class Domain::PostFile::InkbunnyPostFile
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_retryable_error(*args, &blk); end
@@ -2080,6 +2086,9 @@ class Domain::PostFile::InkbunnyPostFile
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_retryable_error(*args, &blk); end
@@ -2151,6 +2160,9 @@ class Domain::PostFile::InkbunnyPostFile
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_removed(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_retryable_error(*args, &blk); end

View File

@@ -8,6 +8,7 @@
class Domain::UserAvatar
include GeneratedAssociationMethods
include GeneratedAttributeMethods
include EnumMethodsModule
extend CommonRelationMethods
extend GeneratedRelationMethods
@@ -51,6 +52,9 @@ class Domain::UserAvatar
).returns(::Domain::UserAvatar)
end
def new(attributes = nil, &block); end
sig { returns(T::Hash[T.any(String, Symbol), String]) }
def states; end
end
module CommonRelationMethods
@@ -426,6 +430,32 @@ class Domain::UserAvatar
def third_to_last!; end
end
module EnumMethodsModule
sig { void }
def state_file_404!; end
sig { returns(T::Boolean) }
def state_file_404?; end
sig { void }
def state_http_error!; end
sig { returns(T::Boolean) }
def state_http_error?; end
sig { void }
def state_ok!; end
sig { returns(T::Boolean) }
def state_ok?; end
sig { void }
def state_pending!; end
sig { returns(T::Boolean) }
def state_pending?; end
end
module GeneratedAssociationMethods
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_last_log_entry(*args, &blk); end
@@ -582,6 +612,18 @@ class Domain::UserAvatar
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def none(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_file_404(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_http_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_ok(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def null_relation?(*args, &blk); end
@@ -641,6 +683,18 @@ class Domain::UserAvatar
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def select(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_file_404(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_http_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_ok(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def strict_loading(*args, &blk); end
@@ -1164,7 +1218,7 @@ class Domain::UserAvatar
sig { returns(T.nilable(::String)) }
def state; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
sig { params(value: T.nilable(T.any(::String, ::Symbol))).returns(T.nilable(T.any(::String, ::Symbol))) }
def state=(value); end
sig { returns(T::Boolean) }
@@ -1185,7 +1239,12 @@ class Domain::UserAvatar
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def state_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
sig do
params(
from: T.nilable(T.any(::String, ::Symbol)),
to: T.nilable(T.any(::String, ::Symbol))
).returns(T::Boolean)
end
def state_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
@@ -1194,7 +1253,12 @@ class Domain::UserAvatar
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def state_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
sig do
params(
from: T.nilable(T.any(::String, ::Symbol)),
to: T.nilable(T.any(::String, ::Symbol))
).returns(T::Boolean)
end
def state_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
@@ -1461,6 +1525,18 @@ class Domain::UserAvatar
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def none(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_file_404(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_http_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_ok(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def null_relation?(*args, &blk); end
@@ -1520,6 +1596,18 @@ class Domain::UserAvatar
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def select(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_file_404(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_http_error(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_ok(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def state_pending(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def strict_loading(*args, &blk); end

View File

@@ -27,6 +27,8 @@ class GoodJob::JobsController
include ::GoodJob::ApplicationController::HelperMethods
include ::HelpersInterface
include ::Domain::PostsHelper
include ::Domain::UsersHelper
include ::Domain::PostGroupsHelper
include ::GoodJobHelper
end

View File

@@ -126,7 +126,7 @@ describe Domain::Fa::Job::ScanPostJob do
post = Domain::Post::FaPost.find_by(fa_id: 59_714_213)
expect(SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanFileJob)).to match(
array_including(
{ post_file: post.file, caused_by_entry: @log_entries.first },
{ file: post.file, caused_by_entry: @log_entries.first },
),
)
end

View File

@@ -23,7 +23,7 @@ describe Domain::Inkbunny::Job::UserAvatarJob do
end
def perform_job
perform_now({ user: user })
perform_now({ avatar: avatar })
end
context "when avatar_url_str is blank" do
@@ -159,7 +159,7 @@ describe Domain::Inkbunny::Job::UserAvatarJob do
end
it "sets error state and then ok state" do
perform_now({ user: user }, should_raise: Scraper::JobBase::JobError)
perform_now({ avatar: avatar }, should_raise: Scraper::JobBase::JobError)
user.reload
avatar.reload
@@ -168,7 +168,7 @@ describe Domain::Inkbunny::Job::UserAvatarJob do
expect(avatar.log_entry).to be_nil
expect(avatar.error_message).to include("500")
perform_now({ user: user })
perform_now({ avatar: avatar })
user.reload
avatar.reload
@@ -189,7 +189,10 @@ describe Domain::Inkbunny::Job::UserAvatarJob do
let(:existing_downloaded_at) { 1.day.ago }
it "keeps previous file but updates state to error" do
perform_now({ user: user }, should_raise: Scraper::JobBase::JobError)
perform_now(
{ avatar: avatar },
should_raise: Scraper::JobBase::JobError,
)
user.reload
avatar.reload
@@ -202,7 +205,7 @@ describe Domain::Inkbunny::Job::UserAvatarJob do
existing_downloaded_at,
)
perform_now({ user: user })
perform_now({ avatar: avatar })
user.reload
avatar.reload

View File

@@ -47,7 +47,7 @@ describe Domain::Fa::Parser::Page do
assert_equal 6645, up.num_comments_given
assert_equal 55, up.num_journals
assert_equal 365_602, up.num_favorites
assert_equal "Jan 12th, 2006 07:52", up.registered_since
assert_equal Time.zone.parse("Jan 12th, 2006 07:52"), up.registered_since
end
it "user page old old version is correct" do

View File

@@ -46,7 +46,7 @@ describe Domain::Fa::Parser::Page do
assert_equal 7_227, up.num_comments_given
assert_equal 6, up.num_journals
assert_equal 1_236_200, up.num_favorites
assert_equal DateTime.new(2006, 1, 12, 7, 52), up.registered_since
assert_equal Time.zone.parse("Jan 12th, 2006 07:52"), up.registered_since
assert_equal "//a.furaffinity.net/1556545516/miles-df.gif",
up.profile_thumb_url
end
@@ -435,7 +435,7 @@ describe Domain::Fa::Parser::Page do
assert_page_type parser, :probably_user_page?
up = parser.user_page
assert_equal up.name, "MarziMoo"
assert_equal up.registered_since, DateTime.parse("Sep 7, 2018 06:19")
assert_equal up.registered_since, Time.zone.parse("Sep 7, 2018 06:19")
end
it "handles pages with no favorites" do

View File

@@ -12,10 +12,7 @@ RSpec.describe Domain::Post::FaPost do
end
it "validates state inclusion" do
post.state = "invalid"
expect(post).not_to be_valid
expect(post.errors[:state]).to include("is not included in the list")
expect do post.state = "invalid" end.to raise_error(ArgumentError)
%w[ok removed scan_error file_error].each do |valid_state|
post.state = valid_state
expect(post).to be_valid