Skip to content

Instantly share code, notes, and snippets.

@searls
Created January 27, 2025 21:18
Show Gist options
  • Save searls/b945fef7a32414ce9654ce10222f761b to your computer and use it in GitHub Desktop.
Save searls/b945fef7a32414ce9654ce10222f761b to your computer and use it in GitHub Desktop.
Example code for working with the bluesky API and the bskyrb gem. Copyright Searls LLC, 2025. All rights reserved. Not open source, only made available for your viewing pleasure. For a license to use this code, e-mail [email protected]
class Platforms::Bsky::AssemblesRichTextFacets
# Name of the game here is to take something output by IdentifiesPatternRanges
# (which exists to identify unbreakable tokens) and use the embedded metadata
# about which of these things are hashtags and links to construct the rich text
# facet hash that bluesky wants in order to render links and hashtags
def assemble(pattern_ranges)
pattern_ranges.map { |range|
if range[:type] == :tag
facet_for_tag(range)
elsif range[:type] == :link
facet_for_link(range)
end
}
end
private
# Per the docs, bsky's byte ranges are inclusive on both ends
# https://docs.bsky.app/docs/advanced-guides/post-richtext
def bsky_byte_range_for(range)
{
"byteStart" => range[:byte_index],
"byteEnd" => range[:byte_index] + range[:byte_length] + 1
}
end
def facet_for_tag(range)
{
"$type" => "app.bsky.richtext.facet",
"index" => bsky_byte_range_for(range),
"features" => [
{
"tag" => range[:substring].delete_prefix("#"),
"$type" => "app.bsky.richtext.facet#tag"
}
]
}
end
def facet_for_link(range)
url = UrlUtils.ensure_protocol(range[:substring])
{
"$type" => "app.bsky.richtext.facet",
"index" => bsky_byte_range_for(range),
"features" => [
{
"uri" => url,
"$type" => "app.bsky.richtext.facet#link"
}
]
}
end
end
require "net/http"
require "mime/types"
class Platforms::Bsky::AttachesWebCard
def attach!(crosspost_config, record_manager)
image_data, content_type = download_image(crosspost_config[:og_image])
raise "Failed to download og_image: #{crosspost_config[:og_image]}" unless image_data
# Dropping down to do this ourselves b/c the bsky gem assumes you're reading the image from a file
#
upload_response = HTTParty.post(
record_manager.upload_blob_uri(record_manager.session.pds),
body: image_data,
headers: record_manager.default_authenticated_headers(record_manager.session).merge("Content-Type" => content_type)
)
raise "Failed to upload og_image: #{crosspost_config[:og_image]} to Bsky. Response: #{TODO}" unless upload_response.success?
{
"$type" => "app.bsky.embed.external",
"external" => {
"uri" => crosspost_config[:url],
"title" => crosspost_config[:og_title].presence || crosspost_config[:title],
"description" => crosspost_config[:og_description].presence || crosspost_config[:title],
"thumb" => upload_response["blob"]
}
}
end
private
def download_image(image_url)
uri = URI.parse(image_url)
response = Net::HTTP.get_response(uri)
return unless response.is_a?(Net::HTTPSuccess)
content_type = response.content_type || MIME::Types.type_for(uri.path).first&.content_type
[response.body, content_type]
end
end
class Platforms::Bsky
class SyndicatesBskyPost
PDS_URL = "https://bsky.social"
def initialize
@attaches_web_card = AttachesWebCard.new
end
def syndicate!(crosspost, crosspost_config, crosspost_content, rich_text_facets)
session = Bskyrb::Session.new(
Bskyrb::Credentials.new(
crosspost.account.credentials["email"],
crosspost.account.credentials["password"]
), PDS_URL
)
uri = post!(crosspost_config, crosspost_content, rich_text_facets, session)
if uri.nil?
PublishesCrosspost::Result.new(success?: false, message: "Failed to create Bsky post")
else
url = uri_to_url(uri, session)
crosspost.update!(
remote_id: uri,
url: url,
content: crosspost_content,
status: "published",
published_at: Now.time
)
PublishesCrosspost::Result.new(success?: true)
end
rescue Bskyrb::UnauthorizedError => e
PublishesCrosspost::Result.new(success?: false, message: "Failed to authenticate to Bsky (invalid credentials)", error: e)
rescue => e
PublishesCrosspost::Result.new(success?: false, message: "Failed to syndicate to Bsky", error: e)
end
private
def post!(crosspost_config, crosspost_content, rich_text_facets, session)
record_manager = Bskyrb::RecordManager.new(session)
embed = @attaches_web_card.attach!(crosspost_config, record_manager) if crosspost_config[:attach_og_card]
result = record_manager.create_record({
"collection" => "app.bsky.feed.post",
"$type" => "app.bsky.feed.post",
"repo" => session.did,
"record" => {
"$type" => "app.bsky.feed.post",
"createdAt" => Now.time.iso8601(3),
"text" => crosspost_content,
"facets" => rich_text_facets,
"embed" => embed
}
}.compact)
result&.dig("uri")
end
def uri_to_url(uri, session)
bsky_handle = DIDKit::Resolver.new.get_validated_handle(session.did)
post_id = uri[/[^\/]+$/]
"https://bsky.app/profile/#{bsky_handle}/post/#{post_id}"
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment