create multiple fingerprints wip

This commit is contained in:
Dylan Knutson
2025-03-09 20:33:49 +00:00
parent 87e1d50ae2
commit 70fb486cff
70 changed files with 4641 additions and 812 deletions

View File

@@ -503,3 +503,39 @@ task clear_e621_user_favs_migrated_at: :environment do
# pb.progress += b.size
# end
end
task create_post_file_fingerprints: :environment do
def migrate_posts_for_user(user)
puts "migrating posts for #{user.to_param}"
pb =
ProgressBar.create(
total: user.posts.count,
format: "%t: %c/%C %B %p%% %a %e",
)
user
.posts
.includes(files: :blob)
.find_in_batches(batch_size: 16) do |batch|
ReduxApplicationRecord.transaction do
batch.each do |post|
post.files.each { |file| file.ensure_fingerprint! }
puts "migrated #{post.id} / #{post.to_param} / '#{post.title_for_view}'"
pb.progress = [pb.progress + 1, pb.total].min
end
end
end
end
if ENV["user"].present?
for_user = ENV["user"] || raise("need 'user'")
user = DomainController.find_model_from_param(Domain::User, for_user)
raise "user '#{for_user}' not found" unless user
migrate_posts_for_user(user)
elsif ENV["users_descending"].present?
# all users with posts, ordered by post count descending
users = Domain::User::FaUser.order(num_watched_by: :desc).limit(20)
users.find_each(batch_size: 1) { |user| migrate_posts_for_user(user) }
else
raise "need 'user' or 'users_descending'"
end
end

View File

@@ -38,3 +38,4 @@
- [ ] fix for IDs that have a dot in them - e.g. https://refurrer.com/users/fa@jakke.
- [ ] Rich inline links to e621 e.g. https://refurrer.com/posts/fa@60070060
- [ ] Find FaPost that have favs recorded but no scan / file, enqueue scan
- [ ] Bunch of posts with empty responses: posts = Domain::Post.joins(files: :log_entry).where(files: { http_log_entries: { response_sha256: BlobFile::EMPTY_FILE_SHA256 }}).limit(10)

View File

@@ -205,8 +205,6 @@ class BlobEntriesController < ApplicationController
[800, 600]
when "content-container"
[768, 2048]
# [2048, 2048]
# [128, 128]
end
end

View File

@@ -122,7 +122,7 @@ class Domain::PostsController < DomainController
return false unless content_type
ret =
Domain::PostFileFingerprint::VALID_CONTENT_TYPES.any? do |type|
Domain::PostFile::Thumbnail::THUMBABLE_CONTENT_TYPES.any? do |type|
content_type.match?(type)
end
@@ -231,16 +231,16 @@ class Domain::PostsController < DomainController
sig { params(image_path: String).returns(String) }
def generate_fingerprint(image_path)
# Use the new from_file_path method to create a fingerprint
fingerprint = Domain::PostFileFingerprint.from_file_path(image_path)
# The hash_value is guaranteed to be present by the from_file_path implementation
T.must(fingerprint.hash_value)
Domain::PostFile::BitFingerprint.from_file_path(image_path)
end
# Find similar images based on the fingerprint
sig { params(hash_value: String).returns(ActiveRecord::Relation) }
def find_similar_fingerprints(hash_value)
sig { params(fingerprint_value: String).returns(ActiveRecord::Relation) }
def find_similar_fingerprints(fingerprint_value)
# Use the model's similar_to_fingerprint method directly
Domain::PostFileFingerprint.similar_to_fingerprint(hash_value).limit(10)
Domain::PostFile::BitFingerprint.order_by_fingerprint_distance(
fingerprint_value,
).limit(10)
end
sig { override.returns(DomainController::DomainParamConfig) }

View File

@@ -53,44 +53,21 @@ class DomainController < ApplicationController
) || raise(ActiveRecord::RecordNotFound)
end
public
sig(:final) do
type_parameters(:Klass)
.params(
klass:
T.all(
T.class_of(ReduxApplicationRecord),
HasCompositeToParam::ClassMethods,
T::Class[T.type_parameter(:Klass)],
HasCompositeToParam::ClassMethods[T.type_parameter(:Klass)],
),
param: T.nilable(String),
)
.returns(T.nilable(T.type_parameter(:Klass)))
end
def self.find_model_from_param(klass, param)
composite_param = HasCompositeToParam.parse_composite_param(param)
if composite_param.nil?
raise ActionController::BadRequest, "invalid id: #{param.inspect}"
end
klass_param, id = composite_param
group_klass =
klass.subclasses.find do |subclass|
param_prefix, _ =
T.cast(
subclass,
HasCompositeToParam::ClassMethods,
).param_prefix_and_attribute
param_prefix == klass_param
end
if group_klass.nil?
raise ActionController::BadRequest, "unknown model type: #{klass_param}"
end
_, param_attribute =
T.cast(
group_klass,
HasCompositeToParam::ClassMethods,
).param_prefix_and_attribute
group_klass.find_by(param_attribute => id)
klass.find_by_param(param)
end
end

View File

@@ -287,6 +287,7 @@ module Domain::PostsHelper
hosts: FA_HOSTS,
patterns: [
%r{/view/(\d+)/?},
%r{/full/(\d+)/?},
%r{/controls/submissions/changeinfo/(\d+)/?},
],
find_proc: ->(helper, match, _) do

View File

@@ -26,7 +26,10 @@ export const getPreviewContainerClassName = (
maxWidth: string,
maxHeight: string,
) => {
// calculate if the page is mobile
const isMobile = window.innerWidth < 640;
return [
isMobile ? 'hidden' : '',
`max-w-[${maxWidth}] max-h-[${maxHeight}] rounded-lg`,
'border border-slate-400 bg-slate-100',
'divide-y divide-slate-300',

View File

@@ -11,7 +11,7 @@ class Domain::Fa::Job::Base < Scraper::JobBase
protected
BUGGY_USER_URL_NAMES = T.let(["click here", ".."], T::Array[String])
BUGGY_USER_URL_NAMES = T.let(["click here", "..", "."], T::Array[String])
sig { params(user: Domain::User::FaUser).returns(T::Boolean) }
def buggy_user?(user)

View File

@@ -0,0 +1,16 @@
# typed: strict
class Domain::PostFileThumbnailJob < Scraper::JobBase
queue_as :thumbnails
sig { override.returns(Symbol) }
def self.http_factory_method
raise NotImplementedError
end
sig { override.params(args: T::Hash[Symbol, T.untyped]).void }
def perform(args)
post_file = T.cast(args[:post_file], Domain::PostFile)
Domain::PostFile::Thumbnail.create_for_post_file!(post_file)
Domain::PostFile::BitFingerprint.create_for_post_file!(post_file)
end
end

View File

@@ -1,35 +1,22 @@
# typed: false
# typed: true
class Domain::Fa::PostEnqueuer
include HasBulkEnqueueJobs
include HasColorLogger
include HasMeasureDuration
include Domain::Fa::HasCountFailedInQueue
def initialize(
reverse_scan_holes:,
start_at:,
low_water_mark:,
high_water_mark:
)
def initialize(start_at:, stop_at:, low_water_mark:, high_water_mark:)
stop_at ||= 0
@low_water_mark = low_water_mark
@high_water_mark = high_water_mark
raise if @high_water_mark <= @low_water_mark
@post_iterator =
Enumerator.new do |e|
if reverse_scan_holes
while start_at > 0
if !Domain::Post::FaPost.exists?(fa_id: start_at)
e << [nil, start_at, nil]
end
start_at -= 1
while start_at > stop_at
if !Domain::Post::FaPost.exists?(fa_id: start_at)
e << [nil, start_at, nil]
end
else
Domain::Post::FaPost
.where("id >= ?", start_at)
.where
.missing(:file)
.where(state: "ok")
.find_each { |p| e << [p.id, p.fa_id, p.file_url_str] }
start_at -= 1
end
end
end
@@ -42,11 +29,7 @@ class Domain::Fa::PostEnqueuer
"enqueuing #{to_enqueue.to_s.bold} more posts - #{already_enqueued.to_s.bold} already enqueued",
)
rows =
measure(
proc do |p|
p && "gathered #{p.length.to_s.bold} posts" || "gathering posts..."
end,
) do
measure("gathering posts...") do
to_enqueue
.times
.map do

View File

@@ -35,6 +35,11 @@ class BlobFile < ReduxApplicationRecord
enum :version, { v1: 1 }
after_initialize { self.version ||= :v1 }
has_many :phash_thumbnails,
class_name: "Phash::Thumbnail",
foreign_key: :blob_sha256,
primary_key: :sha256
validates_presence_of(:sha256, :content_type, :size_bytes)
validates :sha256, length: { is: 32 }
validates :content_bytes,

View File

@@ -4,27 +4,60 @@ module HasCompositeToParam
extend T::Helpers
abstract!
requires_ancestor { Kernel }
requires_ancestor { ReduxApplicationRecord }
sig(:final) { returns(T.nilable(String)) }
def to_param
klass, param = self.class.param_prefix_and_attribute
klass, param = T.unsafe(self.class).param_prefix_and_attribute
param_value = send(param)
param_value.present? ? "#{klass}@#{param_value}" : nil
end
sig { params(param: T.nilable(String)).returns(T.nilable([String, String])) }
def self.parse_composite_param(param)
return nil unless param.present?
klass, param_value = param.split("@", 2)
return nil unless klass.present? && param_value.present?
[klass, param_value]
end
module ClassMethods
extend T::Sig
extend T::Helpers
extend T::Generic
requires_ancestor { Kernel }
has_attached_class!
abstract!
sig(:final) do
params(param: T.nilable(String)).returns(T.nilable([String, String]))
end
def parse_composite_param(param)
return nil unless param.present?
klass, param_value = param.split("@", 2)
return nil unless klass.present? && param_value.present?
[klass, param_value]
end
sig(:final) do
params(param: T.nilable(String)).returns(T.nilable(T.attached_class))
end
def find_by_param(param)
composite_param = parse_composite_param(param)
if composite_param.nil?
raise ActionController::BadRequest, "invalid id: #{param.inspect}"
end
klass_param, id = composite_param
klass = self
group_klass =
T
.unsafe(klass)
.subclasses
.find do |subclass|
param_prefix, _ = subclass.param_prefix_and_attribute
param_prefix == klass_param
end
if group_klass.nil?
raise ActionController::BadRequest, "unknown model type: #{klass_param}"
end
_, param_attribute = group_klass.param_prefix_and_attribute
group_klass.find_by(param_attribute => id)
end
sig { abstract.returns([String, Symbol]) }
def param_prefix_and_attribute
end

View File

@@ -10,12 +10,15 @@ class Domain::PostFile < ReduxApplicationRecord
optional: true,
foreign_key: :blob_sha256
has_one :fingerprint,
class_name: "::Domain::PostFileFingerprint",
foreign_key: :blob_sha256,
primary_key: :blob_sha256,
dependent: :destroy,
inverse_of: :post_file
has_many :bit_fingerprints,
class_name: "::Domain::PostFile::BitFingerprint",
dependent: :destroy,
inverse_of: :post_file
has_many :thumbnails,
class_name: "::Domain::PostFile::Thumbnail",
dependent: :destroy,
inverse_of: :post_file
attr_json :state, :string
attr_json :url_str, :string
@@ -41,19 +44,27 @@ class Domain::PostFile < ReduxApplicationRecord
self.type ||= self.class.name if new_record?
end
after_save do
if self.fingerprint.nil? && (blob = self.blob) &&
(content_type = blob.content_type) &&
(
Domain::PostFileFingerprint::VALID_CONTENT_TYPES.any? do |type|
content_type.match?(type)
end
)
fingerprint = Domain::PostFileFingerprint.from_post_file(self)
fingerprint&.save!
end
rescue => e
logger.error("could not save fingerprint for post_file #{self.id}: #{e}")
before_save { self.blob_sha256 ||= self.log_entry&.response&.sha256 }
sig { returns(T.nilable(String)) }
def content_type
return nil unless log_entry = self.log_entry
return nil unless response = log_entry.response
response.content_type ||
begin
return nil unless blob = self.blob
blob.content_type
end
end
sig { returns(T.nilable(String)) }
def sha256
self.blob_sha256 ||
begin
return nil unless log_entry = self.log_entry
return nil unless response = log_entry.response
response.sha256
end
end
sig { returns(T.nilable(BlobFile)) }

View File

@@ -0,0 +1,132 @@
# typed: strict
class Domain::PostFile::BitFingerprint < ReduxApplicationRecord
include AttrJsonRecordAliases
self.table_name = "domain_post_file_bit_fingerprints"
belongs_to :post_file,
class_name: "::Domain::PostFile",
inverse_of: :bit_fingerprints
belongs_to :thumbnail, class_name: "::Domain::PostFile::Thumbnail"
validates :fingerprint_value, presence: true
# in bytes
HASH_SIZE_BYTES = 32
# Find similar images based on the fingerprint
sig { params(fingerprint: String).returns(ActiveRecord::Relation) }
def self.order_by_fingerprint_distance(fingerprint)
order(
Arel.sql(
"(fingerprint_value <~> '#{ActiveRecord::Base.connection.quote_string(fingerprint)}') ASC",
),
)
end
sig do
params(post_file: Domain::PostFile).returns(
T::Array[Domain::PostFile::BitFingerprint],
)
end
def self.create_for_post_file!(post_file)
logger.tagged("fingerprint", make_arg_tag(post_file)) do
thumbnails = post_file.thumbnails.where(thumb_type: :size_32_32).to_a
existing_bit_fingerprints = post_file.bit_fingerprints.to_a
thumbnails_missing_fingerprints =
thumbnails.reject do |thumbnail|
existing_bit_fingerprints.any? do |existing_bit_fingerprint|
existing_bit_fingerprint.thumbnail_id == thumbnail.id
end
end
logger.info(
format_tags(
make_tag(
"thumbnails_missing_fingerprints",
thumbnails_missing_fingerprints.map(&:id),
),
),
)
thumbnails_missing_fingerprints
.map do |thumbnail|
logger.tagged(make_tag("thumbnail", thumbnail.id)) do
unless thumbnail_path = thumbnail.absolute_file_path
logger.info(format_tags("unable to compute thumbnail path"))
next
end
fingerprint_value = from_file_path(thumbnail_path)
fingerprint =
post_file.bit_fingerprints.build(thumbnail:, fingerprint_value:)
logger.info(
format_tags(
"computed fingerprint #{fingerprint_value.to_i(2).to_s(16).upcase}",
),
)
fingerprint.save!
fingerprint
end
end
.compact
end
end
# Calculate the Hamming distance between this fingerprint and another fingerprint
# @param other_fingerprint [Domain::PostFileFingerprint] The fingerprint to compare with
# @return [Integer, nil] The Hamming distance (number of differing bits) or nil if either fingerprint is invalid
sig do
params(
other_fingerprint: T.nilable(Domain::PostFile::BitFingerprint),
).returns(T.nilable(Integer))
end
def hamming_distance_to(other_fingerprint)
return nil unless (this_hash = self.fingerprint_value)
return nil unless (other_hash = other_fingerprint&.fingerprint_value)
self.class.hamming_distance(this_hash, other_hash)
end
# Calculate the Hamming distance between two hash values
# @param hash_value1 [String] The first hash value
# @param hash_value2 [String] The second hash value
# @return [Integer, nil] The Hamming distance (number of differing bits) or nil if either hash value is invalid
sig do
params(hash_value1: String, hash_value2: String).returns(T.nilable(Integer))
end
def self.hamming_distance(hash_value1, hash_value2)
hash_value1.chars.zip(hash_value2.chars).count { |c1, c2| c1 != c2 }
end
# Calculate the similarity percentage between this fingerprint and another fingerprint
# @param other_fingerprint [Domain::PostFileFingerprint] The fingerprint to compare with
# @return [Float, nil] The similarity percentage between 0 and 100 or nil if either fingerprint is invalid
sig do
params(
other_fingerprint: T.nilable(Domain::PostFile::BitFingerprint),
).returns(T.nilable(Float))
end
def similarity_percentage_to(other_fingerprint)
return nil unless (distance = hamming_distance_to(other_fingerprint))
# Maximum possible distance for a 256-bit hash
max_distance = HASH_SIZE_BYTES * 8
# Calculate similarity percentage based on distance
result = ((max_distance - distance) / max_distance.to_f * 100).round(1)
# Ensure the return type is Float
Float(result)
end
# Create a PostFileFingerprint instance from a file path
# @param file_path [String] Path to the image file
# @return [Domain::PostFileFingerprint] A non-persisted fingerprint model
sig { params(file_path: String).returns(String) }
def self.from_file_path(file_path)
unless File.exist?(file_path)
raise ArgumentError, "File does not exist: #{file_path}"
end
fingerprint = DHashVips::IDHash.fingerprint(file_path)
fingerprint.to_s(2).rjust(HASH_SIZE_BYTES * 8, "0")
end
end

View File

@@ -0,0 +1,8 @@
# typed: strict
class Domain::PostFile::FingerprintJoin < ReduxApplicationRecord
self.table_name = "domain_post_file_fingerprint_joins"
belongs_to :thumbnail, class_name: "::Domain::PostFile::Thumbnail"
belongs_to :post_file, class_name: "::Domain::PostFile"
belongs_to :fingerprint, polymorphic: true
end

View File

@@ -0,0 +1,209 @@
# typed: strict
class Domain::PostFile::Thumbnail < ReduxApplicationRecord
extend T::Sig
self.table_name = "domain_post_file_thumbnails"
belongs_to :post_file,
class_name: "::Domain::PostFile",
inverse_of: :thumbnails
enum :thumb_type, { content_container: 0, size_32_32: 1 }
validates :thumb_type, presence: true
validates :frame, presence: true
validate :validate_content_type!
THUMB_ROOT_DIR =
T.let(File.join(BlobFile::ROOT_DIR, "domain_post_file_thumbnails"), String)
THUMB_FILE_PATH_PATTERN = T.let([2, 2, 1].freeze, T::Array[Integer])
SINGLE_FRAME_CONTENT_TYPES =
T.let(
[%r{image/jpeg}, %r{image/jpg}, %r{image/png}, %r{image/bmp}],
T::Array[Regexp],
)
MULTI_FRAME_CONTENT_TYPES =
T.let(
[
%r{image/gif},
%r{image/apng},
%r{image/webp},
%r{video/webm},
%r{video/mp4},
],
T::Array[Regexp],
)
THUMBABLE_CONTENT_TYPES =
T.let(
SINGLE_FRAME_CONTENT_TYPES + MULTI_FRAME_CONTENT_TYPES,
T::Array[Regexp],
)
class ThumbnailOptions < T::ImmutableStruct
extend T::Sig
const :width, Integer
const :height, T.nilable(Integer)
# 0..100
const :quality, T.nilable(Integer)
# :both, :up, :down, :force
const :size, Symbol
const :interlace, T::Boolean
const :for_frames, T::Array[Numeric]
end
THUMB_TYPE_TO_OPTIONS =
T.let(
{
content_container:
ThumbnailOptions.new(
width: 768,
height: 2048,
quality: 95,
size: :down,
interlace: true,
for_frames: [0],
),
size_32_32:
ThumbnailOptions.new(
width: 32,
height: 32,
quality: 95,
size: :force,
interlace: false,
for_frames: [0.0, 0.1, 0.5, 0.9, 1.0],
),
},
T::Hash[Symbol, ThumbnailOptions],
)
sig { returns(T.nilable(String)) }
def absolute_file_path
return nil unless (thumb_type = self.thumb_type)
return nil unless (post_file = self.post_file)
return nil unless (sha256 = post_file.sha256)
sha256_hex = HexUtil.bin2hex(sha256)
[
THUMB_ROOT_DIR,
*BlobFile.path_segments(THUMB_FILE_PATH_PATTERN, sha256_hex),
].join("/") + ".jpg"
end
sig { params(post_file: Domain::PostFile).returns(T::Boolean) }
def self.can_thumbnail_post_file?(post_file)
return false unless content_type = post_file.content_type
can_thumbnail_content_type?(content_type)
end
sig { params(content_type: String).returns(T::Boolean) }
def self.can_thumbnail_content_type?(content_type)
THUMBABLE_CONTENT_TYPES.any? { |type| content_type.match?(type) }
end
sig do
params(post_file: Domain::PostFile).returns(
T::Array[Domain::PostFile::Thumbnail],
)
end
def self.create_for_post_file!(post_file)
logger.tagged("thumbnail", make_arg_tag(post_file)) do
return [] unless content_type = post_file.content_type
return [] unless can_thumbnail_content_type?(content_type)
blob_file_path = post_file.blob&.absolute_file_path
return [] unless blob_file_path
vips_image = Vips::Image.new_from_file(blob_file_path)
num_frames = frames_for_image(content_type, vips_image)
logger.info(format_tags(make_tag("num_frames", num_frames)))
return [] if num_frames.zero?
existing_thumb_types = post_file.thumbnails.to_a.map(&:thumb_type)
logger.info(
format_tags(make_tag("existing_thumb_types", existing_thumb_types)),
)
FileUtils.mkdir_p(BlobFile::TMP_DIR)
thumbnails = []
THUMB_TYPE_TO_OPTIONS.each do |thumb_type, options|
logger.tagged(make_tag("thumb_type", thumb_type)) do
next if existing_thumb_types.include?(thumb_type)
logger.info(format_tags("creating thumbnail"))
# get the number of frames in the post file
frame_numbers =
options
.for_frames
.map { |frame_fraction| (frame_fraction * (num_frames - 1)).to_i }
.uniq
.sort
frame_numbers.each do |frame|
logger.tagged(make_tag("frame", frame)) do
thumbnail = post_file.thumbnails.build(thumb_type:, frame:)
unless thumb_file_path = thumbnail.absolute_file_path
logger.info(format_tags("unable to compute thumbnail path"))
next
end
if File.exist?(thumb_file_path)
logger.info(format_tags("thumbnail file already exists"))
else
FileUtils.mkdir_p(File.dirname(thumb_file_path))
tmp_file_path =
File.join(
BlobFile::TMP_DIR,
"thumb-#{thumb_type}-#{SecureRandom.uuid}",
)
logger.info(format_tags("writing thumbnail file"))
# TODO - check that this works for gifs, mp4, etc
vips_image.thumbnail_image(
options.width,
height: options.height,
size: options.size,
).jpegsave(
tmp_file_path,
interlace: options.interlace,
Q: options.quality,
)
FileUtils.mv(tmp_file_path, thumb_file_path)
end
thumbnail.save!
thumbnails << thumbnail
end
end
end
end
thumbnails
end
end
sig { params(content_type: String, vips_image: Vips::Image).returns(Integer) }
def self.frames_for_image(content_type, vips_image)
case content_type
when %r{image/gif}
vips_image.get("n-pages")
when %r{video/webm}, %r{video/mp4}
vips_image.get("n-frames")
else
1
end
end
private
sig { void }
def validate_content_type!
return unless (post_file = self.post_file)
content_type = post_file.content_type
if content_type.nil?
errors.add(:post_file, "must have a content type")
return
end
unless self.class.can_thumbnail_content_type?(content_type)
errors.add(:post_file, "must be a thumbnailable content type")
end
end
end

View File

@@ -0,0 +1,13 @@
# typed: strict
class Domain::PostFile::VectorFingerprint < ReduxApplicationRecord
self.table_name = "domain_post_file_vector_fingerprints"
has_one :fingerprint_join,
class_name: "Domain::PostFile::FingerprintJoin",
foreign_key: :fingerprint_id,
inverse_of: :fingerprint
has_one :post_file, through: :fingerprint_join
belongs_to :thumbnail, class_name: "Domain::PostFile::Thumbnail"
end

View File

@@ -1,155 +0,0 @@
# typed: strict
class Domain::PostFileFingerprint < ReduxApplicationRecord
include AttrJsonRecordAliases
self.table_name = "domain_post_file_fingerprints"
belongs_to :post_file,
foreign_key: :blob_sha256,
primary_key: :blob_sha256,
class_name: "::Domain::PostFile",
inverse_of: :fingerprint
validates :hash_value, presence: true
# in bytes
HASH_SIZE = 32
VALID_CONTENT_TYPES =
T.let(
[
%r{image/jpeg},
%r{image/jpg},
%r{image/png},
%r{image/gif},
%r{image/webp},
],
T::Array[Regexp],
)
before_validation do |fingerprint|
next false if fingerprint.hash_value.present?
next false unless (path = post_file&.blob&.absolute_file_path)
next false unless File.exist?(path)
next false unless blob = post_file&.blob
next false unless content_type = blob.content_type
unless VALID_CONTENT_TYPES.any? { |type| content_type.match?(type) }
next false
end
fingerprint = DHashVips::IDHash.fingerprint(path)
self.hash_value = fingerprint.to_s(2).rjust(HASH_SIZE * 8, "0")
end
# Find similar images based on the fingerprint
sig { params(fingerprint: String).returns(ActiveRecord::Relation) }
def self.similar_to_fingerprint(fingerprint)
includes(post_file: :post).order(
Arel.sql(
"(hash_value <~> '#{ActiveRecord::Base.connection.quote_string(fingerprint)}') ASC",
),
)
end
# Calculate the Hamming distance between this fingerprint and another fingerprint
# @param other_fingerprint [Domain::PostFileFingerprint] The fingerprint to compare with
# @return [Integer, nil] The Hamming distance (number of differing bits) or nil if either fingerprint is invalid
sig do
params(other_fingerprint: T.nilable(Domain::PostFileFingerprint)).returns(
T.nilable(Integer),
)
end
def hamming_distance_to(other_fingerprint)
this_hash = hash_value
other_hash = other_fingerprint&.hash_value
return nil if this_hash.blank? || other_hash.blank?
self.class.hamming_distance(this_hash, other_hash)
end
# Calculate the Hamming distance between two hash values
# @param hash_value1 [String] The first hash value
# @param hash_value2 [String] The second hash value
# @return [Integer, nil] The Hamming distance (number of differing bits) or nil if either hash value is invalid
sig do
params(hash_value1: String, hash_value2: String).returns(T.nilable(Integer))
end
def self.hamming_distance(hash_value1, hash_value2)
hash_value1.chars.zip(hash_value2.chars).count { |c1, c2| c1 != c2 }
end
# Calculate the similarity percentage between this fingerprint and another fingerprint
# @param other_fingerprint [Domain::PostFileFingerprint] The fingerprint to compare with
# @return [Float, nil] The similarity percentage between 0 and 100 or nil if either fingerprint is invalid
sig do
params(other_fingerprint: T.nilable(Domain::PostFileFingerprint)).returns(
T.nilable(Float),
)
end
def similarity_percentage_to(other_fingerprint)
distance = hamming_distance_to(other_fingerprint)
return nil unless distance
# Maximum possible distance for a 256-bit hash
max_distance = HASH_SIZE * 8
# Calculate similarity percentage based on distance
result = ((max_distance - distance) / max_distance.to_f * 100).round(1)
# Ensure the return type is Float
Float(result)
end
sig do
params(post_file: Domain::PostFile).returns(
T.nilable(Domain::PostFileFingerprint),
)
end
def self.from_post_file(post_file)
blob_file_path = post_file.blob&.absolute_file_path
content_type = post_file.blob&.content_type
return nil unless blob_file_path
return nil unless content_type
unless VALID_CONTENT_TYPES.any? { |type| content_type.match?(type) }
return nil
end
model = from_file_path(blob_file_path)
model.post_file = post_file
model
end
# Create a PostFileFingerprint instance from a file path
# @param file_path [String] Path to the image file
# @return [Domain::PostFileFingerprint] A non-persisted fingerprint model
sig { params(file_path: String).returns(Domain::PostFileFingerprint) }
def self.from_file_path(file_path)
unless File.exist?(file_path)
raise ArgumentError, "File does not exist: #{file_path}"
end
fingerprint = DHashVips::IDHash.fingerprint(file_path)
from_dhash_fingerprint(fingerprint)
end
# Create a PostFileFingerprint instance from a Vips::Image
# @param vips_image [Vips::Image] Vips image object
# @return [Domain::PostFileFingerprint] A non-persisted fingerprint model
sig { params(vips_image: T.untyped).returns(Domain::PostFileFingerprint) }
def self.from_vips_image(vips_image)
# Generate fingerprint directly from the Vips::Image object
fingerprint = DHashVips::IDHash.fingerprint(vips_image)
from_dhash_fingerprint(fingerprint)
end
private
# Create a PostFileFingerprint instance from a DHashVips fingerprint
# @param fingerprint [Object] DHashVips fingerprint object
# @return [Domain::PostFileFingerprint] A non-persisted fingerprint model
sig { params(fingerprint: T.untyped).returns(Domain::PostFileFingerprint) }
def self.from_dhash_fingerprint(fingerprint)
# Convert the numeric fingerprint to a binary string and pad to the correct length
# HASH_SIZE = 32 (bytes) * 8 = 256 bits
hash_value = fingerprint.to_s(2).rjust(HASH_SIZE * 8, "0")
new_fingerprint = new
new_fingerprint.hash_value = hash_value
new_fingerprint
end
end

View File

@@ -21,7 +21,7 @@ class Domain::User::FaUser < Domain::User
attr_json_due_timestamp :scanned_page_at, 3.months
attr_json_due_timestamp :scanned_follows_at, 3.months
attr_json_due_timestamp :scanned_followed_by_at, 3.months
attr_json_due_timestamp :scanned_favs_at, 1.month
attr_json_due_timestamp :scanned_favs_at, 3.months
attr_json_due_timestamp :scanned_incremental_at, 1.month
attr_json :registered_at, :datetime
attr_json :migrated_followed_users_at, :datetime

View File

@@ -45,11 +45,21 @@ class ApplicationPolicy
user&.moderator? || false
end
sig(:final) { returns(T::Boolean) }
def is_role_at_least_moderator?
is_role_moderator? || is_role_admin?
end
sig(:final) { returns(T::Boolean) }
def is_role_user?
user&.user? || false
end
sig(:final) { returns(T::Boolean) }
def is_role_at_least_user?
is_role_user? || is_role_moderator? || is_role_admin?
end
sig(:final) { returns(T::Boolean) }
def is_real_user?
user&.is_a?(User) || false

View File

@@ -14,17 +14,17 @@ class Domain::PostPolicy < ApplicationPolicy
sig { returns(T::Boolean) }
def visual_search?
is_role_user?
is_role_at_least_user?
end
sig { returns(T::Boolean) }
def visual_results?
is_role_user?
is_role_at_least_user?
end
sig { returns(T::Boolean) }
def view_file?
is_role_admin? || is_role_moderator? || is_role_user?
is_role_at_least_user?
end
sig { returns(T::Boolean) }
@@ -34,12 +34,12 @@ class Domain::PostPolicy < ApplicationPolicy
sig { returns(T::Boolean) }
def users_faving_post?
is_role_admin? || is_role_moderator? || is_role_user?
is_role_at_least_user?
end
sig { returns(T::Boolean) }
def view_faved_by?
is_role_admin? || is_role_moderator? || is_role_user?
is_role_at_least_user?
end
sig { returns(T::Boolean) }

View File

@@ -1,10 +0,0 @@
class CreateDomainPostFileThumbnails < ActiveRecord::Migration[7.0]
def change
create_table :domain_post_file_fingerprints do |t|
t.binary :blob_sha256, null: false, index: true
t.bit :hash_value, limit: 256
t.timestamps
t.index :hash_value, using: :hnsw, opclass: :bit_hamming_ops
end
end
end

View File

@@ -0,0 +1,8 @@
class AddIndexOnPostFileBlobSha256 < ActiveRecord::Migration[7.2]
disable_ddl_transaction!
def change
up_only { execute "SET DEFAULT_TABLESPACE = mirai;" }
add_index :domain_post_files, :blob_sha256, algorithm: :concurrently
end
end

View File

@@ -0,0 +1,30 @@
class CreatePostFileThumbnailsFingerprints < ActiveRecord::Migration[7.2]
def change
create_table :domain_post_file_thumbnails do |t|
t.references :post_file, null: false, index: false
t.integer :thumb_type, null: false
t.integer :frame, null: false, default: 0
t.timestamps
t.index %i[post_file_id thumb_type frame], unique: true
end
create_table :domain_post_file_bit_fingerprints do |t|
t.references :post_file, null: false, index: false
t.references :thumbnail, null: false, index: false
t.bit :fingerprint_value, limit: 256
t.timestamps
t.index %i[post_file_id thumbnail_id], unique: true
t.index :fingerprint_value, using: :hnsw, opclass: :bit_hamming_ops
end
# create_table :domain_post_file_vector_fingerprints do |t|
# t.integer :type, null: false
# t.references :thumbnail, null: false, index: true
# t.vector :fingerprint_value, limit: 256
# t.timestamps
# t.index :fingerprint_value, using: :hnsw, opclass: :vector_cosine_ops
# end
end
end

View File

@@ -2697,23 +2697,24 @@ ALTER SEQUENCE public.domain_inkbunny_users_id_seq OWNED BY public.domain_inkbun
--
-- Name: domain_post_file_fingerprints; Type: TABLE; Schema: public; Owner: -
-- Name: domain_post_file_bit_fingerprints; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_post_file_fingerprints (
CREATE TABLE public.domain_post_file_bit_fingerprints (
id bigint NOT NULL,
blob_sha256 bytea NOT NULL,
hash_value bit(256),
post_file_id bigint NOT NULL,
thumbnail_id bigint NOT NULL,
fingerprint_value bit(256),
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: domain_post_file_fingerprints_id_seq; Type: SEQUENCE; Schema: public; Owner: -
-- Name: domain_post_file_bit_fingerprints_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_post_file_fingerprints_id_seq
CREATE SEQUENCE public.domain_post_file_bit_fingerprints_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
@@ -2722,10 +2723,43 @@ CREATE SEQUENCE public.domain_post_file_fingerprints_id_seq
--
-- Name: domain_post_file_fingerprints_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
-- Name: domain_post_file_bit_fingerprints_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_post_file_fingerprints_id_seq OWNED BY public.domain_post_file_fingerprints.id;
ALTER SEQUENCE public.domain_post_file_bit_fingerprints_id_seq OWNED BY public.domain_post_file_bit_fingerprints.id;
--
-- Name: domain_post_file_thumbnails; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_post_file_thumbnails (
id bigint NOT NULL,
post_file_id bigint NOT NULL,
thumb_type integer NOT NULL,
frame integer DEFAULT 0 NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: domain_post_file_thumbnails_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_post_file_thumbnails_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: domain_post_file_thumbnails_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_post_file_thumbnails_id_seq OWNED BY public.domain_post_file_thumbnails.id;
SET default_tablespace = mirai;
@@ -4621,10 +4655,17 @@ ALTER TABLE ONLY public.domain_inkbunny_users ALTER COLUMN id SET DEFAULT nextva
--
-- Name: domain_post_file_fingerprints id; Type: DEFAULT; Schema: public; Owner: -
-- Name: domain_post_file_bit_fingerprints id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_file_fingerprints ALTER COLUMN id SET DEFAULT nextval('public.domain_post_file_fingerprints_id_seq'::regclass);
ALTER TABLE ONLY public.domain_post_file_bit_fingerprints ALTER COLUMN id SET DEFAULT nextval('public.domain_post_file_bit_fingerprints_id_seq'::regclass);
--
-- Name: domain_post_file_thumbnails id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_file_thumbnails ALTER COLUMN id SET DEFAULT nextval('public.domain_post_file_thumbnails_id_seq'::regclass);
--
@@ -5410,11 +5451,19 @@ ALTER TABLE ONLY public.domain_inkbunny_users
--
-- Name: domain_post_file_fingerprints domain_post_file_fingerprints_pkey; Type: CONSTRAINT; Schema: public; Owner: -
-- Name: domain_post_file_bit_fingerprints domain_post_file_bit_fingerprints_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_file_fingerprints
ADD CONSTRAINT domain_post_file_fingerprints_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.domain_post_file_bit_fingerprints
ADD CONSTRAINT domain_post_file_bit_fingerprints_pkey PRIMARY KEY (id);
--
-- Name: domain_post_file_thumbnails domain_post_file_thumbnails_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_file_thumbnails
ADD CONSTRAINT domain_post_file_thumbnails_pkey PRIMARY KEY (id);
SET default_tablespace = mirai;
@@ -5748,6 +5797,20 @@ SET default_tablespace = '';
CREATE UNIQUE INDEX idx_on_good_job_execution_id_685ddb5560 ON public.good_job_execution_log_lines_collections USING btree (good_job_execution_id);
--
-- Name: idx_on_post_file_id_thumb_type_frame_17152086d1; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX idx_on_post_file_id_thumb_type_frame_17152086d1 ON public.domain_post_file_thumbnails USING btree (post_file_id, thumb_type, frame);
--
-- Name: idx_on_post_file_id_thumbnail_id_28e9a641fb; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX idx_on_post_file_id_thumbnail_id_28e9a641fb ON public.domain_post_file_bit_fingerprints USING btree (post_file_id, thumbnail_id);
SET default_tablespace = mirai;
--
@@ -7019,21 +7082,21 @@ CREATE INDEX index_domain_inkbunny_users_on_shallow_update_log_entry_id ON publi
--
-- Name: index_domain_post_file_fingerprints_on_blob_sha256; Type: INDEX; Schema: public; Owner: -
-- Name: index_domain_post_file_bit_fingerprints_on_fingerprint_value; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_post_file_fingerprints_on_blob_sha256 ON public.domain_post_file_fingerprints USING btree (blob_sha256);
--
-- Name: index_domain_post_file_fingerprints_on_hash_value; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_post_file_fingerprints_on_hash_value ON public.domain_post_file_fingerprints USING hnsw (hash_value public.bit_hamming_ops);
CREATE INDEX index_domain_post_file_bit_fingerprints_on_fingerprint_value ON public.domain_post_file_bit_fingerprints USING hnsw (fingerprint_value public.bit_hamming_ops);
SET default_tablespace = mirai;
--
-- Name: index_domain_post_files_on_blob_sha256; Type: INDEX; Schema: public; Owner: -; Tablespace: mirai
--
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: -; Tablespace: mirai
--
@@ -8846,8 +8909,9 @@ ALTER TABLE ONLY public.domain_twitter_tweets
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
('20250306021628'),
('20250305195421'),
('20250302074924'),
('20250301000001'),
('20250226003653'),
('20250222035939'),
('20250206224121'),

View File

@@ -1,7 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "media-thumbnailer"
version = "0.1.0"

View File

@@ -1,6 +0,0 @@
[package]
name = "media-thumbnailer"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View File

@@ -1 +0,0 @@
{"rustc_fingerprint":2276600985026381000,"outputs":{"13331785392996375709":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/vscode/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.85.0 (4d91de4e4 2025-02-17)\nbinary: rustc\ncommit-hash: 4d91de4e48198da2e33413efdcd9cd2cc0c46688\ncommit-date: 2025-02-17\nhost: x86_64-unknown-linux-gnu\nrelease: 1.85.0\nLLVM version: 19.1.7\n","stderr":""},"2063776225603076451":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/vscode/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}

View File

@@ -1,3 +0,0 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

View File

@@ -1 +0,0 @@
{"rustc":8277423686421874925,"features":"[]","declared_features":"[]","target":9596570253498806976,"profile":17672942494452627365,"path":4942398508502643691,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/media-thumbnailer-5574003c59783d36/dep-bin-media-thumbnailer","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@@ -1 +0,0 @@
This file has an mtime of when this was started.

View File

@@ -1 +0,0 @@
{"rustc":8277423686421874925,"features":"[]","declared_features":"[]","target":9596570253498806976,"profile":3316208278650011218,"path":4942398508502643691,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/media-thumbnailer-706bac28c87bc41d/dep-test-bin-media-thumbnailer","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -1,5 +0,0 @@
/workspaces/redux-scraper/media-thumbnailer/target/debug/deps/libmedia_thumbnailer-5574003c59783d36.rmeta: src/main.rs
/workspaces/redux-scraper/media-thumbnailer/target/debug/deps/media_thumbnailer-5574003c59783d36.d: src/main.rs
src/main.rs:

View File

@@ -1,5 +0,0 @@
/workspaces/redux-scraper/media-thumbnailer/target/debug/deps/libmedia_thumbnailer-706bac28c87bc41d.rmeta: src/main.rs
/workspaces/redux-scraper/media-thumbnailer/target/debug/deps/media_thumbnailer-706bac28c87bc41d.d: src/main.rs
src/main.rs:

View File

@@ -99,14 +99,15 @@ namespace :fa do
start_at =
ENV["start_at"]&.to_i ||
raise("need start_at (highest fa_id already present)")
stop_at = ENV["stop_at"]&.to_i
low_water_mark = 50
high_water_mark = 300
poll_duration = 10
enqueuer =
Domain::Fa::PostEnqueuer.new(
reverse_scan_holes: true,
start_at: start_at,
stop_at: stop_at,
low_water_mark: low_water_mark,
high_water_mark: high_water_mark,
)

View File

@@ -6,6 +6,7 @@
class BlobFile
include GeneratedAssociationMethods
include GeneratedAttributeMethods
include EnumMethodsModule
extend CommonRelationMethods
@@ -345,6 +346,8 @@ class BlobFile
def v1?; end
end
module GeneratedAssociationMethods; end
module GeneratedAssociationRelationMethods
sig { returns(PrivateAssociationRelation) }
def all; end

View File

@@ -453,6 +453,20 @@ class Domain::PostFile
end
module GeneratedAssociationMethods
sig { returns(T::Array[T.untyped]) }
def bit_fingerprint_ids; end
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def bit_fingerprint_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::PostFile` class because it declared `has_many :bit_fingerprints`.
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
sig { returns(::Domain::PostFile::BitFingerprint::PrivateCollectionProxy) }
def bit_fingerprints; end
sig { params(value: T::Enumerable[::Domain::PostFile::BitFingerprint]).void }
def bit_fingerprints=(value); end
sig { returns(T.nilable(::BlobFile)) }
def blob; end
@@ -468,9 +482,6 @@ class Domain::PostFile
sig { params(args: T.untyped, blk: T.untyped).returns(::BlobFile) }
def build_blob(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFileFingerprint) }
def build_fingerprint(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_log_entry(*args, &blk); end
@@ -483,12 +494,6 @@ class Domain::PostFile
sig { params(args: T.untyped, blk: T.untyped).returns(::BlobFile) }
def create_blob!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFileFingerprint) }
def create_fingerprint(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFileFingerprint) }
def create_fingerprint!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_log_entry(*args, &blk); end
@@ -501,12 +506,6 @@ class Domain::PostFile
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::Post) }
def create_post!(*args, &blk); end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
def fingerprint; end
sig { params(value: T.nilable(::Domain::PostFileFingerprint)).void }
def fingerprint=(value); end
sig { returns(T.nilable(::HttpLogEntry)) }
def log_entry; end
@@ -534,9 +533,6 @@ class Domain::PostFile
sig { returns(T.nilable(::BlobFile)) }
def reload_blob; end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
def reload_fingerprint; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_log_entry; end
@@ -546,14 +542,25 @@ class Domain::PostFile
sig { void }
def reset_blob; end
sig { void }
def reset_fingerprint; end
sig { void }
def reset_log_entry; end
sig { void }
def reset_post; end
sig { returns(T::Array[T.untyped]) }
def thumbnail_ids; end
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def thumbnail_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::PostFile` class because it declared `has_many :thumbnails`.
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
sig { returns(::Domain::PostFile::Thumbnail::PrivateCollectionProxy) }
def thumbnails; end
sig { params(value: T::Enumerable[::Domain::PostFile::Thumbnail]).void }
def thumbnails=(value); end
end
module GeneratedAssociationRelationMethods

View File

@@ -1,11 +1,11 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Domain::PostFileFingerprint`.
# Please instead update this file by running `bin/tapioca dsl Domain::PostFileFingerprint`.
# This is an autogenerated file for dynamic methods in `Domain::PostFile::BitFingerprint`.
# Please instead update this file by running `bin/tapioca dsl Domain::PostFile::BitFingerprint`.
class Domain::PostFileFingerprint
class Domain::PostFile::BitFingerprint
include GeneratedAssociationMethods
include GeneratedAttributeMethods
extend CommonRelationMethods
@@ -47,8 +47,8 @@ class Domain::PostFileFingerprint
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def new(attributes = nil, &block); end
end
@@ -56,7 +56,7 @@ class Domain::PostFileFingerprint
module CommonRelationMethods
sig do
params(
block: T.nilable(T.proc.params(record: ::Domain::PostFileFingerprint).returns(T.untyped))
block: T.nilable(T.proc.params(record: ::Domain::PostFile::BitFingerprint).returns(T.untyped))
).returns(T::Boolean)
end
def any?(&block); end
@@ -66,20 +66,20 @@ class Domain::PostFileFingerprint
sig do
params(
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def build(attributes = nil, &block); end
@@ -90,111 +90,111 @@ class Domain::PostFileFingerprint
sig do
params(
column_name: NilClass,
block: T.proc.params(object: ::Domain::PostFileFingerprint).void
block: T.proc.params(object: ::Domain::PostFile::BitFingerprint).void
).returns(Integer)
end
def count(column_name = nil, &block); end
sig do
params(
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def create(attributes = nil, &block); end
sig do
params(
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def create!(attributes = nil, &block); end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def create_or_find_by(attributes, &block); end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def create_or_find_by!(attributes, &block); end
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def destroy_all; end
sig { params(conditions: T.untyped).returns(T::Boolean) }
def exists?(conditions = :none); end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def fifth; end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def fifth!; end
sig do
params(
args: T.any(String, Symbol, ::ActiveSupport::Multibyte::Chars, T::Boolean, BigDecimal, Numeric, ::ActiveRecord::Type::Binary::Data, ::ActiveRecord::Type::Time::Value, Date, Time, ::ActiveSupport::Duration, T::Class[T.anything])
).returns(::Domain::PostFileFingerprint)
).returns(::Domain::PostFile::BitFingerprint)
end
sig do
params(
args: T::Array[T.any(String, Symbol, ::ActiveSupport::Multibyte::Chars, T::Boolean, BigDecimal, Numeric, ::ActiveRecord::Type::Binary::Data, ::ActiveRecord::Type::Time::Value, Date, Time, ::ActiveSupport::Duration, T::Class[T.anything])]
).returns(T::Enumerable[::Domain::PostFileFingerprint])
).returns(T::Enumerable[::Domain::PostFile::BitFingerprint])
end
sig do
params(
args: NilClass,
block: T.proc.params(object: ::Domain::PostFileFingerprint).void
).returns(T.nilable(::Domain::PostFileFingerprint))
block: T.proc.params(object: ::Domain::PostFile::BitFingerprint).void
).returns(T.nilable(::Domain::PostFile::BitFingerprint))
end
def find(args = nil, &block); end
sig { params(args: T.untyped).returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { params(args: T.untyped).returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def find_by(*args); end
sig { params(args: T.untyped).returns(::Domain::PostFileFingerprint) }
sig { params(args: T.untyped).returns(::Domain::PostFile::BitFingerprint) }
def find_by!(*args); end
sig do
@@ -204,7 +204,7 @@ class Domain::PostFileFingerprint
batch_size: Integer,
error_on_ignore: T.untyped,
order: Symbol,
block: T.proc.params(object: ::Domain::PostFileFingerprint).void
block: T.proc.params(object: ::Domain::PostFile::BitFingerprint).void
).void
end
sig do
@@ -214,7 +214,7 @@ class Domain::PostFileFingerprint
batch_size: Integer,
error_on_ignore: T.untyped,
order: Symbol
).returns(T::Enumerator[::Domain::PostFileFingerprint])
).returns(T::Enumerator[::Domain::PostFile::BitFingerprint])
end
def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc, &block); end
@@ -225,7 +225,7 @@ class Domain::PostFileFingerprint
batch_size: Integer,
error_on_ignore: T.untyped,
order: Symbol,
block: T.proc.params(object: T::Array[::Domain::PostFileFingerprint]).void
block: T.proc.params(object: T::Array[::Domain::PostFile::BitFingerprint]).void
).void
end
sig do
@@ -235,78 +235,78 @@ class Domain::PostFileFingerprint
batch_size: Integer,
error_on_ignore: T.untyped,
order: Symbol
).returns(T::Enumerator[T::Enumerator[::Domain::PostFileFingerprint]])
).returns(T::Enumerator[T::Enumerator[::Domain::PostFile::BitFingerprint]])
end
def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc, &block); end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def find_or_create_by(attributes, &block); end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def find_or_create_by!(attributes, &block); end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def find_or_initialize_by(attributes, &block); end
sig { params(signed_id: T.untyped, purpose: T.untyped).returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { params(signed_id: T.untyped, purpose: T.untyped).returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def find_signed(signed_id, purpose: nil); end
sig { params(signed_id: T.untyped, purpose: T.untyped).returns(::Domain::PostFileFingerprint) }
sig { params(signed_id: T.untyped, purpose: T.untyped).returns(::Domain::PostFile::BitFingerprint) }
def find_signed!(signed_id, purpose: nil); end
sig { params(arg: T.untyped, args: T.untyped).returns(::Domain::PostFileFingerprint) }
sig { params(arg: T.untyped, args: T.untyped).returns(::Domain::PostFile::BitFingerprint) }
def find_sole_by(arg, *args); end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { params(limit: Integer).returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
sig { params(limit: Integer).returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def first(limit = nil); end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def first!; end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def forty_two; end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def forty_two!; end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def fourth; end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def fourth!; end
sig { returns(Array) }
@@ -340,16 +340,16 @@ class Domain::PostFileFingerprint
sig { params(record: T.untyped).returns(T::Boolean) }
def include?(record); end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { params(limit: Integer).returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
sig { params(limit: Integer).returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def last(limit = nil); end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def last!; end
sig do
params(
block: T.nilable(T.proc.params(record: ::Domain::PostFileFingerprint).returns(T.untyped))
block: T.nilable(T.proc.params(record: ::Domain::PostFile::BitFingerprint).returns(T.untyped))
).returns(T::Boolean)
end
def many?(&block); end
@@ -365,33 +365,33 @@ class Domain::PostFileFingerprint
sig do
params(
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
sig do
params(
attributes: T::Array[T.untyped],
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(T::Array[::Domain::PostFileFingerprint])
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
sig do
params(
attributes: T.untyped,
block: T.nilable(T.proc.params(object: ::Domain::PostFileFingerprint).void)
).returns(::Domain::PostFileFingerprint)
block: T.nilable(T.proc.params(object: ::Domain::PostFile::BitFingerprint).void)
).returns(::Domain::PostFile::BitFingerprint)
end
def new(attributes = nil, &block); end
sig do
params(
block: T.nilable(T.proc.params(record: ::Domain::PostFileFingerprint).returns(T.untyped))
block: T.nilable(T.proc.params(record: ::Domain::PostFile::BitFingerprint).returns(T.untyped))
).returns(T::Boolean)
end
def none?(&block); end
sig do
params(
block: T.nilable(T.proc.params(record: ::Domain::PostFileFingerprint).returns(T.untyped))
block: T.nilable(T.proc.params(record: ::Domain::PostFile::BitFingerprint).returns(T.untyped))
).returns(T::Boolean)
end
def one?(&block); end
@@ -402,19 +402,19 @@ class Domain::PostFileFingerprint
sig { params(column_names: T.untyped).returns(T.untyped) }
def pluck(*column_names); end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def second; end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def second!; end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def second_to_last; end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def second_to_last!; end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def sole; end
sig { params(initial_value_or_column: T.untyped).returns(T.any(Integer, Float, BigDecimal)) }
@@ -422,28 +422,28 @@ class Domain::PostFileFingerprint
type_parameters(:U)
.params(
initial_value_or_column: T.nilable(T.type_parameter(:U)),
block: T.proc.params(object: ::Domain::PostFileFingerprint).returns(T.type_parameter(:U))
block: T.proc.params(object: ::Domain::PostFile::BitFingerprint).returns(T.type_parameter(:U))
).returns(T.type_parameter(:U))
end
def sum(initial_value_or_column = nil, &block); end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { params(limit: Integer).returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
sig { params(limit: Integer).returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def take(limit = nil); end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def take!; end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def third; end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def third!; end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
sig { returns(T.nilable(::Domain::PostFile::BitFingerprint)) }
def third_to_last; end
sig { returns(::Domain::PostFileFingerprint) }
sig { returns(::Domain::PostFile::BitFingerprint) }
def third_to_last!; end
end
@@ -451,12 +451,21 @@ class Domain::PostFileFingerprint
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile) }
def build_post_file(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::Thumbnail) }
def build_thumbnail(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile) }
def create_post_file(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile) }
def create_post_file!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::Thumbnail) }
def create_thumbnail(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFile::Thumbnail) }
def create_thumbnail!(*args, &blk); end
sig { returns(T.nilable(::Domain::PostFile)) }
def post_file; end
@@ -472,8 +481,26 @@ class Domain::PostFileFingerprint
sig { returns(T.nilable(::Domain::PostFile)) }
def reload_post_file; end
sig { returns(T.nilable(::Domain::PostFile::Thumbnail)) }
def reload_thumbnail; end
sig { void }
def reset_post_file; end
sig { void }
def reset_thumbnail; end
sig { returns(T.nilable(::Domain::PostFile::Thumbnail)) }
def thumbnail; end
sig { params(value: T.nilable(::Domain::PostFile::Thumbnail)).void }
def thumbnail=(value); end
sig { returns(T::Boolean) }
def thumbnail_changed?; end
sig { returns(T::Boolean) }
def thumbnail_previously_changed?; end
end
module GeneratedAssociationRelationMethods
@@ -640,51 +667,6 @@ class Domain::PostFileFingerprint
end
module GeneratedAttributeMethods
sig { returns(T.nilable(::String)) }
def blob_sha256; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def blob_sha256=(value); end
sig { returns(T::Boolean) }
def blob_sha256?; end
sig { returns(T.nilable(::String)) }
def blob_sha256_before_last_save; end
sig { returns(T.untyped) }
def blob_sha256_before_type_cast; end
sig { returns(T::Boolean) }
def blob_sha256_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def blob_sha256_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def blob_sha256_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def blob_sha256_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def blob_sha256_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def blob_sha256_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def blob_sha256_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def blob_sha256_previously_was; end
sig { returns(T.nilable(::String)) }
def blob_sha256_was; end
sig { void }
def blob_sha256_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def created_at; end
@@ -741,49 +723,49 @@ class Domain::PostFileFingerprint
def created_at_will_change!; end
sig { returns(T.nilable(::String)) }
def hash_value; end
def fingerprint_value; end
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
def hash_value=(value); end
def fingerprint_value=(value); end
sig { returns(T::Boolean) }
def hash_value?; end
def fingerprint_value?; end
sig { returns(T.nilable(::String)) }
def hash_value_before_last_save; end
def fingerprint_value_before_last_save; end
sig { returns(T.untyped) }
def hash_value_before_type_cast; end
def fingerprint_value_before_type_cast; end
sig { returns(T::Boolean) }
def hash_value_came_from_user?; end
def fingerprint_value_came_from_user?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def hash_value_change; end
def fingerprint_value_change; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def hash_value_change_to_be_saved; end
def fingerprint_value_change_to_be_saved; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def hash_value_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
def fingerprint_value_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def hash_value_in_database; end
def fingerprint_value_in_database; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def hash_value_previous_change; end
def fingerprint_value_previous_change; end
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
def hash_value_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
def fingerprint_value_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::String)) }
def hash_value_previously_was; end
def fingerprint_value_previously_was; end
sig { returns(T.nilable(::String)) }
def hash_value_was; end
def fingerprint_value_was; end
sig { void }
def hash_value_will_change!; end
def fingerprint_value_will_change!; end
sig { returns(T.nilable(::Integer)) }
def id; end
@@ -875,14 +857,56 @@ class Domain::PostFileFingerprint
sig { void }
def id_will_change!; end
sig { returns(T.nilable(::Integer)) }
def post_file_id; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def post_file_id=(value); end
sig { returns(T::Boolean) }
def post_file_id?; end
sig { returns(T.nilable(::Integer)) }
def post_file_id_before_last_save; end
sig { returns(T.untyped) }
def post_file_id_before_type_cast; end
sig { returns(T::Boolean) }
def post_file_id_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def post_file_id_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def post_file_id_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def post_file_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def post_file_id_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def post_file_id_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def post_file_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def post_file_id_previously_was; end
sig { returns(T.nilable(::Integer)) }
def post_file_id_was; end
sig { void }
def restore_blob_sha256!; end
def post_file_id_will_change!; end
sig { void }
def restore_created_at!; end
sig { void }
def restore_hash_value!; end
def restore_fingerprint_value!; end
sig { void }
def restore_id!; end
@@ -890,15 +914,15 @@ class Domain::PostFileFingerprint
sig { void }
def restore_id_value!; end
sig { void }
def restore_post_file_id!; end
sig { void }
def restore_thumbnail_id!; end
sig { void }
def restore_updated_at!; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_blob_sha256; end
sig { returns(T::Boolean) }
def saved_change_to_blob_sha256?; end
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
def saved_change_to_created_at; end
@@ -906,10 +930,10 @@ class Domain::PostFileFingerprint
def saved_change_to_created_at?; end
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
def saved_change_to_hash_value; end
def saved_change_to_fingerprint_value; end
sig { returns(T::Boolean) }
def saved_change_to_hash_value?; end
def saved_change_to_fingerprint_value?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_id; end
@@ -923,12 +947,69 @@ class Domain::PostFileFingerprint
sig { returns(T::Boolean) }
def saved_change_to_id_value?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_post_file_id; end
sig { returns(T::Boolean) }
def saved_change_to_post_file_id?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def saved_change_to_thumbnail_id; end
sig { returns(T::Boolean) }
def saved_change_to_thumbnail_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(::Integer)) }
def thumbnail_id; end
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
def thumbnail_id=(value); end
sig { returns(T::Boolean) }
def thumbnail_id?; end
sig { returns(T.nilable(::Integer)) }
def thumbnail_id_before_last_save; end
sig { returns(T.untyped) }
def thumbnail_id_before_type_cast; end
sig { returns(T::Boolean) }
def thumbnail_id_came_from_user?; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def thumbnail_id_change; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def thumbnail_id_change_to_be_saved; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def thumbnail_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def thumbnail_id_in_database; end
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
def thumbnail_id_previous_change; end
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
def thumbnail_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
sig { returns(T.nilable(::Integer)) }
def thumbnail_id_previously_was; end
sig { returns(T.nilable(::Integer)) }
def thumbnail_id_was; end
sig { void }
def thumbnail_id_will_change!; end
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
def updated_at; end
@@ -984,14 +1065,11 @@ class Domain::PostFileFingerprint
sig { void }
def updated_at_will_change!; end
sig { returns(T::Boolean) }
def will_save_change_to_blob_sha256?; end
sig { returns(T::Boolean) }
def will_save_change_to_created_at?; end
sig { returns(T::Boolean) }
def will_save_change_to_hash_value?; end
def will_save_change_to_fingerprint_value?; end
sig { returns(T::Boolean) }
def will_save_change_to_id?; end
@@ -999,6 +1077,12 @@ class Domain::PostFileFingerprint
sig { returns(T::Boolean) }
def will_save_change_to_id_value?; end
sig { returns(T::Boolean) }
def will_save_change_to_post_file_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_thumbnail_id?; end
sig { returns(T::Boolean) }
def will_save_change_to_updated_at?; end
end
@@ -1168,17 +1252,17 @@ class Domain::PostFileFingerprint
include CommonRelationMethods
include GeneratedAssociationRelationMethods
Elem = type_member { { fixed: ::Domain::PostFileFingerprint } }
Elem = type_member { { fixed: ::Domain::PostFile::BitFingerprint } }
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def to_a; end
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def to_ary; end
end
class PrivateAssociationRelationGroupChain < PrivateAssociationRelation
Elem = type_member { { fixed: ::Domain::PostFileFingerprint } }
Elem = type_member { { fixed: ::Domain::PostFile::BitFingerprint } }
sig { params(column_name: T.any(String, Symbol)).returns(T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]) }
def average(column_name); end
@@ -1213,7 +1297,7 @@ class Domain::PostFileFingerprint
end
class PrivateAssociationRelationWhereChain
Elem = type_member { { fixed: ::Domain::PostFileFingerprint } }
Elem = type_member { { fixed: ::Domain::PostFile::BitFingerprint } }
sig { params(args: T.untyped).returns(PrivateAssociationRelation) }
def associated(*args); end
@@ -1229,18 +1313,18 @@ class Domain::PostFileFingerprint
include CommonRelationMethods
include GeneratedAssociationRelationMethods
Elem = type_member { { fixed: ::Domain::PostFileFingerprint } }
Elem = type_member { { fixed: ::Domain::PostFile::BitFingerprint } }
sig do
params(
records: T.any(::Domain::PostFileFingerprint, T::Enumerable[T.any(::Domain::PostFileFingerprint, T::Enumerable[::Domain::PostFileFingerprint])])
records: T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[::Domain::PostFile::BitFingerprint])])
).returns(PrivateCollectionProxy)
end
def <<(*records); end
sig do
params(
records: T.any(::Domain::PostFileFingerprint, T::Enumerable[T.any(::Domain::PostFileFingerprint, T::Enumerable[::Domain::PostFileFingerprint])])
records: T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[::Domain::PostFile::BitFingerprint])])
).returns(PrivateCollectionProxy)
end
def append(*records); end
@@ -1250,45 +1334,45 @@ class Domain::PostFileFingerprint
sig do
params(
records: T.any(::Domain::PostFileFingerprint, T::Enumerable[T.any(::Domain::PostFileFingerprint, T::Enumerable[::Domain::PostFileFingerprint])])
records: T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[::Domain::PostFile::BitFingerprint])])
).returns(PrivateCollectionProxy)
end
def concat(*records); end
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def load_target; end
sig do
params(
records: T.any(::Domain::PostFileFingerprint, T::Enumerable[T.any(::Domain::PostFileFingerprint, T::Enumerable[::Domain::PostFileFingerprint])])
records: T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[::Domain::PostFile::BitFingerprint])])
).returns(PrivateCollectionProxy)
end
def prepend(*records); end
sig do
params(
records: T.any(::Domain::PostFileFingerprint, T::Enumerable[T.any(::Domain::PostFileFingerprint, T::Enumerable[::Domain::PostFileFingerprint])])
records: T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[::Domain::PostFile::BitFingerprint])])
).returns(PrivateCollectionProxy)
end
def push(*records); end
sig do
params(
other_array: T.any(::Domain::PostFileFingerprint, T::Enumerable[T.any(::Domain::PostFileFingerprint, T::Enumerable[::Domain::PostFileFingerprint])])
).returns(T::Array[::Domain::PostFileFingerprint])
other_array: T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[T.any(::Domain::PostFile::BitFingerprint, T::Enumerable[::Domain::PostFile::BitFingerprint])])
).returns(T::Array[::Domain::PostFile::BitFingerprint])
end
def replace(other_array); end
sig { returns(PrivateAssociationRelation) }
def scope; end
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def target; end
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def to_a; end
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def to_ary; end
end
@@ -1296,17 +1380,17 @@ class Domain::PostFileFingerprint
include CommonRelationMethods
include GeneratedRelationMethods
Elem = type_member { { fixed: ::Domain::PostFileFingerprint } }
Elem = type_member { { fixed: ::Domain::PostFile::BitFingerprint } }
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def to_a; end
sig { returns(T::Array[::Domain::PostFileFingerprint]) }
sig { returns(T::Array[::Domain::PostFile::BitFingerprint]) }
def to_ary; end
end
class PrivateRelationGroupChain < PrivateRelation
Elem = type_member { { fixed: ::Domain::PostFileFingerprint } }
Elem = type_member { { fixed: ::Domain::PostFile::BitFingerprint } }
sig { params(column_name: T.any(String, Symbol)).returns(T::Hash[T.untyped, T.any(Integer, Float, BigDecimal)]) }
def average(column_name); end
@@ -1341,7 +1425,7 @@ class Domain::PostFileFingerprint
end
class PrivateRelationWhereChain
Elem = type_member { { fixed: ::Domain::PostFileFingerprint } }
Elem = type_member { { fixed: ::Domain::PostFile::BitFingerprint } }
sig { params(args: T.untyped).returns(PrivateRelation) }
def associated(*args); end

File diff suppressed because it is too large Load Diff

View File

@@ -454,6 +454,20 @@ class Domain::PostFile::InkbunnyPostFile
module EnumMethodsModule; end
module GeneratedAssociationMethods
sig { returns(T::Array[T.untyped]) }
def bit_fingerprint_ids; end
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def bit_fingerprint_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::PostFile` class because it declared `has_many :bit_fingerprints`.
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
sig { returns(::Domain::PostFile::BitFingerprint::PrivateCollectionProxy) }
def bit_fingerprints; end
sig { params(value: T::Enumerable[::Domain::PostFile::BitFingerprint]).void }
def bit_fingerprints=(value); end
sig { returns(T.nilable(::BlobFile)) }
def blob; end
@@ -463,9 +477,6 @@ class Domain::PostFile::InkbunnyPostFile
sig { params(args: T.untyped, blk: T.untyped).returns(::BlobFile) }
def build_blob(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFileFingerprint) }
def build_fingerprint(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def build_log_entry(*args, &blk); end
@@ -478,12 +489,6 @@ class Domain::PostFile::InkbunnyPostFile
sig { params(args: T.untyped, blk: T.untyped).returns(::BlobFile) }
def create_blob!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFileFingerprint) }
def create_fingerprint(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::PostFileFingerprint) }
def create_fingerprint!(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(::HttpLogEntry) }
def create_log_entry(*args, &blk); end
@@ -496,12 +501,6 @@ class Domain::PostFile::InkbunnyPostFile
sig { params(args: T.untyped, blk: T.untyped).returns(::Domain::Post) }
def create_post!(*args, &blk); end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
def fingerprint; end
sig { params(value: T.nilable(::Domain::PostFileFingerprint)).void }
def fingerprint=(value); end
sig { returns(T.nilable(::HttpLogEntry)) }
def log_entry; end
@@ -517,9 +516,6 @@ class Domain::PostFile::InkbunnyPostFile
sig { returns(T.nilable(::BlobFile)) }
def reload_blob; end
sig { returns(T.nilable(::Domain::PostFileFingerprint)) }
def reload_fingerprint; end
sig { returns(T.nilable(::HttpLogEntry)) }
def reload_log_entry; end
@@ -529,14 +525,25 @@ class Domain::PostFile::InkbunnyPostFile
sig { void }
def reset_blob; end
sig { void }
def reset_fingerprint; end
sig { void }
def reset_log_entry; end
sig { void }
def reset_post; end
sig { returns(T::Array[T.untyped]) }
def thumbnail_ids; end
sig { params(ids: T::Array[T.untyped]).returns(T::Array[T.untyped]) }
def thumbnail_ids=(ids); end
# This method is created by ActiveRecord on the `Domain::PostFile` class because it declared `has_many :thumbnails`.
# 🔗 [Rails guide for `has_many` association](https://guides.rubyonrails.org/association_basics.html#the-has-many-association)
sig { returns(::Domain::PostFile::Thumbnail::PrivateCollectionProxy) }
def thumbnails; end
sig { params(value: T::Enumerable[::Domain::PostFile::Thumbnail]).void }
def thumbnails=(value); end
end
module GeneratedAssociationRelationMethods

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Domain::PostFileThumbnailJob`.
# Please instead update this file by running `bin/tapioca dsl Domain::PostFileThumbnailJob`.
class Domain::PostFileThumbnailJob
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::PostFileThumbnailJob).void)
).returns(T.any(Domain::PostFileThumbnailJob, FalseClass))
end
def perform_later(args, &block); end
sig { params(args: T::Hash[::Symbol, T.untyped]).void }
def perform_now(args); end
end
end

View File

@@ -34,10 +34,10 @@ RSpec.describe Domain::PostsController, type: :controller do
context "with an image URL" do
let(:mock_hash_value) { "1010101010101010" }
let(:mock_fingerprints) { Domain::PostFileFingerprint.none }
let(:mock_fingerprints) { Domain::PostFile::BitFingerprint.none }
let(:temp_file_path) { "/tmp/test_image.jpg" }
it "uses PostFileFingerprint model methods for fingerprinting and finding similar images" do
it "uses Phash::Fingerprint model methods for fingerprinting and finding similar images" do
# We need to mock the image downloading and processing since we can't do that in tests
allow(controller).to receive(:process_image_input).and_return(
[temp_file_path, "image/jpeg"],
@@ -47,17 +47,12 @@ RSpec.describe Domain::PostsController, type: :controller do
)
# Set up expectations for our model methods - this is what we're really testing
expect(Domain::PostFileFingerprint).to receive(:from_file_path).with(
temp_file_path,
).and_return(
instance_double(
Domain::PostFileFingerprint,
hash_value: mock_hash_value,
),
)
expect(Domain::PostFile::BitFingerprint).to receive(
:from_file_path,
).with(temp_file_path).and_return(mock_hash_value)
expect(Domain::PostFileFingerprint).to receive(
:similar_to_fingerprint,
expect(Domain::PostFile::BitFingerprint).to receive(
:order_by_fingerprint_distance,
).with(mock_hash_value).and_return(mock_fingerprints)
post :visual_results,

View File

@@ -7,4 +7,22 @@ FactoryBot.define do
content_type { "text/plain" }
sha256 { Digest::SHA256.digest(contents) }
end
trait :image_blob do
content_type { "image/jpeg" }
transient do
contents do
File.read(
Rails.root.join(
"test/fixtures/files/images/thumb-036aaab6-low-quality.jpeg",
),
)
end
end
end
trait :text_blob do
content_type { "text/plain" }
transient { contents { "test content #{SecureRandom.alphanumeric(10)}" } }
end
end

View File

@@ -12,12 +12,12 @@ FactoryBot.define do
state { "ok" }
url_str { "https://example.com/image.jpg" }
last_status_code { 200 }
before(:create) do
self.log_entry =
before(:create) do |post_file|
post_file.log_entry =
create(
:http_log_entry,
url_str: self.url_str,
status_code: self.last_status_code,
url_str: post_file.url_str,
status_code: post_file.last_status_code,
)
end
end
@@ -26,12 +26,12 @@ FactoryBot.define do
state { "terminal_error" }
url_str { "https://example.com/image.jpg" }
last_status_code { 404 }
before(:create) do
self.log_entry =
before(:create) do |post_file|
post_file.log_entry =
create(
:http_log_entry,
url_str: self.url_str,
status_code: self.last_status_code,
url_str: post_file.url_str,
status_code: post_file.last_status_code,
)
end
end
@@ -40,36 +40,25 @@ FactoryBot.define do
state { "retryable_error" }
url_str { "https://example.com/image.jpg" }
last_status_code { 500 }
before(:create) do
self.log_entry =
before(:create) do |post_file|
post_file.log_entry =
create(
:http_log_entry,
url_str: self.url_str,
status_code: self.last_status_code,
url_str: post_file.url_str,
status_code: post_file.last_status_code,
)
end
end
end
trait :image_file do
file_name { "image.jpg" }
url_str { "https://example.com/image.jpg" }
file_order { 1 }
md5_initial { "d41d8cd98f00b204e9800998ecf8427e" }
md5_full { "d41d8cd98f00b204e9800998ecf8427e" }
before(:create) do
self.log_entry =
create(
:blob_file,
content_type: "image/jpeg",
contents:
File.read(
Rails.root.join(
"test/fixtures/files/images/thumb-036aaab6-low-quality.jpeg",
),
),
)
end
log_entry { create(:http_log_entry, :image_entry) }
end
trait :text_file do
log_entry { create(:http_log_entry, :text_entry) }
url_str { log_entry.uri_str }
end
end

View File

@@ -0,0 +1,7 @@
FactoryBot.define do
factory :domain_post_file_thumbnail, class: "Domain::PostFile::Thumbnail" do
post_file { create(:domain_post_file, :image_file) }
thumb_type { :size_32_32 }
frame { 0 }
end
end

View File

@@ -1,7 +0,0 @@
# typed: false
FactoryBot.define do
factory :domain_post_file_thumbnail, class: "Domain::PostFileThumbnail" do
association :post_file, factory: :domain_post_file
thumbnail_type { Domain::ThumbnailType::Small.name }
end
end

View File

@@ -51,5 +51,15 @@ FactoryBot.define do
trait :proxy do
performed_by { "proxy-1" }
end
trait :image_entry do
content_type { "image/jpeg" }
response { create(:blob_file, :image_blob) }
end
trait :text_entry do
content_type { "text/plain" }
response { create(:blob_file, :text_blob) }
end
end
end

View File

@@ -113,11 +113,9 @@ describe Domain::Fa::User do
expect(user.scanned_incremental_at).to eq(incremental_at)
expect(user.due_for_incremental_scan?).to be_falsey
expect(user.time_ago_for_incremental_scan).to eq("1 day ago")
expect(user.scanned_follows_at).to eq(follows_at)
expect(user.due_for_follows_scan?).to be_falsey
expect(user.time_ago_for_follows_scan).to eq("2 days ago")
# truthy if a scan has not happened in a long time
incremental_at = Time.parse 1.year.ago.iso8601

View File

@@ -1,7 +1,7 @@
# typed: false
require "rails_helper"
RSpec.describe Domain::PostFileFingerprint, type: :model do
RSpec.describe Domain::PostFile::BitFingerprint, type: :model do
describe ".similar_to_fingerprint" do
let(:image_paths) do
{
@@ -20,38 +20,30 @@ RSpec.describe Domain::PostFileFingerprint, type: :model do
@fingerprints = []
image_paths.keys.each_with_index do |image_path, index|
# Create post and attach the original image
post = create(:domain_post_fa_post)
# Read the image file content
image_content =
File.read(
Rails.root.join("test/fixtures/files", image_path),
mode: "rb",
)
# Create a blob file with the image content
blob =
log_entry =
create(
:blob_file,
content_bytes: image_content,
:http_log_entry,
content_type: "image/jpeg",
sha256: Digest::SHA256.digest(image_content),
response:
create(
:blob_file,
content_type: "image/jpeg",
contents:
File.read(
Rails.root.join("test/fixtures/files", image_path),
mode: "rb",
),
),
)
# Create a post file with the blob
post_file =
create(
:domain_post_file,
post: post,
state: "ok",
blob_sha256: blob.sha256,
)
# Create a fingerprint for the post file
# The fingerprint should be automatically calculated in the before_validation callback
fingerprint = Domain::PostFileFingerprint.create!(post_file: post_file)
@fingerprints << fingerprint
post_file = create(:domain_post_file, state: "ok", log_entry:)
thumbs = Domain::PostFile::Thumbnail.create_for_post_file!(post_file)
expect(thumbs.size).to eq(2)
fingerprints = described_class.create_for_post_file!(post_file)
expect(fingerprints.size).to eq(1)
@fingerprints << fingerprints.first
end
end
@@ -66,15 +58,15 @@ RSpec.describe Domain::PostFileFingerprint, type: :model do
# Generate a fingerprint for the low-quality image
fingerprint = DHashVips::IDHash.fingerprint(low_quality_image_path.to_s)
hash_value =
fingerprint_value =
fingerprint.to_s(2).rjust(
Domain::PostFileFingerprint::HASH_SIZE * 8,
Domain::PostFile::BitFingerprint::HASH_SIZE_BYTES * 8,
"0",
)
# Find similar fingerprints
similar_fingerprints =
Domain::PostFileFingerprint.similar_to_fingerprint(hash_value)
described_class.order_by_fingerprint_distance(fingerprint_value).to_a
# The original image's fingerprint should be in the top results
# The PostgreSQL operator <~> (hamming distance) may produce ties
@@ -85,16 +77,16 @@ RSpec.describe Domain::PostFileFingerprint, type: :model do
# Get the distance for the first result to compare with our expected result
first_result_distance =
Domain::PostFileFingerprint.hamming_distance(
hash_value,
similar_fingerprints.first.hash_value,
described_class.hamming_distance(
fingerprint_value,
similar_fingerprints.first.fingerprint_value,
)
# Get the distance for our expected fingerprint
expected_distance =
Domain::PostFileFingerprint.hamming_distance(
hash_value,
expected_fingerprint.hash_value,
described_class.hamming_distance(
fingerprint_value,
expected_fingerprint.fingerprint_value,
)
# Verify our expected fingerprint is in the results and has the same or
@@ -116,8 +108,8 @@ RSpec.describe Domain::PostFileFingerprint, type: :model do
other_fingerprint_indices = (0...@fingerprints.size).to_a - [index]
other_fingerprint_indices.each do |other_index|
# Generate a fingerprint from the low-quality image
low_quality_fingerprint = Domain::PostFileFingerprint.new
low_quality_fingerprint.hash_value = hash_value
low_quality_fingerprint = described_class.new
low_quality_fingerprint.fingerprint_value = fingerprint_value
# Compare with an original image that it should NOT match
other_similarity =
@@ -148,9 +140,9 @@ RSpec.describe Domain::PostFileFingerprint, type: :model do
# Test the hamming_distance class method
expect(
Domain::PostFileFingerprint.hamming_distance(
reference_fingerprint.hash_value,
@fingerprints[1].hash_value,
described_class.hamming_distance(
reference_fingerprint.fingerprint_value,
@fingerprints[1].fingerprint_value,
),
).to be > 0
end
@@ -183,79 +175,19 @@ RSpec.describe Domain::PostFileFingerprint, type: :model do
.to_s
# Create a fingerprint from the file path
fingerprint = Domain::PostFileFingerprint.from_file_path(file_path)
fingerprint = described_class.from_file_path(file_path)
# Check that the fingerprint was created properly
expect(fingerprint).to be_a(Domain::PostFileFingerprint)
expect(fingerprint.hash_value).to be_present
expect(fingerprint.hash_value.length).to eq(
Domain::PostFileFingerprint::HASH_SIZE * 8,
expect(fingerprint).to be_a(String)
expect(fingerprint.length).to eq(
Domain::PostFile::BitFingerprint::HASH_SIZE_BYTES * 8,
)
expect(fingerprint.persisted?).to be(false)
end
it "raises an error for non-existent files" do
expect {
Domain::PostFileFingerprint.from_file_path("/non/existent/file.jpg")
described_class.from_file_path("/non/existent/file.jpg")
}.to raise_error(ArgumentError, /File does not exist/)
end
end
describe ".from_vips_image" do
it "creates a fingerprint from a Vips::Image" do
# Load a test image as a Vips::Image
file_path =
Rails
.root
.join(
"test/fixtures/files/images/thumb-036aaab6-content-container.jpeg",
)
.to_s
vips_image = Vips::Image.new_from_file(file_path)
# Create a fingerprint from the Vips::Image
fingerprint = Domain::PostFileFingerprint.from_vips_image(vips_image)
# Check that the fingerprint was created properly
expect(fingerprint).to be_a(Domain::PostFileFingerprint)
expect(fingerprint.hash_value).to be_present
expect(fingerprint.hash_value.length).to eq(
Domain::PostFileFingerprint::HASH_SIZE * 8,
)
expect(fingerprint.persisted?).to be(false)
end
it "generates fingerprints with high similarity to from_file_path for the same image" do
# This test verifies that fingerprints from vips_image and file_path are highly similar
# for the same image, even if not exactly identical due to format considerations
file_path =
Rails
.root
.join(
"test/fixtures/files/images/thumb-ac63d9d7-content-container.jpeg",
)
.to_s
# Create fingerprints using both methods
vips_image = Vips::Image.new_from_file(file_path)
from_vips_fingerprint =
Domain::PostFileFingerprint.from_vips_image(vips_image)
from_file_fingerprint =
Domain::PostFileFingerprint.from_file_path(file_path)
# The fingerprints might not be 100% identical due to how Vips handles direct images
# vs how it handles file loading, but they should have high similarity
similarity =
100 -
(
Domain::PostFileFingerprint.hamming_distance(
from_vips_fingerprint.hash_value,
from_file_fingerprint.hash_value,
).to_f / (Domain::PostFileFingerprint::HASH_SIZE * 8) * 100
)
# The similarity should be very high (above 90%)
expect(similarity).to be > 90
end
end
end

View File

@@ -0,0 +1,53 @@
# typed: false
require "rails_helper"
RSpec.describe Domain::PostFile::Thumbnail, type: :model do
describe "validation" do
it "validates thumbable content types" do
post_file = create(:domain_post_file, :image_file)
# Create a PostFileThumbnail with valid params
thumbnail =
described_class.new(post_file:, thumb_type: :size_32_32, frame: 0)
expect(thumbnail).to be_valid
expect(thumbnail.save).to be true
end
it "rejects non-thumbable content types" do
post_file = create(:domain_post_file, :text_file)
# Create a PostFileThumbnail with invalid content type
thumbnail =
described_class.new(post_file:, thumb_type: :size_32_32, frame: 0)
expect(thumbnail).not_to be_valid
expect(thumbnail.errors[:post_file]).to include(
"must be a thumbnailable content type",
)
end
end
describe "thumbnailing" do
context "with JPEG images" do
it "successfully creates a thumbnail from a JPEG image" do
post_file = create(:domain_post_file, :image_file)
# Create a PostFileThumbnail
thumbnail =
described_class.new(post_file:, thumb_type: :size_32_32, frame: 0)
expect(thumbnail.save).to be true
# Verify the thumbnail file was created
expect(thumbnail.absolute_file_path).not_to be_nil
expect(File.exist?(thumbnail.absolute_file_path)).to be true
# Verify the thumbnail has a reasonable size (should be smaller than original)
original_size =
File.size(post_file.log_entry.response.absolute_file_path)
thumbnail_size = File.size(thumbnail.absolute_file_path)
expect(thumbnail_size).to be < original_size
end
end
end
end

View File

@@ -2,108 +2,4 @@
require "rails_helper"
RSpec.describe Domain::PostFile, type: :model do
describe "after_save callback" do
it "creates and saves a fingerprint when saving a post file with a valid image blob" do
# Create a post
post = create(:domain_post_fa_post)
# Setup an image file
image_path =
Rails.root.join(
"test/fixtures/files/images/thumb-036aaab6-content-container.jpeg",
)
image_content = File.read(image_path, mode: "rb")
# Create blob with image content
blob =
create(
:blob_file,
content_bytes: image_content,
content_type: "image/jpeg",
sha256: Digest::SHA256.digest(image_content),
)
# Count fingerprints before creating the post file
fingerprint_count_before = Domain::PostFileFingerprint.count
# Create and save the post file with the blob reference
post_file =
create(
:domain_post_file,
post: post,
state: "ok",
blob_sha256: blob.sha256,
)
# Verify a fingerprint was automatically created and saved
expect(Domain::PostFileFingerprint.count).to eq(
fingerprint_count_before + 1,
)
expect(Domain::PostFileFingerprint.last.post_file).to eq(post_file)
# Verify the fingerprint has actual content
fingerprint = Domain::PostFileFingerprint.last
expect(fingerprint.hash_value).to be_present
expect(fingerprint.hash_value.length).to eq(
Domain::PostFileFingerprint::HASH_SIZE * 8,
)
end
it "does not create a fingerprint for non-image files" do
# Create a post
post = create(:domain_post_fa_post)
# Setup a text file
text_content = "This is a text file, not an image"
# Create blob with text content
blob =
create(
:blob_file,
content_bytes: text_content,
content_type: "text/plain",
sha256: Digest::SHA256.digest(text_content),
)
# Count fingerprints before creating the post file
fingerprint_count_before = Domain::PostFileFingerprint.count
# Create and save the post file with the blob reference
post_file =
create(
:domain_post_file,
post: post,
state: "ok",
blob_sha256: blob.sha256,
)
# Verify no new fingerprint was created
expect(Domain::PostFileFingerprint.count).to eq(fingerprint_count_before)
expect(
Domain::PostFileFingerprint.where(post_file: post_file).count,
).to eq(0)
end
it "does not create a fingerprint for files with missing blobs" do
# Create a post
post = create(:domain_post_fa_post)
# Count fingerprints before creating the post file
fingerprint_count_before = Domain::PostFileFingerprint.count
# Create post file without a blob
post_file =
create(
:domain_post_file,
post: post,
state: "pending", # No blob associated
)
# Verify no new fingerprint was created
expect(Domain::PostFileFingerprint.count).to eq(fingerprint_count_before)
expect(
Domain::PostFileFingerprint.where(post_file: post_file).count,
).to eq(0)
end
end
end