diff --git a/app/helpers/domain/domains_helper.rb b/app/helpers/domain/domains_helper.rb
index 920628f9..e5a2ec56 100644
--- a/app/helpers/domain/domains_helper.rb
+++ b/app/helpers/domain/domains_helper.rb
@@ -53,6 +53,7 @@ module Domain::DomainsHelper
weasyl.com
x.com
youtube.com
+ sofurry.com
] + ALLOWED_PLAIN_TEXT_URL_DOMAINS
).freeze,
T::Array[String],
diff --git a/app/helpers/domain/posts_helper.rb b/app/helpers/domain/posts_helper.rb
index 7b6efee4..fcbba86d 100644
--- a/app/helpers/domain/posts_helper.rb
+++ b/app/helpers/domain/posts_helper.rb
@@ -43,6 +43,11 @@ module Domain::PostsHelper
domain_icon_path: "domain-icons/inkbunny.png",
domain_icon_title: "Inkbunny",
),
+ Domain::DomainType::Sofurry =>
+ DomainData.new(
+ domain_icon_path: "domain-icons/sofurry.png",
+ domain_icon_title: "SoFurry",
+ ),
},
T::Hash[Domain::DomainType, DomainData],
)
diff --git a/app/jobs/domain/sofurry/job/scan_gallery_job.rb b/app/jobs/domain/sofurry/job/scan_gallery_job.rb
index 3feb0410..26a76431 100644
--- a/app/jobs/domain/sofurry/job/scan_gallery_job.rb
+++ b/app/jobs/domain/sofurry/job/scan_gallery_job.rb
@@ -19,6 +19,7 @@ class Domain::Sofurry::Job::ScanGalleryJob < Domain::Sofurry::Job::Base
const :tags, T::Array[String]
const :description, T.nilable(String)
const :posted_at, Time
+ const :gallery_log_entry, T.nilable(HttpLogEntry)
end
sig { override.params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
@@ -121,24 +122,17 @@ class Domain::Sofurry::Job::ScanGalleryJob < Domain::Sofurry::Job::Base
end
posts.each do |post|
- if (existing = existing_posts[post.sofurry_id])
- existing.title = post.title
- existing.tags_array = post.tags
- existing.content_level = post.content_level
- existing.description = post.description
- existing.content_level = post.content_level
- existing.posted_at = post.posted_at.in_time_zone("UTC")
- else
- user.posts.build(
- sofurry_id: post.sofurry_id,
- title: post.title,
- tags_array: post.tags,
- media_type: post.media_type.serialize,
- description: post.description,
- content_level: post.content_level,
- posted_at: post.posted_at,
- )
- end
+ post_model =
+ existing_posts[post.sofurry_id] ||
+ user.posts.build({ sofurry_id: post.sofurry_id })
+
+ post_model.title = post.title
+ post_model.tags_array = post.tags
+ post_model.content_level = post.content_level
+ post_model.description = post.description
+ post_model.posted_at = post.posted_at.in_time_zone("UTC")
+ post_model.media_type = post.media_type.serialize
+ post_model.last_gallery_log_entry = post.gallery_log_entry
end
ReduxApplicationRecord.transaction do
@@ -256,6 +250,7 @@ class Domain::Sofurry::Job::ScanGalleryJob < Domain::Sofurry::Job::Base
content_level: post_json["contentLevel"].to_i,
posted_at: Time.at(post_json["postTime"].to_i),
media_type: media_type,
+ gallery_log_entry: response.log_entry,
)
end,
)
diff --git a/app/jobs/domain/sofurry/job/scan_user_job.rb b/app/jobs/domain/sofurry/job/scan_user_job.rb
index ccb6dd28..c272e6bd 100644
--- a/app/jobs/domain/sofurry/job/scan_user_job.rb
+++ b/app/jobs/domain/sofurry/job/scan_user_job.rb
@@ -7,14 +7,24 @@ class Domain::Sofurry::Job::ScanUserJob < Domain::Sofurry::Job::Base
user = user_from_args!
logger.push_tags(make_arg_tag(user))
logger.info(format_tags(make_arg_tag(user.page_scan, name: "page_scan")))
- scan_user_profile(user) if force_scan? || user.page_scan.due?
+ user = scan_user_profile(user) if force_scan? || user.page_scan.due?
+
+ if user.avatar.nil? && (sfid = user.sofurry_id)
+ avatar =
+ user.create_avatar!(
+ url_str: "https://www.sofurry.com/std/avatar?user=#{sfid}",
+ )
+ defer_job(Domain::Sofurry::Job::UserAvatarJob, { avatar: })
+ end
ensure
user.save! if user
end
private
- sig { params(user: Domain::User::SofurryUser).returns(T.untyped) }
+ sig do
+ params(user: Domain::User::SofurryUser).returns(Domain::User::SofurryUser)
+ end
def scan_user_profile(user)
logger.info("Scanning user page for #{user.username}")
user_sofurry_id = user.sofurry_id
@@ -22,7 +32,7 @@ class Domain::Sofurry::Job::ScanUserJob < Domain::Sofurry::Job::Base
if user_sofurry_id.nil? && user_sofurry_url_name.nil?
logger.error("user has no sofurry_id or username")
- return
+ return user
end
url =
@@ -35,7 +45,7 @@ class Domain::Sofurry::Job::ScanUserJob < Domain::Sofurry::Job::Base
response = http_client.get(url)
if response.status_code != 200
logger.error("failed to get user profile: #{response.status_code}")
- return
+ return user
end
user.last_scan_log_entry = response.log_entry
@@ -46,7 +56,7 @@ class Domain::Sofurry::Job::ScanUserJob < Domain::Sofurry::Job::Base
logger.error(format_tags("user not found"))
user.scanned_page_at = Time.now
user.state_error!
- return
+ return user
else
user.state_error!
fatal_error(
@@ -55,8 +65,20 @@ class Domain::Sofurry::Job::ScanUserJob < Domain::Sofurry::Job::Base
end
end
- user.sofurry_id ||= user_profile_json["userID"]&.to_i
- user.username = user_profile_json["username"]
+ json_sofurry_id = user_profile_json["userID"]&.to_i
+ if user_sofurry_id.present? && user_sofurry_id != json_sofurry_id
+ fatal_error(
+ "sofurry_id mismatch: #{user_sofurry_id} != #{json_sofurry_id}",
+ )
+ end
+
+ if user_sofurry_id.nil? && user.new_record?
+ by_sofurry_id =
+ Domain::User::SofurryUser.find_by(sofurry_id: json_sofurry_id)
+ user = by_sofurry_id if by_sofurry_id.present?
+ end
+
+ user.sofurry_id ||= json_sofurry_id
registered_at = user_profile_json["registrationDate"]
if registered_at == "long long ago"
user.registered_at = Time.at(0)
@@ -85,12 +107,6 @@ class Domain::Sofurry::Job::ScanUserJob < Domain::Sofurry::Job::Base
user.state_ok!
user.save!
- if user.avatar.nil?
- avatar =
- user.create_avatar!(
- url_str: "https://www.sofurry.com/std/avatar?id=#{user.sofurry_id}",
- )
- defer_job(Domain::Sofurry::Job::UserAvatarJob, { avatar: })
- end
+ user
end
end
diff --git a/app/lib/domain/sofurry/gallery_page_parser.rb b/app/lib/domain/sofurry/gallery_page_parser.rb
index a630973e..95edaef3 100644
--- a/app/lib/domain/sofurry/gallery_page_parser.rb
+++ b/app/lib/domain/sofurry/gallery_page_parser.rb
@@ -55,15 +55,27 @@ class Domain::Sofurry::GalleryPageParser
def posts
@posts ||=
begin
- @doc
- .css(".sf-browse-shortlist,.sf-browse-shortlist-zebra")
- .map do |elem|
- title_id = elem.css(".sf-browse-shortlist-title a") || next
- id = title_id&.attr("href").value.split("/")&.last&.to_i || next
- title = title_id&.text || next
- ShortGalleryEntry.new(id:, title:)
- end
- .compact
+ [
+ @doc
+ .css(".sf-browse-shortlist,.sf-browse-shortlist-zebra")
+ .map do |elem|
+ title_id = elem.css(".sf-browse-shortlist-title a").first || next
+ id = title_id&.attr("href").split("/")&.last&.to_i || next
+ title = title_id&.text || next
+ ShortGalleryEntry.new(id:, title:)
+ end
+ .compact,
+ @doc
+ .css("a.sfArtworkSmallInner")
+ .map do |elem|
+ id = elem.attr("href").split("/")&.last&.to_i || next
+ img_elem = elem.css("img.sfArtworkItem").first || next
+ title =
+ img_elem.attr("alt")&.strip&.rpartition("|by")&.first || next
+ ShortGalleryEntry.new(id:, title:)
+ end
+ .compact,
+ ].flatten
end
end
diff --git a/app/models/domain/post.rb b/app/models/domain/post.rb
index 1e596bf6..234618e4 100644
--- a/app/models/domain/post.rb
+++ b/app/models/domain/post.rb
@@ -188,6 +188,16 @@ class Domain::Post < ReduxApplicationRecord
def primary_creator_name_fallback_for_view
nil
end
+
+ class TagForView < T::Struct
+ const :category, Symbol
+ const :value, String
+ end
+
+ sig { overridable.returns(T.nilable(T::Array[TagForView])) }
+ def tags_for_view
+ nil
+ end
end
# eager load all subclasses
diff --git a/app/models/domain/post/fa_post.rb b/app/models/domain/post/fa_post.rb
index d9e86457..a0225ff3 100644
--- a/app/models/domain/post/fa_post.rb
+++ b/app/models/domain/post/fa_post.rb
@@ -185,4 +185,9 @@ class Domain::Post::FaPost < Domain::Post
end
end
end
+
+ sig { override.returns(T.nilable(T::Array[TagForView])) }
+ def tags_for_view
+ keywords.map { |value| TagForView.new(category: :general, value:) }
+ end
end
diff --git a/app/models/domain/post/sofurry_post.rb b/app/models/domain/post/sofurry_post.rb
index 0d1d9c24..8c2bb807 100644
--- a/app/models/domain/post/sofurry_post.rb
+++ b/app/models/domain/post/sofurry_post.rb
@@ -22,6 +22,11 @@ class Domain::Post::SofurryPost < Domain::Post
attr_json :last_scan_log_entry_id, :integer
belongs_to :last_scan_log_entry, class_name: "::HttpLogEntry", optional: true
+ attr_json :last_gallery_log_entry_id, :integer
+ belongs_to :last_gallery_log_entry,
+ class_name: "::HttpLogEntry",
+ optional: true
+
scope :for_media_type, ->(media_type) { where(media_type:) }
attr_json_enum(
@@ -69,7 +74,7 @@ class Domain::Post::SofurryPost < Domain::Post
sig { override.returns(T.nilable(Addressable::URI)) }
def external_url_for_view
if sfid = self.sofurry_id
- Addressable::URI.parse("https://#{sfid}.sofurry.com")
+ Addressable::URI.parse("https://www.sofurry.com/view/#{sfid}")
end
end
@@ -97,4 +102,14 @@ class Domain::Post::SofurryPost < Domain::Post
def self.view_prefix
"sf"
end
+
+ sig { override.returns(T.nilable(T::Array[TagForView])) }
+ def tags_for_view
+ tags_array.map { |value| TagForView.new(category: :general, value:) }
+ end
+
+ sig { override.returns(T.nilable(HttpLogEntry)) }
+ def scanned_post_log_entry_for_view
+ last_scan_log_entry
+ end
end
diff --git a/app/models/domain/user/sofurry_user.rb b/app/models/domain/user/sofurry_user.rb
index 306e36f9..a2e7cecd 100644
--- a/app/models/domain/user/sofurry_user.rb
+++ b/app/models/domain/user/sofurry_user.rb
@@ -2,7 +2,7 @@
class Domain::User::SofurryUser < Domain::User
include HasAttrJsonEnum
- validates :sofurry_id, presence: true, uniqueness: true
+ validates :sofurry_id, uniqueness: true, allow_blank: true
validates :username, uniqueness: true, allow_blank: true
validates :useralias, uniqueness: true, allow_blank: true
validate :username_and_useralias_match?
diff --git a/app/policies/domain/post/sofurry_post_policy.rb b/app/policies/domain/post/sofurry_post_policy.rb
new file mode 100644
index 00000000..2747c6ab
--- /dev/null
+++ b/app/policies/domain/post/sofurry_post_policy.rb
@@ -0,0 +1,3 @@
+# typed: strict
+class Domain::Post::SofurryPostPolicy < Domain::PostPolicy
+end
diff --git a/app/views/domain/posts/default/_section_tags.html.erb b/app/views/domain/posts/default/_section_tags.html.erb
index 25cd2d4c..ef725c31 100644
--- a/app/views/domain/posts/default/_section_tags.html.erb
+++ b/app/views/domain/posts/default/_section_tags.html.erb
@@ -1 +1,24 @@
-<%# By default, posts do not have tags %>
+<% tags = post.tags_for_view %>
+<% return if tags.nil? %>
+<%= sky_section_tag("Tags") do %>
+ <% if tags.any? %>
+ <% tags = tags.group_by(&:category) %>
+
+ <% tag_category_order.map { |category| tags[category] || [] }.each do |tags| %>
+ <% tags.each do |tag| %>
+
+ <% icon = font_awesome_category_icon(tag.category) %>
+ <% if icon %>
+
+ <% end %>
+ <%= tag.value %>
+
+ <% end %>
+ <% end %>
+
+ <% else %>
+ No tags
+ <% end %>
+<% end %>
diff --git a/app/views/domain/posts/default/_section_visualy_similar_posts.html.erb b/app/views/domain/posts/default/_section_visualy_similar_posts.html.erb
index 6174b5e7..86dfb844 100644
--- a/app/views/domain/posts/default/_section_visualy_similar_posts.html.erb
+++ b/app/views/domain/posts/default/_section_visualy_similar_posts.html.erb
@@ -1,4 +1,5 @@
-<% post_file = post.primary_file_for_view %>
+<% post_file = post.primary_file_for_view || return%>
+<% return unless post_file && is_thumbable_content_type?(post_file.content_type) %>
diff --git a/app/views/domain/posts/fa/_section_tags.html.erb b/app/views/domain/posts/fa/_section_tags.html.erb
deleted file mode 100644
index 4c6575dd..00000000
--- a/app/views/domain/posts/fa/_section_tags.html.erb
+++ /dev/null
@@ -1,20 +0,0 @@
-<%= sky_section_tag("Keywords") do %>
- <% keywords = keywords_for_fa_post(post) %>
- <% if keywords.any? %>
-
- <% keywords.each do |keyword| %>
-
- <% icon = font_awesome_category_icon(:general) %>
- <% if icon %>
-
- <% end %>
- <%= keyword %>
-
- <% end %>
-
- <% else %>
-
No keywords
- <% end %>
-<% end%>
diff --git a/sorbet/rbi/dsl/domain/inkbunny/job/static_file_job.rbi b/sorbet/rbi/dsl/domain/inkbunny/job/static_file_job.rbi
index 2eb7c2e8..a8eb2749 100644
--- a/sorbet/rbi/dsl/domain/inkbunny/job/static_file_job.rbi
+++ b/sorbet/rbi/dsl/domain/inkbunny/job/static_file_job.rbi
@@ -12,16 +12,5 @@ class Domain::Inkbunny::Job::StaticFileJob
class << self
sig { returns(ColorLogger) }
def logger; end
-
- sig do
- params(
- args: T::Hash[::Symbol, T.untyped],
- block: T.nilable(T.proc.params(job: Domain::Inkbunny::Job::StaticFileJob).void)
- ).returns(T.any(Domain::Inkbunny::Job::StaticFileJob, FalseClass))
- end
- def perform_later(args, &block); end
-
- sig { params(args: T::Hash[::Symbol, T.untyped]).returns(T.untyped) }
- def perform_now(args); end
end
end
diff --git a/sorbet/rbi/dsl/domain/inkbunny/job/user_avatar_job.rbi b/sorbet/rbi/dsl/domain/inkbunny/job/user_avatar_job.rbi
index 5646189c..6f4831d0 100644
--- a/sorbet/rbi/dsl/domain/inkbunny/job/user_avatar_job.rbi
+++ b/sorbet/rbi/dsl/domain/inkbunny/job/user_avatar_job.rbi
@@ -12,16 +12,5 @@ class Domain::Inkbunny::Job::UserAvatarJob
class << self
sig { returns(ColorLogger) }
def logger; end
-
- sig do
- params(
- args: T::Hash[::Symbol, T.untyped],
- block: T.nilable(T.proc.params(job: Domain::Inkbunny::Job::UserAvatarJob).void)
- ).returns(T.any(Domain::Inkbunny::Job::UserAvatarJob, FalseClass))
- end
- def perform_later(args, &block); end
-
- sig { params(args: T::Hash[::Symbol, T.untyped]).returns(T.untyped) }
- def perform_now(args); end
end
end
diff --git a/sorbet/rbi/dsl/domain/post/sofurry_post.rbi b/sorbet/rbi/dsl/domain/post/sofurry_post.rbi
index 65e13eb0..7012fa8d 100644
--- a/sorbet/rbi/dsl/domain/post/sofurry_post.rbi
+++ b/sorbet/rbi/dsl/domain/post/sofurry_post.rbi
@@ -544,6 +544,9 @@ class Domain::Post::SofurryPost
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile) }
def build_file(*args, &blk); end
+ sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
+ def build_last_gallery_log_entry(*args, &blk); end
+
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_last_scan_log_entry(*args, &blk); end
@@ -565,6 +568,12 @@ class Domain::Post::SofurryPost
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile) }
def create_file!(*args, &blk); end
+ sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
+ def create_last_gallery_log_entry(*args, &blk); end
+
+ sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
+ def create_last_gallery_log_entry!(*args, &blk); end
+
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_last_scan_log_entry(*args, &blk); end
@@ -623,6 +632,18 @@ class Domain::Post::SofurryPost
sig { params(value: T::Enumerable[::Domain::PostGroup::SofurryFolder]).void }
def folders=(value); end
+ sig { returns(T.nilable(::HttpLogEntry)) }
+ def last_gallery_log_entry; end
+
+ sig { params(value: T.nilable(::HttpLogEntry)).void }
+ def last_gallery_log_entry=(value); end
+
+ sig { returns(T::Boolean) }
+ def last_gallery_log_entry_changed?; end
+
+ sig { returns(T::Boolean) }
+ def last_gallery_log_entry_previously_changed?; end
+
sig { returns(T.nilable(::HttpLogEntry)) }
def last_scan_log_entry; end
@@ -667,6 +688,9 @@ class Domain::Post::SofurryPost
sig { returns(T.nilable(::Domain::PostFile)) }
def reload_file; end
+ sig { returns(T.nilable(::HttpLogEntry)) }
+ def reload_last_gallery_log_entry; end
+
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_last_scan_log_entry; end
@@ -682,6 +706,9 @@ class Domain::Post::SofurryPost
sig { void }
def reset_file; end
+ sig { void }
+ def reset_last_gallery_log_entry; end
+
sig { void }
def reset_last_scan_log_entry; end
@@ -1304,6 +1331,51 @@ class Domain::Post::SofurryPost
sig { void }
def json_attributes_will_change!; end
+ sig { returns(T.nilable(::Integer)) }
+ def last_gallery_log_entry_id; end
+
+ sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
+ def last_gallery_log_entry_id=(value); end
+
+ sig { returns(T::Boolean) }
+ def last_gallery_log_entry_id?; end
+
+ sig { returns(T.nilable(::Integer)) }
+ def last_gallery_log_entry_id_before_last_save; end
+
+ sig { returns(T.untyped) }
+ def last_gallery_log_entry_id_before_type_cast; end
+
+ sig { returns(T::Boolean) }
+ def last_gallery_log_entry_id_came_from_user?; end
+
+ sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
+ def last_gallery_log_entry_id_change; end
+
+ sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
+ def last_gallery_log_entry_id_change_to_be_saved; end
+
+ sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
+ def last_gallery_log_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
+
+ sig { returns(T.nilable(::Integer)) }
+ def last_gallery_log_entry_id_in_database; end
+
+ sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
+ def last_gallery_log_entry_id_previous_change; end
+
+ sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
+ def last_gallery_log_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
+
+ sig { returns(T.nilable(::Integer)) }
+ def last_gallery_log_entry_id_previously_was; end
+
+ sig { returns(T.nilable(::Integer)) }
+ def last_gallery_log_entry_id_was; end
+
+ sig { void }
+ def last_gallery_log_entry_id_will_change!; end
+
sig { returns(T.nilable(::Integer)) }
def last_scan_log_entry_id; end
@@ -1529,6 +1601,9 @@ class Domain::Post::SofurryPost
sig { void }
def restore_json_attributes!; end
+ sig { void }
+ def restore_last_gallery_log_entry_id!; end
+
sig { void }
def restore_last_scan_log_entry_id!; end
@@ -1604,6 +1679,12 @@ class Domain::Post::SofurryPost
sig { returns(T::Boolean) }
def saved_change_to_json_attributes?; end
+ sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
+ def saved_change_to_last_gallery_log_entry_id; end
+
+ sig { returns(T::Boolean) }
+ def saved_change_to_last_gallery_log_entry_id?; end
+
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_last_scan_log_entry_id; end
@@ -2026,6 +2107,9 @@ class Domain::Post::SofurryPost
sig { returns(T::Boolean) }
def will_save_change_to_json_attributes?; end
+ sig { returns(T::Boolean) }
+ def will_save_change_to_last_gallery_log_entry_id?; end
+
sig { returns(T::Boolean) }
def will_save_change_to_last_scan_log_entry_id?; end
diff --git a/sorbet/rbi/dsl/domain/sofurry/job/static_file_job.rbi b/sorbet/rbi/dsl/domain/sofurry/job/static_file_job.rbi
new file mode 100644
index 00000000..d4cde7ef
--- /dev/null
+++ b/sorbet/rbi/dsl/domain/sofurry/job/static_file_job.rbi
@@ -0,0 +1,16 @@
+# typed: true
+
+# DO NOT EDIT MANUALLY
+# This is an autogenerated file for dynamic methods in `Domain::Sofurry::Job::StaticFileJob`.
+# Please instead update this file by running `bin/tapioca dsl Domain::Sofurry::Job::StaticFileJob`.
+
+
+class Domain::Sofurry::Job::StaticFileJob
+ sig { returns(ColorLogger) }
+ def logger; end
+
+ class << self
+ sig { returns(ColorLogger) }
+ def logger; end
+ end
+end
diff --git a/sorbet/rbi/dsl/domain/sofurry/job/user_avatar_job.rbi b/sorbet/rbi/dsl/domain/sofurry/job/user_avatar_job.rbi
new file mode 100644
index 00000000..6fd86b04
--- /dev/null
+++ b/sorbet/rbi/dsl/domain/sofurry/job/user_avatar_job.rbi
@@ -0,0 +1,16 @@
+# typed: true
+
+# DO NOT EDIT MANUALLY
+# This is an autogenerated file for dynamic methods in `Domain::Sofurry::Job::UserAvatarJob`.
+# Please instead update this file by running `bin/tapioca dsl Domain::Sofurry::Job::UserAvatarJob`.
+
+
+class Domain::Sofurry::Job::UserAvatarJob
+ sig { returns(ColorLogger) }
+ def logger; end
+
+ class << self
+ sig { returns(ColorLogger) }
+ def logger; end
+ end
+end
diff --git a/sorbet/rbi/dsl/domain/static_file_job.rbi b/sorbet/rbi/dsl/domain/static_file_job.rbi
new file mode 100644
index 00000000..ca948b68
--- /dev/null
+++ b/sorbet/rbi/dsl/domain/static_file_job.rbi
@@ -0,0 +1,27 @@
+# typed: true
+
+# DO NOT EDIT MANUALLY
+# This is an autogenerated file for dynamic methods in `Domain::StaticFileJob`.
+# Please instead update this file by running `bin/tapioca dsl Domain::StaticFileJob`.
+
+
+class Domain::StaticFileJob
+ sig { returns(ColorLogger) }
+ def logger; end
+
+ class << self
+ sig { returns(ColorLogger) }
+ def logger; end
+
+ sig do
+ params(
+ args: T::Hash[::Symbol, T.untyped],
+ block: T.nilable(T.proc.params(job: Domain::StaticFileJob).void)
+ ).returns(T.any(Domain::StaticFileJob, FalseClass))
+ end
+ def perform_later(args, &block); end
+
+ sig { params(args: T::Hash[::Symbol, T.untyped]).returns(T.untyped) }
+ def perform_now(args); end
+ end
+end
diff --git a/sorbet/rbi/dsl/domain/user_avatar_job.rbi b/sorbet/rbi/dsl/domain/user_avatar_job.rbi
new file mode 100644
index 00000000..c91a2eef
--- /dev/null
+++ b/sorbet/rbi/dsl/domain/user_avatar_job.rbi
@@ -0,0 +1,27 @@
+# typed: true
+
+# DO NOT EDIT MANUALLY
+# This is an autogenerated file for dynamic methods in `Domain::UserAvatarJob`.
+# Please instead update this file by running `bin/tapioca dsl Domain::UserAvatarJob`.
+
+
+class Domain::UserAvatarJob
+ sig { returns(ColorLogger) }
+ def logger; end
+
+ class << self
+ sig { returns(ColorLogger) }
+ def logger; end
+
+ sig do
+ params(
+ args: T::Hash[::Symbol, T.untyped],
+ block: T.nilable(T.proc.params(job: Domain::UserAvatarJob).void)
+ ).returns(T.any(Domain::UserAvatarJob, FalseClass))
+ end
+ def perform_later(args, &block); end
+
+ sig { params(args: T::Hash[::Symbol, T.untyped]).returns(T.untyped) }
+ def perform_now(args); end
+ end
+end
diff --git a/spec/jobs/domain/sofurry/job/scan_gallery_job_spec.rb b/spec/jobs/domain/sofurry/job/scan_gallery_job_spec.rb
index 0716ca03..b282cc4a 100644
--- a/spec/jobs/domain/sofurry/job/scan_gallery_job_spec.rb
+++ b/spec/jobs/domain/sofurry/job/scan_gallery_job_spec.rb
@@ -28,6 +28,9 @@ describe Domain::Sofurry::Job::ScanGalleryJob do
story_post = Domain::Post::SofurryPost.find_by(sofurry_id: 2_219_563)
expect(story_post.title).to eq("Chapter 1: Beyond Reach")
expect(story_post.media_type).to eq("stories")
+ expect(story_post.posted_at).to be_within(10.seconds).of(
+ Time.parse("2025-01-26 04:29:59 -0800"),
+ )
expect(story_post.folders.count).to eq(1)
expect(story_post.tags_array).to eq(
[
diff --git a/spec/lib/domain/sofurry/gallery_page_parser_spec.rb b/spec/lib/domain/sofurry/gallery_page_parser_spec.rb
index db6add8e..2223b633 100644
--- a/spec/lib/domain/sofurry/gallery_page_parser_spec.rb
+++ b/spec/lib/domain/sofurry/gallery_page_parser_spec.rb
@@ -41,6 +41,17 @@ describe Domain::Sofurry::GalleryPageParser do
expect(parser.username).to eq("Matkaja")
end
+ it "extracts posts from art page" do
+ parser =
+ get_parser("browse_user_gallery/101763.art_page_1.html", user_id: 101_763)
+ expect(parser.username).to eq("gewitter")
+ expect(parser.posts.length).to eq(30)
+ expect(parser.posts.first.id).to eq(2_265_979)
+ expect(parser.posts.first.title).to eq("Trying Some Spanking with Clara")
+ expect(parser.posts.last.id).to eq(2_220_943)
+ expect(parser.posts.last.title).to eq("Patreon Sketch YCH 68")
+ end
+
def get_parser(file, user_id:)
Domain::Sofurry::GalleryPageParser.new(
File.read(Rails.root.join("test/fixtures/files/domain/sofurry/#{file}")),
diff --git a/test/fixtures/files/domain/sofurry/browse_user_gallery/101763.art_page_1.html b/test/fixtures/files/domain/sofurry/browse_user_gallery/101763.art_page_1.html
new file mode 100644
index 00000000..1adefae1
--- /dev/null
+++ b/test/fixtures/files/domain/sofurry/browse_user_gallery/101763.art_page_1.html
@@ -0,0 +1,629 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
SoFurry - Art User
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
226597922641802262422226121922604542259316225748422574832255408225435822535722247561224687922459452244529224337622417272239595223793122358712234280223302822311572229394222767122263112224864222298622209452220943
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+