Add Bluesky user scanning job and related infrastructure
- Add Domain::Bluesky::Job::ScanUserJob for processing user media - Add Domain::Bluesky::Job::Base as parent class for Bluesky jobs - Update BlueskyUser and BlueskyPostFile models with media handling - Add migration for Bluesky media fields in post_files table - Update StaticFileJob to handle Bluesky media downloads - Add comprehensive test coverage for new functionality - Update Sorbet RBI files for type checking
This commit is contained in:
37
app/jobs/domain/bluesky/job/base.rb
Normal file
37
app/jobs/domain/bluesky/job/base.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
# typed: strict
|
||||
class Domain::Bluesky::Job::Base < Scraper::JobBase
|
||||
abstract!
|
||||
discard_on ActiveJob::DeserializationError
|
||||
include HasBulkEnqueueJobs
|
||||
|
||||
sig { override.returns(Symbol) }
|
||||
def self.http_factory_method
|
||||
:get_generic_http_client
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
sig { returns(T.nilable(Domain::User::BlueskyUser)) }
|
||||
def user_from_args
|
||||
if (user = arguments[0][:user]).is_a?(Domain::User::BlueskyUser)
|
||||
user
|
||||
elsif (did = arguments[0][:did]).present?
|
||||
Domain::User::BlueskyUser.find_or_initialize_by(did: did)
|
||||
elsif (handle = arguments[0][:handle]).present?
|
||||
Domain::User::BlueskyUser.find_or_initialize_by(handle: handle)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
sig { returns(Domain::User::BlueskyUser) }
|
||||
def user_from_args!
|
||||
T.must(user_from_args)
|
||||
end
|
||||
|
||||
sig { params(user: Domain::User::BlueskyUser).returns(T::Boolean) }
|
||||
def buggy_user?(user)
|
||||
# Add any known problematic handles/DIDs here
|
||||
false
|
||||
end
|
||||
end
|
||||
310
app/jobs/domain/bluesky/job/scan_user_job.rb
Normal file
310
app/jobs/domain/bluesky/job/scan_user_job.rb
Normal file
@@ -0,0 +1,310 @@
|
||||
# typed: strict
|
||||
class Domain::Bluesky::Job::ScanUserJob < Domain::Bluesky::Job::Base
|
||||
self.default_priority = -30
|
||||
|
||||
sig { override.params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
||||
def perform(args)
|
||||
user = user_from_args!
|
||||
logger.push_tags(make_arg_tag(user))
|
||||
logger.info("Starting Bluesky user scan for #{user.handle}")
|
||||
|
||||
return if buggy_user?(user)
|
||||
|
||||
# Scan user profile/bio
|
||||
user = scan_user_profile(user) if force_scan? ||
|
||||
user.scanned_profile_at.nil? || due_for_profile_scan?(user)
|
||||
|
||||
# Scan user's historical posts
|
||||
if user.state_ok? &&
|
||||
(
|
||||
force_scan? || user.scanned_posts_at.nil? ||
|
||||
due_for_posts_scan?(user)
|
||||
)
|
||||
scan_user_posts(user)
|
||||
end
|
||||
|
||||
logger.info("Completed Bluesky user scan")
|
||||
ensure
|
||||
user.save! if user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig do
|
||||
params(user: Domain::User::BlueskyUser).returns(Domain::User::BlueskyUser)
|
||||
end
|
||||
def scan_user_profile(user)
|
||||
logger.info("Scanning user profile for #{user.handle}")
|
||||
|
||||
# Use AT Protocol API to get user profile
|
||||
profile_url =
|
||||
"https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=#{user.did}&collection=app.bsky.actor.profile&rkey=self"
|
||||
|
||||
response = http_client.get(profile_url)
|
||||
if response.status_code != 200
|
||||
logger.error("Failed to get user profile: #{response.status_code}")
|
||||
user.state_error!
|
||||
return user
|
||||
end
|
||||
|
||||
# Note: Store log entry reference if needed for debugging
|
||||
|
||||
begin
|
||||
profile_data = JSON.parse(response.body)
|
||||
|
||||
if profile_data["error"]
|
||||
logger.error("Profile API error: #{profile_data["error"]}")
|
||||
user.state_error!
|
||||
return user
|
||||
end
|
||||
|
||||
record = profile_data["value"]
|
||||
if record
|
||||
# Update user profile information
|
||||
user.description = record["description"]
|
||||
user.display_name = record["displayName"]
|
||||
user.profile_raw = record
|
||||
|
||||
# Process avatar if present
|
||||
if record["avatar"] && record["avatar"]["ref"]
|
||||
process_user_avatar(user, record["avatar"])
|
||||
end
|
||||
end
|
||||
|
||||
user.scanned_profile_at = Time.current
|
||||
user.state_ok! unless user.state_error?
|
||||
rescue JSON::ParserError => e
|
||||
logger.error("Failed to parse profile JSON: #{e.message}")
|
||||
user.state_error!
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
sig { params(user: Domain::User::BlueskyUser).void }
|
||||
def scan_user_posts(user)
|
||||
logger.info("Scanning historical posts for #{user.handle}")
|
||||
|
||||
# Use AT Protocol API to list user's posts
|
||||
posts_url =
|
||||
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=#{user.did}&collection=app.bsky.feed.post&limit=100"
|
||||
|
||||
cursor = T.let(nil, T.nilable(String))
|
||||
posts_processed = 0
|
||||
posts_with_media = 0
|
||||
|
||||
loop do
|
||||
url = cursor ? "#{posts_url}&cursor=#{cursor}" : posts_url
|
||||
|
||||
response = http_client.get(url)
|
||||
if response.status_code != 200
|
||||
logger.error("Failed to get user posts: #{response.status_code}")
|
||||
break
|
||||
end
|
||||
|
||||
begin
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
if data["error"]
|
||||
logger.error("Posts API error: #{data["error"]}")
|
||||
break
|
||||
end
|
||||
|
||||
records = data["records"] || []
|
||||
|
||||
records.each do |record_data|
|
||||
posts_processed += 1
|
||||
|
||||
record = record_data["value"]
|
||||
next unless record && record["embed"]
|
||||
|
||||
# Only process posts with media
|
||||
posts_with_media += 1
|
||||
user_did = user.did
|
||||
next unless user_did
|
||||
process_historical_post(user, record_data, record, user_did)
|
||||
end
|
||||
|
||||
cursor = data["cursor"]
|
||||
break if cursor.nil? || records.empty?
|
||||
|
||||
# Add small delay to avoid rate limiting
|
||||
sleep(0.1)
|
||||
rescue JSON::ParserError => e
|
||||
logger.error("Failed to parse posts JSON: #{e.message}")
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
user.scanned_posts_at = Time.current
|
||||
logger.info(
|
||||
"Processed #{posts_processed} posts, #{posts_with_media} with media",
|
||||
)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
user: Domain::User::BlueskyUser,
|
||||
record_data: T::Hash[String, T.untyped],
|
||||
record: T::Hash[String, T.untyped],
|
||||
user_did: String,
|
||||
).void
|
||||
end
|
||||
def process_historical_post(user, record_data, record, user_did)
|
||||
uri = record_data["uri"]
|
||||
rkey = record_data["uri"].split("/").last
|
||||
|
||||
# Check if we already have this post
|
||||
existing_post = Domain::Post::BlueskyPost.find_by(at_uri: uri)
|
||||
return if existing_post
|
||||
|
||||
begin
|
||||
post =
|
||||
Domain::Post::BlueskyPost.create!(
|
||||
at_uri: uri,
|
||||
bluesky_rkey: rkey,
|
||||
text: record["text"] || "",
|
||||
bluesky_created_at: Time.parse(record["createdAt"]),
|
||||
post_raw: record,
|
||||
)
|
||||
|
||||
post.creator = user
|
||||
post.save!
|
||||
|
||||
# Process media if present
|
||||
process_post_media(post, record["embed"], user_did) if record["embed"]
|
||||
|
||||
logger.debug("Created historical post: #{post.bluesky_rkey}")
|
||||
rescue => e
|
||||
logger.error("Failed to create historical post #{rkey}: #{e.message}")
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
embed_data: T::Hash[String, T.untyped],
|
||||
did: String,
|
||||
).void
|
||||
end
|
||||
def process_post_media(post, embed_data, did)
|
||||
case embed_data["$type"]
|
||||
when "app.bsky.embed.images"
|
||||
process_post_images(post, embed_data["images"], did)
|
||||
when "app.bsky.embed.recordWithMedia"
|
||||
if embed_data["media"] &&
|
||||
embed_data["media"]["$type"] == "app.bsky.embed.images"
|
||||
process_post_images(post, embed_data["media"]["images"], did)
|
||||
end
|
||||
when "app.bsky.embed.external"
|
||||
process_external_embed(post, embed_data["external"], did)
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
images: T::Array[T::Hash[String, T.untyped]],
|
||||
did: String,
|
||||
).void
|
||||
end
|
||||
def process_post_images(post, images, did)
|
||||
files = []
|
||||
images.each_with_index do |image_data, index|
|
||||
blob_data = image_data["image"]
|
||||
next unless blob_data && blob_data["ref"]
|
||||
|
||||
post_file =
|
||||
post.files.build(
|
||||
type: "Domain::PostFile::BlueskyPostFile",
|
||||
file_order: index,
|
||||
url_str: construct_blob_url(did, blob_data["ref"]["$link"]),
|
||||
state: "pending",
|
||||
alt_text: image_data["alt"],
|
||||
blob_ref: blob_data["ref"]["$link"],
|
||||
)
|
||||
|
||||
# Store aspect ratio if present
|
||||
if image_data["aspectRatio"]
|
||||
post_file.aspect_ratio_width = image_data["aspectRatio"]["width"]
|
||||
post_file.aspect_ratio_height = image_data["aspectRatio"]["height"]
|
||||
end
|
||||
|
||||
post_file.save!
|
||||
Domain::StaticFileJob.perform_later({ post_file: })
|
||||
files << post_file
|
||||
end
|
||||
|
||||
logger.debug(
|
||||
"Created #{files.size} #{"file".pluralize(files.size)} for historical post: #{post.bluesky_rkey}",
|
||||
)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
external_data: T::Hash[String, T.untyped],
|
||||
did: String,
|
||||
).void
|
||||
end
|
||||
def process_external_embed(post, external_data, did)
|
||||
thumb_data = external_data["thumb"]
|
||||
return unless thumb_data && thumb_data["ref"]
|
||||
|
||||
post_file =
|
||||
post.files.build(
|
||||
type: "Domain::PostFile::BlueskyPostFile",
|
||||
file_order: 0,
|
||||
url_str: construct_blob_url(did, thumb_data["ref"]["$link"]),
|
||||
state: "pending",
|
||||
blob_ref: thumb_data["ref"]["$link"],
|
||||
)
|
||||
|
||||
post_file.save!
|
||||
Domain::StaticFileJob.perform_later({ post_file: })
|
||||
|
||||
logger.debug(
|
||||
"Created external thumbnail for historical post: #{post.bluesky_rkey}",
|
||||
)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
user: Domain::User::BlueskyUser,
|
||||
avatar_data: T::Hash[String, T.untyped],
|
||||
).void
|
||||
end
|
||||
def process_user_avatar(user, avatar_data)
|
||||
return if user.avatar.present?
|
||||
return unless avatar_data["ref"]
|
||||
|
||||
user_did = user.did
|
||||
return unless user_did
|
||||
|
||||
user.create_avatar!(
|
||||
url_str: construct_blob_url(user_did, avatar_data["ref"]["$link"]),
|
||||
)
|
||||
|
||||
# Enqueue avatar download job if we had one
|
||||
logger.debug("Created avatar for user: #{user.handle}")
|
||||
end
|
||||
|
||||
sig { params(did: String, cid: String).returns(String) }
|
||||
def construct_blob_url(did, cid)
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{did}&cid=#{cid}"
|
||||
end
|
||||
|
||||
sig { params(user: Domain::User::BlueskyUser).returns(T::Boolean) }
|
||||
def due_for_profile_scan?(user)
|
||||
scanned_at = user.scanned_profile_at
|
||||
return true if scanned_at.nil?
|
||||
scanned_at < 1.month.ago
|
||||
end
|
||||
|
||||
sig { params(user: Domain::User::BlueskyUser).returns(T::Boolean) }
|
||||
def due_for_posts_scan?(user)
|
||||
scanned_at = user.scanned_posts_at
|
||||
return true if scanned_at.nil?
|
||||
scanned_at < 1.week.ago
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,11 @@
|
||||
class Domain::StaticFileJob < Scraper::JobBase
|
||||
include Domain::StaticFileJobHelper
|
||||
queue_as :static_file
|
||||
abstract!
|
||||
|
||||
sig { override.returns(Symbol) }
|
||||
def self.http_factory_method
|
||||
:get_generic_http_client
|
||||
end
|
||||
|
||||
sig { override.params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
||||
def perform(args)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# typed: true
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Tasks::Bluesky
|
||||
@@ -7,8 +7,10 @@ module Tasks::Bluesky
|
||||
extend T::Sig
|
||||
CURSOR_KEY = "task-bluesky-jetstream-cursor-1"
|
||||
|
||||
def initialize
|
||||
@resolver = DIDKit::Resolver.new
|
||||
sig { params(pg_notify: T::Boolean).void }
|
||||
def initialize(pg_notify: true)
|
||||
@pg_notify = pg_notify
|
||||
@resolver = T.let(DIDKit::Resolver.new, DIDKit::Resolver)
|
||||
@dids = T.let(Concurrent::Set.new, Concurrent::Set)
|
||||
@dids.merge(Bluesky::MonitoredDid.pluck(:did))
|
||||
logger.info(
|
||||
@@ -20,7 +22,15 @@ module Tasks::Bluesky
|
||||
T.let(
|
||||
Skyfall::Jetstream.new(
|
||||
"jetstream2.us-east.bsky.network",
|
||||
{ cursor: load_cursor, wanted_collections: "app.bsky.feed.post" },
|
||||
{
|
||||
cursor: nil,
|
||||
# cursor: load_cursor,
|
||||
wanted_collections: %w[
|
||||
app.bsky.feed.post
|
||||
app.bsky.embed.images
|
||||
app.bsky.embed.recordWithMedia
|
||||
],
|
||||
},
|
||||
),
|
||||
Skyfall::Jetstream,
|
||||
)
|
||||
@@ -40,7 +50,7 @@ module Tasks::Bluesky
|
||||
end
|
||||
@bluesky_client.on_message do |msg|
|
||||
handle_message(msg)
|
||||
if msg.seq % 100_000 == 0
|
||||
if msg.seq % 10_000 == 0
|
||||
logger.info("saving cursor: #{msg.seq.to_s.bold}")
|
||||
save_cursor(msg.seq)
|
||||
end
|
||||
@@ -48,7 +58,8 @@ module Tasks::Bluesky
|
||||
@bluesky_client.on_error { |e| logger.error("ERROR: #{e.to_s.red.bold}") }
|
||||
|
||||
# Start the thread to listen to postgres NOTIFYs to add to the @dids set
|
||||
pg_notify_thread = Thread.new { listen_to_postgres_notifies }
|
||||
pg_notify_thread =
|
||||
Thread.new { listen_to_postgres_notifies } if @pg_notify
|
||||
|
||||
@bluesky_client.connect
|
||||
rescue Interrupt
|
||||
@@ -74,13 +85,21 @@ module Tasks::Bluesky
|
||||
return unless @dids.include?(msg.did)
|
||||
msg.operations.each do |op|
|
||||
next unless op.action == :create && op.type == :bsky_post
|
||||
embed_data =
|
||||
T.let(op.raw_record["embed"], T.nilable(T::Hash[String, T.untyped]))
|
||||
next unless embed_data
|
||||
|
||||
post =
|
||||
Domain::Post::BlueskyPost.find_or_create_by!(at_uri: op.uri) do |post|
|
||||
post.bluesky_rkey = op.rkey
|
||||
post.text = op.raw_record["text"]
|
||||
post.bluesky_created_at = msg.time.in_time_zone("UTC")
|
||||
post.creator = creator_for(msg)
|
||||
post.post_raw = op.raw_record
|
||||
end
|
||||
|
||||
process_media(post, embed_data, msg.did)
|
||||
|
||||
logger.info(
|
||||
"created bluesky post: `#{post.bluesky_rkey}` / `#{post.at_uri}`",
|
||||
)
|
||||
@@ -142,5 +161,106 @@ module Tasks::Bluesky
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
embed_data: T::Hash[String, T.untyped],
|
||||
did: String,
|
||||
).void
|
||||
end
|
||||
def process_media(post, embed_data, did)
|
||||
case embed_data["$type"]
|
||||
when "app.bsky.embed.images"
|
||||
process_images(post, embed_data["images"], did)
|
||||
when "app.bsky.embed.recordWithMedia"
|
||||
# Handle quote posts with media
|
||||
if embed_data["media"] &&
|
||||
embed_data["media"]["$type"] == "app.bsky.embed.images"
|
||||
process_images(post, embed_data["media"]["images"], did)
|
||||
end
|
||||
when "app.bsky.embed.external"
|
||||
# Handle external embeds (website cards) - could have thumbnail images
|
||||
process_external_embed(post, embed_data["external"], did)
|
||||
else
|
||||
logger.debug("unknown embed type: #{embed_data["$type"]}")
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
images: T::Array[T::Hash[String, T.untyped]],
|
||||
did: String,
|
||||
).void
|
||||
end
|
||||
def process_images(post, images, did)
|
||||
files = []
|
||||
images.each_with_index do |image_data, index|
|
||||
blob_data = image_data["image"]
|
||||
next unless blob_data && blob_data["ref"]
|
||||
|
||||
# Create PostFile record for the image
|
||||
post_file =
|
||||
post.files.build(
|
||||
type: "Domain::PostFile::BlueskyPostFile",
|
||||
file_order: index,
|
||||
url_str: construct_blob_url(did, blob_data["ref"]["$link"]),
|
||||
state: "pending",
|
||||
alt_text: image_data["alt"],
|
||||
blob_ref: blob_data["ref"]["$link"],
|
||||
)
|
||||
|
||||
# Store aspect ratio if present
|
||||
if image_data["aspectRatio"]
|
||||
post_file.aspect_ratio_width = image_data["aspectRatio"]["width"]
|
||||
post_file.aspect_ratio_height = image_data["aspectRatio"]["height"]
|
||||
end
|
||||
|
||||
post_file.save!
|
||||
Domain::StaticFileJob.perform_later({ post_file: })
|
||||
files << post_file
|
||||
end
|
||||
|
||||
logger.info(
|
||||
"created #{files.size} #{"file".pluralize(files.size)} for post: #{post.bluesky_rkey} / #{did}",
|
||||
)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
external_data: T::Hash[String, T.untyped],
|
||||
did: String,
|
||||
).void
|
||||
end
|
||||
def process_external_embed(post, external_data, did)
|
||||
# Handle thumbnail image from external embeds (website cards)
|
||||
thumb_data = external_data["thumb"]
|
||||
return unless thumb_data && thumb_data["ref"]
|
||||
|
||||
post_file =
|
||||
post.files.build(
|
||||
type: "Domain::PostFile::BlueskyPostFile",
|
||||
file_order: 0,
|
||||
url_str: construct_blob_url(did, thumb_data["ref"]["$link"]),
|
||||
state: "pending",
|
||||
)
|
||||
|
||||
# Store metadata
|
||||
post_file.alt_text = "Website preview thumbnail"
|
||||
post_file.blob_ref = thumb_data["ref"]["$link"]
|
||||
post_file.save!
|
||||
|
||||
logger.info("created bluesky external thumbnail: #{post_file.url_str}")
|
||||
end
|
||||
|
||||
sig { params(did: String, cid: String).returns(String) }
|
||||
def construct_blob_url(did, cid)
|
||||
# Construct the Bluesky blob URL using the AT Protocol getBlob endpoint
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{did}&cid=#{cid}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
|
||||
class Domain::PostFile::BlueskyPostFile < Domain::PostFile
|
||||
aux_table :bluesky
|
||||
validates :file_order, presence: true, uniqueness: true
|
||||
validates :file_order, presence: true, uniqueness: { scope: :post_id }
|
||||
end
|
||||
|
||||
@@ -17,6 +17,7 @@ class Domain::User::BlueskyUser < Domain::User
|
||||
validates :state, presence: true
|
||||
|
||||
after_initialize { self.state ||= "ok" if new_record? }
|
||||
after_commit :enqueue_initial_scan, on: :create
|
||||
|
||||
sig { override.returns([String, Symbol]) }
|
||||
def self.param_prefix_and_attribute
|
||||
@@ -81,4 +82,15 @@ class Domain::User::BlueskyUser < Domain::User
|
||||
"Unknown"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig { void }
|
||||
def enqueue_initial_scan
|
||||
# Only enqueue for valid users with proper DIDs and handles
|
||||
return unless state_ok? && did.present? && handle.present?
|
||||
|
||||
# Enqueue the scan job to run immediately
|
||||
Domain::Bluesky::Job::ScanUserJob.perform_later({ user: self })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddBlueskyMediaFieldsToPostFiles < ActiveRecord::Migration[7.2]
|
||||
extend T::Sig
|
||||
|
||||
sig { void }
|
||||
def change
|
||||
# Add media metadata fields to the Bluesky post files aux table
|
||||
change_table :domain_post_files_bluesky_aux do |t|
|
||||
t.text :alt_text # Alt text for accessibility
|
||||
t.integer :aspect_ratio_width # Image aspect ratio width
|
||||
t.integer :aspect_ratio_height # Image aspect ratio height
|
||||
t.string :blob_ref # Bluesky blob CID reference
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1261,7 +1261,11 @@ CREATE TABLE public.domain_post_files (
|
||||
|
||||
CREATE TABLE public.domain_post_files_bluesky_aux (
|
||||
base_table_id bigint NOT NULL,
|
||||
file_order integer NOT NULL
|
||||
file_order integer NOT NULL,
|
||||
alt_text text,
|
||||
aspect_ratio_width integer,
|
||||
aspect_ratio_height integer,
|
||||
blob_ref character varying
|
||||
);
|
||||
|
||||
|
||||
@@ -5841,6 +5845,7 @@ ALTER TABLE ONLY public.domain_twitter_tweets
|
||||
SET search_path TO "$user", public;
|
||||
|
||||
INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20250808004604'),
|
||||
('20250805200056'),
|
||||
('20250805191557'),
|
||||
('20250805070115'),
|
||||
|
||||
114
sorbet/rbi/dsl/domain/post/bluesky_post.rbi
generated
114
sorbet/rbi/dsl/domain/post/bluesky_post.rbi
generated
@@ -1003,96 +1003,6 @@ class Domain::Post::BlueskyPost
|
||||
sig { void }
|
||||
def created_at_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def creator_did=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def creator_did?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def creator_did_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def creator_did_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def creator_did_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def creator_did_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def creator_did_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def creator_did_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def creator_did_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did_was; end
|
||||
|
||||
sig { void }
|
||||
def creator_did_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def first_seen_entry_id_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def first_seen_entry_id_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def first_seen_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def first_seen_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_was; end
|
||||
|
||||
sig { void }
|
||||
def first_seen_entry_id_will_change!; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def hashtags; end
|
||||
|
||||
@@ -1835,12 +1745,6 @@ class Domain::Post::BlueskyPost
|
||||
sig { void }
|
||||
def restore_created_at!; end
|
||||
|
||||
sig { void }
|
||||
def restore_creator_did!; end
|
||||
|
||||
sig { void }
|
||||
def restore_first_seen_entry_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_hashtags!; end
|
||||
|
||||
@@ -1931,18 +1835,6 @@ class Domain::Post::BlueskyPost
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_created_at?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_creator_did; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_creator_did?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_first_seen_entry_id; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.untyped, T.untyped])) }
|
||||
def saved_change_to_hashtags; end
|
||||
|
||||
@@ -2377,12 +2269,6 @@ class Domain::Post::BlueskyPost
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_created_at?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_creator_did?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_hashtags?; end
|
||||
|
||||
|
||||
228
sorbet/rbi/dsl/domain/post_file/bluesky_post_file.rbi
generated
228
sorbet/rbi/dsl/domain/post_file/bluesky_post_file.rbi
generated
@@ -754,6 +754,186 @@ class Domain::PostFile::BlueskyPostFile
|
||||
end
|
||||
|
||||
module GeneratedAttributeMethods
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def alt_text=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def alt_text?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def alt_text_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def alt_text_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def alt_text_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def alt_text_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def alt_text_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def alt_text_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def alt_text_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text_was; end
|
||||
|
||||
sig { void }
|
||||
def alt_text_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def aspect_ratio_height?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def aspect_ratio_height_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def aspect_ratio_height_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_height_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_height_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def aspect_ratio_height_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_height_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def aspect_ratio_height_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height_was; end
|
||||
|
||||
sig { void }
|
||||
def aspect_ratio_height_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def aspect_ratio_width?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def aspect_ratio_width_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def aspect_ratio_width_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_width_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_width_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def aspect_ratio_width_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_width_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def aspect_ratio_width_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width_was; end
|
||||
|
||||
sig { void }
|
||||
def aspect_ratio_width_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def blob_ref=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def blob_ref?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def blob_ref_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def blob_ref_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def blob_ref_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def blob_ref_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def blob_ref_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def blob_ref_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def blob_ref_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref_was; end
|
||||
|
||||
sig { void }
|
||||
def blob_ref_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_sha256; end
|
||||
|
||||
@@ -1169,6 +1349,18 @@ class Domain::PostFile::BlueskyPostFile
|
||||
sig { void }
|
||||
def post_id_will_change!; end
|
||||
|
||||
sig { void }
|
||||
def restore_alt_text!; end
|
||||
|
||||
sig { void }
|
||||
def restore_aspect_ratio_height!; end
|
||||
|
||||
sig { void }
|
||||
def restore_aspect_ratio_width!; end
|
||||
|
||||
sig { void }
|
||||
def restore_blob_ref!; end
|
||||
|
||||
sig { void }
|
||||
def restore_blob_sha256!; end
|
||||
|
||||
@@ -1256,6 +1448,30 @@ class Domain::PostFile::BlueskyPostFile
|
||||
sig { void }
|
||||
def retry_count_will_change!; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_alt_text; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_alt_text?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_aspect_ratio_height; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_aspect_ratio_height?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_aspect_ratio_width; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_aspect_ratio_width?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_blob_ref; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_blob_ref?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_blob_sha256; end
|
||||
|
||||
@@ -1540,6 +1756,18 @@ class Domain::PostFile::BlueskyPostFile
|
||||
sig { void }
|
||||
def url_str_will_change!; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_alt_text?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_aspect_ratio_height?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_aspect_ratio_width?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_blob_ref?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_blob_sha256?; end
|
||||
|
||||
|
||||
114
sorbet/rbi/dsl/domain/user/bluesky_user.rbi
generated
114
sorbet/rbi/dsl/domain/user/bluesky_user.rbi
generated
@@ -1057,51 +1057,6 @@ class Domain::User::BlueskyUser
|
||||
sig { void }
|
||||
def display_name_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def first_seen_entry_id_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def first_seen_entry_id_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def first_seen_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def first_seen_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_was; end
|
||||
|
||||
sig { void }
|
||||
def first_seen_entry_id_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def followers_count; end
|
||||
|
||||
@@ -1372,51 +1327,6 @@ class Domain::User::BlueskyUser
|
||||
sig { void }
|
||||
def json_attributes_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def last_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def last_seen_entry_id_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def last_seen_entry_id_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def last_seen_entry_id_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def last_seen_entry_id_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def last_seen_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def last_seen_entry_id_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def last_seen_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id_was; end
|
||||
|
||||
sig { void }
|
||||
def last_seen_entry_id_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Time)) }
|
||||
def migrated_user_favs_at; end
|
||||
|
||||
@@ -1564,9 +1474,6 @@ class Domain::User::BlueskyUser
|
||||
sig { void }
|
||||
def restore_display_name!; end
|
||||
|
||||
sig { void }
|
||||
def restore_first_seen_entry_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_followers_count!; end
|
||||
|
||||
@@ -1585,9 +1492,6 @@ class Domain::User::BlueskyUser
|
||||
sig { void }
|
||||
def restore_json_attributes!; end
|
||||
|
||||
sig { void }
|
||||
def restore_last_seen_entry_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_migrated_user_favs_at!; end
|
||||
|
||||
@@ -1651,12 +1555,6 @@ class Domain::User::BlueskyUser
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_display_name?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_first_seen_entry_id; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_followers_count; end
|
||||
|
||||
@@ -1693,12 +1591,6 @@ class Domain::User::BlueskyUser
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_json_attributes?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_last_seen_entry_id; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_last_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Time), T.nilable(::Time)])) }
|
||||
def saved_change_to_migrated_user_favs_at; end
|
||||
|
||||
@@ -2269,9 +2161,6 @@ class Domain::User::BlueskyUser
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_display_name?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_followers_count?; end
|
||||
|
||||
@@ -2290,9 +2179,6 @@ class Domain::User::BlueskyUser
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_json_attributes?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_last_seen_entry_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_migrated_user_favs_at?; end
|
||||
|
||||
|
||||
228
sorbet/rbi/dsl/domain_post_files_bluesky_aux.rbi
generated
228
sorbet/rbi/dsl/domain_post_files_bluesky_aux.rbi
generated
@@ -618,6 +618,141 @@ class DomainPostFilesBlueskyAux
|
||||
end
|
||||
|
||||
module GeneratedAttributeMethods
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def alt_text=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def alt_text?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def alt_text_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def alt_text_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def alt_text_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def alt_text_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def alt_text_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def alt_text_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def alt_text_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def alt_text_was; end
|
||||
|
||||
sig { void }
|
||||
def alt_text_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def aspect_ratio_height?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def aspect_ratio_height_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def aspect_ratio_height_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_height_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_height_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def aspect_ratio_height_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_height_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def aspect_ratio_height_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_height_was; end
|
||||
|
||||
sig { void }
|
||||
def aspect_ratio_height_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def aspect_ratio_width?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def aspect_ratio_width_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def aspect_ratio_width_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_width_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_width_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def aspect_ratio_width_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def aspect_ratio_width_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def aspect_ratio_width_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def aspect_ratio_width_was; end
|
||||
|
||||
sig { void }
|
||||
def aspect_ratio_width_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def base_table_id; end
|
||||
|
||||
@@ -663,6 +798,51 @@ class DomainPostFilesBlueskyAux
|
||||
sig { void }
|
||||
def base_table_id_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def blob_ref=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def blob_ref?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def blob_ref_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def blob_ref_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def blob_ref_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def blob_ref_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def blob_ref_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def blob_ref_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def blob_ref_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def blob_ref_was; end
|
||||
|
||||
sig { void }
|
||||
def blob_ref_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def file_order; end
|
||||
|
||||
@@ -753,21 +933,57 @@ class DomainPostFilesBlueskyAux
|
||||
sig { void }
|
||||
def id_will_change!; end
|
||||
|
||||
sig { void }
|
||||
def restore_alt_text!; end
|
||||
|
||||
sig { void }
|
||||
def restore_aspect_ratio_height!; end
|
||||
|
||||
sig { void }
|
||||
def restore_aspect_ratio_width!; end
|
||||
|
||||
sig { void }
|
||||
def restore_base_table_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_blob_ref!; end
|
||||
|
||||
sig { void }
|
||||
def restore_file_order!; end
|
||||
|
||||
sig { void }
|
||||
def restore_id!; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_alt_text; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_alt_text?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_aspect_ratio_height; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_aspect_ratio_height?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_aspect_ratio_width; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_aspect_ratio_width?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_base_table_id; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_base_table_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_blob_ref; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_blob_ref?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_file_order; end
|
||||
|
||||
@@ -780,9 +996,21 @@ class DomainPostFilesBlueskyAux
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_alt_text?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_aspect_ratio_height?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_aspect_ratio_width?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_base_table_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_blob_ref?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_file_order?; end
|
||||
|
||||
|
||||
114
sorbet/rbi/dsl/domain_posts_bluesky_aux.rbi
generated
114
sorbet/rbi/dsl/domain_posts_bluesky_aux.rbi
generated
@@ -833,96 +833,6 @@ class DomainPostsBlueskyAux
|
||||
sig { void }
|
||||
def bluesky_rkey_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def creator_did=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def creator_did?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def creator_did_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def creator_did_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def creator_did_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def creator_did_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def creator_did_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def creator_did_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def creator_did_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def creator_did_was; end
|
||||
|
||||
sig { void }
|
||||
def creator_did_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def first_seen_entry_id_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def first_seen_entry_id_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def first_seen_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def first_seen_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_was; end
|
||||
|
||||
sig { void }
|
||||
def first_seen_entry_id_will_change!; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def hashtags; end
|
||||
|
||||
@@ -1475,12 +1385,6 @@ class DomainPostsBlueskyAux
|
||||
sig { void }
|
||||
def restore_bluesky_rkey!; end
|
||||
|
||||
sig { void }
|
||||
def restore_creator_did!; end
|
||||
|
||||
sig { void }
|
||||
def restore_first_seen_entry_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_hashtags!; end
|
||||
|
||||
@@ -1553,18 +1457,6 @@ class DomainPostsBlueskyAux
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_bluesky_rkey?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_creator_did; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_creator_did?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_first_seen_entry_id; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.untyped, T.untyped])) }
|
||||
def saved_change_to_hashtags; end
|
||||
|
||||
@@ -1873,12 +1765,6 @@ class DomainPostsBlueskyAux
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_bluesky_rkey?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_creator_did?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_hashtags?; end
|
||||
|
||||
|
||||
114
sorbet/rbi/dsl/domain_users_bluesky_aux.rbi
generated
114
sorbet/rbi/dsl/domain_users_bluesky_aux.rbi
generated
@@ -835,51 +835,6 @@ class DomainUsersBlueskyAux
|
||||
sig { void }
|
||||
def display_name_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def first_seen_entry_id_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def first_seen_entry_id_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def first_seen_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def first_seen_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_was; end
|
||||
|
||||
sig { void }
|
||||
def first_seen_entry_id_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def followers_count; end
|
||||
|
||||
@@ -1060,51 +1015,6 @@ class DomainUsersBlueskyAux
|
||||
sig { void }
|
||||
def id_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id; end
|
||||
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def last_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def last_seen_entry_id_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def last_seen_entry_id_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def last_seen_entry_id_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def last_seen_entry_id_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def last_seen_entry_id_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def last_seen_entry_id_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::Integer), to: T.nilable(::Integer)).returns(T::Boolean) }
|
||||
def last_seen_entry_id_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def last_seen_entry_id_was; end
|
||||
|
||||
sig { void }
|
||||
def last_seen_entry_id_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def posts_count; end
|
||||
|
||||
@@ -1207,9 +1117,6 @@ class DomainUsersBlueskyAux
|
||||
sig { void }
|
||||
def restore_display_name!; end
|
||||
|
||||
sig { void }
|
||||
def restore_first_seen_entry_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_followers_count!; end
|
||||
|
||||
@@ -1222,9 +1129,6 @@ class DomainUsersBlueskyAux
|
||||
sig { void }
|
||||
def restore_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_last_seen_entry_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_posts_count!; end
|
||||
|
||||
@@ -1264,12 +1168,6 @@ class DomainUsersBlueskyAux
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_display_name?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_first_seen_entry_id; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_followers_count; end
|
||||
|
||||
@@ -1294,12 +1192,6 @@ class DomainUsersBlueskyAux
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_last_seen_entry_id; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_last_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_posts_count; end
|
||||
|
||||
@@ -1507,9 +1399,6 @@ class DomainUsersBlueskyAux
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_display_name?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_followers_count?; end
|
||||
|
||||
@@ -1522,9 +1411,6 @@ class DomainUsersBlueskyAux
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_last_seen_entry_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_posts_count?; end
|
||||
|
||||
|
||||
206
spec/jobs/domain/bluesky/job/scan_user_job_spec.rb
Normal file
206
spec/jobs/domain/bluesky/job/scan_user_job_spec.rb
Normal file
@@ -0,0 +1,206 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Domain::Bluesky::Job::ScanUserJob do
|
||||
include PerformJobHelpers
|
||||
|
||||
let(:http_client_mock) { instance_double("::Scraper::HttpClient") }
|
||||
let(:user) do
|
||||
create(
|
||||
:domain_user_bluesky_user,
|
||||
did: "did:plc:test123",
|
||||
handle: "testuser.bsky.social",
|
||||
scanned_profile_at: nil,
|
||||
scanned_posts_at: nil,
|
||||
)
|
||||
end
|
||||
|
||||
before { Scraper::ClientFactory.http_client_mock = http_client_mock }
|
||||
|
||||
describe "#perform" do
|
||||
context "when user profile scanning is due" do
|
||||
let(:profile_response_body) do
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.actor.profile/self",
|
||||
"cid" => "bafyreiabc123",
|
||||
"value" => {
|
||||
"displayName" => "Test User",
|
||||
"description" => "A test user profile",
|
||||
"avatar" => {
|
||||
"ref" => {
|
||||
"$link" => "bafkreiavatar123",
|
||||
},
|
||||
"mimeType" => "image/jpeg",
|
||||
"size" => 50_000,
|
||||
},
|
||||
},
|
||||
}.to_json
|
||||
end
|
||||
|
||||
let(:posts_response_body) do
|
||||
{
|
||||
"records" => [
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/post1",
|
||||
"cid" => "bafyreiapost123",
|
||||
"value" => {
|
||||
"text" => "Hello world with image!",
|
||||
"createdAt" => "2025-01-08T12:00:00.000Z",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "Test image",
|
||||
"aspectRatio" => {
|
||||
"width" => 1920,
|
||||
"height" => 1080,
|
||||
},
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreiimage123",
|
||||
},
|
||||
"mimeType" => "image/jpeg",
|
||||
"size" => 256_000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/post2",
|
||||
"cid" => "bafyreiapost456",
|
||||
"value" => {
|
||||
"text" => "Just a text post",
|
||||
"createdAt" => "2025-01-08T11:00:00.000Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
"cursor" => nil,
|
||||
}.to_json
|
||||
end
|
||||
|
||||
before do
|
||||
# Mock profile API call
|
||||
expect(http_client_mock).to receive(:get).with(
|
||||
"https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=#{user.did}&collection=app.bsky.actor.profile&rkey=self",
|
||||
anything,
|
||||
).and_return(
|
||||
double(
|
||||
status_code: 200,
|
||||
body: profile_response_body,
|
||||
log_entry: double,
|
||||
),
|
||||
)
|
||||
|
||||
# Mock posts API call
|
||||
expect(http_client_mock).to receive(:get).with(
|
||||
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=#{user.did}&collection=app.bsky.feed.post&limit=100",
|
||||
anything,
|
||||
).and_return(
|
||||
double(
|
||||
status_code: 200,
|
||||
body: posts_response_body,
|
||||
log_entry: double,
|
||||
),
|
||||
)
|
||||
|
||||
# Mock static file job enqueueing - allow it but don't require it
|
||||
allow(Domain::StaticFileJob).to receive(:perform_later)
|
||||
end
|
||||
|
||||
it "scans user profile and updates user data" do
|
||||
perform_now({ user: user })
|
||||
|
||||
user.reload
|
||||
expect(user.display_name).to eq("Test User")
|
||||
expect(user.description).to eq("A test user profile")
|
||||
expect(user.scanned_profile_at).to be_present
|
||||
expect(user.scanned_posts_at).to be_present
|
||||
expect(user.state).to eq("ok")
|
||||
end
|
||||
|
||||
it "creates avatar for user" do
|
||||
expect { perform_now({ user: user }) }.to change {
|
||||
user.reload.avatar.present?
|
||||
}.from(false).to(true)
|
||||
|
||||
avatar = user.reload.avatar
|
||||
expect(avatar.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreiavatar123",
|
||||
)
|
||||
end
|
||||
|
||||
it "creates posts with media and associated files" do
|
||||
expect { perform_now({ user: user }) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(1).and change(Domain::PostFile::BlueskyPostFile, :count).by(1)
|
||||
|
||||
post = Domain::Post::BlueskyPost.last
|
||||
expect(post.text).to eq("Hello world with image!")
|
||||
expect(post.creator).to eq(user)
|
||||
expect(post.bluesky_rkey).to eq("post1")
|
||||
|
||||
file = post.files.first
|
||||
expect(file.alt_text).to eq("Test image")
|
||||
expect(file.blob_ref).to eq("bafkreiimage123")
|
||||
expect(file.aspect_ratio_width).to eq(1920)
|
||||
expect(file.aspect_ratio_height).to eq(1080)
|
||||
expect(file.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreiimage123",
|
||||
)
|
||||
end
|
||||
|
||||
it "does not create posts without media" do
|
||||
perform_now({ user: user })
|
||||
|
||||
# Should only create 1 post (the one with media), not the text-only post
|
||||
expect(Domain::Post::BlueskyPost.count).to eq(1)
|
||||
expect(Domain::Post::BlueskyPost.first.text).to eq(
|
||||
"Hello world with image!",
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user already scanned recently" do
|
||||
before do
|
||||
user.update!(scanned_profile_at: 1.day.ago, scanned_posts_at: 1.day.ago)
|
||||
end
|
||||
|
||||
it "skips scanning if not due" do
|
||||
expect(http_client_mock).not_to receive(:get)
|
||||
|
||||
perform_now({ user: user })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "user creation callback" do
|
||||
it "enqueues scan job when user is created" do
|
||||
expect(Domain::Bluesky::Job::ScanUserJob).to receive(:perform_later).with(
|
||||
{ user: instance_of(Domain::User::BlueskyUser) },
|
||||
)
|
||||
|
||||
create(
|
||||
:domain_user_bluesky_user,
|
||||
did: "did:plc:newuser123",
|
||||
handle: "newuser.bsky.social",
|
||||
)
|
||||
end
|
||||
|
||||
it "does not enqueue scan job for users in error state" do
|
||||
expect(Domain::Bluesky::Job::ScanUserJob).not_to receive(:perform_later)
|
||||
|
||||
create(
|
||||
:domain_user_bluesky_user,
|
||||
did: "did:plc:erroruser123",
|
||||
handle: "erroruser.bsky.social",
|
||||
state: "error",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
462
spec/lib/tasks/bluesky/monitor_spec.rb
Normal file
462
spec/lib/tasks/bluesky/monitor_spec.rb
Normal file
@@ -0,0 +1,462 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
require_relative "../../../../app/lib/tasks/bluesky/monitor"
|
||||
|
||||
RSpec.describe Tasks::Bluesky::Monitor do
|
||||
subject(:monitor) { described_class.new(pg_notify: false) }
|
||||
|
||||
let(:test_did) { "did:plc:test123456789" }
|
||||
let(:base_time) { Time.parse("2025-01-08 12:00:00 UTC") }
|
||||
|
||||
before do
|
||||
# Add the test DID to the monitored set
|
||||
monitor.instance_variable_get(:@dids).add(test_did)
|
||||
|
||||
# Create a Bluesky user for the test DID
|
||||
create(
|
||||
:domain_user_bluesky_user,
|
||||
did: test_did,
|
||||
handle: "testuser.bsky.social",
|
||||
)
|
||||
end
|
||||
|
||||
# Helper method to create real CommitMessage objects
|
||||
def create_commit_message(did:, time:, rkey:, record:)
|
||||
message_json = {
|
||||
"did" => did,
|
||||
"time_us" => (time.to_f * 1_000_000).to_i,
|
||||
"kind" => "commit",
|
||||
"commit" => {
|
||||
"rev" => "#{rkey}rev",
|
||||
"operation" => "create",
|
||||
"collection" => "app.bsky.feed.post",
|
||||
"rkey" => rkey,
|
||||
"record" => record,
|
||||
"cid" => "bafyreih#{rkey}",
|
||||
},
|
||||
}
|
||||
|
||||
Skyfall::Jetstream::Message.new(message_json.to_json)
|
||||
end
|
||||
|
||||
describe "#handle_message" do
|
||||
context "when message is a commit with a post containing media" do
|
||||
context "with image embeds" do
|
||||
let(:commit_message) do
|
||||
create_commit_message(
|
||||
did: test_did,
|
||||
time: base_time,
|
||||
rkey: "test123",
|
||||
record: {
|
||||
"text" => "Check out this image!",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "A beautiful sunset",
|
||||
"aspectRatio" => {
|
||||
"height" => 1080,
|
||||
"width" => 1920,
|
||||
},
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreiabc123",
|
||||
},
|
||||
"mimeType" => "image/jpeg",
|
||||
"size" => 256_000,
|
||||
},
|
||||
},
|
||||
{
|
||||
"alt" => "",
|
||||
"aspectRatio" => {
|
||||
"height" => 800,
|
||||
"width" => 600,
|
||||
},
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreidef456",
|
||||
},
|
||||
"mimeType" => "image/png",
|
||||
"size" => 128_000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "creates a post with associated media files" do
|
||||
expect { monitor.handle_message(commit_message) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(1).and change(Domain::PostFile::BlueskyPostFile, :count).by(2)
|
||||
|
||||
post = Domain::Post::BlueskyPost.last
|
||||
expect(post.at_uri).to eq(
|
||||
"at://#{test_did}/app.bsky.feed.post/test123",
|
||||
)
|
||||
expect(post.text).to eq("Check out this image!")
|
||||
expect(post.bluesky_rkey).to eq("test123")
|
||||
expect(post.bluesky_created_at).to eq(base_time)
|
||||
|
||||
files = post.files.order(:file_order)
|
||||
expect(files.count).to eq(2)
|
||||
|
||||
# First image
|
||||
first_file = files.first
|
||||
expect(first_file.alt_text).to eq("A beautiful sunset")
|
||||
expect(first_file.blob_ref).to eq("bafkreiabc123")
|
||||
expect(first_file.aspect_ratio_width).to eq(1920)
|
||||
expect(first_file.aspect_ratio_height).to eq(1080)
|
||||
expect(first_file.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{test_did}&cid=bafkreiabc123",
|
||||
)
|
||||
expect(first_file.file_order).to eq(0)
|
||||
expect(first_file.state).to eq("pending")
|
||||
|
||||
# Second image
|
||||
second_file = files.second
|
||||
expect(second_file.alt_text).to eq("")
|
||||
expect(second_file.blob_ref).to eq("bafkreidef456")
|
||||
expect(second_file.aspect_ratio_width).to eq(600)
|
||||
expect(second_file.aspect_ratio_height).to eq(800)
|
||||
expect(second_file.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{test_did}&cid=bafkreidef456",
|
||||
)
|
||||
expect(second_file.file_order).to eq(1)
|
||||
expect(second_file.state).to eq("pending")
|
||||
end
|
||||
|
||||
it "enqueues download jobs for the media files" do
|
||||
expect(Domain::StaticFileJob).to receive(:perform_later).twice
|
||||
|
||||
monitor.handle_message(commit_message)
|
||||
end
|
||||
end
|
||||
|
||||
context "with recordWithMedia embed (quote post with media)" do
|
||||
let(:commit_message) do
|
||||
create_commit_message(
|
||||
did: test_did,
|
||||
time: base_time,
|
||||
rkey: "quote123",
|
||||
record: {
|
||||
"text" => "Quote tweet with media",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.recordWithMedia",
|
||||
"record" => {
|
||||
"$type" => "app.bsky.embed.record",
|
||||
"record" => {
|
||||
"uri" => "at://other.user/app.bsky.feed.post/abc123",
|
||||
"cid" => "bafyreianotherpost",
|
||||
},
|
||||
},
|
||||
"media" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "Quote post image",
|
||||
"aspectRatio" => {
|
||||
"height" => 720,
|
||||
"width" => 1280,
|
||||
},
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreiquote123",
|
||||
},
|
||||
"mimeType" => "image/webp",
|
||||
"size" => 64_000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "creates a post with media from the quote post" do
|
||||
expect { monitor.handle_message(commit_message) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(1).and change(Domain::PostFile::BlueskyPostFile, :count).by(1)
|
||||
|
||||
post = Domain::Post::BlueskyPost.last
|
||||
file = post.files.first
|
||||
|
||||
expect(file.alt_text).to eq("Quote post image")
|
||||
expect(file.blob_ref).to eq("bafkreiquote123")
|
||||
expect(file.aspect_ratio_width).to eq(1280)
|
||||
expect(file.aspect_ratio_height).to eq(720)
|
||||
end
|
||||
end
|
||||
|
||||
context "with external embed (website card with thumbnail)" do
|
||||
let(:commit_message) do
|
||||
create_commit_message(
|
||||
did: test_did,
|
||||
time: base_time,
|
||||
rkey: "external123",
|
||||
record: {
|
||||
"text" => "Check out this website",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.external",
|
||||
"external" => {
|
||||
"uri" => "https://example.com",
|
||||
"title" => "Example Website",
|
||||
"description" => "A great website",
|
||||
"thumb" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreithumb123",
|
||||
},
|
||||
"mimeType" => "image/jpeg",
|
||||
"size" => 32_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "creates a post with thumbnail file" do
|
||||
expect { monitor.handle_message(commit_message) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(1).and change(Domain::PostFile::BlueskyPostFile, :count).by(1)
|
||||
|
||||
post = Domain::Post::BlueskyPost.last
|
||||
file = post.files.first
|
||||
|
||||
expect(file.blob_ref).to eq("bafkreithumb123")
|
||||
expect(file.file_order).to eq(0)
|
||||
expect(file.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{test_did}&cid=bafkreithumb123",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is a commit with a post without media" do
|
||||
let(:commit_message) do
|
||||
instance_double(
|
||||
Skyfall::Jetstream::CommitMessage,
|
||||
type: :commit,
|
||||
did: test_did,
|
||||
time: base_time,
|
||||
operations: [operation],
|
||||
)
|
||||
end
|
||||
|
||||
let(:operation) do
|
||||
instance_double(
|
||||
Skyfall::Jetstream::Operation,
|
||||
action: :create,
|
||||
type: :bsky_post,
|
||||
uri: "at://#{test_did}/app.bsky.feed.post/textonly",
|
||||
rkey: "textonly",
|
||||
raw_record: {
|
||||
"text" => "Just a text post",
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "does not create a post" do
|
||||
expect { monitor.handle_message(commit_message) }.not_to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
)
|
||||
end
|
||||
|
||||
it "does not create any media files" do
|
||||
expect { monitor.handle_message(commit_message) }.not_to change(
|
||||
Domain::PostFile::BlueskyPostFile,
|
||||
:count,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is from a DID not in the monitored set" do
|
||||
let(:unmonitored_did) { "did:plc:unmonitored123" }
|
||||
let(:commit_message) do
|
||||
instance_double(
|
||||
Skyfall::Jetstream::CommitMessage,
|
||||
type: :commit,
|
||||
did: unmonitored_did,
|
||||
time: base_time,
|
||||
operations: [operation],
|
||||
)
|
||||
end
|
||||
|
||||
let(:operation) do
|
||||
instance_double(
|
||||
Skyfall::Jetstream::Operation,
|
||||
action: :create,
|
||||
type: :bsky_post,
|
||||
uri: "at://#{unmonitored_did}/app.bsky.feed.post/test123",
|
||||
rkey: "test123",
|
||||
raw_record: {
|
||||
"text" => "Post with media from unmonitored user",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "Should be ignored",
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreiignored",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "does not create a post" do
|
||||
expect { monitor.handle_message(commit_message) }.not_to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when message is not a commit" do
|
||||
let(:non_commit_message) do
|
||||
instance_double(Skyfall::Jetstream::Message, type: :account)
|
||||
end
|
||||
|
||||
it "does not create a post" do
|
||||
expect { monitor.handle_message(non_commit_message) }.not_to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when operation is not a create action" do
|
||||
let(:commit_message) do
|
||||
instance_double(
|
||||
Skyfall::Jetstream::CommitMessage,
|
||||
type: :commit,
|
||||
did: test_did,
|
||||
time: base_time,
|
||||
operations: [operation],
|
||||
)
|
||||
end
|
||||
|
||||
let(:operation) do
|
||||
instance_double(
|
||||
Skyfall::Jetstream::Operation,
|
||||
action: :delete,
|
||||
type: :bsky_post,
|
||||
uri: "at://#{test_did}/app.bsky.feed.post/deleted",
|
||||
rkey: "deleted",
|
||||
raw_record: {
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "does not create a post" do
|
||||
expect { monitor.handle_message(commit_message) }.not_to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "when operation is not a bsky_post type" do
|
||||
let(:commit_message) do
|
||||
instance_double(
|
||||
Skyfall::Jetstream::CommitMessage,
|
||||
type: :commit,
|
||||
did: test_did,
|
||||
time: base_time,
|
||||
operations: [operation],
|
||||
)
|
||||
end
|
||||
|
||||
let(:operation) do
|
||||
instance_double(
|
||||
Skyfall::Jetstream::Operation,
|
||||
action: :create,
|
||||
type: :bsky_like,
|
||||
uri: "at://#{test_did}/app.bsky.feed.like/like123",
|
||||
rkey: "like123",
|
||||
raw_record: {
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "does not create a post" do
|
||||
expect { monitor.handle_message(commit_message) }.not_to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "edge cases" do
|
||||
context "with malformed embed data" do
|
||||
let(:commit_message) do
|
||||
create_commit_message(
|
||||
did: test_did,
|
||||
time: base_time,
|
||||
rkey: "malformed123",
|
||||
record: {
|
||||
"text" => "Post with malformed embed",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "Missing image data",
|
||||
# Missing "image" field
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "creates the post but skips malformed media" do
|
||||
expect { monitor.handle_message(commit_message) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(1).and not_change(Domain::PostFile::BlueskyPostFile, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context "with unknown embed type" do
|
||||
let(:commit_message) do
|
||||
create_commit_message(
|
||||
did: test_did,
|
||||
time: base_time,
|
||||
rkey: "unknown123",
|
||||
record: {
|
||||
"text" => "Post with unknown embed type",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.unknown",
|
||||
"data" => "some data",
|
||||
},
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "creates the post but does not process the unknown embed" do
|
||||
expect { monitor.handle_message(commit_message) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(1).and not_change(Domain::PostFile::BlueskyPostFile, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user