9 Commits

Author SHA1 Message Date
Dylan Knutson
f2f8a9c34a Refactor PostFiles component to use URL parameters and simplify implementation
- Change from hash fragments (#file=2) to URL parameters (?idx=2) for server-side prerendering support
- Simplify React component by removing complex client-side hydration logic
- Remove unnecessary props: totalFiles, hasMultipleFiles (derive from files.length)
- Remove redundant useCallback and popstate handlers
- Update Rails helper to read URL parameter and pass correct initialSelectedIndex
- Maintain all functionality: carousel, keyboard navigation, URL state management
2025-08-09 00:59:26 +00:00
Dylan Knutson
36ceae80fe Refactor file carousel into React component hierarchy
- Create PostFiles top-level component managing file display state
- Add FileCarousel component for thumbnail navigation
- Add DisplayedFile component for content rendering
- Add FileDetails component for metadata display
- Update props_for_post_files helper to generate HTML content server-side
- Replace HTML/JS carousel with prerendered React components
- Maintain single file layout compatibility
- Add proper TypeScript interfaces and error handling
- Register components in application and server bundles

Components now handle:
- Multiple file carousel display above content
- File content switching via React state
- Server-side rendered HTML injection
- File details metadata display
- Responsive thumbnail grid with selection states
2025-08-09 00:31:07 +00:00
Dylan Knutson
5f5a54d68f 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
2025-08-08 05:07:07 +00:00
Dylan Knutson
e30e20b033 tests for bsky posts 2025-08-08 00:40:28 +00:00
Dylan Knutson
608044e8fb bsky rkey based post tracking 2025-08-07 09:01:34 +00:00
Dylan Knutson
9efeedd1ff basic bluesky monitoring infra 2025-08-05 20:51:40 +00:00
Dylan Knutson
3512c3f32e skyfall gem, rework migrations 2025-08-05 19:21:38 +00:00
Dylan Knutson
e9f3b0e822 bluesky initial impl 2025-08-05 18:53:14 +00:00
Dylan Knutson
6b8fce7ddc do not normalize urls 2025-08-05 18:52:50 +00:00
71 changed files with 24014 additions and 109 deletions

View File

@@ -1,14 +1,22 @@
# How to use this codebase
- ALWAYS run `srb tc` after making changes to Ruby files to ensure the codebase is typechecked.
- ALWAYS run `bin/rspec <path_to_spec_file>` after a spec file is modified.
- Run `bin/tapioca dsl` after changing a model or concern.
- Run `bin/tapioca gems` after changing the Gemfile.
- Run `srb tc` after making changes to Ruby files to ensure the codebase is typechecked.
- Run `bin/rspec <path_to_spec_file>` after a spec file is modified.
- Run `tapioca dsl` if models or concerns are modified.
- After modifying a file that has a corresponding spec file, run `bin/rspec <path_to_spec_file>` to run just that spec file.
- Run `bin/rspec <path_to_spec_file>` to run tests for a single file.
- There are no view-specific tests, so if a view changes then run the controller tests instead.
- For instance, if you modify `app/models/domain/post.rb`, run `bin/rspec spec/models/domain/post_spec.rb`. If you modify `app/views/domain/users/index.html.erb`, run `bin/rspec spec/controllers/domain/users_controller_spec.rb`.
- At the end of a series of changes, ALWAYS run `just test` to run the entire test suite.
- At the end of a long series of changes, run `just test`.
- If specs are failing, then fix the failures, and rerun with `bin/rspec <path_to_spec_file>`.
# Typescript Development
- React is the only frontend framework used.
- Styling is done with Tailwind CSS and FontAwesome.
- Put new typescript files in `app/javascript/bundles/Main/components/`
# === BACKLOG.MD GUIDELINES START ===
# Instructions for the usage of Backlog.md CLI Tool

View File

@@ -24,7 +24,8 @@
"esbenp.prettier-vscode",
"ms-azuretools.vscode-docker",
"1YiB.rust-bundle",
"rust-lang.rust-analyzer"
"rust-lang.rust-analyzer",
"saoudrizwan.claude-dev"
]
}
},

View File

@@ -187,3 +187,7 @@ gem "sorbet-runtime", SORBET_VERSION
gem "tapioca", "0.16.6", require: false, group: %i[development test]
gem "rspec-sorbet", group: [:test]
gem "sorbet-struct-comparable"
gem "skyfall", "~> 0.6.0"
gem "didkit", "~> 0.2.3"

View File

@@ -110,6 +110,7 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
attr_json (2.5.0)
activerecord (>= 6.0.0, < 8.1)
base32 (0.3.4)
base64 (0.3.0)
bcrypt (3.1.20)
benchmark (0.4.1)
@@ -129,6 +130,7 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
cbor (0.5.9.9)
charlock_holmes (0.7.9)
cloudflare-rails (6.2.0)
actionpack (>= 7.1.0, < 8.1.0)
@@ -165,6 +167,7 @@ GEM
warden (~> 1.2.3)
dhash-vips (0.2.3.0)
ruby-vips (~> 2.0, != 2.1.1, != 2.1.0)
didkit (0.2.3)
diff-lcs (1.5.1)
discard (1.4.0)
activerecord (>= 4.2, < 9.0)
@@ -202,6 +205,7 @@ GEM
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
eventmachine (1.2.7)
execjs (2.10.0)
factory_bot (6.5.0)
activesupport (>= 5.0.0)
@@ -219,6 +223,9 @@ GEM
multipart-post (~> 2.0)
faraday-net_http (3.4.1)
net-http (>= 0.5.0)
faye-websocket (0.12.0)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.8.0)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm64-darwin)
@@ -609,6 +616,12 @@ GEM
semantic_range (>= 2.3.0)
shoulda-matchers (6.4.0)
activesupport (>= 5.2.0)
skyfall (0.6.0)
base32 (~> 0.3, >= 0.3.4)
base64 (~> 0.1)
cbor (~> 0.5, >= 0.5.9.6)
eventmachine (~> 1.2, >= 1.2.7)
faye-websocket (~> 0.12)
sorbet (0.5.12221)
sorbet-static (= 0.5.12221)
sorbet-runtime (0.5.12221)
@@ -693,7 +706,8 @@ GEM
selenium-webdriver (~> 4.0, < 4.11)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
@@ -733,6 +747,7 @@ DEPENDENCIES
debug (~> 1.11)
devise (~> 4.9)
dhash-vips
didkit (~> 0.2.3)
discard
disco
docx
@@ -787,6 +802,7 @@ DEPENDENCIES
selenium-webdriver
shakapacker (~> 6.6)
shoulda-matchers
skyfall (~> 0.6.0)
sorbet (= 0.5.12221)
sorbet-runtime (= 0.5.12221)
sorbet-struct-comparable

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -19,6 +19,8 @@ module Domain::DomainModelHelper
"Inkbunny"
when Domain::DomainType::Sofurry
"Sofurry"
when Domain::DomainType::Bluesky
"Bluesky"
end
end
@@ -33,6 +35,8 @@ module Domain::DomainModelHelper
"IB"
when Domain::DomainType::Sofurry
"SF"
when Domain::DomainType::Bluesky
"BSKY"
end
end
end

View File

@@ -6,5 +6,6 @@ class Domain::DomainType < T::Enum
E621 = new
Inkbunny = new
Sofurry = new
Bluesky = new
end
end

View File

@@ -48,6 +48,11 @@ module Domain::PostsHelper
domain_icon_path: "domain-icons/sofurry.png",
domain_icon_title: "SoFurry",
),
Domain::DomainType::Bluesky =>
DomainData.new(
domain_icon_path: "domain-icons/bluesky.png",
domain_icon_title: "Bluesky",
),
},
T::Hash[Domain::DomainType, DomainData],
)
@@ -189,6 +194,89 @@ module Domain::PostsHelper
file.log_entry&.response_size&.then { |size| number_to_human_size(size) }
end
sig do
params(
ok_files: T::Array[Domain::PostFile],
initial_file_index: T.nilable(Integer),
).returns(T::Hash[Symbol, T.untyped])
end
def props_for_post_files(ok_files, initial_file_index: nil)
files_data =
ok_files.map.with_index do |file, index|
thumbnail_path = nil
content_html = nil
file_details_html = nil
if file.log_entry&.status_code == 200
log_entry = file.log_entry
# Generate thumbnail path
begin
if log_entry && (response_sha256 = log_entry.response_sha256)
thumbnail_path =
blob_path(
HexUtil.bin2hex(response_sha256),
format: "jpg",
thumb: "small",
)
end
rescue StandardError
# thumbnail_path remains nil
end
# Generate content HTML
begin
content_html =
ApplicationController.renderer.render(
partial: "log_entries/content_container",
locals: {
log_entry: log_entry,
},
assigns: {
current_user: nil,
},
)
rescue StandardError
# content_html remains nil
end
# Generate file details HTML
begin
file_details_html =
ApplicationController.renderer.render(
partial: "log_entries/file_details_sky_section",
locals: {
log_entry: log_entry,
},
assigns: {
current_user: nil,
},
)
rescue StandardError
# file_details_html remains nil
end
end
{
id: file.id,
thumbnailPath: thumbnail_path,
hasContent: file.log_entry&.status_code == 200,
index: index,
contentHtml: content_html,
fileDetailsHtml: file_details_html,
}
end
# Validate initial_file_index
validated_initial_index = 0
if initial_file_index && initial_file_index >= 0 &&
initial_file_index < ok_files.count
validated_initial_index = initial_file_index
end
{ files: files_data, initialSelectedIndex: validated_initial_index }
end
sig { params(url: String).returns(T.nilable(String)) }
def icon_asset_for_url(url)
domain = extract_domain(url)

View File

@@ -102,6 +102,8 @@ module Domain::UsersHelper
asset_path("domain-icons/inkbunny.png")
when Domain::User::SofurryUser
asset_path("domain-icons/sofurry.png")
when Domain::User::BlueskyUser
asset_path("domain-icons/bluesky.png")
else
Kernel.raise "Unknown user type: #{user.class}"
end

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { FileData } from './PostFiles';
import { FileDetails } from './FileDetails';
interface DisplayedFileProps {
file: FileData;
}
export const DisplayedFile: React.FC<DisplayedFileProps> = ({ file }) => {
return (
<>
{/* File content display */}
<div className="file-content-display mb-4">
{file.contentHtml ? (
<div dangerouslySetInnerHTML={{ __html: file.contentHtml }} />
) : (
<section className="flex grow justify-center text-slate-500">
<div>
<i className="fa-solid fa-file-arrow-down"></i>
No file content available
</div>
</section>
)}
</div>
{/* File details */}
{file.fileDetailsHtml && <FileDetails html={file.fileDetailsHtml} />}
</>
);
};
export default DisplayedFile;

View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import { FileData } from './PostFiles';
interface FileCarouselProps {
files: FileData[];
totalFiles: number;
selectedIndex: number;
onFileSelect: (fileId: number, index: number) => void;
}
export const FileCarousel: React.FC<FileCarouselProps> = ({
files,
totalFiles,
selectedIndex,
onFileSelect,
}) => {
const handleFileClick = (file: FileData) => {
onFileSelect(file.id, file.index);
};
// Only render if there are multiple files
if (files.length <= 1) {
return null;
}
return (
<div className="mb-4">
<div className="flex gap-2 overflow-x-auto" id="file-carousel">
{files.map((file) => {
const isSelected = file.index === selectedIndex;
const buttonClasses = [
'flex-shrink-0',
'w-20',
'h-20',
'rounded-md',
'border-2',
'transition-all',
'duration-200',
'hover:border-blue-400',
isSelected ? 'border-blue-500' : 'border-gray-300',
];
if (file.thumbnailPath) {
buttonClasses.push('overflow-hidden');
} else {
buttonClasses.push(
'bg-gray-100',
'flex',
'items-center',
'justify-center',
);
}
return (
<button
key={file.id}
className={buttonClasses.join(' ')}
onClick={() => handleFileClick(file)}
data-file-id={file.id}
data-index={file.index}
title={`File ${file.index + 1} of ${totalFiles}`}
>
{file.thumbnailPath ? (
<img
src={file.thumbnailPath}
className="h-full w-full object-cover"
alt={`File ${file.index + 1}`}
/>
) : (
<i className="fa-solid fa-file text-gray-400"></i>
)}
</button>
);
})}
</div>
</div>
);
};
export default FileCarousel;

View File

@@ -0,0 +1,16 @@
import * as React from 'react';
interface FileDetailsProps {
html: string;
}
export const FileDetails: React.FC<FileDetailsProps> = ({ html }) => {
return (
<div
className="file-details-section"
dangerouslySetInnerHTML={{ __html: html }}
/>
);
};
export default FileDetails;

View File

@@ -0,0 +1,99 @@
import * as React from 'react';
import { useState, useEffect, useCallback } from 'react';
import { FileCarousel } from './FileCarousel';
import { DisplayedFile } from './DisplayedFile';
export interface FileData {
id: number;
thumbnailPath?: string;
hasContent: boolean;
index: number;
contentHtml?: string;
fileDetailsHtml?: string;
}
interface PostFilesProps {
files: FileData[];
initialSelectedIndex?: number;
}
export const PostFiles: React.FC<PostFilesProps> = ({
files,
initialSelectedIndex = 0,
}) => {
const [selectedIndex, setSelectedIndex] = useState(initialSelectedIndex);
// Update URL parameter when selected file changes
const updateUrlWithFileIndex = (index: number) => {
if (typeof window === 'undefined' || files.length <= 1) return;
const url = new URL(window.location.href);
url.searchParams.set('idx', index.toString());
window.history.replaceState({}, '', url.toString());
};
const handleFileSelect = (fileId: number, index: number) => {
setSelectedIndex(index);
updateUrlWithFileIndex(index);
};
const navigateToNextFile = () => {
if (files.length > 1) {
const nextIndex = (selectedIndex + 1) % files.length;
handleFileSelect(files[nextIndex].id, nextIndex);
}
};
const navigateToPreviousFile = () => {
if (files.length > 1) {
const prevIndex = (selectedIndex - 1 + files.length) % files.length;
handleFileSelect(files[prevIndex].id, prevIndex);
}
};
// Add keyboard navigation
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Only handle arrow keys if we have multiple files
if (files.length <= 1) return;
switch (event.key) {
case 'ArrowLeft':
event.preventDefault();
navigateToPreviousFile();
break;
case 'ArrowRight':
event.preventDefault();
navigateToNextFile();
break;
}
};
// Add event listener to document
document.addEventListener('keydown', handleKeyDown);
// Cleanup event listener on unmount
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [selectedIndex, files.length]);
const selectedFile = files[selectedIndex];
return (
<section id="file-display-section">
{files.length > 1 && (
<FileCarousel
files={files}
totalFiles={files.length}
selectedIndex={selectedIndex}
onFileSelect={handleFileSelect}
/>
)}
{selectedFile && <DisplayedFile file={selectedFile} />}
</section>
);
};
export default PostFiles;

View File

@@ -4,6 +4,7 @@ import UserSearchBar from '../bundles/Main/components/UserSearchBar';
import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper';
import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper';
import { TrackedObjectsChart } from '../bundles/Main/components/TrackedObjectsChart';
import { PostFiles } from '../bundles/Main/components/PostFiles';
import { initCollapsibleSections } from '../bundles/UI/collapsibleSections';
import { IpAddressInput } from '../bundles/UI/components';
import { StatsPage } from '../bundles/Main/components/StatsPage';
@@ -16,6 +17,7 @@ ReactOnRails.register({
PostHoverPreviewWrapper,
UserHoverPreviewWrapper,
TrackedObjectsChart,
PostFiles,
IpAddressInput,
StatsPage,
VisualSearchForm,

View File

@@ -3,10 +3,12 @@ import ReactOnRails from 'react-on-rails';
import UserSearchBar from '../bundles/Main/components/UserSearchBarServer';
import { PostHoverPreviewWrapper } from '../bundles/Main/components/PostHoverPreviewWrapper';
import { UserHoverPreviewWrapper } from '../bundles/Main/components/UserHoverPreviewWrapper';
import { PostFiles } from '../bundles/Main/components/PostFiles';
// This is how react_on_rails can see the UserSearchBar in the browser.
ReactOnRails.register({
UserSearchBar,
PostHoverPreviewWrapper,
UserHoverPreviewWrapper,
PostFiles,
});

View File

@@ -0,0 +1,43 @@
# 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?
resolver = DIDKit::Resolver.new
resolved =
resolver.resolve_handle(handle) ||
fatal_error("failed to resolve handle: #{handle}")
Domain::User::BlueskyUser.find_or_initialize_by(
did: resolved.did,
) { |user| user.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

View 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

View File

@@ -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)

View File

@@ -127,7 +127,7 @@ class Scraper::JobBase < ApplicationJob
sig { returns(Domain::PostFile) }
def post_file_from_args!
T.cast(arguments[0][:file], Domain::PostFile)
T.cast(arguments[0][:file] || arguments[0][:post_file], Domain::PostFile)
end
# The primary log entry for this job. Typically, this is the first request

View File

@@ -42,7 +42,7 @@ class Scraper::ClientFactory
end
def self.get_generic_http_client
if Rails.env.test? || Rails.env.development?
if Rails.env.test?
@http_client_mock || raise("no http client mock set")
else
_http_client_impl(:generic, Scraper::GenericHttpClientConfig)

View File

@@ -65,10 +65,12 @@ class Scraper::CurlHttpPerformer
curl = get_curl
start_at = Time.now
curl.url = request.uri.normalize.to_s
# TODO - normalizing the URL breaks URLs with utf-8 characters
# curl.url = request.uri.normalize.to_s
curl.url = request.uri.to_s
curl.follow_location = request.follow_redirects
request.request_headers.each { |key, value| curl.headers[key.to_s] = value }
curl.headers["User-Agent"] = "FurryArchiver/1.0 / dhelta"
curl.headers["User-Agent"] = "FurryArchiver/1.0 / telegram: @DeltaNoises"
case request.http_method
when Method::Get
curl.get

5
app/lib/tasks/bluesky.rb Normal file
View File

@@ -0,0 +1,5 @@
# typed: true
# frozen_string_literal: true
module Tasks::Bluesky
end

View File

@@ -0,0 +1,266 @@
# typed: strict
# frozen_string_literal: true
module Tasks::Bluesky
class Monitor
include HasColorLogger
extend T::Sig
CURSOR_KEY = "task-bluesky-jetstream-cursor-1"
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(
"loaded #{@dids.size} #{"did".pluralize(@dids.size)} from database",
)
logger.info("dids: #{@dids.to_a.join(", ")}")
@bluesky_client =
T.let(
Skyfall::Jetstream.new(
"jetstream2.us-east.bsky.network",
{
cursor: nil,
# cursor: load_cursor,
wanted_collections: %w[
app.bsky.feed.post
app.bsky.embed.images
app.bsky.embed.recordWithMedia
],
},
),
Skyfall::Jetstream,
)
@bluesky_client.user_agent = "ReFurrer/1.0 (bsky:delta.refurrer.com)"
end
sig { void }
def run
@bluesky_client.on_connecting { logger.info("connecting...") }
@bluesky_client.on_connect { logger.info("connected") }
@bluesky_client.on_disconnect { logger.info("disconnected") }
@bluesky_client.on_reconnect do
logger.info("connection lost, trying to reconnect...")
end
@bluesky_client.on_timeout do
logger.info("connection stalled, triggering a reconnect...")
end
@bluesky_client.on_message do |msg|
handle_message(msg)
if msg.seq % 10_000 == 0
logger.info("saving cursor: #{msg.seq.to_s.bold}")
save_cursor(msg.seq)
end
end
@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 } if @pg_notify
@bluesky_client.connect
rescue Interrupt
logger.info("shutting down...")
@bluesky_client.disconnect
@bluesky_client.close
pg_notify_thread&.raise(Interrupt)
pg_notify_thread&.join
logger.info("shutdown complete")
end
sig { params(msg: Skyfall::Jetstream::Message).void }
def handle_message(msg)
case msg
when Skyfall::Jetstream::CommitMessage
handle_commit_message(msg)
end
end
sig { params(msg: Skyfall::Jetstream::CommitMessage).void }
def handle_commit_message(msg)
return unless msg.type == :commit
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}`",
)
end
end
sig do
params(msg: Skyfall::Jetstream::CommitMessage).returns(
T.nilable(Domain::User::BlueskyUser),
)
end
def creator_for(msg)
did = msg.did
Domain::User::BlueskyUser.find_or_create_by!(did:) do |creator|
creator.handle = @resolver.get_validated_handle(did) || did
logger.info(
"created bluesky user: `#{creator.handle}` / `#{creator.did}`",
)
end
end
sig { returns(T.nilable(Integer)) }
def load_cursor
GlobalState.get(CURSOR_KEY)&.to_i
end
sig { params(cursor: Integer).void }
def save_cursor(cursor)
GlobalState.set(CURSOR_KEY, cursor.to_s)
end
sig { void }
def listen_to_postgres_notifies
logger.info("listening to postgres NOTIFYs")
ActiveRecord::Base.connection_pool.with_connection do |conn|
conn = T.cast(conn, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
conn.exec_query("LISTEN #{Bluesky::MonitoredDid::ADDED_NOTIFY_CHANNEL}")
conn.exec_query(
"LISTEN #{Bluesky::MonitoredDid::REMOVED_NOTIFY_CHANNEL}",
)
conn.raw_connection.wait_for_notify do |event, pid, payload|
logger.info("NOTIFY: #{event} / pid: #{pid} / payload: #{payload}")
case event
when Bluesky::MonitoredDid::ADDED_NOTIFY_CHANNEL
@dids.add(payload)
when Bluesky::MonitoredDid::REMOVED_NOTIFY_CHANNEL
@dids.delete(payload)
end
end
rescue Interrupt
logger.info("interrupt in notify thread...")
ensure
logger.info("unlistening to postgres NOTIFYs")
conn.exec_query(
"UNLISTEN #{Bluesky::MonitoredDid::ADDED_NOTIFY_CHANNEL}",
)
conn.exec_query(
"UNLISTEN #{Bluesky::MonitoredDid::REMOVED_NOTIFY_CHANNEL}",
)
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

View File

@@ -0,0 +1,25 @@
# typed: strict
class Bluesky::MonitoredDid < ReduxApplicationRecord
self.table_name = "bluesky_monitored_dids"
validates :did, presence: true, uniqueness: true
ADDED_NOTIFY_CHANNEL = "bluesky_did_added"
REMOVED_NOTIFY_CHANNEL = "bluesky_did_removed"
after_create_commit :notify_monitor_added
after_destroy_commit :notify_monitor_removed
sig { void }
def notify_monitor_added
self.class.connection.execute(
"NOTIFY #{ADDED_NOTIFY_CHANNEL}, '#{self.did}'",
)
end
sig { void }
def notify_monitor_removed
self.class.connection.execute(
"NOTIFY #{REMOVED_NOTIFY_CHANNEL}, '#{self.did}'",
)
end
end

View File

@@ -0,0 +1,102 @@
# typed: strict
class Domain::Post::BlueskyPost < Domain::Post
aux_table :bluesky, allow_redefining: :title
belongs_to :first_seen_entry, class_name: "::HttpLogEntry", optional: true
has_multiple_files! Domain::PostFile::BlueskyPostFile
has_single_creator! Domain::User::BlueskyUser
has_faving_users! Domain::User::BlueskyUser
after_initialize { self.state ||= "ok" if new_record? }
enum :state, { ok: "ok", removed: "removed" }, prefix: "state"
validates :state, presence: true
validates :at_uri, presence: true, uniqueness: true
validates :bluesky_rkey, presence: true
validates :bluesky_created_at, presence: true
sig { override.returns([String, Symbol]) }
def self.param_prefix_and_attribute
["bsky", :bluesky_rkey]
end
sig { override.returns(String) }
def self.view_prefix
"bsky"
end
sig { override.returns(Symbol) }
def self.post_order_attribute
:bluesky_created_at
end
sig { override.returns(Domain::DomainType) }
def self.domain_type
Domain::DomainType::Bluesky
end
sig { override.returns(T.nilable(Domain::User)) }
def primary_creator_for_view
self.creator
end
sig { override.returns(T.nilable(T.any(String, Integer))) }
def domain_id_for_view
self.bluesky_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}",
)
end
end
sig { override.returns(T.nilable(Domain::PostFile)) }
def primary_file_for_view
self.files.first
end
sig { override.returns(T::Boolean) }
def pending_scan?
scanned_at.nil?
end
sig { override.returns(T.nilable(String)) }
def description_html_for_view
text
end
sig { override.returns(String) }
def description_html_base_domain
"bsky.app"
end
sig { override.returns(T.nilable(String)) }
def description_for_view
self.text
end
sig { override.returns(T.nilable(Integer)) }
def num_favorites_for_view
like_count
end
sig { override.returns(T.nilable(String)) }
def title
# Use first 50 characters of text as title, or a fallback
current_text = text
current_text.present? ? current_text.truncate(50) : "Bluesky Post"
end
sig { override.returns(T.nilable(T::Array[TagForView])) }
def tags_for_view
return nil unless hashtags.present?
hashtags.map { |tag| TagForView.new(category: :general, value: tag) }
end
end

View File

@@ -0,0 +1,7 @@
# typed: strict
# frozen_string_literal: true
class Domain::PostFile::BlueskyPostFile < Domain::PostFile
aux_table :bluesky
validates :file_order, presence: true, uniqueness: { scope: :post_id }
end

View File

@@ -0,0 +1,96 @@
# typed: strict
class Domain::User::BlueskyUser < Domain::User
aux_table :bluesky
due_timestamp :scanned_profile_at, 1.month
due_timestamp :scanned_posts_at, 1.week
has_created_posts! Domain::Post::BlueskyPost
has_faved_posts! Domain::Post::BlueskyPost
enum :state,
{ ok: "ok", account_disabled: "account_disabled", error: "error" },
prefix: :state
validates :handle, presence: true
validates :did, presence: true
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
["bsky", :handle]
end
sig { override.returns(String) }
def self.view_prefix
"bsky"
end
sig { override.returns(Domain::DomainType) }
def self.domain_type
Domain::DomainType::Bluesky
end
sig { override.returns(String) }
def description_html_base_domain
"bsky.app"
end
sig { override.returns(String) }
def site_name_for_view
"Bluesky"
end
sig { override.returns(T.nilable(String)) }
def description_html_for_view
description
end
sig { override.returns(T::Array[String]) }
def names_for_search
[display_name, handle].compact
end
sig { override.returns(T.nilable(String)) }
def external_url_for_view
"https://bsky.app/profile/#{did}" if did.present?
end
sig { override.returns(T.nilable(String)) }
def name_for_view
display_name || handle
end
sig { override.returns(String) }
def account_status_for_view
"Active" # TODO: Implement proper status checking
end
sig { returns(String) }
def account_state_for_view
case state
when "ok"
"Ok"
when "account_disabled"
"Disabled"
when "error"
"Error"
else
"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

View File

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

View File

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

View File

@@ -0,0 +1,67 @@
<% if policy(post).view_file? %>
<% ok_files = post.files.state_ok.order(:created_at).to_a %>
<% current_file = ok_files.first || post.primary_file_for_view %>
<% if ok_files.any? && current_file&.log_entry&.status_code == 200 %>
<!-- React PostFiles Component handles everything -->
<%
# Extract file index from URL parameter
idx_param = params[:idx]
initial_file_index = idx_param.present? ? idx_param.to_i : nil
%>
<%= react_component(
"PostFiles",
{
prerender: true,
props: props_for_post_files(ok_files, initial_file_index: initial_file_index),
html_options: {
id: "post-files-component"
}
}
) %>
<% elsif current_file.present? && (log_entry = current_file.log_entry) %>
<!-- Fallback for error states -->
<section id="file-display-section">
<section class="flex grow justify-center text-slate-500">
<div>
<i class="fa-solid fa-exclamation-triangle"></i>
File error
<% if log_entry.status_code == 404 %>
(404 not found)
<% else %>
(<%= log_entry.status_code %>)
<% end %>
</div>
</section>
</section>
<% elsif current_file.present? && current_file.state_pending? %>
<!-- Fallback for pending state -->
<section id="file-display-section">
<section class="flex grow justify-center text-slate-500">
<div>
<i class="fa-solid fa-file-arrow-down"></i>
File pending download
</div>
</section>
</section>
<% else %>
<!-- Fallback for no file -->
<section id="file-display-section">
<section class="flex grow justify-center overflow-clip">
<div class="text-slate-500">
<i class="fa-solid fa-file-arrow-down"></i>
No file
</div>
</section>
</section>
<% end %>
<% else %>
<section class="sky-section">
<%= link_to post.external_url_for_view.to_s,
target: "_blank",
rel: "noopener noreferrer",
class: "section-header flex items-center gap-2 hover:text-slate-600" do %>
<span>View Post on <%= domain_name_for_model(post) %></span>
<i class="fa-solid fa-arrow-up-right-from-square"></i>
<% end %>
</section>
<% end %>

View File

@@ -1,53 +0,0 @@
<% if policy(post).view_file? %>
<% file = post.primary_file_for_view %>
<section>
<% if file.present? && (log_entry = file.log_entry) %>
<% if log_entry.status_code == 200 %>
<%= render partial: "log_entries/content_container",
locals: {
log_entry: log_entry,
} %>
<% else %>
<section class="flex grow justify-center text-slate-500">
<div>
<i class="fa-solid fa-exclamation-triangle"></i>
File error
<% if log_entry.status_code == 404 %>
(404 not found)
<% else %>
(<%= log_entry.status_code %>)
<% end %>
</div>
</section>
<% end %>
<% elsif file.present? && file.state_pending? %>
<section class="flex grow justify-center text-slate-500">
<div>
<i class="fa-solid fa-file-arrow-down"></i>
File pending download
</div>
</section>
<% else %>
<section class="flex grow justify-center overflow-clip">
<div class="text-slate-500">
<i class="fa-solid fa-file-arrow-down"></i>
No file
</div>
</section>
<% end %>
</section>
<%= render partial: "log_entries/file_details_sky_section",
locals: {
log_entry: post.primary_file_for_view&.log_entry,
} %>
<% else %>
<section class="sky-section">
<%= link_to post.external_url_for_view.to_s,
target: "_blank",
rel: "noopener noreferrer",
class: "section-header flex items-center gap-2 hover:text-slate-600" do %>
<span>View Post on <%= domain_name_for_model(post) %></span>
<i class="fa-solid fa-arrow-up-right-from-square"></i>
<% end %>
</section>
<% end %>

View File

@@ -29,7 +29,7 @@
<%
description = []
description << "posted #{@post.posted_at.strftime("%B %d, %Y")}" if @post.respond_to?(:posted_at) && @post.posted_at.present?
description << "by #{@post.primary_creator_for_view&.name || "Unknown"}"
description << "by #{@post.primary_creator_for_view&.name_for_view || "Unknown"}"
description << "@ #{domain_name_for_model(@post)}"
%>
<meta name="og:description" content="<%= description.join(" ") %>">
@@ -46,7 +46,7 @@
<div class="mx-auto mt-4 flex w-full max-w-2xl flex-col gap-4 pb-4">
<%= render_for_model(@post, "section_post_title", as: :post) %>
<%= render_for_model(@post, "section_post_groups", as: :post) %>
<%= render_for_model(@post, "section_primary_file", as: :post) %>
<%= render_for_model(@post, "section_file", as: :post) %>
<%= render_for_model(@post, "section_description", as: :post) %>
<%= render_for_model(@post, "section_tags", as: :post) %>
<%= render_for_model(@post, "section_sources", as: :post) %>

View File

@@ -31,12 +31,15 @@ class ActiveRecord::Migration
end
sig do
params(enum_name: Symbol, values: T.class_of(ReduxApplicationRecord)).void
params(
enum_name: Symbol,
values: T.any(T.class_of(ReduxApplicationRecord), String),
).void
end
def add_enum_value(enum_name, *values)
up_only do
values.each do |value|
execute "ALTER TYPE #{enum_name} ADD VALUE IF NOT EXISTS '#{value.name}'"
execute "ALTER TYPE #{enum_name} ADD VALUE IF NOT EXISTS '#{value.is_a?(String) ? value : value.name}'"
end
end
end

View File

@@ -0,0 +1,34 @@
# typed: strict
# frozen_string_literal: true
class AddAuxTablesForDomainUsersBlueskyUsers < ActiveRecord::Migration[7.2]
extend T::Sig
sig { void }
def change
add_enum_value :domain_user_type, "Domain::User::BlueskyUser"
create_aux_table :domain_users, :bluesky do |t|
t.string :state, null: false
t.string :did, null: false, index: true # Decentralized identifier
# handle is the "username"
t.string :handle, null: false, index: true
t.string :display_name
# profile "bio"
t.text :description
# Bluesky-specific fields
t.integer :followers_count
t.integer :following_count
t.integer :posts_count
# avatar is tracked via Domain::UserAvatar, not here
# Scanning timestamps
t.datetime :scanned_profile_at
t.datetime :scanned_posts_at
# Raw data storage for debugging/analysis
t.column :profile_raw, :jsonb, default: {}
end
end
end

View File

@@ -0,0 +1,43 @@
# typed: strict
# frozen_string_literal: true
class AddAuxTablesForDomainPostsBlueskyPosts < ActiveRecord::Migration[7.2]
extend T::Sig
sig { void }
def change
add_enum_value :domain_post_type, "Domain::Post::BlueskyPost"
create_aux_table :domain_posts, :bluesky do |t|
t.string :state, null: false
t.string :bluesky_rkey, null: false, index: true # Record key from AT URI
t.string :at_uri # Full AT Protocol URI
t.text :text # Post content
# Post metadata (posted_at is in main table)
t.datetime :bluesky_created_at, index: true
t.datetime :scanned_at
t.string :language
# Engagement metrics
t.integer :like_count
t.integer :repost_count
t.integer :reply_count
t.integer :quote_count
# Content arrays
t.column :hashtags, :jsonb, default: []
t.column :mentions, :jsonb, default: []
t.column :links, :jsonb, default: []
# References to other posts
t.string :reply_to_uri # AT URI of post being replied to
t.string :quote_uri # AT URI of post being quoted
# Raw data storage for debugging/analysis
t.column :post_raw, :jsonb, default: {}
# Error tracking
t.string :scan_error
end
end
end

View File

@@ -0,0 +1,14 @@
# typed: strict
# frozen_string_literal: true
class AddAuxTablesForDomainPostFilesBluesky < ActiveRecord::Migration[7.2]
extend T::Sig
sig { void }
def change
add_enum_value :domain_post_file_type, "Domain::PostFile::BlueskyPostFile"
create_aux_table :domain_post_files, :bluesky do |t|
t.integer :file_order, null: false
end
end
end

View File

@@ -0,0 +1,8 @@
class CreateBlueskyMonitoredDidsTable < ActiveRecord::Migration[7.2]
def change
create_table :bluesky_monitored_dids do |t|
t.string :did, null: false, index: { unique: true }
t.timestamps
end
end
end

View File

@@ -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

View File

@@ -100,7 +100,8 @@ COMMENT ON EXTENSION vector IS 'vector data type and ivfflat access method';
CREATE TYPE public.domain_post_file_type AS ENUM (
'Domain::PostFile',
'Domain::PostFile::InkbunnyPostFile'
'Domain::PostFile::InkbunnyPostFile',
'Domain::PostFile::BlueskyPostFile'
);
@@ -135,7 +136,8 @@ CREATE TYPE public.domain_post_type AS ENUM (
'Domain::Post::E621Post',
'Domain::Post::InkbunnyPost',
'Domain::Post::SofurryPost',
'Domain::Post::WeasylPost'
'Domain::Post::WeasylPost',
'Domain::Post::BlueskyPost'
);
@@ -158,7 +160,8 @@ CREATE TYPE public.domain_user_type AS ENUM (
'Domain::User::E621User',
'Domain::User::InkbunnyUser',
'Domain::User::SofurryUser',
'Domain::User::WeasylUser'
'Domain::User::WeasylUser',
'Domain::User::BlueskyUser'
);
@@ -1100,6 +1103,37 @@ CREATE TABLE public.blob_files_63 (
);
--
-- Name: bluesky_monitored_dids; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.bluesky_monitored_dids (
id bigint NOT NULL,
did character varying NOT NULL,
created_at timestamp(6) without time zone NOT NULL,
updated_at timestamp(6) without time zone NOT NULL
);
--
-- Name: bluesky_monitored_dids_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.bluesky_monitored_dids_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: bluesky_monitored_dids_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.bluesky_monitored_dids_id_seq OWNED BY public.bluesky_monitored_dids.id;
--
-- Name: domain_fa_fav_id_and_dates; Type: TABLE; Schema: public; Owner: -
--
@@ -1221,6 +1255,39 @@ CREATE TABLE public.domain_post_files (
);
--
-- Name: domain_post_files_bluesky_aux; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_post_files_bluesky_aux (
base_table_id bigint NOT NULL,
file_order integer NOT NULL,
alt_text text,
aspect_ratio_width integer,
aspect_ratio_height integer,
blob_ref character varying
);
--
-- Name: domain_post_files_bluesky_aux_base_table_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_post_files_bluesky_aux_base_table_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: domain_post_files_bluesky_aux_base_table_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_post_files_bluesky_aux_base_table_id_seq OWNED BY public.domain_post_files_bluesky_aux.base_table_id;
--
-- Name: domain_post_files_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
@@ -1336,6 +1403,52 @@ CREATE TABLE public.domain_posts (
);
--
-- Name: domain_posts_bluesky_aux; Type: TABLE; Schema: public; Owner: -
--
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,
text text,
bluesky_created_at timestamp(6) without time zone,
scanned_at timestamp(6) without time zone,
language character varying,
like_count integer,
repost_count integer,
reply_count integer,
quote_count integer,
hashtags jsonb DEFAULT '[]'::jsonb,
mentions jsonb DEFAULT '[]'::jsonb,
links jsonb DEFAULT '[]'::jsonb,
reply_to_uri character varying,
quote_uri character varying,
post_raw jsonb DEFAULT '{}'::jsonb,
scan_error character varying
);
--
-- Name: domain_posts_bluesky_aux_base_table_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_posts_bluesky_aux_base_table_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: domain_posts_bluesky_aux_base_table_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_posts_bluesky_aux_base_table_id_seq OWNED BY public.domain_posts_bluesky_aux.base_table_id;
--
-- Name: domain_posts_e621_aux; Type: TABLE; Schema: public; Owner: -
--
@@ -1827,6 +1940,45 @@ CREATE TABLE public.domain_users (
);
--
-- Name: domain_users_bluesky_aux; Type: TABLE; Schema: public; Owner: -
--
CREATE TABLE public.domain_users_bluesky_aux (
base_table_id bigint NOT NULL,
state character varying NOT NULL,
did character varying NOT NULL,
handle character varying NOT NULL,
display_name character varying,
description text,
followers_count integer,
following_count integer,
posts_count integer,
scanned_profile_at timestamp(6) without time zone,
scanned_posts_at timestamp(6) without time zone,
profile_raw jsonb DEFAULT '{}'::jsonb
);
--
-- Name: domain_users_bluesky_aux_base_table_id_seq; Type: SEQUENCE; Schema: public; Owner: -
--
CREATE SEQUENCE public.domain_users_bluesky_aux_base_table_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
--
-- Name: domain_users_bluesky_aux_base_table_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
--
ALTER SEQUENCE public.domain_users_bluesky_aux_base_table_id_seq OWNED BY public.domain_users_bluesky_aux.base_table_id;
--
-- Name: domain_users_e621_aux; Type: TABLE; Schema: public; Owner: -
--
@@ -2907,6 +3059,13 @@ ALTER TABLE ONLY public.blob_files ATTACH PARTITION public.blob_files_62 FOR VAL
ALTER TABLE ONLY public.blob_files ATTACH PARTITION public.blob_files_63 FOR VALUES WITH (modulus 64, remainder 63);
--
-- Name: bluesky_monitored_dids id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.bluesky_monitored_dids ALTER COLUMN id SET DEFAULT nextval('public.bluesky_monitored_dids_id_seq'::regclass);
--
-- Name: domain_fa_fav_id_and_dates id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -2935,6 +3094,13 @@ ALTER TABLE ONLY public.domain_post_file_thumbnails ALTER COLUMN id SET DEFAULT
ALTER TABLE ONLY public.domain_post_files ALTER COLUMN id SET DEFAULT nextval('public.domain_post_files_id_seq'::regclass);
--
-- Name: domain_post_files_bluesky_aux base_table_id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_files_bluesky_aux ALTER COLUMN base_table_id SET DEFAULT nextval('public.domain_post_files_bluesky_aux_base_table_id_seq'::regclass);
--
-- Name: domain_post_files_inkbunny_aux base_table_id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -2956,6 +3122,13 @@ ALTER TABLE ONLY public.domain_post_groups ALTER COLUMN id SET DEFAULT nextval('
ALTER TABLE ONLY public.domain_posts ALTER COLUMN id SET DEFAULT nextval('public.domain_posts_id_seq'::regclass);
--
-- Name: domain_posts_bluesky_aux base_table_id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_posts_bluesky_aux ALTER COLUMN base_table_id SET DEFAULT nextval('public.domain_posts_bluesky_aux_base_table_id_seq'::regclass);
--
-- Name: domain_posts_e621_aux base_table_id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -3026,6 +3199,13 @@ ALTER TABLE ONLY public.domain_user_search_names ALTER COLUMN id SET DEFAULT nex
ALTER TABLE ONLY public.domain_users ALTER COLUMN id SET DEFAULT nextval('public.domain_users_id_seq'::regclass);
--
-- Name: domain_users_bluesky_aux base_table_id; Type: DEFAULT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_users_bluesky_aux ALTER COLUMN base_table_id SET DEFAULT nextval('public.domain_users_bluesky_aux_base_table_id_seq'::regclass);
--
-- Name: domain_users_e621_aux base_table_id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -3125,6 +3305,14 @@ ALTER TABLE ONLY public.ar_internal_metadata
ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key);
--
-- Name: bluesky_monitored_dids bluesky_monitored_dids_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.bluesky_monitored_dids
ADD CONSTRAINT bluesky_monitored_dids_pkey PRIMARY KEY (id);
--
-- Name: domain_fa_fav_id_and_dates domain_fa_fav_id_and_dates_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -3149,6 +3337,14 @@ ALTER TABLE ONLY public.domain_post_file_thumbnails
ADD CONSTRAINT domain_post_file_thumbnails_pkey PRIMARY KEY (id);
--
-- Name: domain_post_files_bluesky_aux domain_post_files_bluesky_aux_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_files_bluesky_aux
ADD CONSTRAINT domain_post_files_bluesky_aux_pkey PRIMARY KEY (base_table_id);
--
-- Name: domain_post_files_inkbunny_aux domain_post_files_inkbunny_aux_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -3173,6 +3369,14 @@ ALTER TABLE ONLY public.domain_post_groups
ADD CONSTRAINT domain_post_groups_pkey PRIMARY KEY (id);
--
-- Name: domain_posts_bluesky_aux domain_posts_bluesky_aux_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_posts_bluesky_aux
ADD CONSTRAINT domain_posts_bluesky_aux_pkey PRIMARY KEY (base_table_id);
--
-- Name: domain_posts_e621_aux domain_posts_e621_aux_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -3253,6 +3457,14 @@ ALTER TABLE ONLY public.domain_user_search_names
ADD CONSTRAINT domain_user_search_names_pkey PRIMARY KEY (id);
--
-- Name: domain_users_bluesky_aux domain_users_bluesky_aux_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_users_bluesky_aux
ADD CONSTRAINT domain_users_bluesky_aux_pkey PRIMARY KEY (base_table_id);
--
-- Name: domain_users_e621_aux domain_users_e621_aux_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -4064,6 +4276,13 @@ CREATE UNIQUE INDEX index_blob_files_62_on_sha256 ON public.blob_files_62 USING
CREATE UNIQUE INDEX index_blob_files_63_on_sha256 ON public.blob_files_63 USING btree (sha256);
--
-- Name: index_bluesky_monitored_dids_on_did; Type: INDEX; Schema: public; Owner: -
--
CREATE UNIQUE INDEX index_bluesky_monitored_dids_on_did ON public.bluesky_monitored_dids USING btree (did);
--
-- Name: index_domain_fa_fav_id_and_dates_on_fav_fa_id; Type: INDEX; Schema: public; Owner: -
--
@@ -4085,6 +4304,13 @@ CREATE INDEX index_domain_fa_fav_id_and_dates_on_user_id ON public.domain_fa_fav
CREATE UNIQUE INDEX index_domain_fa_fav_id_and_dates_on_user_id_and_post_fa_id ON public.domain_fa_fav_id_and_dates USING btree (user_id, post_fa_id);
--
-- Name: index_domain_post_files_bluesky_aux_on_base_table_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_post_files_bluesky_aux_on_base_table_id ON public.domain_post_files_bluesky_aux USING btree (base_table_id);
--
-- Name: index_domain_post_files_inkbunny_aux_on_base_table_id; Type: INDEX; Schema: public; Owner: -
--
@@ -4148,6 +4374,27 @@ 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_base_table_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_posts_bluesky_aux_on_base_table_id ON public.domain_posts_bluesky_aux USING btree (base_table_id);
--
-- Name: index_domain_posts_bluesky_aux_on_bluesky_created_at; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_posts_bluesky_aux_on_bluesky_created_at ON public.domain_posts_bluesky_aux USING btree (bluesky_created_at);
--
-- Name: index_domain_posts_bluesky_aux_on_bluesky_rkey; 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);
--
-- Name: index_domain_posts_e621_aux_on_base_table_id; Type: INDEX; Schema: public; Owner: -
--
@@ -4400,6 +4647,27 @@ CREATE UNIQUE INDEX index_domain_user_user_follows_on_from_id_and_to_id ON publi
CREATE INDEX index_domain_user_user_follows_on_to_id_and_from_id ON public.domain_user_user_follows USING btree (to_id, from_id);
--
-- Name: index_domain_users_bluesky_aux_on_base_table_id; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_users_bluesky_aux_on_base_table_id ON public.domain_users_bluesky_aux USING btree (base_table_id);
--
-- Name: index_domain_users_bluesky_aux_on_did; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_users_bluesky_aux_on_did ON public.domain_users_bluesky_aux USING btree (did);
--
-- Name: index_domain_users_bluesky_aux_on_handle; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX index_domain_users_bluesky_aux_on_handle ON public.domain_users_bluesky_aux USING btree (handle);
--
-- Name: index_domain_users_e621_aux_on_base_table_id; Type: INDEX; Schema: public; Owner: -
--
@@ -5282,6 +5550,14 @@ ALTER TABLE ONLY public.domain_twitter_medias
ADD CONSTRAINT fk_rails_278c1d09f0 FOREIGN KEY (tweet_id) REFERENCES public.domain_twitter_tweets(id);
--
-- Name: domain_posts_bluesky_aux fk_rails_2a2f4bfba8; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_posts_bluesky_aux
ADD CONSTRAINT fk_rails_2a2f4bfba8 FOREIGN KEY (base_table_id) REFERENCES public.domain_posts(id);
--
-- Name: domain_users_inkbunny_aux fk_rails_304ea0307f; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -5306,6 +5582,14 @@ ALTER TABLE ONLY public.http_log_entries
ADD CONSTRAINT fk_rails_42f35e9da0 FOREIGN KEY (response_headers_id) REFERENCES public.http_log_entry_headers(id);
--
-- Name: domain_post_files_bluesky_aux fk_rails_47e4648919; Type: FK CONSTRAINT; Schema: public; Owner: -
--
ALTER TABLE ONLY public.domain_post_files_bluesky_aux
ADD CONSTRAINT fk_rails_47e4648919 FOREIGN KEY (base_table_id) REFERENCES public.domain_post_files(id);
--
-- Name: domain_fa_fav_id_and_dates fk_rails_4ad7be007e; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -5338,6 +5622,14 @@ ALTER TABLE ONLY public.domain_twitter_medias
ADD CONSTRAINT fk_rails_5fffa41fa6 FOREIGN KEY (file_id) REFERENCES public.http_log_entries(id);
--
-- Name: domain_users_bluesky_aux fk_rails_673dd1243a; Type: FK CONSTRAINT; Schema: public; Owner: -
--
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_e621_aux fk_rails_73ac068c64; Type: FK CONSTRAINT; Schema: public; Owner: -
--
@@ -5553,6 +5845,11 @@ ALTER TABLE ONLY public.domain_twitter_tweets
SET search_path TO "$user", public;
INSERT INTO "schema_migrations" (version) VALUES
('20250808004604'),
('20250805200056'),
('20250805191557'),
('20250805070115'),
('20250805070114'),
('20250805045947'),
('20250805044757'),
('20250731035548'),

40
rake/bluesky.rake Normal file
View File

@@ -0,0 +1,40 @@
# typed: true
# frozen_string_literal: true
T.bind(self, T.all(Rake::DSL, Object))
namespace :bluesky do
desc "Start the Bluesky monitor"
task monitor: :environment do
Tasks::Bluesky::Monitor.new.run
end
def resolve_did(handle)
DIDKit::Resolver.new.resolve_handle(handle)&.did
end
desc "Add a DID to the Bluesky monitor"
task add: :environment do
if (handle = ENV["handle"])
did = resolve_did(handle)
puts "resolved did: #{did}"
else
did = ENV["did"]
end
raise "did is required" if did.blank?
Bluesky::MonitoredDid.create!(did: did)
end
desc "Remove a DID from the Bluesky monitor"
task remove: :environment do
if (handle = ENV["handle"])
did = resolve_did(handle)
puts "resolved did: #{did}"
else
did = ENV["did"]
end
raise "did is required" if did.blank?
Bluesky::MonitoredDid.find_by(did: did)&.destroy!
end
end

44
rake/posts.rake Normal file
View File

@@ -0,0 +1,44 @@
# typed: true
# frozen_string_literal: true
T.bind(self, T.all(Rake::DSL, Object))
namespace :posts do
desc "Find 404 post files with mismatched normalized URLs"
task find_404_post_files_with_mismatched_normalized_urls: :environment do
query =
Domain::PostFile
.where(state: "terminal_error")
.where("url_str ~ '[^\\x00-\\x7F]'")
.includes(:log_entry, post: :files)
query.find_each(batch_size: 32, order: :desc) do |post_file|
le = post_file.log_entry
next if le.nil?
next if le.status_code != 404
next if post_file.url_str.blank?
uri = Addressable::URI.parse(post_file.url_str)
next if uri.to_s == uri.normalize.to_s
next if post_file.post&.files&.any? { |file| file.state_ok? }
if post_file.post&.files&.any? { |file|
file.url_str =~ /furarchiver.net/
}
next
end
puts "#{post_file.post.to_param} ::::: #{post_file.to_param} ::::: #{post_file.url_str}"
end
end
desc "Redownload 404 post files with mismatched normalized URLs"
task redownload_404_post_files_with_mismatched_normalized_urls:
:environment do
File
.readlines(ENV["file"] || raise("must provide file"))
.map(&:strip)
.each do |line|
_, file_id, _ = line.split(" ::::: ")
raise("no file id") if file_id.blank?
file = Domain::PostFile.find(file_id)
file.state_pending!
Domain::Fa::Job::ScanFileJob.perform_now(file:)
end
end
end

View File

@@ -46,6 +46,7 @@ class ApplicationController
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TelegramBotLogsHelper
include ::TimestampHelper
include ::UiHelper
include ::DeviseHelper

1268
sorbet/rbi/dsl/bluesky/monitored_did.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,7 @@ class DeviseController
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TelegramBotLogsHelper
include ::TimestampHelper
include ::UiHelper
include ::DeviseHelper

View File

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

View File

@@ -0,0 +1,27 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Domain::Bluesky::Job::ScanUserJob`.
# Please instead update this file by running `bin/tapioca dsl Domain::Bluesky::Job::ScanUserJob`.
class Domain::Bluesky::Job::ScanUserJob
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::ScanUserJob).void)
).returns(T.any(Domain::Bluesky::Job::ScanUserJob, 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

2709
sorbet/rbi/dsl/domain/post/bluesky_post.rbi generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2592
sorbet/rbi/dsl/domain/user/bluesky_user.rbi generated Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2199
sorbet/rbi/dsl/domain_posts_bluesky_aux.rbi generated Normal file

File diff suppressed because it is too large Load Diff

1818
sorbet/rbi/dsl/domain_users_bluesky_aux.rbi generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -46,6 +46,7 @@ class Rails::ApplicationController
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TelegramBotLogsHelper
include ::TimestampHelper
include ::UiHelper
include ::DeviseHelper

View File

@@ -46,6 +46,7 @@ class Rails::Conductor::BaseController
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TelegramBotLogsHelper
include ::TimestampHelper
include ::UiHelper
include ::DeviseHelper

View File

@@ -46,6 +46,7 @@ class Rails::HealthController
include ::FaUriHelper
include ::GoodJobHelper
include ::IpAddressHelper
include ::TelegramBotLogsHelper
include ::TimestampHelper
include ::UiHelper
include ::DeviseHelper

16
sorbet/rbi/dsl/tasks/bluesky/monitor.rbi generated Normal file
View File

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

View File

@@ -407,10 +407,10 @@ class TelegramBotLog
def status_invalid_image?; end
sig { void }
def status_no_results!; end
def status_processing!; end
sig { returns(T::Boolean) }
def status_no_results?; end
def status_processing?; end
sig { void }
def status_success!; end
@@ -534,7 +534,7 @@ class TelegramBotLog
def not_status_invalid_image(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_status_no_results(*args, &blk); end
def not_status_processing(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def not_status_success(*args, &blk); end
@@ -606,6 +606,9 @@ class TelegramBotLog
end
def select(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def slow_requests(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def status_error(*args, &blk); end
@@ -613,7 +616,7 @@ class TelegramBotLog
def status_invalid_image(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def status_no_results(*args, &blk); end
def status_processing(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateAssociationRelation) }
def status_success(*args, &blk); end
@@ -1923,7 +1926,7 @@ class TelegramBotLog
def not_status_invalid_image(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_status_no_results(*args, &blk); end
def not_status_processing(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def not_status_success(*args, &blk); end
@@ -1995,6 +1998,9 @@ class TelegramBotLog
end
def select(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def slow_requests(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def status_error(*args, &blk); end
@@ -2002,7 +2008,7 @@ class TelegramBotLog
def status_invalid_image(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def status_no_results(*args, &blk); end
def status_processing(*args, &blk); end
sig { params(args: T.untyped, blk: T.untyped).returns(PrivateRelation) }
def status_success(*args, &blk); end

57
sorbet/rbi/gems/base32@0.3.4.rbi generated Normal file
View File

@@ -0,0 +1,57 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `base32` gem.
# Please instead update this file by running `bin/tapioca gem base32`.
# Module for encoding and decoding in Base32 per RFC 3548
#
# source://base32//lib/base32.rb#4
module Base32
class << self
# source://base32//lib/base32.rb#38
def chunks(str, size); end
# source://base32//lib/base32.rb#52
def decode(str); end
# source://base32//lib/base32.rb#48
def encode(str); end
# source://base32//lib/base32.rb#56
def random_base32(length = T.unsafe(nil), padding = T.unsafe(nil)); end
# Returns the value of attribute table.
#
# source://base32//lib/base32.rb#9
def table; end
# @raise [ArgumentError]
#
# source://base32//lib/base32.rb#64
def table=(table); end
# @return [Boolean]
#
# source://base32//lib/base32.rb#69
def table_valid?(table); end
end
end
# source://base32//lib/base32.rb#12
class Base32::Chunk
# @return [Chunk] a new instance of Chunk
#
# source://base32//lib/base32.rb#13
def initialize(bytes); end
# source://base32//lib/base32.rb#17
def decode; end
# source://base32//lib/base32.rb#29
def encode; end
end
# source://base32//lib/base32.rb#5
Base32::TABLE = T.let(T.unsafe(nil), String)

123
sorbet/rbi/gems/cbor@0.5.9.9.rbi generated Normal file
View File

@@ -0,0 +1,123 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `cbor` gem.
# Please instead update this file by running `bin/tapioca gem cbor`.
# source://cbor//lib/cbor/version.rb#1
module CBOR
private
def decode(*_arg0); end
def dump(*_arg0); end
def encode(*_arg0); end
def load(*_arg0); end
def pack(*_arg0); end
def unpack(*_arg0); end
class << self
def decode(*_arg0); end
def dump(*_arg0); end
def encode(*_arg0); end
def load(*_arg0); end
def pack(*_arg0); end
def unpack(*_arg0); end
end
end
class CBOR::Buffer
def initialize(*_arg0); end
def <<(_arg0); end
def clear; end
def close; end
def empty?; end
def flush; end
def io; end
def read(*_arg0); end
def read_all(*_arg0); end
def size; end
def skip(_arg0); end
def skip_all(_arg0); end
def to_a; end
def to_s; end
def to_str; end
def write(_arg0); end
def write_to(_arg0); end
end
class CBOR::MalformedFormatError < ::CBOR::UnpackError; end
class CBOR::Packer
def initialize(*_arg0); end
def buffer; end
def clear; end
def empty?; end
def flush; end
def pack(_arg0); end
def size; end
def to_a; end
def to_s; end
def to_str; end
def write(_arg0); end
def write_array_header(_arg0); end
def write_map_header(_arg0); end
def write_nil; end
def write_to(_arg0); end
end
class CBOR::Simple < ::Struct
def to_cbor(*_arg0); end
def value; end
def value=(_); end
class << self
def [](*_arg0); end
def inspect; end
def keyword_init?; end
def members; end
def new(*_arg0); end
end
end
class CBOR::StackError < ::CBOR::UnpackError; end
class CBOR::Tagged < ::Struct
def tag; end
def tag=(_); end
def to_cbor(*_arg0); end
def value; end
def value=(_); end
class << self
def [](*_arg0); end
def inspect; end
def keyword_init?; end
def members; end
def new(*_arg0); end
end
end
class CBOR::TypeError < ::StandardError; end
class CBOR::UnpackError < ::StandardError; end
class CBOR::Unpacker
def initialize(*_arg0); end
def buffer; end
def each; end
def feed(_arg0); end
def feed_each(_arg0); end
def read; end
def read_array_header; end
def read_map_header; end
def reset; end
def skip; end
def skip_nil; end
def unpack; end
end
# source://cbor//lib/cbor/version.rb#2
CBOR::VERSION = T.let(T.unsafe(nil), String)

366
sorbet/rbi/gems/didkit@0.2.3.rbi generated Normal file
View File

@@ -0,0 +1,366 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `didkit` gem.
# Please instead update this file by running `bin/tapioca gem didkit`.
# source://didkit//lib/didkit.rb#13
DID = DIDKit::DID
# source://didkit//lib/didkit/errors.rb#1
module DIDKit; end
# source://didkit//lib/didkit/errors.rb#5
class DIDKit::APIError < ::StandardError
# @return [APIError] a new instance of APIError
#
# source://didkit//lib/didkit/errors.rb#8
def initialize(response); end
# source://didkit//lib/didkit/errors.rb#17
def body; end
# Returns the value of attribute response.
#
# source://didkit//lib/didkit/errors.rb#6
def response; end
# source://didkit//lib/didkit/errors.rb#13
def status; end
end
# source://didkit//lib/didkit/at_handles.rb#2
module DIDKit::AtHandles
# @raise [FormatError]
#
# source://didkit//lib/didkit/at_handles.rb#6
def parse_also_known_as(aka); end
end
# source://didkit//lib/didkit/at_handles.rb#3
class DIDKit::AtHandles::FormatError < ::StandardError; end
# source://didkit//lib/didkit/did.rb#6
class DIDKit::DID
include ::DIDKit::Requests
# @return [DID] a new instance of DID
#
# source://didkit//lib/didkit/did.rb#15
def initialize(did, resolved_by = T.unsafe(nil)); end
# source://didkit//lib/didkit/did.rb#80
def ==(other); end
# @return [Boolean]
#
# source://didkit//lib/didkit/did.rb#72
def account_exists?; end
# Returns the value of attribute did.
#
# source://didkit//lib/didkit/did.rb#13
def did; end
# source://didkit//lib/didkit/did.rb#40
def get_audit_log; end
# source://didkit//lib/didkit/did.rb#32
def get_document; end
# source://didkit//lib/didkit/did.rb#36
def get_validated_handle; end
# @return [Boolean]
#
# source://didkit//lib/didkit/did.rb#52
def is_known_by_relay?(relay, options = T.unsafe(nil)); end
# Returns the value of attribute resolved_by.
#
# source://didkit//lib/didkit/did.rb#13
def resolved_by; end
# Returns the value of attribute did.
#
# source://didkit//lib/didkit/did.rb#13
def to_s; end
# Returns the value of attribute type.
#
# source://didkit//lib/didkit/did.rb#13
def type; end
# source://didkit//lib/didkit/did.rb#48
def web_domain; end
class << self
# source://didkit//lib/didkit/did.rb#9
def resolve_handle(handle); end
end
end
# source://didkit//lib/didkit/errors.rb#2
class DIDKit::DIDError < ::StandardError; end
# source://didkit//lib/didkit/document.rb#7
class DIDKit::Document
include ::DIDKit::AtHandles
include ::DIDKit::Services
# @raise [FormatError]
# @return [Document] a new instance of Document
#
# source://didkit//lib/didkit/document.rb#16
def initialize(did, json); end
# Returns the value of attribute did.
#
# source://didkit//lib/didkit/document.rb#14
def did; end
# source://didkit//lib/didkit/document.rb#40
def get_validated_handle; end
# Returns the value of attribute handles.
#
# source://didkit//lib/didkit/document.rb#14
def handles; end
# Returns the value of attribute json.
#
# source://didkit//lib/didkit/document.rb#14
def json; end
# Returns the value of attribute services.
#
# source://didkit//lib/didkit/document.rb#14
def services; end
end
# source://didkit//lib/didkit/document.rb#8
class DIDKit::Document::FormatError < ::StandardError; end
# source://didkit//lib/didkit/plc_importer.rb#8
class DIDKit::PLCImporter
# @return [PLCImporter] a new instance of PLCImporter
#
# source://didkit//lib/didkit/plc_importer.rb#14
def initialize(since: T.unsafe(nil)); end
# @return [Boolean]
#
# source://didkit//lib/didkit/plc_importer.rb#83
def eof?; end
# Returns the value of attribute error_handler.
#
# source://didkit//lib/didkit/plc_importer.rb#12
def error_handler; end
# Sets the attribute error_handler
#
# @param value the value to set the attribute error_handler to.
#
# source://didkit//lib/didkit/plc_importer.rb#12
def error_handler=(_arg0); end
# source://didkit//lib/didkit/plc_importer.rb#75
def fetch(&block); end
# source://didkit//lib/didkit/plc_importer.rb#49
def fetch_audit_log(did); end
# source://didkit//lib/didkit/plc_importer.rb#54
def fetch_page; end
# source://didkit//lib/didkit/plc_importer.rb#41
def get_export(args = T.unsafe(nil)); end
# Returns the value of attribute ignore_errors.
#
# source://didkit//lib/didkit/plc_importer.rb#12
def ignore_errors; end
# Sets the attribute ignore_errors
#
# @param value the value to set the attribute ignore_errors to.
#
# source://didkit//lib/didkit/plc_importer.rb#31
def ignore_errors=(val); end
# Returns the value of attribute last_date.
#
# source://didkit//lib/didkit/plc_importer.rb#12
def last_date; end
# Sets the attribute last_date
#
# @param value the value to set the attribute last_date to.
#
# source://didkit//lib/didkit/plc_importer.rb#12
def last_date=(_arg0); end
# source://didkit//lib/didkit/plc_importer.rb#27
def plc_service; end
end
# source://didkit//lib/didkit/plc_importer.rb#10
DIDKit::PLCImporter::MAX_PAGE = T.let(T.unsafe(nil), Integer)
# source://didkit//lib/didkit/plc_importer.rb#9
DIDKit::PLCImporter::PLC_SERVICE = T.let(T.unsafe(nil), String)
# source://didkit//lib/didkit/plc_operation.rb#8
class DIDKit::PLCOperation
include ::DIDKit::AtHandles
include ::DIDKit::Services
# @raise [FormatError]
# @return [PLCOperation] a new instance of PLCOperation
#
# source://didkit//lib/didkit/plc_operation.rb#17
def initialize(json); end
# Returns the value of attribute created_at.
#
# source://didkit//lib/didkit/plc_operation.rb#15
def created_at; end
# Returns the value of attribute did.
#
# source://didkit//lib/didkit/plc_operation.rb#15
def did; end
# Returns the value of attribute handles.
#
# source://didkit//lib/didkit/plc_operation.rb#15
def handles; end
# Returns the value of attribute json.
#
# source://didkit//lib/didkit/plc_operation.rb#15
def json; end
# Returns the value of attribute services.
#
# source://didkit//lib/didkit/plc_operation.rb#15
def services; end
# Returns the value of attribute type.
#
# source://didkit//lib/didkit/plc_operation.rb#15
def type; end
end
# source://didkit//lib/didkit/plc_operation.rb#9
class DIDKit::PLCOperation::FormatError < ::StandardError; end
# source://didkit//lib/didkit/requests.rb#1
module DIDKit::Requests
# source://didkit//lib/didkit/requests.rb#2
def get_response(url, options = T.unsafe(nil)); end
end
# source://didkit//lib/didkit/resolver.rb#11
class DIDKit::Resolver
include ::DIDKit::Requests
# @return [Resolver] a new instance of Resolver
#
# source://didkit//lib/didkit/resolver.rb#19
def initialize(options = T.unsafe(nil)); end
# source://didkit//lib/didkit/resolver.rb#97
def get_validated_handle(did_or_doc); end
# Returns the value of attribute nameserver.
#
# source://didkit//lib/didkit/resolver.rb#17
def nameserver; end
# Sets the attribute nameserver
#
# @param value the value to set the attribute nameserver to.
#
# source://didkit//lib/didkit/resolver.rb#17
def nameserver=(_arg0); end
# source://didkit//lib/didkit/resolver.rb#70
def parse_did_from_dns(txt); end
# source://didkit//lib/didkit/resolver.rb#74
def parse_did_from_well_known(text); end
# source://didkit//lib/didkit/resolver.rb#103
def pick_valid_handle(did, handles); end
# source://didkit//lib/didkit/resolver.rb#64
def resolv_options; end
# source://didkit//lib/didkit/resolver.rb#79
def resolve_did(did); end
# source://didkit//lib/didkit/resolver.rb#85
def resolve_did_plc(did); end
# source://didkit//lib/didkit/resolver.rb#91
def resolve_did_web(did); end
# source://didkit//lib/didkit/resolver.rb#23
def resolve_handle(handle); end
# source://didkit//lib/didkit/resolver.rb#37
def resolve_handle_by_dns(domain); end
# source://didkit//lib/didkit/resolver.rb#51
def resolve_handle_by_well_known(domain); end
end
# source://didkit//lib/didkit/resolver.rb#13
DIDKit::Resolver::MAX_REDIRECTS = T.let(T.unsafe(nil), Integer)
# source://didkit//lib/didkit/resolver.rb#12
DIDKit::Resolver::RESERVED_DOMAINS = T.let(T.unsafe(nil), Array)
# source://didkit//lib/didkit/service_record.rb#5
class DIDKit::ServiceRecord
# @return [ServiceRecord] a new instance of ServiceRecord
#
# source://didkit//lib/didkit/service_record.rb#11
def initialize(key, type, endpoint); end
# Returns the value of attribute endpoint.
#
# source://didkit//lib/didkit/service_record.rb#9
def endpoint; end
# Returns the value of attribute key.
#
# source://didkit//lib/didkit/service_record.rb#9
def key; end
# Returns the value of attribute type.
#
# source://didkit//lib/didkit/service_record.rb#9
def type; end
end
# source://didkit//lib/didkit/service_record.rb#6
class DIDKit::ServiceRecord::FormatError < ::StandardError; end
# source://didkit//lib/didkit/services.rb#2
module DIDKit::Services
# source://didkit//lib/didkit/services.rb#3
def get_service(key, type); end
# source://didkit//lib/didkit/services.rb#11
def labeler_endpoint; end
# source://didkit//lib/didkit/services.rb#7
def pds_endpoint; end
end
# source://didkit//lib/didkit/version.rb#4
DIDKit::VERSION = T.let(T.unsafe(nil), String)

5205
sorbet/rbi/gems/eventmachine@1.2.7.rbi generated Normal file

File diff suppressed because it is too large Load Diff

548
sorbet/rbi/gems/faye-websocket@0.12.0.rbi generated Normal file
View File

@@ -0,0 +1,548 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `faye-websocket` gem.
# Please instead update this file by running `bin/tapioca gem faye-websocket`.
# source://faye-websocket//lib/faye/websocket.rb#13
module Faye; end
# source://faye-websocket//lib/faye/eventsource.rb#4
class Faye::EventSource
include ::WebSocket::Driver::EventEmitter
include ::Faye::WebSocket::API::EventTarget
# @return [EventSource] a new instance of EventSource
#
# source://faye-websocket//lib/faye/eventsource.rb#21
def initialize(env, options = T.unsafe(nil)); end
# source://faye-websocket//lib/faye/eventsource.rb#100
def close; end
# Returns the value of attribute env.
#
# source://faye-websocket//lib/faye/eventsource.rb#7
def env; end
# source://faye-websocket//lib/faye/eventsource.rb#57
def last_event_id; end
# source://faye-websocket//lib/faye/eventsource.rb#94
def ping(message = T.unsafe(nil)); end
# source://faye-websocket//lib/faye/eventsource.rb#61
def rack_response; end
# Returns the value of attribute ready_state.
#
# source://faye-websocket//lib/faye/eventsource.rb#7
def ready_state; end
# source://faye-websocket//lib/faye/eventsource.rb#79
def send(message, options = T.unsafe(nil)); end
# Returns the value of attribute url.
#
# source://faye-websocket//lib/faye/eventsource.rb#7
def url; end
private
# source://faye-websocket//lib/faye/eventsource.rb#67
def open; end
class << self
# source://faye-websocket//lib/faye/eventsource.rb#17
def determine_url(env); end
# @return [Boolean]
#
# source://faye-websocket//lib/faye/eventsource.rb#11
def eventsource?(env); end
end
end
# source://faye-websocket//lib/faye/eventsource.rb#9
Faye::EventSource::DEFAULT_RETRY = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/eventsource.rb#112
class Faye::EventSource::Stream < ::Faye::RackStream
# source://faye-websocket//lib/faye/eventsource.rb#113
def fail; end
end
# source://faye-websocket//lib/faye/rack_stream.rb#2
class Faye::RackStream
include ::EventMachine::Deferrable
# @return [RackStream] a new instance of RackStream
#
# source://faye-websocket//lib/faye/rack_stream.rb#18
def initialize(socket); end
# source://faye-websocket//lib/faye/rack_stream.rb#55
def clean_rack_hijack; end
# source://faye-websocket//lib/faye/rack_stream.rb#61
def close_connection; end
# source://faye-websocket//lib/faye/rack_stream.rb#66
def close_connection_after_writing; end
# source://faye-websocket//lib/faye/rack_stream.rb#71
def each(&callback); end
# source://faye-websocket//lib/faye/rack_stream.rb#75
def fail; end
# source://faye-websocket//lib/faye/rack_stream.rb#30
def hijack_rack_socket; end
# source://faye-websocket//lib/faye/rack_stream.rb#78
def receive(data); end
# source://faye-websocket//lib/faye/rack_stream.rb#81
def write(data); end
end
# source://faye-websocket//lib/faye/rack_stream.rb#6
module Faye::RackStream::Reader
# source://faye-websocket//lib/faye/rack_stream.rb#9
def receive_data(data); end
# Returns the value of attribute stream.
#
# source://faye-websocket//lib/faye/rack_stream.rb#7
def stream; end
# Sets the attribute stream
#
# @param value the value to set the attribute stream to.
#
# source://faye-websocket//lib/faye/rack_stream.rb#7
def stream=(_arg0); end
# source://faye-websocket//lib/faye/rack_stream.rb#13
def unbind; end
end
# source://faye-websocket//lib/faye/websocket.rb#17
class Faye::WebSocket
include ::WebSocket::Driver::EventEmitter
include ::Faye::WebSocket::API::EventTarget
include ::Faye::WebSocket::API
# @return [WebSocket] a new instance of WebSocket
#
# source://faye-websocket//lib/faye/websocket.rb#69
def initialize(env, protocols = T.unsafe(nil), options = T.unsafe(nil)); end
# Returns the value of attribute env.
#
# source://faye-websocket//lib/faye/websocket.rb#66
def env; end
# source://faye-websocket//lib/faye/websocket.rb#91
def rack_response; end
# source://faye-websocket//lib/faye/websocket.rb#85
def start_driver; end
class << self
# source://faye-websocket//lib/faye/websocket.rb#31
def determine_url(env, schemes = T.unsafe(nil)); end
# source://faye-websocket//lib/faye/websocket.rb#40
def ensure_reactor_running; end
# source://faye-websocket//lib/faye/websocket.rb#45
def load_adapter(backend); end
# @return [Boolean]
#
# source://faye-websocket//lib/faye/websocket.rb#52
def secure_request?(env); end
# @return [Boolean]
#
# source://faye-websocket//lib/faye/websocket.rb#62
def websocket?(env); end
end
end
# source://faye-websocket//lib/faye/websocket.rb#25
Faye::WebSocket::ADAPTERS = T.let(T.unsafe(nil), Hash)
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#1
module Faye::WebSocket::API
include ::WebSocket::Driver::EventEmitter
include ::Faye::WebSocket::API::EventTarget
extend ::Forwardable
# source://faye-websocket//lib/faye/websocket/api.rb#22
def initialize(options = T.unsafe(nil)); end
# Returns the value of attribute buffered_amount.
#
# source://faye-websocket//lib/faye/websocket/api.rb#20
def buffered_amount; end
# source://faye-websocket//lib/faye/websocket/api.rb#90
def close(code = T.unsafe(nil), reason = T.unsafe(nil)); end
# source://faye-websocket//lib/faye/websocket/api.rb#85
def ping(message = T.unsafe(nil), &callback); end
# source://faye-websocket//lib/faye/websocket/api.rb#109
def protocol; end
# Returns the value of attribute ready_state.
#
# source://faye-websocket//lib/faye/websocket/api.rb#20
def ready_state; end
# source://faye-websocket//lib/faye/websocket/api.rb#66
def send(message); end
# Returns the value of attribute url.
#
# source://faye-websocket//lib/faye/websocket/api.rb#20
def url; end
# source://forwardable/1.3.3/forwardable.rb#231
def version(*args, **_arg1, &block); end
# source://faye-websocket//lib/faye/websocket/api.rb#62
def write(data); end
private
# source://faye-websocket//lib/faye/websocket/api.rb#138
def begin_close(reason, code, options = T.unsafe(nil)); end
# source://faye-websocket//lib/faye/websocket/api.rb#130
def emit_error(message); end
# source://faye-websocket//lib/faye/websocket/api.rb#154
def finalize_close; end
# source://faye-websocket//lib/faye/websocket/api.rb#115
def open; end
# source://faye-websocket//lib/faye/websocket/api.rb#169
def parse(data); end
# source://faye-websocket//lib/faye/websocket/api.rb#123
def receive_message(data); end
end
# source://faye-websocket//lib/faye/websocket/api.rb#11
Faye::WebSocket::API::CLOSED = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/websocket/api.rb#13
Faye::WebSocket::API::CLOSE_TIMEOUT = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/websocket/api.rb#10
Faye::WebSocket::API::CLOSING = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/websocket/api.rb#8
Faye::WebSocket::API::CONNECTING = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/websocket/api/event.rb#36
class Faye::WebSocket::API::CloseEvent < ::Faye::WebSocket::API::Event
# Returns the value of attribute code.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#37
def code; end
# Returns the value of attribute reason.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#37
def reason; end
end
# source://faye-websocket//lib/faye/websocket/api/event.rb#40
class Faye::WebSocket::API::ErrorEvent < ::Faye::WebSocket::API::Event
# Returns the value of attribute message.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#41
def message; end
end
# source://faye-websocket//lib/faye/websocket/api/event.rb#3
class Faye::WebSocket::API::Event
# @return [Event] a new instance of Event
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#11
def initialize(event_type, options); end
# Returns the value of attribute bubbles.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#4
def bubbles; end
# Returns the value of attribute cancelable.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#4
def cancelable; end
# Returns the value of attribute current_target.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#5
def current_target; end
# Sets the attribute current_target
#
# @param value the value to set the attribute current_target to.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#5
def current_target=(_arg0); end
# Returns the value of attribute event_phase.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#5
def event_phase; end
# Sets the attribute event_phase
#
# @param value the value to set the attribute event_phase to.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#5
def event_phase=(_arg0); end
# source://faye-websocket//lib/faye/websocket/api/event.rb#16
def init_event(event_type, can_bubble, cancelable); end
# source://faye-websocket//lib/faye/websocket/api/event.rb#25
def prevent_default; end
# source://faye-websocket//lib/faye/websocket/api/event.rb#22
def stop_propagation; end
# Returns the value of attribute target.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#5
def target; end
# Sets the attribute target
#
# @param value the value to set the attribute target to.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#5
def target=(_arg0); end
# Returns the value of attribute type.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#4
def type; end
class << self
# source://faye-websocket//lib/faye/websocket/api/event.rb#51
def create(type, options = T.unsafe(nil)); end
end
end
# source://faye-websocket//lib/faye/websocket/api/event.rb#8
Faye::WebSocket::API::Event::AT_TARGET = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/websocket/api/event.rb#9
Faye::WebSocket::API::Event::BUBBLING_PHASE = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/websocket/api/event.rb#7
Faye::WebSocket::API::Event::CAPTURING_PHASE = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#2
module Faye::WebSocket::API::EventTarget
include ::WebSocket::Driver::EventEmitter
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#16
def add_event_listener(event_type, listener, use_capture = T.unsafe(nil)); end
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#20
def add_listener(event_type, callable = T.unsafe(nil), &block); end
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#32
def dispatch_event(event); end
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#8
def onclose=(handler); end
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#8
def onerror=(handler); end
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#8
def onmessage=(handler); end
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#8
def onopen=(handler); end
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#28
def remove_event_listener(event_type, listener, use_capture = T.unsafe(nil)); end
private
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#55
def event_buffers; end
# source://faye-websocket//lib/faye/websocket/api/event_target.rb#49
def flush(event_type, listener); end
end
# source://faye-websocket//lib/faye/websocket/api/event.rb#32
class Faye::WebSocket::API::MessageEvent < ::Faye::WebSocket::API::Event
# Returns the value of attribute data.
#
# source://faye-websocket//lib/faye/websocket/api/event.rb#33
def data; end
end
# source://faye-websocket//lib/faye/websocket/api.rb#9
Faye::WebSocket::API::OPEN = T.let(T.unsafe(nil), Integer)
# source://faye-websocket//lib/faye/websocket/api/event.rb#29
class Faye::WebSocket::API::OpenEvent < ::Faye::WebSocket::API::Event; end
# source://faye-websocket//lib/faye/websocket/api/event.rb#44
Faye::WebSocket::API::TYPES = T.let(T.unsafe(nil), Hash)
# source://faye-websocket//lib/faye/websocket/adapter.rb#4
module Faye::WebSocket::Adapter
# @return [Boolean]
#
# source://faye-websocket//lib/faye/websocket/adapter.rb#10
def eventsource?; end
# @return [Boolean]
#
# source://faye-websocket//lib/faye/websocket/adapter.rb#15
def socket_connection?; end
# @return [Boolean]
#
# source://faye-websocket//lib/faye/websocket/adapter.rb#5
def websocket?; end
end
# source://faye-websocket//lib/faye/websocket/client.rb#6
class Faye::WebSocket::Client
include ::WebSocket::Driver::EventEmitter
include ::Faye::WebSocket::API::EventTarget
include ::Faye::WebSocket::API
extend ::Forwardable
# @return [Client] a new instance of Client
#
# source://faye-websocket//lib/faye/websocket/client.rb#15
def initialize(url, protocols = T.unsafe(nil), options = T.unsafe(nil)); end
# source://forwardable/1.3.3/forwardable.rb#231
def headers(*args, **_arg1, &block); end
# source://forwardable/1.3.3/forwardable.rb#231
def status(*args, **_arg1, &block); end
private
# source://faye-websocket//lib/faye/websocket/client.rb#42
def configure_proxy(proxy); end
# source://faye-websocket//lib/faye/websocket/client.rb#67
def on_connect(stream); end
# source://faye-websocket//lib/faye/websocket/client.rb#75
def on_network_error(error); end
# source://faye-websocket//lib/faye/websocket/client.rb#86
def ssl_handshake_completed; end
# source://faye-websocket//lib/faye/websocket/client.rb#80
def ssl_verify_peer(cert); end
# source://faye-websocket//lib/faye/websocket/client.rb#59
def start_tls(uri, options); end
end
# source://faye-websocket//lib/faye/websocket/client.rb#92
module Faye::WebSocket::Client::Connection
# source://faye-websocket//lib/faye/websocket/client.rb#95
def connection_completed; end
# Returns the value of attribute parent.
#
# source://faye-websocket//lib/faye/websocket/client.rb#93
def parent; end
# Sets the attribute parent
#
# @param value the value to set the attribute parent to.
#
# source://faye-websocket//lib/faye/websocket/client.rb#93
def parent=(_arg0); end
# source://faye-websocket//lib/faye/websocket/client.rb#107
def receive_data(data); end
# source://faye-websocket//lib/faye/websocket/client.rb#103
def ssl_handshake_completed; end
# source://faye-websocket//lib/faye/websocket/client.rb#99
def ssl_verify_peer(cert); end
# source://faye-websocket//lib/faye/websocket/client.rb#111
def unbind(error = T.unsafe(nil)); end
# source://faye-websocket//lib/faye/websocket/client.rb#116
def write(data); end
end
# source://faye-websocket//lib/faye/websocket/client.rb#10
Faye::WebSocket::Client::DEFAULT_PORTS = T.let(T.unsafe(nil), Hash)
# source://faye-websocket//lib/faye/websocket/client.rb#11
Faye::WebSocket::Client::SECURE_PROTOCOLS = T.let(T.unsafe(nil), Array)
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#26
class Faye::WebSocket::SSLError < ::OpenSSL::SSL::SSLError; end
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#28
class Faye::WebSocket::SslVerifier
# @return [SslVerifier] a new instance of SslVerifier
#
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#29
def initialize(hostname, ssl_opts); end
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#56
def ssl_handshake_completed; end
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#41
def ssl_verify_peer(cert_text); end
private
# @return [Boolean]
#
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#86
def identity_verified?; end
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#74
def parse_cert(cert_text); end
# @return [Boolean]
#
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#70
def should_verify?; end
# source://faye-websocket//lib/faye/websocket/ssl_verifier.rb#80
def store_cert(certificate); end
end
# source://faye-websocket//lib/faye/websocket.rb#96
class Faye::WebSocket::Stream < ::Faye::RackStream
# source://faye-websocket//lib/faye/websocket.rb#97
def fail; end
# source://faye-websocket//lib/faye/websocket.rb#101
def receive(data); end
end

829
sorbet/rbi/gems/skyfall@0.6.0.rbi generated Normal file
View File

@@ -0,0 +1,829 @@
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for types exported from the `skyfall` gem.
# Please instead update this file by running `bin/tapioca gem skyfall`.
# CIDs in DAG-CBOR: https://ipld.io/specs/codecs/dag-cbor/spec/
# CIDs in JSON: https://ipld.io/specs/codecs/dag-json/spec/
# multibase: https://github.com/multiformats/multibase
#
# source://skyfall//lib/skyfall/version.rb#3
module Skyfall; end
# source://skyfall//lib/skyfall/cid.rb#10
class Skyfall::CID
# @return [CID] a new instance of CID
#
# source://skyfall//lib/skyfall/cid.rb#27
def initialize(data); end
# source://skyfall//lib/skyfall/cid.rb#39
def ==(other); end
# Returns the value of attribute data.
#
# source://skyfall//lib/skyfall/cid.rb#11
def data; end
# source://skyfall//lib/skyfall/cid.rb#35
def inspect; end
# source://skyfall//lib/skyfall/cid.rb#31
def to_s; end
class << self
# @raise [DecodeError]
#
# source://skyfall//lib/skyfall/cid.rb#13
def from_cbor_tag(tag); end
# @raise [DecodeError]
#
# source://skyfall//lib/skyfall/cid.rb#19
def from_json(string); end
end
end
# source://skyfall//lib/skyfall/car_archive.rb#26
class Skyfall::CarArchive
# @return [CarArchive] a new instance of CarArchive
#
# source://skyfall//lib/skyfall/car_archive.rb#31
def initialize(data); end
# Returns the value of attribute roots.
#
# source://skyfall//lib/skyfall/car_archive.rb#29
def roots; end
# source://skyfall//lib/skyfall/car_archive.rb#39
def section_with_cid(cid); end
# Returns the value of attribute sections.
#
# source://skyfall//lib/skyfall/car_archive.rb#29
def sections; end
private
# @raise [DecodeError]
#
# source://skyfall//lib/skyfall/car_archive.rb#80
def read_header(buffer); end
# @raise [DecodeError]
#
# source://skyfall//lib/skyfall/car_archive.rb#91
def read_section(buffer); end
class << self
# source://skyfall//lib/skyfall/car_archive.rb#44
def convert_data(object); end
# source://skyfall//lib/skyfall/car_archive.rb#74
def make_bytes(data); end
# source://skyfall//lib/skyfall/car_archive.rb#70
def make_cid_link(cid); end
end
end
# source://skyfall//lib/skyfall/car_archive.rb#13
class Skyfall::CarSection
# @return [CarSection] a new instance of CarSection
#
# source://skyfall//lib/skyfall/car_archive.rb#16
def initialize(cid, body_data); end
# source://skyfall//lib/skyfall/car_archive.rb#21
def body; end
# Returns the value of attribute cid.
#
# source://skyfall//lib/skyfall/car_archive.rb#14
def cid; end
end
# source://skyfall//lib/skyfall/collection.rb#2
module Skyfall::Collection
class << self
# source://skyfall//lib/skyfall/collection.rb#46
def from_short_code(code); end
# source://skyfall//lib/skyfall/collection.rb#42
def short_code(collection); end
end
end
# source://skyfall//lib/skyfall/collection.rb#4
Skyfall::Collection::BSKY_ACTOR_STATUS = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#11
Skyfall::Collection::BSKY_BLOCK = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#20
Skyfall::Collection::BSKY_CHAT_DECLARATION = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#5
Skyfall::Collection::BSKY_FEED = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#12
Skyfall::Collection::BSKY_FOLLOW = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#18
Skyfall::Collection::BSKY_LABELER = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#6
Skyfall::Collection::BSKY_LIKE = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#13
Skyfall::Collection::BSKY_LIST = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#14
Skyfall::Collection::BSKY_LISTBLOCK = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#15
Skyfall::Collection::BSKY_LISTITEM = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#7
Skyfall::Collection::BSKY_POST = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#8
Skyfall::Collection::BSKY_POSTGATE = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#3
Skyfall::Collection::BSKY_PROFILE = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#9
Skyfall::Collection::BSKY_REPOST = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#16
Skyfall::Collection::BSKY_STARTERPACK = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#10
Skyfall::Collection::BSKY_THREADGATE = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#17
Skyfall::Collection::BSKY_VERIFICATION = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/collection.rb#22
Skyfall::Collection::SHORT_CODES = T.let(T.unsafe(nil), Hash)
# source://skyfall//lib/skyfall/errors.rb#2
class Skyfall::DecodeError < ::StandardError; end
# source://skyfall//lib/skyfall/extensions.rb#5
module Skyfall::Extensions; end
# source://skyfall//lib/skyfall/firehose.rb#5
class Skyfall::Firehose < ::Skyfall::Stream
# @return [Firehose] a new instance of Firehose
#
# source://skyfall//lib/skyfall/firehose.rb#16
def initialize(server, endpoint, cursor = T.unsafe(nil)); end
# Returns the value of attribute cursor.
#
# source://skyfall//lib/skyfall/firehose.rb#14
def cursor; end
# Sets the attribute cursor
#
# @param value the value to set the attribute cursor to.
#
# source://skyfall//lib/skyfall/firehose.rb#14
def cursor=(_arg0); end
# source://skyfall//lib/skyfall/firehose.rb#29
def handle_message(msg); end
private
# source://skyfall//lib/skyfall/firehose.rb#45
def build_websocket_url; end
# source://skyfall//lib/skyfall/firehose.rb#49
def check_cursor(cursor); end
# source://skyfall//lib/skyfall/firehose.rb#59
def check_endpoint(endpoint); end
end
# source://skyfall//lib/skyfall/firehose/account_message.rb#4
class Skyfall::Firehose::AccountMessage < ::Skyfall::Firehose::Message
# source://skyfall//lib/skyfall/firehose/account_message.rb#5
def active?; end
# source://skyfall//lib/skyfall/firehose/account_message.rb#9
def status; end
end
# source://skyfall//lib/skyfall/firehose/commit_message.rb#7
class Skyfall::Firehose::CommitMessage < ::Skyfall::Firehose::Message
# source://skyfall//lib/skyfall/firehose/commit_message.rb#17
def blocks; end
# source://skyfall//lib/skyfall/firehose/commit_message.rb#8
def commit; end
# source://skyfall//lib/skyfall/firehose/commit_message.rb#21
def operations; end
# source://skyfall//lib/skyfall/firehose/commit_message.rb#12
def prev; end
# source://skyfall//lib/skyfall/firehose/commit_message.rb#25
def raw_record_for_operation(op); end
end
# source://skyfall//lib/skyfall/firehose/handle_message.rb#9
class Skyfall::Firehose::HandleMessage < ::Skyfall::Firehose::Message
# source://skyfall//lib/skyfall/firehose/handle_message.rb#10
def handle; end
end
# source://skyfall//lib/skyfall/firehose/identity_message.rb#4
class Skyfall::Firehose::IdentityMessage < ::Skyfall::Firehose::Message
# source://skyfall//lib/skyfall/firehose/identity_message.rb#5
def handle; end
end
# source://skyfall//lib/skyfall/firehose/info_message.rb#4
class Skyfall::Firehose::InfoMessage < ::Skyfall::Firehose::Message
# source://skyfall//lib/skyfall/firehose/info_message.rb#9
def initialize(type_object, data_object); end
# source://skyfall//lib/skyfall/firehose/info_message.rb#20
def inspectable_variables; end
# source://skyfall//lib/skyfall/firehose/info_message.rb#5
def message; end
# source://skyfall//lib/skyfall/firehose/info_message.rb#5
def name; end
# source://skyfall//lib/skyfall/firehose/info_message.rb#16
def to_s; end
end
# source://skyfall//lib/skyfall/firehose/info_message.rb#7
Skyfall::Firehose::InfoMessage::OUTDATED_CURSOR = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/firehose/labels_message.rb#5
class Skyfall::Firehose::LabelsMessage
# source://skyfall//lib/skyfall/firehose/labels_message.rb#11
def initialize(type_object, data_object); end
# source://skyfall//lib/skyfall/firehose/labels_message.rb#8
def data_object; end
# source://skyfall//lib/skyfall/firehose/labels_message.rb#19
def labels; end
# source://skyfall//lib/skyfall/firehose/labels_message.rb#9
def seq; end
# source://skyfall//lib/skyfall/firehose/labels_message.rb#9
def type; end
# source://skyfall//lib/skyfall/firehose/labels_message.rb#8
def type_object; end
end
# source://skyfall//lib/skyfall/firehose/message.rb#9
class Skyfall::Firehose::Message
# source://skyfall//lib/skyfall/firehose/message.rb#48
def initialize(type_object, data_object); end
# source://skyfall//lib/skyfall/firehose/message.rb#26
def data_object; end
# source://skyfall//lib/skyfall/firehose/message.rb#22
def did; end
# source://skyfall//lib/skyfall/firehose/message.rb#73
def inspect; end
# source://skyfall//lib/skyfall/firehose/message.rb#69
def inspectable_variables; end
# source://skyfall//lib/skyfall/firehose/message.rb#57
def operations; end
# source://skyfall//lib/skyfall/firehose/message.rb#22
def repo; end
# source://skyfall//lib/skyfall/firehose/message.rb#22
def seq; end
# source://skyfall//lib/skyfall/firehose/message.rb#65
def time; end
# source://skyfall//lib/skyfall/firehose/message.rb#22
def type; end
# source://skyfall//lib/skyfall/firehose/message.rb#26
def type_object; end
# source://skyfall//lib/skyfall/firehose/message.rb#61
def unknown?; end
class << self
# source://skyfall//lib/skyfall/firehose/message.rb#80
def decode_cbor_objects(data); end
# source://skyfall//lib/skyfall/firehose/message.rb#28
def new(data); end
end
end
# source://skyfall//lib/skyfall/firehose.rb#9
Skyfall::Firehose::NAMED_ENDPOINTS = T.let(T.unsafe(nil), Hash)
# source://skyfall//lib/skyfall/firehose/operation.rb#5
class Skyfall::Firehose::Operation
# source://skyfall//lib/skyfall/firehose/operation.rb#6
def initialize(message, json); end
# source://skyfall//lib/skyfall/firehose/operation.rb#21
def action; end
# source://skyfall//lib/skyfall/firehose/operation.rb#37
def cid; end
# source://skyfall//lib/skyfall/firehose/operation.rb#25
def collection; end
# source://skyfall//lib/skyfall/firehose/operation.rb#11
def did; end
# source://skyfall//lib/skyfall/firehose/operation.rb#53
def inspect; end
# source://skyfall//lib/skyfall/firehose/operation.rb#49
def inspectable_variables; end
# source://skyfall//lib/skyfall/firehose/operation.rb#17
def path; end
# source://skyfall//lib/skyfall/firehose/operation.rb#41
def raw_record; end
# source://skyfall//lib/skyfall/firehose/operation.rb#11
def repo; end
# source://skyfall//lib/skyfall/firehose/operation.rb#29
def rkey; end
# source://skyfall//lib/skyfall/firehose/operation.rb#45
def type; end
# source://skyfall//lib/skyfall/firehose/operation.rb#33
def uri; end
end
# source://skyfall//lib/skyfall/firehose.rb#7
Skyfall::Firehose::SUBSCRIBE_LABELS = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/firehose.rb#6
Skyfall::Firehose::SUBSCRIBE_REPOS = T.let(T.unsafe(nil), String)
# source://skyfall//lib/skyfall/firehose/sync_message.rb#4
class Skyfall::Firehose::SyncMessage < ::Skyfall::Firehose::Message
# source://skyfall//lib/skyfall/firehose/sync_message.rb#5
def rev; end
end
# source://skyfall//lib/skyfall/firehose/tombstone_message.rb#9
class Skyfall::Firehose::TombstoneMessage < ::Skyfall::Firehose::Message; end
# source://skyfall//lib/skyfall/firehose/unknown_message.rb#4
class Skyfall::Firehose::UnknownMessage < ::Skyfall::Firehose::Message; end
# source://skyfall//lib/skyfall/jetstream.rb#8
class Skyfall::Jetstream < ::Skyfall::Stream
# @return [Jetstream] a new instance of Jetstream
#
# source://skyfall//lib/skyfall/jetstream.rb#11
def initialize(server, params = T.unsafe(nil)); end
# Returns the value of attribute cursor.
#
# source://skyfall//lib/skyfall/jetstream.rb#9
def cursor; end
# Sets the attribute cursor
#
# @param value the value to set the attribute cursor to.
#
# source://skyfall//lib/skyfall/jetstream.rb#9
def cursor=(_arg0); end
# source://skyfall//lib/skyfall/jetstream.rb#25
def handle_message(msg); end
private
# source://skyfall//lib/skyfall/jetstream.rb#40
def build_websocket_url; end
# source://skyfall//lib/skyfall/jetstream.rb#110
def check_cursor(cursor); end
# source://skyfall//lib/skyfall/jetstream.rb#69
def check_option(k, v); end
# @raise [ArgumentError]
#
# source://skyfall//lib/skyfall/jetstream.rb#47
def check_params(params); end
# source://skyfall//lib/skyfall/jetstream.rb#84
def check_wanted_collections(list); end
# source://skyfall//lib/skyfall/jetstream.rb#99
def check_wanted_dids(list); end
end
# source://skyfall//lib/skyfall/jetstream/account_message.rb#5
class Skyfall::Jetstream::AccountMessage < ::Skyfall::Jetstream::Message
# source://skyfall//lib/skyfall/jetstream/account_message.rb#6
def initialize(json); end
# source://skyfall//lib/skyfall/jetstream/account_message.rb#11
def active?; end
# source://skyfall//lib/skyfall/jetstream/account_message.rb#15
def status; end
end
# source://skyfall//lib/skyfall/jetstream/commit_message.rb#6
class Skyfall::Jetstream::CommitMessage < ::Skyfall::Jetstream::Message
# source://skyfall//lib/skyfall/jetstream/commit_message.rb#7
def initialize(json); end
# source://skyfall//lib/skyfall/jetstream/commit_message.rb#12
def operations; end
end
# source://skyfall//lib/skyfall/jetstream/identity_message.rb#5
class Skyfall::Jetstream::IdentityMessage < ::Skyfall::Jetstream::Message
# source://skyfall//lib/skyfall/jetstream/identity_message.rb#6
def initialize(json); end
# source://skyfall//lib/skyfall/jetstream/identity_message.rb#11
def handle; end
end
# source://skyfall//lib/skyfall/jetstream/message.rb#7
class Skyfall::Jetstream::Message
# source://skyfall//lib/skyfall/jetstream/message.rb#35
def initialize(json); end
# source://skyfall//lib/skyfall/jetstream/message.rb#13
def did; end
# source://skyfall//lib/skyfall/jetstream/message.rb#18
def json; end
# source://skyfall//lib/skyfall/jetstream/message.rb#46
def operations; end
# source://skyfall//lib/skyfall/jetstream/message.rb#13
def repo; end
# source://skyfall//lib/skyfall/jetstream/message.rb#13
def seq; end
# source://skyfall//lib/skyfall/jetstream/message.rb#50
def time; end
# source://skyfall//lib/skyfall/jetstream/message.rb#13
def time_us; end
# source://skyfall//lib/skyfall/jetstream/message.rb#13
def type; end
# source://skyfall//lib/skyfall/jetstream/message.rb#42
def unknown?; end
class << self
# source://skyfall//lib/skyfall/jetstream/message.rb#20
def new(data); end
end
end
# source://skyfall//lib/skyfall/jetstream/operation.rb#5
class Skyfall::Jetstream::Operation
# source://skyfall//lib/skyfall/jetstream/operation.rb#6
def initialize(message, json); end
# source://skyfall//lib/skyfall/jetstream/operation.rb#21
def action; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#37
def cid; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#25
def collection; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#11
def did; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#53
def inspect; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#49
def inspectable_variables; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#17
def path; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#41
def raw_record; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#11
def repo; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#29
def rkey; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#45
def type; end
# source://skyfall//lib/skyfall/jetstream/operation.rb#33
def uri; end
end
# source://skyfall//lib/skyfall/jetstream/unknown_message.rb#4
class Skyfall::Jetstream::UnknownMessage < ::Skyfall::Jetstream::Message; end
# source://skyfall//lib/skyfall/label.rb#5
class Skyfall::Label
# @raise [DecodeError]
# @return [Label] a new instance of Label
#
# source://skyfall//lib/skyfall/label.rb#8
def initialize(data); end
# source://skyfall//lib/skyfall/label.rb#27
def authority; end
# source://skyfall//lib/skyfall/label.rb#35
def cid; end
# source://skyfall//lib/skyfall/label.rb#47
def created_at; end
# source://skyfall//lib/skyfall/label.rb#47
def cts; end
# Returns the value of attribute data.
#
# source://skyfall//lib/skyfall/label.rb#6
def data; end
# source://skyfall//lib/skyfall/label.rb#51
def exp; end
# source://skyfall//lib/skyfall/label.rb#51
def expires_at; end
# @return [Boolean]
#
# source://skyfall//lib/skyfall/label.rb#43
def neg; end
# @return [Boolean]
#
# source://skyfall//lib/skyfall/label.rb#43
def negation?; end
# source://skyfall//lib/skyfall/label.rb#27
def src; end
# source://skyfall//lib/skyfall/label.rb#31
def subject; end
# source://skyfall//lib/skyfall/label.rb#31
def uri; end
# source://skyfall//lib/skyfall/label.rb#39
def val; end
# source://skyfall//lib/skyfall/label.rb#39
def value; end
# source://skyfall//lib/skyfall/label.rb#23
def ver; end
# source://skyfall//lib/skyfall/label.rb#23
def version; end
end
# source://skyfall//lib/skyfall/stream.rb#8
class Skyfall::Stream
# @return [Stream] a new instance of Stream
#
# source://skyfall//lib/skyfall/stream.rb#15
def initialize(service); end
# Returns the value of attribute auto_reconnect.
#
# source://skyfall//lib/skyfall/stream.rb#12
def auto_reconnect; end
# Sets the attribute auto_reconnect
#
# @param value the value to set the attribute auto_reconnect to.
#
# source://skyfall//lib/skyfall/stream.rb#12
def auto_reconnect=(_arg0); end
# Returns the value of attribute check_heartbeat.
#
# source://skyfall//lib/skyfall/stream.rb#13
def check_heartbeat; end
# Sets the attribute check_heartbeat
#
# @param value the value to set the attribute check_heartbeat to.
#
# source://skyfall//lib/skyfall/stream.rb#116
def check_heartbeat=(value); end
# source://skyfall//lib/skyfall/stream.rb#98
def close; end
# source://skyfall//lib/skyfall/stream.rb#30
def connect; end
# source://skyfall//lib/skyfall/stream.rb#108
def default_user_agent; end
# source://skyfall//lib/skyfall/stream.rb#98
def disconnect; end
# source://skyfall//lib/skyfall/stream.rb#86
def handle_message(msg); end
# Returns the value of attribute heartbeat_interval.
#
# source://skyfall//lib/skyfall/stream.rb#13
def heartbeat_interval; end
# Sets the attribute heartbeat_interval
#
# @param value the value to set the attribute heartbeat_interval to.
#
# source://skyfall//lib/skyfall/stream.rb#13
def heartbeat_interval=(_arg0); end
# Returns the value of attribute heartbeat_timeout.
#
# source://skyfall//lib/skyfall/stream.rb#13
def heartbeat_timeout; end
# Sets the attribute heartbeat_timeout
#
# @param value the value to set the attribute heartbeat_timeout to.
#
# source://skyfall//lib/skyfall/stream.rb#13
def heartbeat_timeout=(_arg0); end
# source://skyfall//lib/skyfall/stream.rb#160
def inspect; end
# source://skyfall//lib/skyfall/stream.rb#156
def inspectable_variables; end
# Returns the value of attribute last_update.
#
# source://skyfall//lib/skyfall/stream.rb#12
def last_update; end
# Sets the attribute last_update
#
# @param value the value to set the attribute last_update to.
#
# source://skyfall//lib/skyfall/stream.rb#12
def last_update=(_arg0); end
# source://skyfall//lib/skyfall/stream.rb#147
def on_connect(&block); end
# source://skyfall//lib/skyfall/stream.rb#151
def on_connect=(block); end
# source://skyfall//lib/skyfall/stream.rb#147
def on_connecting(&block); end
# source://skyfall//lib/skyfall/stream.rb#151
def on_connecting=(block); end
# source://skyfall//lib/skyfall/stream.rb#147
def on_disconnect(&block); end
# source://skyfall//lib/skyfall/stream.rb#151
def on_disconnect=(block); end
# source://skyfall//lib/skyfall/stream.rb#147
def on_error(&block); end
# source://skyfall//lib/skyfall/stream.rb#151
def on_error=(block); end
# source://skyfall//lib/skyfall/stream.rb#147
def on_message(&block); end
# source://skyfall//lib/skyfall/stream.rb#151
def on_message=(block); end
# source://skyfall//lib/skyfall/stream.rb#147
def on_raw_message(&block); end
# source://skyfall//lib/skyfall/stream.rb#151
def on_raw_message=(block); end
# source://skyfall//lib/skyfall/stream.rb#147
def on_reconnect(&block); end
# source://skyfall//lib/skyfall/stream.rb#151
def on_reconnect=(block); end
# source://skyfall//lib/skyfall/stream.rb#147
def on_timeout(&block); end
# source://skyfall//lib/skyfall/stream.rb#151
def on_timeout=(block); end
# source://skyfall//lib/skyfall/stream.rb#91
def reconnect; end
# source://skyfall//lib/skyfall/stream.rb#126
def start_heartbeat_timer; end
# source://skyfall//lib/skyfall/stream.rb#141
def stop_heartbeat_timer; end
# Returns the value of attribute user_agent.
#
# source://skyfall//lib/skyfall/stream.rb#12
def user_agent; end
# Sets the attribute user_agent
#
# @param value the value to set the attribute user_agent to.
#
# source://skyfall//lib/skyfall/stream.rb#12
def user_agent=(_arg0); end
# source://skyfall//lib/skyfall/stream.rb#112
def version_string; end
private
# source://skyfall//lib/skyfall/stream.rb#184
def build_root_url(service); end
# source://skyfall//lib/skyfall/stream.rb#176
def build_websocket_client(url); end
# source://skyfall//lib/skyfall/stream.rb#180
def build_websocket_url; end
# source://skyfall//lib/skyfall/stream.rb#168
def reconnect_delay; end
end
# source://skyfall//lib/skyfall/stream.rb#9
Skyfall::Stream::EVENTS = T.let(T.unsafe(nil), Array)
# source://skyfall//lib/skyfall/stream.rb#10
Skyfall::Stream::MAX_RECONNECT_INTERVAL = T.let(T.unsafe(nil), Integer)
# source://skyfall//lib/skyfall/errors.rb#8
class Skyfall::SubscriptionError < ::StandardError
# @return [SubscriptionError] a new instance of SubscriptionError
#
# source://skyfall//lib/skyfall/errors.rb#11
def initialize(error_type, error_message = T.unsafe(nil)); end
# Returns the value of attribute error_message.
#
# source://skyfall//lib/skyfall/errors.rb#9
def error_message; end
# Returns the value of attribute error_type.
#
# source://skyfall//lib/skyfall/errors.rb#9
def error_type; end
end
# source://skyfall//lib/skyfall/errors.rb#5
class Skyfall::UnsupportedError < ::StandardError; end
# source://skyfall//lib/skyfall/version.rb#4
Skyfall::VERSION = T.let(T.unsafe(nil), String)

View File

@@ -31,19 +31,19 @@ class WebSocket::Driver
# source://websocket-driver//lib/websocket/driver.rb#72
def initialize(socket, options = T.unsafe(nil)); end
# source://websocket-driver//lib/websocket/driver.rb#90
# source://websocket-driver//lib/websocket/driver.rb#92
def add_extension(extension); end
# source://websocket-driver//lib/websocket/driver.rb#123
# source://websocket-driver//lib/websocket/driver.rb#125
def binary(message); end
# source://websocket-driver//lib/websocket/driver.rb#135
# source://websocket-driver//lib/websocket/driver.rb#137
def close(reason = T.unsafe(nil), code = T.unsafe(nil)); end
# source://websocket-driver//lib/websocket/driver.rb#127
# source://websocket-driver//lib/websocket/driver.rb#129
def ping(*args); end
# source://websocket-driver//lib/websocket/driver.rb#131
# source://websocket-driver//lib/websocket/driver.rb#133
def pong(*args); end
# Returns the value of attribute protocol.
@@ -56,54 +56,54 @@ class WebSocket::Driver
# source://websocket-driver//lib/websocket/driver.rb#70
def ready_state; end
# source://websocket-driver//lib/websocket/driver.rb#94
# source://websocket-driver//lib/websocket/driver.rb#96
def set_header(name, value); end
# source://websocket-driver//lib/websocket/driver.rb#100
# source://websocket-driver//lib/websocket/driver.rb#102
def start; end
# source://websocket-driver//lib/websocket/driver.rb#85
# source://websocket-driver//lib/websocket/driver.rb#87
def state; end
# source://websocket-driver//lib/websocket/driver.rb#118
# source://websocket-driver//lib/websocket/driver.rb#120
def text(message); end
private
# source://websocket-driver//lib/websocket/driver.rb#156
# source://websocket-driver//lib/websocket/driver.rb#158
def fail(type, message); end
# source://websocket-driver//lib/websocket/driver.rb#144
# source://websocket-driver//lib/websocket/driver.rb#146
def fail_handshake(error); end
# source://websocket-driver//lib/websocket/driver.rb#162
# source://websocket-driver//lib/websocket/driver.rb#164
def open; end
# source://websocket-driver//lib/websocket/driver.rb#169
# source://websocket-driver//lib/websocket/driver.rb#171
def queue(message); end
class << self
# source://websocket-driver//lib/websocket/driver.rb#174
# source://websocket-driver//lib/websocket/driver.rb#176
def client(socket, options = T.unsafe(nil)); end
# source://websocket-driver//lib/websocket/driver.rb#198
# source://websocket-driver//lib/websocket/driver.rb#200
def encode(data, encoding = T.unsafe(nil)); end
# source://websocket-driver//lib/websocket/driver.rb#213
# source://websocket-driver//lib/websocket/driver.rb#216
def host_header(uri); end
# source://websocket-driver//lib/websocket/driver.rb#182
# source://websocket-driver//lib/websocket/driver.rb#184
def rack(socket, options = T.unsafe(nil)); end
# source://websocket-driver//lib/websocket/driver.rb#178
# source://websocket-driver//lib/websocket/driver.rb#180
def server(socket, options = T.unsafe(nil)); end
# source://websocket-driver//lib/websocket/driver.rb#221
# source://websocket-driver//lib/websocket/driver.rb#224
def validate_options(options, valid_keys); end
# @return [Boolean]
#
# source://websocket-driver//lib/websocket/driver.rb#229
# source://websocket-driver//lib/websocket/driver.rb#238
def websocket?(env); end
end
end
@@ -363,34 +363,34 @@ class WebSocket::Driver::Hybi < ::WebSocket::Driver
private
# source://websocket-driver//lib/websocket/driver/hybi.rb#336
# source://websocket-driver//lib/websocket/driver/hybi.rb#338
def check_frame_length; end
# source://websocket-driver//lib/websocket/driver/hybi.rb#347
# source://websocket-driver//lib/websocket/driver/hybi.rb#349
def emit_frame(buffer); end
# source://websocket-driver//lib/websocket/driver/hybi.rb#395
# source://websocket-driver//lib/websocket/driver/hybi.rb#397
def emit_message; end
# source://websocket-driver//lib/websocket/driver/hybi.rb#270
# source://websocket-driver//lib/websocket/driver/hybi.rb#272
def fail(type, message); end
# source://websocket-driver//lib/websocket/driver/hybi.rb#232
# source://websocket-driver//lib/websocket/driver/hybi.rb#234
def handshake_response; end
# source://websocket-driver//lib/websocket/driver/hybi.rb#325
# source://websocket-driver//lib/websocket/driver/hybi.rb#327
def parse_extended_length(buffer); end
# source://websocket-driver//lib/websocket/driver/hybi.rb#308
# source://websocket-driver//lib/websocket/driver/hybi.rb#310
def parse_length(octet); end
# source://websocket-driver//lib/websocket/driver/hybi.rb#275
# source://websocket-driver//lib/websocket/driver/hybi.rb#277
def parse_opcode(octet); end
# source://websocket-driver//lib/websocket/driver/hybi.rb#196
# source://websocket-driver//lib/websocket/driver/hybi.rb#198
def send_frame(frame); end
# source://websocket-driver//lib/websocket/driver/hybi.rb#258
# source://websocket-driver//lib/websocket/driver/hybi.rb#260
def shutdown(code, reason, error = T.unsafe(nil)); end
class << self

View File

@@ -0,0 +1,13 @@
# typed: strict
class DIDKit::Resolver
sig { params(handle: String).returns(T.nilable(DIDKit::DID)) }
def resolve_handle(handle)
end
end
class DIDKit::DID
sig { returns(String) }
def did
end
end

View File

@@ -0,0 +1,65 @@
# typed: strict
class Skyfall::Jetstream
sig do
params(
block: T.proc.params(message: ::Skyfall::Jetstream::Message).void,
).void
end
def on_message(&block)
end
end
class Skyfall::Jetstream::Message
sig { returns(Integer) }
def seq
end
sig { returns(Symbol) }
def type
end
sig { returns(Time) }
def time
end
end
class Skyfall::Jetstream::CommitMessage
sig { returns(T::Array[Skyfall::Jetstream::Operation]) }
def operations
end
end
class Skyfall::Jetstream::Operation
sig { returns(Symbol) }
def action
end
sig { returns(Symbol) }
def type
end
sig { returns(String) }
def repo
end
sig { returns(String) }
def collection
end
sig { returns(String) }
def rkey
end
sig { returns(String) }
def path
end
sig { returns(String) }
def uri
end
sig { returns(Skyfall::CID) }
def cid
end
end

View File

@@ -2,8 +2,6 @@
require "rails_helper"
RSpec.describe Domain::PostsController, type: :controller do
render_views
# Create a real user with admin role
let(:user) { create(:user, :admin) }
@@ -75,6 +73,11 @@ RSpec.describe Domain::PostsController, type: :controller do
it_behaves_like "a post"
end
context "with a bluesky post" do
let(:post) { create(:domain_post_bluesky_post) }
it_behaves_like "a post"
end
context "when post file is pending download" do
let(:post) do
create(

View File

@@ -0,0 +1,15 @@
# typed: false
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(:at_uri) do |n|
"at://did:plc:#{n.to_s.rjust(10, "0")}/app.bsky.feed.post/rkey#{n}"
end
bluesky_created_at { Time.now }
state { "ok" }
text { "Hello from Bluesky" }
hashtags { %w[test bluesky] }
like_count { 0 }
end
end

View File

@@ -0,0 +1,8 @@
# typed: false
FactoryBot.define do
factory :domain_user_bluesky_user, class: "Domain::User::BlueskyUser" do
sequence(:handle) { |n| "user#{n}.bsky.social" }
sequence(:did) { |n| "did:plc:#{n.to_s.rjust(10, "0")}" }
state { "ok" }
end
end

View 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

View 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