Ruby から Bluesky に投稿してみる

Ruby から Bluesky に投稿してみようとあれこれやってみたのでメモ。

Gem

Bluesky のプロトコルについて何もわかってないので Gem を探す。

bluesky という gem が見つかったけど、これは違うやつっぽい。 bskyrb というのがあったので使ってみる。 atproto というのもあったけどこれは調べてない。

% gem install bskyrb
Fetching bskyrb-0.5.3.gem
Fetching httparty-0.21.0.gem
Fetching xrpc-0.0.4.gem
When you HTTParty, you must party hard!
Successfully installed httparty-0.21.0
Successfully installed xrpc-0.0.4
Successfully installed bskyrb-0.5.3
Parsing documentation for httparty-0.21.0
Installing ri documentation for httparty-0.21.0
Parsing documentation for xrpc-0.0.4
unknown encoding name "'application/json'," for lib/xrpc/client.rb, skipping
Installing ri documentation for xrpc-0.0.4
Parsing documentation for bskyrb-0.5.3
Installing ri documentation for bskyrb-0.5.3
Done installing documentation for httparty, xrpc, bskyrb after 0 seconds
3 gems installed

Bskyrb

README に書かれてる通りだけどこんな感じで使う。

require 'bskyrb'
username = 'tmtms.bsky.social'
password = 'xxxxxxxxxxxxxxxxx'
pds_url = 'https://bsky.social'

credentials = Bskyrb::Credentials.new(username, password)
session = Bskyrb::Session.new(credentials, pds_url)
bsky = Bskyrb::RecordManager.new(session)

Post(投稿)

bsky.create_post('API からの投稿テスト')
#=>
# {"uri"=>
#   "at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyy7fmciva2w",
#  "cid"=>"bafyreifpmd2ovx5cmytpnje35pgzr7cdkacaksxeh42nfdlwxaotehtpve"}

戻り値の uriat://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyy7fmciva2w は、https://staging.bsky.app/profile/tmtms.bsky.social/post/3jyy7fmciva2w に対応してるっぽい。よくわかってないけど。

投稿の取得

Post の URI かブラウザで見るときの URL を指定する。

bsky.get_post_by_url('at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyy7fmciva2w')
または
bsky.get_post_by_url('https://staging.bsky.app/profile/tmtms.bsky.social/post/3jyy7fmciva2w')
#=>
# #<Bskyrb::AppBskyFeedDefs::PostView:0x00007f3ffb2c8660
#  @author=
#   {"did"=>"did:plc:vgflzemtmzcqr3eyrtt4wh7c",
#    "handle"=>"tmtms.bsky.social",
#    "displayName"=>"tmtms / とみたまさひろ",
#    "avatar"=>
#     "https://cdn.bsky.social/imgproxy/JpuYoHvseSZTA4-XJl6NOTJWMrBDO46GFNfNLxAJ8aM/rs:fill:1000:1000:1:0/plain/bafkreiho7263tsofusirvy2soncqqiewwd5747qfh7655t2ydfitou37ni@jpeg",
#    "viewer"=>{"muted"=>false, "blockedBy"=>false},
#    "labels"=>[]},
#  @cid="bafyreifpmd2ovx5cmytpnje35pgzr7cdkacaksxeh42nfdlwxaotehtpve",
#  @embed=nil,
#  @indexedAt="2023-06-25T10:03:06.127Z",
#  @labels=[],
#  @likeCount=2,
#  @record=
#   {"text"=>"API からの投稿テスト",
#    "$type"=>"app.bsky.feed.post",
#    "createdAt"=>"2023-06-25T19:03:04.236+09:00"},
#  @replyCount=0,
#  @repostCount=1,
#  @uri="at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyy7fmciva2w",
#  @viewer=
#   {"repost"=>
#     "at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.repost/3jyya4rb26n2m",
#    "like"=>
#     "at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.like/3jyyaaxqcbv2m"}>

Like(いいね)

投稿の URI を指定して like

bsky.like('at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyy7fmciva2w')
#=>
# {"uri"=>
#   "at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.like/3jyyaaxqcbv2m",
#  "cid"=>"bafyreicq3afwv4ayry7bu3plcfvmhjr5xwpkqneztsqr6y2bs5jt5fxjpq"}

Repost(再投稿)

Twitter でいうところのリツイート。

bsky.repost('at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyy7fmciva2w')
#=>
# {"uri"=>
#   "at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.repost/3jyya4rb26n2m",
#  "cid"=>"bafyreihiahaprk52am5ukogenkz7xtw3npi2cu7oqwszkjudu2wxmtzi74"}

テキスト中の URL

こんな風にテキスト中に URL 文字列を含めてもリンクにはならない。

bsky.create_post("テスト https://tmtms.net")
#=>
# {"uri"=>
#   "at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyyanhmjry2x",
#  "cid"=>"bafyreib24x7xh3kbjyck2d67fj7qie2w7xdxxjihl64vaf6oyauagpr6dy"}

リンクになってる投稿を取得して見てみる。

bsy.get_post_by_url('https://staging.bsky.app/profile/tmtms.bsky.social/post/3jyybr2bcdl2a')
#=>
# #<Bskyrb::AppBskyFeedDefs::PostView:0x00007f3ffb2615f0
#  @author=
#   {"did"=>"did:plc:vgflzemtmzcqr3eyrtt4wh7c",
#    "handle"=>"tmtms.bsky.social",
#    "displayName"=>"tmtms / とみたまさひろ",
#    "avatar"=>
#     "https://cdn.bsky.social/imgproxy/JpuYoHvseSZTA4-XJl6NOTJWMrBDO46GFNfNLxAJ8aM/rs:fill:1000:1000:1:0/plain/bafkreiho7263tsofusirvy2soncqqiewwd5747qfh7655t2ydfitou37ni@jpeg",
#    "viewer"=>{"muted"=>false, "blockedBy"=>false},
#    "labels"=>[]},
#  @cid="bafyreig4rfs7xdlpzq7ewjzn5itvosyccifz3sj2hhowj6mop46e7xflzq",
#  @embed=nil,
#  @indexedAt="2023-06-25T10:45:17.362Z",
#  @labels=[],
#  @likeCount=0,
#  @record=
#   {"text"=>"リンクのテスト https://tmtms.net",
#    "$type"=>"app.bsky.feed.post",
#    "facets"=>
#     [{"index"=>{"byteEnd"=>39, "byteStart"=>22},
#       "features"=>
#        [{"uri"=>"https://tmtms.net",
#          "$type"=>"app.bsky.richtext.facet#link"}]}],
#    "createdAt"=>"2023-06-25T10:45:17.066Z"},
#  @replyCount=0,
#  @repostCount=0,
#  @uri="at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyybr2bcdl2a",
#  @viewer={}>

どうやら、テキストの何バイト目から何バイト目までがリンクで URI みたいな指定がされてるぽい。投稿時に同じような指定をすればよさそう。

bskyrb のコードを読んでみたら post の代わりに create_record を使えば JSON で色々指定できるぽい。

data = {
  'collection' => 'app.bsky.feed.post',
  'repo' => session.did,
  'record' => {
    '$type' => 'app.bsky.feed.post',
    'createdAt' => Time.now.iso8601(3),
    'text' => 'ホームページはこちら',
    'facets' => [
      {
        'index' => {'byteStart' => 21, 'byteEnd' => 30},
        'features' => [
          {
            'uri' => 'https://tmtms.net/',
            '$type' => 'app.bsky.richtext.facet#link',
          },
        ],
      },
    ],
  },
}
bsky.create_record(data)
#=>
# {"uri"=>
#   "at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyycpsqjuc2b",
#  "cid"=>"bafyreidsaocpktig5jxud45x3jhdtaq2755cmsoibkh4oxc2yyjeqvummi"}

「こちら」の部分がリンクになった!

リンクカード

アプリやブラウザから投稿するときに本文中に URL が含まれていると「Add link card」という表示が出て、そこをクリックすると og:image や og:title などがカード形式で表示される。 Twitter は特に何もしなくても表示されたんだけど、Bluesky は今のところ(?)「Add link card」を押さないとカードがつかないまま。

API の投稿で同じことをやるには次のような感じでやればいいっぽい。

まずサムネイル画像をアップロードする。

HTTParty.post(
  bsky.upload_blob_uri(session.pds),
  body: File.read('tengu.jpg'),
  headers: {
    "Content-Type" => 'image/jpeg',
    "Authorization" => "Bearer #{session.access_token}",
  }
)
#=>
# {"blob"=>
#   {"$type"=>"blob",
#    "ref"=>
#     {"$link"=>"bafkreifwykwd2waeshgjze5rccphfsoqfjykjv2fa6psbxc65ym2nf6id4"},
#    "mimeType"=>"image/jpeg",
#    "size"=>117643}}

Bskyrb は、bsky.upload_blob('tengu.jpg', 'image/jpeg') で上と同じことができるようにしてるっぽいんだけど、Content-Type の指定が無視されてしまうバグがあって使えなかった。そのうち直るんじゃないかな。たぶん。

この戻り値の blob を投稿時に embedthumb に指定する。

data = {
  'collection' => 'app.bsky.feed.post',
  '$type' => 'app.bsky.feed.post',
  'repo' => session.did,
  'record' => {
    '$type' => 'app.bsky.feed.post',
    'createdAt' => Time.now.iso8601(3),
    'text' => 'ブログへのリンク。画像と説明はダミー。',
    'embed' => {
      '$type' => 'app.bsky.embed.external',
      'external' => {
        'uri' => 'https://blog.tmtms.net/',
        'title' => 'tmtms のメモ',
        'description' => '天狗はブログの内容とは関係ないよ',
        'thumb' => {
          '$type' => 'blob',
          'mimeType'=>'image/jpeg',
          'size' => 117643,
          'ref' => {
            '$link' => 'bafkreifwykwd2waeshgjze5rccphfsoqfjykjv2fa6psbxc65ym2nf6id4',
          },
        },
      },
    },
  },
}
bsky.create_record(data)
#=>
# {"uri"=>
#   "at://did:plc:vgflzemtmzcqr3eyrtt4wh7c/app.bsky.feed.post/3jyyli7jvjr2y",
#  "cid"=>"bafyreiao4a7zpyqddspkv3nciiq7xhzhsjv6snmicgyp73plagj4pvmply"}

できた! 🎉🎉🎉


プロトコルのことを何もわかってないままだけど、一応ひと通りの投稿はできるようになった。

しかし、もともと Twitter の自分の投稿を拾って Bluesky に投げたいと思って調べてたんだけど、Twitter の API が使えなくなってしまったのでどうしたもんかなー…。

この続きはcodocで購入