Try   HackMD

今年に入ってからずっと、OCaml で ActivityPub サーバを書いています。 名前は Waq(ワク)で、例によって GitHub で大公開しています。 SNS に必要な最低限の機能は揃っていて、 README に従うと Docker Compose でデモが動かせるので、よければ試してみてください。

時候の挨拶

こんにちは、社会人になって 2 ヶ月くらい経った艮鮟鱇です。 最近は社内の掲示板で、昼に「オヒル=ゴ=ハーン(12001300)」とか言って、 同期の腹筋を崩壊させる仕事をしています。試用期間で切られないかが心配です[1]。冗談です。

開発の背景

そもそもの発端は、私が自分で Mastodon サーバを運営しているところにあります。 このサーバは日本での Mastodon の第一次ブーム(2017 年 4 月頃)から 1 年ほど遅れて 始めたもので、現在に至るまで安定して動作し続けています。 途中サーバを入れ替えるなどのメンテナンスも行いましたが、 データロストのような大きな不具合もなく、このあいだ無事 5 周年を迎えました。 初めは Twitter と併用していましたが、最近はもっぱらこちらで活動しています。

この Mastodon サーバを運用する過程で、Mastodon を初めとする Fediverse が使っている ActivityPub プロトコル(AP)に技術的興味を持ち、 できれば自分で実装してみたいなぁと思うようになりました。 ただ、自分の中での優先順位は割と低めで、アイデアだけでしばらく放ってありました。

事態が変わってきたのは去年(2022 年)の 11 月、某氏が Twitter を 購入していろいろなものが無茶苦茶になってきたあたりからで、 自分の(かつての)活動場所が、こんなに簡単に壊れるのかと驚きました。 幸い、自分が活動場所を移した Mastodon は分散 SNS なので、この手の話とは無縁でしたが、 Mastodon の他にも、自分がすべてを把握できているような AP 実装があれば、 仮に Mastodon が何らかの理由で使えなくなってしまっても 活動し続けることができるだろうという気持ちと、 あとは単に AP への技術的な興味がこの機会に高まったので、AP 実装の開発を始めました。

以降、特に Mastodon とか Fediverse とかの基本的な仕組みの説明はないんですが、 気になる方は Mastodon 公式が出している動画(英語ですが日本語字幕があります)とか、昔自分が書いた記事とかスライドとかを参考にしてください。

目標

Waq の(究極的な)目標は、Mastodon の drop-in な代替実装を書くことです。 ここで "drop-in" という言葉は、 ある瞬間に Mastodon サーバを Waq に入れ替えて動作させても、 RDB や API などを含めて全てがそのまま動作するようにするという意味で使っています。 現在の Waq はこの目標からはほど遠い状態ですが、例えばクライント・サーバ間の API は Mastodon の REST API のサブセットにしたり、RDB のスキーマも Mastodon のスキーマの サブセットにして、目標に近づくべく開発しています。 何年後になるか分かりませんしそれまで開発を続けるかもよく分かりませんが、最終的には、 自分が運営している Mastodon サーバを Waq で置き換えられるといいなと思っています。

ただし "drop-in" の対象にフロントエンドは含まれていません。Waq が提供するのは REST API や WebSocket を使ったストリーミング API といったバックエンド部分だけで、 フロントエンドは Mastodon 向けに作られたクライアント実装 (例えば Elk とか Subway Tooter とか)を使うことを想定しています。 将来的には(Pleroma がやっているように)Mastodon 本体のフロントエンド部分を 剥がしてきて Waq に向けて動かせるといいなと思っています。現状は Elk と Subway Tooter を使って動作確認をしています。

現状

現在のところ、Waq は次のような機能を持っています。

  • 投稿・ファボ・ブースト(リツイート)・フォローなどの基本的な機能
  • メンション・リプライ
  • 通知・プッシュ通知
  • 画像投稿・画像表示(Blurhash つき)
  • oEmbed/OGP によるプレビューカード

逆に、次のような機能は(実装予定ではあるものの)現状は実装されていません。

  • ピンどめ投稿
  • カスタム絵文字
  • ハッシュタグ
  • リスト
  • 投稿範囲(公開・非公開)

技術スタック

OCaml

実装言語に OCaml という言語を採用しています。これは単に、 自分の中で OCaml のブームが来ているからで、それ以上の理由は特にありません[2] 実のところ、後述するように OCaml の Web 周りのエコシステムは(貧弱ではないにせよ) あまり整っていないので、必要なライブラリを色々自分で作る羽目にはなっていますが、 楽しく車輪の再発明プログラミングをしています。 所詮趣味プロジェクトですし、このあたりは自由です。

OCaml は関数型プログラミング言語の一種です。世の中で(おそらく)一番有名な[3] 関数型プログラミング言語である Haskell と比べると、モナドがなかったり、 書き換え可能な変数があったりしていて、とっつきやすい言語になっています[4] それでいて Haskell と同様に強い静的型を持っているので、コンパイルが通れば (つまり型検査が通れば)実行時型エラーが起こらないことが保証され(ることになってい)ます[5] 正直な話をすると Haskell はあんまり使ったことがないんですが、 OCaml は型推論と型安全性を持ちつつ純粋でない処理も書けるので、 和洋折衷な感じで(?)便利だなぁと思っています。

ちなみに、OCaml の O は Objective の O なので、OCaml ではオブジェクト指向なコードも 書けます。OCaml community ではあんまり使われていないようです[6]が、後述する O/R マッパーを作るときには便利でした。

OCaml はメジャー・マイナーのくくりでいうとマイナーな方に属する言語だとは思いますが、 それなりに歴史があることもあって、一部の界隈ではかなり使われています。例えば、 プログラミング言語理論の界隈では割と使われていたり、 日本の大学の講義で習得が必須になっていたり あるアメリカの金融会社ではメインの言語として使われていたりします。 また Web 周りのライブラリやツールもそれなりにあって、例えば OCaml を JavaScript に変換してブラウザで動かすためのツールがあったり、Web フレームワークが色々あったりします。OCamlverse にまとまっています。

Web フレームワーク

Web フレームワークが色々あると述べた舌の根も乾かぬうちになんですが、 Waq では(小さな)Web フレームワークを自作しています。 より正確には、HTTP サーバの核の部分(Ruby でいう Puma 的な部分)は有名 HTTP サーバであるところの ocaml-cohttp を使っていて、その上に HTTP リクエストをパーズしたりルーティングしたりする小さい自前スタックが載っています。

Waq の開発初期に使おうと思っていたのは Dream で、これはインタフェースが分かりやすく、ドキュメントも豊富で、かなりとっつきやすくなっています。特に意見がなければ、OCaml で Web アプリを新規に開発したい人はこのライブラリを使うのが良いと思います。 ただいくつか懸念点があって、(i) ビルドツールがデファクトスタンダードの Dune ではなさそうであること[7]や (ii) HTTP サーバが(開発が活発な)ocaml-cohttp ではないこと[8] なにより (iii) Waq を開発し始めた時点では Dream のメンテナンスが停滞していた[9]ため、 採用しませんでした。Dream のインタフェースは自体はかなり好きだったので、 自作スタックもそれに寄せた作りになっています。 実のところ、 Dream は今年の 3 月から突如開発が再開され活発になったので、 そのうち自作スタックを捨ててこちらに乗り換えるかもしれません。

ちなみに、OCaml の Web フレームワークは Ocsigen というやつが伝統的にはいちばん有名なんですが、 ドキュメントのチュートリアルがかなり古い感じがしたのと、 インタフェースがよくわからなかったのとで避けました。 ただし、かなり早期に Dream を使おう(そして Dream とインタフェースを似せた 自前スタックを使おう)と決めたので、あんまり真面目に調べていないことを白状しておきます。

O/R マッパー

Waq の RDBMS には PostgreSQL を採用しています。これは Mastodon が使っているからで、 それ以上の理由は特にありません。

PostgreSQL を使うための O/R マッパーですが、 結論から言うと、これも簡単なものを自作しました。 OCaml コードとしてスキーマを書くと、必要なオブジェクト(クラス)や 関数を用意してくれるので、SQL を自分で書くことなくクエリを発行し、結果のオブジェクトを 得ることができます。1:1 や 1:N の関係にも対応していて、それ用に関数が生成されます。 OCaml コードからオブジェクト定義・関数を生成する部分は PPX と呼ばれるプリプロセッサを使っています。 詳細は README を参照してください。

さて一般的に、OCaml から RDB を使う一番ナウいやり方(筆者調べ)は、 OCaml ライブラリ Caqti を使う方法です。 Caqti は複数の RDB を扱える共通のインタフェースを提供するため、 Caqti を使うことで、バックエンドの RDB が何であるかを気にせず SQL を書いて 実行することができます。また ppx_rapper を合わせて使うことで、 Caqti によって得られたクエリ結果をレコードにマップすることができます。 OCaml のレコードは C や Go の構造体みたいなものなので、 Caqti + ppx_rapper でおおよそ Go の sqlx と同じようなことが実現できることになります。

ただ、Caqti はユーザが SQL を自分で書く必要があり、O/R マッパーとは呼べません[10] また Caqti を sqlx だと思って使うにしても、 Caqti + ppx_rapper と sqlx には SELECT * FROM ... の対応の度合いにおいて 大きな差がありました。 Sqlx では、予め構造体にアノテーションをつけておくことで、 クエリ中で列名とフィールド名の対応を明示せずとも勝手に構造体に値をマップしてくれます[11] しかし ppx_rapper はこれに対応しておらず、SQL を発行するたびに列名と フィールド名の対応を書く必要があります。複雑な SQL 文を何度も書く場合、 これはかなり面倒でした[12]

というわけで、自分が便利に使えるほどのものがなかったので、 それなら作っちゃうかということで作りました。ちなみにあんまりちゃんとテストしていないので、 多分 SQL インジェクションとかできる気がしています。そのうちちゃんとテストします。

ちなみに伝統的には、OCaml から RDB を扱う方法として PG'OCaml がありました。PG'OCaml は SQL 文に対して 適切に型をつけることで型安全性を担保していて、 さらに SELECT * FROM ... もサポートしています。 しかし PG'OCaml はこのところ更新されていないようです また、PG'OCaml は、その型安全性を担保するためにコンパイル時に PostgreSQL に クエリを発行するという挙動を行います[13]。ソースコードのコンパイルが、 ソースコード外の状態に依存して成功・失敗が決まるという設計がどうしても 受け入れられなかったので、Waq では PG'OCaml を採用しませんでした。

ActivityPub

ActivityPub の仕様は W3C によって公開されています。ただし、ActivityPub 実装者にはよく知られたように この仕様だけを読んでも実装を書くことはできません。というのも、 実際にサーバ間でやり取りされるメッセージ(activity)は Activity Streams 2.0Activity Vocabulary として別立てで定義されているほか、 メッセージは JSON-LD と呼ばれる方式でエンコードされるからです。 また実用上はこれらの仕様だけでは足りず、Mastodon を初めとする各種の実装は これらを拡張して通信をしているようです[14] 例えば Mastodon の場合、拡張(の一部)は このあたりで明文化されています[15]

ただし実のところ、(雑な)ActivityPub 実装を書くだけならこれらを通読する必要は全然なく、 Mastodon から流れてくる activity を模擬するように実装を書けば十分です。 実際筆者は仕様をほぼ読んでおらず、Mastodon を動かして得られる通信情報をもとに実装し、 必要に応じて辞書的に仕様を参照しています。 実装していると「ある操作をしたときに飛ばすべき・受け取るべき activity はなにか」という ことを気にすることになりますが、一応 Activity Vocabulary の 5.8 章で使い分け "Activity Type Motivating Use Cases" が示されているとはいえ、 どの操作をしたときにどの activity が飛ぶのかを仕様だけから判断することはかなり 難しく、実装を動かして確認するほうが遥かに簡単です[16]

また本来、activity として流れてくるのは JSON ではなく JSON-LD であって、 JSON-LD では一つのデータを表すのに複数の表示を用いることが許されているので、 それらを正しく扱えるように実装するべきです。 ただ、OCaml には JSON-LD ライブラリが無く、実装するにも手間がかかります。 そのため Waq では、Mastodon が流す JSON-LD のフォーマットに べったり依存し、これを単なる JSON だと思うことで処理しています。 Mastodon が使う形式以外のものが流れてくると うまく扱えないはずですが、今のところ Pleroma や Misskey 相手にも ちゃんと通信できているようです[17]

Web Push API

Waq では Web Push (RFC 8291, RFC 8292) に対応しています。 これによって、メンションやファボ、リブログ(ブースト)されたときに Subway Tooter などで通知を受け取ることができます。 例によって OCaml には Web Push を扱うためのライブラリが存在しなかったので、 自作しました。

Web Push は RFC に疑似コードとともにプロトコルが解説されているので それを読みつつ、Go の Web Push 実装(webpush-go)を読んで実装を行いました。 ただし、どうも実際にブラウザに搭載されているのは 仕様のドラフト版のようで、 それに気づくまで時間がかかりました。 ドラフトでは HTTP リクエストに含めるヘッダの作り方が若干異なりました[18]

ちなみに Web Push では ECDH や HMAC を 使って共通鍵を作り AES-GCM でメッセージに暗号化を施すのですが、 このあたりの暗号プリミティブは MirageOS のコミュニティによって整備されている ライブラリがあるので 助かりました。

このライブラリはそれなりに需要がある気がしなくもないので、 適当なタイミングでライブラリとして切り出したいなぁと思っていますが、やっていません。

Blurhash

Mastodon では画像のプレースホルダーやぼかしとして BlurHash を使っています。Waq でもこれを生成できるようにしました。 ご多分にもれずこれも OCaml 実装がなかったので実装しました。 これもそのうちライブラリとして切り出せるとよさそうです。

ちなみに BlurHash のアルゴリズムが計算している DFT(離散フーリエ変換)が、 DFT の定義そのものではない気がしています[19]が、 フーリエ変換をろくに学んでいないのでよくわかりません。BlurHash よりも性能が良いと 主張している ThumbHash は正しい DFT 計算をしていそうなので、この部分の寄与がどの程度あるのかとか調べてみると面白いのかなと 思っています。

E2E テスト

Waq では E2E テストを主軸にして、振る舞いが期待通りかを確認しています。 より具体的に言うと、ローカルで Waq を立ち上げ、これを API 経由で叩いて、 結果が期待通りかをテストします。この手法の良いところは、 Waq を立ち上げると同時にローカルで Mastodon も立ち上げることで、 Waq・Mastodon 間の通信のテストも行えるという点です。 Waq では機能を追加するごとに E2E テストも追加し、 その機能がサーバをまたいでも正しく動作することを確認しています。

ちなみに、Mastodon はサーバ間通信を行う際に、相手が TLS でつながる相手であることを 仮定しています。この制約を満たすために、Waq の E2E テストでは Cloudflare Tunnel を 使っています。事前にトンネルを作ってリバースプロキシとして動作するようにしておき、 E2E テストの際にはトンネルの外側ドメインを指定することで TLS で通信できるように します。

E2E テストがつらいのは、どうしてもテストの成否が不安定になる部分です。 また Mastodon も立ち上げる必要がある都合上、事前に必要なライブラリを ホストにインストールしておくなどする必要があり、気軽に動かすというわけにはいきません。

そこで、現在 Waq では、この E2E テストを Docker Compose に載せようと作業中です。 一応載せる基盤となる部分はすでにできていて、今現在は古いテストを新しい基盤に 移植する作業をしています。

ところで Mastodon の GitHub リポジトリには、デプロイ環境で使うための Dockerfile は 用意されているものの開発用のものは用意されていません。今回 E2E テスト用に作った Docker Compose ファイルは手軽に Mastodon サーバを立てたり壊したりできるので、 Mastodon の開発をする人にも便利だったりするのかなぁと思っています。 また、異なる ActivityPub 実装を加えて立ち上げることも容易なので、 自分で ActivityPub 実装を書く人にとっても自前実装のテストがしやすく便利かもしれません。

まとめ

そんなこんなで ActivityPub 実装を書いてるので、よかったら遊んでみてください。 もう一度、実装へのリンクを貼っておきます お相手は艮鮟鱇でした。またね。


  1. この記事を書き始めたとき(5 月下旬)は試用期間だったんですが、投稿するまでの間に晴れて試用期間が終わってました。 ↩︎

  2. daidoquer2 を Elixir で作ったときも同様でした。 ↩︎

  3. 最近だと Elixir とかのほうが有名かもしれないですけども。 ↩︎

  4. OCaml と Haskell の違いは正格・遅延評価など他にも色々ありますが、まぁ OCaml よりも Haskell のほうが知っている人が多そうかなというのでこういう説明をしがち。 ↩︎

  5. 一応、OCaml 全体を対象にして形式化して型安全かを証明している人は居ないという認識なので「(ることになってい)」と書きましたが、まぁめっちゃ細かい話ですし、C++ とか Go とか Java とかと比べると sound 感がかなりあるので、まぁ。 ↩︎

  6. 例えばこの StackOverflow の回答とか。 ↩︎

  7. Dune でもビルドはできそうなんですが、example を見ていると esy というツールが推奨っぽく見えます。 ↩︎

  8. Dream では http/af というライブラリが使われていて、これは ocaml-cohttp を高速化するというプロジェクトのようなんですが、最近は cohttp 側も高速化に成功したっぽい記述もあり、それなら更新が続いていそうな cohttp を使いたいかなぁくらいの判断です。 ↩︎

  9. discuss.ocaml.org でも話題になったりしていました ↩︎

  10. 実のところ、OCaml で使える O/R マッパーは、現在無いようです。一応、過去には OCaml で O/R マッパーが開発されていたこともあるようなのですが、現在では放棄されているようです。 ↩︎

  11. sqlx についてはこのチュートリアルがわかりやすい印象です。 ↩︎

  12. SELECT * なんて書くなという話もあるんですが、カラムを適当に増やしたり減らしたりするたびにクエリを変えるのが面倒だったので、特に初期は多用していました。 ` ↩︎

  13. ついでに、placeholder が無いようなクエリについてはコンパイル時に実行してしまうっぽい挙動をしていたというのもかなり気になりました。ただしちゃんと検証していないので誤解かも。 ↩︎

  14. 「ようです」と書いたのは、筆者が(後述するように)仕様を丹念に読んだわけではないので、どこまでが仕様でどこまでが拡張なのか判断がついていないためです。 ↩︎

  15. Misskey も同様にドキュメントになっています。 ↩︎

  16. 例えば Mastodon で「自分は bot です」フラグを立てるように設定すると、ActivityPub 上は Person ではなく Service として表現されるようになる、とか。 ↩︎

  17. 結局のところ Mastodon と通信できないと ActivityPub 対応する意味があんまりないので、みんなそれに合わせて実装してそうみたいな直観があります。 ↩︎

  18. webpush-go はドラフト版ではないヘッダの作り方をするので Google Chrome とかでは対応してなさそうに見えているんですが、ちゃんと検証していないので詳しいことはわかりません。Issue を漁った感じ Chrome にも対応していると書いてあるので、なにか自分が見落としているかも。 ↩︎

  19. cos の中に
    12
    がなさそう。 ↩︎