D言語のBlueskyクライアントライブラリを作った

# はじめに
Blueskyが一般に公開されて10か月たちました。
今世界中でも有数の勢いのあるSNSといって過言ではないBlueskyですが、なんといってもいいところは、昔Twitterがそうであったように、APIを無償で公開している点です。
TwitterはAPIを超高額設定にしてデベロッパーの手足をもぐようなことをして(有益有害問わず)Botなどを締め出しましたが、BlueskyはBotやクライアントアプリを自由に作ることができます。
いろいろな言語でクライアントライブラリが作られていますが、D言語はメジャーな言語ではありませんので、BlueskyのAPIにアクセスしようと思ったら自力でライブラリを作る必要があります。
……と、いうわけで、つくりました。

https://github.com/shoo/bsky

https://code.dlang.org/packages/bsky

今回の記事では、ライブラリの紹介(ちょっと)と、作ってみた感想やAPIの使い勝手なんか(メイン)をお話しします。


# 使い方
D言語のBlueskyクライアントライブラリ `bsky` の使い方は、以下。 

```d
import std.stdio, std.process, std.range;
import bsky;
auto client = new Bluesky;
client.login(environment.get("BSKY_LOGINID"), environment.get("BSKY_LOGINPASS"));
scope (exit)
	client.logout();
foreach (post; client.timeline.take(100).toMessages)
	writeln(i"$(post.postBy.displayName) < $(post.text)");
```

上記でやっているのは以下。

- クライアントのオブジェクト生成
- ログイン(サンプルではID, パスワードは環境変数から読み取っている)
- ログインしたユーザーのタイムラインを100件取得して、各ポストに対して以下のフォーマットで書き出し
  `(発言者名) < (ポスト内容のテキスト)`
- ログアウト

ちなみに、アプリを作る際ログインのためにパスワードを保存するのはリスクなので、アクセストークンやリフレッシュトークンの保存・復元・自動更新にも対応しています。

# 作ってみた感想
## 認証関連(ログイン・アクセストークン)
私は過去にもTwitterのAPIを使ったり、[Microsoft Graph](https://github.com/shoo/msgraph)を使ったことがありますが、それらとは違いoAuthではないため、ログイン方法がシンプルにID/パスワードです。
セキュリティ的には若干不安がありますが、初期検討にはらくちんでした。
一度ログインしたらあとはアクセストークンが取得でき、それをHTTPヘッダーのAuthorizationでBearer認証するあたりは Microsoft Graphと同じ感じでした。
ちなみにこの辺、oAuthの途中でHTTPサーバーを必要とするMicrosoft Graphがぶっちぎりで面倒でした。HTTPサーバー作るってことは、脆弱性の塊を作ることに等しいですからね……。
今後BlueskyもoAuth対応を視野に入れているとのことですが、面倒な感じにならないことを期待。


## APIの使い勝手
APIの使い勝手は微妙(趣深く、何ともいえない美しさや味わいがある、の意**ではない**)ですね。

例えば検索結果のポストデータを取得する場合 [`app.bsky.feed.searchPosts`](https://docs.bsky.app/docs/api/app-bsky-feed-search-posts) なんかを呼ぶわけですが、これで取得できるデータがなんとも冗長。
どうも、BlueskyのWebサイトで表示する内容が一発で取得できることを前提にデータ構造が作られているようで、そのポストに返信しているポストの内容まで(必要なくとも)引っ付いてくる。

一方で、 [`app.bsky.graph.getFollowers`](https://docs.bsky.app/docs/api/app-bsky-graph-get-followers) のAPIで取得できるフォロワー一覧では、いつフォローしてくれたのか、みたいな情報は取得できない。
……といった感じで、とあるAPIでは情報過剰な一方で、別のAPIでは情報が(やりたいことにもよりますが)足りてない、みたいなことがあるような感じです。

また、記事にいいね(お気に入り/Like)のマークつけるとか、リポストする、みたいな単純な操作に対して、専用のAPIがありません。
これらはユーザーごとに設けられたリポジトリという保存領域にそれ用のレコードを追加する、みたいな感じで行います。データベース関連履修してないと理解困難かもしれませんね。
お気に入りであれば、 [`com.atproto.repo.createRecord`](https://docs.bsky.app/docs/api/com-atproto-repo-create-record) というAPIで、ユーザーのリポジトリ(≒データベース)に、 [`app.bsky.feed.like`](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/feed/like.json) というコレクション(≒テーブル)の、 `subject` (≒カラム)に、お気に入りしたいポストのURI(ポストごとに振られたIDのようなもの)記載したレコードを作成することで行います。
このレコードを削除すると、お気に入りも解除されるという寸法。


## データ構造
前述のお気に入り追加のような仕組みは通常のポストや、リポストなんかも同じ仕組みのようです。
この仕組みが理解できないとAPIないじゃん……ってなり悩むことに。その割に、この仕組みを解説しているところがどこなのかよくわからない……。
全体的にドキュメントがプアな気がします。
ちなみに、私が主に使ってる情報源は以下。

https://docs.bsky.app/

https://github.com/bluesky-social/atproto/tree/main/lexicons

私の場合、例えばとあるユーザーをフォローしたい場合、以下のような手順で情報を探ります

1. [API一覧](https://docs.bsky.app/) にドンピシャのものがないか確認する
2. なければ [`com.atproto.repo.createRecord`](https://docs.bsky.app/docs/api/com-atproto-repo-create-record) でレコードの直接操作が必要なのだろうとあたりをつけて [Lexicons](https://github.com/bluesky-social/atproto/tree/main/lexicons) からそれっぽいのを探す。この場合 [`app.bsky.graph.follow`](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/follow.json) がそれっぽい。これがあてずっぽうなので、なんか解説してくれてるところとか、collection一覧みたいなの求む……。
3. [Lexiconsの各JSON](https://github.com/bluesky-social/atproto/blob/main/lexicons/app/bsky/graph/follow.json) からデータ構造確認して、`"required": ["subject", "createdAt"]` って書いてある `subject` フィールドにDID(ユーザーBluesky内でユニークなID ≠ ハンドル)を設定して `createdAt` に現在時刻設定すれば行けそうってところをあたりをつける。
4. 実験してみる

という感じにやってます。


## 画像/動画つきのポスト対応
黎明期のTwitterでは、画像付きのツイートなんてのはなくて、URLを含めたツイートを、クライアント側で検出して、ダウンロード・表示してた記憶があります。
今や画像投稿もできないSNSなんてありえませんので、Blueskyも標準でこのあたりに対応しています。最近動画にも対応しましたね。
この画像アップロードもちょっと曲者。1MBまでしか投稿できないし、サーバーで強制的にJPEGに変換されてメタデータは削除されるし、画質も荒くなる。
ロスレスでメタデータ残したPNG画像アップロードしたいんじゃい！！という人はどうすれば……？
もう少し、こう、何というか……手心というか……

画像や動画は [`com.atproto.repo.uploadBlob`](https://docs.bsky.app/docs/api/com-atproto-repo-upload-blob) でアップロードし、 [`com.atproto.repo.createRecord`](https://docs.bsky.app/docs/api/com-atproto-repo-create-record) でembed内に埋め込む、みたいなことをします。このあたりの手順については[公式に解説がある](https://docs.bsky.app/docs/advanced-guides/posts#images-embeds)。……のだけど、動画に関しては記事がない。動画も同じように扱うことができるようなのだけど、[`app.bsky.video.uploadVideo`](https://docs.bsky.app/docs/api/app-bsky-video-upload-video)みたいなAPIもあるみたいでちょっとよくわからない。

また、画像には alt という文字情報を追記することができます。目の不自由な人が画像の代わりに音声読み上げとかで情報を得たりするために使ったりします。
この alt ですが、BlueskyのWebアプリでは文字数制限があるようで、一定文字数以上の入力はできないようになっています。しかし、[Lexicons](https://github.com/bluesky-social/atproto/blob/c34426fc55e8b9f28d9b1d64eab081985d1b47b5/lexicons/app/bsky/embed/images.json#L26) を確認する限り、文字数制限が設けられていないっぽい。試しに私の作ったクライアントライブラリでたくさん書いて投稿してみたらやっぱり実際に投稿可能なようでした。Webアプリで表示すると、長すぎて表示が見切れてしまうかもしれませんが、少なくとも投稿の制限はなさそうでした。


## テスト
テスト方法については別途記事を書こうと思います。
この手のネットワーク関連のライブラリやアプリケーションを作るにあたって、テストをどうしたらいいかというのは悩みどころですよね。デバッグのため試しにやってみるという程度ならいくらでもやりようはあるのですが、CI/CDを考えて単体テストでやろうと思うと途端に難易度が上がる。
今回ライブラリを作るにあたって、単体テストでカバレッジ率80%以上を目指し、実際に実現することが出来たので、この話は別途したいと思います。


# 作ったライブラリを使ってやったこと
## インプレッション分析
自分で作成したアート作品や記事なんかを投稿したら、みんなに見てもらえたか気になりますよね？そういった数値をインプレッションとかいう指標で表したりしますが、この指標、伸びやすいのは何時でしょう？
何曜日に投稿するとインプレッションを稼ぎやすいでしょう？
Blueskyの場合表示数は取得できないため、いいね数やリポスト数を指標として使うのがいいでしょう。
こういう情報、個人的にお遊びでやってる分には適当でいいと思いますが、企業がマジでやる場合は大事なことかと思います。
……というわけで、それを調べるツールを作りました。
仕組みは、検索ワードで検索し、得られたポストに対していいね一覧を取得。いいねがつけられた時間とポストの時間で各種情報を標準出力に出すというもの。

サンプルに置いておくので試してみて下さい。

https://github.com/shoo/bsky/tree/main/examples/like_histgram

## 自分と趣味の近い人を見つける
その機能が[これ](https://docs.bsky.app/docs/api/app-bsky-feed-get-suggested-feeds)とか[これ](https://docs.bsky.app/docs/api/app-bsky-graph-get-suggested-follows-by-actor)なんじゃという話は置いておいて、自分と同じ趣味趣向のポストをお気に入り・リポストしているひとは、たぶん趣味が近いだろう、という想定の下、検索したポストをたくさんお気に入りしているユーザーをリストアップするツールを作りました。

仕組みは、検索をしてヒットしたポストにいいねやリポストをした人を特定して、いいねやリポストをたくさんしている人を上位にしてソートするというもの。ついでに宣伝垢除外のためにフォロー数/フォロワー数の比率とかポスト数とかで除外条件つけたりしてみるなど。

作る際に難しかったのは、誰がいついいねやリポストしたかという情報を得るには、

[検索(app.bsky.feed.searchPosts)](https://docs.bsky.app/docs/api/app-bsky-feed-search-posts)
　→ 各ポストに対して[いいね一覧(app.bsky.feed.getLikes)](https://docs.bsky.app/docs/api/app-bsky-feed-get-likes)
　→ 各ポストに対して [リポスト一覧(app.bsky.feed.getRepostedBy)](https://docs.bsky.app/docs/api/app-bsky-feed-get-reposted-by)
　　→ (時刻が得られないので)各リポストに対して [レコード一覧(com.atproto.repo.listRecords)](https://docs.bsky.app/docs/api/com-atproto-repo-list-records)
　→ 各ポストに対して [引用ポスト一覧(app.bsky.feed.getQuotes)](https://docs.bsky.app/docs/api/app-bsky-feed-get-quotes)

とかやる必要があるので、リクエスト数がすごいたくさんになってしまうことでした。検索で3000件とかやったら普通にリクエスト数でAPI上限に達してしまった。
（検索30, いいね一覧3000, リポスト一覧3000, レコード一覧3000*N, 引用ポスト一覧3000で合計12000リクエストくらい？そりゃそうなる）
しょうがないので必要ないリクエストはしないようにしたり(リポスト数0なのにgetRepostedByしようとしないようにしたとか)、制限のないエンドポイント(public.api.bsky.app)に切り替えたり、PDSに直接問い合わせたり、適当なキャッシュ作ってリトライできるようにしたりしました。

サンプルに置いておくので試してみて下さい。

https://github.com/shoo/bsky/tree/main/examples/recommend_follows

ちなみに、使ってみたところ上位に挙がってきたアカウントはなんか偏った思想の方々が多くなってしまい、微妙にコレジャナイ感のある仕上がりでした。検索ワードによるのだと思いますが。

## 勝手にランキング
インプレッション分析と似た発想ですが、検索したポストの中で一番お気に入り・リポストが多かったり、勢いがあったりするポストをランキングするツールを作りました。アートコンテストみたいなので使えるかもしれませんね。

サンプルに置いておくので試してみて下さい。

https://github.com/shoo/bsky/tree/main/examples/heatposts

# おわりに
[bsky](https://github.com/shoo/bsky) というライブラリを作成するにあたって思ったことですが……D言語のドキュメントが存外恵まれていたことがわかりました。
やはりドキュメントは大事。