タイトル通りではあるが、その言葉全部通じない人も少なからずいるし、そもそもどうやっては書いてない。まずは言葉の定義から順にする:ActivityPubって何、それを使った通信が一体どんなものか、SNS連合とは何なのか、そして最後どうやってそれと通信できたかを述べる。
ちょっと歴史から
遠い昔遥か彼方の銀河系でツイッターに飽きた民が、自由を手にするために独立を図った。GNU Socialなどのサービスが生まれて、GNUだけのオープンソースの自由ソフトウェアとしてある程度普及した。どっかの一つの大企業が運営するんじゃなく、ブログと似た感じで、一人個人でもサーバーさえあればお一人様だけのものが作れる。ブログと違ってそのソフトは他のサーバーと通信が可能。その独立したサーバーで成り立つゆるいネットワークがSNS連合。
それを可能にするには必須なのは共通言語であるプロトコル。自由ソフトあるあるの感じはするが、プロトコルだけでも数がある。GNU Socialが土台にしたOStatusの他にも、Facebookを代替しようと始まったdisapora*やHubzillaの裏になるZotなどなどある。これらは疎通はできないが、複数も対応してるサービスも実はある。
ActivityPubの登場
OStatusのStatusNetで満足できずもっともっといろいろなやりとり方を許容する方法が欲しいと、pump.ioが動き出した。そしていつの間にか、大手SNSがユーザーのデータを悪用するなど、イメージがだんだん悪化する中で、もっと正式なものがあった方が情弱の人にも近づきやすいだろうと。いつの間にかSNSプロトコルの開発がW3Cの中で正式になって、つい2018年の頭ActivityPubとしてリリースされた。
Mastodonの作者はOStatusにいろいろ不満あってリリース前からActivityPubに関わっていて、W3Cの推奨になる前からMastodonがその対応はしていた。そこから一年間経った今で俺の見解は、diaspora*でさえActivityPub対応が検討されてる以上、新しいサービスはActivityPub対応すればほとんど通じるだろう。
コンコン
ツイッタの闇に疲れて去年からマストドンをメインとして使うようにしてる。マストドンの民けっこう自由意識が高く、だれにも指図されない環境求める人が多い印象がある。そして技術力もそれなりにあって、使ってるサービスになんかの不満があると、ぶつぶつ言いながら(主要SNSみたいに)耐えるではなく、自分のを作るのがよく目にする。そこにマストドンと実際に疎通ができるPleromaやMisskeyをみて、俺も作りたくなった。それがキツネの始まり。
APIはSwaggerを使って確立して、その上に完全に分離したフロントをつけるのは計画。ただ自分のモチベーションを保つために定期的に何らかの成果物が必要。ちょこちょこと裏の機能だけ追加するでは物足りない。そこで目標としたのは他サーバーのユーザーとフォローし合えること。
必要なもの
- HTTPS
- WebFinger
- ActivityPubの形式のJSON-LD
- HTTP署名
HTTPS
HTTPSは以外と問題になる。ローカルで開発して、外部のサーバーと通信しようとすると、ローカルにもちろんHTTPSでつなげない。対策は3つある。
1つ目は、通信する相手もローカルにたててHTTPS要求しないようにソースをいじる。ActivityPub対応のサービス基本的にオープンソースなのでできる。俺は開発するのは基本的にdocker-composeの仮想ネットワークの中なので、そこにマストドンやプレロマが入ったコンテナを追加する。プレロマは元からそういう想定でdocker-compose.ymlまであるけど、マストドンはドッカーをデプロイ用にしか使ってないのでローカルで動かせるのは二苦労。プレロマの方が通信の確認が厳しいし、それがおすすめ。
2つ目は、ngrokやlocaltunnelでHTTPSのドメインをもらう。両方とも設定がすごく簡単で、localtunnelはサブドメイン指定も無料でできるのですごく便利。それで物足りなくなったらsshの逆ポート転送 (reverse ssh port forwarding)で自分のHTTPS可能サーバーを使ってつなげることも可能。
3つ目は、素直にサーバーにブツを上げてHTTPS可能にする。Let’s Encryptがある以上、証明書にお金かかる心配もないし、一瞬でできる。その代わりにサーバーにデプロイする手間が、ローカル開発と比べて圧倒的。
俺はこの順番で3つとも使っている。開発のデバグ多い最初のフェーズで完全ローカルのドッカーの仮想空間で、そのあとlocaltunnelで外部と通信して、最後はサーバーにあげて本番ぽく。
もう一個HTTPS関係でひっかかるのは、TLS1.3。自分で鯖缶になろうと思う人はどうしても技術力ありがちなんで、新しいTLS1.3「のみ」対応するサーバーもある。それが最新のJVMじゃないと通信できないし、Nettyを使うAlephはそれでもやり方まだ不明。
WebFinger
WebFingerはusername@hostの形をした覚えやすい(そしてOStatusでも使われてる)ユーザー名からそのユーザーのプロフィールなどの情報を取得する方法。中身がほとんど確定しているのはホストのメタデータを提供するエンドポイント。
ユーザーの情報を問い合わせできるのはresourceエンドポイントで、大体の形は決まっている。大体と言うのは、ほとんどの情報フィールドが任意で、通信相手のサービスが何を求めるかで増えたり減ったりする。
最初はキツネの中で作ったが、単一責任の法則うんぬんかんぬんで別のライブラリに切り分けた。WebFingerは全体的にXMLとJSON両方とも対応するプロトコルで、XMLの出力と解読で若干苦労した。
ActivityPub形式のJSON-LD
APIが最初からJSONで動いているのでActivityPub対応というのは、 @context
のお決まりのフィールドに適切な値を置いて、標準にないフィールドを使うとその名前空間もちゃんと @context
に追加するぐらい。
マストドンは内部でこのコンテキストのやりとりとか算出をライブラリ使って本格的にやっているが、こだわりがなければマストドンのコンテキストをそのままコピペして使っても支障がない。一方コンテキストが欠けていると、マストドンがそのJSONを不正と認識して無視するので注意。
俺はまだマストドンのコンテキストでいいや、となってるから優先順位的に3の次やけど、余裕ができたらJSONLD-JAVAというライブラリと遊んで、そのラッパを作ることになるかもしれない。ルビーなどにも似たようなライブラリは存在する。
HTTP署名
リクエストにHTTP署名をつけるのは決して楽な話ではない。規格を読みながら、マストドンの作者のブログを読みながら、実際のマストドンやプレロマのコードを見ながら実装してた。
強いて言えば規格通りにやりゃええ。ただそこでひっかかるところが多い。
Base64の文字列は=でpaddingされちゃだめなのか、逆にするべきなのかとか。
規格では keyId
は基本なんだっていいけど、実際のサービスはそれがユーザー名と部分一致しないといけないとか。
算出する時にDateヘダーの月日の数値が一桁になるとだめで、RFCの日付形式としてそれでいいのに二桁じゃないとマストドンと通信できないとか。おそらく向こうのnginxが勝手にヘダーを変えてる推測。ソースにはそれらしいものは見当たらないので。
一応実際の通信ができる、できなかったら速攻直すライブラリは出した。ドキュメントは余裕があったら書く。(笑)
実際の通信
最後に実際のフォローし合うという流れを軽く説明しよう。
- 相手を検索する。WebFingerでusername@hostからありそうなところをたどって、そのユーザーのActivityPub用のエンドポイントと受信箱のURLを探り出す。
- 相手の受信箱に、正しく形成されたActivityPubのFollowをPOSTで投げる。このリクエストはHTTP署名が必須で、ないもしくは不正であれば401で断られる。
- 相手がフォローを承認したら(ロックなしのユーザーは自動承認)向こうからこっちの受信箱にAcceptがPOSTされる。
逆もそんな感じ:Followが飛んできたらAcceptを返せば関係成立。Acceptを返さないと、向こうがFollowが承認待ち状態と見る。フォローを拒否する意味のRejectを返すかは実装次第で、AP的には返さなくてもいい。
そして関係が成立したら、向こうからフォローされた人が投稿とかしたら、そのActivity全部こっちの受信箱に送ってくれる。これで連合が成り立って動く。