5 Commits

Author SHA1 Message Date
Dylan Knutson
5b67f2ad9a show tor hle in ui, test thumbnail enqueue 2025-07-25 00:41:28 +00:00
Dylan Knutson
dffdef51cd backup tor archive scraping 2025-07-25 00:25:12 +00:00
Dylan Knutson
d86612ee2e Create task task-72 2025-07-25 00:19:25 +00:00
Dylan Knutson
0e92d9a7e1 remove unused indexes 2025-07-24 21:45:29 +00:00
Dylan Knutson
211d5eb62c fuzzysearch enqueue job 2025-07-24 21:45:21 +00:00
61 changed files with 4221 additions and 1009 deletions

View File

@@ -95,9 +95,3 @@ ENV PATH "/home/vscode/.exo/bin:$PATH"
# install just (command runner)
RUN curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | sudo bash -s -- --to /usr/local/bin
# RUN source /usr/local/share/nvm/nvm.sh && nvm install 18 && nvm use 18 && npm install -g yarn 2>&1
# ENV PATH /usr/local/share/nvm/current/bin:$PATH
# # install `backlog` tool
# RUN npm i -g backlog.md

View File

@@ -98,9 +98,18 @@ services:
- WIREGUARD_ADDRESSES=10.165.87.232/32,fd7d:76ee:e68f:a993:4d1b:a77a:b471:a606/128
- SERVER_CITIES="San Jose California, Fremont California"
tor:
image: dockurr/tor
volumes:
- devcontainer-redux-tor-config:/etc/tor
- devcontainer-redux-tor-data:/var/lib/tor
restart: always
volumes:
postgres-17-data:
devcontainer-redux-gem-cache:
devcontainer-redux-blob-files:
devcontainer-redux-grafana-data:
devcontainer-redux-prometheus-data:
devcontainer-redux-tor-config:
devcontainer-redux-tor-data:

View File

@@ -24,4 +24,14 @@ function blob-files-stats
set -l files_dir (blob-files-dir || return 1)
printf "apparent size: %s\n" (du -sh --apparent-size $files_dir)
printf "actual size: %s\n" (du -sh $files_dir)
end
end
function curl-fa-onion
curl \
--socks5-hostname tor:9050 \
--compressed \
-A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0" \
-H "Accept-Encoding: gzip, deflate" \
-H "Connection: keep-alive" \
"http://g6jy5jkx466lrqojcngbnksugrcfxsl562bzuikrka5rv7srgguqbjid.onion/$argv[1]"
end

View File

@@ -67,7 +67,7 @@ module Domain::PostsHelper
def gallery_file_for_post(post)
file = post.primary_file_for_view
return nil unless file.present?
return nil unless file.state_ok?
return nil unless file.state_ok? || file.last_status_code == 200
return nil unless file.log_entry_id.present?
content_type = file.log_entry&.content_type
return nil unless content_type.present?

View File

@@ -13,6 +13,7 @@ module FaUriHelper
const :original_file_posted, Integer
const :latest_file_posted, Integer
const :filename, String
const :filename_with_ts, String
sig { returns(Time) }
def original_file_posted_at
@@ -35,7 +36,7 @@ module FaUriHelper
path = uri.path
match =
path.match(
%r{/art/(?<url_name>[^/]+)/(?<latest_ts>\d+)/(?<original_ts>\d+)\.(?<filename>.*)},
%r{/art/(?<url_name>[^/]+)/(stories/)?(?<latest_ts>\d+)/(?<original_ts>\d+)\.(?<filename>.*)},
)
return nil unless match
url_name = match[:url_name]
@@ -47,6 +48,7 @@ module FaUriHelper
original_file_posted: original_ts,
latest_file_posted: latest_ts,
filename:,
filename_with_ts: path.split("/").last,
)
end

View File

@@ -78,7 +78,7 @@ class Domain::E621::Job::ScanPostFavsJob < Domain::E621::Job::Base
breaker += 1
end
post.scanned_post_favs_at = Time.now
post.scanned_post_favs_at = Time.current
post.save!
end
end

View File

@@ -0,0 +1,90 @@
# typed: strict
# frozen_string_literal: true
class Domain::Fa::Job::ScanFuzzysearchJob < Domain::Fa::Job::Base
queue_as :fuzzysearch
sig { override.returns(Symbol) }
def self.http_factory_method
:get_fuzzysearch_http_client
end
sig { override.params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
def perform(args)
post_file = T.let(nil, T.nilable(Domain::PostFile))
post = post_from_args!
fs_client = Scraper::FuzzysearchApiClient.new(http_client)
logger.tagged(make_arg_tag(post)) do
if post.fuzzysearch_checked_at.present? && !force_scan?
logger.warn("fuzzysearch already checked, skipping")
return
end
fa_id = post.fa_id
if fa_id.nil?
logger.error("post has no fa_id, skipping")
return
end
unless post.state_removed?
logger.warn("post is not removed, skipping")
return
end
response = fs_client.search_fa_id_info(fa_id)
post.fuzzysearch_checked_at = Time.now
if response.is_a?(HttpLogEntry)
post.fuzzysearch_entry = response
logger.error("fuzzysearch query failed")
return
end
post.fuzzysearch_entry = response.log_entry
post.fuzzysearch_json = response.json
if creator = post.creator
if creator.url_name != response.artist_url_name
fatal_error(
format_tags(
make_tag("existing", creator.url_name),
make_tag("fuzzysearch", response.artist_url_name),
"fuzzysearch artist url name mismatch",
),
)
end
else
url_name = response.artist_url_name
creator =
Domain::User::FaUser.find_or_initialize_by(url_name:) do |user|
# TODO - bug in has_aux_table, attributes not initialized before
# block is called
user.name ||= response.artist_name
user.full_name ||= response.artist_name
end
creator.save! if creator.new_record?
post.creator = creator
end
if post.keywords.blank? || post.keywords.empty?
post.keywords = response.tags
end
post_file = post.file
post_file ||=
post.build_file(url_str: response.file_url) do |post_file|
post_file.last_status_code = 404
post_file.state = "terminal_error"
end
if post_file.new_record?
post_file.enqueue_job_after_save(
Job::FaPostFurArchiverPostFileJob,
{ post_file: },
)
end
end
ensure
post.save! if post
post_file.save! if post_file
end
end

View File

@@ -2,8 +2,8 @@
class Job::FaPostFurArchiverPostFileJob < Scraper::JobBase
extend T::Sig
include Domain::StaticFileJobHelper
queue_as :fur_archiver
queue_as :static_file
discard_on Scraper::JobBase::JobError, ActiveJob::DeserializationError
sig { override.returns(Symbol) }
@@ -13,102 +13,95 @@ class Job::FaPostFurArchiverPostFileJob < Scraper::JobBase
sig { override.params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
def perform(args)
post_file = T.cast(args[:post_file], Domain::PostFile)
logger.tagged(make_arg_tag(post_file), make_arg_tag(post_file.post)) do
handle(post_file)
post = T.cast(args[:post], T.nilable(Domain::Post::FaPost))
if post.nil?
post_file = T.cast(args[:post_file], T.nilable(Domain::PostFile))
fatal_error("no post or post file found, skipping") if post_file.nil?
post = T.cast(post_file.post, T.nilable(Domain::Post::FaPost))
fatal_error("no post found, skipping") if post.nil?
end
# todo - try multiple post files?
post_file =
post
.files
.to_a
.sort_by { |file| T.must(file.created_at) }
.reverse
.find do |file|
url_str = file.url_str || next
uri = Addressable::URI.parse(url_str)
FaUriHelper.is_fa_cdn_host?(uri.host)
end
fatal_error("no existing post file found, skipping") if post_file.nil?
logger.tagged(make_arg_tag(post), make_arg_tag(post_file)) do
if post_file.state_ok? && post_file.last_status_code == 200
logger.info("file already downloaded, skipping")
return :ok
end
unless post_file.last_status_code == 404
logger.warn("last status code is not 404, skipping")
return
end
unless post_file.state_terminal_error?
logger.warn("post file not in terminal error state, skipping")
return
end
file_url_str = post_file.url_str
fatal_error("no file url str") unless file_url_str
url_parsed = FaUriHelper.parse_fa_media_url(file_url_str)
fatal_error("failed to parse fa file url") unless url_parsed
creator_url_name = post.creator&.url_name
fatal_error("no creator url name") unless creator_url_name
unless creator_url_name == url_parsed.url_name
logger.tagged(
make_tag("in_db", creator_url_name),
make_tag("in_url", url_parsed.url_name),
) { fatal_error("creator name mismatch") }
end
next if try_from_furarchiver(post, url_parsed)
try_from_tor(post, url_parsed)
end
end
FA_URL_PATTERN =
%r{
https://(d\.facdn\.net|d\.furaffinity\.net)/art/([^\/]+)/(\d+)/([^\/]+)
}x
sig { params(post_file: Domain::PostFile).void }
def handle(post_file)
post = T.cast(post_file.post, Domain::Post::FaPost)
if post.file != post_file && post.file&.state_ok?
logger.info("file already downloaded, deleting old file")
post_file.destroy
return
end
if post_file.state_ok? && post_file.last_status_code == 200
logger.info("file already downloaded, skipping")
return
end
if post.tried_from_fur_archiver?
logger.warn("already tried to download from fur archiver, skipping")
return
end
unless post_file.last_status_code == 404
logger.warn("last status code is not 404, skipping")
return
end
unless post_file.state_terminal_error?
logger.warn("post file not in terminal error state, skipping")
return
end
user_url_name = post.creator&.url_name
fatal_error("no user url name") unless user_url_name
fa_file_url_str = post_file.url_str
fatal_error("no fa file url") unless fa_file_url_str
match = fa_file_url_str.match(FA_URL_PATTERN)
unless match
if fa_file_url_str.include?("#{user_url_name}/stories/")
logger.warn("old stories URL, force rescan")
post.reload
Domain::Fa::Job::ScanPostJob.perform_now(
{ post: post, force_scan: true },
)
post.reload
unless post.state_ok?
fatal_error("post not in ok state after rescan: #{post.state}")
end
return if post.file&.state_ok? || post.file&.state_pending?
match = fa_file_url_str.match(FA_URL_PATTERN)
unless match
fatal_error("invalid fa file url after rescan: #{fa_file_url_str}")
end
else
fatal_error("invalid fa file url: #{fa_file_url_str}")
end
end
unless url_user_url_name = match.captures[1]
fatal_error("no user url name in url: #{fa_file_url_str}")
end
unless url_file_name = match.captures[3]
fatal_error("no file name in url: #{fa_file_url_str}")
end
unless user_url_name == url_user_url_name
logger.tagged(
make_tag("in_db", user_url_name),
make_tag("in_url", url_user_url_name),
) { fatal_error("user name mismatch") }
end
# returns true if the post file was found and downloaded
sig do
params(
post: Domain::Post::FaPost,
url_parsed: FaUriHelper::FaMediaUrlInfo,
).returns(T::Boolean)
end
def try_from_furarchiver(post, url_parsed)
fur_archiver_url_str =
"https://furarchiver.net/File/View?artist=#{url_user_url_name}&filename=#{url_file_name}"
"https://furarchiver.net/File/View?artist=#{url_parsed.url_name}&filename=#{url_parsed.filename_with_ts}"
post.tried_from_fur_archiver = true
post_file = post.files.build(url_str: fur_archiver_url_str)
archiver_post_file =
post.files.find_or_create_by!(url_str: fur_archiver_url_str)
if archiver_post_file.state_ok?
logger.warn("already downloaded from fur archiver, skipping")
return true
elsif archiver_post_file.state_terminal_error?
logger.warn("previously failed to download from fur archiver, trying tor")
return false
end
begin
response = http_client.get(fur_archiver_url_str)
rescue Scraper::HttpClient::InvalidURLError,
Curl::Err::HostResolutionError => e
post_file.state_terminal_error!
post_file.error_message = e.message
archiver_post_file.state_terminal_error!
archiver_post_file.error_message = e.message
archiver_post_file.save!
logger.error(
format_tags(
"invalid fur archiver url, terminal error state",
@@ -116,14 +109,40 @@ class Job::FaPostFurArchiverPostFileJob < Scraper::JobBase
make_tag("url", fur_archiver_url_str),
),
)
return
return false
ensure
post.save! if post
post.tried_from_fur_archiver = true
post.save!
end
post_file.save!
post.reload
handle_file_download_response(archiver_post_file, response)
return archiver_post_file.state_ok?
end
handle_file_download_response(post_file, response)
sig do
params(
post: Domain::Post::FaPost,
url_parsed: FaUriHelper::FaMediaUrlInfo,
).void
end
def try_from_tor(post, url_parsed)
tor_path = "fa/#{url_parsed.url_name}/#{url_parsed.filename_with_ts}"
tor_url_str =
"http://g6jy5jkx466lrqojcngbnksugrcfxsl562bzuikrka5rv7srgguqbjid.onion/#{tor_path}"
tor_post_file =
post.files.find_by(url_str: tor_url_str) ||
post.files.create!(url_str: tor_url_str)
if tor_post_file.state_ok?
logger.warn("already downloaded from tor, skipping")
return
elsif tor_post_file.state_terminal_error?
logger.warn("previously failed to download from tor, skipping")
return
end
response = tor_http_client.get(tor_url_str)
handle_file_download_response(tor_post_file, response)
end
end

View File

@@ -18,12 +18,16 @@ class Scraper::JobBase < ApplicationJob
@http_client = http_client
end
sig { params(url: String).returns(Scraper::HttpClient::Response) }
sig do
params(url: T.any(String, Addressable::URI)).returns(
Scraper::HttpClient::Response,
)
end
def get(url)
around_request(
proc do
@http_client.get(
url,
url.to_s,
caused_by_entry: @job.causing_log_entry,
use_http_cache: @job.use_http_cache?,
)
@@ -31,12 +35,16 @@ class Scraper::JobBase < ApplicationJob
)
end
sig { params(url: String).returns(Scraper::HttpClient::Response) }
sig do
params(url: T.any(String, Addressable::URI)).returns(
Scraper::HttpClient::Response,
)
end
def post(url)
around_request(
proc do
@http_client.post(
url,
url.to_s,
caused_by_entry: @job.causing_log_entry,
use_http_cache: @job.use_http_cache?,
)
@@ -65,6 +73,7 @@ class Scraper::JobBase < ApplicationJob
@deferred_jobs = T.let(Set.new, T::Set[DeferredJob])
@suppressed_jobs = T.let(Set.new, T::Set[SuppressedJob])
@http_client = T.let(nil, T.nilable(Scraper::HttpClient))
@tor_http_client = T.let(nil, T.nilable(Scraper::HttpClient))
@gallery_dl_client = T.let(nil, T.nilable(Scraper::GalleryDlClient))
@first_log_entry = T.let(nil, T.nilable(HttpLogEntry))
@last_log_entry = T.let(nil, T.nilable(HttpLogEntry))
@@ -80,6 +89,12 @@ class Scraper::JobBase < ApplicationJob
WrappedHttpClient.new(self, @http_client)
end
sig { returns(WrappedHttpClient) }
def tor_http_client
@tor_http_client ||= Scraper::ClientFactory.get_tor_http_client
WrappedHttpClient.new(self, @tor_http_client)
end
sig { returns(Scraper::GalleryDlClient) }
def gallery_dl_client
@gallery_dl_client ||= Scraper::ClientFactory.get_gallery_dl_client

View File

@@ -1,5 +1,7 @@
# typed: true
class Scraper::ClientFactory
extend T::Sig
@http_clients = Concurrent::ThreadLocalVar.new() { {} }
@gallery_dl_clients = Concurrent::ThreadLocalVar.new(nil)
@@ -48,6 +50,14 @@ class Scraper::ClientFactory
end
end
def self.get_fuzzysearch_http_client
if Rails.env.test?
@http_client_mock || raise("no http client mock set")
else
_http_client_impl(:fuzzysearch, Scraper::FuzzysearchHttpClientConfig)
end
end
def self.get_sofurry_http_client
if Rails.env.test?
@http_client_mock || raise("no http client mock set")
@@ -80,6 +90,18 @@ class Scraper::ClientFactory
end
end
def self.get_tor_http_client
if Rails.env.test?
@http_client_mock || raise("no http client mock set")
else
_http_client_impl(
:tor,
Scraper::TorHttpClientConfig,
Scraper::TorCurlHttpPerformer,
)
end
end
def self._gallery_dl_client_impl
@gallery_dl_clients.value ||=
begin
@@ -97,9 +119,20 @@ class Scraper::ClientFactory
end
end
def self._http_client_impl(key, config_klass)
sig do
params(
key: Symbol,
config_klass: T.class_of(Scraper::HttpClientConfig),
performer_klass: T.class_of(Scraper::CurlHttpPerformer),
).returns(Scraper::HttpClient)
end
def self._http_client_impl(
key,
config_klass,
performer_klass = Scraper::CurlHttpPerformer
)
@http_clients.value[key] ||= begin
Scraper::HttpClient.new(config_klass.new, Scraper::CurlHttpPerformer.new)
Scraper::HttpClient.new(config_klass.new, performer_klass.new)
end
end
end

View File

@@ -38,32 +38,33 @@ class Scraper::CurlHttpPerformer
"direct"
end
sig { params(request: Request).returns(Response) }
sig(:final) { params(request: Request).returns(Response) }
def do_request(request)
do_request_impl(request)
end
private
sig { returns(String) }
def performed_by
proxy_url = ENV["HTTP_PROXY_URL"]
case proxy_url
when nil
"direct"
when /airvpn-netherlands-proxy:(\d+)/
"airvpn-1-netherlands"
when /airvpn-san-jose-proxy:(\d+)/
"airvpn-2-san-jose"
else
raise("Unknown proxy URL: #{proxy_url}")
end
end
sig { params(request: Request).returns(Response) }
def do_request_impl(request)
curl = get_curl
start_at = Time.now
proxy_url = ENV["HTTP_PROXY_URL"]
performed_by =
case proxy_url
when nil
"direct"
when /airvpn-netherlands-proxy:(\d+)/
"airvpn-1-netherlands"
when /airvpn-san-jose-proxy:(\d+)/
"airvpn-2-san-jose"
else
raise("Unknown proxy URL: #{proxy_url}")
end
curl.proxy_url = proxy_url
curl.timeout = 30
curl.url = request.uri.normalize.to_s
curl.follow_location = request.follow_redirects
@@ -120,7 +121,7 @@ class Scraper::CurlHttpPerformer
response_headers:,
response_time_ms:,
body: body_str,
performed_by:,
performed_by: performed_by,
)
end
@@ -131,6 +132,8 @@ class Scraper::CurlHttpPerformer
t.thread_variable_set(:curl, Curl::Easy.new)
end
curl = t.thread_variable_get(:curl)
proxy_url = ENV["HTTP_PROXY_URL"]
curl.proxy_url = proxy_url
curl.headers = {}
curl
end

View File

@@ -0,0 +1,89 @@
# typed: strict
# frozen_string_literal: true
class Scraper::FuzzysearchApiClient
extend T::Sig
include HasColorLogger
API_BASE_URL = "https://api-next.fuzzysearch.net"
API_PATH_FA_ID_INFO = "/v1/file/furaffinity"
sig { params(http_client: Scraper::JobBase::WrappedHttpClient).void }
def initialize(http_client)
@http_client = http_client
end
class FaIdInfo < T::ImmutableStruct
include T::Struct::ActsAsComparable
const :log_entry, HttpLogEntry
const :json, T::Hash[String, T.untyped]
const :fa_id, Integer
const :artist_url_name, String
const :artist_name, String
const :deleted, T::Boolean
const :file_url, String
const :file_sha256, String
const :tags, T::Array[String]
end
sig { params(fa_id: Integer).returns(T.any(HttpLogEntry, FaIdInfo)) }
def search_fa_id_info(fa_id)
url = Addressable::URI.parse("#{API_BASE_URL}#{API_PATH_FA_ID_INFO}")
url.query_values = { search: fa_id.to_s }
response = @http_client.get(url)
if response.status_code != 200
logger.error(
format_tags(
make_tag("status_code", response.status_code),
make_tag("uri", url.to_s),
"fuzzysearch query failed",
),
)
return response.log_entry
end
json = JSON.parse(response.body)
unless json.is_a?(Array)
logger.error("fuzzysearch response is not an array")
return response.log_entry
end
if json.empty?
logger.error("fuzzysearch response is empty")
return response.log_entry
end
json = json.first
unless json.is_a?(Hash)
logger.error("fuzzysearch response is not a hash")
return response.log_entry
end
file_url = json["url"]
if file_url.blank?
logger.error("fuzzysearch response has no file url")
return response.log_entry
end
url_parsed = FaUriHelper.parse_fa_media_url(file_url)
if url_parsed.blank?
logger.error(
format_tags(make_tag("file_url", file_url), "failed to parse file url"),
)
return response.log_entry
end
FaIdInfo.new(
log_entry: response.log_entry,
json:,
fa_id: json["id"],
artist_url_name: url_parsed.url_name,
artist_name: json["artist"],
deleted: json["deleted"],
file_url:,
file_sha256: json["sha256"],
tags: json["tags"],
)
end
private
end

View File

@@ -0,0 +1,49 @@
# typed: strict
# frozen_string_literal: true
class Scraper::FuzzysearchHttpClientConfig < Scraper::HttpClientConfig
API_KEY_STATE_KEY = "fuzzysearch-api-key"
sig { void }
def initialize
api_key!
end
sig { override.returns(T.nilable(T::Array[T.untyped])) }
def cookies
[]
end
sig { override.returns(T::Array[[String, Numeric]]) }
def ratelimit
[["api-next.fuzzysearch.net", 1.0]]
end
sig { override.returns(Integer) }
def redirect_limit
2
end
sig { override.returns(T::Array[String]) }
def allowed_domains
%w[api-next.fuzzysearch.net]
end
sig do
override
.params(request: Scraper::CurlHttpPerformer::Request)
.returns(Scraper::CurlHttpPerformer::Request)
end
def map_request(request)
request.request_headers["X-Api-Key"] = api_key!
request.request_headers["Accept"] = "application/json"
request
end
private
sig { returns(String) }
def api_key!
GlobalState.get(API_KEY_STATE_KEY) ||
raise("fuzzysearch api key is not set")
end
end

View File

@@ -0,0 +1,28 @@
# typed: strict
# frozen_string_literal: true
class Scraper::TorCurlHttpPerformer < Scraper::CurlHttpPerformer
extend T::Sig
sig { override.returns(String) }
def performed_by
"tor-1"
end
sig { override.returns(Curl::Easy) }
def get_curl
t = Thread.current
unless t.thread_variable?(:curl)
t.thread_variable_set(:curl, Curl::Easy.new)
end
curl = T.cast(t.thread_variable_get(:curl), Curl::Easy)
curl.proxy_url = "socks5h://tor:9050"
curl.headers = {
"User-Agent" =>
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:128.0) Gecko/20100101 Firefox/128.0",
"Accept-Encoding" => "gzip, deflate",
"Connection" => "keep-alive",
}
curl
end
end

View File

@@ -0,0 +1,29 @@
# typed: strict
# frozen_string_literal: true
class Scraper::TorHttpClientConfig < Scraper::HttpClientConfig
TOR_ARCHIVE_HOST =
"g6jy5jkx466lrqojcngbnksugrcfxsl562bzuikrka5rv7srgguqbjid.onion"
extend T::Sig
sig { override.returns(T.nilable(T::Array[T::Hash[Symbol, T.untyped]])) }
def cookies
nil
end
sig { override.returns(T::Array[[String, Numeric]]) }
def ratelimit
[[TOR_ARCHIVE_HOST, 1.0]]
end
sig { override.returns(Integer) }
def redirect_limit
2
end
sig { override.returns(T::Array[String]) }
def allowed_domains
[TOR_ARCHIVE_HOST]
end
end

View File

@@ -0,0 +1,81 @@
# typed: strict
class Tasks::Fa::QueryMissingPostsFromFuzzysearch < EnqueueJobBase
extend T::Sig
include Domain::Fa::HasCountFailedInQueue
sig { params(start_at: T.nilable(String), kwargs: T.untyped).void }
def initialize(start_at: nil, **kwargs)
super(**kwargs)
@start_at = T.let(get_progress(start_at)&.to_i, T.nilable(Integer))
end
sig { override.returns(String) }
def progress_key
"fa-query-missing-posts-from-fuzzysearch"
end
sig { override.void }
def start_enqueuing
greatest_ok_post_fa_id =
Domain::Post::FaPost.where(state: :ok).maximum(:fa_id)
query =
Domain::Post::FaPost.where(state: :removed, fuzzysearch_checked_at: nil)
# clamp to greatest ok post fa_id
query =
query.where(fa_id: ..greatest_ok_post_fa_id) if greatest_ok_post_fa_id
query = query.where(fa_id: ..@start_at) if @start_at
query = query.where.missing(:file).order(fa_id: :desc)
log("finding greatest qualifying fa_id...")
greatest_post_fa_id = query.first&.fa_id
log("counting posts...")
count = query.count
puts "number of posts to process: #{count}"
pb = create_progress_bar(count)
while greatest_post_fa_id
posts = query.where(fa_id: ..greatest_post_fa_id).limit(32).to_a
break if posts.empty?
posts.each do |post|
break if interrupted?
enqueue do
Domain::Fa::Job::ScanFuzzysearchJob.perform_later(
{ fa_id: post.fa_id },
)
end
post_desc =
"#{(post.creator&.to_param || "(none)").rjust(20)} / #{post.to_param}".ljust(
40,
)
log("migrate post :: #{post_desc}") if pb.progress % 10 == 0
rescue StandardError
log("error processing post :: #{post_desc}")
ensure
pb.progress = [pb.progress + 1, pb.total].min
end
last_processed_fa_id = posts.map(&:fa_id).compact.min
if last_processed_fa_id
save_progress(last_processed_fa_id.to_s)
greatest_post_fa_id = last_processed_fa_id - 1
else
break
end
# Check for interruption after processing batch
break if interrupted?
end
log("finished")
end
sig { override.returns(Integer) }
def queue_size
count_failed_in_queue("fuzzysearch")
end
end

View File

@@ -34,7 +34,7 @@ module BelongsToWithCounterCache
association(association_name).target,
T.nilable(ActiveRecord::Base),
)
if target.send(counter_name_column).nil?
if target.send(counter_name_column).nil? && !target.new_record?
target.class.reset_counters(target.id, inverse_association_name)
end
end

View File

@@ -21,14 +21,20 @@ class Domain::Post::FaPost < Domain::Post
attr_json :first_gallery_page_id, :integer
attr_json :first_seen_entry_id, :integer
attr_json :fuzzysearch_checked_at, ActiveModelUtcTimeValue.new
attr_json :fuzzysearch_json, ActiveModel::Type::Value.new
attr_json :fuzzysearch_entry_id, :integer
# TODO - convert `file` to Domain::PostFile::FaPostFile and
# move this to Domain::PostFile::FaPostFile
attr_json :tried_from_fur_archiver, :boolean, default: false
attr_json :tried_from_tor, :boolean, default: false
belongs_to :last_user_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_browse_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_gallery_page, class_name: "::HttpLogEntry", optional: true
belongs_to :first_seen_entry, class_name: "::HttpLogEntry", optional: true
belongs_to :fuzzysearch_entry, class_name: "::HttpLogEntry", optional: true
has_single_file!
has_single_creator! Domain::User::FaUser
@@ -177,7 +183,7 @@ class Domain::Post::FaPost < Domain::Post
pa = super
return pa unless pa.nil?
if file_url_str = file&.url_str
for file_url_str in files.map(&:url_str).compact
parsed = FaUriHelper.parse_fa_media_url(file_url_str)
return parsed.original_file_posted_at.in_time_zone("UTC") if parsed
end
@@ -199,4 +205,21 @@ class Domain::Post::FaPost < Domain::Post
def tags_for_view
keywords.map { |value| TagForView.new(category: :general, value:) }
end
sig { returns(T.nilable(Domain::PostFile)) }
def fur_archiver_post_file
files.to_a.find do |file|
uri = Addressable::URI.parse(file.url_str)
uri.host == "furarchiver.net"
end
end
sig { returns(T.nilable(Domain::PostFile)) }
def tor_post_file
files.to_a.find do |file|
uri = Addressable::URI.parse(file.url_str)
uri.host ==
"g6jy5jkx466lrqojcngbnksugrcfxsl562bzuikrka5rv7srgguqbjid.onion"
end
end
end

View File

@@ -14,6 +14,7 @@ class HttpLogEntry < ReduxApplicationRecord
serverhost-1
airvpn-1-netherlands
airvpn-2-san-jose
tor-1
],
prefix: true

View File

@@ -0,0 +1,8 @@
<span>
<i class="fa-solid <%= icon_class %> mr-1"></i>
<%= label %>: <% if value.present? %>
<%= value %>
<% else %>
<span class="text-slate-400"> - </span>
<% end %>
</span>

View File

@@ -1,18 +1,24 @@
<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-calendar-days mr-1"></i>
Status: <%= post.status_for_view %>
</span>
<% if policy(post).view_tried_from_fur_archiver? && post.tried_from_fur_archiver? %>
<span>
<i class="fa-solid fa-download mr-1"></i>
FurArchiver
</span>
<%= render partial: "domain/posts/title_stat", locals: { label: "Views", value: post.num_views, icon_class: "fa-eye" } %>
<%= render partial: "domain/posts/title_stat", locals: { label: "Comments", value: post.num_comments, icon_class: "fa-comment" } %>
<%= render partial: "domain/posts/title_stat", locals: { label: "Status", value: post.status_for_view, icon_class: "fa-calendar-days" } %>
<% if policy(post).view_tried_from_fur_archiver? %>
<% if post.fuzzysearch_checked_at? %>
<% hle = post.fuzzysearch_entry %>
<span>
<i class="fa-solid fa-search mr-1"></i>
<%= link_to "FuzzySearch", log_entry_path(hle), title: post.fuzzysearch_checked_at&.strftime("%Y-%m-%d %H:%M:%S"), class: "text-blue-600" %>
</span>
<% end %>
<% if (hle = post.fur_archiver_post_file&.log_entry) %>
<span>
<i class="fa-solid fa-download mr-1"></i>
<%= link_to "FurArchiver", log_entry_path(hle), title: hle.requested_at&.strftime("%Y-%m-%d %H:%M:%S"), class: "text-blue-600" %>
</span>
<% end %>
<% if (hle = post.tor_post_file&.log_entry) %>
<span>
<i class="fa-solid fa-mask mr-1"></i>
<%= link_to "Tor", log_entry_path(hle), title: hle.requested_at&.strftime("%Y-%m-%d %H:%M:%S"), class: "text-blue-600" %>
</span>
<% end %>
<% end %>

View File

@@ -0,0 +1,21 @@
---
id: task-72
title: Investigate removed PostFile records for terminal_error transition
status: To Do
assignee: []
created_date: '2025-07-25'
labels: []
dependencies: []
---
## Description
Analyze Domain::PostFile records currently in 'removed' state to determine if they should be transitioned to terminal_error state instead. This will help ensure proper state management and potentially improve job processing logic.
## Acceptance Criteria
- [ ] Analysis of all removed PostFile records is complete
- [ ] Criteria for terminal_error transition is documented
- [ ] Recommendation for state transition approach is provided
- [ ] If applicable
- [ ] migration strategy is outlined

View File

@@ -0,0 +1,10 @@
class AddFuzzysearchCheckToFaPosts < ActiveRecord::Migration[7.2]
def change
add_column :domain_fa_posts_fa_aux, :fuzzysearch_checked_at, :datetime
add_reference :domain_fa_posts_fa_aux,
:fuzzysearch_log_entry,
foreign_key: {
to_table: :log_entries,
}
end
end

View File

@@ -0,0 +1,22 @@
# typed: strict
# frozen_string_literal: true
class RemoveUnusedIndexes < ActiveRecord::Migration[7.2]
extend T::Sig
sig { void }
def up
remove_index :domain_user_post_favs,
name: "idx_domain_user_post_favs_on_explicit_time"
remove_index :domain_user_post_favs,
name: "idx_domain_user_post_favs_on_inferred_time"
remove_index :domain_post_files, column: :blob_sha256
remove_index :http_log_entries, column: :response_headers_id
remove_index :http_log_entries, column: :request_headers_id
end
sig { void }
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@@ -3330,13 +3330,6 @@ CREATE UNIQUE INDEX idx_domain_post_groups_on_sofurry_id ON public.domain_post_g
CREATE UNIQUE INDEX idx_domain_posts_on_sofurry_id ON public.domain_posts USING btree ((((json_attributes ->> 'sofurry_id'::text))::integer)) WHERE (type = 'Domain::Post::SofurryPost'::public.domain_post_type);
--
-- Name: idx_domain_user_post_favs_on_explicit_time; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_domain_user_post_favs_on_explicit_time ON public.domain_user_post_favs USING btree ((((json_attributes ->> 'explicit_time'::text))::integer)) WHERE (type = 'Domain::UserPostFav::FaUserPostFav'::public.domain_user_post_fav_type);
--
-- Name: idx_domain_user_post_favs_on_fav_id; Type: INDEX; Schema: public; Owner: -
--
@@ -3344,13 +3337,6 @@ CREATE INDEX idx_domain_user_post_favs_on_explicit_time ON public.domain_user_po
CREATE UNIQUE INDEX idx_domain_user_post_favs_on_fav_id ON public.domain_user_post_favs USING btree ((((json_attributes ->> 'fav_id'::text))::integer)) WHERE (type = 'Domain::UserPostFav::FaUserPostFav'::public.domain_user_post_fav_type);
--
-- Name: idx_domain_user_post_favs_on_inferred_time; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX idx_domain_user_post_favs_on_inferred_time ON public.domain_user_post_favs USING btree ((((json_attributes ->> 'inferred_time'::text))::integer)) WHERE (type = 'Domain::UserPostFav::FaUserPostFav'::public.domain_user_post_fav_type);
--
-- Name: idx_domain_users_e621_on_name_lower; Type: INDEX; Schema: public; Owner: -
--
@@ -3918,13 +3904,6 @@ CREATE INDEX index_domain_post_files_inkbunny_aux_on_base_table_id ON public.dom
CREATE INDEX index_domain_post_files_inkbunny_aux_on_ib_id ON public.domain_post_files_inkbunny_aux USING btree (ib_id);
--
-- Name: index_domain_post_files_on_blob_sha256; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_post_files_on_blob_sha256 ON public.domain_post_files USING btree (blob_sha256);
--
-- Name: index_domain_post_files_on_log_entry_id; Type: INDEX; Schema: public; Owner: -
--
@@ -4443,20 +4422,6 @@ CREATE INDEX index_good_jobs_on_scheduled_at ON public.good_jobs USING btree (sc
CREATE INDEX index_http_log_entries_on_caused_by_id ON public.http_log_entries USING btree (caused_by_id);
--
-- Name: index_http_log_entries_on_request_headers_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_http_log_entries_on_request_headers_id ON public.http_log_entries USING btree (request_headers_id);
--
-- Name: index_http_log_entries_on_response_headers_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_http_log_entries_on_response_headers_id ON public.http_log_entries USING btree (response_headers_id);
--
-- Name: index_http_log_entries_on_response_sha256; Type: INDEX; Schema: public; Owner: -
--
@@ -5297,6 +5262,7 @@ ALTER TABLE ONLY public.domain_twitter_tweets
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
('20250724213505'),
('20250723194407'),
('20250723193659'),
('20250722235434'),

View File

@@ -96,6 +96,16 @@ namespace :fa do
loop { sleep poll_duration if enqueuer.run_once == :sleep }
end
desc "Pull missing post information from FuzzySearch"
task pull_missing_post_info_from_fuzzysearch: %i[
set_logger_stdout
environment
] do
Tasks::Fa::QueryMissingPostsFromFuzzysearch.new(
start_at: ENV["start_at"],
).run
end
desc "run a single browse page job"
task browse_page_job: %i[set_logger_stdout environment] do
Domain::Fa::Job::BrowsePageJob.set(

View File

@@ -43,6 +43,7 @@ class ApplicationController
include ::Domain::Users::FaUsersHelper
include ::Domain::VisualSearchHelper
include ::DomainSourceHelper
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TimestampHelper

View File

@@ -40,6 +40,7 @@ class DeviseController
include ::Domain::Users::FaUsersHelper
include ::Domain::VisualSearchHelper
include ::DomainSourceHelper
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TimestampHelper

View File

@@ -0,0 +1,27 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Domain::Fa::Job::ScanFuzzysearchJob`.
# Please instead update this file by running `bin/tapioca dsl Domain::Fa::Job::ScanFuzzysearchJob`.
class Domain::Fa::Job::ScanFuzzysearchJob
sig { returns(ColorLogger) }
def logger; end
class << self
sig { returns(ColorLogger) }
def logger; end
sig do
params(
args: T::Hash[::Symbol, T.untyped],
block: T.nilable(T.proc.params(job: Domain::Fa::Job::ScanFuzzysearchJob).void)
).returns(T.any(Domain::Fa::Job::ScanFuzzysearchJob, FalseClass))
end
def perform_later(args, &block); end
sig { params(args: T::Hash[::Symbol, T.untyped]).returns(T.untyped) }
def perform_now(args); end
end
end

View File

@@ -632,61 +632,6 @@ class Domain::Factors::UserPostFavPostFactors
end
module GeneratedAttributeMethods
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at=(value); end
sig { returns(T::Boolean) }
def created_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_before_last_save; end
sig { returns(T.untyped) }
def created_at_before_type_cast; end
sig { returns(T::Boolean) }
def created_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def created_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def created_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_was; end
sig { void }
def created_at_will_change!; end
sig { returns(T.untyped) }
def embedding; end
@@ -822,9 +767,6 @@ class Domain::Factors::UserPostFavPostFactors
sig { void }
def post_id_will_change!; end
sig { void }
def restore_created_at!; end
sig { void }
def restore_embedding!; end
@@ -834,15 +776,6 @@ class Domain::Factors::UserPostFavPostFactors
sig { void }
def restore_post_id!; end
sig { void }
def restore_updated_at!; 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.untyped, T.untyped])) }
def saved_change_to_embedding; end
@@ -861,70 +794,6 @@ class Domain::Factors::UserPostFavPostFactors
sig { returns(T::Boolean) }
def saved_change_to_post_id?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_updated_at; end
sig { returns(T::Boolean) }
def saved_change_to_updated_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at=(value); end
sig { returns(T::Boolean) }
def updated_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_before_last_save; end
sig { returns(T.untyped) }
def updated_at_before_type_cast; end
sig { returns(T::Boolean) }
def updated_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def updated_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def 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 updated_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def updated_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def updated_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_was; end
sig { void }
def updated_at_will_change!; end
sig { returns(T::Boolean) }
def will_save_change_to_created_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_embedding?; end
@@ -933,9 +802,6 @@ class Domain::Factors::UserPostFavPostFactors
sig { returns(T::Boolean) }
def will_save_change_to_post_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_updated_at?; end
end
module GeneratedRelationMethods

View File

@@ -632,61 +632,6 @@ class Domain::Factors::UserPostFavUserFactors
end
module GeneratedAttributeMethods
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at=(value); end
sig { returns(T::Boolean) }
def created_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_before_last_save; end
sig { returns(T.untyped) }
def created_at_before_type_cast; end
sig { returns(T::Boolean) }
def created_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def created_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def created_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_was; end
sig { void }
def created_at_will_change!; end
sig { returns(T.untyped) }
def embedding; end
@@ -777,27 +722,15 @@ class Domain::Factors::UserPostFavUserFactors
sig { void }
def id_will_change!; end
sig { void }
def restore_created_at!; end
sig { void }
def restore_embedding!; end
sig { void }
def restore_id!; end
sig { void }
def restore_updated_at!; end
sig { void }
def restore_user_id!; 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.untyped, T.untyped])) }
def saved_change_to_embedding; end
@@ -810,73 +743,12 @@ class Domain::Factors::UserPostFavUserFactors
sig { returns(T::Boolean) }
def saved_change_to_id?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_updated_at; end
sig { returns(T::Boolean) }
def saved_change_to_updated_at?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_user_id; end
sig { returns(T::Boolean) }
def saved_change_to_user_id?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at=(value); end
sig { returns(T::Boolean) }
def updated_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_before_last_save; end
sig { returns(T.untyped) }
def updated_at_before_type_cast; end
sig { returns(T::Boolean) }
def updated_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def updated_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def 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 updated_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def updated_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def updated_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_was; end
sig { void }
def updated_at_will_change!; end
sig { returns(T.nilable(::Integer)) }
def user_id; end
@@ -922,18 +794,12 @@ class Domain::Factors::UserPostFavUserFactors
sig { void }
def user_id_will_change!; end
sig { returns(T::Boolean) }
def will_save_change_to_created_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_embedding?; end
sig { returns(T::Boolean) }
def will_save_change_to_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_updated_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_user_id?; end
end

View File

@@ -632,61 +632,6 @@ class Domain::Factors::UserUserFollowFromFactors
end
module GeneratedAttributeMethods
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at=(value); end
sig { returns(T::Boolean) }
def created_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_before_last_save; end
sig { returns(T.untyped) }
def created_at_before_type_cast; end
sig { returns(T::Boolean) }
def created_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def created_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def created_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_was; end
sig { void }
def created_at_will_change!; end
sig { returns(T.untyped) }
def embedding; end
@@ -777,27 +722,15 @@ class Domain::Factors::UserUserFollowFromFactors
sig { void }
def id_will_change!; end
sig { void }
def restore_created_at!; end
sig { void }
def restore_embedding!; end
sig { void }
def restore_id!; end
sig { void }
def restore_updated_at!; end
sig { void }
def restore_user_id!; 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.untyped, T.untyped])) }
def saved_change_to_embedding; end
@@ -810,73 +743,12 @@ class Domain::Factors::UserUserFollowFromFactors
sig { returns(T::Boolean) }
def saved_change_to_id?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_updated_at; end
sig { returns(T::Boolean) }
def saved_change_to_updated_at?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_user_id; end
sig { returns(T::Boolean) }
def saved_change_to_user_id?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at=(value); end
sig { returns(T::Boolean) }
def updated_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_before_last_save; end
sig { returns(T.untyped) }
def updated_at_before_type_cast; end
sig { returns(T::Boolean) }
def updated_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def updated_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def 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 updated_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def updated_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def updated_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_was; end
sig { void }
def updated_at_will_change!; end
sig { returns(T.nilable(::Integer)) }
def user_id; end
@@ -922,18 +794,12 @@ class Domain::Factors::UserUserFollowFromFactors
sig { void }
def user_id_will_change!; end
sig { returns(T::Boolean) }
def will_save_change_to_created_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_embedding?; end
sig { returns(T::Boolean) }
def will_save_change_to_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_updated_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_user_id?; end
end

View File

@@ -632,61 +632,6 @@ class Domain::Factors::UserUserFollowToFactors
end
module GeneratedAttributeMethods
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at=(value); end
sig { returns(T::Boolean) }
def created_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_before_last_save; end
sig { returns(T.untyped) }
def created_at_before_type_cast; end
sig { returns(T::Boolean) }
def created_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_change_to_be_saved; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def created_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def created_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def created_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at_was; end
sig { void }
def created_at_will_change!; end
sig { returns(T.untyped) }
def embedding; end
@@ -777,27 +722,15 @@ class Domain::Factors::UserUserFollowToFactors
sig { void }
def id_will_change!; end
sig { void }
def restore_created_at!; end
sig { void }
def restore_embedding!; end
sig { void }
def restore_id!; end
sig { void }
def restore_updated_at!; end
sig { void }
def restore_user_id!; 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.untyped, T.untyped])) }
def saved_change_to_embedding; end
@@ -810,73 +743,12 @@ class Domain::Factors::UserUserFollowToFactors
sig { returns(T::Boolean) }
def saved_change_to_id?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_updated_at; end
sig { returns(T::Boolean) }
def saved_change_to_updated_at?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_user_id; end
sig { returns(T::Boolean) }
def saved_change_to_user_id?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at; end
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at=(value); end
sig { returns(T::Boolean) }
def updated_at?; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_before_last_save; end
sig { returns(T.untyped) }
def updated_at_before_type_cast; end
sig { returns(T::Boolean) }
def updated_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def updated_at_change; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def 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 updated_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_in_database; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def updated_at_previous_change; end
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def updated_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_previously_was; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at_was; end
sig { void }
def updated_at_will_change!; end
sig { returns(T.nilable(::Integer)) }
def user_id; end
@@ -922,18 +794,12 @@ class Domain::Factors::UserUserFollowToFactors
sig { void }
def user_id_will_change!; end
sig { returns(T::Boolean) }
def will_save_change_to_created_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_embedding?; end
sig { returns(T::Boolean) }
def will_save_change_to_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_updated_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_user_id?; end
end

View File

@@ -446,6 +446,9 @@ class Domain::Post::E621Post
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_caused_by_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainPostsE621Aux) }
def build_e621_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile) }
def build_file(*args, &blk); end
@@ -485,6 +488,12 @@ class Domain::Post::E621Post
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_caused_by_entry!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainPostsE621Aux) }
def create_e621_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainPostsE621Aux) }
def create_e621_aux!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile) }
def create_file(*args, &blk); end
@@ -527,6 +536,12 @@ class Domain::Post::E621Post
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::UserPostFav) }
def create_user_post_fav!(*args, &blk); end
sig { returns(T.nilable(::DomainPostsE621Aux)) }
def e621_aux; end
sig { params(value: T.nilable(::DomainPostsE621Aux)).void }
def e621_aux=(value); end
sig { returns(T::Array[T.untyped]) }
def faving_user_ids; end
@@ -622,6 +637,9 @@ class Domain::Post::E621Post
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_caused_by_entry; end
sig { returns(T.nilable(::DomainPostsE621Aux)) }
def reload_e621_aux; end
sig { returns(T.nilable(::Domain::PostFile)) }
def reload_file; end
@@ -646,6 +664,9 @@ class Domain::Post::E621Post
sig { void }
def reset_caused_by_entry; end
sig { void }
def reset_e621_aux; end
sig { void }
def reset_file; end
@@ -1178,16 +1199,16 @@ class Domain::Post::E621Post
sig { void }
def e621_id_will_change!; end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def e621_updated_at; end
sig { params(value: T.nilable(::Time)).returns(T.nilable(::Time)) }
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def e621_updated_at=(value); end
sig { returns(T::Boolean) }
def e621_updated_at?; end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def e621_updated_at_before_last_save; end
sig { returns(T.untyped) }
@@ -1196,28 +1217,38 @@ class Domain::Post::E621Post
sig { returns(T::Boolean) }
def e621_updated_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def e621_updated_at_change; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def e621_updated_at_change_to_be_saved; end
sig { params(from: T.nilable(::Time), to: T.nilable(::Time)).returns(T::Boolean) }
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def e621_updated_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def e621_updated_at_in_database; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def e621_updated_at_previous_change; end
sig { params(from: T.nilable(::Time), to: T.nilable(::Time)).returns(T::Boolean) }
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def e621_updated_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def e621_updated_at_previously_was; end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def e621_updated_at_was; end
sig { void }
@@ -2046,7 +2077,7 @@ class Domain::Post::E621Post
sig { returns(T::Boolean) }
def saved_change_to_e621_id?; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_e621_updated_at; end
sig { returns(T::Boolean) }
@@ -2154,7 +2185,7 @@ class Domain::Post::E621Post
sig { returns(T::Boolean) }
def saved_change_to_scan_log_entry_id?; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_scanned_post_favs_at; end
sig { returns(T::Boolean) }
@@ -2304,16 +2335,16 @@ class Domain::Post::E621Post
sig { void }
def scan_log_entry_id_will_change!; end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def scanned_post_favs_at; end
sig { params(value: T.nilable(::Time)).returns(T.nilable(::Time)) }
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def scanned_post_favs_at=(value); end
sig { returns(T::Boolean) }
def scanned_post_favs_at?; end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def scanned_post_favs_at_before_last_save; end
sig { returns(T.untyped) }
@@ -2322,28 +2353,38 @@ class Domain::Post::E621Post
sig { returns(T::Boolean) }
def scanned_post_favs_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def scanned_post_favs_at_change; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def scanned_post_favs_at_change_to_be_saved; end
sig { params(from: T.nilable(::Time), to: T.nilable(::Time)).returns(T::Boolean) }
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def scanned_post_favs_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def scanned_post_favs_at_in_database; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def scanned_post_favs_at_previous_change; end
sig { params(from: T.nilable(::Time), to: T.nilable(::Time)).returns(T::Boolean) }
sig do
params(
from: T.nilable(::ActiveSupport::TimeWithZone),
to: T.nilable(::ActiveSupport::TimeWithZone)
).returns(T::Boolean)
end
def scanned_post_favs_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def scanned_post_favs_at_previously_was; end
sig { returns(T.nilable(::Time)) }
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def scanned_post_favs_at_was; end
sig { void }

View File

@@ -488,6 +488,9 @@ class Domain::Post::FaPost
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_first_seen_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_fuzzysearch_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_last_submission_log_entry(*args, &blk); end
@@ -530,6 +533,12 @@ class Domain::Post::FaPost
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_first_seen_entry!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_fuzzysearch_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_fuzzysearch_entry!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_last_submission_log_entry(*args, &blk); end
@@ -630,6 +639,18 @@ class Domain::Post::FaPost
sig { returns(T::Boolean) }
def first_seen_entry_previously_changed?; end
sig { returns(T.nilable(::HttpLogEntry)) }
def fuzzysearch_entry; end
sig { params(value: T.nilable(::HttpLogEntry)).void }
def fuzzysearch_entry=(value); end
sig { returns(T::Boolean) }
def fuzzysearch_entry_changed?; end
sig { returns(T::Boolean) }
def fuzzysearch_entry_previously_changed?; end
sig { returns(T.nilable(::HttpLogEntry)) }
def last_submission_log_entry; end
@@ -669,6 +690,9 @@ class Domain::Post::FaPost
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_first_seen_entry; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_fuzzysearch_entry; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_last_submission_log_entry; end
@@ -696,6 +720,9 @@ class Domain::Post::FaPost
sig { void }
def reset_first_seen_entry; end
sig { void }
def reset_fuzzysearch_entry; end
sig { void }
def reset_last_submission_log_entry; end
@@ -1264,6 +1291,141 @@ class Domain::Post::FaPost
sig { void }
def first_seen_entry_id_will_change!; end
sig { returns(T.nilable(::Time)) }
def fuzzysearch_checked_at; end
sig { params(value: T.nilable(::Time)).returns(T.nilable(::Time)) }
def fuzzysearch_checked_at=(value); end
sig { returns(T::Boolean) }
def fuzzysearch_checked_at?; end
sig { returns(T.nilable(::Time)) }
def fuzzysearch_checked_at_before_last_save; end
sig { returns(T.untyped) }
def fuzzysearch_checked_at_before_type_cast; end
sig { returns(T::Boolean) }
def fuzzysearch_checked_at_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
def fuzzysearch_checked_at_change; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
def fuzzysearch_checked_at_change_to_be_saved; end
sig { params(from: T.nilable(::Time), to: T.nilable(::Time)).returns(T::Boolean) }
def fuzzysearch_checked_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Time)) }
def fuzzysearch_checked_at_in_database; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
def fuzzysearch_checked_at_previous_change; end
sig { params(from: T.nilable(::Time), to: T.nilable(::Time)).returns(T::Boolean) }
def fuzzysearch_checked_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Time)) }
def fuzzysearch_checked_at_previously_was; end
sig { returns(T.nilable(::Time)) }
def fuzzysearch_checked_at_was; end
sig { void }
def fuzzysearch_checked_at_will_change!; end
sig { returns(T.nilable(::Integer)) }
def fuzzysearch_entry_id; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def fuzzysearch_entry_id=(value); end
sig { returns(T::Boolean) }
def fuzzysearch_entry_id?; end
sig { returns(T.nilable(::Integer)) }
def fuzzysearch_entry_id_before_last_save; end
sig { returns(T.untyped) }
def fuzzysearch_entry_id_before_type_cast; end
sig { returns(T::Boolean) }
def fuzzysearch_entry_id_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def fuzzysearch_entry_id_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def fuzzysearch_entry_id_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def fuzzysearch_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def fuzzysearch_entry_id_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def fuzzysearch_entry_id_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def fuzzysearch_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def fuzzysearch_entry_id_previously_was; end
sig { returns(T.nilable(::Integer)) }
def fuzzysearch_entry_id_was; end
sig { void }
def fuzzysearch_entry_id_will_change!; end
sig { returns(T.untyped) }
def fuzzysearch_json; end
sig { params(value: T.untyped).returns(T.untyped) }
def fuzzysearch_json=(value); end
sig { returns(T::Boolean) }
def fuzzysearch_json?; end
sig { returns(T.untyped) }
def fuzzysearch_json_before_last_save; end
sig { returns(T.untyped) }
def fuzzysearch_json_before_type_cast; end
sig { returns(T::Boolean) }
def fuzzysearch_json_came_from_user?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def fuzzysearch_json_change; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def fuzzysearch_json_change_to_be_saved; end
sig { params(from: T.untyped, to: T.untyped).returns(T::Boolean) }
def fuzzysearch_json_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.untyped) }
def fuzzysearch_json_in_database; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def fuzzysearch_json_previous_change; end
sig { params(from: T.untyped, to: T.untyped).returns(T::Boolean) }
def fuzzysearch_json_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.untyped) }
def fuzzysearch_json_previously_was; end
sig { returns(T.untyped) }
def fuzzysearch_json_was; end
sig { void }
def fuzzysearch_json_will_change!; end
sig { returns(T.nilable(::String)) }
def gender; end
@@ -1790,6 +1952,15 @@ class Domain::Post::FaPost
sig { void }
def restore_first_seen_entry_id!; end
sig { void }
def restore_fuzzysearch_checked_at!; end
sig { void }
def restore_fuzzysearch_entry_id!; end
sig { void }
def restore_fuzzysearch_json!; end
sig { void }
def restore_gender!; end
@@ -1844,6 +2015,9 @@ class Domain::Post::FaPost
sig { void }
def restore_tried_from_fur_archiver!; end
sig { void }
def restore_tried_from_tor!; end
sig { void }
def restore_type!; end
@@ -1892,6 +2066,24 @@ class Domain::Post::FaPost
sig { returns(T::Boolean) }
def saved_change_to_first_seen_entry_id?; end
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
def saved_change_to_fuzzysearch_checked_at; end
sig { returns(T::Boolean) }
def saved_change_to_fuzzysearch_checked_at?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_fuzzysearch_entry_id; end
sig { returns(T::Boolean) }
def saved_change_to_fuzzysearch_entry_id?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def saved_change_to_fuzzysearch_json; end
sig { returns(T::Boolean) }
def saved_change_to_fuzzysearch_json?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_gender; end
@@ -2000,6 +2192,12 @@ class Domain::Post::FaPost
sig { returns(T::Boolean) }
def saved_change_to_tried_from_fur_archiver?; end
sig { returns(T.nilable([T.nilable(T::Boolean), T.nilable(T::Boolean)])) }
def saved_change_to_tried_from_tor; end
sig { returns(T::Boolean) }
def saved_change_to_tried_from_tor?; end
sig { returns(T.nilable([T.untyped, T.untyped])) }
def saved_change_to_type; end
@@ -2337,6 +2535,51 @@ class Domain::Post::FaPost
sig { void }
def tried_from_fur_archiver_will_change!; end
sig { returns(T.nilable(T::Boolean)) }
def tried_from_tor; end
sig { params(value: T.nilable(T::Boolean)).returns(T.nilable(T::Boolean)) }
def tried_from_tor=(value); end
sig { returns(T::Boolean) }
def tried_from_tor?; end
sig { returns(T.nilable(T::Boolean)) }
def tried_from_tor_before_last_save; end
sig { returns(T.untyped) }
def tried_from_tor_before_type_cast; end
sig { returns(T::Boolean) }
def tried_from_tor_came_from_user?; end
sig { returns(T.nilable([T.nilable(T::Boolean), T.nilable(T::Boolean)])) }
def tried_from_tor_change; end
sig { returns(T.nilable([T.nilable(T::Boolean), T.nilable(T::Boolean)])) }
def tried_from_tor_change_to_be_saved; end
sig { params(from: T.nilable(T::Boolean), to: T.nilable(T::Boolean)).returns(T::Boolean) }
def tried_from_tor_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(T::Boolean)) }
def tried_from_tor_in_database; end
sig { returns(T.nilable([T.nilable(T::Boolean), T.nilable(T::Boolean)])) }
def tried_from_tor_previous_change; end
sig { params(from: T.nilable(T::Boolean), to: T.nilable(T::Boolean)).returns(T::Boolean) }
def tried_from_tor_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(T::Boolean)) }
def tried_from_tor_previously_was; end
sig { returns(T.nilable(T::Boolean)) }
def tried_from_tor_was; end
sig { void }
def tried_from_tor_will_change!; end
sig { returns(T.untyped) }
def type; end
@@ -2458,6 +2701,15 @@ class Domain::Post::FaPost
sig { returns(T::Boolean) }
def will_save_change_to_first_seen_entry_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_fuzzysearch_checked_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_fuzzysearch_entry_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_fuzzysearch_json?; end
sig { returns(T::Boolean) }
def will_save_change_to_gender?; end
@@ -2512,6 +2764,9 @@ class Domain::Post::FaPost
sig { returns(T::Boolean) }
def will_save_change_to_tried_from_fur_archiver?; end
sig { returns(T::Boolean) }
def will_save_change_to_tried_from_tor?; end
sig { returns(T::Boolean) }
def will_save_change_to_type?; end

View File

@@ -457,7 +457,7 @@ class Domain::PostFile::InkbunnyPostFile
def build_blob(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainPostFilesInkbunnyAux) }
def build_domain_post_files_inkbunny_aux(*args, &blk); end
def build_inkbunny_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_log_entry(*args, &blk); end
@@ -472,10 +472,10 @@ class Domain::PostFile::InkbunnyPostFile
def create_blob!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainPostFilesInkbunnyAux) }
def create_domain_post_files_inkbunny_aux(*args, &blk); end
def create_inkbunny_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainPostFilesInkbunnyAux) }
def create_domain_post_files_inkbunny_aux!(*args, &blk); end
def create_inkbunny_aux!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_log_entry(*args, &blk); end
@@ -490,10 +490,10 @@ class Domain::PostFile::InkbunnyPostFile
def create_post!(*args, &blk); end
sig { returns(T.nilable(::DomainPostFilesInkbunnyAux)) }
def domain_post_files_inkbunny_aux; end
def inkbunny_aux; end
sig { params(value: T.nilable(::DomainPostFilesInkbunnyAux)).void }
def domain_post_files_inkbunny_aux=(value); end
def inkbunny_aux=(value); end
sig { returns(T.nilable(::HttpLogEntry)) }
def log_entry; end
@@ -511,7 +511,7 @@ class Domain::PostFile::InkbunnyPostFile
def reload_blob; end
sig { returns(T.nilable(::DomainPostFilesInkbunnyAux)) }
def reload_domain_post_files_inkbunny_aux; end
def reload_inkbunny_aux; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_log_entry; end
@@ -523,7 +523,7 @@ class Domain::PostFile::InkbunnyPostFile
def reset_blob; end
sig { void }
def reset_domain_post_files_inkbunny_aux; end
def reset_inkbunny_aux; end
sig { void }
def reset_log_entry; end

View File

@@ -508,7 +508,7 @@ class Domain::User::E621User
def build_avatar(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersE621Aux) }
def build_domain_users_e621_aux(*args, &blk); end
def build_e621_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::UserAvatar) }
def create_avatar(*args, &blk); end
@@ -517,16 +517,16 @@ class Domain::User::E621User
def create_avatar!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersE621Aux) }
def create_domain_users_e621_aux(*args, &blk); end
def create_e621_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersE621Aux) }
def create_domain_users_e621_aux!(*args, &blk); end
def create_e621_aux!(*args, &blk); end
sig { returns(T.nilable(::DomainUsersE621Aux)) }
def domain_users_e621_aux; end
def e621_aux; end
sig { params(value: T.nilable(::DomainUsersE621Aux)).void }
def domain_users_e621_aux=(value); end
def e621_aux=(value); end
sig { returns(T::Array[T.untyped]) }
def faved_post_ids; end
@@ -602,13 +602,13 @@ class Domain::User::E621User
def reload_avatar; end
sig { returns(T.nilable(::DomainUsersE621Aux)) }
def reload_domain_users_e621_aux; end
def reload_e621_aux; end
sig { void }
def reset_avatar; end
sig { void }
def reset_domain_users_e621_aux; end
def reset_e621_aux; end
sig { returns(T::Array[T.untyped]) }
def uploaded_post_ids; end

View File

@@ -529,7 +529,7 @@ class Domain::User::FaUser
def build_avatar(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersFaAux) }
def build_domain_users_fa_aux(*args, &blk); end
def build_fa_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_last_gallery_page_log_entry(*args, &blk); end
@@ -544,10 +544,10 @@ class Domain::User::FaUser
def create_avatar!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersFaAux) }
def create_domain_users_fa_aux(*args, &blk); end
def create_fa_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersFaAux) }
def create_domain_users_fa_aux!(*args, &blk); end
def create_fa_aux!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_last_gallery_page_log_entry(*args, &blk); end
@@ -562,10 +562,10 @@ class Domain::User::FaUser
def create_last_user_page_log_entry!(*args, &blk); end
sig { returns(T.nilable(::DomainUsersFaAux)) }
def domain_users_fa_aux; end
def fa_aux; end
sig { params(value: T.nilable(::DomainUsersFaAux)).void }
def domain_users_fa_aux=(value); end
def fa_aux=(value); end
sig { returns(T::Array[T.untyped]) }
def faved_post_ids; end
@@ -665,7 +665,7 @@ class Domain::User::FaUser
def reload_avatar; end
sig { returns(T.nilable(::DomainUsersFaAux)) }
def reload_domain_users_fa_aux; end
def reload_fa_aux; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_last_gallery_page_log_entry; end
@@ -677,7 +677,7 @@ class Domain::User::FaUser
def reset_avatar; end
sig { void }
def reset_domain_users_fa_aux; end
def reset_fa_aux; end
sig { void }
def reset_last_gallery_page_log_entry; end

View File

@@ -498,7 +498,7 @@ class Domain::User::InkbunnyUser
def build_deep_update_log_entry(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersInkbunnyAux) }
def build_domain_users_inkbunny_aux(*args, &blk); end
def build_inkbunny_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_shallow_update_log_entry(*args, &blk); end
@@ -516,10 +516,10 @@ class Domain::User::InkbunnyUser
def create_deep_update_log_entry!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersInkbunnyAux) }
def create_domain_users_inkbunny_aux(*args, &blk); end
def create_inkbunny_aux(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::DomainUsersInkbunnyAux) }
def create_domain_users_inkbunny_aux!(*args, &blk); end
def create_inkbunny_aux!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_shallow_update_log_entry(*args, &blk); end
@@ -539,12 +539,6 @@ class Domain::User::InkbunnyUser
sig { returns(T::Boolean) }
def deep_update_log_entry_previously_changed?; end
sig { returns(T.nilable(::DomainUsersInkbunnyAux)) }
def domain_users_inkbunny_aux; end
sig { params(value: T.nilable(::DomainUsersInkbunnyAux)).void }
def domain_users_inkbunny_aux=(value); end
sig { returns(T::Array[T.untyped]) }
def faved_post_ids; end
@@ -601,6 +595,12 @@ class Domain::User::InkbunnyUser
sig { params(value: T::Enumerable[::Domain::User]).void }
def followed_users=(value); end
sig { returns(T.nilable(::DomainUsersInkbunnyAux)) }
def inkbunny_aux; end
sig { params(value: T.nilable(::DomainUsersInkbunnyAux)).void }
def inkbunny_aux=(value); end
sig { returns(T::Array[T.untyped]) }
def post_ids; end
@@ -622,7 +622,7 @@ class Domain::User::InkbunnyUser
def reload_deep_update_log_entry; end
sig { returns(T.nilable(::DomainUsersInkbunnyAux)) }
def reload_domain_users_inkbunny_aux; end
def reload_inkbunny_aux; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_shallow_update_log_entry; end
@@ -634,7 +634,7 @@ class Domain::User::InkbunnyUser
def reset_deep_update_log_entry; end
sig { void }
def reset_domain_users_inkbunny_aux; end
def reset_inkbunny_aux; end
sig { void }
def reset_shallow_update_log_entry; end

View File

@@ -421,32 +421,32 @@ class DomainPostFilesInkbunnyAux
end
module GeneratedAssociationMethods
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::InkbunnyPostFile) }
def build_main(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::InkbunnyPostFile) }
def create_main(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::InkbunnyPostFile) }
def create_main!(*args, &blk); end
sig { returns(T.nilable(::Domain::PostFile::InkbunnyPostFile)) }
def base_table; end
def main; end
sig { params(value: T.nilable(::Domain::PostFile::InkbunnyPostFile)).void }
def base_table=(value); end
def main=(value); end
sig { returns(T::Boolean) }
def base_table_changed?; end
def main_changed?; end
sig { returns(T::Boolean) }
def base_table_previously_changed?; end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::InkbunnyPostFile) }
def build_base_table(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::InkbunnyPostFile) }
def create_base_table(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::InkbunnyPostFile) }
def create_base_table!(*args, &blk); end
def main_previously_changed?; end
sig { returns(T.nilable(::Domain::PostFile::InkbunnyPostFile)) }
def reload_base_table; end
def reload_main; end
sig { void }
def reset_base_table; end
def reset_main; end
end
module GeneratedAssociationRelationMethods

2587
sorbet/rbi/dsl/domain_posts_e621_aux.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -418,32 +418,32 @@ class DomainUsersE621Aux
end
module GeneratedAssociationMethods
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::E621User) }
def build_main(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::E621User) }
def create_main(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::E621User) }
def create_main!(*args, &blk); end
sig { returns(T.nilable(::Domain::User::E621User)) }
def base_table; end
def main; end
sig { params(value: T.nilable(::Domain::User::E621User)).void }
def base_table=(value); end
def main=(value); end
sig { returns(T::Boolean) }
def base_table_changed?; end
def main_changed?; end
sig { returns(T::Boolean) }
def base_table_previously_changed?; end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::E621User) }
def build_base_table(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::E621User) }
def create_base_table(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::E621User) }
def create_base_table!(*args, &blk); end
def main_previously_changed?; end
sig { returns(T.nilable(::Domain::User::E621User)) }
def reload_base_table; end
def reload_main; end
sig { void }
def reset_base_table; end
def reset_main; end
end
module GeneratedAssociationRelationMethods

View File

@@ -408,32 +408,32 @@ class DomainUsersFaAux
end
module GeneratedAssociationMethods
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::FaUser) }
def build_main(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::FaUser) }
def create_main(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::FaUser) }
def create_main!(*args, &blk); end
sig { returns(T.nilable(::Domain::User::FaUser)) }
def base_table; end
def main; end
sig { params(value: T.nilable(::Domain::User::FaUser)).void }
def base_table=(value); end
def main=(value); end
sig { returns(T::Boolean) }
def base_table_changed?; end
def main_changed?; end
sig { returns(T::Boolean) }
def base_table_previously_changed?; end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::FaUser) }
def build_base_table(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::FaUser) }
def create_base_table(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::FaUser) }
def create_base_table!(*args, &blk); end
def main_previously_changed?; end
sig { returns(T.nilable(::Domain::User::FaUser)) }
def reload_base_table; end
def reload_main; end
sig { void }
def reset_base_table; end
def reset_main; end
end
module GeneratedAssociationRelationMethods

View File

@@ -416,32 +416,32 @@ class DomainUsersInkbunnyAux
end
module GeneratedAssociationMethods
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def build_main(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def create_main(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def create_main!(*args, &blk); end
sig { returns(T.nilable(::Domain::User::InkbunnyUser)) }
def base_table; end
def main; end
sig { params(value: T.nilable(::Domain::User::InkbunnyUser)).void }
def base_table=(value); end
def main=(value); end
sig { returns(T::Boolean) }
def base_table_changed?; end
def main_changed?; end
sig { returns(T::Boolean) }
def base_table_previously_changed?; end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def build_base_table(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def create_base_table(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::User::InkbunnyUser) }
def create_base_table!(*args, &blk); end
def main_previously_changed?; end
sig { returns(T.nilable(::Domain::User::InkbunnyUser)) }
def reload_base_table; end
def reload_main; end
sig { void }
def reset_base_table; end
def reset_main; end
end
module GeneratedAssociationRelationMethods

View File

@@ -6,7 +6,6 @@
class FlatSstEntry
include GeneratedAttributeMethods
extend CommonRelationMethods
extend GeneratedRelationMethods
@@ -552,179 +551,6 @@ class FlatSstEntry
def without_count; end
end
module GeneratedAttributeMethods
sig { returns(T.nilable(::String)) }
def contents; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def contents=(value); end
sig { returns(T::Boolean) }
def contents?; end
sig { returns(T.nilable(::String)) }
def contents_before_last_save; end
sig { returns(T.untyped) }
def contents_before_type_cast; end
sig { returns(T::Boolean) }
def contents_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def contents_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def contents_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def contents_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def contents_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def contents_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def contents_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def contents_previously_was; end
sig { returns(T.nilable(::String)) }
def contents_was; end
sig { void }
def contents_will_change!; end
sig { returns(T.nilable(::String)) }
def id; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def id=(value); end
sig { returns(T::Boolean) }
def id?; end
sig { returns(T.nilable(::String)) }
def id_before_last_save; end
sig { returns(T.untyped) }
def id_before_type_cast; end
sig { returns(T::Boolean) }
def id_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def id_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def id_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def id_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def id_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def id_previously_was; end
sig { returns(T.nilable(::String)) }
def id_was; end
sig { void }
def id_will_change!; end
sig { returns(T.nilable(::String)) }
def key; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def key=(value); end
sig { returns(T::Boolean) }
def key?; end
sig { returns(T.nilable(::String)) }
def key_before_last_save; end
sig { returns(T.untyped) }
def key_before_type_cast; end
sig { returns(T::Boolean) }
def key_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def key_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def key_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def key_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def key_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def key_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def key_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def key_previously_was; end
sig { returns(T.nilable(::String)) }
def key_was; end
sig { void }
def key_will_change!; end
sig { void }
def restore_contents!; end
sig { void }
def restore_id!; end
sig { void }
def restore_key!; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_contents; end
sig { returns(T::Boolean) }
def saved_change_to_contents?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_id; end
sig { returns(T::Boolean) }
def saved_change_to_id?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_key; end
sig { returns(T::Boolean) }
def saved_change_to_key?; end
sig { returns(T::Boolean) }
def will_save_change_to_contents?; end
sig { returns(T::Boolean) }
def will_save_change_to_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_key?; end
end
module GeneratedRelationMethods
sig { returns(PrivateRelation) }
def all; end

View File

@@ -397,6 +397,18 @@ class HttpLogEntry
end
module EnumMethodsModule
sig { void }
def performed_by_airvpn_1_netherlands!; end
sig { returns(T::Boolean) }
def performed_by_airvpn_1_netherlands?; end
sig { void }
def performed_by_airvpn_2_san_jose!; end
sig { returns(T::Boolean) }
def performed_by_airvpn_2_san_jose?; end
sig { void }
def performed_by_dedipath_1!; end
@@ -433,6 +445,12 @@ class HttpLogEntry
sig { returns(T::Boolean) }
def performed_by_serverhost_1?; end
sig { void }
def performed_by_tor_1!; end
sig { returns(T::Boolean) }
def performed_by_tor_1?; end
sig { void }
def verb_get!; end
@@ -643,6 +661,12 @@ class HttpLogEntry
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def none(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_performed_by_airvpn_1_netherlands(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_performed_by_airvpn_2_san_jose(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_performed_by_dedipath_1(*args, &blk); end
@@ -661,6 +685,9 @@ class HttpLogEntry
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_performed_by_serverhost_1(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_performed_by_tor_1(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_verb_get(*args, &blk); end
@@ -699,6 +726,12 @@ class HttpLogEntry
end
def per(num); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def performed_by_airvpn_1_netherlands(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def performed_by_airvpn_2_san_jose(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def performed_by_dedipath_1(*args, &blk); end
@@ -717,6 +750,9 @@ class HttpLogEntry
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def performed_by_serverhost_1(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def performed_by_tor_1(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def preload(*args, &blk); end
@@ -1998,6 +2034,12 @@ class HttpLogEntry
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def none(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_performed_by_airvpn_1_netherlands(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_performed_by_airvpn_2_san_jose(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_performed_by_dedipath_1(*args, &blk); end
@@ -2016,6 +2058,9 @@ class HttpLogEntry
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_performed_by_serverhost_1(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_performed_by_tor_1(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_verb_get(*args, &blk); end
@@ -2054,6 +2099,12 @@ class HttpLogEntry
end
def per(num); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def performed_by_airvpn_1_netherlands(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def performed_by_airvpn_2_san_jose(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def performed_by_dedipath_1(*args, &blk); end
@@ -2072,6 +2123,9 @@ class HttpLogEntry
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def performed_by_serverhost_1(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def performed_by_tor_1(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def preload(*args, &blk); end

View File

@@ -43,6 +43,7 @@ class Rails::ApplicationController
include ::Domain::Users::FaUsersHelper
include ::Domain::VisualSearchHelper
include ::DomainSourceHelper
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TimestampHelper

View File

@@ -43,6 +43,7 @@ class Rails::Conductor::BaseController
include ::Domain::Users::FaUsersHelper
include ::Domain::VisualSearchHelper
include ::DomainSourceHelper
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TimestampHelper

View File

@@ -43,6 +43,7 @@ class Rails::HealthController
include ::Domain::Users::FaUsersHelper
include ::Domain::VisualSearchHelper
include ::DomainSourceHelper
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TimestampHelper

View File

@@ -0,0 +1,16 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Scraper::FuzzysearchApiClient`.
# Please instead update this file by running `bin/tapioca dsl Scraper::FuzzysearchApiClient`.
class Scraper::FuzzysearchApiClient
sig { returns(ColorLogger) }
def logger; end
class << self
sig { returns(ColorLogger) }
def logger; end
end
end

View File

@@ -0,0 +1,16 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Scraper::FuzzysearchHttpClientConfig`.
# Please instead update this file by running `bin/tapioca dsl Scraper::FuzzysearchHttpClientConfig`.
class Scraper::FuzzysearchHttpClientConfig
sig { returns(ColorLogger) }
def logger; end
class << self
sig { returns(ColorLogger) }
def logger; end
end
end

View File

@@ -0,0 +1,16 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Scraper::TorHttpClientConfig`.
# Please instead update this file by running `bin/tapioca dsl Scraper::TorHttpClientConfig`.
class Scraper::TorHttpClientConfig
sig { returns(ColorLogger) }
def logger; end
class << self
sig { returns(ColorLogger) }
def logger; end
end
end

View File

@@ -0,0 +1,16 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Tasks::Fa::QueryMissingPostsFromFuzzysearch`.
# Please instead update this file by running `bin/tapioca dsl Tasks::Fa::QueryMissingPostsFromFuzzysearch`.
class Tasks::Fa::QueryMissingPostsFromFuzzysearch
sig { returns(ColorLogger) }
def logger; end
class << self
sig { returns(ColorLogger) }
def logger; end
end
end

View File

@@ -16,7 +16,7 @@ FactoryBot.define do
post_file.log_entry =
create(
:http_log_entry,
url_str: post_file.url_str,
uri_str: post_file.url_str,
status_code: post_file.last_status_code,
)
end

View File

@@ -13,6 +13,7 @@ RSpec.describe FaUriHelper do
original_file_posted: 1_740_700_581,
latest_file_posted: 1_740_700_581,
filename: "zzreg_stippling-crop.jpg",
filename_with_ts: "1740700581.zzreg_stippling-crop.jpg",
),
)
expect(parsed.original_file_posted_at).to eq(Time.at(1_740_700_581))
@@ -29,10 +30,27 @@ RSpec.describe FaUriHelper do
original_file_posted: 1_740_700_581,
latest_file_posted: 1_753_374_875,
filename: "zzreg_stippling-crop.jpg",
filename_with_ts: "1740700581.zzreg_stippling-crop.jpg",
),
)
expect(parsed.original_file_posted_at).to eq(Time.at(1_740_700_581))
expect(parsed.latest_file_posted_at).to eq(Time.at(1_753_374_875))
end
it "parses story uris" do
url =
"https://d.furaffinity.net/art/irontankris/stories/1753207806/1753207806.thumbnail.irontankris_royal_rivalry_gains.rtf.jpg"
parsed = described_class.parse_fa_media_url(url)
expect(parsed).to eq(
FaUriHelper::FaMediaUrlInfo.new(
url_name: "irontankris",
original_file_posted: 1_753_207_806,
latest_file_posted: 1_753_207_806,
filename: "thumbnail.irontankris_royal_rivalry_gains.rtf.jpg",
filename_with_ts:
"1753207806.thumbnail.irontankris_royal_rivalry_gains.rtf.jpg",
),
)
end
end
end

View File

@@ -0,0 +1,198 @@
# typed: false
require "rails_helper"
describe Domain::Fa::Job::ScanFuzzysearchJob do
let(:http_client_mock) { instance_double("::Scraper::HttpClient") }
before { Scraper::ClientFactory.http_client_mock = http_client_mock }
let(:client_mock_config) { [] }
let!(:log_entries) do
HttpClientMockHelpers.init_http_client_mock(
http_client_mock,
client_mock_config,
)
end
let(:fuzzysearch_response_51015903) do
JSON.parse(File.read("test/fixtures/files/fuzzysearch/51015903.json"))
end
let(:fuzzysearch_response_21275696) do
JSON.parse(File.read("test/fixtures/files/fuzzysearch/21275696.json"))
end
let(:fuzzysearch_response_53068507) do
JSON.parse(File.read("test/fixtures/files/fuzzysearch/53068507.json"))
end
let(:fuzzysearch_response_61665194) do
JSON.parse(File.read("test/fixtures/files/fuzzysearch/61665194.json"))
end
describe "post was marked removed" do
let(:post) do
create(
:domain_post_fa_post,
state: "removed",
keywords: nil,
fa_id: fa_id,
)
end
context "and fuzzysearch has post info" do
let(:fa_id) { 51_015_903 }
let(:client_mock_config) do
[
{
uri:
"https://api-next.fuzzysearch.net/v1/file/furaffinity?search=#{fa_id}",
status_code: 200,
content_type: "application/json",
contents: fuzzysearch_response_51015903.to_json,
},
]
end
it "updates the post" do
perform_now({ post: })
post.reload
expect(post.fuzzysearch_checked_at).to be_present
expect(post.fuzzysearch_entry).to be_present
expect(post.fuzzysearch_json).to be_present
end
it "sets tags" do
perform_now({ post: })
post.reload
expect(post.keywords).to eq(%w[some_tag another_tag])
end
it "sets file" do
perform_now({ post: })
post.reload
expect(post.file).to be_present
expect(post.file.url_str).to eq(
"https://d.furaffinity.net/art/crimetxt/1676417528/1676417528.crimetxt_2023-02-15_00_18_48.png",
)
end
it "sets the creator" do
perform_now({ post: })
post.reload
expect(post.creator).to be_present
expect(post.creator.url_name).to eq("crimetxt")
end
it "enqueues a fur archiver post file job" do
perform_now({ post: })
post.reload
job_args = SpecUtil.enqueued_job_args(Job::FaPostFurArchiverPostFileJob)
expect(job_args).to match(
[{ post_file: post.file, caused_by_entry: post.fuzzysearch_entry }],
)
end
context "the post file is already downloaded" do
before do
post.file = create(:domain_post_file, :has_file)
post.save!
end
it "does not enqueue a fur archiver post file job" do
perform_now({ post: })
post.reload
job_args =
SpecUtil.enqueued_job_args(Job::FaPostFurArchiverPostFileJob)
expect(job_args).to be_empty
end
end
end
context "and fuzzysearch has no post info" do
let(:fa_id) { 21_275_696 }
let(:client_mock_config) do
[
{
uri:
"https://api-next.fuzzysearch.net/v1/file/furaffinity?search=#{fa_id}",
status_code: 200,
content_type: "application/json",
contents: fuzzysearch_response_21275696.to_json,
},
]
end
it "does not set the creator" do
perform_now({ post: })
post.reload
expect(post.creator).to be_nil
end
it "does not create a file" do
perform_now({ post: })
post.reload
expect(post.file).to be_nil
end
end
context "and the artist name has capitalizations" do
let(:fa_id) { 53_068_507 }
let(:client_mock_config) do
[
{
uri:
"https://api-next.fuzzysearch.net/v1/file/furaffinity?search=#{fa_id}",
status_code: 200,
content_type: "application/json",
contents: fuzzysearch_response_53068507.to_json,
},
]
end
it "sets the creator" do
perform_now({ post: })
post.reload
expect(post.creator).to be_present
expect(post.creator.url_name).to eq("meesh")
expect(post.creator.name).to eq("Meesh")
expect(post.creator.full_name).to eq("Meesh")
end
end
context "and the post has a story url" do
let(:fa_id) { 61_665_194 }
let(:client_mock_config) do
[
{
uri:
"https://api-next.fuzzysearch.net/v1/file/furaffinity?search=#{fa_id}",
status_code: 200,
content_type: "application/json",
contents: fuzzysearch_response_61665194.to_json,
},
]
end
it "does not change the post state" do
perform_now({ post: })
post.reload
expect(post.state).to eq("removed")
end
it "sets the artist" do
perform_now({ post: })
post.reload
expect(post.creator).to be_present
expect(post.creator.url_name).to eq("irontankris")
end
it "updates keywords", quiet: false do
post.keywords = []
post.save!
perform_now({ post: })
post.reload
expect(post.keywords).to include("female", "mlp", "little", "anthro")
end
end
end
end

View File

@@ -78,7 +78,7 @@ RSpec.describe Job::FaPostFurArchiverPostFileJob do
it "downloads the file from fur archiver" do
expect do
perform_now({ post_file: post_file })
perform_now({ post: post })
post.reload
end.to change { post.file&.log_entry }.from(old_log_entry).to(
have_attributes(uri: have_attributes(to_s: fur_archiver_url_str)),
@@ -87,7 +87,7 @@ RSpec.describe Job::FaPostFurArchiverPostFileJob do
it "updates the post_file blob" do
expect do
perform_now({ post_file: post_file })
perform_now({ post: post })
post.reload
end.to change { post.file&.blob }.from(old_log_entry.response).to(
@log_entries[0].response,
@@ -96,17 +96,24 @@ RSpec.describe Job::FaPostFurArchiverPostFileJob do
it "sets the last status code" do
expect do
perform_now({ post_file: post_file })
perform_now({ post: post })
post.reload
end.to change { post.file&.last_status_code }.from(404).to(200)
end
it "sets the post_file state to ok" do
expect do
perform_now({ post_file: post_file })
perform_now({ post: post })
post.reload
end.to change { post.file&.state }.from("terminal_error").to("ok")
end
it "does not perform the request twice" do
perform_now({ post: post })
perform_now({ post: post })
post.reload
expect(post.files.length).to eq(2)
end
end
context "with a d.facdn.net url" do
@@ -122,5 +129,58 @@ RSpec.describe Job::FaPostFurArchiverPostFileJob do
end
include_examples "correct behavior"
end
context "when furarchiver returns 404" do
let(:client_mock_config) do
[
{
uri: fur_archiver_url_str,
status_code: 404,
content_type: "text/html",
contents: "not found",
},
{
uri:
"http://g6jy5jkx466lrqojcngbnksugrcfxsl562bzuikrka5rv7srgguqbjid.onion/fa/wolfsparta/1496842943.wolfsparta_caught_pt2.png",
status_code: 200,
content_type: "image/png",
contents: image_data,
caused_by_entry_idx: 0,
},
]
end
let(:file_url_str) do
"https://d.facdn.net/art/wolfsparta/1496842943/1496842943.wolfsparta_caught_pt2.png"
end
it "creates a tor post file" do
perform_now({ post: post })
post.reload
# should have original post file, the fur archiver post file, and the tor post file
expect(post.files.count).to eq(3)
original, furarchiver, tor = post.files.sort_by(&:id)
expect(original.state).to eq("terminal_error")
expect(furarchiver.state).to eq("terminal_error")
expect(tor.state).to eq("ok")
expect(tor.url_str).to eq(
"http://g6jy5jkx466lrqojcngbnksugrcfxsl562bzuikrka5rv7srgguqbjid.onion/fa/wolfsparta/1496842943.wolfsparta_caught_pt2.png",
)
end
it "does not perform the request twice" do
perform_now({ post: post })
perform_now({ post: post })
post.reload
expect(post.files.length).to eq(3)
end
it "enqueues the thumbnail job" do
perform_now({ post: post })
job_args = SpecUtil.enqueued_job_args(Domain::PostFileThumbnailJob)
expect(job_args).to eq(
[{ post_file: post.files.last, caused_by_entry: @log_entries[0] }],
)
end
end
end
end

View File

@@ -0,0 +1,18 @@
[
{
"id": 21275696,
"file_id": null,
"artist": null,
"hash": null,
"hash_str": null,
"url": null,
"filename": null,
"rating": null,
"posted_at": null,
"file_size": null,
"sha256": null,
"updated_at": "2023-08-16T08:02:27.746944Z",
"deleted": true,
"tags": []
}
]

View File

@@ -0,0 +1,18 @@
[
{
"id": 51015903,
"file_id": 1676417528,
"artist": "crimetxt",
"hash": 2367248181756250600,
"hash_str": "2367248181756250660",
"url": "https://d.furaffinity.net/art/crimetxt/1676417528/1676417528.crimetxt_2023-02-15_00_18_48.png",
"filename": "1676417528.crimetxt_2023-02-15_00_18_48.png",
"rating": "adult",
"posted_at": "2023-02-14T23:32:00Z",
"file_size": 2188273,
"sha256": "d488dabd8eb22398a228fb662eb520bb4daaac3a9ab0dc9be8b8c5e1b9522efb",
"updated_at": null,
"deleted": false,
"tags": ["some_tag", "another_tag"]
}
]

View File

@@ -0,0 +1,25 @@
[
{
"id": 53068507,
"file_id": 1690504099,
"artist": "Meesh",
"hash": -5278701664616650000,
"hash_str": "-5278701664616649812",
"url": "https://d.furaffinity.net/art/meesh/1690504135/1690504099.meesh_pamperingjack3_crop.png",
"filename": "1690504099.meesh_pamperingjack3_crop.png",
"rating": "adult",
"posted_at": "2023-07-28T00:28:00Z",
"file_size": 555926,
"sha256": "f47e974ef7e72c53fce5a52b28b7c34576eb26af04299155d1cc3912379b0dea",
"updated_at": null,
"deleted": false,
"tags": [
"advertisement",
"patreon",
"female",
"preview_limited",
"beastars",
"juno"
]
}
]

View File

@@ -0,0 +1,34 @@
[
{
"id": 61665194,
"file_id": 1753207806,
"artist": "irontankris",
"hash": 3088256223167493600,
"hash_str": "3088256223167493676",
"url": "https://d.furaffinity.net/art/irontankris/stories/1753207806/1753207806.thumbnail.irontankris_royal_rivalry_gains.rtf.jpg",
"filename": "1753207806.thumbnail.irontankris_royal_rivalry_gains.rtf.jpg",
"rating": "adult",
"posted_at": "2025-07-22T18:10:00Z",
"file_size": 10283,
"sha256": "3809e988c41506211bd2885061646432e021b3810afc9080d244dd63a84a783e",
"updated_at": null,
"deleted": false,
"tags": [
"female",
"mlp",
"little",
"pony",
"sex",
"lesbian",
"weight",
"gain",
"wg",
"incest",
"princess_celestia",
"princess_luna",
"celestia",
"luna",
"anthro"
]
}
]