more migrating views over to new unified schema

This commit is contained in:
Dylan Knutson
2025-02-07 04:55:46 +00:00
parent 9a462713b6
commit 1761c89dc5
75 changed files with 5555 additions and 430 deletions

View File

@@ -94,11 +94,14 @@ task :reverse_csv do
end
task migrate_domain: :environment do
# Domain::MigrateToDomain.new.migrate_e621_users
# Domain::MigrateToDomain.new.migrate_e621_posts
# Domain::MigrateToDomain.new.migrate_fa_users
Domain::MigrateToDomain.new.migrate_e621_users
Domain::MigrateToDomain.new.migrate_e621_posts
Domain::MigrateToDomain.new.migrate_fa_users
Domain::MigrateToDomain.new.migrate_fa_posts
# Domain::MigrateToDomain.new.migrate_e621_users_favs
# Domain::MigrateToDomain.new.migrate_fa_users_favs
# Domain::MigrateToDomain.new.migrate_fa_users_followers
Domain::MigrateToDomain.new.migrate_e621_users_favs
Domain::MigrateToDomain.new.migrate_fa_users_favs
Domain::MigrateToDomain.new.migrate_fa_users_followed_users
Domain::MigrateToDomain.new.migrate_inkbunny_users
Domain::MigrateToDomain.new.migrate_inkbunny_posts
Domain::MigrateToDomain.new.migrate_inkbunny_pools
end

View File

@@ -0,0 +1,19 @@
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<!-- Background circle -->
<circle cx="8" cy="8" r="7" fill="#E0E0E0" />
<!-- Stylized "www" text -->
<path
d="M4 8.5C4 6.5 5 5.5 6 5.5C7 5.5 8 6.5 8 8.5C8 6.5 9 5.5 10 5.5C11 5.5 12 6.5 12 8.5"
stroke="#666666"
stroke-width="1.5"
stroke-linecap="round"
fill="none"
/>
</svg>

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -45,12 +45,12 @@ class Domain::Fa::PostsController < ApplicationController
post = Domain::Fa::Post.find_by(fa_id: fa_id)
enqueued = try_enqueue_post_scan(post, fa_id)
if post && (file = post.file).present?
if post && (file = post.file).present? && created_at = file.created_at
state_string =
"downloaded #{helpers.time_ago_in_words(file.created_at, include_seconds: true)} ago"
elsif post && post.scanned?
"downloaded #{helpers.time_ago_in_words(created_at, include_seconds: true)} ago"
elsif post && post.scanned? && scanned_at = post.scanned_at
state_string =
"scanned #{helpers.time_ago_in_words(post.scanned_at, include_seconds: true)} ago"
"scanned #{helpers.time_ago_in_words(scanned_at, include_seconds: true)} ago"
else
state_string = []
!post ? state_string << "not seen" : state_string << "#{post.state}"

View File

@@ -0,0 +1,44 @@
# typed: true
class Domain::PostsController < ApplicationController
before_action :set_post, only: %i[show]
# GET /domain/posts/:id
def show
authorize @post
end
def index
authorize Domain::Post
@posts =
policy_scope(Domain::Post)
.order(created_at: :desc)
.page(params[:page])
.per(50)
.without_count
active_sources = (params[:sources] || SourceHelper.all_source_names)
unless SourceHelper.has_all_sources?(active_sources)
postable_types = SourceHelper.source_names_to_class_names(active_sources)
@posts = @posts.where(type: postable_types) if postable_types.any?
end
end
private
def set_post
type, pk = params[:id].split("/")
if type.nil? || pk.nil?
# return 404
render status: :not_found
end
@post =
case type
when "fa"
Domain::Post::FaPost.find_by(fa_id: pk)
when "e621"
Domain::Post::E621Post.find_by(e621_id: pk)
when "ib"
Domain::Post::InkbunnyPost.find_by(ib_post_id: pk)
end || raise(ActiveRecord::RecordNotFound)
end
end

View File

@@ -0,0 +1,29 @@
# typed: true
class Domain::UsersController < ApplicationController
before_action :set_user, only: %i[show]
skip_before_action :authenticate_user!, only: %i[show]
def show
authorize @user
end
private
def set_user
type, pk = params[:id].split("/")
if type.nil? || pk.nil?
# return 404
render status: :not_found
end
@user =
case type
when "fa"
Domain::User::FaUser.find_by(url_name: pk)
when "e621"
Domain::User::E621User.find_by(e621_id: pk)
when "ib"
Domain::User::InkbunnyUser.find_by(name: pk)
end || raise(ActiveRecord::RecordNotFound)
end
end

View File

@@ -0,0 +1,62 @@
# typed: strict
module Domain::PostsHelper
extend T::Sig
extend T::Helpers
include HelpersInterface
abstract!
class DomainData < T::Struct
const :domain_icon_path, String
const :domain_icon_title, String
end
DEFAULT_DOMAIN_DATA =
DomainData.new(
domain_icon_path: "generic-domain.svg",
domain_icon_title: "Unknown",
)
DOMAIN_DATA =
T.let(
{
Domain::Post::FaPost =>
DomainData.new(
domain_icon_path: "domain-icons/fa.png",
domain_icon_title: "Furaffinity",
),
Domain::Post::E621Post =>
DomainData.new(
domain_icon_path: "domain-icons/e621.png",
domain_icon_title: "E621",
),
Domain::Post::InkbunnyPost =>
DomainData.new(
domain_icon_path: "domain-icons/inkbunny.png",
domain_icon_title: "Inkbunny",
),
},
T::Hash[T.class_of(Domain::Post), DomainData],
)
sig { params(post: Domain::Post).returns(String) }
def domain_post_domain_icon_path(post)
post_class = post.class
path =
if (domain_data = DOMAIN_DATA[post_class])
domain_data.domain_icon_path
else
DEFAULT_DOMAIN_DATA.domain_icon_path
end
asset_path(path)
end
sig { params(post: Domain::Post).returns(String) }
def domain_post_domain_icon_title(post)
post_class = post.class
if (domain_data = DOMAIN_DATA[post_class])
domain_data.domain_icon_title
else
DEFAULT_DOMAIN_DATA.domain_icon_title
end
end
end

View File

@@ -0,0 +1,161 @@
# typed: strict
module Domain::UsersHelper
extend T::Sig
extend T::Helpers
include HelpersInterface
abstract!
sig do
params(
avatar: T.nilable(Domain::UserAvatar),
thumb: T.nilable(String),
).returns(String)
end
def domain_user_avatar_img_src_path(avatar, thumb: nil)
if (sha256 = avatar&.log_entry&.response_sha256)
blob_path(HexUtil.bin2hex(sha256), format: "jpg", thumb: thumb)
else
# default / 'not found' avatar image
# "/blobs/9080fd4e7e23920eb2dccfe2d86903fc3e748eebb2e5aa8c657bbf6f3d941cdc/contents.jpg"
asset_path("user-circle.svg")
end
end
sig { params(user: Domain::User).returns(String) }
def site_name_for_user(user)
case user
when Domain::User::E621User
"E621"
when Domain::User::FaUser
"Furaffinity"
when Domain::User::InkbunnyUser
"Inkbunny"
else
Kernel.raise "Unknown user type: #{user.class}"
end
end
sig { params(user: Domain::User).returns(String) }
def site_icon_path_for_user(user)
case user
when Domain::User::E621User
asset_path("domain-icons/e621.png")
when Domain::User::FaUser
asset_path("domain-icons/fa.png")
when Domain::User::InkbunnyUser
asset_path("domain-icons/ib.png")
else
Kernel.raise "Unknown user type: #{user.class}"
end
end
sig { params(html: String).returns(String) }
def sanitized_user_profile_html(html)
# try to preload all the FA usernames in the profile
maybe_url_names =
Nokogiri
.HTML(html)
.css("a")
.flat_map do |node|
node = T.cast(node, Nokogiri::XML::Element)
href = T.cast(Addressable::URI.parse(node["href"]), Addressable::URI)
right_host = href.host.nil? || href.host == "www.furaffinity.net"
right_path = href.path =~ %r{/user/.+}
if right_host && right_path
[href]
else
[]
end
end
.map do |href|
T.cast(href.path.split("/")[2]&.downcase, T.nilable(String))
end
preloaded_users =
T.cast(
Domain::User::FaUser
.where(url_name: maybe_url_names)
.includes(:avatar)
.index_by(&:url_name),
T::Hash[String, Domain::User::FaUser],
)
raw Sanitize.fragment(
html,
elements: %w[br img b i span strong],
attributes: {
"span" => %w[style],
"a" => [],
},
css: {
properties: %w[font-size color],
},
transformers:
Kernel.lambda do |env|
return unless env[:node_name] == "a"
node = T.cast(env[:node], Nokogiri::XML::Element)
href = URI.parse(node["href"])
unless href.host == nil || href.host == "www.furaffinity.net"
return
end
return unless href.path =~ %r{/user/.+}
url_name = href.path&.split("/")&.[](2)&.downcase
return unless url_name.present?
Sanitize.node!(
node,
{ elements: %w[a], attributes: { "a" => %w[href] } },
)
node["href"] = domain_user_path("fa/#{url_name}")
node["class"] = "text-slate-200 underline decoration-slate-200 " +
"decoration-dashed decoration-dashed decoration-1"
whitelist = [node]
user =
preloaded_users[url_name] ||
Domain::User::FaUser.find_by(url_name: url_name)
if user && (avatar = user.avatar)
img = Nokogiri::XML::Node.new("img", node.document)
img["class"] = "inline w-5"
img["src"] = domain_user_avatar_img_src_path(
avatar,
thumb: "32-avatar",
)
node.prepend_child(img)
whitelist << img
end
{ node_allowlist: whitelist }
end,
)
end
sig { params(user: Domain::User).returns(T::Array[[String, Integer]]) }
def stat_rows_for_user(user)
rows = []
rows << ["Favorites", user.user_post_favs.count] if user.has_faved_posts?
if user.has_followed_users?
rows << ["Following", user.user_user_follows_from.count]
end
if user.has_followed_by_users?
rows << ["Followed by", user.user_user_follows_to.count]
end
can_view_timestamps = policy(user).view_page_scanned_at_timestamps?
if user.is_a?(Domain::User::FaUser) && can_view_timestamps
rows << ["Favorites", time_ago_or_never(user.scanned_gallery_at)]
rows << ["Gallery scanned", time_ago_or_never(user.scanned_gallery_at)]
rows << ["Page scanned", time_ago_or_never(user.scanned_page_at)]
elsif user.is_a?(Domain::User::E621User) && can_view_timestamps
if user.favs_are_hidden
rows << ["Favorites hidden", "yes"]
else
rows << ["Server favorites", user.num_other_favs_cached]
end
rows << ["Favorites scanned", time_ago_or_never(user.scanned_favs_at)]
end
rows
end
end

View File

@@ -0,0 +1,48 @@
# typed: strict
module HelpersInterface
extend T::Sig
extend T::Helpers
abstract!
sig do
params(timestamp: T.nilable(ActiveSupport::TimeWithZone)).returns(String)
end
def time_ago_or_never(timestamp)
timestamp ? time_ago_in_words(timestamp) + " ago" : "never"
end
sig do
abstract
.params(sha256: String, format: String, thumb: T.nilable(String))
.returns(String)
end
def blob_path(sha256, format:, thumb: nil)
end
sig { abstract.params(path: String).returns(String) }
def asset_path(path)
end
sig { abstract.params(value: String).returns(String) }
def raw(value)
end
sig { abstract.params(id: String).returns(String) }
def domain_user_path(id)
end
sig do
abstract
.params(
timestamp: ActiveSupport::TimeWithZone,
include_seconds: T::Boolean,
)
.returns(String)
end
def time_ago_in_words(timestamp, include_seconds: false)
end
sig { abstract.params(user: Domain::User).returns(Domain::UserPolicy) }
def policy(user)
end
end

View File

@@ -2,116 +2,79 @@
class Domain::Fa::Job::ScanFileJob < Domain::Fa::Job::Base
queue_as :static_file
sig { override.params(args: T::Hash[Symbol, T.untyped]).void }
def initialize(*args)
super(*T.unsafe(args))
@file = T.let(nil, T.nilable(Domain::PostFile))
@post = T.let(nil, T.nilable(T.any(Domain::Fa::Post, Domain::Post::FaPost)))
end
sig { override.params(args: T::Hash[Symbol, T.untyped]).void }
def perform(args)
@file = args[:post_file]
@post = args[:post]
@force_scan = !!args[:force_scan]
file =
T.cast(args[:post_file], T.nilable(Domain::PostFile)) ||
begin
post = args[:post]
if post.nil?
if args[:fa_id]
logger.error "no post model - fa_id: #{args[:fa_id]}, enqueue scan"
defer_job(Domain::Fa::Job::ScanPostJob, { fa_id: args[:fa_id] })
else
fatal_error("no post model or fa_id")
end
return
end
post =
if post.is_a?(Domain::Fa::Post)
Domain::Post::FaPost.find_by!(fa_id: post.fa_id)
elsif post.is_a?(Domain::Post::FaPost)
post
else
fatal_error(
"invalid post model: #{post.class}, expected Domain::Fa::Post or Domain::Post::FaPost",
)
raise
end
post.file
end
if @post.nil?
logger.error "no post model - fa_id: #{args[:fa_id]}, enqueue scan"
if args[:fa_id]
defer_job(Domain::Fa::Job::ScanPostJob, { fa_id: args[:fa_id] })
end
file = T.must(file)
logger.info "scanning file: #{file.id}, state=#{file.state}, retry_count=#{file.retry_count}"
if file.state == "terminal_error" && !@force_scan
logger.warn("state == terminal_error, abort without retrying")
return
end
post =
if @post.is_a?(Domain::Fa::Post)
Domain::Post::FaPost.find_by!(fa_id: @post.fa_id)
elsif @post.is_a?(Domain::Post::FaPost)
@post
else
fatal_error("invalid post model: #{@post.class}")
raise
end
file = post.file
if file.nil?
logger.error "no file model - fa_id: #{post.fa_id}, enqueue scan"
defer_job(Domain::Fa::Job::ScanPostJob, { post: post })
return
end
logger.prefix = "[fa_id #{post.fa_id.to_s.bold} / #{post.state&.bold}]"
if file.state == "error" && file.url_str.nil?
logger.error "removed and has no file, skipping"
if file.state == "retryable_error" && file.retry_count >= 3
logger.warn("retry_count >= 3, abort without retrying")
return
end
if !post.scanned_at.present?
logger.error "has not been scanned yet, doing so first"
enqueue_post_scan(post)
if file.state == "ok" && !@force_scan
logger.warn("already have file, skipping")
return
end
if file.log_entry.present?
logger.warn("already have file")
return
end
file_url_str = T.must(file.url_str)
file_uri = Addressable::URI.parse(file_url_str)
file_uri_host = file_uri&.host
if file_uri_host
is_unresolvable_host = false
is_unresolvable_host ||= file_uri_host == "d9.facdn.net"
uri_tld = file_uri_host.split(".").last
is_unresolvable_host ||=
uri_tld.length >= 6 && file_uri_host.start_with?("d.facdn.net")
if is_unresolvable_host
logger.error("host is #{file_uri_host}, which will not resolve")
post.state = "error"
post.scan_file_error = "unresolvable host"
post.save!
return
end
end
if file.state == "error" && !@force_scan
logger.warn("state == error, skipping")
return
file_url_str = file.url_str
if file_url_str.nil?
fatal_error("file has no url")
raise
end
response = http_client.get(file_url_str)
if response.status_code == 404
post.state = "error"
post.scan_file_error = "404"
post.save!
logger.error "404, aborting"
return
end
if response.status_code != 200
defer_job(
Domain::Fa::Job::ScanPostJob,
{ post: post, caused_by_entry: response.log_entry, force_scan: true },
)
err_msg =
"error downloading - log entry #{response.log_entry.id} / status code #{response.status_code}"
post.save!
if response.status_code == 404 && post.state == "removed"
logger.error(err_msg)
return
else
fatal_error(err_msg)
end
end
logger.debug "#{HexUtil.humansize(T.must(response.log_entry.response&.size))} / #{response.log_entry.content_type} / #{response.log_entry.response_time_ms} ms"
file.state = "ok"
file.log_entry = response.log_entry
file.save!
file.last_status_code = response.status_code
if response.status_code == 200
file.state = "ok"
file.retry_count = 0
logger.info "response is 200, ok"
elsif response.status_code == 404
file.state = "terminal_error"
logger.error "response is 404, abort without retrying"
else
file.state = "retryable_error"
file.retry_count += 1
logger.warn "response is #{response.status_code}, retrying later"
# job runner will retry this job as it threw an exception
fatal_error("response #{response.status_code}, aborting")
end
ensure
file.save! if file
end
end

View File

@@ -21,10 +21,8 @@ class Domain::Fa::Job::ScanPostJob < Domain::Fa::Job::Base
end
file = post.file
if (
file.present? && file.state == "ok" && file.url_str.present? &&
!file.log_entry.present?
) || force_scan?
if (file.present? && file.state == "pending" && file.url_str.present?) ||
force_scan?
logger.info("enqueue file job (#{self.priority})")
defer_job(
Domain::Fa::Job::ScanFileJob,

View File

@@ -31,7 +31,7 @@ class DbSampler
sig { void.params(file: T.untyped) }
def initialize(file)
@file = T.let(file, StringIO)
@file = T.let(file, T.any(StringIO, IO))
@handled = T.let(Set.new, T::Set[ReduxApplicationRecord])
end
@@ -80,6 +80,7 @@ class DbSampler
deferred.each do |model|
import_model(model)
rescue StandardError
$stderr.puts("error importing deferred #{model_id(model)}, skipping")
end
end
end
@@ -105,14 +106,18 @@ class DbSampler
$stderr.puts("skipped existing #{model_id(model)}")
else
model2 = model.class.new
model
model2
.attribute_names
.map(&:to_sym)
.each do |attr|
model2.write_attribute(attr, model.read_attribute(attr))
end
model2.save(validate: false)
$stderr.puts("imported #{model_id(model)}")
begin
model2.save(validate: false)
$stderr.puts("imported #{model_id(model)}")
rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
$stderr.puts("skipped #{model_id(model)}")
end
end
end

View File

@@ -46,12 +46,23 @@ class Domain::MigrateToDomain
format: "%t: %c/%C %B %p%% %a %e",
output: @pb_sink,
)
query.find_in_batches(batch_size: 10_000) do |batch|
migrate_batch(Domain::Post::E621Post, batch) do |old_model|
initialize_e621_post_from(old_model)
query
.includes(:file)
.find_in_batches(batch_size: 10_000) do |batch|
ReduxApplicationRecord.transaction do
models =
migrate_batch(Domain::Post::E621Post, batch) do |old_model|
initialize_e621_post_from(old_model)
end
migrate_batch(Domain::PostFile, models.filter(&:file)) do |post|
file = T.must(post.file)
file.post_id = T.must(post.id)
file
end
end
pb.progress = [pb.progress + batch.size, pb.total].min
end
pb.progress = [pb.progress + batch.size, pb.total].min
end
end
sig { void }
@@ -72,15 +83,17 @@ class Domain::MigrateToDomain
output: @pb_sink,
)
query.find_in_batches(batch_size: 10_000) do |batch|
models =
migrate_batch(Domain::User::FaUser, batch) do |old_user|
initialize_fa_user_from(old_user)
end
ReduxApplicationRecord.transaction do
models =
migrate_batch(Domain::User::FaUser, batch) do |old_user|
initialize_fa_user_from(old_user)
end
migrate_batch(Domain::UserAvatar, models.filter(&:avatar)) do |user|
avatar = T.must(user.avatar)
avatar.user_id = user.id
avatar
migrate_batch(Domain::UserAvatar, models.filter(&:avatar)) do |user|
avatar = T.must(user.avatar)
avatar.user_id = user.id
avatar
end
end
pb.progress = [pb.progress + batch.size, pb.total].min
@@ -106,7 +119,7 @@ class Domain::MigrateToDomain
output: @pb_sink,
)
missing_fa_ids.each_slice(10_000) do |fa_ids|
missing_fa_ids.each_slice(1_000) do |fa_ids|
batch =
Domain::Fa::Post.where(fa_id: fa_ids).includes(:creator, :file).to_a
ReduxApplicationRecord.transaction do
@@ -140,29 +153,54 @@ class Domain::MigrateToDomain
sig { void }
def migrate_e621_users_favs
logger.info "migrating e621 users favs"
Domain::User::E621User
.where(migrated_user_favs_at: nil)
.find_each { |user| migrate_e621_user_favs(user) }
query = Domain::User::E621User.where(migrated_user_favs_at: nil)
pb =
ProgressBar.create(
throttle_rate: 0.2,
total: query.count,
format: "%t: %c/%C %B %p%% %a %e",
output: @pb_sink,
)
query.find_each do |user|
ReduxApplicationRecord.transaction { migrate_e621_user_favs(user) }
pb.progress = [pb.progress + 1, pb.total].min
end
end
sig { void }
def migrate_fa_users_favs
logger.info "migrating fa users favs"
Domain::User::FaUser
.where(migrated_user_favs_at: nil)
.find_each { |user| migrate_fa_user_favs(user) }
query = Domain::User::FaUser.where(migrated_user_favs_at: nil)
pb =
ProgressBar.create(
throttle_rate: 0.2,
total: query.count,
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
end
end
sig { void }
def migrate_fa_users_followed_users
logger.info "migrating fa followed users"
Domain::User::FaUser
.where(migrated_followed_users_at: nil)
.find_each do |user|
ReduxApplicationRecord.transaction do
migrate_fa_user_followed_users(user)
end
query = Domain::User::FaUser.where(migrated_followed_users_at: nil)
pb =
ProgressBar.create(
throttle_rate: 0.2,
total: query.count,
format: "%t: %c/%C %B %p%% %a %e",
output: @pb_sink,
)
query.find_each do |user|
ReduxApplicationRecord.transaction do
migrate_fa_user_followed_users(user)
end
pb.progress = [pb.progress + 1, pb.total].min
end
end
sig { void }
@@ -203,7 +241,7 @@ class Domain::MigrateToDomain
query =
Domain::Inkbunny::Post
.where.not(ib_post_id: Domain::Post::InkbunnyPost.select(:ib_id))
.includes(:creator, :files, :pools)
.includes(:creator, { files: :log_entry }, :pools)
pb =
ProgressBar.create(
@@ -233,9 +271,10 @@ class Domain::MigrateToDomain
model.files.each { |file| file.post_id = model.id }
end
migrate_batch(Domain::PostFile, models.flat_map(&:files)) do |file|
file
end
migrate_batch(
Domain::PostFile::InkbunnyPostFile,
models.flat_map(&:files),
) { |file| file }
end
pb.progress = [pb.progress + batch.size, pb.total].min
end
@@ -323,6 +362,17 @@ class Domain::MigrateToDomain
new_post.rating = old_post.rating
new_post.submission_type = old_post.submission_type
new_post.created_at = old_post.created_at
new_post.posted_at = old_post.posted_at
new_post.title = old_post.title
new_post.writing = old_post.writing
new_post.num_views = old_post.num_views
new_post.num_files = old_post.num_files
new_post.num_favs = old_post.num_favs
new_post.num_comments = old_post.num_comments
new_post.keywords = old_post.keywords
new_post.last_file_updated_at = old_post.last_file_updated_at
new_post.deep_update_log_entry_id = old_post.deep_update_log_entry_id
new_post.shallow_update_log_entry_id = old_post.shallow_update_log_entry_id
if old_creator = old_post.creator
new_post.creator =
@@ -331,10 +381,26 @@ class Domain::MigrateToDomain
new_post.files =
old_post.files.map do |old_file|
new_file = Domain::PostFile.new
new_state =
case old_file.state
when "ok"
old_file.log_entry_id.present? ? "ok" : "pending"
else
"terminal_error"
end
new_file = Domain::PostFile::InkbunnyPostFile.new
new_file.ib_id = old_file.ib_file_id
new_file.log_entry_id = old_file.log_entry_id
new_file.last_status_code = old_file.log_entry&.status_code
new_file.url_str = old_file.url_str
new_file.state = old_file.state
new_file.state = new_state
new_file.file_name = old_file.file_name
new_file.md5_initial = old_file.md5_initial
new_file.md5_full = old_file.md5_full
new_file.md5s = old_file.md5s
new_file.file_order = old_file.file_order
new_file.ib_detail_raw = old_file.ib_detail_raw
new_file.ib_created_at = old_file.ib_created_at
new_file
end
@@ -406,6 +472,7 @@ class Domain::MigrateToDomain
new_post.sources_array = old_post.sources_array
new_post.artists_array = old_post.artists_array
new_post.e621_updated_at = old_post.e621_updated_at
new_post.posted_at = old_post.posted_at
new_post.last_index_page_id = old_post.last_index_page_id
new_post.caused_by_entry_id = old_post.caused_by_entry_id
new_post.scan_log_entry_id = old_post.scan_log_entry_id
@@ -415,13 +482,43 @@ class Domain::MigrateToDomain
new_post.file_error = old_post.file_error
new_post.created_at = old_post.created_at
new_post.parent_post_e621_id = old_post.parent_e621_id
old_file = old_post.file
file_url_str = old_post.file_url_str
if old_file || file_url_str
new_file = Domain::PostFile.new
new_file.url_str = file_url_str
new_file.log_entry = old_file
if old_file.present? && old_file.status_code == 200
new_file.state = "ok"
elsif old_file.present?
new_file.state = "terminal_error"
new_file.error_message = "status_code: #{old_file.status_code}"
else
new_file.state = "pending"
end
new_post.file = new_file
end
new_post
end
sig { params(old_user: Domain::Fa::User).returns(Domain::User::FaUser) }
def initialize_fa_user_from(old_user)
new_user = Domain::User::FaUser.new
new_user.state = old_user.state
new_user.state =
case old_user.state
when "ok"
"ok"
when "scan_error"
if /disabled or not found/ =~ old_user.state_detail["scan_error"]
"account_disabled"
else
"error"
end
else
raise("unknown fa user state: #{old_user.state}")
end
new_user.url_name = old_user.url_name
new_user.name = old_user.name
new_user.full_name = old_user.full_name
@@ -492,10 +589,10 @@ class Domain::MigrateToDomain
Domain::User::FaUser.find_by!(url_name: post.creator&.url_name)
end
if post.file.present?
if post.file.present? || post.file_uri.present?
new_file = Domain::PostFile.new
new_file.log_entry_id = post.file_id
new_file.url_str = post.file_url_str
new_file.url_str = post.file_uri.to_s
new_file.state = post.state
new_post.file = new_file
end
@@ -548,9 +645,6 @@ class Domain::MigrateToDomain
else
user.migrated_followed_users_at = Time.current
user.save!
logger.info(
"migrated fa user followers #{user.name} (#{new_user_ids.size})",
)
end
end
@@ -576,9 +670,6 @@ class Domain::MigrateToDomain
else
new_user.migrated_user_favs_at = Time.current
new_user.save!
logger.info(
"migrated e621 user favs #{new_user.name} (#{new_post_ids.size})",
)
end
end

View File

@@ -88,13 +88,9 @@ module AttrJsonRecordAliases
included { include AttrJson::Record }
requires_ancestor { ActiveRecord::Base }
sig { abstract.returns(T.class_of(ActiveRecord::Base)) }
def class
end
sig { params(attributes: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
def update_json_columns(attributes)
klass = self.class
klass = T.cast(self.class, T.class_of(ActiveRecord::Base))
updated_json = klass.connection.quote(JSON.dump(attributes))
klass.connection.execute(<<~SQL)
UPDATE #{klass.quoted_table_name}
@@ -105,8 +101,9 @@ module AttrJsonRecordAliases
sig { params(name: T.untyped, value: T.untyped).returns(T.untyped) }
def write_attribute(name, value)
klass = T.cast(self.class, T.class_of(ActiveRecord::Base))
ret = super(name, value)
if attribute_def = ImplHelper.get_json_attr_def(self.class, name)
if attribute_def = ImplHelper.get_json_attr_def(klass, name)
public_send(attribute_def.container_attribute)[
attribute_def.store_key
] = read_attribute(name)
@@ -154,52 +151,6 @@ module AttrJsonRecordAliases
""
end
end
# sig do
# params(
# attr_name: Symbol,
# type: T.any(Symbol, ActiveModel::Type::Value),
# ).returns(String)
# end
# def json_attribute_expression(attr_name, type)
# adapter_class = T.unsafe(self).adapter_class
# "#{adapter_class.quote_table_name(self.table_name)}.#{adapter_class.quote_column_name("json_attributes")}->>'#{attr_name}'"
# end
# sig do
# params(
# attr_name: Symbol,
# type: T.any(Symbol, ActiveModel::Type::Value),
# ).void
# end
# def json_attributes_scope(attr_name, type)
# attribute_expression = json_attribute_expression(attr_name, type)
# db_type = json_attribute_type_cast(type)
# scope :"where_#{attr_name}",
# ->(expr, *binds) do
# where("((#{attribute_expression})#{db_type}) #{expr}", binds)
# end
# scope :"order_#{attr_name}",
# ->(dir) do
# unless [:asc, :desc, nil].include?(dir)
# raise("invalid direction: #{dir}")
# end
# order(Arel.sql("#{attribute_expression} #{dir}"))
# end
# end
# sig do
# params(
# attr_name: Symbol,
# type: T.any(Symbol, ActiveModel::Type::Value),
# options: T.untyped,
# ).void
# end
# def attr_json_scoped(attr_name, type, **options)
# T.unsafe(self).attr_json(attr_name, type, **options)
# # json_attributes_scope(attr_name, type)
# end
end
mixes_in_class_methods(ClassMethods)

View File

@@ -44,7 +44,10 @@ class Domain::E621::Post < ReduxApplicationRecord
foreign_key: :e621_id,
optional: true
has_many :favs, class_name: "Domain::E621::Fav", inverse_of: :post
has_many :favs,
class_name: "Domain::E621::Fav",
inverse_of: :post,
dependent: :destroy
has_many :faving_users,
class_name: "Domain::E621::User",
through: :favs,

View File

@@ -5,6 +5,19 @@ class Domain::Post < ReduxApplicationRecord
self.table_name = "domain_posts"
abstract!
class_attribute :class_has_creators, :class_belongs_to_groups
sig { returns(T::Boolean) }
def self.has_creators?
class_has_creators
end
sig { returns(T::Boolean) }
def self.belongs_to_groups?
class_belongs_to_groups
end
# so sorbet knows this is a string
sig { returns(String) }
def type
super
@@ -19,24 +32,29 @@ class Domain::Post < ReduxApplicationRecord
def to_param
end
sig { void }
def self.has_single_file!
# single file association
has_one :file,
class_name: "::Domain::PostFile",
inverse_of: :post,
dependent: :destroy
end
attr_json :posted_at, :datetime
# multiple files association
has_many :files,
class_name: "::Domain::PostFile",
dependent: :destroy,
inverse_of: :post,
dependent: :destroy
foreign_key: :post_id
sig { params(klass: T.class_of(Domain::PostFile)).void }
def self.has_single_file!(klass = Domain::PostFile)
has_one :file, class_name: klass.name, foreign_key: :post_id
end
sig { params(klass: T.class_of(Domain::PostFile)).void }
def self.has_multiple_files!(klass = Domain::PostFile)
has_many :files,
class_name: klass.name,
foreign_key: :post_id,
inverse_of: :post
end
sig { params(klass: T.class_of(Domain::User)).void }
def self.has_single_creator!(klass)
# single creator association
has_one :primary_user_post_creation,
class_name: "::Domain::UserPostCreation",
inverse_of: :post,
@@ -93,4 +111,34 @@ class Domain::Post < ReduxApplicationRecord
source: :group,
class_name: group_klass.name
end
sig { abstract.returns(T.nilable(String)) }
def title
end
sig { abstract.returns(T.nilable(T.any(String, Integer))) }
def domain_id_for_view
end
sig { abstract.returns(String) }
def domain_abbreviation_for_view
end
sig { abstract.returns(T.nilable(Addressable::URI)) }
def domain_url_for_view
end
sig { abstract.returns(T.nilable(Domain::PostFile)) }
def primary_file_for_view
end
sig { overridable.returns(T.nilable(Domain::User)) }
def primary_creator_for_view
nil
end
sig { overridable.returns(T.nilable(String)) }
def primary_creator_name_fallback_for_view
nil
end
end

View File

@@ -66,4 +66,36 @@ class Domain::Post::E621Post < Domain::Post
def to_param
"e621/#{self.e621_id}" if self.e621_id.present?
end
sig { override.returns(T.nilable(String)) }
def title
"E621 Post #{self.e621_id}"
end
sig { override.returns(T.nilable(String)) }
def primary_creator_name_fallback_for_view
self.tags_array["artist"].first || self.artists_array.first
end
sig { override.returns(T.nilable(T.any(String, Integer))) }
def domain_id_for_view
self.e621_id
end
sig { override.returns(String) }
def domain_abbreviation_for_view
"E621"
end
sig { override.returns(T.nilable(Addressable::URI)) }
def domain_url_for_view
if self.e621_id.present?
Addressable::URI.parse("https://e621.net/posts/#{self.e621_id}")
end
end
sig { override.returns(T.nilable(Domain::PostFile)) }
def primary_file_for_view
self.file
end
end

View File

@@ -14,7 +14,6 @@ class Domain::Post::FaPost < Domain::Post
attr_json :num_favorites, :integer
attr_json :num_comments, :integer
attr_json :num_views, :integer
attr_json :posted_at, :datetime
attr_json :scanned_at, :datetime
attr_json :scan_file_error, :string
attr_json :last_user_page_id, :integer
@@ -29,8 +28,8 @@ class Domain::Post::FaPost < Domain::Post
belongs_to :first_gallery_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_seen_entry, class_name: "::HttpLogEntry", optional: true
has_single_creator! Domain::User::FaUser
has_single_file!
has_single_creator! Domain::User::FaUser
has_faving_users! Domain::User::FaUser
after_initialize { self.state ||= "ok" }
@@ -43,6 +42,38 @@ class Domain::Post::FaPost < Domain::Post
"fa/#{self.fa_id}" if self.fa_id.present?
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(String) }
def domain_abbreviation_for_view
"FA"
end
sig { override.returns(T.nilable(Addressable::URI)) }
def domain_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 do
params(
submission: Domain::Fa::Parser::ListedSubmissionParserHelper,

View File

@@ -4,12 +4,29 @@ class Domain::Post::InkbunnyPost < Domain::Post
attr_json :state, :string
attr_json :rating, :string
attr_json :submission_type, :string
attr_json :title, :string
attr_json :writing, :string
attr_json :num_views, :integer
attr_json :num_files, :integer
attr_json :num_favs, :integer
attr_json :num_comments, :integer
attr_json :keywords, :string, array: true
attr_json :last_file_updated_at, :datetime
attr_json :deep_update_log_entry_id, :integer
attr_json :shallow_update_log_entry_id, :integer
has_multiple_files! Domain::PostFile::InkbunnyPostFile
has_single_creator! Domain::User::InkbunnyUser
has_faving_users! Domain::User::InkbunnyUser
belongs_to_groups! :pools,
Domain::PostGroup::InkbunnyPool,
Domain::PostGroupJoin::InkbunnyPoolJoin
belongs_to :deep_update_log_entry,
class_name: "::HttpLogEntry",
optional: true
belongs_to :shallow_update_log_entry,
class_name: "::HttpLogEntry",
optional: true
validates :ib_id, presence: true
validates :state, presence: true, inclusion: { in: %w[ok error] }
@@ -40,4 +57,36 @@ class Domain::Post::InkbunnyPost < Domain::Post
def to_param
"ib/#{self.ib_id}" if self.ib_id.present?
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.ib_id
end
sig { override.returns(String) }
def domain_abbreviation_for_view
"IB"
end
sig { override.returns(T.nilable(Addressable::URI)) }
def domain_url_for_view
if self.ib_id.present?
Addressable::URI.parse("https://inkbunny.net/post/#{self.ib_id}")
end
end
sig { override.returns(T.nilable(Domain::PostFile)) }
def primary_file_for_view
self.files.first
end
end

View File

@@ -3,14 +3,33 @@ class Domain::PostFile < ReduxApplicationRecord
include AttrJsonRecordAliases
self.table_name = "domain_post_files"
belongs_to :post, class_name: "::Domain::Post", inverse_of: :files
belongs_to :post, foreign_key: :post_id, class_name: "::Domain::Post"
belongs_to :log_entry, class_name: "::HttpLogEntry", optional: true
belongs_to :blob,
class_name: "::BlobEntry",
optional: true,
foreign_key: :blob_sha256
attr_json :state, :string
attr_json :url_str, :string
attr_json :error_message, :string
attr_json :last_status_code, :integer
attr_json :retry_count, :integer
validates :state, inclusion: { in: %w[ok error] }
validates :state,
inclusion: {
in: %w[pending ok retryable_error terminal_error],
}
after_initialize { self.state ||= "ok" }
after_initialize do
self.state ||= "pending" if new_record?
self.type ||= self.class.name if new_record?
end
before_save { self.blob_sha256 ||= self.log_entry&.response_sha256 }
sig { returns(Integer) }
def retry_count
super || 0
end
end

View File

@@ -0,0 +1,11 @@
# typed: strict
class Domain::PostFile::InkbunnyPostFile < Domain::PostFile
attr_json :ib_id, :integer
attr_json :ib_detail_raw, ActiveModel::Type::Value.new
attr_json :ib_created_at, :datetime
attr_json :file_name, :string
attr_json :md5_initial, :string
attr_json :md5_full, :string
attr_json :md5s, ActiveModel::Type::Value.new
attr_json :file_order, :integer
end

View File

@@ -5,6 +5,31 @@ class Domain::User < ReduxApplicationRecord
self.table_name = "domain_users"
abstract!
class_attribute :class_has_created_posts,
:class_has_faved_posts,
:class_has_followed_users,
:class_has_followed_by_users
sig(:final) { returns(T::Boolean) }
def has_created_posts?
class_has_created_posts.present?
end
sig(:final) { returns(T::Boolean) }
def has_faved_posts?
class_has_faved_posts.present?
end
sig(:final) { returns(T::Boolean) }
def has_followed_users?
class_has_followed_users.present?
end
sig(:final) { returns(T::Boolean) }
def has_followed_by_users?
class_has_followed_by_users.present?
end
sig { returns(String) }
def type
super
@@ -50,6 +75,7 @@ class Domain::User < ReduxApplicationRecord
sig { params(klass: T.class_of(Domain::Post)).void }
def self.has_created_posts!(klass)
self.class_has_created_posts = klass
has_many :posts,
through: :user_post_creations,
source: :post,
@@ -58,6 +84,7 @@ class Domain::User < ReduxApplicationRecord
sig { params(klass: T.class_of(Domain::Post)).void }
def self.has_faved_posts!(klass)
self.class_has_faved_posts = klass
has_many :faved_posts,
through: :user_post_favs,
source: :post,
@@ -66,6 +93,7 @@ class Domain::User < ReduxApplicationRecord
sig { params(klass: T.class_of(Domain::User)).void }
def self.has_followed_users!(klass)
self.class_has_followed_users = klass
has_many :followed_users,
through: :user_user_follows_from,
source: :to,
@@ -74,6 +102,7 @@ class Domain::User < ReduxApplicationRecord
sig { params(klass: T.class_of(Domain::User)).void }
def self.has_followed_by_users!(klass)
self.class_has_followed_by_users = klass
has_many :followed_by_users,
through: :user_user_follows_to,
source: :from,
@@ -89,4 +118,20 @@ class Domain::User < ReduxApplicationRecord
class_name: "::Domain::UserAvatar",
inverse_of: :user,
dependent: :destroy
sig { abstract.returns(String) }
def account_status_for_view
end
sig { abstract.returns(T.nilable(ActiveSupport::TimeWithZone)) }
def registered_at_for_view
end
sig { abstract.returns(T.nilable(String)) }
def external_profile_url_for_view
end
sig { abstract.returns(T.nilable(String)) }
def name_for_view
end
end

View File

@@ -6,6 +6,7 @@ class Domain::User::E621User < Domain::User
attr_json :num_other_favs_cached, :integer
attr_json :scanned_favs_status, :string
attr_json :scanned_favs_at, :datetime
attr_json :registered_at, :datetime
has_many :uploaded_posts,
class_name: "::Domain::Post::E621Post",
@@ -26,4 +27,24 @@ class Domain::User::E621User < Domain::User
def to_param
"e621/#{e621_id}" if e621_id.present?
end
sig { override.returns(String) }
def account_status_for_view
"ok"
end
sig { override.returns(T.nilable(ActiveSupport::TimeWithZone)) }
def registered_at_for_view
registered_at
end
sig { override.returns(T.nilable(String)) }
def external_profile_url_for_view
"https://e621.net/users/#{e621_id}" if e621_id.present?
end
sig { override.returns(T.nilable(String)) }
def name_for_view
name
end
end

View File

@@ -4,6 +4,7 @@ class Domain::User::FaUser < Domain::User
attr_json :name, :string
attr_json :url_name, :string
attr_json :full_name, :string
attr_json :account_status, :string
attr_json :artist_type, :string
attr_json :mood, :string
attr_json :profile_html, :string
@@ -42,7 +43,17 @@ class Domain::User::FaUser < Domain::User
validates :name, presence: true
validates :url_name, presence: true
validates :state, presence: true, inclusion: { in: %w[ok error] }
validates :state,
presence: true,
inclusion: {
in: %w[ok account_disabled error],
}
validates :account_status,
inclusion: {
in: %w[ok account_disabled error],
allow_nil: true,
}
after_initialize { self.state ||= "ok" if new_record? }
@@ -91,6 +102,35 @@ class Domain::User::FaUser < Domain::User
end
end
# TODO - write a test for this
sig { override.returns(String) }
def account_status_for_view
account_status ||
begin
if (hle = last_user_page_log_entry) && (response = hle.response) &&
(contents = response.contents)
parser =
Domain::Fa::Parser::Page.new(contents, require_logged_in: false)
parser.user_page.account_status if parser.probably_user_page?
end
end || "unknown"
end
sig { override.returns(T.nilable(String)) }
def external_profile_url_for_view
"https://www.furaffinity.net/user/#{url_name}" if url_name.present?
end
sig { override.returns(T.nilable(ActiveSupport::TimeWithZone)) }
def registered_at_for_view
registered_at
end
sig { override.returns(T.nilable(String)) }
def name_for_view
url_name
end
private
sig do

View File

@@ -20,6 +20,9 @@ class Domain::User::InkbunnyUser < Domain::User
class_name: "::HttpLogEntry",
optional: true
has_created_posts! Domain::Post::InkbunnyPost
has_faved_posts! Domain::Post::InkbunnyPost
validates :ib_id, presence: true
validates :name, presence: true
validates :state, presence: true, inclusion: { in: %w[ok error] }
@@ -28,4 +31,25 @@ class Domain::User::InkbunnyUser < Domain::User
def to_param
"ib/#{name}" if name.present?
end
sig { override.returns(String) }
def account_status_for_view
"ok"
end
# TODO - can we get this from the API? or scrape the user page?
sig { override.returns(T.nilable(ActiveSupport::TimeWithZone)) }
def registered_at_for_view
nil
end
sig { override.returns(T.nilable(String)) }
def external_profile_url_for_view
"https://inkbunny.net/user/#{name}" if name.present?
end
sig { override.returns(T.nilable(String)) }
def name_for_view
name
end
end

View File

@@ -0,0 +1,3 @@
# typed: strict
class Domain::Post::E621PostPolicy < Domain::PostPolicy
end

View File

@@ -0,0 +1,3 @@
# typed: strict
class Domain::Post::FaPostPolicy < Domain::PostPolicy
end

View File

@@ -0,0 +1,3 @@
# typed: strict
class Domain::Post::InkbunnyPostPolicy < Domain::PostPolicy
end

View File

@@ -0,0 +1,19 @@
class Domain::PostPolicy < ApplicationPolicy
def show?
true
end
def index?
true
end
def view_file?
user&.admin?
end
class Scope < ApplicationPolicy::Scope
def resolve
scope.all
end
end
end

View File

@@ -0,0 +1,3 @@
# typed: strict
class Domain::User::E621UserPolicy < Domain::UserPolicy
end

View File

@@ -0,0 +1,3 @@
# typed: strict
class Domain::User::FaUserPolicy < Domain::UserPolicy
end

View File

@@ -0,0 +1,3 @@
# typed: strict
class Domain::User::InkbunnyUserPolicy < Domain::UserPolicy
end

View File

@@ -0,0 +1,14 @@
# typed: strict
class Domain::UserPolicy < ApplicationPolicy
extend T::Sig
sig { returns(T::Boolean) }
def show?
true
end
sig { returns(T::Boolean) }
def view_page_scanned_at_timestamps?
user&.admin?
end
end

View File

@@ -1,7 +1,4 @@
<div
id="<%= dom_id post %>"
class="mx-auto mt-4 flex w-full flex-col gap-4 pb-4 md:max-w-2xl"
>
<div class="mx-auto mt-4 flex w-full flex-col gap-4 pb-4 md:max-w-2xl">
<section class="border border-slate-300 bg-slate-50 p-4 md:rounded-md">
<div class="flex items-center justify-between">
<div>

View File

@@ -1,7 +1,4 @@
<div
id="<%= dom_id post %>"
class="mx-auto mt-4 flex w-full max-w-2xl flex-col gap-4 pb-4"
>
<div class="mx-auto mt-4 flex w-full max-w-2xl flex-col gap-4 pb-4">
<section class="rounded-md border border-slate-300 bg-slate-50 p-4">
<div class="flex items-center justify-between gap-4">
<div class="flex min-w-0 items-center gap-4">

View File

@@ -1,4 +1,4 @@
<div id="<%= dom_id user %>" class="mx-auto my-4 w-full space-y-4 md:max-w-2xl">
<div class="mx-auto my-4 w-full space-y-4 md:max-w-2xl">
<%= render "domain/fa/users/show_sections/name_icon_and_status", user: user %>
<div class="flex flex-col gap-4 sm:flex-row">
<div class="w-full sm:w-1/2">

View File

@@ -1,7 +1,4 @@
<div
id="<%= dom_id @post %>"
class="mx-auto mt-4 flex w-full max-w-2xl flex-col gap-4 pb-4"
>
<div class="mx-auto mt-4 flex w-full max-w-2xl flex-col gap-4 pb-4">
<section class="rounded-md border border-slate-300 bg-slate-50 p-4">
<div class="flex items-center justify-between gap-4">
<div class="flex min-w-0 items-center gap-4">

View File

@@ -0,0 +1,53 @@
<div
class="m-4 flex h-fit flex-col rounded-lg border border-slate-300 bg-slate-50 shadow-sm"
>
<div class="flex justify-between border-b border-slate-300 p-4">
<div>
<%= render partial: "inline_postable_domain_link", locals: { post: post } %>
</div>
<div>
<% if creator = post.primary_creator_for_view %>
<%= link_to creator.name_for_view,
domain_user_path(creator),
class: "text-blue-600 hover:text-blue-800" %>
<% elsif fallback = post.primary_creator_name_fallback_for_view %>
<%= fallback %>
<% else %>
(nobody)
<% end %>
</div>
</div>
<div class="flex items-center justify-center p-4">
<% if primary_file =
post.primary_file_for_view && primary_file&.blob_sha256.present? %>
<%= link_to domain_post_path(post) do %>
<%= image_tag blob_path(
HexUtil.bin2hex(primary_file.blob_sha256),
format: "jpg",
thumb: "small",
),
class:
"max-h-[300px] max-w-[300px] rounded-md border border-slate-300 object-contain shadow-md",
alt: post.title %>
<% end %>
<% else %>
<span>No file available</span>
<% end %>
</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" %>
</h2>
<div class="px-4 pb-4 text-sm text-slate-500">
<div class="flex justify-end">
<% if post.posted_at %>
Posted <%= time_ago_in_words(post.posted_at) %> ago
<% else %>
Post date unknown
<% end %>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
<div class="flex w-full items-center justify-between">
<%= image_tag domain_post_domain_icon_path(post),
class: "w-6 h-6",
title: domain_post_domain_icon_title(post) %>
<% link_class =
"flex items-center text-slate-500 hover:text-slate-700 decoration-dotted underline" %>
<% url = post.domain_url_for_view %>
<% if url.present? %>
<%= link_to url.to_s, target: "_blank", rel: "noopener", class: link_class do %>
<span
><%= post.domain_abbreviation_for_view %>
#<%= post.domain_id_for_view %></span
>
<%= render partial: "shared/icons/external_link",
locals: {
class_name: "w-4 h-4 ml-1",
} %>
<% end %>
<% else %>
<span
><%= post.domain_abbreviation_for_view %>
#<%= post.domain_id_for_view %></span
>
<% end %>
</div>

View File

@@ -0,0 +1,105 @@
<% content_for :head do %>
<style>
.grid-cell {
padding: 0.25rem;
border-right: 1px solid #e2e8f0;
}
.grid-cell:last-child {
padding-left: 0;
padding-right: 1rem;
border-right: none;
}
.grid-cell:first-child {
padding-left: 1rem;
}
.grid-row:hover .grid-cell {
background-color: #f1f5f9;
}
</style>
<% end %>
<div class="mx-auto mt-4 text-center sm:mt-6">
<h1 class="text-2xl">All Posts <%= page_str(params) %></h1>
</div>
<div class="mb-6 bg-white shadow">
<div class="mx-auto px-4 sm:px-6 lg:px-8">
<div
class="flex flex-col items-center justify-between gap-4 py-4 sm:flex-row"
>
<!-- Domain Filters -->
<div class="flex flex-wrap gap-2">
<span class="my-auto font-medium text-gray-700">Sources:</span>
<% active_sources = (params[:sources] || SourceHelper.all_source_names).uniq %>
<% SourceHelper.all_source_names.each do |source| %>
<% is_active = active_sources.include?(source) %>
<% link_sources =
(
if is_active
active_sources - [source]
else
active_sources + [source]
end
) %>
<% posts_path_url =
if SourceHelper.has_all_sources?(link_sources)
domain_posts_path(view: params[:view])
else
domain_posts_path(sources: link_sources, view: params[:view])
end %>
<%= link_to(
source.titleize,
posts_path_url,
class:
"px-3 py-1 rounded-full text-sm #{is_active ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"}",
) %>
<% end %>
</div>
<!-- View Type Selector -->
<div class="flex items-center gap-2">
<span class="font-medium text-gray-700">View:</span>
<%= link_to(
domain_posts_path(view: "gallery", sources: params[:sources]),
class:
"px-3 py-1 rounded-full text-sm #{params[:view] != "table" ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"}",
) do %>
<i class="fas fa-th-large mr-1"></i> Gallery
<% end %>
<%= link_to(
domain_posts_path(view: "table", sources: params[:sources]),
class:
"px-3 py-1 rounded-full text-sm #{params[:view] == "table" ? "bg-blue-500 text-white" : "bg-gray-100 text-gray-700 hover:bg-gray-200"}",
) do %>
<i class="fas fa-list mr-1"></i> Table
<% end %>
</div>
</div>
</div>
</div>
<%= render partial: "shared/pagination_controls", locals: { collection: @posts } %>
<% if params[:view] == "table" %>
<div
class="mx-auto grid grid-cols-[auto_1fr_auto_auto_auto] border-b border-slate-300 text-sm"
>
<div class="grid-row contents">
<div class="grid-cell text-center font-semibold">Thumbnail</div>
<div class="grid-cell text-left font-semibold">Title</div>
<div class="grid-cell text-center font-semibold">Artist</div>
<div class="grid-cell text-center font-semibold">Source</div>
<div class="grid-cell text-right font-semibold">Posted</div>
</div>
<div class="col-span-full border-b border-slate-300"></div>
<% @posts.each do |post| %>
<%= render partial: "as_table_row_item", locals: { post: post } %>
<% end %>
</div>
<% else %>
<div class="mx-auto flex flex-wrap justify-center">
<% @posts.each do |post| %>
<%= render partial: "as_gallery_item", locals: { post: post } %>
<% end %>
</div>
<% end %>
<%= render partial: "shared/pagination_controls", locals: { collection: @posts } %>

View File

@@ -0,0 +1,116 @@
<div class="mx-auto mt-4 flex w-full max-w-2xl flex-col gap-4 pb-4">
<section class="rounded-md border border-slate-300 bg-slate-50 p-4">
<div class="flex items-center justify-between gap-4">
<div class="flex min-w-0 items-center gap-4">
<div class="flex min-w-0 items-center gap-2">
<span class="truncate text-lg font-medium">
<%= link_to @post.title,
@post.external_url_for_view,
class: "text-blue-600 hover:underline",
target: "_blank" %>
</span>
<i class="fa-solid fa-arrow-up-right-from-square text-slate-400"></i>
</div>
<div class="flex items-center gap-2 whitespace-nowrap text-slate-600">
by
<%= render "domain/fa/users/inline_link",
user: @post.creator,
with_post_count: false %>
</div>
</div>
</div>
<div class="mt-2 flex flex-wrap gap-x-4 text-sm text-slate-600">
<span>
<i class="fa-regular fa-calendar mr-1"></i>
Posted: <%= @post.posted_at&.strftime("%Y-%m-%d") %>
(<%= time_ago_in_words(@post.posted_at) if @post.posted_at %> ago)
</span>
<span>
<i class="fa-solid fa-eye mr-1"></i>
Views: <%= @post.num_views %>
</span>
<span>
<i class="fa-solid fa-comment mr-1"></i>
Comments: <%= @post.num_comments %>
</span>
<span>
<i class="fa-solid fa-heart mr-1"></i>
Favorites: <%= @post.num_favorites %>
<% if policy(@post).view_scraper_metadata? %>
(<%= link_to pluralize(@post.faved_by.count, "fav"),
favorites_domain_fa_post_path(@post),
class: "text-blue-600 hover:underline" %>)
<% end %>
</span>
</div>
<% if policy(@post).view_scraper_metadata? %>
<% scanned_at = @post.scanned_at %>
<% scanned_hle = @post.last_submission_page || guess_scanned_http_log_entry(@post) %>
<% scanned_at ||= scanned_hle&.requested_at %>
<% if scanned_at %>
<div class="mt-2 text-sm text-slate-500">
<div class="mt-2 text-sm text-slate-500">
<% if scanned_hle %>
<%= link_to "Scanned #{time_ago_in_words(scanned_at)} ago",
log_entry_path(scanned_hle),
class: "text-blue-600 hover:underline",
target: "_blank" %>
<% else %>
<span> Scanned <%= time_ago_in_words(scanned_at) %> ago </span>
<% end %>
</div>
</div>
<% else %>
<div class="mt-2 text-sm text-slate-500">Unknown when post scanned</div>
<% end %>
<% if hle = guess_file_downloaded_http_log_entry(@post) %>
<div class="mt-2 text-sm text-slate-500">
<%= link_to "File downloaded #{time_ago_in_words(hle.requested_at)} ago",
log_entry_path(hle),
class: "text-blue-600 hover:underline",
target: "_blank" %>
</div>
<% else %>
<div class="mt-2 text-sm text-slate-500">
Unknown when file downloaded
</div>
<% end %>
<% end %>
</section>
<% if policy(@post).view_file? %>
<section>
<% if @post.file %>
<%= render partial: "log_entries/content_container",
locals: {
log_entry: @post.file,
} %>
<% else %>
<% if !@post.scanned? %>
<%= button_to "Force scan post", scan_post_domain_fa_post_path(fa_id: @post.fa_id) %>
<% elsif !@post.file %>
<%= button_to "Force scan file", scan_post_domain_fa_post_path(fa_id: @post.fa_id) %>
<% else %>
Scanned and have file
<% end %>
<% end %>
</section>
<%= render partial: "log_entries/file_details_sky_section",
locals: {
log_entry: @post.file,
} %>
<% else %>
<section class="sky-section">
<%= link_to "https://www.furaffinity.net/view/#{@post.fa_id}/",
target: "_blank",
rel: "noopener noreferrer",
class: "section-header flex items-center gap-2 hover:text-slate-600" do %>
<span>View Post on FurAffinity</span>
<i class="fa-solid fa-arrow-up-right-from-square"></i>
<% end %>
</section>
<% end %>
<%= render "section_description", { post: @post } %>
<%= render "section_similar_posts", { post: @post } %>
</div>

View File

@@ -0,0 +1,13 @@
<div class="mx-auto my-4 w-full space-y-4 md:max-w-2xl">
<%= render "domain/users/show_sections/name_icon_and_status", user: @user %>
<div class="flex flex-col gap-4 sm:flex-row">
<div class="w-full sm:w-1/2">
<%= render "domain/users/show_sections/stats", user: @user %>
</div>
<div class="w-full sm:w-1/2">
<%= render "domain/users/show_sections/recent_created_posts", user: @user %>
</div>
</div>
<%= render "domain/users/show_sections/profile_description", user: @user %>
<%= render "domain/users/show_sections/similar_users", user: @user %>
</div>

View File

@@ -0,0 +1,47 @@
<section class="animated-shadow-sky sky-section flex divide-none p-3">
<div class="flex grow items-center gap-4">
<img
src="<%= domain_user_avatar_img_src_path(user.avatar) %>"
class="h-12 w-12 rounded-lg"
/>
<div>
<div class="text-lg font-bold text-slate-900">
<%= user.name_for_view %>
</div>
<div class="flex gap-6 text-sm text-slate-400">
<% if policy(user).view_page_scanned_at_timestamps? %>
<div class="flex flex-col">
<span class="font-medium italic text-slate-500">Status</span>
<span class=""><%= user.account_status_for_view %></span>
</div>
<% if user.respond_to?(:state) %>
<div class="flex flex-col">
<span class="font-medium italic text-slate-500">State</span>
<span class=""><%= user.state %></span>
</div>
<% end %>
<% end %>
<div class="flex flex-col">
<span class="font-medium italic text-slate-500">Registered</span>
<span class="">
<% if registered_at = user.registered_at_for_view %>
<%= time_ago_in_words(registered_at) %>
ago
<% else %>
unknown
<% end %>
</span>
</div>
</div>
</div>
</div>
<a
href="<%= user.external_profile_url_for_view %>"
target="_blank"
rel="noopener noreferrer"
class="sky-link flex items-center gap-2"
>
<span class="font-bold"><%= site_name_for_user(user) %></span>
<img src="<%= site_icon_path_for_user(user) %>" class="h-5 w-5" />
</a>
</section>

View File

@@ -0,0 +1,3 @@
<% if user.is_a?(Domain::User::FaUser) %>
<%= render "domain/users/show_sections/profile_description_fa", user: user %>
<% end %>

View File

@@ -0,0 +1,12 @@
<section class="animated-shadow-sky sky-section">
<% if (profile_html = user.profile_html) %>
<h2 class="section-header">Profile Description</h2>
<div class="bg-slate-800 p-4 text-slate-200">
<% cache(user, expires_in: 12.hours) do %>
<%= sanitized_user_profile_html(profile_html) %>
<% end %>
</div>
<% else %>
<div class="px-4 py-3 text-slate-500">No profile description available</div>
<% end %>
</section>

View File

@@ -0,0 +1,30 @@
<% if user.has_created_posts? %>
<section class="animated-shadow-sky sky-section">
<h2 class="section-header">
<span class="font-medium text-slate-900">Recent Posts</span>
<span class="float-right">
<%= link_to "#{user.posts.count} total",
domain_user_posts_path(user),
class: "sky-link" %>
</span>
</h2>
<% if user.posts.any? %>
<% user
.posts
.order(fa_id: :desc)
.limit(5)
.each do |post| %>
<div class="flex items-center px-4 py-2">
<span class="grow truncate">
<%= link_to post.title, domain_post_path(post), class: "sky-link block truncate" %>
</span>
<span class="whitespace-nowrap text-slate-500">
<%= time_ago_in_words(post.created_at) %> ago
</span>
</div>
<% end %>
<% else %>
<div class="px-4 py-3 text-slate-500">No posts found</div>
<% end %>
</section>
<% end %>

View File

@@ -0,0 +1,11 @@
<section class="sky-section animated-shadow-sky divide-y">
<h2 class="section-header">User Stats</h2>
<% stat_rows_for_user(user).each do |value_label, value| %>
<div class="flex items-center px-4 py-2">
<span class="grow text-slate-900"><%= value_label %></span>
<span class="text-slate-500">
<%= value.is_a?(Integer) ? number_with_delimiter(value, delimiter: ",") : value %>
</span>
</div>
<% end %>
</section>

View File

@@ -18,6 +18,22 @@ Rails.application.routes.draw do
end
end
resources :domain_users,
only: [:show],
constraints: {
id: %r{[^/]+/[^/]+},
},
controller: "domain/users" do
resources :posts, only: [:index]
end
resources :domain_posts,
only: %i[show index],
constraints: {
id: %r{[^/]+/[^/]+},
},
controller: "domain/posts"
namespace :domain do
namespace :fa do
resources :users,

View File

@@ -8,6 +8,7 @@ class CreateUnifiedDomainTables < ActiveRecord::Migration[7.2]
Domain::Post::E621Post
Domain::Post::InkbunnyPost
Domain::Post::SofurryPost
Domain::Post::WeasylPost
]
USER_TYPES = %w[
@@ -15,19 +16,37 @@ class CreateUnifiedDomainTables < ActiveRecord::Migration[7.2]
Domain::User::E621User
Domain::User::InkbunnyUser
Domain::User::SofurryUser
Domain::User::WeasylUser
]
POST_FILE_TYPES = %w[Domain::PostFile Domain::PostFile::InkbunnyPostFile]
GROUP_JOIN_TYPES = %w[
Domain::PostGroupJoin::InkbunnyPoolJoin
Domain::PostGroupJoin::E621PoolJoin
]
GROUP_TYPES = %w[Domain::PostGroup::InkbunnyPool Domain::PostGroup::E621Pool]
sig { params(name: String, values: T::Array[String]).void }
def create_enum(name, values)
reversible do |dir|
dir.up do
execute "CREATE TYPE #{name} AS ENUM (#{values.map { |t| "'#{t}'" }.join(",")})"
end
dir.down { execute "DROP TYPE #{name}" }
end
end
sig { void }
def change
up_only { execute "SET DEFAULT_TABLESPACE = mirai" }
reversible do |dir|
dir.up do
execute "CREATE TYPE domain_post_type AS ENUM (#{POST_TYPES.map { |t| "'#{t}'" }.join(", ")})"
end
dir.down { execute "DROP TYPE domain_post_type" }
end
create_enum("domain_post_type", POST_TYPES)
create_enum("domain_user_type", USER_TYPES)
create_enum("domain_post_file_type", POST_FILE_TYPES)
create_enum("domain_post_group_type", GROUP_TYPES)
create_enum("domain_post_group_join_type", GROUP_JOIN_TYPES)
create_table :domain_posts do |t|
t.enum :type, null: false, enum_type: "domain_post_type"
@@ -37,14 +56,6 @@ class CreateUnifiedDomainTables < ActiveRecord::Migration[7.2]
t.index :type
end
reversible do |dir|
dir.up do
execute "CREATE TYPE domain_user_type AS ENUM (#{USER_TYPES.map { |t| "'#{t}'" }.join(", ")})"
end
dir.down { execute "DROP TYPE domain_user_type" }
end
create_table :domain_users do |t|
t.enum :type, null: false, enum_type: "domain_user_type"
t.jsonb :json_attributes, default: {}
@@ -54,25 +65,26 @@ class CreateUnifiedDomainTables < ActiveRecord::Migration[7.2]
end
create_table :domain_user_avatars do |t|
t.string :type, null: false
t.references :user, null: false, foreign_key: { to_table: :domain_users }
t.jsonb :json_attributes, default: {}
t.timestamps
end
create_table :domain_post_files do |t|
t.enum :type, null: false, enum_type: "domain_post_file_type"
t.references :post, null: false, foreign_key: { to_table: :domain_posts }
t.references :log_entry,
null: true,
foreign_key: {
to_table: :http_log_entries,
}
t.binary :blob_sha256, null: true
t.jsonb :json_attributes, default: {}
t.timestamps
t.index :type
end
create_table :domain_post_files do |t|
t.references :post, null: false, foreign_key: { to_table: :domain_posts }
t.references :log_entry,
null: false,
foreign_key: {
to_table: :http_log_entries,
}
t.jsonb :json_attributes, default: {}
t.timestamps
end
create_table :domain_user_post_creations, id: false do |t|
t.references :user,
null: false,
@@ -128,52 +140,39 @@ class CreateUnifiedDomainTables < ActiveRecord::Migration[7.2]
end
reversible do |dir|
dir.up do
# timestamps are formatted like `2001-02-03T04:05:06Z`
# aka ISO 8601
execute <<~SQL
CREATE OR REPLACE FUNCTION f_cast_isots(text)
RETURNS timestamptz AS
$$SELECT to_timestamp($1, 'YYYY-MM-DDTHH24:MI:SSZ')$$
LANGUAGE sql IMMUTABLE;
SQL
end
dir.up {}
dir.down { execute "DROP FUNCTION f_cast_isots(text)" }
dir.down {}
end
add_index :domain_posts,
"(cast(json_attributes->>'fa_id' as integer))",
where: "type = 'Domain::Post::FaPost'",
name: "idx_domain_fa_posts_on_fa_id",
unique: true
create_table :domain_post_groups do |t|
t.enum :type, null: false, enum_type: "domain_post_group_type"
t.jsonb :json_attributes, default: {}
t.timestamps
add_index :domain_posts,
"((json_attributes->>'e621_id')::integer)",
where: "type = 'Domain::Post::E621Post'",
name: "idx_domain_e621_posts_on_e621_id",
unique: true
t.index :type
end
add_index :domain_posts,
"((json_attributes->>'uploader_user_id')::integer)",
where: "type = 'Domain::Post::E621Post'",
name: "idx_domain_e621_posts_on_uploader_user_id",
unique: true
create_table :domain_post_group_joins, id: false do |t|
t.enum :type, null: false, enum_type: "domain_post_group_join_type"
t.references :post,
index: false,
null: false,
foreign_key: {
to_table: :domain_posts,
}
t.references :group,
index: false,
null: false,
foreign_key: {
to_table: :domain_post_groups,
}
t.jsonb :json_attributes, default: {}
t.timestamps
add_index :domain_users,
"((json_attributes->>'url_name')::text)",
where: "type = 'Domain::User::FaUser'",
name: "idx_domain_fa_users_on_url_name",
unique: true
add_index :domain_users,
"((json_attributes->>'e621_id')::integer)",
where: "type = 'Domain::User::E621User'",
name: "idx_domain_e621_users_on_e621_id",
unique: true
add_index :domain_users,
"(json_attributes->>'migrated_user_favs_at')",
name: "idx_domain_users_on_migrated_user_favs_at"
t.index %i[post_id group_id], unique: true
t.index %i[group_id post_id]
t.index :type
end
end
end

View File

@@ -1,69 +0,0 @@
# typed: strict
class CreateUnifiedPostGroupTables < ActiveRecord::Migration[7.2]
extend T::Sig
GROUP_JOIN_TYPES = %w[
Domain::PostGroupJoin::InkbunnyPoolJoin
Domain::PostGroupJoin::E621PoolJoin
]
GROUP_TYPES = %w[Domain::PostGroup::InkbunnyPool Domain::PostGroup::E621Pool]
sig { void }
def change
up_only { execute "SET DEFAULT_TABLESPACE = mirai" }
reversible do |dir|
dir.up do
execute "CREATE TYPE domain_post_group_type AS ENUM (#{GROUP_TYPES.map { |t| "'#{t}'" }.join(",")})"
end
dir.down { execute "DROP TYPE domain_post_group_type" }
end
reversible do |dir|
dir.up do
execute "CREATE TYPE domain_post_group_join_type AS ENUM (#{GROUP_JOIN_TYPES.map { |t| "'#{t}'" }.join(",")})"
end
dir.down { execute "DROP TYPE domain_post_group_join_type" }
end
# allow null for log_entry_id for files which have not yet been downloaded
up_only do
change_column :domain_post_files, :log_entry_id, :integer, null: true
end
# avatars is not polymorphic, so we can remove the type column
remove_column :domain_user_avatars, :type, :string, null: false
create_table :domain_post_groups do |t|
t.enum :type, null: false, enum_type: "domain_post_group_type"
t.jsonb :json_attributes, default: {}
t.timestamps
t.index :type
end
create_table :domain_post_group_joins, id: false do |t|
t.enum :type, null: false, enum_type: "domain_post_group_join_type"
t.references :post,
index: false,
null: false,
foreign_key: {
to_table: :domain_posts,
}
t.references :group,
index: false,
null: false,
foreign_key: {
to_table: :domain_post_groups,
}
t.jsonb :json_attributes, default: {}
t.timestamps
t.index %i[post_id group_id], unique: true
t.index %i[group_id post_id]
t.index :type
end
end
end

View File

@@ -0,0 +1,71 @@
class CreateUnifiedJsonAttributeIndexes < ActiveRecord::Migration[7.2]
def change
# Domain::Post::FaPost.fa_id
add_index :domain_posts,
"(cast(json_attributes->>'fa_id' as integer))",
where: "type = 'Domain::Post::FaPost'",
name: "idx_domain_fa_posts_on_fa_id",
unique: true
# Domain::Post::E621Post.e621_id
add_index :domain_posts,
"((json_attributes->>'e621_id')::integer)",
where: "type = 'Domain::Post::E621Post'",
name: "idx_domain_e621_posts_on_e621_id",
unique: true
# Domain::Post::InkbunnyPost.ib_id
add_index :domain_posts,
"((json_attributes->>'ib_id')::integer)",
where: "type = 'Domain::Post::InkbunnyPost'",
name: "idx_domain_inkbunny_posts_on_ib_id",
unique: true
# Domain::Post::E621Post.uploader_user_id
add_index :domain_posts,
"((json_attributes->>'uploader_user_id')::integer)",
where: "type = 'Domain::Post::E621Post'",
name: "idx_domain_e621_posts_on_uploader_user_id",
unique: true
# Domain::User::FaUser.url_name
add_index :domain_users,
"((json_attributes->>'url_name')::text)",
where: "type = 'Domain::User::FaUser'",
name: "idx_domain_fa_users_on_url_name",
unique: true
# Domain::User::E621User.e621_id
add_index :domain_users,
"((json_attributes->>'e621_id')::integer)",
where: "type = 'Domain::User::E621User'",
name: "idx_domain_e621_users_on_e621_id",
unique: true
# Domain::User::InkbunnyUser.ib_id
add_index :domain_users,
"((json_attributes->>'ib_id')::integer)",
where: "type = 'Domain::User::InkbunnyUser'",
name: "idx_domain_inkbunny_users_on_ib_id",
unique: true
# Domain::User::InkbunnyUser.name
add_index :domain_users,
"((json_attributes->>'name')::text)",
where: "type = 'Domain::User::InkbunnyUser'",
name: "idx_domain_inkbunny_users_on_name",
unique: true
# Domain::User.migrated_user_favs_at
add_index :domain_users,
"(json_attributes->>'migrated_user_favs_at')",
name: "idx_domain_users_on_migrated_user_favs_at"
# Domain::PostFile::InkbunnyPostFile.ib_id
add_index :domain_post_files,
"((json_attributes->>'ib_id')::integer)",
where: "type = 'Domain::PostFile::InkbunnyPostFile'",
name: "idx_domain_inkbunny_post_files_on_ib_id",
unique: true
end
end

View File

@@ -93,6 +93,16 @@ CREATE EXTENSION IF NOT EXISTS vector WITH SCHEMA public;
COMMENT ON EXTENSION vector IS 'vector data type and ivfflat access method';
--
-- Name: domain_post_file_type; Type: TYPE; Schema: public; Owner: -
--
CREATE TYPE public.domain_post_file_type AS ENUM (
'Domain::PostFile',
'Domain::PostFile::InkbunnyPostFile'
);
--
-- Name: domain_post_group_join_type; Type: TYPE; Schema: public; Owner: -
--
@@ -121,7 +131,8 @@ CREATE TYPE public.domain_post_type AS ENUM (
'Domain::Post::FaPost',
'Domain::Post::E621Post',
'Domain::Post::InkbunnyPost',
'Domain::Post::SofurryPost'
'Domain::Post::SofurryPost',
'Domain::Post::WeasylPost'
);
@@ -133,7 +144,8 @@ CREATE TYPE public.domain_user_type AS ENUM (
'Domain::User::FaUser',
'Domain::User::E621User',
'Domain::User::InkbunnyUser',
'Domain::User::SofurryUser'
'Domain::User::SofurryUser',
'Domain::User::WeasylUser'
);
@@ -149,15 +161,6 @@ CREATE TYPE public.postable_type AS ENUM (
);
--
-- Name: f_cast_isots(text); Type: FUNCTION; Schema: public; Owner: -
--
CREATE FUNCTION public.f_cast_isots(text) RETURNS timestamp with time zone
LANGUAGE sql IMMUTABLE
AS $_$SELECT to_timestamp($1, 'YYYY-MM-DDTHH24:MI:SSZ')$_$;
SET default_tablespace = '';
SET default_table_access_method = heap;
@@ -2687,8 +2690,10 @@ SET default_tablespace = mirai;
CREATE TABLE public.domain_post_files (
id bigint NOT NULL,
type public.domain_post_file_type NOT NULL,
post_id bigint NOT NULL,
log_entry_id integer,
log_entry_id bigint,
blob_sha256 bytea,
json_attributes jsonb DEFAULT '{}'::jsonb,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
@@ -5465,6 +5470,34 @@ CREATE UNIQUE INDEX idx_domain_fa_posts_on_fa_id ON public.domain_posts USING bt
CREATE UNIQUE INDEX idx_domain_fa_users_on_url_name ON public.domain_users USING btree (((json_attributes ->> 'url_name'::text))) WHERE (type = 'Domain::User::FaUser'::public.domain_user_type);
--
-- Name: idx_domain_inkbunny_post_files_on_ib_id; Type: INDEX; Schema: public; Owner: -; Tablespace: mirai
--
CREATE UNIQUE INDEX idx_domain_inkbunny_post_files_on_ib_id ON public.domain_post_files USING btree ((((json_attributes ->> 'ib_id'::text))::integer)) WHERE (type = 'Domain::PostFile::InkbunnyPostFile'::public.domain_post_file_type);
--
-- Name: idx_domain_inkbunny_posts_on_ib_id; Type: INDEX; Schema: public; Owner: -; Tablespace: mirai
--
CREATE UNIQUE INDEX idx_domain_inkbunny_posts_on_ib_id ON public.domain_posts USING btree ((((json_attributes ->> 'ib_id'::text))::integer)) WHERE (type = 'Domain::Post::InkbunnyPost'::public.domain_post_type);
--
-- Name: idx_domain_inkbunny_users_on_ib_id; Type: INDEX; Schema: public; Owner: -; Tablespace: mirai
--
CREATE UNIQUE INDEX idx_domain_inkbunny_users_on_ib_id ON public.domain_users USING btree ((((json_attributes ->> 'ib_id'::text))::integer)) WHERE (type = 'Domain::User::InkbunnyUser'::public.domain_user_type);
--
-- Name: idx_domain_inkbunny_users_on_name; Type: INDEX; Schema: public; Owner: -; Tablespace: mirai
--
CREATE UNIQUE INDEX idx_domain_inkbunny_users_on_name ON public.domain_users USING btree (((json_attributes ->> 'name'::text))) WHERE (type = 'Domain::User::InkbunnyUser'::public.domain_user_type);
--
-- Name: idx_domain_users_on_migrated_user_favs_at; Type: INDEX; Schema: public; Owner: -; Tablespace: mirai
--
@@ -6767,6 +6800,13 @@ CREATE INDEX index_domain_post_files_on_log_entry_id ON public.domain_post_files
CREATE INDEX index_domain_post_files_on_post_id ON public.domain_post_files USING btree (post_id);
--
-- Name: index_domain_post_files_on_type; Type: INDEX; Schema: public; Owner: -; Tablespace: mirai
--
CREATE INDEX index_domain_post_files_on_type ON public.domain_post_files USING btree (type);
--
-- Name: index_domain_post_group_joins_on_group_id_and_post_id; Type: INDEX; Schema: public; Owner: -; Tablespace: mirai
--
@@ -8413,7 +8453,7 @@ ALTER TABLE ONLY public.domain_twitter_tweets
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
('20250205035529'),
('20250206224121'),
('20250203235035'),
('20250131060105'),
('20250131055824'),

View File

@@ -32,6 +32,9 @@ class ApplicationController
include ::Domain::E621::PostsHelper
include ::Domain::Fa::PostsHelper
include ::Domain::Fa::UsersHelper
include ::HelpersInterface
include ::Domain::PostsHelper
include ::Domain::UsersHelper
include ::GoodJobHelper
include ::IndexablePostsHelper
include ::LogEntriesHelper

View File

@@ -29,6 +29,9 @@ class DeviseController
include ::Domain::E621::PostsHelper
include ::Domain::Fa::PostsHelper
include ::Domain::Fa::UsersHelper
include ::HelpersInterface
include ::Domain::PostsHelper
include ::Domain::UsersHelper
include ::GoodJobHelper
include ::IndexablePostsHelper
include ::LogEntriesHelper

View File

@@ -15,13 +15,13 @@ class Domain::Fa::Job::UserIncrementalJob
sig do
params(
args: T.untyped,
args: T::Hash[::Symbol, T.untyped],
block: T.nilable(T.proc.params(job: Domain::Fa::Job::UserIncrementalJob).void)
).returns(T.any(Domain::Fa::Job::UserIncrementalJob, FalseClass))
end
def perform_later(args, &block); end
sig { params(args: T.untyped).returns(T.untyped) }
sig { params(args: T::Hash[::Symbol, T.untyped]).void }
def perform_now(args); end
end
end

View File

@@ -15,13 +15,13 @@ class Domain::Fa::Job::UserPageJob
sig do
params(
args: T.untyped,
args: T::Hash[::Symbol, T.untyped],
block: T.nilable(T.proc.params(job: Domain::Fa::Job::UserPageJob).void)
).returns(T.any(Domain::Fa::Job::UserPageJob, FalseClass))
end
def perform_later(args, &block); end
sig { params(args: T.untyped).returns(T.untyped) }
sig { params(args: T::Hash[::Symbol, T.untyped]).void }
def perform_now(args); end
end
end

View File

@@ -789,6 +789,61 @@ class Domain::Post
sig { void }
def json_attributes_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at=(value); end
sig { returns(T::Boolean) }
def posted_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_before_last_save; end
sig { returns(T.untyped) }
def posted_at_before_type_cast; end
sig { returns(T::Boolean) }
def posted_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def posted_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def posted_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_was; end
sig { void }
def posted_at_will_change!; end
sig { void }
def restore_created_at!; end
@@ -801,6 +856,9 @@ class Domain::Post
sig { void }
def restore_json_attributes!; end
sig { void }
def restore_posted_at!; end
sig { void }
def restore_type!; end
@@ -831,6 +889,12 @@ class Domain::Post
sig { returns(T::Boolean) }
def saved_change_to_json_attributes?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_posted_at; end
sig { returns(T::Boolean) }
def saved_change_to_posted_at?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def saved_change_to_type; end
@@ -955,6 +1019,9 @@ class Domain::Post
sig { returns(T::Boolean) }
def will_save_change_to_json_attributes?; end
sig { returns(T::Boolean) }
def will_save_change_to_posted_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_type?; end

View File

@@ -1428,6 +1428,61 @@ class Domain::Post::E621Post
sig { void }
def pools_array_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at=(value); end
sig { returns(T::Boolean) }
def posted_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_before_last_save; end
sig { returns(T.untyped) }
def posted_at_before_type_cast; end
sig { returns(T::Boolean) }
def posted_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def posted_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def posted_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_was; end
sig { void }
def posted_at_will_change!; end
sig { returns(T.untyped) }
def prev_md5s; end
@@ -1560,6 +1615,9 @@ class Domain::Post::E621Post
sig { void }
def restore_pools_array!; end
sig { void }
def restore_posted_at!; end
sig { void }
def restore_prev_md5s!; end
@@ -1677,6 +1735,12 @@ class Domain::Post::E621Post
sig { returns(T::Boolean) }
def saved_change_to_pools_array?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_posted_at; end
sig { returns(T::Boolean) }
def saved_change_to_posted_at?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def saved_change_to_prev_md5s; end
@@ -2210,6 +2274,9 @@ class Domain::Post::E621Post
sig { returns(T::Boolean) }
def will_save_change_to_pools_array?; end
sig { returns(T::Boolean) }
def will_save_change_to_posted_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_prev_md5s?; end

View File

@@ -451,27 +451,57 @@ class Domain::Post::InkbunnyPost
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def build_creator(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_deep_update_log_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::UserPostCreation) }
def build_primary_user_post_creation(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_shallow_update_log_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def create_creator(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def create_creator!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_deep_update_log_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_deep_update_log_entry!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::UserPostCreation) }
def create_primary_user_post_creation(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::UserPostCreation) }
def create_primary_user_post_creation!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_shallow_update_log_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_shallow_update_log_entry!(*args, &blk); end
sig { returns(T.nilable(::Domain::User::InkbunnyUser)) }
def creator; end
sig { params(value: T.nilable(::Domain::User::InkbunnyUser)).void }
def creator=(value); end
sig { returns(T.nilable(::HttpLogEntry)) }
def deep_update_log_entry; end
sig { params(value: T.nilable(::HttpLogEntry)).void }
def deep_update_log_entry=(value); end
sig { returns(T::Boolean) }
def deep_update_log_entry_changed?; end
sig { returns(T::Boolean) }
def deep_update_log_entry_previously_changed?; end
sig { returns(T::Array[T.untyped]) }
def faving_user_ids; end
@@ -492,12 +522,12 @@ class Domain::Post::InkbunnyPost
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def file_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::Post` class because it declared `has_many :files`.
# This method is created by ActiveRecord on the `Domain::Post::InkbunnyPost` class because it declared `has_many :files`.
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
sig { returns(::Domain::PostFile::PrivateCollectionProxy) }
sig { returns(::Domain::PostFile::InkbunnyPostFile::PrivateCollectionProxy) }
def files; end
sig { params(value: T::Enumerable[::Domain::PostFile]).void }
sig { params(value: T::Enumerable[::Domain::PostFile::InkbunnyPostFile]).void }
def files=(value); end
sig { returns(T::Array[T.untyped]) }
@@ -537,15 +567,39 @@ class Domain::Post::InkbunnyPost
sig { returns(T.nilable(::Domain::User::InkbunnyUser)) }
def reload_creator; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_deep_update_log_entry; end
sig { returns(T.nilable(::Domain::UserPostCreation)) }
def reload_primary_user_post_creation; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_shallow_update_log_entry; end
sig { void }
def reset_creator; end
sig { void }
def reset_deep_update_log_entry; end
sig { void }
def reset_primary_user_post_creation; end
sig { void }
def reset_shallow_update_log_entry; end
sig { returns(T.nilable(::HttpLogEntry)) }
def shallow_update_log_entry; end
sig { params(value: T.nilable(::HttpLogEntry)).void }
def shallow_update_log_entry=(value); end
sig { returns(T::Boolean) }
def shallow_update_log_entry_changed?; end
sig { returns(T::Boolean) }
def shallow_update_log_entry_previously_changed?; end
sig { returns(T::Array[T.untyped]) }
def user_post_creation_ids; end
@@ -775,6 +829,51 @@ class Domain::Post::InkbunnyPost
sig { void }
def created_at_will_change!; end
sig { returns(T.nilable(::Integer)) }
def deep_update_log_entry_id; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def deep_update_log_entry_id=(value); end
sig { returns(T::Boolean) }
def deep_update_log_entry_id?; end
sig { returns(T.nilable(::Integer)) }
def deep_update_log_entry_id_before_last_save; end
sig { returns(T.untyped) }
def deep_update_log_entry_id_before_type_cast; end
sig { returns(T::Boolean) }
def deep_update_log_entry_id_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def deep_update_log_entry_id_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def deep_update_log_entry_id_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def deep_update_log_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def deep_update_log_entry_id_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def deep_update_log_entry_id_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def deep_update_log_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def deep_update_log_entry_id_previously_was; end
sig { returns(T.nilable(::Integer)) }
def deep_update_log_entry_id_was; end
sig { void }
def deep_update_log_entry_id_will_change!; end
sig { returns(T.nilable(::Integer)) }
def ib_id; end
@@ -955,6 +1054,341 @@ class Domain::Post::InkbunnyPost
sig { void }
def json_attributes_will_change!; end
sig { returns(T.untyped) }
def keywords; end
sig { params(value: T.untyped).returns(T.untyped) }
def keywords=(value); end
sig { returns(T::Boolean) }
def keywords?; end
sig { returns(T.untyped) }
def keywords_before_last_save; end
sig { returns(T.untyped) }
def keywords_before_type_cast; end
sig { returns(T::Boolean) }
def keywords_came_from_user?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def keywords_change; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def keywords_change_to_be_saved; end
sig { params(from: T.untyped, to: T.untyped).returns(T::Boolean) }
def keywords_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.untyped) }
def keywords_in_database; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def keywords_previous_change; end
sig { params(from: T.untyped, to: T.untyped).returns(T::Boolean) }
def keywords_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.untyped) }
def keywords_previously_was; end
sig { returns(T.untyped) }
def keywords_was; end
sig { void }
def keywords_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def last_file_updated_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def last_file_updated_at=(value); end
sig { returns(T::Boolean) }
def last_file_updated_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def last_file_updated_at_before_last_save; end
sig { returns(T.untyped) }
def last_file_updated_at_before_type_cast; end
sig { returns(T::Boolean) }
def last_file_updated_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def last_file_updated_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def last_file_updated_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def last_file_updated_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def last_file_updated_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def last_file_updated_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def last_file_updated_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def last_file_updated_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def last_file_updated_at_was; end
sig { void }
def last_file_updated_at_will_change!; end
sig { returns(T.nilable(::Integer)) }
def num_comments; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def num_comments=(value); end
sig { returns(T::Boolean) }
def num_comments?; end
sig { returns(T.nilable(::Integer)) }
def num_comments_before_last_save; end
sig { returns(T.untyped) }
def num_comments_before_type_cast; end
sig { returns(T::Boolean) }
def num_comments_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_comments_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_comments_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def num_comments_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def num_comments_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_comments_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def num_comments_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def num_comments_previously_was; end
sig { returns(T.nilable(::Integer)) }
def num_comments_was; end
sig { void }
def num_comments_will_change!; end
sig { returns(T.nilable(::Integer)) }
def num_favs; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def num_favs=(value); end
sig { returns(T::Boolean) }
def num_favs?; end
sig { returns(T.nilable(::Integer)) }
def num_favs_before_last_save; end
sig { returns(T.untyped) }
def num_favs_before_type_cast; end
sig { returns(T::Boolean) }
def num_favs_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_favs_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_favs_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def num_favs_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def num_favs_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_favs_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def num_favs_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def num_favs_previously_was; end
sig { returns(T.nilable(::Integer)) }
def num_favs_was; end
sig { void }
def num_favs_will_change!; end
sig { returns(T.nilable(::Integer)) }
def num_files; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def num_files=(value); end
sig { returns(T::Boolean) }
def num_files?; end
sig { returns(T.nilable(::Integer)) }
def num_files_before_last_save; end
sig { returns(T.untyped) }
def num_files_before_type_cast; end
sig { returns(T::Boolean) }
def num_files_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_files_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_files_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def num_files_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def num_files_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_files_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def num_files_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def num_files_previously_was; end
sig { returns(T.nilable(::Integer)) }
def num_files_was; end
sig { void }
def num_files_will_change!; end
sig { returns(T.nilable(::Integer)) }
def num_views; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def num_views=(value); end
sig { returns(T::Boolean) }
def num_views?; end
sig { returns(T.nilable(::Integer)) }
def num_views_before_last_save; end
sig { returns(T.untyped) }
def num_views_before_type_cast; end
sig { returns(T::Boolean) }
def num_views_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_views_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_views_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def num_views_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def num_views_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def num_views_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def num_views_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def num_views_previously_was; end
sig { returns(T.nilable(::Integer)) }
def num_views_was; end
sig { void }
def num_views_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at=(value); end
sig { returns(T::Boolean) }
def posted_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_before_last_save; end
sig { returns(T.untyped) }
def posted_at_before_type_cast; end
sig { returns(T::Boolean) }
def posted_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def posted_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def posted_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def posted_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def posted_at_was; end
sig { void }
def posted_at_will_change!; end
sig { returns(T.nilable(::String)) }
def rating; end
@@ -1003,6 +1437,9 @@ class Domain::Post::InkbunnyPost
sig { void }
def restore_created_at!; end
sig { void }
def restore_deep_update_log_entry_id!; end
sig { void }
def restore_ib_id!; end
@@ -1015,27 +1452,63 @@ class Domain::Post::InkbunnyPost
sig { void }
def restore_json_attributes!; end
sig { void }
def restore_keywords!; end
sig { void }
def restore_last_file_updated_at!; end
sig { void }
def restore_num_comments!; end
sig { void }
def restore_num_favs!; end
sig { void }
def restore_num_files!; end
sig { void }
def restore_num_views!; end
sig { void }
def restore_posted_at!; end
sig { void }
def restore_rating!; end
sig { void }
def restore_shallow_update_log_entry_id!; end
sig { void }
def restore_state!; end
sig { void }
def restore_submission_type!; end
sig { void }
def restore_title!; end
sig { void }
def restore_type!; end
sig { void }
def restore_updated_at!; end
sig { void }
def restore_writing!; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_created_at; end
sig { returns(T::Boolean) }
def saved_change_to_created_at?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_deep_update_log_entry_id; end
sig { returns(T::Boolean) }
def saved_change_to_deep_update_log_entry_id?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_ib_id; end
@@ -1060,12 +1533,60 @@ class Domain::Post::InkbunnyPost
sig { returns(T::Boolean) }
def saved_change_to_json_attributes?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def saved_change_to_keywords; end
sig { returns(T::Boolean) }
def saved_change_to_keywords?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_last_file_updated_at; end
sig { returns(T::Boolean) }
def saved_change_to_last_file_updated_at?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_num_comments; end
sig { returns(T::Boolean) }
def saved_change_to_num_comments?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_num_favs; end
sig { returns(T::Boolean) }
def saved_change_to_num_favs?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_num_files; end
sig { returns(T::Boolean) }
def saved_change_to_num_files?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_num_views; end
sig { returns(T::Boolean) }
def saved_change_to_num_views?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_posted_at; end
sig { returns(T::Boolean) }
def saved_change_to_posted_at?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_rating; end
sig { returns(T::Boolean) }
def saved_change_to_rating?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_shallow_update_log_entry_id; end
sig { returns(T::Boolean) }
def saved_change_to_shallow_update_log_entry_id?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_state; end
@@ -1078,6 +1599,12 @@ class Domain::Post::InkbunnyPost
sig { returns(T::Boolean) }
def saved_change_to_submission_type?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_title; end
sig { returns(T::Boolean) }
def saved_change_to_title?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def saved_change_to_type; end
@@ -1090,6 +1617,57 @@ class Domain::Post::InkbunnyPost
sig { returns(T::Boolean) }
def saved_change_to_updated_at?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_writing; end
sig { returns(T::Boolean) }
def saved_change_to_writing?; end
sig { returns(T.nilable(::Integer)) }
def shallow_update_log_entry_id; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def shallow_update_log_entry_id=(value); end
sig { returns(T::Boolean) }
def shallow_update_log_entry_id?; end
sig { returns(T.nilable(::Integer)) }
def shallow_update_log_entry_id_before_last_save; end
sig { returns(T.untyped) }
def shallow_update_log_entry_id_before_type_cast; end
sig { returns(T::Boolean) }
def shallow_update_log_entry_id_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def shallow_update_log_entry_id_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def shallow_update_log_entry_id_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def shallow_update_log_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def shallow_update_log_entry_id_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def shallow_update_log_entry_id_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def shallow_update_log_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def shallow_update_log_entry_id_previously_was; end
sig { returns(T.nilable(::Integer)) }
def shallow_update_log_entry_id_was; end
sig { void }
def shallow_update_log_entry_id_will_change!; end
sig { returns(T.nilable(::String)) }
def state; end
@@ -1180,6 +1758,51 @@ class Domain::Post::InkbunnyPost
sig { void }
def submission_type_will_change!; end
sig { returns(T.nilable(::String)) }
def title; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def title=(value); end
sig { returns(T::Boolean) }
def title?; end
sig { returns(T.nilable(::String)) }
def title_before_last_save; end
sig { returns(T.untyped) }
def title_before_type_cast; end
sig { returns(T::Boolean) }
def title_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def title_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def title_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def title_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def title_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def title_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def title_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def title_previously_was; end
sig { returns(T.nilable(::String)) }
def title_was; end
sig { void }
def title_will_change!; end
sig { returns(T.untyped) }
def type; end
@@ -1283,6 +1906,9 @@ class Domain::Post::InkbunnyPost
sig { returns(T::Boolean) }
def will_save_change_to_created_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_deep_update_log_entry_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_ib_id?; end
@@ -1295,20 +1921,95 @@ class Domain::Post::InkbunnyPost
sig { returns(T::Boolean) }
def will_save_change_to_json_attributes?; end
sig { returns(T::Boolean) }
def will_save_change_to_keywords?; end
sig { returns(T::Boolean) }
def will_save_change_to_last_file_updated_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_num_comments?; end
sig { returns(T::Boolean) }
def will_save_change_to_num_favs?; end
sig { returns(T::Boolean) }
def will_save_change_to_num_files?; end
sig { returns(T::Boolean) }
def will_save_change_to_num_views?; end
sig { returns(T::Boolean) }
def will_save_change_to_posted_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_rating?; end
sig { returns(T::Boolean) }
def will_save_change_to_shallow_update_log_entry_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_state?; end
sig { returns(T::Boolean) }
def will_save_change_to_submission_type?; end
sig { returns(T::Boolean) }
def will_save_change_to_title?; end
sig { returns(T::Boolean) }
def will_save_change_to_type?; end
sig { returns(T::Boolean) }
def will_save_change_to_updated_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_writing?; end
sig { returns(T.nilable(::String)) }
def writing; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def writing=(value); end
sig { returns(T::Boolean) }
def writing?; end
sig { returns(T.nilable(::String)) }
def writing_before_last_save; end
sig { returns(T.untyped) }
def writing_before_type_cast; end
sig { returns(T::Boolean) }
def writing_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def writing_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def writing_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def writing_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def writing_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def writing_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def writing_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def writing_previously_was; end
sig { returns(T.nilable(::String)) }
def writing_was; end
sig { void }
def writing_will_change!; end
end
module GeneratedRelationMethods

View File

@@ -411,12 +411,33 @@ class Domain::PostFile
end
module GeneratedAssociationMethods
sig { returns(T.nilable(::BlobEntry)) }
def blob; end
sig { params(value: T.nilable(::BlobEntry)).void }
def blob=(value); end
sig { returns(T::Boolean) }
def blob_changed?; end
sig { returns(T::Boolean) }
def blob_previously_changed?; end
sig { params(args: T.untyped, blk: T.untyped).returns(::BlobEntry) }
def build_blob(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_log_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::Post) }
def build_post(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::BlobEntry) }
def create_blob(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::BlobEntry) }
def create_blob!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_log_entry(*args, &blk); end
@@ -453,12 +474,18 @@ class Domain::PostFile
sig { returns(T::Boolean) }
def post_previously_changed?; end
sig { returns(T.nilable(::BlobEntry)) }
def reload_blob; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_log_entry; end
sig { returns(T.nilable(::Domain::Post)) }
def reload_post; end
sig { void }
def reset_blob; end
sig { void }
def reset_log_entry; end
@@ -611,6 +638,51 @@ class Domain::PostFile
end
module GeneratedAttributeMethods
sig { returns(T.nilable(::String)) }
def blob_sha256; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def blob_sha256=(value); end
sig { returns(T::Boolean) }
def blob_sha256?; end
sig { returns(T.nilable(::String)) }
def blob_sha256_before_last_save; end
sig { returns(T.untyped) }
def blob_sha256_before_type_cast; end
sig { returns(T::Boolean) }
def blob_sha256_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def blob_sha256_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def blob_sha256_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def blob_sha256_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def blob_sha256_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def blob_sha256_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def blob_sha256_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def blob_sha256_previously_was; end
sig { returns(T.nilable(::String)) }
def blob_sha256_was; end
sig { void }
def blob_sha256_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at; end
@@ -846,6 +918,51 @@ class Domain::PostFile
sig { void }
def json_attributes_will_change!; end
sig { returns(T.nilable(::Integer)) }
def last_status_code; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def last_status_code=(value); end
sig { returns(T::Boolean) }
def last_status_code?; end
sig { returns(T.nilable(::Integer)) }
def last_status_code_before_last_save; end
sig { returns(T.untyped) }
def last_status_code_before_type_cast; end
sig { returns(T::Boolean) }
def last_status_code_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def last_status_code_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def last_status_code_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def last_status_code_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def last_status_code_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def last_status_code_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def last_status_code_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def last_status_code_previously_was; end
sig { returns(T.nilable(::Integer)) }
def last_status_code_was; end
sig { void }
def last_status_code_will_change!; end
sig { returns(T.nilable(::Integer)) }
def log_entry_id; end
@@ -936,6 +1053,9 @@ class Domain::PostFile
sig { void }
def post_id_will_change!; end
sig { void }
def restore_blob_sha256!; end
sig { void }
def restore_created_at!; end
@@ -951,21 +1071,81 @@ class Domain::PostFile
sig { void }
def restore_json_attributes!; end
sig { void }
def restore_last_status_code!; end
sig { void }
def restore_log_entry_id!; end
sig { void }
def restore_post_id!; end
sig { void }
def restore_retry_count!; end
sig { void }
def restore_state!; end
sig { void }
def restore_type!; end
sig { void }
def restore_updated_at!; end
sig { void }
def restore_url_str!; end
sig { returns(T.nilable(::Integer)) }
def retry_count; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def retry_count=(value); end
sig { returns(T::Boolean) }
def retry_count?; end
sig { returns(T.nilable(::Integer)) }
def retry_count_before_last_save; end
sig { returns(T.untyped) }
def retry_count_before_type_cast; end
sig { returns(T::Boolean) }
def retry_count_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def retry_count_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def retry_count_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def retry_count_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def retry_count_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def retry_count_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def retry_count_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def retry_count_previously_was; end
sig { returns(T.nilable(::Integer)) }
def retry_count_was; end
sig { void }
def retry_count_will_change!; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_blob_sha256; end
sig { returns(T::Boolean) }
def saved_change_to_blob_sha256?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_created_at; end
@@ -996,6 +1176,12 @@ class Domain::PostFile
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_status_code; end
sig { returns(T::Boolean) }
def saved_change_to_last_status_code?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_log_entry_id; end
@@ -1008,12 +1194,24 @@ class Domain::PostFile
sig { returns(T::Boolean) }
def saved_change_to_post_id?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_retry_count; end
sig { returns(T::Boolean) }
def saved_change_to_retry_count?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_state; end
sig { returns(T::Boolean) }
def saved_change_to_state?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def saved_change_to_type; end
sig { returns(T::Boolean) }
def saved_change_to_type?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_updated_at; end
@@ -1071,6 +1269,51 @@ class Domain::PostFile
sig { void }
def state_will_change!; end
sig { returns(T.untyped) }
def type; end
sig { params(value: T.untyped).returns(T.untyped) }
def type=(value); end
sig { returns(T::Boolean) }
def type?; end
sig { returns(T.untyped) }
def type_before_last_save; end
sig { returns(T.untyped) }
def type_before_type_cast; end
sig { returns(T::Boolean) }
def type_came_from_user?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def type_change; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def type_change_to_be_saved; end
sig { params(from: T.untyped, to: T.untyped).returns(T::Boolean) }
def type_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.untyped) }
def type_in_database; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def type_previous_change; end
sig { params(from: T.untyped, to: T.untyped).returns(T::Boolean) }
def type_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.untyped) }
def type_previously_was; end
sig { returns(T.untyped) }
def type_was; end
sig { void }
def type_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at; end
@@ -1171,6 +1414,9 @@ class Domain::PostFile
sig { void }
def url_str_will_change!; end
sig { returns(T::Boolean) }
def will_save_change_to_blob_sha256?; end
sig { returns(T::Boolean) }
def will_save_change_to_created_at?; end
@@ -1186,15 +1432,24 @@ class Domain::PostFile
sig { returns(T::Boolean) }
def will_save_change_to_json_attributes?; end
sig { returns(T::Boolean) }
def will_save_change_to_last_status_code?; end
sig { returns(T::Boolean) }
def will_save_change_to_log_entry_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_post_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_retry_count?; end
sig { returns(T::Boolean) }
def will_save_change_to_state?; end
sig { returns(T::Boolean) }
def will_save_change_to_type?; end
sig { returns(T::Boolean) }
def will_save_change_to_updated_at?; end

File diff suppressed because it is too large Load Diff

View File

@@ -1175,6 +1175,61 @@ class Domain::User::E621User
sig { void }
def num_other_favs_cached_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def registered_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def registered_at=(value); end
sig { returns(T::Boolean) }
def registered_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def registered_at_before_last_save; end
sig { returns(T.untyped) }
def registered_at_before_type_cast; end
sig { returns(T::Boolean) }
def registered_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def registered_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def registered_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def registered_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def registered_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def registered_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def registered_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def registered_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def registered_at_was; end
sig { void }
def registered_at_will_change!; end
sig { void }
def restore_created_at!; end
@@ -1202,6 +1257,9 @@ class Domain::User::E621User
sig { void }
def restore_num_other_favs_cached!; end
sig { void }
def restore_registered_at!; end
sig { void }
def restore_scanned_favs_at!; end
@@ -1268,6 +1326,12 @@ class Domain::User::E621User
sig { returns(T::Boolean) }
def saved_change_to_num_other_favs_cached?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_registered_at; end
sig { returns(T::Boolean) }
def saved_change_to_registered_at?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_scanned_favs_at; end
@@ -1519,6 +1583,9 @@ class Domain::User::E621User
sig { returns(T::Boolean) }
def will_save_change_to_num_other_favs_cached?; end
sig { returns(T::Boolean) }
def will_save_change_to_registered_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_scanned_favs_at?; end

View File

@@ -790,6 +790,51 @@ class Domain::User::FaUser
end
module GeneratedAttributeMethods
sig { returns(T.nilable(::String)) }
def account_status; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def account_status=(value); end
sig { returns(T::Boolean) }
def account_status?; end
sig { returns(T.nilable(::String)) }
def account_status_before_last_save; end
sig { returns(T.untyped) }
def account_status_before_type_cast; end
sig { returns(T::Boolean) }
def account_status_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def account_status_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def account_status_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def account_status_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def account_status_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def account_status_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def account_status_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def account_status_previously_was; end
sig { returns(T.nilable(::String)) }
def account_status_was; end
sig { void }
def account_status_will_change!; end
sig { returns(T.nilable(::String)) }
def artist_type; end
@@ -1820,6 +1865,9 @@ class Domain::User::FaUser
sig { void }
def registered_at_will_change!; end
sig { void }
def restore_account_status!; end
sig { void }
def restore_artist_type!; end
@@ -1913,6 +1961,12 @@ class Domain::User::FaUser
sig { void }
def restore_url_name!; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_account_status; end
sig { returns(T::Boolean) }
def saved_change_to_account_status?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_artist_type; end
@@ -2564,6 +2618,9 @@ class Domain::User::FaUser
sig { void }
def url_name_will_change!; end
sig { returns(T::Boolean) }
def will_save_change_to_account_status?; end
sig { returns(T::Boolean) }
def will_save_change_to_artist_type?; end

View File

@@ -513,12 +513,12 @@ class Domain::User::InkbunnyUser
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def faved_post_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::User` class because it declared `has_many :faved_posts, through: :user_post_favs`.
# This method is created by ActiveRecord on the `Domain::User::InkbunnyUser` class because it declared `has_many :faved_posts, through: :user_post_favs`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::Post::PrivateCollectionProxy) }
sig { returns(::Domain::Post::InkbunnyPost::PrivateCollectionProxy) }
def faved_posts; end
sig { params(value: T::Enumerable[::Domain::Post]).void }
sig { params(value: T::Enumerable[::Domain::Post::InkbunnyPost]).void }
def faved_posts=(value); end
sig { returns(T::Array[T.untyped]) }
@@ -557,10 +557,10 @@ class Domain::User::InkbunnyUser
# This method is created by ActiveRecord on the `Domain::User::InkbunnyUser` class because it declared `has_many :posts, through: :user_post_creations`.
# 🔗 [Rails guide for `has_many_through` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-through-association)
sig { returns(::Domain::Inkbunny::Post::PrivateCollectionProxy) }
sig { returns(::Domain::Post::InkbunnyPost::PrivateCollectionProxy) }
def posts; end
sig { params(value: T::Enumerable[::Domain::Inkbunny::Post]).void }
sig { params(value: T::Enumerable[::Domain::Post::InkbunnyPost]).void }
def posts=(value); end
sig { returns(T.nilable(::Domain::UserAvatar)) }

View File

@@ -60,6 +60,18 @@ module GeneratedPathHelpersModule
sig { params(args: T.untyped).returns(String) }
def domain_inkbunny_user_posts_path(*args); end
sig { params(args: T.untyped).returns(String) }
def domain_post_path(*args); end
sig { params(args: T.untyped).returns(String) }
def domain_posts_path(*args); end
sig { params(args: T.untyped).returns(String) }
def domain_user_path(*args); end
sig { params(args: T.untyped).returns(String) }
def domain_user_posts_path(*args); end
sig { params(args: T.untyped).returns(String) }
def edit_global_state_path(*args); end

View File

@@ -60,6 +60,18 @@ module GeneratedUrlHelpersModule
sig { params(args: T.untyped).returns(String) }
def domain_inkbunny_user_url(*args); end
sig { params(args: T.untyped).returns(String) }
def domain_post_url(*args); end
sig { params(args: T.untyped).returns(String) }
def domain_posts_url(*args); end
sig { params(args: T.untyped).returns(String) }
def domain_user_posts_url(*args); end
sig { params(args: T.untyped).returns(String) }
def domain_user_url(*args); end
sig { params(args: T.untyped).returns(String) }
def edit_global_state_url(*args); end

View File

@@ -32,6 +32,9 @@ class Rails::ApplicationController
include ::Domain::E621::PostsHelper
include ::Domain::Fa::PostsHelper
include ::Domain::Fa::UsersHelper
include ::HelpersInterface
include ::Domain::PostsHelper
include ::Domain::UsersHelper
include ::GoodJobHelper
include ::IndexablePostsHelper
include ::LogEntriesHelper

View File

@@ -32,6 +32,9 @@ class Rails::Conductor::BaseController
include ::Domain::E621::PostsHelper
include ::Domain::Fa::PostsHelper
include ::Domain::Fa::UsersHelper
include ::HelpersInterface
include ::Domain::PostsHelper
include ::Domain::UsersHelper
include ::GoodJobHelper
include ::IndexablePostsHelper
include ::LogEntriesHelper

View File

@@ -32,6 +32,9 @@ class Rails::HealthController
include ::Domain::E621::PostsHelper
include ::Domain::Fa::PostsHelper
include ::Domain::Fa::UsersHelper
include ::HelpersInterface
include ::Domain::PostsHelper
include ::Domain::UsersHelper
include ::GoodJobHelper
include ::IndexablePostsHelper
include ::LogEntriesHelper

View File

@@ -337,8 +337,7 @@ describe Domain::Fa::Job::BrowsePageJob do
before do
post = find_post.call
post.scanned_at = 1.hour.ago
file = post.build_file(url_str: "http://www.example.com/img.jpg")
file.save!
post.files.create!(url_str: "http://www.example.com/img.jpg")
post.save!
perform_now({})
end

View File

@@ -0,0 +1,181 @@
# typed: false
require "rails_helper"
RSpec.describe Domain::Fa::Job::ScanFileJob do
include PerformJobHelpers
let(:fa_post) { create(:domain_post_fa_post) }
let(:post_file) { create(:domain_post_file) }
let(:http_client_mock) { instance_double("::Scraper::HttpClient") }
before do
Scraper::ClientFactory.http_client_mock = http_client_mock
@log_entries =
HttpClientMockHelpers.init_http_client_mock(
http_client_mock,
client_mock_config,
)
end
describe "#perform" do
context "with post_file arg" do
context "with 200 response" do
let(:client_mock_config) do
[
{
uri: post_file.url_str,
status_code: 200,
content_type: "image/jpeg",
contents: "fake image data",
},
]
end
it "processes a file successfully" do
post_file.update!(state: "pending")
perform_now({ post_file: post_file })
post_file.reload
expect(post_file.state).to eq("ok")
expect(post_file.retry_count).to eq(0)
end
end
context "with 404 response" do
let(:client_mock_config) do
[
{
uri: post_file.url_str,
status_code: 404,
content_type: "text/html",
contents: "not found",
},
]
end
it "handles 404 response" do
post_file.update!(state: "pending")
perform_now({ post_file: post_file })
post_file.reload
expect(post_file.state).to eq("terminal_error")
expect(post_file.retry_count).to eq(0)
end
end
context "with 500 response" do
let(:client_mock_config) do
[
{
uri: post_file.url_str,
status_code: 500,
content_type: "text/html",
contents: "server error",
},
]
end
it "handles other error responses" do
post_file.update!(state: "pending")
perform_now(
{ post_file: post_file },
should_raise: /response 500, aborting/,
)
post_file.reload
expect(post_file.state).to eq("retryable_error")
expect(post_file.retry_count).to eq(1)
end
end
context "with existing state" do
let(:client_mock_config) { [] }
it "skips processing when state is ok" do
post_file.update!(state: "ok")
perform_now({ post_file: post_file })
post_file.reload
expect(post_file.state).to eq("ok")
end
it "skips processing when state is terminal_error" do
post_file.update!(state: "terminal_error")
perform_now({ post_file: post_file })
post_file.reload
expect(post_file.state).to eq("terminal_error")
end
it "skips processing when retry_count >= 3 for retryable_error" do
post_file.update!(state: "retryable_error", retry_count: 3)
perform_now({ post_file: post_file })
post_file.reload
expect(post_file.state).to eq("retryable_error")
expect(post_file.retry_count).to eq(3)
end
end
end
context "with post arg" do
let(:post) { create(:domain_post_fa_post) }
context "with valid post file" do
let(:post_file) { create(:domain_post_file, post: post) }
let(:client_mock_config) do
[
{
uri: post_file.url_str,
status_code: 200,
content_type: "image/jpeg",
contents: "fake image data",
},
]
end
it "processes using the post's file" do
post_file.update!(state: "pending")
perform_now({ post: post })
post_file.reload
expect(post_file.state).to eq("ok")
end
end
context "with missing post" do
let(:client_mock_config) { [] }
it "enqueues a scan post job when post not found using fa_id" do
perform_now({ fa_id: "12345" })
expect(
SpecUtil.enqueued_job_args(Domain::Fa::Job::ScanPostJob),
).to include(hash_including(fa_id: "12345"))
end
end
end
context "error handling" do
let(:client_mock_config) { [] }
it "raises error for invalid post type" do
invalid_post = create(:domain_e621_post)
perform_now(
{ post: invalid_post },
should_raise: /invalid post model: Domain::E621::Post/,
)
end
it "raises error when file has no url" do
post_file.update!(url_str: nil, state: "pending")
perform_now({ post_file: post_file }, should_raise: /file has no url/)
end
end
end
end

View File

@@ -28,6 +28,7 @@ RSpec.describe Domain::MigrateToDomain do
sources_array: old_post.sources_array,
artists_array: old_post.artists_array,
e621_updated_at: be_within(1.second).of(old_post.e621_updated_at),
posted_at: be_within(1.second).of(old_post.posted_at),
last_index_page_id: old_post.last_index_page_id,
caused_by_entry_id: old_post.caused_by_entry_id,
scan_log_entry_id: old_post.scan_log_entry_id,
@@ -133,9 +134,83 @@ RSpec.describe Domain::MigrateToDomain do
file_error: nil,
parent_e621_id: nil,
scanned_post_favs_at: Time.current,
posted_at: 2.days.ago,
)
end
# Add new test cases for file handling
context "when handling post files" do
let(:file_url) { "https://example.com/image.jpg" }
let(:log_entry) do
create(:http_log_entry, uri_str: "https://example.com/image.jpg")
end
it "creates a file with state 'ok' when file exists with status 200" do
old_post.file = log_entry
old_post.file_url_str = file_url
old_post.save!
migrator.migrate_e621_posts
new_post = Domain::Post::E621Post.find_by(e621_id: old_post.e621_id)
expect(new_post.file).to have_attributes(
url_str: file_url,
log_entry_id: log_entry.id,
state: "ok",
)
end
it "creates a file with state 'terminal_error' when file exists with non-200 status" do
error_log_entry =
create(
:http_log_entry,
:with_error,
uri_str: "https://example.com/image.jpg",
)
old_post.update!(
file: error_log_entry,
file_url_str: file_url,
file_error:
Domain::E621::Post::FileError.new(
status_code: 404,
log_entry_id: error_log_entry.id,
retry_count: 1,
),
)
migrator.migrate_e621_posts
new_post = Domain::Post::E621Post.find_by(e621_id: old_post.e621_id)
expect(new_post.file).to have_attributes(
url_str: file_url,
log_entry_id: error_log_entry.id,
state: "terminal_error",
error_message: "status_code: 404",
)
end
it "creates a file with state 'pending' when only file_url_str exists" do
old_post.update!(file_url_str: file_url)
migrator.migrate_e621_posts
new_post = Domain::Post::E621Post.find_by(e621_id: old_post.e621_id)
expect(new_post.file).to have_attributes(
url_str: file_url,
log_entry_id: nil,
state: "pending",
)
end
it "does not create a file when neither file nor file_url_str exists" do
migrator.migrate_e621_posts
new_post = Domain::Post::E621Post.find_by(e621_id: old_post.e621_id)
expect(new_post.file).to be_nil
end
end
it "migrates posts that don't exist in the new table" do
expect { migrator.migrate_e621_posts }.to change(
Domain::Post::E621Post,
@@ -185,6 +260,7 @@ RSpec.describe Domain::MigrateToDomain do
file_error: nil,
parent_e621_id: nil,
scanned_post_favs_at: Time.current,
posted_at: 2.days.ago,
)
end

View File

@@ -193,6 +193,17 @@ RSpec.describe Domain::MigrateToDomain do
end
end
it "handles disabled users" do
old_user.state = "scan_error"
old_user.state_detail = {
"scan_error" =>
"account disabled or not found, see last_scanned_page_id",
}
old_user.save!
migrator.migrate_fa_users
end
it "handles users with avatars" do
avatar = create(:domain_fa_user_avatar, user: old_user)
old_user.avatar = avatar

View File

@@ -61,19 +61,14 @@ RSpec.describe Domain::MigrateToDomain do
expect(new_post.creator).to be_nil
end
if old_post.files.present?
expect(new_post.files.map(&:log_entry_id)).to match_array(
old_post.files.map(&:log_entry_id),
)
expect(new_post.files.map(&:url_str)).to match_array(
old_post.files.map(&:url_str),
)
expect(new_post.files.map(&:state)).to match_array(
old_post.files.map(&:state),
)
else
expect(new_post.files).to be_empty
end
new_post
.files
.zip(old_post.files)
.each do |new_file, old_file|
expect_inkbunny_files_match(new_file, old_file)
end
expect(new_post.files).to be_empty if old_post.files.empty?
end
def expect_inkbunny_pools_match(old_pool, new_pool)
@@ -106,6 +101,34 @@ RSpec.describe Domain::MigrateToDomain do
end
end
def expect_inkbunny_files_match(new_file, old_file)
new_state =
case old_file.state
when "ok"
old_file.log_entry_id.present? ? "ok" : "pending"
else
"terminal_error"
end
expect(new_file).to have_attributes(
ib_id: old_file.ib_file_id,
file_name: old_file.file_name,
url_str: old_file.url_str,
file_order: old_file.file_order,
md5_initial: old_file.md5_initial,
md5_full: old_file.md5_full,
md5s: old_file.md5s,
state: new_state,
log_entry_id: old_file.log_entry_id,
last_status_code: old_file.log_entry&.status_code,
)
expect(new_file.ib_created_at).to match_presence_or_be_within(
1.second,
of: old_file.ib_created_at,
)
end
describe "#migrate_inkbunny_users" do
let!(:old_user) do
create(
@@ -283,14 +306,9 @@ RSpec.describe Domain::MigrateToDomain do
)
end
let!(:old_post) do
post = create(:domain_inkbunny_post, creator: creator)
file = create(:domain_inkbunny_file, post: post)
post.files << file
post
end
it "migrates posts that don't exist in the new table" do
old_post = create(:domain_inkbunny_post, creator: creator)
expect { migrator.migrate_inkbunny_posts }.to change(
Domain::Post::InkbunnyPost,
:count,
@@ -301,6 +319,8 @@ RSpec.describe Domain::MigrateToDomain do
end
it "skips posts that already exist in the new table" do
old_post = create(:domain_inkbunny_post, creator: creator)
# Create a post in the new table first
Domain::Post::InkbunnyPost.create!(
ib_id: old_post.ib_post_id,
@@ -316,9 +336,9 @@ RSpec.describe Domain::MigrateToDomain do
end
it "handles multiple posts in batches" do
# Create a few more old posts
additional_posts =
2.times.map do |i|
# Create a few old posts
old_posts =
3.times.map do |i|
post = create(:domain_inkbunny_post, creator: creator)
file = create(:domain_inkbunny_file, post: post)
post.files << file
@@ -332,13 +352,11 @@ RSpec.describe Domain::MigrateToDomain do
expect(Domain::Post::InkbunnyPost.count).to eq(3)
expect(Domain::Post::InkbunnyPost.pluck(:ib_id)).to contain_exactly(
old_post.ib_post_id,
additional_posts[0].ib_post_id,
additional_posts[1].ib_post_id,
*old_posts.map(&:ib_post_id),
)
# Verify all posts were migrated correctly
([old_post] + additional_posts).each do |old_post|
old_posts.each do |old_post|
new_post =
Domain::Post::InkbunnyPost.find_by(ib_id: old_post.ib_post_id)
expect_inkbunny_posts_match(old_post, new_post)
@@ -360,12 +378,111 @@ RSpec.describe Domain::MigrateToDomain do
expect { migrator.migrate_inkbunny_posts }.to change(
Domain::Post::InkbunnyPost,
:count,
).by(2)
).by(1)
new_post =
Domain::Post::InkbunnyPost.find_by(ib_id: post_without_files.ib_post_id)
expect_inkbunny_posts_match(post_without_files, new_post)
end
it "handles posts with files and verifies file migration" do
old_post = create(:domain_inkbunny_post, creator: creator)
old_file =
create(
:domain_inkbunny_file,
post: old_post,
ib_file_id: 456,
file_name: "test.jpg",
url_str: "https://example.com/test.jpg",
ib_created_at: Time.current,
file_order: 1,
md5_initial: "abc123",
md5_full: "abc123_full",
md5s: ["abc123"],
state: :ok,
state_detail: {
},
log_entry: create(:http_log_entry),
blob_entry: create(:blob_entry),
)
expect { migrator.migrate_inkbunny_posts }.to change(
Domain::Post::InkbunnyPost,
:count,
).by(1).and change(Domain::PostFile::InkbunnyPostFile, :count).by(1)
new_post = Domain::Post::InkbunnyPost.find_by(ib_id: old_post.ib_post_id)
expect(new_post.files.count).to eq(1)
new_file = new_post.files.first
expect_inkbunny_files_match(new_file, old_file)
end
it "handles posts with multiple files" do
old_post = create(:domain_inkbunny_post, creator: creator)
old_files =
3.times.map do |i|
create(
:domain_inkbunny_file,
post: old_post,
ib_file_id: 789 + i,
file_name: "test#{i}.jpg",
url_str: "https://example.com/test#{i}.jpg",
ib_created_at: Time.current,
file_order: i + 1,
md5_initial: "def#{i}456",
md5_full: "def#{i}456_full",
md5s: ["def#{i}456"],
state: :ok,
state_detail: {
},
log_entry: create(:http_log_entry),
blob_entry: create(:blob_entry),
)
end
expect { migrator.migrate_inkbunny_posts }.to change(
Domain::Post::InkbunnyPost,
:count,
).by(1).and change(Domain::PostFile::InkbunnyPostFile, :count).by(3)
new_post = Domain::Post::InkbunnyPost.find_by(ib_id: old_post.ib_post_id)
expect(new_post.files.count).to eq(3)
expect(new_post.files.map(&:ib_id)).to contain_exactly(789, 790, 791)
old_files.each do |old_file|
new_file = new_post.files.find { |f| f.ib_id == old_file.ib_file_id }
expect_inkbunny_files_match(new_file, old_file)
end
end
it "handles posts with files in error states" do
old_post = create(:domain_inkbunny_post, creator: creator)
old_file =
create(
:domain_inkbunny_file,
post: old_post,
ib_file_id: 999,
file_name: "error.jpg",
url_str: "https://example.com/error.jpg",
ib_created_at: Time.current,
file_order: 1,
state: :error,
state_detail: {
"error" => "download failed",
},
log_entry: create(:http_log_entry),
)
expect { migrator.migrate_inkbunny_posts }.to change(
Domain::Post::InkbunnyPost,
:count,
).by(1).and change(Domain::PostFile::InkbunnyPostFile, :count).by(1)
new_post = Domain::Post::InkbunnyPost.find_by(ib_id: old_post.ib_post_id)
new_file = new_post.files.first
expect_inkbunny_files_match(new_file, old_file)
end
end
describe "#migrate_inkbunny_pools" do