作成しているツールでRubyからBlueskyへ投稿したかったので、ShreyanJain9/bskyrb: Ruby Gem for interacting with BlueSky/AT Protocolというライブラリを使うことにした。
使用例を見ると、
require 'bskyrb'
username = 'your_username'
password = 'your_password'
pds_url = 'https://bsky.social'
credentials = Bskyrb::Credentials.new(username, password)
session = Bskyrb::Session.new(credentials, pds_url)
bsky = Bskyrb::RecordManager.new(session)
post_uri = bsky.create_post("Hello world from bskyrb!")["uri"]
bsky.like(post_uri)
bsky.repost(post_uri)
bsky.create_reply(post_uri, "Replying to post from bskyrb")
とあり、一見簡単そうに見えるがBlueskyの場合はXやMastodonのAPIとは異なり、投稿するメッセージの中に、
- URLがあってもリンクは設定されない
- ハッシュタグがあってもリンクは設定されない
- URLのコンテンツにOGBタグがあってもリンクカードは表示されない
という仕様であり、なかなか面倒であることがわかった。
そんなわけで上記のドキュメントを参照しつつ、それぞれどうやれば良いのか調べてみた。
リンクの設定
メッセージ中にURLがあれば、その部分を自動的にリンク設定したかったため、Rich text facetsの例にある以下の例を参考に、
{
text: 'Go to this site',
facets: [
{
index: {
byteStart: 6,
byteEnd: 15
},
features: [{
$type: 'app.bsky.richtext.facet#link',
uri: 'https://example.com'
}]
}
]
}
以下のようなコードでfacetを作成した。
def create_facets_for_urls(text)
facets = []
text_copy = text.dup
URI.extract(text_copy, ['http', 'https']).each do |url|
byte_start = text_copy[0, text_copy.index(url)].encode('UTF-8').bytesize
byte_end = byte_start + url.bytesize
facets << {
'index' => {'byteStart' => byte_start, 'byteEnd' => byte_end},
'features' => [
{
'uri' => url,
'$type' => 'app.bsky.richtext.facet#link',
},
],
}
text_copy[text_copy.index(url), url.length] = "\0" * url.length
end
facets
end
ハッシュタグの設定
メッセージ中にハッシュタグがあれば、その部分を自動的にリンク設定したかったため、同じようなロジックでfacetを作成する。
# Creates facets for hashtags in the text
def create_facets_for_hashtags(text)
facets = []
text.scan(/#[\w\p{Han}ー]+/) do |hashtag|
byte_start = text[0, text.index(hashtag)].encode('UTF-8').bytesize
byte_end = byte_start + hashtag.bytesize
facets << {
'index' => {'byteStart' => byte_start, 'byteEnd' => byte_end},
'features' => [
{
'tag' => hashtag.gsub('#', ''),
'$type' => 'app.bsky.richtext.facet#tag',
},
],
}
end
facets
end
リンクカードの表示
メッセージ中のURLにあるコンテンツにOGPタグがあれば、リンクカードを表示するため以下の流れで対応した。
- URLにあるコンテンツにOGPタグがあるかを見に行く。無ければ以降の処理はスキップ
- og:title、og:descriptionなど処理に必要なタグ情報を取得する
- og:imageのサムネイル画像をダウンロードしBlueskyへアップロードする
- 3の戻り値を使いembedを作成する
3〜4のコードはこんなイメージ
# Handles the creation of embeds by uploading images and generating embed data
def create_embeds(ogp_data)
embeds = []
if ogp_data['og:image']
begin
Tempfile.create(['thumbnail', File.extname(URI.parse(ogp_data['og:image']).path)]) do |file|
file.binmode
file.write URI.open(ogp_data['og:image']).read
file.rewind
content_type = determine_content_type(file.path)
response = upload_image_to_bluesky(file, content_type)
if response && response.parsed_response
thumb_blob = response.parsed_response['blob']
embed = create_embed_data(ogp_data, thumb_blob)
embeds << embed
else
warn "Failed to upload image to Bluesky: Invalid response #{response.inspect}"
end
end
rescue StandardError => e
warn "Failed to process OGP image: #{e.message}"
end
end
embeds
end
# Creates the embed data structure for Bluesky post
def create_embed_data(ogp_data, thumb_blob)
{
'$type' => 'app.bsky.embed.external',
'external' => {
'uri' => ogp_data['og:url'],
'title' => ogp_data['og:title'],
'description' => ogp_data['og:description'],
'thumb' => thumb_blob
}
}
end