WhalebirdはMisskeyサポートを開始します

Whalebird 4.0.0より,Misskeyをサポートします. Misskeyのドメインを入れていただければ,ログインできるようになってます.

f:id:h3poteto:20200325215330p:plain

ただし,一部機能は使えない(かもしれない)のと,対応していない機能はあります.

対応予定

  • 絵文字リアクション
  • 引用Renote

その他要望,バグ等がある場合はIssueを立ててください.

github.com

中身の話

対応方針

もともとMisskeyに対応してほしいというリクエストは2019年前半にはもらっていた. ただ,APIドキュメントを見る限り,MastodonAPIとの互換性はない. なので,Pleromaのときのように,ちょっと追加したらそのまま使えるわけではなかった. そのため,1年くらいひっぱることになってしまった.

で,数カ月悩んだ結果,これをWhalebird本体側で対応するのは諦めた. APIエンドポイントが違うのであれば,なんとか対応しきれないでもないが,そもそも返すエンティティに互換性がない. エンティティを相互に変換するか,もしくは表示側でSNSごとにコンポーネントを挿げ替えるようなことをしないといけない.

相互変換ならまだしも,コンポーネント挿げ替えとなると,例えば今後親しいFediverseのSNSを追加する際に,毎回コンポーネントを新規に追加していかなければならない. これは負担が大きすぎるし,デバッグの手間がかかりすぎる.

相互変換するとして,それでもエンドポイントは違うので,結局大量の分岐が必要になる. ので,諦めた.

Whalebirdは,MastodonAPIを叩くのに,megalodonというAPI Clientライブラリを使っている. こいつを拡張し,Mastodon, Pleroma, Misskeyのエンドポイント,エンティティを透過的に扱えるライブラリにしてはどうか.

megalodonとしてのinterfaceを定義して,それに合致するメソッドを持ってさえいれば,SNSの差異を気にすることなく使えるようにする. もちろん,ここでインプットとアウトプットのエンティティ相互変換も行う.

ただし,各SNSはすべてが共通するメソッドを持つわけではない.例えば,Mastodonに存在するBookmarkというエンティティは,Misskeyには存在しない. エンティティがないのだから,それに関するエンドポイントも存在しない. そういったものであっても,megalodonとしては使える状態にあってほしい. なので,megalodonのinterfaceは,実装するSNSの持つメソッドを,すべて包含するような定義にしておく. そして,各SNS実装内部で,存在しないメソッドであれば NoImplement な例外を投げれば良い.

megalodon ⊇ Mastodon かつ megalodon ⊇ Pleroma かつ megalodon ⊇ Misskey という感じ. megalodon interfaceが定義するメソッドはすべてのSNSの上位集合になる.

というような妄想を3ヶ月くらいし続けた結果,まぁだいたい行けそうなくらいに固まってきたので,megalodonをそのような形に作り変えた. メソッドが馬鹿みたいに増える予感はしたので,ドキュメントを生成できるようにしておいた.

h3poteto.github.io

作戦

もともと,megalodonはaxiosの薄いラッパー程度のライブラリだった. ヘッダーにUAaccess tokenの付与を行うだけの,get, post, patch, put, delete を提供しているだけのライブラリだった.

なので,MastodonAPIを叩くにしても,エンドポイントの指定はユーザが行う必要があった.

const client = new Megalodon(access_token, base_url)
client.get('/api/v1/timelines/home')

みたいな感じのコードを書いていた.

方針としては,interfaceでメソッドを全部定義する必要があったので,この方式を全廃し,

const client = new Megalodon(access_token, base_url)
client.getHomeTimeline()

というような形にすべてを書き換えるところが第一弾.

次に,SNSを指定したら,合致するSNSのクラスを返すようなgenerator関数を定義してやる.つまり,

const client = generator('mastodon', base_url, access_token)
client.getHomeTimeline()

という使い方に変更するのが第二弾.

最後に,SNSの特定までをmegalodonにやらせるために,detector関数を定義する.

const sns = detector(base_url)
const client = generator(sns, base_url, access_token)
client.getHomeTimeline()

こうすることで,Whalebird上から,どのSNSを操作するかという情報をすべて排除することができる.

こうしておけば,megalodon側で,MegalodonInterfaceを守るMisskeyAPIClientクラスを作って,generatorで misskey 指定を許可し,detectorで misskey を検出できるようにしておけば,それだけでWhalebird側のコードをいじることなく対応が完了する(厳密には少しいじる必要はあったが).

実装について

MisskeyのAPIとドキュメント

これはもしかしたら有名な話なのかもしれないが,MisskeyのAPIドキュメントがあまりアテにならない. 本家である,misskey.ioのドキュメントを参照しても,

misskey.io

実際にcurlで帰ってくるレスポンスと別物が定義されていることがある.

掲載されているエンドポイント自体はおそらく正しいが…….それとてすべてを確認できたわけではない.一応今回確認した範囲で404はなかったと思う.ここに載ってないAPIが公開されていたとしても,それはわからないが.

なので,MisskeyAPIClientを作る上で,ドキュメントはエンドポイントの参照くらいにしか使えなかった.結局すべて自分でcurlを叩いて確認するということになった.

また,MisskeyのAPIは独特だ. すべてのエンドポイントはPOSTしか受け付けない.いわゆるRESTではない. 参照系のAPIであってもGETは使わず,すべてをPOSTで受け付けている.

また,ユーザの詳細情報を取得するAPI(/api/users)と,タイムライン等の投稿を取得するAPI(/api/notes/timeline)には,両方共ユーザの情報が含まれるが,noteのエンティティに含まれるユーザ情報は簡易版で,ユーザ詳細で得られるエンティティより項目数が圧倒的に少ない.

エンティティの相互変換を行う上でこのへんは非常に扱いにくい点である.

OAuth2フローの違い

Misskeyの認証は,厳密に言えばMastodon(が使っているdoorkeeper-gem)が利用している,OAuth2.0のAuthorization Code Grant(RFC 6749)ではない. そのため認証用のメソッドはそのまま流用できなかった.

多少のカスタマイズを行っている. まず,アプリケーションの登録を行う.この時点でのMastodonとの差異は,ClientIDはもらえず,ClientSecretだけがもらえるという点.

次に認可エンドポイントのURLとSessionTokenを得るために,/api/auth/session/generate にPOSTリクエストを投げる.このときClientSecretをパラメータとして付与する. このフローはMastodonには存在しない.Mastodonの認可エンドポイントは予め決められていて,パラメータだけ変えれば良い.

そして得られた認可エンドポイントにリクエストを投げる. ここにリクエストすると,認可画面になるので,ログインした上で認可ボタンを押す. Mastodonは,この時点で認可コードを発行し画面に表示するが,Misskeyは何も発行してくれない.

最後にトークンエンドポイントにリクエストする.Mastodonであれば,ClientID, ClientSecretと一緒に認可コードを送るが,Misskeyの場合は, /api/auth/session/userkey にClientSecretとSessionTokenを付与する.

これで得られたTokenを, sha256(token + ClientSecret) とすると,APIアクセスに必要なAccessTokenが得られる.

Streaming

MisskeyはStreamingをWebSocketで実現している.この辺は,Mastodon, Pleromaと同じ. ただし,Mastodon, PleromaはタイムラインごとにWebSocketのエンドポイントを設けている.Publicなら,/api/v1/streaming/public みたいな. しかし,Misskeyはすべて/streamingで接続し,接続後にタイムラインごとのチャンネルに接続する必要がある.これは,WebSocket上で,

type: 'connect',
body: {
  channel: 'homeTimeline',
  id: 'arbitrary id'
}

というようなjsonを送ることでチャンネルとの接続を開始できる.そして,接続したチャンネルからのメッセージをすべて同じWebSocketコネクションで受け取る. それを振り分けるために,IDには一意なIDを付与しておき,そのIDによって,どのチャンネルからのメッセージかを判別する.

残りは筋力

MastodonにしろMisskeyにしろ,APIの数は結構多い.今回の実装でも,interfaceだけで1000行程度,APIClient classについては2000行程度の実装で,だいたいmegalodonのinterfaceを満たすものができる. Pleromaは,ほぼMastodonと同じエンドポイント,エンティティを有するので,MastodonAPIClientを継承させて特異なものだけをoverrideしている.

行数は多く見えるが,ロジックとして複雑なのはエンティティの変換部分くらいなもので,基本的には対応するAPIを呼び出していくだけだ. いずれにしろ,このくらいの実装で新しくSNSを追加できるようになった.

まとめ

というわけで,megalodonは他のSNSを追加できる状態になった.気になるのがあったら,そのうち増やすかもしれない. とはいえ基本的には,MastodonAPIをベースとしているので,かけ離れた機能を実装するかは怪しい.

とりあえず次は絵文字リアクションを作りたいんだ……PleromaもMisskeyも実装してあるからね.これはちょっと近そう.