前編では、ActivityPub/
後編の記事では、Takahēサーバーの基本的なアーキテクチャや、Takahēの特徴的なコンポーネント、内部で使われている面白いライブラリなどを紹介します。記事の最後では、docker-compose
を使って実際にTakahēサーバーをコンテナで起動し、手元で試してみます。
Takaheの基本的なアーキテクチャ
Takahēは、主に3つのコンポーネントから作られています。メインのTakahēサーバーは、Mastodon互換のREST APIとActivityPub APIの2種類のAPIを実装しているDjangoアプリです。また、Statorと呼ばれるバックグラウンドワーカーもDjangoで実装されており、非同期処理のタスクや他のActivityPubサーバーとのやり取りを行います。Takahēでは、サーバーとStatorワーカーはステートレスな実装になっており、すべてのステートフルなデータはPostgreSQLデータベースに保存されています。
Djangoウェブフレームワーク
Takahēサーバーのコアとなる部分は、Djangoウェブフレームワークを使用してPythonで書かれています。Djangoは、アメリカ合衆国カンザス州ローレンスにある新聞社のウェブ部門であるWorld Onlineで開発されました[1]。
Djangoの特徴は、プログラマでなくても扱いやすいテンプレートエンジン、さまざまなデータモデルを表現できる柔軟なORM、優れたデータベースマイグレーション機能、ビルトインの管理サイト・
Djangoが初めてリリースされたのは18年前の2005年ですが、現在まで常に改良が続けられてきました。2023年12月にリリース予定のバージョン5.
Takahēの作者であるAndrew Godwin
APIライブラリhatchwayが提供するMastodon互換REST API
Takahēサーバーが提供する1つ目のAPIは、ユーザーがクライアントアプリケーションから接続するときに利用されるMastodon互換のREST APIです。クライアントアプリは、はじめにOAuth2認証のAPIエンドポイントを使ってアカウントに接続した後、各種APIエンドポイントからTakahēアカウントの情報を取得したりアカウントを操作したりできます。
たとえば、GET /api/
APIエンドポイントからは、ホームタイムラインの投稿一覧がJSONで取得できます。POST /api/
に投稿のIDを送信すれば、Takahē上でその投稿をいいねする処理が行われます。こうして取得したJSONデータを自由に加工したり、各種イベントハンドラでAPIと通信することで、クライアントアプリは自由にUIが実装できるようになるわけです。
PythonのAPIフレームワークとして最近非常に人気があるものに、FastAPIがあります。FastAPIの特徴の1つにPython標準の型情報を活用したデータのバリデーション機能がありますが、この機能は主にPydanticと呼ばれるバリデーションライブラリで実装されています[4]。
Pydanticでバリデーションを行うと型が保証された扱いやすいデータが得られ、APIフレームワークの実装では非常に便利なため、Djangoでもこれを活用したdjango-ninja
というサードパーティのAPIライブラリが開発されています。Takahēでも当初はこのライブラリを使用してREST APIの実装が進められましたが、MastodonのREST APIの特殊な仕様により、Pydanticの機能が十分に活用できない問題などが見つかりました[5]。
そこでTakahēのために、特殊な仕様にも対応できてDjango標準のAPIに近いインターフェイスを持った、Pydanticを活用した新しいAPIフレームワークdjango-hatchway
が開発されました。このフレームワークはTakahē以外でも汎用に利用できる独立したライブラリとして、GitHubで公開されています。Mastodon互換のREST APIは、このフレームワークを利用して実装されています。
ActivityPubプロトコルとは?
Takahēサーバーが提供する2つ目のAPIは、ActivityPub APIです。このAPIは、TakahēサーバーがMastodonやMisskeyなどの他のFediverseサーバーと通信するために利用されます。このときサーバー間の通信で利用されるプロトコルが、Fediverseの基盤となっている
ActivityPubプロトコルは、複数のサーバー間でフォローや投稿などのアクティビティ
マイクロブログサービスの場合には、具体的なアクティビティの種類として、投稿のアクティビティNote
、いいねのアクティビティLike
、フォローのアクティビティFollow
などがあり、サーバー間で共通のアクティビティとして利用されます。
たとえば、AさんがBさんをフォローすると、AさんのサーバーからBさんのサーバーに対して、Follow
する』」という意味のJSONオブジェクトが届けられます。BさんがAさんからのフォローを承認すると、今度はBさんのサーバーからAさんのサーバーに対して、Follow
アクティビティ』Accept
する』」というJSONオブジェクトが返信されます。このようにして、ActivityPubプロトコルにしたがった、一連のJSONオブジェクトがサーバー間で送受信されることにより、ActivityPubサーバーとして動作するサーバーが実現します。
オープンなプロトコルを実装した複数の独立したサーバー間でメッセージが送られるさまは、メールサーバーによるメールの送受信に似ているかもしれません。ActivityPubの場合には、アクターの受信ボックス
具体例として、Takahē公式アカウント@takahe@takahe.
)@mastodon@mastodon.
)Follow
アクティビティをAccept
するアクティビティ」
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Accept",
"id": "https://jointakahe.org/@takahe@jointakahe.org/#accept/1",
"actor": "https://jointakahe.org/@takahe@jointakahe.org/",
"object": {
"type": "Follow",
"id": "https://mastodon.social/0d4a4320-d807-4c56-9936-dd265fefea34",
"actor": "https://mastodon.social/users/Mastodon",
"object": "https://jointakahe.org/@takahe@jointakahe.org/"
}
}
ActivityPubの仕様の詳しい解説とPerlでの実装例については、連載記事
ActivityPubサーバーとして振る舞うStatorワーカー
ActivityPubの他のサーバーとのやり取りでは、処理に時間がかかったり通信が失敗したりする可能性があります。このような処理を行いたいときは、一般にバックグランド処理を非同期で実行するタスクキューシステムが利用されます。DjangoではCeleryと呼ばれるタスクキューフレームワークがRedisと合わせて利用されることが多く、実際、同じくDjangoで書かれた書評サービスのActivityPubサーバーBookwyrmでも、このCeleryとRedisを組み合わせた構成が採用されています。
ところが、Takahēはこれとはまったく異なるアプローチを取り、reconciliation loop
ノート:reconciliation loop
「調整ループ」
たとえば、目標の状態として
Kubernetesでは大規模にスケールするように互いに独立した多数のリソースとコントローラーから構成されますが、Takahēの調整ループではTakahēで必要とされる処理に合わせて、1つのDjangoアプリの中に独自の形でコンパクトに実装されています。
Takahēの調整ループ
Takahēの調整ループにおける
Statorワーカーはループ処理の中で、データベースに保存されたStatorオブジェクトの中から処理の準備ができている状態のオブジェクトを取り出します。それぞれのStatorオブジェクトには現在のステートが記録されているので、Statorワーカーはそのステートに対応するハンドラメソッドを選んで実行して、必要な処理を実行します。処理が完了したら、ハンドラメソッドから次に遷移するべきステートが返されるので、データベース内のStatorオブジェクトのステートを新しいステートに更新します。こうして保存されたStatorオブジェクトは、再び次のStatorワーカーがループ処理で自分を取り出すのを待つことになります[6]。
Follow
のStatorモデル
「フォロー」Follow
を例にとって、Statorモデルとその内部のステートグラフが具体的にどのように定義されているのかを見てみましょう。次に示すのは、FollowのStatorモデルの模式図です。
このようなモデルが、データベース上では1つのレコードとして記録されています。Follow
Statorモデル内の右側のステートグラフは、具体的には以下のようなクラスとして定義されています[7]。
前半のブロックでは、グラフの各ノードに対応する3つのステートが定義されています。unrequested
はフォローリクエストを受けた状態、accepting
はフォローの許可が進行中である状態、accepted
はフォローの許可が完了した状態をそれぞれ表現しています。
後半のブロックでは、矢印のグラフのエッジに相当する状態遷移が定義されています。unrequested
はaccepting
に遷移でき、accepting
はaccepted
に遷移できることを表現しています。
次に、このFollowStates
をステートとして持つFollow
Statorモデル本体のコードを見てみましょう。
id
、source
、target
は、通常のDjangoモデルのフィールドです。それぞれ、Follow
オブジェクトのID、フォロー元アカウントとフォロー先アカウント名は、それぞれFediverseアカウントを表現するIdentity
Statorモデルを外部キーとして参照しています。そして、先ほど定義したFollowStates
が特殊なstate
フィールドとしてモデルに設定されています。
Follow
Statorモデルのステートハンドラとして、ここではaccepting
ステートを処理するハンドラメソッドhandle_
を挙げました。このハンドラメッソでは、Takahēユーザーが他のアカウントからのフォローリクエストを許可した後に実行される処理が定義されています。はじめにFollow
Statorモデルのメソッド.to_
でフォロー許可を表すAccept
アクティビティのJSONオブジェクトを作成し、フォロー元アカウントのサーバーのInboxに送信した後、次に遷移するべき状態であるaccepted
を返します。Statorはこの結果を受けて、Follow
ステートを次の状態に進めます。Statorがこのようなステートを処理するハンドラメソッドと一連のステートの遷移を繰り返し実行することで、Statorオブジェクトが適切に処理されていきます。
Follow
以外にも、ドメイン、投稿、絵文字、Inboxのアイテムを表現するStatorモデルとして、それぞれDomain
、Post
、Emoji
、InboxMessage
などのStatorモデルが定義されています。そして、それぞれのStatorモデルには、適切な処理が行われるように固有のステートグラフが上手く定義されています。 このようにして、TakahēサーバーとStatarワーカーは協調して動作し、内部で必要なデータ処理を行ったり、ActivityPubプロトコルにしたがって外部サーバーと適切に通信することで、Fediverseの中でActivityPubサーバーとして振る舞えるようになっているわけです。
Takahēのアーキテクチャの優れた点
Takahēでは追加のタスクキューシステムを使う代わりに、Stator向けにモデル化されたすべての状態
また、外部のサーバー障害により通信が失敗したり、ワーカープロセスがクラッシュして処理が失敗したとしても、単純に新しいStatorワーカーを起動して、データベースに保存された状態を元に簡単にリトライができるようになっています。
ノート:SQLite対応のドロップ:
Takahēは当初、Djangoでよく利用されるPostgreSQLデータベースに加えて、SQLiteデータベースも実験的にサポートしていました。SQLiteはファイルシステム上の単一ファイルとしてデーモンプロセスなしで利用できるため、特に省リソースで利用するときには大きな利点があります。しかし、ActivityPubオブジェクトの処理に便利なJSON関数の対応不足や、PostgreSQLで利用できるテキスト検索用インデックスへの未対応など、Takahēのユースケースに必要な機能が足りないことが多く、残念ながらTakahēバージョン0.
フロントエンドでのhtmxと_hyperscriptの活用
Takahēのフロントエンドでは、近年Djangoコミュニティで話題になることが多い2つのライブラリ
ハイパーメディアのアイデアでHTMLを拡張するhtmx
htmxは、ハイパーメディアの思想を推し進め、いくつかの属性を追加するだけでHTML要素を拡張して、ページに簡単にマイクロインタラクションが追加できる軽量なJavaScriptライブラリです。
Takahēでの利用例の1つは、タイムラインのページネーションです。上記のコードは、タイムラインの投稿リストとページネーションのボタン部分のHTMLを抜粋したものです。htmxの動作は、hx-
というprefixが付いた属性により定義できます。この例では、タイムライン1ページ目のhref=".?page=2"
)、取得されたページの投稿リストと新しいhx-select=".page-content"
)hx-target
とhx-swap
で指定された.pagination
のouterHTML
部分と置換されます。
Djangoの典型的なページネーションの実装では、2ページ目を表示するためにページ全体のリロードが行われますが、このようにhtmxを利用すれば、ページ全体の遷移やリロードを行うことなく、次のページの投稿リストを読み込めます。
このTakahēの例では、ページネーションの実装をシンプルにするために、元と同じページ全体のHTMLを取得して一部だけを利用するような実装になっています。しかし、Djangoの柔軟なテンプレートエンジンを活用すれば、たとえばコンポーネントのような小さな単位でHTMLスニペットを動的にレンダリングして送信するなど、htmxは工夫次第でさまざまな活用方法が考えられます。
JavaScriptを使用せずに僅かな記述だけでシングルページアプリケーションのような体験を簡単にインクリメンタルに導入でき、Djangoの柔軟なテンプレートシステムとサーバーサイドレンダリングとも親和性が高いことが、Djangoコミュニティで人気になっている理由だと考えられます。
自然言語風の記述でHTMLを操作できる_hyperscript
同じ作者による_hyperscriptも、最小限のJavaScriptでHTML上にマイクロインタラクションを追加できる面白いライブラリです。_hyperscriptと呼ばれる自然言語に似た独自のプログラミング言語ランタイムが内部に実装されていて、HTMLの操作をこの言語で記述します。言語の文法デザインは、HyperTalkと呼ばれるプログラミング言語の影響を受けていて、macOSユーザーであれば、同じ言語の影響を受けているAppleScriptと似ていることに気づくかもしれません。
ここでは、Takahēのプロファイル上のボタンを一例として挙げます。_hyperscriptのコードは、HTML要素の_
属性の値としてHTML内に記述します。ここで<a>
で作られたボタンをクリックすると、クリップボードにユーザー名がコピーされ、テキストがコピーされたことを示すためにボタンの色が2秒間変更されます。
自然言語
_="コピーアイコンをクリックすると
アカウント名のテキストをクリップボード(`navigator.clipboard`)に書き込み
`copied` クラスを追加し
2秒待ち
そして `copied` クラスを削除する"
_hyperscriptでできるHTML要素の操作はjQueryでできることに近いですが、_hyperscriptはコードのローカリティの重要性を強調しています。jQueryが操作するHTML要素から離れたJavaScript上に動作を記述する必要があるのに対して、_hyperscriptでは操作対象のHTML要素やそのすぐ近くの要素自体にコードを書きます。これにより、自然言語風の文法と合わせて、コードを読むときの認知的な負荷が下がり、コードの読み書きが容易になっています。
Takahēサーバーを動かす
Takahēのアーキテクチャと関連ライブラリを紹介してきましたが、ここからは、実際にdocker-compose
を利用して手元でTakahēサーバーを実行する方法を紹介します。
ただし、Elkなどのクライアントアプリや他のActivityPubサーバーと通信するためには、HTTPS接続が可能なパブリックなサーバーが必要になるため、ローカル環境でできるのはTakahēサーバー内でのやり取りに限られます。そのような環境を用意するのが難しい場合、後述のGitpodの一時環境を利用することもできます。
docker-compose
で実行する
前提条件として、以下の条件を確認してください。
git
がインストールされていること- Dockerがインストールされていること
(または、PodmanなどのOCI互換のコンテナエンジン) docker-compose
コマンドが利用できること
レポジトリと設定ファイルの準備
まずは、GitHubからTakahēのGitレポジトリを取得しましょう。
git clone https://github.com/jointakahe/takahe cd takahe/
今回使用するDockerfile
やdocker-compose.
ファイルは、docker/
フォルダ以下に置かれています。以下にdocker-compose.
の主要な部分の抜粋を示します。
version: "3.4"
x-takahe-common:
&takahe-common
image: takahe:latest
environment:
TAKAHE_DATABASE_SERVER: "postgres://postgres:insecure_password@db/takahe"
TAKAHE_DEBUG: "true"
TAKAHE_SECRET_KEY: "insecure_secret"
TAKAHE_CSRF_HOSTS: '["http://127.0.0.1:8000", "https://127.0.0.1:8000"]'
TAKAHE_USE_PROXY_HEADERS: "true"
TAKAHE_EMAIL_BACKEND: "console://console"
TAKAHE_MAIN_DOMAIN: "example.com"
TAKAHE_ENVIRONMENT: "development"
GUNICORN_EXTRA_CMD_ARGS: "--reload"
...
services:
db:
image: docker.io/postgres:15-alpine
...
web:
<<: *takahe-common
ports:
- "8000:8000"
...
stator:
<<: *takahe-common
command: ["/takahe/manage.py", "runstator"]
...
...
はじめのx-takahe-common:
のセクションでは、TakahēサーバーコンテナとStatorコンテナで共通に利用される設定が書かれています。takahe:latest
は、Docker Hubで公開されている安定版のTakahēコンテナです。その後に多くの環境変数の設定が並んでいますが、Takahēの主要な設定はすべて環境変数経由で設定できるようになっています。自分でサーバーを公開する際にはこれらの値を変更します。
services:
のセクションにはdb
web
stator
注意:安全ではないデフォルト設定:
Gitリポジトリ上のdocker-compose.
ファイルは、Takahēの開発に適した設定で参考用に提供されているものです。デフォルトの設定は、安全でないパスワードが使われていたり、エラー時にデバッグ情報が表示される開発モードdevelopment
)
ノート:画像のアップロード:
デフォルトのdocker-compose.
には画像関連の設定が含まれていないため、画像のアップロードや表示が上手くできません。画像を試したい場合は、environment
のセクションに以下の設定を追加してください。
TAKAHE_MEDIA_BACKEND: "local://"
TAKAHE_MEDIA_ROOT: "/takahe/media/"
TAKAHE_MEDIA_URL: "http://<server-domain-name>/media/"
<server-domain-name>
部分は、アクセスするサーバーのURLに合わせて127.
などに置換する必要があります。
これにより、コンテナ内のローカルディレクトリに画像が保存されるようになります。また、TakahēではDjangoのプラグインdjango-storage
を利用しているため、Google Cloud Storage、Amazon S3、Minioなどのオブジェクトストレージを画像などのバックエンドとして簡単に利用できるようになっています。
Takahēサーバーを起動するには、takahe/
ディレクトリ上で次のコマンドを実行します。
docker-compose -f docker/docker-compose.yml up
このコマンドだけで、メインの3コンポーネントが起動して、さらにデータベースのマイグレーションなどの初期設定も自動的に実行されます。サービスが起動したら、アクセスする前にTakahēにログインするための管理者ユーザーを作成します。次のコマンドを実行してください。
docker-compose -f docker/docker-compose.yml exec web -- python -m manage createsuperuser
このコマンドは、Takahēサーバーweb
)
ユーザーが作成できたら、ブラウザを開いてhttp://
にアクセスします。作成したアカウントでTakahēにログインします。
初期状態ではドメインが存在しないため、アカウント作成前にドメインを追加する必要があります。ドメイン名には、docker-compose.
で設定したexample.
を入力しますlocalhost:8000
ではないことに注意してください)。
これで@example.
ドメインを持つアカウントが自由に作成できるようになりました。
なお、クライアントアプリが利用できない場合でも、設定画面から簡易的な投稿フォームが利用できるため、ここから実験的な投稿を行うことが可能です。複数のユーザーを作成して、投稿やフォローなどを試してみてください。
Gitpod上で実行する
Gitpodは、ブラウザ上ブラウザ上でLinux開発環境を提供するクラウドサービスです。最近ではGitHubがGitHub Codespaceという競合サービスを提供していますが、Gitpodはこの分野での先駆的な存在です。Gitpodを利用するとHTTPS接続ができる一時的なサブドメインが使えるようになるので、開発環境上でdocker-compose
を利用してTakahēサーバーを実行することで、クライアントアプリからの接続なども実験できるようになります。執筆時点でGitpodは月50時間まで無料で利用可能ですが、以下の手順を実行するにはGitpodのアカウント作成が必要なことに注意してください。
注意:Gitpodの一時的な開発環境:
Gitpodが提供する環境は一定時間経つと自動的にシャットダウンしてしまうため、Gitpodの開発環境で作ったアカウントは実験目的で自分で利用することに留めてください。他の公開ActivityPubサーバーと接続してしまうと、相手のサーバーに接続不可能なサーバーと通信させてエラーを起こさせることになってしまいます。
Gitpodの構成ファイルが置かれたGitHubリポジトリを開き、
開発環境のマシン性能の選択画面に移動しますが、デフォルトの設定で問題ないためそのまま
開発環境が開くと、用意されたGitpodのセットアップスプリプトが自動的に実行されます。このスクリプト内では、上記と同様にdocker-compose
でTakahēを実行しますが、それに加えて、Gitpod環境に合わせたドメイン名の設定なども自動化されています。スクリプトの実行には2、3分ほど時間がかかります。
スクリプトの実行が最後まで実行されると、8000番ポートでアクセス可能になったTakahēのウェブページがプレビュータブに表示されるはずです。このページは独立した新しいタブでも開けます。
この時点では、管理者ユーザーが存在しないため、前に説明した方法で管理者ユーザーを作成してログインします。
Kubernetesクラスターにデプロイする
Kubernetesの知識がある人なら、Secret、ConfigMap、PostgreSQLを用意した後、サーバーのコンテナとStatorのコンテナをそれぞれDeploymentで作るすることで、Kubernetesクラスターに簡単にデプロイできます。
実際、takahe.
Kubernetesの構成ファイルは、GitHubの別のリポジトリ
おわりに
この記事が、新しいアイデアを得たり、新しい場所を自分で作るきっかけになれば幸いです。