ActivityPubサーバーに絵文字リアクション

自作実装のActivityPubサーバーで、フォローさんや投稿に対するリアクション「Like」(いいね)は実装済み。WEB拍手的な使い方、ちょっとした賛同をお相手に送るというリアクション。
これで十分だと思ってやってたんだけど。追悼など失意系の投稿に対して「いいね」はさすがに使えない。そんな時は、メンションを送ってお悔やみを投稿するという方法しかなくて、お相手との距離感的に言葉を選ぶのが難しいケースも多い。
Misskeyに代表されるFediverseのサーバー(インスタンス)では絵文字リアクションという方法がある。
「いいね」だけで足りないところを、絵文字で違うニュアンスのリアクションを送り合う。
言葉ではなくて、たぶんこういうふんわりしたちょっとした感情表現もコミュニケーション、SNSならでは、というやつだ。
てことで、
送信する場合限定で絵文字リアクションを実装することにした。
ちなみに、
受信する場合の絵文字リアクションは実装しない。良いリアクションも悪いリアクションも、等しくリアクションという認識なので気にしない。
ただ、
絵文字リアクション=EmojiReactはActivityPubの仕様には入ってないので、サーバーによって対応にばらつきがある。
「EmojiReact」というActivityは仕様にはないので、対応していないサーバーのことも考慮してのことだと思うけど「Like」のActivityに絵文字を仕込むことが慣例的(?)っぽい。
これだと、お悔やみ系の絵文字を送ったつもりが、絵文字に対応していないサーバーに届くと「いいね」や「ハート」になってしまう。
逆に(?)そういった事故を防ぐために「Like」ではなくて「EmojiReact」をActivityとして送れば、対応していないサーバーは「いいね」にすることもなく、Activityをスルーしてくれる。
というやりかたもあると教えていただいた。
どちらを採用するにしてもサーバーが絵文字に対応してるかどうか確認が必要となる。
サーバーが絵文字に対応してるかどうかは、たぶん「nodeinfo」に記載されているだろうから、nodeinfoを見てから送信する/しないの判定を入れるのが良いと思う。
…けど
1)nodeinfoを見てnodeinfo2.NのURIを取得する
2)nodeinfo2.Nを見てemojiの有無を確認する
という2つリクエストが必要になる、よなあ。
その都度リクエストを飛ばして、サーバーの対応状況確認については、ちょっと保留。
現状見た感じで、サーバーごとの絵文字対応状況は
絵文字非対応→「いいね」になるサーバー
→Mastodon系ほぼ全部
絵文字対応サーバー
→Misskey系(IceshrimpやFirefish)、Akkoma・Pleroma系、Fedibird、kmyblue、mitra
無駄なリクエストを飛ばさないようにリストでも作って運用かな。
実際のActivityやJSONでの絵文字の指定などは、もう少し固まったらホームページの方でページを作ろう
「おひとり様ActivityPubサーバーの自作実装」
https://www.doncha.net/activitypub.html
使う絵文字は、unicodeで既定の絵文字、カスタム絵文字(独自の絵文字)の2種類。
本当は絵文字を自作したかったんだけど…て、メモ用紙に手書きで絵を描いてスキャナーで取り込んで4時間ぐらいごちゃごちゃやってみたんだけど、話にならない。目も当てられない。便所の落書きにもならない酷いシロモノ。
40年ぐらい前、エロ漫画の編集をやってた頃は原稿取りに行ってトーン貼りベタ塗りとか手伝ってたし、官能小説雑誌の穴埋めコラムのイラストも描いてたというのにまるで駄目だ。
手を動かす脳を動かすのはある意味筋肉と同じで使ってないとダメになるから、とマンガ家さんたちが口を揃えてたのを思い出した。
https://volpeon.ink/emojis/neocat/
SNSでぼやいたらフリーで使える絵文字を教えていただいてダウンロード。280弱あって全部入れても老眼にはツライだけなので、5つぐらいに絞って登録。とりあえずこれで様子見。
» ローカル環境で電子書籍を作る、Macアプリ・Windows版ツール 「かんたんEPUB3作成easy_epub」
おひとり様ActivityPubサーバーの詳細

先月、ウチのおひとり様ActivityPubサーバーの全体像、実際やってることはホームページに個別ページを作って掲載した。
あらためて、こっちのブログでもその告知。
ブログとホームページの棲み分けというか、以前、SNSに投げた通り
1)SNSで思いつきをちらほらメモ
2)ブログで自分のSNSを見ながら動機や経緯を整理
3)1と2を経て、ホームページに完結した個別ページとして掲載
各Activityを実装するにあたってのブログのエントリとそれを完結させた個別ページはあるけど、サーバー構成について完結させた個別ページはあるけどブログのエントリはなかった。
上記の棲み分けでいうと、2)→3)ではなく3)→2)が今回のエントリ。
おひとり様ActivityPubサーバーの構成::On Golden Pond
https://www.doncha.net/activitypub/activitypub013.html
↑ホームページ掲載はこちら
大雑把にやってることは
・htaccessでリクエストのリダイレクト設定
・リダイレクトされたリクエストをスクリプト1で処理
・ActivityPubの仕様通りにスクリプト2で対応
ウチの構成を丸裸…こういうページを作ることで、自分でも未確認だったり再確認することがいろいろ出てきてびっくり。
昔からよく言われるように「ひとに教えるのは自分に教えることになる」というやつか。
自作実装したのは「自分のわかる範囲で片付ける」ため。
仮にMastodonなど定評のあるActivityPubサーバーソフトウェアをめちゃくちゃ頑張って導入/設定したとしても、何か問題が出た時に、自分のスキル/知識では解決できるとは思えない。管理運用するサーバーの種類も多いし、それらを操作する言語もひとつじゃ済まないし。
おひとり様でサーバーを立ててFediverseに参加できても、すぐに退場することになるんじゃ困る・面白くない。
てことで
・ドメイン取得済み
・レンタルサーバー契約済み
・perlならどうにか手癖で書ける
・apacheなら少しわかる
・データベースも少しはわかる
・htmlとcssは手書きできる
という現状環境と自分のスキル/知識の範囲内。
ActivityPubの仕様、というか決められてる「約束事」は最低限のリクエストのやりとりだけだし、上記程度のスキルで対応できる。
仕様を実装した上で、気をつけたのはひと様の投稿の扱いだけで、それ以外はいろいろがっつり省いた。
だいたい、フロント側はアクセスしたらタイムラインを表示するだけ。もちろん、ページ更新は手動で再読み込みというチープな作りだ。
それでも十分面白いし、自分のタイムラインにどっぷりハマってる。
現状環境、スキル/知識の範囲内でいうとphpやrubyを使えるひとが多いだろうし、phpやrubyでwebというひとはデータベースも使えるだろう。ドメインやレンタルサーバーなんかは外食2回分ぐらい。
やってみると、思っていたよりもFediverse参加のハードルは低い。
おひとり様サーバーを作って参加してみませんか!?
» ローカル環境で電子書籍を作る、Macアプリ・Windows版ツール 「かんたんEPUB3作成easy_epub」
ActivityPubサーバーのバグ修正メモ

ここんとこ時間もあるので、おひとり様サーバーの機能追加やバグ修正。
忘れないようにメモしておこう。
・仏語アクサンや独語ウムラウトまじりの投稿で文字化けを起こしてJSONのdecodeで失敗していた。
JSON.pmは文字コードというかutf8フラグの扱いにクセがあって。
decode_jsonに渡すJSONはutf8フラグがついていてはいけない/事前にutf8フラグを外す必要がある。
なので、事前にEncode::is_utf8でutf8フラグの有無を調べて、utf8フラグがついてたらEncode::encode('utf8',JSON)などと、utf8フラグを外して渡してた。
アクサンやウムラウトは見た目1文字
「â」「ë」
だけど、コードポイントは2バイト使う(日本語などは3バイト)ので、スクリプトではutf8フラグをつけて扱う(検索などにそのまま使えるから)
これをEncode::is_utf8でうまく検知できてなかったっぽい…「ぽい」というのは、まだしっかり特定できてないから。
evalでdecode_jsonを括って$@でエラーを捕捉してるところ、エラーが出たらもう一度同じ処理を入れることにした。原因追求をさぼって結果オーライ、というのは昔からの得意技(技?
・お相手サーバーの生存確認を追加した。
応答の確認に時間がかかるサーバーがあった。セッション切れを起こしてしまうのは致命的。
「LWPでtimeout指定が効かない」
https://t2aki.doncha.net/?id=1731205503
↑この対応で解決したはずなのに、このtimeoutの処理が効いていないサーバーがあった。
同じ500番代なのになんでだろ…。
てことで、投稿を配送する前にお相手サーバーが応答してるかどうかの確認することにした。
lwpでリクエストを投げてもたぶん同じことだろうし、Net::Pingを使うことにした。
今のところ意図通りに捕捉できてる。
・アナウンスした時にフォロワーさん以外に通知が飛ばない。
Activityの配送先は自分のフォロワーさん。なので「自分のフォロワーさんじゃないアカウント」の投稿をAnnounce(ブースト、リポスト、リノート)したら、そのアカウントにも配送する必要がある、ということをすっかり忘れてた。
AnnounceするActivityの投稿者をフォロワーリストで確認。
フォロワーリストに入ってなかったら、配送先に追加。
細々というか、2年近く使ってるというのに、いろいろ出てくるもんだ。
けっこうなスピードで疾走感があった。楽しそうでいいよねえ。
» ローカル環境で電子書籍を作る、Macアプリ・Windows版ツール 「かんたんEPUB3作成easy_epub」
自作ActivityPubサーバーにリプライを実装

当初からリプライを実装するつもりはなかったんだけど、今日とりあえずリプライを実装した。
どうしてリプライを実装しなかったのか。
タイムラインの投稿の返信(リプライ)ボタンをポチッとクリックして、画面の向こうのたぶん面識のないひとに安直に話しかけることの距離感があやういから(わたしの場合)
返信するケースというのは、ほとんどの場合がマウント合戦だと思っていて、
「それ、おれ/わたしも知ってるー!」とか
「おれ/わたしはもうやってきたことだ」とか
「おれ/わたしの方が詳しい/当事者だよ!」とか
そこまでの関係性のない、あかの他人に話しかける動機というのはやっぱり、親近感とか共感だけではなくて、なにかが少し混じってビミョーに違ってると思う。
なので、SNSでやらかすのはこんな局面だろうと思ってるし、わたしもやらかしの返信をしてきたこともあり、リプライは危ないから実装をしない、とActivityPubサーバーを作り始めた時点で決めていた。
なんだけど。
Mentionをいただいて返事をする時に、ウチはリプライに未対応なのでリプライ要素のないJSONを返していた。投稿に対するリプライ要素があればツリー表示となるけど、リプライ要素がないJSONだと単品のMentionとして表示されるだけ(だと思う)
お相手にしてみると「あれ?なんだっけ?これ」ということになるだろうしなあ。
てことで、リプライ要素を付与したActivity(JSON)を返すように実装した。
ただし、返信(リプライ)ボタンを表示するのはMentionをいただいた投稿限定。
ホームタイムラインに流す投稿には表示しない。(わたしの場合)やらかす危険しかないから。
昨日実装した「転送」と今日実装した「リプライ」について、別サーバーの別アカを使って確認したところ、「意図通り」反映してるっぽい。
転送もリプライも、いろいろちょっと危ういんで、様子を見てながら運用する、ということで。
昨日、今日と2日続けてActivityPubサーバーをいじっててしみじみ。
やっぱ、ActivityPubも、それを実装するperlもめっちゃ面白い!
還暦過ぎの趣味、ボケ防止の趣味としては文句のつけようがないよなあ。
通り抜けた向こう側に光がある、という絵面。定番だし、やっぱ好きだわ。
» ローカル環境で電子書籍を作る、Macアプリ・Windows版ツール 「かんたんEPUB3作成easy_epub」
自作ActivityPubサーバーに転送を実装

Activityの転送という仕組みがあって、それはActivityPub的には必須(MUST)とされている。
ずっと未対応だったのはFORMデータ本文=ActivityのJSONに対する署名のやりかたがわからないから。
…今もまったくわかってない。
・Activityを転送する
受け取ったActivityを、受け取ったサーバーがそのまま自分のフォロワーに配送する。
この時、もしもActivityが改ざんされていても受け取ったサーバーはわからないし、わかりようがない。とりあえず、そのまま転送するだけ。
なので、この問題を解決するのが、Activity本文について署名をする=JSON-LD(JSON Linked Data)署名。
この署名をActivityに付与することで、Activityの改ざんなどを検知できる、ということ(らしい)
ただ、このJSON-LD(JSON LinkedData)署名は、AcitivityPub的には必須(MUST)ではない。実際、今日時点で確認したらCreateのActivityにJSON-LDの署名がついているのはMastodon系のサーバーで、ほか、Misskey系やAkkoma系のサーバーではJSON-LDの署名はついていない。
それならば。
ActivityPub的に転送はMUSTなので、JSON-LD署名は未対応のままでも実装だけはしてしまおうと(今さら
どうして転送なんてのがMUSTになっているかというと。
https://argrath.github.io/activitypub/#inbox-forwarding
注: ゴーストリプライ問題を避けるための転送
以下の節は、連合ネットワークで時々問題を引き起こす、 「ゴーストリプライ」問題を緩和するためのものである。この問題を例で示す。
Alyssa は、会議での論文の発表が上手くいったことを投稿し、 友人である Ben を含む彼女のフォロワーコレクションに送りました。Ben は祝福する内容を Alyssa のメッセージに返信し、 宛先に彼女のフォロワーコレクションを含めました。しかし、Ben は Alyssa のフォロワーコレクションの内容にアクセス出来ないので、 Ben のサーバは彼のメッセージを彼らの inbox に転送できません。次の機構がない場合、その後 Alyssa が Ben に返信すると、 彼女のフォロワーは、Ben の返信を見ることなく Alyssa が Ben に返信するのを 見ることになります。これはとても混乱します!
わたしがお相手とMentionで会話をした時に
・わたしは自分の投稿をフォロワーさんにも配送してるのでわたしのお相手への投稿は見える。
・でも、お相手からのわたしへの投稿はわたしのフォロワーさんに見えるとは限らない。
・わたしが壁に向かってなにか喋ってるようにしか見えない、ということになる。
なので、お相手からわたしへの投稿を、わたしのフォロワーさんにも見えるように転送する、ということが必要。
必要かどうかは全面的に賛成ではない、というか必須にする理由としては弱いと思ってるんだけど、仕様的にMUSTとされちゃってるからには対応せざるを得ないよなあ。
で、転送を実装しちゃったんで、転送したNoteに対するDeleteも実装。
・転送したNoteのIDをリストで保存
・Deleteが飛んできたらNoteの転送履歴を確認して転送した投稿が該当してたらDeleteを転送する
ということにした。
転送がMUSTなのは仕様で見てたけど、実装してなかったのは。
JSON-LDに対する署名のやりかたがわからない、というのが一番問題で、次に、それとはまた別レイヤーでの話もあって。
わたしは、自分の投稿がどう扱われようが、割とどうでもいいと思ってる。
だけど。ひと様の投稿については、わたしが関わることでご本人の意図しない形で扱われるのはめちゃくちゃ困るしそれは避けたい。
ActivityPubの仕様上、Delete(削除)、特にリモートサーバーでの削除が意図通りにならない。ご本人は削除したつもりでも転送された別サーバーでは残ってることがないとは言えない。
https://argrath.github.io/activitypub/#delete-activity-inbox
あるアクティビティが発信元サーバからリモートサーバに転送された後、 オブジェクトの表現をリモートをリモートで削除することを 強制する 方法は、ActivityPub プロトコルにはないということに注意すること
なもんで、ひと様のデータを転送することには抵抗があった。
ちなみに転送するNoteは
わたし宛のメンションで、宛先(「to」や「cc」)に「public」や「わたしのfollowers」が指定されているもの。
(DMなんかはもちろん除外)
何かをたった「ひとつ」実装するだけとはいえ、考える/考えなきゃいけないことがいろいろあるよなあ。
足立やっちゃ場の旧日光街道に佇む松尾芭蕉…って、こんなだったんか。初めてみた。
» ローカル環境で電子書籍を作る、Macアプリ・Windows版ツール 「かんたんEPUB3作成easy_epub」
LWPでtimeout指定が効かない

自作実装のActivityPubサーバーから、フォロワーさんの所属しているリモートサーバーに投稿を送信する時に、お相手のリモートサーバーがなんらかの事情で落ちてるケースがある。
レスポンスは500番代のエラーとなる。
perlで定番のLWP UserAgentを使ってリクエストを投げてるんだけど。
400番代のエラーと違って、サーバーに問題が発生している500番のエラーの場合、返事が戻ってくるのに時間がかかる、待たされる。投稿するためのPOSTもアカウント情報取得のためのGETもなかなか戻ってこない。
デフォルトだと、リクエストを投げてお相手の反応が返ってくるまで180秒(3分)待つことになっている。
LWP::UserAgent - Web ユーザエージェントクラス
https://perldoc.jp/docs/modules/libwww-perl-6.04/LWP/UserAgent.pod
timeout( $secs )
秒単位のタイムアウト値を取得または設定します。 デフォルトのtimeout()の値は180秒、つまり3分です。
サーバへの接続においてtimeout秒反応がないと、リクエストは中断します。 つまり、トランザクションが完了してrequest()メソッドが実際に返るまでの 時間を意味します。
ということなのでtimeoutに10秒とか設定してみてたんだけど、どうもそれが効いてない。
検索したら、やっぱりtimeoutの指定は効かないことがあるらしい。
LWP::UserAgentのタイムアウトがうまく効かなかった事象の調査 (序章)
https://papix.hatenablog.com/entry/2020/12/25/180640
もう少し粘って検索してみると
lWP::UserAgentの「:content_cb」(コールバック)のサブルーチンでSIGNALを設定してalarmで対応できるという記事を発見。
LWP::UserAgent get callback with timeout
https://stackoverflow.com/questions/29071348/lwpuseragent-get-callback-with-timeout
リクエストをサブルーチンで処理することになるのが、素人のわたし的に難しそう…影響範囲がわからない。
思いつきでデフォルトを180秒にしたわけじゃあるまいし、なにか理由がありそう。それをここで指定しちゃうと、全部に影響するわけだしなあ、と。
てことで、それならリモートに投稿を送信するリクエストのサブルーチン限定にしてしまえば大丈夫っぽいんじゃないかと。
サブルーチン丸ごとSIGNALのALRMを設定してevalで捕まえることにした。
結果オーライとか、やっつけ仕事だけは昔から得意だし。
my $res = "";
eval{
local $SIG{ALRM} = sub{die "timeout";};
alarm $self->{timeout};
$res = $self->post_actpb({url=>$u, content=>$args->{json}});
alarm 0;
};
if( $@ ){
printf qq{ERROR deliver %s ::: %s}, $u, $@;
}
設定を15秒にしてみたら、意図通りにtimeoutをキャッチして、待ち時間が少なくなった。
ちゃんとしたActivityPubサーバーのMastodonなんかだと、リクエストの送受信は裏側でやってるんで、アクセスしてるユーザーが待たされることはないはず。
わたしの自作ActivityPubサーバーは最低限で、いろいろ手抜きしていて表示するだけのために3分以上待たされるんだよなあ。自業自得というか。
サーバーのお守り代わりの画像
(松戸市立博物館)
» ローカル環境で電子書籍を作る、Macアプリ・Windows版ツール 「かんたんEPUB3作成easy_epub」