DIY ドラマを捨てよう:なぜ ActivityPub を一から構築するのではなく Fedify を使うべきなのか?

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

あなたは ActivityPub のようなプロトコルで動作する分散型ソーシャルウェブ、フェディバースに魅了されているかもしれません。次世代の素晴らしい連合型アプリ、MastodonLemmyPixelfed などに接続された独自のスペースを構築することを夢見ているのかもしれません。一から自分で ActivityPub を実装したいという誘惑は強いものです。完全なコントロール、すべてのバイトを理解する...クールに聞こえますよね!

でも、ちょっと待ってください。その壮大な冒険に乗り出す前に、現実について話しましょう。ActivityPub を正しく実装することは単なる一つのタスクではありません。それは一輪車に乗りながら目隠しをして、いくつもの複雑な標準をジャグリングするようなものです。難しいのです。

そこで Fedify の出番です。これは ActivityPub 開発の最も厄介な部分を処理するために設計された TypeScript フレームワークで、フェデレーションの車輪を再発明するのではなく、あなたのアプリを特別にする部分に集中できるようにします。

この投稿では、DIY ActivityPub 実装の一般的な頭痛の種を分解し、Fedify がどのようにスーパーパワーの痛み止めとして機能するかを、データがどのように表現されるかという基本から始めて説明します。

課題 :データモデリング—ActivityStreams と JSON-LD を流暢に話す

ActivityPub の核心部分は、アクションとオブジェクトを記述するために ActivityStreams 2.0 ボキャブラリー に依存し、このボキャブラリーをエンコードするための構文として JSON-LD を使用しています。この組み合わせは強力ですが、最初から大きな複雑さをもたらします。

まず、膨大な ActivityStreams ボキャブラリーを理解し正しく使用することは障壁です。投稿(NoteArticle)、プロフィール(PersonOrganization)、アクション(CreateFollowLikeAnnounce)など、すべてを仕様で定義された正確な用語とプロパティを使用してモデル化する必要があります。手動での JSON 構築は退屈でエラーが発生しやすいです。

第二に、エンコーディング層である JSON-LD には、直接の JSON 操作を驚くほど難しくする特定のルールがあります:

  • 欠落配列と空配列の違い: JSON-LD では、プロパティが存在しない場合と、空配列として存在する場合は、意味的に同一であることが多いです。値をチェックする際、アプリケーションロジックはこれらのケースを同等に扱う必要があります。例えば、以下の2つの Note オブジェクトは name プロパティに関して同じ意味を持ちます:
    // name プロパティなし
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": ""
    }
    // 以下と同等:
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "name": [],
      "content": ""
    }
  • 単一値と配列の違い: 同様に、単一の値を直接保持するプロパティは、その値を含む単一要素の配列を保持するのと同等であることが多いです。コードは同じ意味を持つ両方の表現を予測する必要があります。例えば、ここでの content プロパティのように:
    // 単一値
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": "Hello"
    }
    // 以下と同等:
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": ["Hello"]
    }
  • オブジェクト参照と埋め込みオブジェクト: プロパティには、完全な JSON-LD オブジェクトを直接埋め込むか、そのオブジェクトを参照する URI 文字列のみを含めることができます。アプリケーションは URI のみが与えられた場合、そのオブジェクトのデータを取得する準備が必要です(逆参照と呼ばれるプロセス)。これらの2つの Announce アクティビティは意味的に同等です(URI が正しく解決されると仮定):
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Announce",
      // 埋め込みオブジェクト:
      "actor": {
        "type": "Person",
        "id": "http://sally.example.org/",
        "name": "Sally"
      },
      "object": {
        "type": "Arrive",
        "id": "https://sally.example.com/arrive",
        /* ... */
      }
    }
    // 以下と同等:
    {
      "@context":
      "https://www.w3.org/ns/activitystreams",
      "type": "Announce",
      // URI 参照:
      "actor": "http://sally.example.org/",
      "object": "https://sally.example.com/arrive"
    }

アプリケーション全体でこれらのボキャブラリールールと JSON-LD のバリエーションをすべて手動で一貫して処理しようとすると、必然的に冗長で複雑で脆弱なコードになり、フェデレーションを破壊する微妙なバグが発生しやすくなります。

Fedify はこのデータモデリングの課題全体を、包括的な型安全な Activity ボキャブラリー API で解決します。ActivityStreams タイプと一般的な拡張機能のための TypeScript クラスを提供し、オートコンプリートとコンパイル時の安全性を提供します。重要なのは、これらのクラスが内部的にすべての厄介な JSON-LD のニュアンスを管理することです。Fedify のプロパティアクセサは一貫したインターフェースを提供します—非機能的プロパティ(tags など)は常に配列を返し、機能的プロパティ(content など)は常に単一の値または null を返します。オブジェクト参照と埋め込みオブジェクトの違いは、逆参照アクセサactivity.getActor() など)を通じてシームレスに処理され、必要に応じて URI を介してリモートオブジェクトを自動的に取得します—これは プロパティハイドレーション として知られる機能です。Fedify を使用すると、クリーンで予測可能な TypeScript API で作業でき、AS ボキャブラリーと JSON-LD エンコーディングの厄介な詳細はフレームワークに任せることができます。

課題 :発見とアイデンティティ—アクターを見つける

データをモデル化できたら、アクターを発見可能にする必要があります。これは主に WebFinger プロトコル (RFC 7033) を含みます。/.well-known/webfinger にサーバーエンドポイントを構築し、リソースクエリ(acct: URI など)を解析し、リクエストされたドメインをサーバーに対して検証し、正確にフォーマットされた JSON リソース記述子(JRD)で応答する必要があります。この JRD には、正しいメディアタイプを使用してアクターの ActivityPub ID を指す self リンクなど、特定のリンクを含める必要があります。これらのいずれかの部分が間違っていると、アクターが見えなくなる可能性があります。

Fedify はこれを大幅に簡素化します。setActorDispatcher() メソッドを通じて提供するアクター情報に基づいて、WebFinger リクエストを自動的に処理します。Fedify は正しい JRD レスポンスを生成します。ユーザー向けハンドルを内部識別子にマッピングするなど、より高度な制御が必要な場合は、mapHandle() または mapAlias() コールバックを簡単に登録できます。アクターの定義に集中すれば、Fedify がそれらを発見可能にします。

// 例:アクターを見つける方法を定義する
federation.setActorDispatcher(
  "/users/{username}",
  async (ctx, username) => { /* ... */ }
);
// これで GET /.well-known/webfinger?resource=acct:username@your.domain が動作します!

課題 :Core ActivityPub メカニクス—リクエストとコレクションの処理

アクタープロファイルの提供には、慎重なコンテントネゴシエーションが必要です。アクターの ID へのリクエストは、マシンクライアント向けには JSON-LD(Accept: application/activity+json)が必要ですが、ブラウザ向けには HTML(Accept: text/html)が必要です。インボックスエンドポイントでの受信アクティビティの処理には、POST リクエストの検証、暗号署名の検証、ペイロードの解析、重複の防止(べき等性)、アクティビティタイプに基づくルーティングが含まれます。正しいページネーションを持つコレクションoutboxfollowers など)の実装は、さらに別のレイヤーを追加します。

Fedify はこれらすべてを合理化します。そのコアリクエストハンドラー(Federation.fetch() またはフレームワークアダプターである @fedify/express を介して)はコンテントネゴシエーションを管理します。setActorDispatcher() でアクターを定義し、フレームワーク(HonoExpressSvelteKit など)でウェブページを定義すると、Fedify が適切にルーティングします。インボックスについては、setInboxListeners() を使用してアクティビティタイプごとにハンドラーを定義でき(例:.on(Follow, ...))、Fedify は KV Store を使用して検証、署名検証、解析、べき等性チェックを自動的に処理します。コレクションの実装はディスパッチャー(例:setFollowersDispatcher())を介して簡素化されます。データのページを取得するロジックを提供すると、Fedify はページネーションを含む正しい Collection または CollectionPage を構築します。

// インボックスハンドラーを定義
federation.setInboxListeners("/inbox", "/users/{handle}/inbox")
  .on(Follow, async (ctx, follow) => { /* フォローを処理 */ })
  .on(Undo, async (ctx, undo) => { /* 取り消しを処理 */ });

// フォロワーコレクションロジックを定義
federation.setFollowersDispatcher(
  "/users/{handle}/followers",
  async (ctx, handle, cursor) => { /* ... */ }
);

課題 :信頼性の高い配信と非同期処理—アクティビティを堅牢に送信する

アクティビティの送信は単純な POST 以上のものが必要です。ネットワークは失敗し、サーバーはダウンします。堅牢な失敗処理と再試行ロジック(理想的にはバックオフを伴う)が必要です。受信アクティビティを同期的に処理するとサーバーがブロックされる可能性があります。多くのフォロワーへの効率的なブロードキャスト(ファンアウト)には、バックグラウンド処理と可能な場合は共有インボックスの使用が必要です。

Fedify は MessageQueue 抽象化 を使用して信頼性とスケーラビリティに対応します。設定されている場合(強く推奨)、Context.sendActivity() は配信タスクをキューに入れます。バックグラウンドワーカーは、設定可能なポリシー(outboxRetryPolicy など)に基づいて 自動再試行 を伴う送信を処理します。Fedify はさまざまなキューバックエンド(Deno KV、Redis、PostgreSQL、AMQP)をサポートしています。高トラフィックのファンアウトのために、Fedify は負荷を効率的に分散するための 最適化された二段階メカニズム を使用します。

// 永続的なキュー(例:Deno KV)で Fedify を設定
const federation = createFederation({
  queue: new DenoKvMessageQueue(/* ... */),
  // ...
});
// 送信は信頼性が高く、ブロッキングしません
await ctx.sendActivity({ handle: "myUser" }, recipient, someActivity);

課題 :セキュリティ—一般的な落とし穴を避ける

ActivityPub サーバーのセキュリティ確保は重要です。サーバー間認証のために HTTP 署名 (draft-cavage-http-signatures-12) を実装する必要があります—これは複雑なプロセスです。データの整合性と互換性のために Linked Data 署名 (LDS) または FEP-8b32 に基づくオブジェクト整合性証明 (OIP) も必要かもしれません。暗号鍵を安全に管理することは不可欠です。最後に、リモートリソースの取得は、適切に検証されていない場合 サーバーサイドリクエストフォージェリ (SSRF) のリスクがあります。

Fedify はセキュリティを念頭に設計されています。setKeyPairsDispatcher() を介して鍵を提供すれば、HTTP 署名、LDS、OIP の作成と検証を自動的に処理します。鍵管理ユーティリティも含まれています。重要なことに、Fedify のデフォルトのドキュメントローダーには 組み込みの SSRF 保護 が含まれており、明示的に許可されていない限りプライベート IP へのリクエストをブロックします。

課題 :相互運用性とメンテナンス—他との良好な連携

フェディバースは多様です。異なるサーバーには癖があります。互換性を確保するにはテストと適応が必要です。標準は新しい Federation Enhancement Proposals (FEPs) とともに進化します。また、サーバー機能を公開するための NodeInfo のようなプロトコルも必要です。

Fedify は 広範な相互運用性 を目指し、積極的にメンテナンスされています。実装の違いを滑らかにするための ActivityTransformer のような機能が含まれています。NodeInfo のサポートは setNodeInfoDispatcher() を通じて組み込まれています。

課題 :開発者エクスペリエンス—実際にアプリを構築する

プロトコルを超えて、どんなサーバーを構築するにもセットアップ、テスト、デバッグが必要です。フェデレーションでは、デバッグがより難しくなります—メッセージの形式が間違っていたのか?署名が間違っていたのか?リモートサーバーがダウンしているのか?互換性の癖なのか?良いツールは不可欠です。

Fedify は開発者エクスペリエンスを大幅に向上させます。TypeScript で構築されているため、優れた型安全性とエディタの自動補完を提供します。fedify CLI は、一般的な開発タスクを合理化するように設計された強力なコンパニオンです。

fedify init を使用して、選択したランタイムとウェブフレームワークに合わせた新しいプロジェクトを迅速にスキャフォールディングできます。

インタラクションのデバッグとデータの検証には、fedify lookup が非常に価値があります。WebFinger ディスカバリーを実行し、オブジェクトのデータを取得することで、任意のリモートアクターやオブジェクトが外部からどのように見えるかを検査できます。Fedify はターミナルに直接解析されたオブジェクト構造とプロパティを表示します。例えば、次のように実行すると:

$ fedify lookup @fedify-example@fedify-blog.deno.dev

まず進行メッセージが表示され、次にアクターの構造化された表現が出力されます:

// fedify lookup コマンドの出力(解析されたオブジェクト構造を表示)
Person {
  id: URL "https://fedify-blog.deno.dev/users/fedify-example",
  name: "Fedify Example Blog",
  published: 2024-03-03T13:18:11.857Z, // 簡略化されたタイムスタンプ
  summary: "This blog is powered by Fedify, a fediverse server framework.",
  url: URL "https://fedify-blog.deno.dev/",
  preferredUsername: "fedify-example",
  publicKey: CryptographicKey {
    id: URL "https://fedify-blog.deno.dev/users/fedify-example#main-key",
    owner: URL "https://fedify-blog.deno.dev/users/fedify-example",
    publicKey: CryptoKey { /* ... CryptoKey の詳細 ... */ }
  },
  // ... inbox、outbox、followers、endpoints などの他のプロパティ ...
}

これにより、データがどのように構造化されているかを簡単に確認したり、Fedify が解析した実際のプロパティを見ることで、インタラクションが失敗している理由をトラブルシューティングしたりできます。

開発中のアプリケーションからの送信アクティビティのテストは、fedify inbox を使用することでずっと簡単になります。このコマンドを実行すると、一時的なローカルサーバーが起動し、公にアクセス可能なインボックスとして機能し、メッセージを受信するために作成される一時的なアクターに関する重要な情報を表示します:

$ fedify inbox
✔ 一時的な ActivityPub サーバーが起動しました: https://<unique_id>.lhr.life/
✔ @<some_test_account>@activitypub.academy にフォローリクエストを送信しました。
╭───────────────┬─────────────────────────────────────────╮
│ Actor handle: │ i@<unique_id>.lhr.life                  │
├───────────────┼─────────────────────────────────────────┤
│   Actor URI:  │ https://<unique_id>.lhr.life/i          │
├───────────────┼─────────────────────────────────────────┤
│  Actor inbox: │ https://<unique_id>.lhr.life/i/inbox    │
├───────────────┼─────────────────────────────────────────┤
│ Shared inbox: │ https://<unique_id>.lhr.life/inbox      │
╰───────────────┴─────────────────────────────────────────╯

Web インターフェースは http://localhost:8000/ で利用可能です

次に、開発中のアプリケーションを設定して、提供された Actor inbox または Shared inbox URI にアクティビティを送信します。アクティビティが到着すると、fedify inboxリクエストが受信されたことを示す要約テーブルのみをコンソールに表示します:

╭────────────────┬─────────────────────────────────────╮
│     Request #: │ 2                                   │
├────────────────┼─────────────────────────────────────┤
│ Activity type: │ Follow                              │
├────────────────┼─────────────────────────────────────┤
│  HTTP request: │ POST /i/inbox                       │
├────────────────┼─────────────────────────────────────┤
│ HTTP response: │ 202                                 │
├────────────────┼─────────────────────────────────────┤
│       Details  │ https://<unique_id>.lhr.life/r/2    │
╰────────────────┴─────────────────────────────────────╯

重要なのは、受信したリクエストに関する詳細情報—完全なヘッダー(Signature など)、リクエスト本文(Activity JSON)、署名検証ステータスを含む—は fedify inbox が提供するウェブインターフェースでのみ利用可能ということです。この Web UI を使用すると、開発中に受信アクティビティを徹底的に検査できます。

受信したアクティビティとその詳細を表示する Fedify Inbox ウェブインターフェースのスクリーンショット。
Fedify Inbox ウェブ UI は、詳細なアクティビティ情報を表示する場所です。

単なる送信を超えて、ローカルマシンからライブフェディバースとのインタラクションをテストする必要がある場合、fedify tunnel を使用して、ローカル開発サーバー全体を一時的に安全に公開できます。このツールスイートは、フェデレーションアプリケーションの構築とデバッグのプロセスを大幅に容易にします。

結論:配管ではなく機能を構築しよう

ActivityPub プロトコル群を一から実装することは、信じられないほど複雑で時間のかかる取り組みです。複数の技術仕様への深い理解、暗号署名、セキュリティ強化、多様なエコシステムのニュアンスのナビゲーションが必要です。教育的ではありますが、フェデレーションアプリケーションの実際のユニークな機能を構築するプロセスを劇的に遅くします。

Fedify は、適切に設計され、安全で型安全な基盤を提供し、フェデレーションの複雑さ—データモデリング、ディスカバリー、コアメカニクス、配信、セキュリティ、相互運用性—を処理します。これにより、アプリケーションのユニークな価値とユーザーエクスペリエンスに集中できます。低レベルのプロトコルの詳細と格闘するのをやめて、フェディバースのビジョンをより速く、より確実に構築し始めましょう。Fedify を試してみてください!

始めるのは簡単です。まず、お好みの方法で Fedify CLI をインストール します。インストールしたら、fedify init your-project-name を実行して新しいプロジェクトテンプレートを作成します。

詳細については、Fedify チュートリアルFedify マニュアル をチェックしてください。楽しいフェデレーションを!

13
0
3

1 comment

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/019631c0-992b-729c-9f4d-c4bf1ffb2456 on your instance and reply to it.

1