video post downloading
This commit is contained in:
@@ -283,7 +283,7 @@ class Domain::PostsController < DomainController
|
||||
relation = T.unsafe(policy_scope(relation)).page(params[:page]).per(50)
|
||||
relation =
|
||||
relation.order(
|
||||
relation.klass.post_order_attribute => :desc,
|
||||
"#{relation.klass.post_order_attribute} DESC, id DESC",
|
||||
) unless skip_ordering
|
||||
relation
|
||||
end
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# typed: strict
|
||||
class Domain::Bluesky::Job::ScanPostsJob < Domain::Bluesky::Job::Base
|
||||
MEDIA_EMBED_TYPES = %w[app.bsky.embed.images app.bsky.embed.video]
|
||||
|
||||
self.default_priority = -25
|
||||
|
||||
sig { override.params(args: T::Hash[Symbol, T.untyped]).returns(T.untyped) }
|
||||
@@ -34,6 +36,62 @@ class Domain::Bluesky::Job::ScanPostsJob < Domain::Bluesky::Job::Base
|
||||
|
||||
private
|
||||
|
||||
sig do
|
||||
params(
|
||||
user: Domain::User::BlueskyUser,
|
||||
record_data: T::Hash[String, T.untyped],
|
||||
).returns(T::Boolean)
|
||||
end
|
||||
def should_record_post?(user, record_data)
|
||||
# Check for quotes first - skip quotes of other users' posts
|
||||
quote_uri = extract_quote_uri(record_data)
|
||||
if quote_uri
|
||||
# Extract DID from the quoted post URI
|
||||
quoted_did = quote_uri.split("/")[2]
|
||||
return false unless quoted_did == user.did
|
||||
end
|
||||
|
||||
# Check for replies - only record if it's a root post or reply to user's own post
|
||||
return true unless record_data.dig("value", "reply")
|
||||
|
||||
# For replies, check if the root post is by the same user
|
||||
reply_data = record_data.dig("value", "reply")
|
||||
root_uri = reply_data.dig("root", "uri")
|
||||
|
||||
return true unless root_uri # If we can't determine root, allow it
|
||||
|
||||
# Extract DID from the root post URI
|
||||
# AT URI format: at://did:plc:xyz/app.bsky.feed.post/rkey
|
||||
root_did = root_uri.split("/")[2]
|
||||
|
||||
# Only record if the root post is by the same user
|
||||
root_did == user.did
|
||||
end
|
||||
|
||||
sig { params(record: T::Hash[String, T.untyped]).returns(T.nilable(String)) }
|
||||
def extract_quote_uri(record)
|
||||
# Check for quote in embed data
|
||||
embed = record["embed"]
|
||||
return nil unless embed
|
||||
|
||||
case embed["$type"]
|
||||
when "app.bsky.embed.record"
|
||||
# Direct quote - check if it's actually a quote of a post
|
||||
record_data = embed["record"]
|
||||
if record_data && record_data["uri"]&.include?("app.bsky.feed.post")
|
||||
record_data["uri"]
|
||||
end
|
||||
when "app.bsky.embed.recordWithMedia"
|
||||
# Quote with media
|
||||
record_data = embed.dig("record", "record")
|
||||
if record_data && record_data["uri"]&.include?("app.bsky.feed.post")
|
||||
record_data["uri"]
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
sig { params(user: Domain::User::BlueskyUser).void }
|
||||
def scan_user_posts(user)
|
||||
# Use AT Protocol API to list user's posts
|
||||
@@ -43,6 +101,7 @@ class Domain::Bluesky::Job::ScanPostsJob < Domain::Bluesky::Job::Base
|
||||
cursor = T.let(nil, T.nilable(String))
|
||||
num_processed_posts = 0
|
||||
num_posts_with_media = 0
|
||||
num_filtered_posts = 0
|
||||
num_created_posts = 0
|
||||
num_pages = 0
|
||||
|
||||
@@ -75,12 +134,27 @@ class Domain::Bluesky::Job::ScanPostsJob < Domain::Bluesky::Job::Base
|
||||
records.each do |record_data|
|
||||
num_processed_posts += 1
|
||||
|
||||
record = record_data["value"]
|
||||
next unless record && record["embed"]
|
||||
embed_type = record_data.dig("value", "embed", "$type")
|
||||
unless MEDIA_EMBED_TYPES.include?(embed_type)
|
||||
logger.info(
|
||||
format_tags(
|
||||
"skipping post, non-media embed type",
|
||||
make_tags(embed_type:),
|
||||
),
|
||||
)
|
||||
next
|
||||
end
|
||||
|
||||
# Only process posts with media
|
||||
num_posts_with_media += 1
|
||||
if process_historical_post(user, record_data, record)
|
||||
|
||||
# Skip posts that are replies to other users or quotes
|
||||
unless should_record_post?(user, record_data)
|
||||
num_filtered_posts += 1
|
||||
next
|
||||
end
|
||||
|
||||
if process_historical_post(user, record_data, response.log_entry)
|
||||
num_created_posts += 1
|
||||
end
|
||||
end
|
||||
@@ -105,6 +179,7 @@ class Domain::Bluesky::Job::ScanPostsJob < Domain::Bluesky::Job::Base
|
||||
make_tags(
|
||||
num_processed_posts:,
|
||||
num_posts_with_media:,
|
||||
num_filtered_posts:,
|
||||
num_created_posts:,
|
||||
num_pages:,
|
||||
),
|
||||
@@ -116,51 +191,42 @@ class Domain::Bluesky::Job::ScanPostsJob < Domain::Bluesky::Job::Base
|
||||
params(
|
||||
user: Domain::User::BlueskyUser,
|
||||
record_data: T::Hash[String, T.untyped],
|
||||
record: T::Hash[String, T.untyped],
|
||||
log_entry: HttpLogEntry,
|
||||
).returns(T::Boolean)
|
||||
end
|
||||
def process_historical_post(user, record_data, record)
|
||||
uri = record_data["uri"]
|
||||
rkey = record_data["uri"].split("/").last
|
||||
def process_historical_post(user, record_data, log_entry)
|
||||
at_uri = record_data["uri"]
|
||||
|
||||
# Check if we already have this post
|
||||
existing_post = user.posts.find_by(bluesky_rkey: rkey)
|
||||
existing_post = user.posts.find_by(at_uri:)
|
||||
if existing_post
|
||||
enqueue_pending_files_job(existing_post)
|
||||
return false
|
||||
end
|
||||
|
||||
begin
|
||||
post =
|
||||
Domain::Post::BlueskyPost.create!(
|
||||
at_uri: uri,
|
||||
bluesky_rkey: rkey,
|
||||
text: record["text"] || "",
|
||||
posted_at: Time.parse(record["createdAt"]),
|
||||
post_raw: record,
|
||||
)
|
||||
# Extract reply and quote URIs from the raw post data
|
||||
reply_to_uri = record_data.dig("value", "reply", "root", "uri")
|
||||
quote_uri = extract_quote_uri(record_data)
|
||||
|
||||
post.creator = user
|
||||
post.save!
|
||||
|
||||
# Process media if present
|
||||
process_post_media(post, record["embed"], user.did!) if record["embed"]
|
||||
|
||||
logger.debug(
|
||||
format_tags(
|
||||
"created historical post",
|
||||
make_tags(bluesky_rkey: post.bluesky_rkey),
|
||||
),
|
||||
post =
|
||||
Domain::Post::BlueskyPost.build(
|
||||
state: "ok",
|
||||
at_uri: at_uri,
|
||||
first_seen_entry: log_entry,
|
||||
text: record_data.dig("value", "text") || "",
|
||||
posted_at: Time.parse(record_data.dig("value", "createdAt")),
|
||||
post_raw: record_data,
|
||||
reply_to_uri: reply_to_uri,
|
||||
quote_uri: quote_uri,
|
||||
)
|
||||
rescue => e
|
||||
logger.error(
|
||||
format_tags(
|
||||
"failed to create historical post",
|
||||
make_tags(bluesky_rkey: rkey, error: e.message),
|
||||
),
|
||||
)
|
||||
false
|
||||
end
|
||||
post.creator = user
|
||||
post.save!
|
||||
|
||||
# Process media if present
|
||||
embed = record_data.dig("value", "embed")
|
||||
process_post_media(post, embed, user.did!) if embed
|
||||
|
||||
logger.debug(format_tags("created post", make_tags(at_uri:)))
|
||||
|
||||
true
|
||||
end
|
||||
@@ -175,14 +241,16 @@ class Domain::Bluesky::Job::ScanPostsJob < Domain::Bluesky::Job::Base
|
||||
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)
|
||||
process_post_images(post, embed_data, did)
|
||||
when "app.bsky.embed.video"
|
||||
process_post_video(post, embed_data, 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)
|
||||
embed_type = embed_data.dig("media", "$type")
|
||||
if embed_type == "app.bsky.embed.images"
|
||||
process_post_images(post, embed_data["media"], did)
|
||||
elsif embed_type == "app.bsky.embed.video"
|
||||
process_post_video(post, embed_data["media"], did)
|
||||
end
|
||||
when "app.bsky.embed.external"
|
||||
process_external_embed(post, embed_data["external"], did)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -202,69 +270,87 @@ class Domain::Bluesky::Job::ScanPostsJob < Domain::Bluesky::Job::Base
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
images: T::Array[T::Hash[String, T.untyped]],
|
||||
embed_data: T::Hash[String, T.untyped],
|
||||
did: String,
|
||||
).void
|
||||
end
|
||||
def process_post_images(post, images, did)
|
||||
files = []
|
||||
def process_post_images(post, embed_data, did)
|
||||
images = embed_data.dig("images") || []
|
||||
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(
|
||||
file_order: index,
|
||||
url_str: construct_blob_url(did, blob_data["ref"]["$link"]),
|
||||
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 = post.files.build(file_order: index)
|
||||
set_blob_ref_and_url(post_file, image_data["image"], did)
|
||||
set_aspect_ratio(post_file, image_data["aspectRatio"])
|
||||
set_alt_text(post_file, image_data["alt"])
|
||||
|
||||
post_file.save!
|
||||
defer_job(Domain::StaticFileJob, { post_file: }, { queue: "bluesky" })
|
||||
files << post_file
|
||||
|
||||
logger.debug(
|
||||
format_tags(
|
||||
"created image for post",
|
||||
make_tags(at_uri: post.at_uri, post_file_id: post_file.id),
|
||||
),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
embed_data: T::Hash[String, T.untyped],
|
||||
did: String,
|
||||
).void
|
||||
end
|
||||
def process_post_video(post, embed_data, did)
|
||||
post_file = post.files.build(file_order: 0)
|
||||
set_blob_ref_and_url(post_file, embed_data["video"], did)
|
||||
set_aspect_ratio(post_file, embed_data["aspectRatio"])
|
||||
set_alt_text(post_file, embed_data["alt"])
|
||||
post_file.save!
|
||||
defer_job(Domain::StaticFileJob, { post_file: }, { queue: "bluesky" })
|
||||
|
||||
logger.debug(
|
||||
format_tags(
|
||||
"created files for historical post",
|
||||
make_tags(bluesky_rkey: post.bluesky_rkey, num_files: files.size),
|
||||
"created video for post",
|
||||
make_tags(at_uri: post.at_uri, post_file_id: post_file.id),
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
sig do
|
||||
params(
|
||||
post: Domain::Post::BlueskyPost,
|
||||
external_data: T::Hash[String, T.untyped],
|
||||
post_file: Domain::PostFile::BlueskyPostFile,
|
||||
file_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"]
|
||||
def set_blob_ref_and_url(post_file, file_data, did)
|
||||
return unless file_data.dig("$type") == "blob"
|
||||
blob_ref = file_data.dig("ref", "$link")
|
||||
return unless blob_ref
|
||||
post_file.blob_ref = blob_ref
|
||||
post_file.url_str = construct_blob_url(did, blob_ref)
|
||||
end
|
||||
|
||||
post_file =
|
||||
post.files.build(
|
||||
file_order: 0,
|
||||
url_str: construct_blob_url(did, thumb_data["ref"]["$link"]),
|
||||
blob_ref: thumb_data["ref"]["$link"],
|
||||
)
|
||||
sig do
|
||||
params(
|
||||
post_file: Domain::PostFile::BlueskyPostFile,
|
||||
aspect_ratio: T.nilable(T::Hash[String, T.untyped]),
|
||||
).void
|
||||
end
|
||||
def set_aspect_ratio(post_file, aspect_ratio)
|
||||
return unless aspect_ratio
|
||||
post_file.aspect_ratio_width = aspect_ratio.dig("width")
|
||||
post_file.aspect_ratio_height = aspect_ratio.dig("height")
|
||||
end
|
||||
|
||||
post_file.save!
|
||||
defer_job(Domain::StaticFileJob, { post_file: }, { queue: "bluesky" })
|
||||
|
||||
logger.debug(
|
||||
format_tags(
|
||||
"created thumbnail for post",
|
||||
make_tags(bluesky_rkey: post.bluesky_rkey),
|
||||
),
|
||||
)
|
||||
sig do
|
||||
params(
|
||||
post_file: Domain::PostFile::BlueskyPostFile,
|
||||
alt_text: T.nilable(String),
|
||||
).void
|
||||
end
|
||||
def set_alt_text(post_file, alt_text)
|
||||
post_file.alt_text = alt_text if alt_text
|
||||
end
|
||||
end
|
||||
|
||||
@@ -91,7 +91,7 @@ module Tasks::Bluesky
|
||||
|
||||
post =
|
||||
Domain::Post::BlueskyPost.find_or_create_by!(at_uri: op.uri) do |post|
|
||||
post.bluesky_rkey = op.rkey
|
||||
post.rkey = op.rkey
|
||||
post.text = op.raw_record["text"]
|
||||
post.posted_at = msg.time.in_time_zone("UTC")
|
||||
post.creator = creator_for(msg)
|
||||
@@ -100,9 +100,7 @@ module Tasks::Bluesky
|
||||
|
||||
process_media(post, embed_data, msg.did)
|
||||
|
||||
logger.info(
|
||||
"created bluesky post: `#{post.bluesky_rkey}` / `#{post.at_uri}`",
|
||||
)
|
||||
logger.info("created bluesky post: `#{post.rkey}` / `#{post.at_uri}`")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -225,7 +223,7 @@ module Tasks::Bluesky
|
||||
end
|
||||
|
||||
logger.info(
|
||||
"created #{files.size} #{"file".pluralize(files.size)} for post: #{post.bluesky_rkey} / #{did}",
|
||||
"created #{files.size} #{"file".pluralize(files.size)} for post: #{post.rkey} / #{did}",
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -14,11 +14,21 @@ class Domain::Post::BlueskyPost < Domain::Post
|
||||
|
||||
validates :state, presence: true
|
||||
validates :at_uri, presence: true, uniqueness: true
|
||||
validates :bluesky_rkey, presence: true
|
||||
validates :rkey, presence: true
|
||||
|
||||
before_validation { self.rkey = at_uri&.split("/")&.last }
|
||||
validate :rkey_matches_at_uri
|
||||
|
||||
sig { void }
|
||||
def rkey_matches_at_uri
|
||||
return true if rkey.blank? && at_uri.blank?
|
||||
return true if rkey == at_uri&.split("/")&.last
|
||||
errors.add(:rkey, "does not match AT URI")
|
||||
end
|
||||
|
||||
sig { override.returns([String, Symbol]) }
|
||||
def self.param_prefix_and_attribute
|
||||
["bsky", :bluesky_rkey]
|
||||
["bsky", :rkey]
|
||||
end
|
||||
|
||||
sig { override.returns(String) }
|
||||
@@ -43,16 +53,14 @@ class Domain::Post::BlueskyPost < Domain::Post
|
||||
|
||||
sig { override.returns(T.nilable(T.any(String, Integer))) }
|
||||
def domain_id_for_view
|
||||
self.bluesky_rkey
|
||||
self.rkey
|
||||
end
|
||||
|
||||
sig { override.returns(T.nilable(Addressable::URI)) }
|
||||
def external_url_for_view
|
||||
handle = self.creator&.handle
|
||||
if bluesky_rkey.present? && handle.present?
|
||||
Addressable::URI.parse(
|
||||
"https://bsky.app/profile/#{handle}/post/#{bluesky_rkey}",
|
||||
)
|
||||
if rkey.present? && handle.present?
|
||||
Addressable::URI.parse("https://bsky.app/profile/#{handle}/post/#{rkey}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -61,9 +69,14 @@ class Domain::Post::BlueskyPost < Domain::Post
|
||||
self.files.first
|
||||
end
|
||||
|
||||
sig { override.returns(T.nilable(HttpLogEntry)) }
|
||||
def scanned_post_log_entry_for_view
|
||||
self.first_seen_entry
|
||||
end
|
||||
|
||||
sig { override.returns(T::Boolean) }
|
||||
def pending_scan?
|
||||
scanned_at.nil?
|
||||
false
|
||||
end
|
||||
|
||||
sig { override.returns(T.nilable(String)) }
|
||||
|
||||
5
db/migrate/20250811172839_rename_rkey.rb
Normal file
5
db/migrate/20250811172839_rename_rkey.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class RenameRkey < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
rename_column :domain_posts_bluesky_aux, :bluesky_rkey, :rkey
|
||||
end
|
||||
end
|
||||
@@ -1409,11 +1409,10 @@ CREATE TABLE public.domain_posts (
|
||||
|
||||
CREATE TABLE public.domain_posts_bluesky_aux (
|
||||
base_table_id bigint NOT NULL,
|
||||
state character varying NOT NULL,
|
||||
bluesky_rkey character varying NOT NULL,
|
||||
at_uri character varying,
|
||||
state character varying NOT NULL,
|
||||
rkey character varying NOT NULL,
|
||||
text text,
|
||||
scanned_at timestamp(6) without time zone,
|
||||
language character varying,
|
||||
like_count integer,
|
||||
repost_count integer,
|
||||
@@ -1425,6 +1424,7 @@ CREATE TABLE public.domain_posts_bluesky_aux (
|
||||
reply_to_uri character varying,
|
||||
quote_uri character varying,
|
||||
post_raw jsonb DEFAULT '{}'::jsonb,
|
||||
first_seen_entry_id bigint,
|
||||
scan_error character varying
|
||||
);
|
||||
|
||||
@@ -4373,6 +4373,13 @@ CREATE INDEX index_domain_post_group_joins_on_type ON public.domain_post_group_j
|
||||
CREATE INDEX index_domain_post_groups_on_type ON public.domain_post_groups USING btree (type);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_domain_posts_bluesky_aux_on_at_uri; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE UNIQUE INDEX index_domain_posts_bluesky_aux_on_at_uri ON public.domain_posts_bluesky_aux USING btree (at_uri);
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_domain_posts_bluesky_aux_on_base_table_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
@@ -4381,10 +4388,10 @@ CREATE INDEX index_domain_posts_bluesky_aux_on_base_table_id ON public.domain_po
|
||||
|
||||
|
||||
--
|
||||
-- Name: index_domain_posts_bluesky_aux_on_bluesky_rkey; Type: INDEX; Schema: public; Owner: -
|
||||
-- Name: index_domain_posts_bluesky_aux_on_first_seen_entry_id; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX index_domain_posts_bluesky_aux_on_bluesky_rkey ON public.domain_posts_bluesky_aux USING btree (bluesky_rkey);
|
||||
CREATE INDEX index_domain_posts_bluesky_aux_on_first_seen_entry_id ON public.domain_posts_bluesky_aux USING btree (first_seen_entry_id);
|
||||
|
||||
|
||||
--
|
||||
@@ -5622,6 +5629,14 @@ ALTER TABLE ONLY public.domain_users_bluesky_aux
|
||||
ADD CONSTRAINT fk_rails_673dd1243a FOREIGN KEY (base_table_id) REFERENCES public.domain_users(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domain_posts_bluesky_aux fk_rails_7393623916; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
ALTER TABLE ONLY public.domain_posts_bluesky_aux
|
||||
ADD CONSTRAINT fk_rails_7393623916 FOREIGN KEY (first_seen_entry_id) REFERENCES public.http_log_entries(id);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domain_posts_e621_aux fk_rails_73ac068c64; Type: FK CONSTRAINT; Schema: public; Owner: -
|
||||
--
|
||||
@@ -5837,6 +5852,7 @@ ALTER TABLE ONLY public.domain_twitter_tweets
|
||||
SET search_path TO "$user", public;
|
||||
|
||||
INSERT INTO "schema_migrations" (version) VALUES
|
||||
('20250811172839'),
|
||||
('20250808004604'),
|
||||
('20250805200056'),
|
||||
('20250805191557'),
|
||||
|
||||
@@ -37,4 +37,11 @@ namespace :bluesky do
|
||||
|
||||
Bluesky::MonitoredDid.find_by(did: did)&.destroy!
|
||||
end
|
||||
|
||||
desc "Delete all bluesky posts/files"
|
||||
task delete_all: :environment do
|
||||
raise unless Rails.env.development?
|
||||
Domain::PostFile::BlueskyPostFile.destroy_all
|
||||
Domain::Post::BlueskyPost.destroy_all
|
||||
end
|
||||
end
|
||||
|
||||
27
sorbet/rbi/dsl/domain/bluesky/job/scan_posts_job.rbi
generated
Normal file
27
sorbet/rbi/dsl/domain/bluesky/job/scan_posts_job.rbi
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
# typed: true
|
||||
|
||||
# DO NOT EDIT MANUALLY
|
||||
# This is an autogenerated file for dynamic methods in `Domain::Bluesky::Job::ScanPostsJob`.
|
||||
# Please instead update this file by running `bin/tapioca dsl Domain::Bluesky::Job::ScanPostsJob`.
|
||||
|
||||
|
||||
class Domain::Bluesky::Job::ScanPostsJob
|
||||
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::Bluesky::Job::ScanPostsJob).void)
|
||||
).returns(T.any(Domain::Bluesky::Job::ScanPostsJob, FalseClass))
|
||||
end
|
||||
def perform_later(args, &block); end
|
||||
|
||||
sig { params(args: T::Hash[::Symbol, T.untyped]).returns(T.untyped) }
|
||||
def perform_now(args); end
|
||||
end
|
||||
end
|
||||
230
sorbet/rbi/dsl/domain/post/bluesky_post.rbi
generated
230
sorbet/rbi/dsl/domain/post/bluesky_post.rbi
generated
@@ -848,51 +848,6 @@ class Domain::Post::BlueskyPost
|
||||
sig { void }
|
||||
def at_uri_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def bluesky_rkey; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def bluesky_rkey=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def bluesky_rkey?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def bluesky_rkey_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def bluesky_rkey_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def bluesky_rkey_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def bluesky_rkey_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def bluesky_rkey_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def bluesky_rkey_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def bluesky_rkey_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def bluesky_rkey_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def bluesky_rkey_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def bluesky_rkey_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def bluesky_rkey_was; end
|
||||
|
||||
sig { void }
|
||||
def bluesky_rkey_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def created_at; end
|
||||
|
||||
@@ -948,6 +903,51 @@ class Domain::Post::BlueskyPost
|
||||
sig { void }
|
||||
def created_at_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
|
||||
|
||||
@@ -1682,10 +1682,10 @@ class Domain::Post::BlueskyPost
|
||||
def restore_at_uri!; end
|
||||
|
||||
sig { void }
|
||||
def restore_bluesky_rkey!; end
|
||||
def restore_created_at!; end
|
||||
|
||||
sig { void }
|
||||
def restore_created_at!; end
|
||||
def restore_first_seen_entry_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_hashtags!; end
|
||||
@@ -1736,10 +1736,10 @@ class Domain::Post::BlueskyPost
|
||||
def restore_repost_count!; end
|
||||
|
||||
sig { void }
|
||||
def restore_scan_error!; end
|
||||
def restore_rkey!; end
|
||||
|
||||
sig { void }
|
||||
def restore_scanned_at!; end
|
||||
def restore_scan_error!; end
|
||||
|
||||
sig { void }
|
||||
def restore_state!; end
|
||||
@@ -1753,24 +1753,69 @@ class Domain::Post::BlueskyPost
|
||||
sig { void }
|
||||
def restore_updated_at!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def rkey=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def rkey?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def rkey_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def rkey_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def rkey_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def rkey_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def rkey_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def rkey_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def rkey_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey_was; end
|
||||
|
||||
sig { void }
|
||||
def rkey_will_change!; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_at_uri; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_at_uri?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_bluesky_rkey; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_bluesky_rkey?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def saved_change_to_created_at; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_created_at?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def saved_change_to_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
|
||||
|
||||
@@ -1867,18 +1912,18 @@ class Domain::Post::BlueskyPost
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_repost_count?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_rkey; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_rkey?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_scan_error; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_scan_error?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def saved_change_to_scanned_at; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_scanned_at?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_state; end
|
||||
|
||||
@@ -1948,61 +1993,6 @@ class Domain::Post::BlueskyPost
|
||||
sig { void }
|
||||
def scan_error_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at; end
|
||||
|
||||
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def scanned_at?; end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def scanned_at_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def scanned_at_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def scanned_at_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def scanned_at_change_to_be_saved; end
|
||||
|
||||
sig do
|
||||
params(
|
||||
from: T.nilable(::ActiveSupport::TimeWithZone),
|
||||
to: T.nilable(::ActiveSupport::TimeWithZone)
|
||||
).returns(T::Boolean)
|
||||
end
|
||||
def scanned_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def scanned_at_previous_change; end
|
||||
|
||||
sig do
|
||||
params(
|
||||
from: T.nilable(::ActiveSupport::TimeWithZone),
|
||||
to: T.nilable(::ActiveSupport::TimeWithZone)
|
||||
).returns(T::Boolean)
|
||||
end
|
||||
def scanned_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at_was; end
|
||||
|
||||
sig { void }
|
||||
def scanned_at_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def state; end
|
||||
|
||||
@@ -2197,10 +2187,10 @@ class Domain::Post::BlueskyPost
|
||||
def will_save_change_to_at_uri?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_bluesky_rkey?; end
|
||||
def will_save_change_to_created_at?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_created_at?; end
|
||||
def will_save_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_hashtags?; end
|
||||
@@ -2251,10 +2241,10 @@ class Domain::Post::BlueskyPost
|
||||
def will_save_change_to_repost_count?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_scan_error?; end
|
||||
def will_save_change_to_rkey?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_scanned_at?; end
|
||||
def will_save_change_to_scan_error?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_state?; end
|
||||
|
||||
182
sorbet/rbi/dsl/domain_posts_bluesky_aux.rbi
generated
182
sorbet/rbi/dsl/domain_posts_bluesky_aux.rbi
generated
@@ -733,50 +733,50 @@ class DomainPostsBlueskyAux
|
||||
sig { void }
|
||||
def base_table_id_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def bluesky_rkey; end
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def bluesky_rkey=(value); end
|
||||
sig { params(value: T.nilable(::Integer)).returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def bluesky_rkey?; end
|
||||
def first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def bluesky_rkey_before_last_save; end
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def bluesky_rkey_before_type_cast; end
|
||||
def first_seen_entry_id_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def bluesky_rkey_came_from_user?; end
|
||||
def first_seen_entry_id_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def bluesky_rkey_change; end
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def bluesky_rkey_change_to_be_saved; 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(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def bluesky_rkey_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); 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(::String)) }
|
||||
def bluesky_rkey_in_database; end
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def bluesky_rkey_previous_change; end
|
||||
sig { returns(T.nilable([T.nilable(::Integer), T.nilable(::Integer)])) }
|
||||
def first_seen_entry_id_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def bluesky_rkey_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); 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(::String)) }
|
||||
def bluesky_rkey_previously_was; end
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def bluesky_rkey_was; end
|
||||
sig { returns(T.nilable(::Integer)) }
|
||||
def first_seen_entry_id_was; end
|
||||
|
||||
sig { void }
|
||||
def bluesky_rkey_will_change!; end
|
||||
def first_seen_entry_id_will_change!; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def hashtags; end
|
||||
@@ -1325,7 +1325,7 @@ class DomainPostsBlueskyAux
|
||||
def restore_base_table_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_bluesky_rkey!; end
|
||||
def restore_first_seen_entry_id!; end
|
||||
|
||||
sig { void }
|
||||
def restore_hashtags!; end
|
||||
@@ -1364,10 +1364,10 @@ class DomainPostsBlueskyAux
|
||||
def restore_repost_count!; end
|
||||
|
||||
sig { void }
|
||||
def restore_scan_error!; end
|
||||
def restore_rkey!; end
|
||||
|
||||
sig { void }
|
||||
def restore_scanned_at!; end
|
||||
def restore_scan_error!; end
|
||||
|
||||
sig { void }
|
||||
def restore_state!; end
|
||||
@@ -1375,6 +1375,51 @@ class DomainPostsBlueskyAux
|
||||
sig { void }
|
||||
def restore_text!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey; end
|
||||
|
||||
sig { params(value: T.nilable(::String)).returns(T.nilable(::String)) }
|
||||
def rkey=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def rkey?; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def rkey_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def rkey_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def rkey_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def rkey_change_to_be_saved; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def rkey_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def rkey_previous_change; end
|
||||
|
||||
sig { params(from: T.nilable(::String), to: T.nilable(::String)).returns(T::Boolean) }
|
||||
def rkey_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def rkey_was; end
|
||||
|
||||
sig { void }
|
||||
def rkey_will_change!; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_at_uri; end
|
||||
|
||||
@@ -1387,11 +1432,11 @@ class DomainPostsBlueskyAux
|
||||
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_bluesky_rkey; 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_bluesky_rkey?; end
|
||||
def saved_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T.nilable([T.untyped, T.untyped])) }
|
||||
def saved_change_to_hashtags; end
|
||||
@@ -1465,18 +1510,18 @@ class DomainPostsBlueskyAux
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_repost_count?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_rkey; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_rkey?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_scan_error; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_scan_error?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def saved_change_to_scanned_at; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def saved_change_to_scanned_at?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::String), T.nilable(::String)])) }
|
||||
def saved_change_to_state; end
|
||||
|
||||
@@ -1534,61 +1579,6 @@ class DomainPostsBlueskyAux
|
||||
sig { void }
|
||||
def scan_error_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at; end
|
||||
|
||||
sig { params(value: T.nilable(::ActiveSupport::TimeWithZone)).returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at=(value); end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def scanned_at?; end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at_before_last_save; end
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
def scanned_at_before_type_cast; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def scanned_at_came_from_user?; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def scanned_at_change; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def scanned_at_change_to_be_saved; end
|
||||
|
||||
sig do
|
||||
params(
|
||||
from: T.nilable(::ActiveSupport::TimeWithZone),
|
||||
to: T.nilable(::ActiveSupport::TimeWithZone)
|
||||
).returns(T::Boolean)
|
||||
end
|
||||
def scanned_at_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at_in_database; end
|
||||
|
||||
sig { returns(T.nilable([T.nilable(::ActiveSupport::TimeWithZone), T.nilable(::ActiveSupport::TimeWithZone)])) }
|
||||
def scanned_at_previous_change; end
|
||||
|
||||
sig do
|
||||
params(
|
||||
from: T.nilable(::ActiveSupport::TimeWithZone),
|
||||
to: T.nilable(::ActiveSupport::TimeWithZone)
|
||||
).returns(T::Boolean)
|
||||
end
|
||||
def scanned_at_previously_changed?(from: T.unsafe(nil), to: T.unsafe(nil)); end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at_previously_was; end
|
||||
|
||||
sig { returns(T.nilable(::ActiveSupport::TimeWithZone)) }
|
||||
def scanned_at_was; end
|
||||
|
||||
sig { void }
|
||||
def scanned_at_will_change!; end
|
||||
|
||||
sig { returns(T.nilable(::String)) }
|
||||
def state; end
|
||||
|
||||
@@ -1696,7 +1686,7 @@ class DomainPostsBlueskyAux
|
||||
def will_save_change_to_base_table_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_bluesky_rkey?; end
|
||||
def will_save_change_to_first_seen_entry_id?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_hashtags?; end
|
||||
@@ -1735,10 +1725,10 @@ class DomainPostsBlueskyAux
|
||||
def will_save_change_to_repost_count?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_scan_error?; end
|
||||
def will_save_change_to_rkey?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_scanned_at?; end
|
||||
def will_save_change_to_scan_error?; end
|
||||
|
||||
sig { returns(T::Boolean) }
|
||||
def will_save_change_to_state?; end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
FactoryBot.define do
|
||||
factory :domain_post_bluesky_post, class: "Domain::Post::BlueskyPost" do
|
||||
association :creator, factory: :domain_user_bluesky_user
|
||||
sequence(:bluesky_rkey) { |n| "rkey#{n}" }
|
||||
sequence(:rkey) { |n| "rkey#{n}" }
|
||||
sequence(:at_uri) do |n|
|
||||
"at://did:plc:#{n.to_s.rjust(10, "0")}/app.bsky.feed.post/rkey#{n}"
|
||||
end
|
||||
|
||||
@@ -16,7 +16,71 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
)
|
||||
end
|
||||
|
||||
let(:client_mock_config) { [] }
|
||||
before { @log_entries = HttpClientMockHelpers.init_with(client_mock_config) }
|
||||
|
||||
describe "#perform" do
|
||||
context "the user has video posts" do
|
||||
let(:posts_response_body) do
|
||||
{
|
||||
"records" => [
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/video_post1",
|
||||
"cid" => "bafyreiapost123",
|
||||
"value" => {
|
||||
"text" => "Hello world with video!",
|
||||
"langs" => ["en"],
|
||||
"createdAt" => "2025-01-08T12:00:00.000Z",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.video",
|
||||
"alt" => "Test video",
|
||||
"aspectRatio" => {
|
||||
"width" => 1920,
|
||||
"height" => 1080,
|
||||
},
|
||||
"video" => {
|
||||
"$type" => "blob",
|
||||
"mimeType" => "video/mp4",
|
||||
"ref" => {
|
||||
"$link" => "bafkreivideo123",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}.to_json
|
||||
end
|
||||
|
||||
let(:client_mock_config) do
|
||||
[
|
||||
{
|
||||
uri:
|
||||
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=#{user.did}&collection=app.bsky.feed.post&limit=100",
|
||||
status_code: 200,
|
||||
content_type: "application/json",
|
||||
contents: posts_response_body,
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "creates the BlueskyPost and BlueskyPostFile" do
|
||||
perform_now({ user: user })
|
||||
|
||||
expect(Domain::Post::BlueskyPost.count).to eq(1)
|
||||
expect(Domain::PostFile::BlueskyPostFile.count).to eq(1)
|
||||
post = Domain::Post::BlueskyPost.find_by(rkey: "video_post1")
|
||||
expect(post.text).to eq("Hello world with video!")
|
||||
expect(post.files.first.blob_ref).to eq("bafkreivideo123")
|
||||
expect(post.files.first.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreivideo123",
|
||||
)
|
||||
expect(post.files.first.aspect_ratio_width).to eq(1920)
|
||||
expect(post.files.first.aspect_ratio_height).to eq(1080)
|
||||
expect(post.files.first.alt_text).to eq("Test video")
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is in ok state" do
|
||||
let(:posts_response_body) do
|
||||
{
|
||||
@@ -57,30 +121,6 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
"createdAt" => "2025-01-08T11:00:00.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/post3",
|
||||
"cid" => "bafyreiapost789",
|
||||
"value" => {
|
||||
"text" => "Post with external embed",
|
||||
"createdAt" => "2025-01-08T10:00:00.000Z",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.external",
|
||||
"external" => {
|
||||
"uri" => "https://example.com",
|
||||
"title" => "Example Site",
|
||||
"description" => "An example website",
|
||||
"thumb" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreithumb123",
|
||||
},
|
||||
"mimeType" => "image/jpeg",
|
||||
"size" => 50_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"cursor" => nil,
|
||||
}.to_json
|
||||
@@ -98,10 +138,6 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
@log_entries = HttpClientMockHelpers.init_with(client_mock_config)
|
||||
end
|
||||
|
||||
it "scans user posts and updates scanned_posts_at" do
|
||||
perform_now({ user: user })
|
||||
|
||||
@@ -109,14 +145,20 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
expect(user.scanned_posts_at).to be_present
|
||||
end
|
||||
|
||||
it "sets the first_seen_entry for posts" do
|
||||
perform_now({ user: user })
|
||||
post = Domain::Post::BlueskyPost.find_by(rkey: "post1")
|
||||
expect(post.first_seen_entry).to eq(@log_entries[0])
|
||||
end
|
||||
|
||||
it "creates posts with media and associated files" do
|
||||
expect { perform_now({ user: user }) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(2).and change(Domain::PostFile::BlueskyPostFile, :count).by(2)
|
||||
).by(1).and change(Domain::PostFile::BlueskyPostFile, :count).by(1)
|
||||
|
||||
# Check image post
|
||||
image_post = Domain::Post::BlueskyPost.find_by(bluesky_rkey: "post1")
|
||||
image_post = Domain::Post::BlueskyPost.find_by(rkey: "post1")
|
||||
expect(image_post.text).to eq("Hello world with image!")
|
||||
expect(image_post.creator).to eq(user)
|
||||
|
||||
@@ -129,35 +171,33 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreiimage123",
|
||||
)
|
||||
|
||||
# Check external embed post
|
||||
external_post = Domain::Post::BlueskyPost.find_by(bluesky_rkey: "post3")
|
||||
expect(external_post.text).to eq("Post with external embed")
|
||||
expect(external_post.creator).to eq(user)
|
||||
# Check external embed post is not created
|
||||
expect(Domain::Post::BlueskyPost.find_by(rkey: "post3")).to be_nil
|
||||
|
||||
external_file = external_post.files.first
|
||||
expect(external_file.blob_ref).to eq("bafkreithumb123")
|
||||
expect(external_file.url_str).to eq(
|
||||
"https://bsky.social/xrpc/com.atproto.sync.getBlob?did=#{user.did}&cid=bafkreithumb123",
|
||||
)
|
||||
# Check external embed file is not created
|
||||
expect(
|
||||
Domain::PostFile::BlueskyPostFile.find_by(
|
||||
blob_ref: "bafkreithumb123",
|
||||
),
|
||||
).to be_nil
|
||||
end
|
||||
|
||||
it "does not create posts without media" do
|
||||
perform_now({ user: user })
|
||||
|
||||
# Should only create 2 posts (the ones with media), not the text-only post
|
||||
expect(Domain::Post::BlueskyPost.count).to eq(2)
|
||||
# Should only create 1 post (the one with media), not the text-only post or the external embed post
|
||||
expect(Domain::Post::BlueskyPost.count).to eq(1)
|
||||
expect(Domain::Post::BlueskyPost.pluck(:text)).to contain_exactly(
|
||||
"Hello world with image!",
|
||||
"Post with external embed",
|
||||
)
|
||||
end
|
||||
|
||||
it "enqueues StaticFileJob for each post file" do
|
||||
perform_now({ user: user })
|
||||
|
||||
# Should enqueue 2 StaticFileJobs (one for image, one for external thumbnail)
|
||||
# Should enqueue 1 StaticFileJobs (only for image)
|
||||
enqueued_jobs = SpecUtil.enqueued_job_args(Domain::StaticFileJob)
|
||||
expect(enqueued_jobs.length).to eq(2)
|
||||
expect(enqueued_jobs.length).to eq(1)
|
||||
end
|
||||
|
||||
it "does not create duplicate posts" do
|
||||
@@ -166,16 +206,15 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
create(
|
||||
:domain_post_bluesky_post,
|
||||
at_uri: "at://#{user.did}/app.bsky.feed.post/post1",
|
||||
bluesky_rkey: "post1",
|
||||
creator: user,
|
||||
)
|
||||
|
||||
expect { perform_now({ user: user }) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(1) # Only the external embed post should be created
|
||||
).by(0) # The image post was already created
|
||||
|
||||
expect(Domain::Post::BlueskyPost.find_by(bluesky_rkey: "post1")).to eq(
|
||||
expect(Domain::Post::BlueskyPost.find_by(rkey: "post1")).to eq(
|
||||
existing_post,
|
||||
)
|
||||
end
|
||||
@@ -211,10 +250,6 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
@log_entries = HttpClientMockHelpers.init_with(client_mock_config)
|
||||
end
|
||||
|
||||
it "handles API errors gracefully" do
|
||||
expect { perform_now({ user: user }) }.to raise_error(
|
||||
/failed to get user posts/,
|
||||
@@ -307,10 +342,6 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
@log_entries = HttpClientMockHelpers.init_with(client_mock_config)
|
||||
end
|
||||
|
||||
it "handles pagination correctly" do
|
||||
expect { perform_now({ user: user }) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
@@ -321,6 +352,14 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
expect(posts.first.text).to eq("First post with image")
|
||||
expect(posts.second.text).to eq("Second post with image")
|
||||
end
|
||||
|
||||
it "sets the correct first_seen_entry for posts" do
|
||||
perform_now({ user: user })
|
||||
post1 = Domain::Post::BlueskyPost.find_by(rkey: "post1")
|
||||
post2 = Domain::Post::BlueskyPost.find_by(rkey: "post2")
|
||||
expect(post1.first_seen_entry).to eq(@log_entries[0])
|
||||
expect(post2.first_seen_entry).to eq(@log_entries[1])
|
||||
end
|
||||
end
|
||||
|
||||
context "when rescanning user with existing posts but pending files" do
|
||||
@@ -328,7 +367,6 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
create(
|
||||
:domain_post_bluesky_post,
|
||||
at_uri: "at://#{user.did}/app.bsky.feed.post/post1",
|
||||
bluesky_rkey: "post1",
|
||||
creator: user,
|
||||
text: "Hello world with image!",
|
||||
)
|
||||
@@ -407,7 +445,6 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
end
|
||||
|
||||
before do
|
||||
@log_entries = HttpClientMockHelpers.init_with(client_mock_config)
|
||||
# Set up existing post with files in different states
|
||||
pending_file
|
||||
ok_file
|
||||
@@ -442,7 +479,7 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
)
|
||||
|
||||
# Verify the existing post wasn't duplicated
|
||||
posts = Domain::Post::BlueskyPost.where(bluesky_rkey: "post1")
|
||||
posts = Domain::Post::BlueskyPost.where(rkey: "post1")
|
||||
expect(posts.count).to eq(1)
|
||||
expect(posts.first).to eq(existing_post)
|
||||
end
|
||||
@@ -456,5 +493,173 @@ RSpec.describe Domain::Bluesky::Job::ScanPostsJob do
|
||||
expect(user.scanned_posts_at).to be > old_timestamp
|
||||
end
|
||||
end
|
||||
|
||||
context "when filtering replies and quotes" do
|
||||
let(:other_user_did) { "did:plc:other123" }
|
||||
|
||||
let(:posts_response_body) do
|
||||
{
|
||||
"records" => [
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/root_post",
|
||||
"cid" => "bafyreiaroot123",
|
||||
"value" => {
|
||||
"text" => "Root post with image",
|
||||
"createdAt" => "2025-01-08T12:00:00.000Z",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "Root image",
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreirootimg123",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/reply_to_self",
|
||||
"cid" => "bafyreiaself123",
|
||||
"value" => {
|
||||
"text" => "Reply to own post with image",
|
||||
"createdAt" => "2025-01-08T11:30:00.000Z",
|
||||
"reply" => {
|
||||
"root" => {
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/root_post",
|
||||
},
|
||||
"parent" => {
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/root_post",
|
||||
},
|
||||
},
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "Reply image",
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreireplyimg123",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/reply_to_other",
|
||||
"cid" => "bafyreiaother123",
|
||||
"value" => {
|
||||
"text" => "Reply to other user with image",
|
||||
"createdAt" => "2025-01-08T11:00:00.000Z",
|
||||
"reply" => {
|
||||
"root" => {
|
||||
"uri" =>
|
||||
"at://#{other_user_did}/app.bsky.feed.post/other_post",
|
||||
},
|
||||
"parent" => {
|
||||
"uri" =>
|
||||
"at://#{other_user_did}/app.bsky.feed.post/other_post",
|
||||
},
|
||||
},
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "Other reply image",
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreiotherimg123",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"uri" => "at://#{user.did}/app.bsky.feed.post/quote_other",
|
||||
"cid" => "bafyreiquote123",
|
||||
"value" => {
|
||||
"text" => "Quote of other user with image",
|
||||
"createdAt" => "2025-01-08T10:30:00.000Z",
|
||||
"embed" => {
|
||||
"$type" => "app.bsky.embed.recordWithMedia",
|
||||
"record" => {
|
||||
"record" => {
|
||||
"uri" =>
|
||||
"at://#{other_user_did}/app.bsky.feed.post/quoted_post",
|
||||
},
|
||||
},
|
||||
"media" => {
|
||||
"$type" => "app.bsky.embed.images",
|
||||
"images" => [
|
||||
{
|
||||
"alt" => "Quote image",
|
||||
"image" => {
|
||||
"$type" => "blob",
|
||||
"ref" => {
|
||||
"$link" => "bafkreiquoteimg123",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"cursor" => nil,
|
||||
}.to_json
|
||||
end
|
||||
|
||||
let(:client_mock_config) do
|
||||
[
|
||||
{
|
||||
uri:
|
||||
"https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=#{user.did}&collection=app.bsky.feed.post&limit=100",
|
||||
status_code: 200,
|
||||
content_type: "application/json",
|
||||
contents: posts_response_body,
|
||||
},
|
||||
]
|
||||
end
|
||||
|
||||
it "only records root posts and replies to user's own posts" do
|
||||
expect { perform_now({ user: user }) }.to change(
|
||||
Domain::Post::BlueskyPost,
|
||||
:count,
|
||||
).by(2) # Only root_post and reply_to_self should be created
|
||||
|
||||
created_posts = Domain::Post::BlueskyPost.order(:created_at)
|
||||
expect(created_posts.map(&:rkey)).to contain_exactly(
|
||||
"root_post",
|
||||
"reply_to_self",
|
||||
)
|
||||
|
||||
# Verify reply_to_uri is stored correctly
|
||||
reply_post = created_posts.find_by(rkey: "reply_to_self")
|
||||
expect(reply_post.reply_to_uri).to eq(
|
||||
"at://#{user.did}/app.bsky.feed.post/root_post",
|
||||
)
|
||||
end
|
||||
|
||||
it "filters out replies to other users and quotes of other users" do
|
||||
perform_now({ user: user })
|
||||
|
||||
# Should not create posts for reply_to_other or quote_other
|
||||
expect(
|
||||
Domain::Post::BlueskyPost.find_by(rkey: "reply_to_other"),
|
||||
).to be_nil
|
||||
expect(Domain::Post::BlueskyPost.find_by(rkey: "quote_other")).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -101,7 +101,7 @@ RSpec.describe Tasks::Bluesky::Monitor do
|
||||
"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.rkey).to eq("test123")
|
||||
expect(post.posted_at).to eq(base_time)
|
||||
|
||||
files = post.files.order(:file_order)
|
||||
|
||||
Reference in New Issue
Block a user