2025/07/11

ActivityPub実装を自作した:マイクロブログの復権

開発前談

ここ一ヶ月余りの間、ろくに文章も書かずになにをしていたのかと言えばActivityPub実装を作っていた。ActivityPub実装とは、TwitterがXと化した経緯をきっかけに再び注目を浴びたMastodonやMisskeyなどのサーバソフトウェアを指している。

これらは単一のWebサービスではなく「ActivityPub」という共通のネットワーク上に存在する無数のサーバが相互接続を行う形で成り立っている。もし皆さんがたとえば「Misskeyにアカウントを作った覚えがある」と認識していたら、それは数多ある「Misskeyサーバ」のどれかの一つだ。

XやInstagramと比べてこれのなにが嬉しいのかと言うと、どこか一つのサーバが突然壊れたり、サーバを管理している人間の人格が壊れても他のサーバに移動してサービスの利用を継続できるところにある。Xを管理している人間の人格はすでに壊れて久しいが、世界のどこにも「別のXサーバ」なるものは存在しないため、どんなに嫌でも人々はそこから移動しづらい状況が続いている。

上記の過ちを繰り返さないためにはソーシャルネットワークそれ自体をActivityPubのような公共のプロコトルで分散化し、特定の人間や企業が権威的に振る舞えない形が望ましい。このアイディア自体は古くから存在していたが、2016年にドイツの青年がMastodonを開発したことで一挙に認知度を得るに至った。以降、Twitter――X――がやらかすたびにちょっとずつActivityPubネットワークの人口が増加している。こうした分散型SNS全体を宇宙になぞらえてFediverseとも呼ぶ。

同じ発想のサービスには「Bluesky」もある。BlueskyはATProtocolという別の分散型プロコトルを採用している。こちらは検索システムやモデレーション(不適切なユーザに対する制裁など)といった負担の大きい機能を特定の権威(運営企業など)に任せ、それ以外を分散化するモデルなので表面的な使い勝手がXに近く、分散型SNSとしては新顔でも知名度はどのActivityPub実装よりもおそらく高い。

といってもActivityPubとBluesky(ATProtocol)は敵対しているわけではなく、むしろ相互接続が可能なブリッジサービスがリリースされていて基本的には協力姿勢を保っている。XやInstagramのような強大なライバルと戦うには少しでも味方が多くいた方がいい。僕も自分のMastodonサーバから何人もBlueskyユーザをフォローしていて、特に支障なく会話を楽しんでいる。

他には「Threads」というSNSがある。Meta(Facebookの運営企業)がリリースした新しいSNSで、Instagram譲りの集客力を活かしてサービス開始2年目にして3億5000万人ものユーザがいる。あと数年もしたら5億ユーザを誇るXさえも追い抜いてしまうかもしれない。

そして驚くべきことにこのSNSはActivityPubへの参加を表明しており、現時点でも限定的な相互接続が可能になっている。いつか完全な参加が実現した日には分散型SNSのユーザが急増する大逆転劇と相成るが、同時に巨大資本がネットワーク上で権威化してしまわないか不安視する声も大きい。

なにしろActivityPubがどんなにオープンなプロトコルでもThreadsとの相互接続がサービス人口的な面で前提化してしまったら、それは実質的に「誰と繋いで誰を弾くか」の決定権をMeta一社が持っている状況に等しくなる。有力な企業アカウントやインフルエンサーを数多く擁するThreadsの潤沢なユーザ資源はネットワーク全体にとって魅力的だが、魅力的であればあるほど権威的にもなりやすい。

分散型SNSの情勢はざっとこんな具合だ。つまり、ここにはお客様の快適さは望めない代わりに懐古的、自己探求的な自由がある。昔、まだ企業が台頭する前のインターネットに似た、リバティとアンダーグラウンド、支配と抵抗が紙一重で拮抗しているような、先の読めない不安と将来への期待が入り混じったロマンに満ちあふれているのだ。

僕が最初にここへ――Fediverseへ――来た時は、巨大なインスタンスのいちユーザでしかなかった。大きいインスタンスは気楽でユーザも見つけやすいが分散型SNSの醍醐味を味わいにくい。顧客ではなくコミュニティの一員として主体的に振る舞うには、むしろ小規模なインスタンスの方がふさわしい。インスタンスからインスタンスへ、移動に次ぐ移動……最終的に、僕ひとりだけの専用インスタンスを自分のサーバ上に構築したのが、今から約2年前の頃になる。

管理者権限をもとにインスタンスを存分に操作する自由は、デジタル世界における自己表現として一層特別な感覚を味わえた。ちょうどギリギリと唸り声をあげるハードディスクのアクセスランプを見ながら、自分の”ホームページ”が世界中に公開されたあの瞬間に似ている。

そうして2年も経つ頃には当初の感動も薄れ、再び自分の主体性、自己探求の限界を問い直す時期がやってきた。冒頭で言った通りここは共通の分散型プロトコルで相互接続を行うネットワーク空間、なにもFediverseを旅する宇宙船にMastodonやMisskeyの刻印が入っていなければならない理由はない。どんな不格好な飛び方でも第二宇宙速度に達した船は宇宙船になれる。

20年以上に渡るインターネット人生の中で僕はあらゆる発信をしてきた。”ホームページ”やブログも飽きるほど作っては壊してきた。しかし共通のプロトコルに合わせて相互接続が可能な、まとまった形式の実装系を開発した経験は、業務を除いてはなかった。だから僕は豪華でピカピカな既製品の実装に別れを告げ、継ぎ接ぎだらけでゆらゆらとしか飛べなくても自分自身の実装系を作ろうと思った。

letter : General Letter Publication System based on ActivityPub / 一般書簡公衆化システム

「作りたい」と言った次の瞬間にはもうできている。なるべくそういう状態を目指したい。幸いにも今回はうまくいった。letterはソフトウェア名だが、個人的にはこれを一般書簡公衆化システムと呼びたい。かつてTwitterは「マイクロブログ」と呼ばれていた。その頃の素朴さを取り戻し、ひいてはマイクロブログの復権を目指す意思がこの抽象化した名称に込められている。

本実装系はRails8で開発した。近年はレガシー扱いされたり、静的型付け言語の優位性を語るダシにされたりと不名誉な有様に陥っているが、典型的なWebアプリケーションを高速に実装する上では依然として有力な選択肢に入ると思われる。特にRails8でSolid QueueやSolid Cable、Solid Cacheといった新機能が搭載されたおかげで、RedisやSidekiqに依存せず様々な処理を行えるようになったのは僕が理想とするミニマルなActivityPub実装を設計する上でとても助けになった。

バックエンドのみならず、本実装系のフロントエンド部もごく簡素で閲覧機能程度しか備えていない。LikeやRepostも内部的にはカウントされていても外からは見えない。ログインすれば一部のサーバ設定が行えるが、どこにもユーザ登録画面はないし、そのための導線もない。この実装系は一人で使う前提だからだ。アカウントは2つまで登録できる。2つ目は鍵アカウントや広報用、テスト用など補助的な用途に使える。

MastodonやMisskeyは分散型SNSの大航海時代を切り拓いてくれた偉大な実装系とはいえ、膨大な処理をさばくための冗長なバックエンド構成や、リッチな内蔵Webクライアントなどの諸機能は一人用にしてはサーバリソースの負担が大きい。おのずとフロントエンド部もマイクロブログというよりはSNSの気配を帯びた画面構成にならざるをえない。

そこへいくと本実装系はブログ的性格を強く打ち出している。そもそも特定のユーザの投稿を読むのにフォロー・フォロワーの関係を持たなければならないというのは誤った認識であり、本来はどこにも会員登録をせずにWebページやRSSを通じて購読可能な状態がマイクロブログの正しい姿だと僕は信じている。そのため、本実装系では投稿本文の閲覧を妨げかねない装飾を極限まで廃している。コンテンツの価値はコンテンツそれ自体に存在すべきだ。

その甲斐あってフロントエンド部は1998年からタイムスリップしてきた逆ジョン・タイターでも即座に馴染むクラシックさを醸し出しており、片や画面の更新処理にはHotwire(Turbo)、デザインにはTailwindCSSを用いて現代的なユーザビリティを実現している。部分的に再描画が行われるので非常に低いロード時間で画面遷移が行われる。なにげに過去の投稿も無限スクロールで取得できる。

設定画面ではプロフィールの編集やリレーサーバ、カスタム絵文字の管理など、Web画面上の方が好ましいと思われる項目を厳選した。とりわけカスタム絵文字はFediverseがもたらした偉大な文明の一つである。技術的にはコロンで囲まれたショートコードをパースして画像に置き換えているだけだが、おかげでblobcatを初めとするすばらしい意匠がこの世界で花開いている。当然、Mastodonと同様に接触済みのインスタンスから絵文字をコピーする機能も設えた。

一方、ユーザアカウントの管理などの単純な機能についてはターミナル上で統合管理ツールを用いて行う。下の画像のやけに立派なアスキーアートはoh-my-logoで作成させていただいた。ちょうどこういうのを用意したいと思っていたのでまさに渡りに船だった。この場を借りて感謝を申し上げたい。

なお、本実装系はサードパーティクライアントの利用が前提ゆえMastodon APIにほぼ100%準拠している。おすすめはPhanpyMoshidonだ。クライアント上の表示も含め、なるべく網羅的に対応するために自分が使っていない機能にも色々と手を加えた。

実装中、頭の中に常にベン図が浮かんでいた。さもなければ不等式の問題に出てくる塗りつぶされた数直線だ。どういう条件にまとめたらすべての表示がいい感じにOKとなるのか地道な試行錯誤を繰り返した。ホームタイムラインではよくてもリプライ欄でNGだったり、リプライ欄でOKでも通知欄でNGだったりする。

@[email protected]をパースできた代わりにhttps://example.comがパースできなかったりもする。フロントエンドとクライアントでは条件が異なる場合も多かった。もちろん、受信と送信もそれぞれ分けて考えないといけない。

 1  # 1. 絵文字パーサー (EmojiParser)
 2
 3  class EmojiParser
 4    EMOJI_REGEX = /:([a-zA-Z0-9_]+):/
 5
 6    def parse
 7      @text.gsub(EMOJI_REGEX) do |match|
 8        shortcode = Regexp.last_match(1)
 9        emoji = find_emoji(shortcode)
10
11        if emoji
12          build_emoji_html(emoji)
13        else
14          match # 絵文字が見つからない場合は元のテキストを返す
15        end
16      end
17    end
18
19 # :shortcode: 形式の絵文字を検出し、CustomEmojiモデルから対応する絵文字を検索してHTMLタグに変換する。
20
21  # 2. URLとメンションのリンク化 (TextLinkingHelper)
22
23  def auto_link_urls(text)
24    if text.include?('<') && text.include?('>')
25      # HTMLの場合は特別な処理
26      linked_text = apply_url_links_to_html(text)
27      mention_linked_text = apply_mention_links_to_html(linked_text)
28    else
29      # プレーンテキストの場合
30      escaped_text = escape_and_format_text(text)
31      linked_text = apply_url_links(escaped_text)
32      mention_linked_text = apply_mention_links(linked_text)
33    end
34    mention_linked_text.html_safe
35  end
36
37 # URLは簡単な正規表現 /(https?:\/\/[^\s]+)/ で検出できるが、メンション部分には特別な工夫が必要だった。
38
39  # 3. 処理の統合 (StatusSerializationHelper)
40
41  def content_data(status)
42    # 絵文字をショートコードに戻してからURLリンク化
43    content_with_shortcodes = parse_content_links_only(status.content || '')
44    linked_content = auto_link_urls(content_with_shortcodes)
45
46    {
47      content: linked_content,
48      emojis: serialized_emojis(status),
49      # 以下省略
50    }
51  end
52
53 # API用には絵文字をショートコード形式に戻し、フロントエンド用には絵文字→URLリンク化の順で処理を行う。

丸一ヶ月も唸り続けただけあり、特定のサードパーティクライアントを不満なく使える水準には達したと思う。これまで一度も使った試しのない投票機能もちゃんと動く。ユーザ名とリンクと絵文字とOGPがすべて正常にパースされるか確かめるたびにテストリプライを送りまくっていたせいで、今や投稿本文がJSONのフィールド列の羅列に見えて仕方がない。

ともあれこうして僕の思想性を強く表した、この世界にたった一つの実装系が外宇宙に飛び立った。まだエンジンのかかりかたがおかしく奇妙な軌道を描くが、文句なく第二宇宙速度を超えて飛んでいる。Fediverseの片隅で、ただ一人、電子で象られた書簡を公衆送信し続ける自由な機械装置である。

反省点

ActivityPub実装の開発は以前から温めていた構想だったのでなるべくクリーンに開発したかったが、実際には思った通りにはいかなかった。一つのAPIの動作確認をするのに結局他のAPIが必要だったり、API以外のコードも書かなければいけなかったりして、なかなか粒度を揃えたコミットを行えなかったのだ。今後は仕事でなくても設計書を書いてみるのも一つの手かもしれない。

次に、テストが後手に回ったことが挙げられる。いわゆるテスト駆動開発とまではいかなくとも、しっかりテストを書いて計画的に開発を進めるつもりはあった。しかし、差し迫った問題に応急処置的な修正をしているうちに正式なテストがどんどん後回しになり、なんだかんだで事後承認的な結果に甘んじてしまった側面は否めない。事実、これが遠因となり動作不良の原因解明が遅れたので気をつけたい。

Rubocopを御しきれなかったのも悔しい。あれはあくまでコード品質を保つ補助具であって、その道具をなだめるためにコードを書いていてはどうしようもない。デフォルト設定に従って受け身でメソッドを分割していたらかえって可読性の低いコードが出来上がってしまった。今後は最初から実装内容に適した設定を心がけたい。

今後のアップデート内容

マイクロブログの復権を目論むからにはやはり投稿内容を復元する機能を追加したい。マイクロブログは投稿頻度の細かさから生活の実態をより精緻に表しやすく、ライフログにも活用しやすい。最悪、ActivityPubに認識されなくてもなんらかの形で元の投稿日時を保持したインポート・エクスポート機能を実装する予定だ。

また、Mastodonが直近のアップデートで実装した引用リポスト機能も内容次第では追従する可能性が高い。今はMisskeyスタイルの引用機能を実装しているものの、これはサードパーティクライアントによって対応の可否がまちまちで難しい。Mastodonの形式であれば各種クライアントも確実に対応するだろうから、こちらも同様の措置を採ることができる。

あわせて読ませたい

Migrate
分散型SNSをモチーフにした中編SF小説。Xからの脱出先に分散型SNSが注目される中、あえてそれをユートピアとして描かず、むしろどんな場所も連帯を損なえばたやすく強大な存在に自由を奪われることをバイオレンスに表現した。

©2011 辻谷陸王 | Fediverse | Keyoxide | RSS | 小説