2023年4月初旬、日本で盛り上がりの兆しを見せ始めた分散型SNS
なお、Bluesky誕生の背景や基本機能などについては、syui氏の記事
Bluesky をコマンドラインから扱う
Blueskyクライアント開発のきっかけ
Blueskyにはすでにクライアントが存在します。BlueskyのオフィシャルからはiOSアプリが、また Android向けにはkamakuraさんが開発している青雲というアプリが提供されています。
通常はこれらのクライアントから投稿をすることでBlueskyを使えます。
筆者がBlueskyのアカウントを作成したのはまだiOSアプリしか存在しないころでした。またWebサイトからもログインすることはできませんでした。
筆者はAndroidユーザですのでBlueskyを体験することができません。しかし筆者はどうしてもBlueskyを体験したくなり、Blueskyプロトコルを調べることにしました。するとBlueskyチームがBlueskyを扱うためのライブラリをいくつか公開しているのを見つけました。
- https://
github. com/ bluesky-social/ atproto - https://
github. com/ bluesky-social/ indigo
atprotoはTypeScriptで、またindigoはGo言語で書かれたBlueskyとAT Protocolを扱うためのライブラリです。筆者はGo言語をメインで使っているのでindigoを使ってCLIアプリケーション
AT Protocolとは
AT Protocolは複数のプロトコルの集合の総称です。
Personal Data Server
Blueskyクライアントが実行するデータ操作は、自分が所属するPDSのサーバに対して行います。
APIは名前空間で分けられた型と操作が定義されており、JSONを扱うXRPCと呼ばれるRPC
Lexiconとは
サンプルを紹介します。次の関数を見てください。
名前空間com.
にある関数getProfile
は引数にオブジェクトを取り、そのオブジェクトはuserをキーに、そして文字列の値を持つとします。さらに戻り値はname、did、displayNameなどのキーを持ち、それぞれ文字列の値を持つとします。
await client.com.example.getProfile({user: 'bob.com'})
// => {name: 'bob.com', did: 'did:plc:1234', displayName: '...', ...}
このgetProfileの定義は次のようになります。
{
"lexicon": 1,
"id": "com.example.getProfile",
"type": "query",
"parameters": {
"user": {"type": "string", "required": true}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["did", "name"],
"properties": {
"did": {"type": "string"},
"name": {"type": "string"},
"displayName": {"type": "string", "maxLength": 64},
"description": {"type": "string", "maxLength": 256}
}
}
}
}
この例では、JavaScriptのコードを示しましたが、すべてのクライアントライブラリはLexiconから自動生成されるため、ほぼ同じインターフェースを提供しています。前述したindigoもほぼ同じ名前のインターフェースでアクセスすることができます。
たとえば、Blueskyでユーザのプロフィール情報を得るAPI app.
のLexiconを確認しましょう。
{
"lexicon": 1,
"id": "app.bsky.actor.getProfiles",
"defs": {
"main": {
"type": "query",
"parameters": {
"type": "params",
"required": ["actors"],
"properties": {
"actors": {
"type": "array",
"items": {"type": "string", "format": "at-identifier"},
"maxLength": 25
}
}
},
"output": {
"encoding": "application/json",
"schema": {
"type": "object",
"required": ["profiles"],
"properties": {
"profiles": {
"type": "array",
"items": {"type": "ref", "ref": "app.bsky.actor.defs#profileViewDetailed"}
}
}
}
}
}
}
}
TypeScriptのインターフェースは次のようになっています。
export default function (server: Server, ctx: AppContext) {
server.app.bsky.actor.getProfile({
auth: authOptionalVerifier,
handler: async ({ auth, params }) => {
const { actor } = params
const requester = auth.credentials.did
const { db, services } = ctx
const actorService = services.actor(db)
const actorRes = await actorService.getActor(actor, true)
if (!actorRes) {
throw new InvalidRequestError('Profile not found')
}
if (softDeleted(actorRes)) {
throw new InvalidRequestError(
'Account has been taken down',
'AccountTakedown',
)
}
return {
encoding: 'application/json',
body: await actorService.views.profileDetailed(actorRes, requester),
}
},
})
}
またGo言語のインターフェースは次のようになっています。
func ActorGetProfile(ctx context.Context, c *xrpc.Client, actor string) (*ActorDefs_ProfileViewDetailed, error) {
var out ActorDefs_ProfileViewDetailed
params := map[string]interface{}{
"actor": actor,
}
if err := c.Do(ctx, xrpc.Query, "", "app.bsky.actor.getProfile", params, nil, &out); err != nil {
return nil, err
}
return &out, nil
}
このように、Lexiconさえ理解できていれば、言語が違っていてもおおよそ同じインターフェースを扱うことができるのが特徴です。
APIを呼び出してみる
実際にGo言語からユーザのプロフィール情報を得るコードを実装してみましょう。まずはクライアントを作ります。Blueskyのクライアントは、JWTを使って認証を行います。
セッションを作るにはcom.
を呼び出します。indigoにはこれをラップしたcomatproto.
が用意されています[1]。
xrpcc := &xrpc.Client{
Client: cliutil.NewHttpClient(),
Host: cfg.Host,
Auth: &xrpc.AuthInfo{Handle: cfg.Handle},
}
auth, err := comatproto.ServerCreateSession(context.TODO(), xrpcc, &comatproto.ServerCreateSession_Input{
Identifier: &xrpcc.Auth.Handle,
Password: cfg.Password,
})
if err != nil {
return nil, fmt.Errorf("cannot create session: %w", err)
}
xrpcc.Auth.Did = auth.Did
xrpcc.Auth.AccessJwt = auth.AccessJwt
xrpcc.Auth.RefreshJwt = auth.RefreshJwt
このauthはセッション作成後、しばらくは再利用が可能なため、筆者が開発したbskyではJSONファイルとして保存しており、次回起動時に再利用しています。またセッションが揮発してしまわないように、次回クライアントを作成する際にはリフレッシュする必要があります。
xrpcc := &xrpc.Client{
Client: cliutil.NewHttpClient(),
Host: cfg.Host,
Auth: &xrpc.AuthInfo{Handle: cfg.Handle},
Auth: auth, // JSON から読み込んだ auth
}
refresh, err := comatproto.ServerRefreshSession(context.TODO(), xrpcc)
if err != nil {
return nil, fmt.Errorf("cannot refresh session: %w", err)
}
xrpcc.Auth.Did = refresh.Did
xrpcc.Auth.AccessJwt = refresh.AccessJwt
xrpcc.Auth.RefreshJwt = refresh.RefreshJwt
残念ながら、現在のAT Protocolにはパスワードを用いた認証方法しか用意されていないため、破棄可能なAPIトークンや、二段階認証といった認証方式は扱えません。こちらは今後に期待したいと思います。では実際にgetProfile
を呼び出します。
profile, err := bsky.ActorGetProfile(context.TODO(), xrpcc, "mattn.bsky.social")
if err != nil {
return fmt.Errorf("cannot get profile: %w", err)
}
fmt.Printf("Did: %s\n", profile.Did)
fmt.Printf("Handle: %s\n", profile.Handle)
fmt.Printf("DisplayName: %s\n", stringp(profile.DisplayName))
fmt.Printf("Description: %s\n", stringp(profile.Description))
fmt.Printf("Follows: %d\n", int64p(profile.FollowsCount))
fmt.Printf("Followers: %d\n", int64p(profile.FollowersCount))
実行すると次のように出力されます。
Did: did:plc:ituhatvv5pyz4rwsj4hfrslh
Handle: mattn.bsky.social
DisplayName: mattn
Description: Long-time #Golang user&contributor, #GoogleDevExpert Go, (略)
Follows: 291
Followers: 316
このdidは、Blueskyの中で筆者のアカウントを識別するIDであり、万が一、筆者が bsky.
Blueskyではユーザを指定する際にはmattn.
といったドメイン形式ではなく、did形式did:plc:ituhatvv5pyz4rwsj4hfrslh
)
bsky
このLexiconから生成されたインターフェースを呼び出すことで、Blueskyを操作できるようにと、実装したコマンドラインクライアントがbskyです。
前述のとおり、筆者はAndroidユーザですのでBlueskyにログインすることはできませんでした。しかし、iOSを使う周りの皆が楽しそうに見えてしまい、どうしてもBlueskyをやりたくなったため、bskyを開発しました。
bskyでできるおもなことを紹介します。
- プロフィールの表示、更新
- タイムラインの表示
- スレッドの表示
- 発言の投稿、削除
- 発言にいいねを付ける/発言にいいねを付けたユーザを見る
- 発言をリポストする/発言をリポストしたユーザを見る
- ユーザをフォローする
- フォローしたユーザを見る
- フォロワーを見る
- 通知を見る
もちろん筆者はログインできないため、これらは自分だけでテストをすることはできませんでした。画像のアップロードが正しくできたかどうかは、Blueskyにいる他のユーザの皆さんに
当然ながら、ありがとうの意味の
bskyは現状Blueskyでできるほとんどの機能をコマンドラインから実行することができます。また、表示を行うコマンドには-json
オプションを付与することでJSONを出力することができます。
よってjqなどのツールと組み合わせれば、シェルスクリプトから呼び出すことが可能です。
応用例①
たとえば、Blueskyでフォローされたユーザを一括でフォロー返ししたいとしましょう。フォロー返しするのに画面からポチポチするのは面倒ですね。bskyを使えば以下のコマンドで実行できます。
#!/bin/bash
set -e
/usr/bin/diff \
<(bsky followers -json | sort) \
<(bsky follows -json | sort) | \
/usr/bin/grep '^>' | /usr/bin/sed 's/^> //' | \
/usr/bin/jq -r -s '.[].did' |\
xargs bsky follow
このスクリプトは、bashのプロセス置換という機能を使うことで、bskyコマンドのJSON出力結果を2つ得て、あたかもファイルのようにdiffコマンドへ渡すものです。
bskyのJSON出力は、NDJSON
diffコマンドはフォロワーとフォローの差分を調べ、増えた分を>
の接頭文字を付けて出力します。これをgrepで絞り込み、さらに加工した後でjqに渡しています。
jqの結果は、フォローしていないユーザのdid一覧になります。あとはxargsコマンドを使い、bsky followに渡しています。
応用例②
もう1つ応用例をご紹介します。次の例はソーシャルニュースサイトRedditの投稿をBlueskyに転送するスクリプトです。
#!/bin/sh
set -e
URL=https://www.reddit.com/r/golang.json
/usr/bin/curl -s -H "User-Agent: Chrome" $URL | \
/usr/bin/jq -c '.data.children[].data' | \
dedup -k id | \
jsonargs -f bsky post "{{.title}} #golang_news" "{{.url}}"
curlでJSONをダウンロードし、jqで1行ごとの情報に整形します。
dedupコマンドは標準入力から1行ごとのJSONを受け取り、そのまま標準出力します。ただし入力のJSONのidというキーに重複があれば、そのJSONを出力から省くようになっています。
jsonargsコマンドは標準入力から1行ごとのJSONを受け取り、引数にてテンプレート形式で指定し値を得られるようにするコマンドです。言ってみればxargsのJSON版と言えるでしょう。
このスクリプトを実行すると、Redditの新着記事がBlueskyに投稿されるようになります。あとはこのスクリプトをcronを使って定期的に実行するようにしておけば、ニュース投稿Botができあがります。
- https://
github. com/ mattn/ dedup - https://
github. com/ mattn/ jsonargs
おわりに
Blueskyを構成するAT Protocolを実装面から確認し、実際にRPCを呼び出す例を示しました。また筆者が実装したCLIコマンドbskyを使って、簡単なスクリプトを実装する例を紹介しました。
bsky コマンドを使うことで、BlueskyのBotを簡単に実装できるようになります。何を実装するかはアイデア次第です。ぜひいろいろと応用して便利なものを作ってみてください。