2023-08-06 タグ大渋滞
2023-08-16 改修も最終段階
[今日のノースピ]
2023-08-17 ξ°( 'ヮ')
[おしらせ]
やっとできた
ばばーん! ……というわけで、このブログが ActivityPub に対応しました!
今のところの仕様はこんなとこかな。
- Mastodon(テスト環境)、Misskey(テスト環境、本番環境)からのリモートフォローを確認
- リプライは各セクションへのコメントとして反映
- いいねは各セクションへのコメントに 👍 の絵文字を追加
- ブースト(リノート)は各セクションへのコメントに 🔄 の絵文字を追加
- 絵文字リアクションは各セクションへのコメントにその絵文字を追加(Misskey のカスタム絵文字だとしても!)
今後似たようなことをやりたい人のために、ノウハウやら詰まった箇所やらは、またどこかで公表してきましょ。
2023-08-19 ActivityPub 対応(1)
[作ってみた]
新タグ誕生。 ここには、このブログを ActivityPub 対応にするまでに必要だったことをまとめます。 前編・中編・後編の3日間に分けています。
実装のステップ
2023年1月から私 SoLa4 は、脱 Twitter を掲げ、活動のメイン拠点をこのブログに移してきました。 その頃から考えていたのが、このブログの ActivityPub 対応。 この度、夏休みに十分な時間が取れることになったので、えいやと実装してみました。 実現しようと思ってたこと はひと通り実現できたので、今のところは満足してます。
まずは、Mastodon 公式の解説記事 や 海外の先人の解説 などを参考に、実装に必要なステップを以下の通りにまとめました。
- ブログの投稿を Note に変換する
- WebFinger と Actor を準備する
- Outbox への GET に応答する
- Inbox への POST を検証する
- Inbox に届いた Activity に応じた処理をする
- 相手方の Inbox に新しい投稿を配送する
- 届いた Like や返信をブログに表示する
- Mastodon や Misskey との連携を確認する
これ以降、それぞれのステップでのポイントを解説していこうかと思います。
※ 一応これで動いてますが、間違ってるところとかあればこっそり教えてください。
投稿を Note に変換する
Note オブジェクト
ActivityPub では、投稿やユーザ、それらに対する操作などは、すべて ActivityVocablary という仕様で定義される JSON-LD オブジェクトになっています。 これらのオブジェクトを GET/POST したり、あるいは応答として受け取ることで、情報をやりとりします。
投稿はこうしたオブジェクトのうち、Note オブジェクトに分類されます。 実装によって色々拡張フィールドを持っていたりもしますが、あくまで今回の目的はブログの情報を配信することなので、画像つきの投稿が扱えれば十分です。
一例として、8/8 の日記 を Note に変換した結果を以下に示します。 (以降、可読性のために整形してますが、実際にはスペース・改行はありません)。
1{ 2 "@context": [ 3 "https://www.w3.org/ns/activitystreams", 4 { 5 "@language": "ja" 6 } 7 ], 8 "type": "Note", 9 "id": "https://puyolayersbridge.sakura.ne.jp/ap/status.rb?id=20230808p02", 10 "published": "2023-08-08T13:24:28Z", 11 "to": [ 12 "https://www.w3.org/ns/activitystreams#Public", 13 "https://puyolayersbridge.sakura.ne.jp/ap/followers.rb" 14 ], 15 "attributedTo": "https://puyolayersbridge.sakura.ne.jp/ap/", 16 "content": "<p>【メローブリーズ&ザ・マタギ】 ヘリオス(沖縄/岩手)のメローブリーズと、ザ・マタギ。スタイルはそれぞれヴァイツェンとペールエール。</p><p><a href=\"https://puyolayersbridge.sakura.ne.jp/?date=20230808\">https://puyolayersbridge.sakura.ne.jp/?date=20230808</a></p>", 17 "attachment": [ 18 { 19 "type": "Document", 20 "url": "https://puyolayersbridge.sakura.ne.jp/images/20230808_0.jpg", 21 "width": 1200, 22 "height": 1800, 23 "mediaType": "image/jpeg" 24 } 25 ], 26 "source": { 27 "content": " ヘリオス(沖縄/岩手)のメローブリーズと、ザ・マタギ。スタイルはそれぞれヴァイツェンとペールエール。", 28 "mediaType": "text/plain" 29 } 30}
@context は、単に URL だけでも良いようですが、言語情報を @language に埋め込んでも良いということなので、今回はそうしています。 source は本来不要なフィールドですが、今回は投稿の更新が必要かどうかのチェックのために用意しています。 (tDiary は日単位で記事を登録する一方、投稿はセクション単位で管理しているため)
tDiary には RSS を配信する makerss.rb が標準プラグインとして添付されています。 それを参考に、各セクションを上記のような JSON-LD に変換する tDiary プラグインを最初に作成しました(約160行)。
※ 実際には @context は後付けしています。
Note の配信
オブジェクトには URL が id としてついていて、Note の場合は投稿へのパーマリンクも兼ねています。 例えば Misskey の場合は、「リモートで表示」を選択すると、その URL にジャンプします。
ということは、何も考えずに作った JSON-LD の置き場所をそのまま id にしてしまうと、ブラウザで見たときにちょっと残念な感じになります。 ACCEPT ヘッダに text/html が含まれている場合にはブラウザと判断して、該当記事にリダイレクトするのが良さそうです。
多分 .htaccess でも出来るような気がしますが、今回はラッパスクリプトを書く形で対応しました(いいねの一覧を埋め込む処理も加えたので、約60行)。
WebFinger と Actor を準備する
WebFinger
次に必要なのは、ユーザがいると他のサーバから認識してもらうことです。 といっても、/.well-known/webfinger という場所を探しに来るので、そこに所定のファイルを置く だけです。 ユーザが複数いる場合は、誰に対する問い合わせかを確認する必要がありますが、今回のように1人だけであれば手抜きができます。
ここの href 欄に書いた URL が、そのユーザを表す Actor オブジェクトへのリンクになります。
Actor オブジェクト
ということで、ユーザ情報は Actor オブジェクトとして定義されます。 これも Note のときと同様、ブラウザからのアクセスの(ACCEPT に text/html を含む)ときはブログのトップにリダイレクトし、 それ以外のときは JSON-LD を返すようにしておきます。
今回は、スクリプトで共通で使っている定数などを別ファイルで定義(約25行)した上で、 それらを使って出力する JSON-LD を組み立てるようにしています(約45行)。 結果として出力しているファイルは以下のとおりです。
1{ 2 "@context": [ 3 "https://www.w3.org/ns/activitystreams", 4 {"@language": "ja"} 5 ], 6 "id": "https://puyolayersbridge.sakura.ne.jp/ap/", 7 "type": "Person", 8 "preferredUsername": "under", 9 "inbox": "https://puyolayersbridge.sakura.ne.jp/ap/inbox.rb", 10 "outbox": "https://puyolayersbridge.sakura.ne.jp/ap/outbox.rb", 11 "following": "https://puyolayersbridge.sakura.ne.jp/ap/following.rb", 12 "followers": "https://puyolayersbridge.sakura.ne.jp/ap/followers.rb", 13 "discoverable": true, 14 "name": "はしのしたの SoLa4", 15 "summary": "「はしのした」の投稿内容をお送りします。", 16 "icon": { 17 "type": "Image", 18 "mediaType": "image/jpeg", 19 "url": "https://puyolayersbridge.sakura.ne.jp/img/icon_sola4.jpg" 20 }, 21 "publicKey": { 22 "id": "https://puyolayersbridge.sakura.ne.jp/ap/#main-key", 23 "owner": "https://puyolayersbridge.sakura.ne.jp/ap/", 24 "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n(長いので中略)\n-----END PUBLIC KEY-----" 25 }
見ての通りの項目が多いです。 inbox と outbox については後のセクションで解説します。
注意すべき点として、discoverable を設定しておかないと Misskey との連携がうまく行かないことがあるようです。 また、publicKeyPem には Mastodon 公式の解説記事 を参考に RSA の鍵ペアを作成し、公開鍵を貼り付けておきます。改行は \n に置き換えておきます。
フォロー、フォロワー一覧
Actor オブジェクトの中で、フォロー一覧は following、フォロワー一覧は followers に、それぞれリンク先を記載しておきます。 following は、こちらからは一切フォローしないので、単に固定の出力をするスクリプトになっています(約20行)。 followers は、認識しているフォロワーの人数を totalItems に、それらの Actor オブジェクトの ID 一覧を orderedItems に、それぞれ出力します(約25行)。
具体的にどういう出力をすればいいかは、上記のリンク先から直接見てもらったほうが早いかと思いますので、詳細は省略します。
2023-08-20 ActivityPub 対応(2)
[作ってみた]
このブログを ActivityPub 対応にするまでに必要だったことをまとめた記事の中編です。 Inbox や Outbox でどんな対応が必要なのかをまとめていきます。
Outbox への GET に応答
Outbox は各ユーザの投稿一覧を返す場所なのですが……。 後でわかったことですが、実際のところ、この項目をあまりマジメにやる必要はありませんでした。 というのも、Mastodon にしろ Misskey にしろ、負荷対策のために他サーバの投稿を Outbox から引っ張ってくることはなく、ユーザの投稿一覧として自サーバの Inbox に送られてきたものだけを保持・表示するようになっているようです。 とはいえ、作ってしまった(約75行)からには一応説明します。
following や followers と同様に、Outbox でどういうデータを返せば良いかは、リンクで示します。
tDiary では8桁の日付+セクション番号で記事に ID がつけられるので、月ごとにページを分けることにしました。 クエリ文字列に「month=●●」が 含まれていない場合 には、totalItems に全体の投稿数、first と last にそれぞれ最初と最後のページへのリンクを記載した、OrderedCollection オブジェクトを返しています。 含まれている場合 には、指定した月の Note をまとめた配列を orderedItems に、prev と next にそれぞれ前と次のページへのリンクを記載した、OrderedCollectionPage オブジェクトを返しています。
Inbox への POST を検証
ActivityPub を自力実装しようとすると、この辺りから難易度が急に跳ね上がります。 Inbox にオブジェクトを送る際には、なりすまし・改ざん防止のために電子署名やハッシュが必要になるためです。 当然、自身の Inbox にもこれらが付加されたオブジェクト、より具体的には様々な種類の Activity オブジェクト(以下アクティビティと表記します)が送られてきます。 Inbox での処理の前半部分では、HTTP ヘッダや本文を見て、これらが正しく作られているかを確認することになります。
Mastodon 公式にも Inbox 周辺の解説記事 があるのですが、残念ながら少し情報が古く、この通りに実装するだけだと、うまく行きません。 色々考慮する必要があったため、記述量も相応に多くなりました(約160行)。 具体的にどこに注意が必要かに重点を置きつつ、説明してみます。
リクエストの例
ここでは、動作検証のときに受信した Misskey からのいいね通知(Like アクティビティ)を例にしてみます。 Inbox への POST として、以下の HTTP ヘッダのついたリクエストが送られてきました。
1POST /PLB/ap/inbox.rb 2(関係ない部分は省略) 3Host: arle.plb.local 4Date: Thu, 17 Aug 2023 13:30:15 GMT 5Digest: SHA-256=HWpKdQdcRSdThb2/yrO9Fbp7FQJFe02GGbYd/QULaxg= 6Signature: keyId="https://amitie.plb.local/users/9iikqmda66#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="YJ0EBC(長いので中略)Fz/Q=="
この リクエストの本文 と、送信者の検証鍵(公開鍵) を、別途アップしておきます。 なお、検証環境ではブログが動作するサーバに arle.plb.local、Misskey が動作するサーバに amitie.plb.local と名前をつけています。
ポイントは Digest と Signature の2つのヘッダです。
Digest には、"SHA-256=" の文字列に続いて、リクエストの本文を SHA-256 でハッシュ化し、Base64 でエンコードした文字列が入ります。 自分で同じ処理をして得られた文字列が Digest ヘッダのものと一致しなければ、リクエストの本文に改ざんがあったと判断し、認証失敗となります。
Signature は、カンマ区切りで4つのパートに分かれています。
- keyId: 送信者の検証鍵(公開鍵)を含む Actor オブジェクトへのリンク
- algorithm: 電子署名に用いるアルゴリズム(rsa-sha256 固定と考えてよい)
- headers: 署名対象となるヘッダの組(スペース区切り)
- signature: 署名を Base64 でエンコードした文字列
algorithm のパートは、Misskey と連携する場合考慮が必要です。 受信時は単に無視しておけば良いのですが、送信時にこのパートをつけていないと Misskey に受け取ってもらえません。
また、Date ヘッダについても、現在時刻から極端に離れていないか確認しておくことが望ましいようです。 仮に第三者がリクエストを傍受できたとすると(その時点で色々まずいですが)、そのリクエストを後で再送する、いわゆるリプレイ攻撃が成立しうるからです。 Date や Digest ヘッダの値を署名対象に含めることで、これらの値の改ざんを防いでいます。
リクエストの検証の例
では、上記のリクエストが正しく作られたものかどうかを検証してみましょう。 コードの例を以下に示します。
1require 'net/http' 2require 'openssl' 3require 'base64' 4 5pem = File.open("public_key_sample.pem"){|f| f.read } 6json = File.open("like_activity_sample.json"){|f| f.read } 7str_to_sign = [ 8 "(request-target): post /PLB/ap/inbox.rb", 9 "date: Thu, 17 Aug 2023 13:30:15 GMT", 10 "host: arle.plb.local", 11 "digest: SHA-256=HWpKdQdcRSdThb2/yrO9Fbp7FQJFe02GGbYd/QULaxg=" 12].join("\n") 13signature_base64 = [ 14 "YJ0EBCo+Cxu4LHlJaMMNQPkKRm7HBHEmMeLX2AZmkEfkm3zz8GvY7fLdR7YGKnxg5a4FGA", 15 "LfQI3ZHEtp6UzYnSB885VwSVYVkJ5sTqiWxFizKuuYOvfRAVgVVzfRT8DlMt1EvBLfIkij", 16 "DByOTui+wmwgUIUSfjWic9zQwrIis8Qaj1sOxwJf1b20yOWUyJMCXUMzZTFZXMKv1OSD7t", 17 "oB0A2X0OwX31cMi9p22DZdUypxbiv7jm9IQUzawMWURNqRzPzC8D5Gkbm1PQekGDVXNkbD", 18 "lblfq0cpb1E2rOdS72Jb30o8ZyW56iI6IOA1N+MXUcr3bg8777DWHqcvM+Fz/Q==" 19].join("") 20 21digest = "SHA-256=" + Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(json)) 22puts "digest = #{digest}" 23 24pubkey = OpenSSL::PKey::RSA.new(pem) 25signature = Base64.decode64(signature_base64) 26result = pubkey.verify("sha256", signature, str_to_sign) 27puts "verification = #{(result) ? "PASS" : "FAIL"}"
str_to_sign が今回の署名対象の文字列です. 今回、Signature ヘッダの headers には (request-target) date host digest が含まれています。 (request-target) にはリクエストの方法(post)と対象を記載し、残りは受け取った HTTP ヘッダの値をそのまま入れます。 なお、Mastodon は content-type も署名対象にする 点に注意が必要です。手抜きをするとハマります。 今回 str_to_sign は定数として与えていますが、実際には受け取ったリクエストから都度作成しましょう。
あとは、リクエスト本文を SHA-256 にかけてハッシュ値が一致するか確認し、作成した署名対象の文字列を Actor オブジェクトから得られた検証鍵(公開鍵)で検証します。 検証に失敗した場合には、相手先には 401 Authorization Required を返しておきましょう。成功した場合は 200 OK なり 202 Accepted を返します。
Activity に応じた処理
Inbox での処理の後半部分は、届いたアクティビティの種類(type)を見て,それに応じた処理をすることです。 今回の目的は、少なくとも Mastodon と Misskey からフォロー可能にすることと、届いたいいね、ブースト/リノート、返信を記録・表示することでしたので、処理は大きく分けて以下の4種類としました。カッコ内はアクティビティの種類です。
- フォロー申請(Follow)
- 返信の投稿(Create)および修正(Update)
- いいね(Like)およびブースト/リノート(Announce)
- フォロー・いいね・ブースト/リノートの取り消し(Undo)および返信の削除(Delete)
これだけ処理の種類があると、流石に記述量も多くなりました(約220行)。 以下、それぞれで行った処理を説明します。
Follow アクティビティ
ActivityPub におけるフォローは、フォローする側が Follow アクティビティを送り、フォローされる側がフォローする側に Accept アクティビティを返送することで、成立となります。 動作検証のときに受信した Misskey からの Follow アクティビティを例として示します。@context は省略しています。
1{ 2 "id": "https://amitie.plb.local/follows/9iim49du5s", 3 "type": "Follow", 4 "actor": "https://amitie.plb.local/users/9iikqmda66", 5 "object": "https://arle.plb.local/PLB/ap/" 6}
フォローする側とされる側の Actor へのリンクがそれぞれ actor と object に入っています。当然ですが、object は自分を指しています。
Follow アクティビティを受け取ったら、まずは相手をフォロワーの一覧に登録します。 この際、後々 Follow に対する Undo(つまりフォロー解除)に対応できるように、このアクティビティの ID をフォロワーの一覧に登録したことを、Undo 用のデータファイルに保存しておくと良いでしょう。
そして、actor を自分にし、object に受け取った Follow アクティビティそのものを格納した、Accept アクティビティを作成します。 後で説明しますが、今回は Activity を相手先の Inbox に配送するためのスクリプトを別に用意しています。 ですので、実際はこの Accept アクティビティを作成・配信するよう配送キューに登録するにとどめています。 最後に配送スクリプトを起動して(後述)、フォローへの対応はおしまいです。
Create/Update アクティビティ
投稿やその更新は、対象の Note オブジェクトを object とした、Create や Update アクティビティとして送られてきます。 また、投稿が別の投稿への返信である場合は、返信先へのリンクがその Note オブジェクトの inReplyTo に登録されています。
ですので、これらのアクティビティへの対応は Create でも Update でも変わらず、以下の通りとなります。
- object の inReplyTo を見て、日記のどのセクションへの返信なのかを確認する
- actor のリンク先から、返信した Actor の情報(名前など)を取りに行く
- object の content を見て、適宜タグを削除するなどの下処理をする
- 各セクションのコメント一覧ファイルに、コメントを登録する
- Undo 用のデータファイルに、当該セクションにコメントを登録したことを保存する
Like/Announce アクティビティ
いいねは Like アクティビティとして、ブースト/リノートは Announce アクティビティとして、それぞれ送られます。 どちらの場合でも対象の投稿は object に登録されています。
そのため、やるべきことはどちらでも一緒で、かつ Create/Update のときとほとんど同じです。 異なるのは、object の inReplyTo のかわりに object そのものを確認することと、下処理の内容だけです。
Misskey には絵文字でリアクションする機能がありますが、これらも Like アクティビティの拡張として送られてきます。 具体的には、絵文字が Unicode の絵文字(😁のような)である場合には content にその絵文字が入っています。 また、カスタム絵文字の場合には、content には :hoge: のような Misskey の記法で書かれた絵文字の名前が入ります。
カスタム絵文字を含む Like アクティビティの例を以下に示します。
1{ 2 "type": "Like", 3 "id": "https://amitie.plb.local/likes/9iirjmcctc", 4 "actor": "https://amitie.plb.local/users/9iikqmda66", 5 "object": "https://arle.plb.local/PLB/ap/status.rb?id=20230817p02", 6 "content": ":rgb:", 7 "_misskey_reaction": ":rgb:", 8 "tag": [ 9 { 10 "id": "https://amitie.plb.local/emojis/rgb", 11 "type": "Emoji", 12 "name": ":rgb:", 13 "updated": "2023-08-17T13:17:37.587Z", 14 "icon": { 15 "type": "Image", 16 "mediaType": "image/png", 17 "url": "https://amitie.plb.local/files/ba81ebb2-db95-43bb-982d-d842dd7063f7" 18 } 19 } 20 ] 21}
tag 以下に書かれている画像の URL を拾ってくれば、Misskey のカスタム絵文字を表示することも可能です。
Undo/Delete アクティビティ
Follow, Like, Announce の取消は Undo アクティビティとして、Create の取消は Delete アクティビティとして、それぞれ送られてきます。 Undo アクティビティの object は取消対象のアクティビティ(ないしその URL)、Delete アクティビティの object は削除対象の Note オブジェクト(ないしその URL)です。
やるべきことは、Undo 用のデータファイルから対象がどこに登録されているかを確認した上で、登録した情報の削除を行う、ということになります。 ただここで気をつけないといけないのは、Misskey では Follow アクティビティの ID と、対応する Undo アクティビティの object が一致しない 場合があるということです。 理由は不明ですが、ともかく Follow に対する Undo で ID が一致するものが見当たらない場合には、actor でマッチングを試みる必要があります。
2023-08-21 ActivityPub 対応(3)
[作ってみた]
このブログを ActivityPub 対応にするまでに必要だったことをまとめた記事の後編です。 なんだか思ってた以上に長い記事になっちゃったぞ?
相手方の Inbox に投稿を配送
Create アクティビティ
Inbox に投稿を配送する場合は、先に作成した Note オブジェクトを object に格納した Create アクティビティを使います。 アクティビティもオブジェクトの一種である以上、ID(パーマリンク)が必要になりますので、Note のときと同様にラッパスクリプトを書く形で対応しました。 また、Follow アクティビティへの対応 で必要であった Accept アクティビティも、同じスクリプトで出力できるようにしています(約55行)。
これを相手方に送る際は、今度はこちらが HTTP ヘッダの一部に署名して、その内容を Signature ヘッダに記載する必要があります。 また、Misskey との連携にあたっては、Authorization ヘッダも必要です。内容は "Signature" + 半角スペース + Signature ヘッダの値とすれば OK です。
実際に署名付きのヘッダを作成するときに使っている関数(約30行)を、以下に示します。 PRIVATE_KEY には PEM 形式の署名鍵(秘密鍵)が入っています。 今更ですが、署名鍵(秘密鍵)はよそに漏れないように十分注意しましょう。
1def make_signed_header(json, uri_str) 2 # check URI 3 uri = URI.parse(uri_str) rescue nil 4 return nil if ! uri 5 6 # generate string to sign 7 host = uri.host 8 date = Time.now.utc.httpdate 9 targ = uri.path 10 targ += "?" + uri.query if uri.query && uri.query != "" 11 digest = "SHA-256=" + Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(json)) 12 str_to_sign = "(request-target): post #{targ}\nhost: #{host}\ndate: #{date}\ndigest: #{digest}" 13 14 # generate signature 15 keypair = OpenSSL::PKey::RSA.new(PRIVATE_KEY) 16 signature = Base64.strict_encode64(keypair.sign("sha256", str_to_sign)) 17 sig_header = "keyId=\"#{ID}#main-key\"," + 18 "algorithm=\"rsa-sha256\"," + 19 "headers=\"(request-target) host date digest\"," + 20 "signature=\"#{signature}\"" 21 22 # output as an HTTP POST request 23 req = Net::HTTP::Post.new(uri.request_uri) 24 req["Host"] = host 25 req["Date"] = date 26 req["Digest"] = digest 27 req["Signature"] = sig_header 28 req["Authorization"] = "Signature #{sig_header}" 29 req["Content-Type"] = "application/activity+json" 30 req["Accept-Encoding"] = "gzip" 31 req.body = json 32 req 33end
配送スクリプト
あとは記事が追加されたときに、対応する Create アクティビティを各フォロワーの Inbox に配送するだけです。 ただ、相手先のサーバが停止してしまっているときに、更新処理で長時間待たされるのは避けたいです。 配送は別スクリプトで行うと良いでしょう。 やるべきことは、配送キューに入っているアクティビティを、順番に配送先の Inbox に POST していくだけです(約75行)。
注意すべきは、CGI として呼ばれたスクリプトが終了しても、配送スクリプトが終了しないようにすること、つまり配送スクリプトを daemon にすることです。 Ruby には Process.daemon というメソッドがあるので、それを使えばいいかなと思っていたのですが、どうもうまく行きませんでした。 最終的には、こちらの記事に書かれていた方法を参考に、fork を二重に行う形で対応しています。
届いた Like や返信をブログに表示
ここまで行けば、あとは記録したリアクションをブログに表示するだけです。 ここで改めて、最初に作った tDiary プラグインが出てきます。 各セクションの終わりに、リアクション一覧を取得して表示する処理を追加していきます(約60行)。 この辺りは、動作確認と並行して作っていきました。
実際にどう見えるかを示すために、試しにセルフで反応を送ってみます。
以上で、現時点での実装をひと通り説明しきりました。 現時点でのコードの行数は全部で1,020行です。 時間があったらコードを整理してどこかに公開しようと思いますが、少し時間がかかりそうです。もうしばらくお待ちください。
Fediverse 上での反応(全2件)
SoLa4: 返信はこんな感じで表示されます。 (2023-08-22 01:31)
Mastodon/Misskey との連携確認
ひと通り個別のスクリプトの動作が確認できたら、あとは実際に連携してみます。 とはいえ、いきなり本番環境でやるのも怖いので、まずはローカル環境で試します。 たまたま Ubuntu がインストールされたミニ PC が転がっていたので、そちらに Docker で Mastodon や Misskey、Nginx を立ち上げました(リンク先は参考にしたページ)。これまでの例で出てきた amitie.plb.local がこのマシンです。 これと Apache が動いている開発用 PC(こちらが arle.plb.local)との間で、オブジェクトをやりとりしてみます。
話がややこしいのは、ActivityPub のやりとりには原則として HTTPS が必要というところです。 何らかの形で証明書をインストールし、通信相手が信頼できることを確認してもらわないといけません。 今回は mkcert でローカル認証局を作成し、それを各環境に入れていくことにします。
まず、Ubuntu 環境で mkcert のビルド済バイナリを入手します。 その後、以下の要領でローカル認証局や証明書を生成します(このページを参考にしました)。
1mkcert -install 2cd `mkcert -CAROOT` 3openssl pkcs12 -export -inkey rootCA-key.pem -in rootCA.pem -out rootCA.pfx 4cd 5mkcert arle.plb.local 6mkcert amitie.plb.local
ここで得られた rootCA.pem がサーバで、rootCA.pfx が Windows のクライアントで、それぞれ必要になります。 また、ホームディレクトリに各マシンの証明書が作成されます。 こちらは Nginx や Apache で使います。
次に、ローカル認証局を各マシンに登録していきます。
Windows の場合は、rootCA.pfx を取り出して、右クリック →「PFX をインストール」からインポートできます。 インポート先は「信頼されたルート証明機関」とします。
Ubuntu の場合は、rootCA.pem を拡張子 crt の適当な名前にリネームしてから、/usr/local/share/ca-certificates/ にコピーします。 その後、update-ca-certificates を実行すると、コピーしたファイルが自動的にローカル認証局として認識されます。いずれも要 sudo です。 なお、この手順は、Mastodon の docker を立てる際にも必要です。 Dockerfile の USER mastodon の切り替えの直前あたりに加えておきましょう。
Node.js は独自の証明書ストアを持っているようです。 こちらにローカル認証局を登録する場合は、rootCA.pem をコピーした先のパスを環境変数 NODE_EXTRA_CA_CERTS に書いておけば良いようです。 Misskey の docker を立てる際には、Dockerfile で NODE_ENV 環境変数 を指定しているあたりに一緒に書いておきましょう。
最後に、Mastodon や Misskey はデフォルトではローカル IP からの Inbox へのアクセスを弾いてしまうので、ローカル環境で動かす場合には許可が必要です。 Mastodon は .env.production の ALLOWED_PRIVATE_ADDRESSES という設定、 Misskey は .config/default.yml の allowedPrivateNetworks という設定で、 それぞれ許可する範囲(例えば 192.168.1.0/24)を指定します。 各環境の hosts でサーバ名が解決できるようにするのもお忘れなく。
Mastodon や Misskey がちゃんと動き出したら、デフォルトのアカウントを作成して、そこからフォロー、返信、いいね等、色々試してみましょう。 ひと通り問題ないことが確認できたら、本番環境に持って行って完了です。