まるぼ実験場

アプリの開発日記を載せるサイトでしたが、ただの技術ブログになりました。

サイトのRSSをFediverse向けに配信出来るようにしたい(1)

最近はSNSをだらだら見るのをほどほどにして、発信は自サイト上に設置したマイクロブログCGIを運用していたりするのだが、このマイクロブログの更新情報をFediverse向けに配信出来たら便利ではないだろうかと思って最近調べていた。
HP更新があるかを見るためにHPにアクセスしてもらう、なんてのはTwitterに慣れきってしまった今の時代では能動的にやってる人間は少ないんじゃないかと思うので、できることなら直接届けたいと思うのだ。

Fediverseとは、独立性を保ったSNSが繋がる連合群のこと。
ja.wikipedia.org
インターネット上にいくつか島(SNS)があってお互い繋がっているみたいなイメージ?
それらは別のサーバー上にありながらお互いActivityPubという実装で繋がっており、例に挙げた2種以外にもActivityPub対応アプリはいくつかあったりするのでそれらも含めて繋がることが可能。
仕様がオープンなのでその気になれば自分専用サーバーやアプリを立ち上げることも可能。
僻地のような自分のサーバーからmisskey.ioやmastodon.jpのような大きなところと繋がることも可能!夢がある。
なのでその規格に対応したパイプを自分作って、データを送信できればマイクロブログ側で完結できそう。
つまり今回、送信専用の自分用サーバーを開こうとしているという方向性になる。
最初はそのサーバー用ソフトウェアをそのまま動かしたら良いんじゃないのか、なんて思ったが、さすがにオーバーすぎかなぁと。
なるべく低予算、低スケールに納めるために今のサーバーに移転してきて、あえてWordPressでもなくてがろぐを使ってるので……。

もっと簡単そうな方法としては、どこかのサーバーにおじゃまさせてもらってbotで動かすという手もありそうだが、分散型SNSの欠点としてデータ管理の問題があると聞き、よそ様のサーバーに余計なデータを残すのも……と思って、
なるべく自分でデータを持っておきたいと考えた。あとはちょっとした技術探究心です。

調べてみると、ActivityPub自体は結構シンプルなやりとりで動いているようで、
HTTPSが使えてCGIが使えるサーバーで独自ドメインを使っているなら実装は可能そうな雰囲気。
(なんとほとんど静的ファイルで動かしたという方も。。すごい。)
blog.alfebelow.com

動かすサーバーはスタードメイン付帯の無料サーバーです。
SSHインストールなどは使えないが、言語を選べば実装できそうかなと思ったので。
(ちなみに、GNU SocialというActivityPub対応マイクロブログPHPで動いているそうだ。)

ファイル構成や書き方は以下あたりを参考に。
scrapbox.io
t2aki.doncha.net
blog.3qe.us
qiita.com

nodeinfo/
└2.1.json
.well-known/
└nodeinfo.json
└host-meta.php(xrd+xml)
└webfinger.php(jrd+json)
(アクターのフォルダ)/
└index.php(activity+json)
└inbox.json
└outbox.json
この構成でサーバーにアップ。

まずアカウント情報取得時、サーバーの/.well-known/nodeinfo を見に来るようだ。
https://tweet.3ka.me/.well-known/nodeinfo

/.well-known/nodeinfo.json

{
  "links": [
    {
      "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1",
      "href": "https://tweet.3ka.me/nodeinfo/2.1"
    }
  ]
}

そして "href": から"https://tweet.3ka.me/nodeinfo/2.1"を見に来る。

/nodeinfo/2.1.json

{
    "openRegistrations": false,
    "protocols": [
        "activitypub"
    ],
    "software": {
        "name": "kky3ka",
        "version": "0.1.0"
    },
    "usage": {
        "users": {
            "total": 1
        }
    },
    "version": "2.1"
}

次に、/.well-known/host-meta からtemplate=の場所を調べに来る。
/.well-known/host-meta.php

<?php
    header("Access-Control-Allow-Origin: *");
    header("Content-Type: application/xrd+xml charset=UTF-8");
    echo '<?xml version="1.0"?>';
    echo '<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">';
    echo '<Link rel="lrdd" type="application/xrd+xml" template="https://tweet.3ka.me/.well-known/webfinger?resource={uri}" />';
    echo '</XRD>';
?>

host-metaとwebfingerとユーザーのデータはヘッダーでContent-typeを指定して返す必要があるそうなので、実態はPHPとした。
なんという力業実装。
templeteのURLに取得したいユーザー情報を入れたリクエストを飛ばす。
この場合、https://tweet.3ka.me/.well-known/webfinger?resource=acct:kky3ka@tweet.3ka.me
というURLが来る。
webfinger.phpの中でユーザ情報を検索して……ってやると思うのだが、今回は1人なのでそのまま返す。
/.well-known/webfinger.php

<?php
    header("Access-Control-Allow-Origin: *");
    header('Content-Type: application/jrd+json charset=utf-8');
    $txt = '{
        "subject": "acct:kky3ka@tweet.3ka.me",
        "links": [
            {
                "rel":"self",
                "type":"application/activity+json",
                "href":"https://tweet.3ka.me/kky3ka"
            }
        ]
    }
    ';
    $json = json_decode($txt);
    echo json_encode($json, JSON_UNESCAPED_SLASHES);
?>

hrefのURLにアクセスしてアクター情報を取得。
https://tweet.3ka.me/kky3ka/
/(アクターの名前)/index.php

<?php
    header("Access-Control-Allow-Origin: *");
    header('Content-Type: application/activity+json charset=utf-8');
    $txt = '{
        "@context":[
            "https://www.w3.org/ns/activitystreams",
            "https://w3id.org/security/v1"
        ],
        "type":"Person",
        "id":"https://tweet.3ka.me/kky3ka",
        "preferredUsername":"kky3ka",
        "name":"三日坊主日記",
        "summary":"Yなサイトから移動。日々のつぶやき",
        "url":"https://tweet.3ka.me/tegalog.cgi",
        "inbox":"https://tweet.3ka.me/kky3ka/inbox.json",
        "outbox":"https://tweet.3ka.me/kky3ka/outbox.json",
        "icon":{
            "type": "Image",
            "mediaType": "image/png",
            "url": "https://tweet.3ka.me/icon.png"
        }
    }';
    $json = json_decode($txt);
    echo json_encode($json, JSON_UNESCAPED_SLASHES);
?>

ここにアカウントのプロフィール的な情報を入れておく。W3CのActivityPubの仕様では、id,type,inbox,outboxが必須(MUST)となっていた(実際にはなくても大丈夫なものもあるらしい……)ので入れておく。
www.w3.org

そうそう、この場合ファイル名が規定と異なるのでそのままではアクセス出来ないため、
指定の名前でアクセス出来るように、.htacessを使ってルーティングしておく。

RewriteEngine on
#webfinger?resource=acct:...
RewriteCond %{QUERY_STRING} ^resource=(.*)$
RewriteRule webfinger.* webfinger.php?%1[L]
#nodeinfo(/)→nodeinfo.json
RewriteRule nodeinfo(.*)$ nodeinfo.json [L]
#host-meta→host-meta.php
RewriteRule host-meta(.*)$ host-meta.php [L]

/.well-known/ディレクトリの例
/の有無で影響があるかは分からないが、試しに登録させてもらったマストドンインスタンスでは/ありでもファイルにアクセスできたのでそれに合わせることにした。
ちなみにそこのサーバーはパラメータなしでwebfingerを直接呼んでみたら400エラーを返していたが、こっちは登録機能なしの1人用サーバーということで特に閲覧制限はしていない。(webfinger.phpを直接叩くとユーザーが見える)
今回は力業で実装したが、やってることはJSONを返すだけなので、bottle.pyみたいなレンタルサーバーでも動いた実績があるマイクロフレームワークを使ってみても良かったかもしれない。

この状態でMisskeyの照会(他サーバーのユーザーを探す機能)で@ユーザー@ドメイン名で検索してみる。
とりあえずMisskey側から認識されることには成功。
注意点としては、記述ミスに注意というところ。
一度失敗すると暫くの期間(具体的な期間は分からない)そのサーバーのユーザーが出なくなるらしい。
一応Fediverseでメインにするつもりだったマストドンインスタンス上で表示されてなかったり……。

とりあえず窓口ができた!って程度で、フォロー(されたことに対応する)機能はできてないので、フォローされたりフォロワーに投稿を届けることは出来ない。
なので次はそこが目標かなと。
投稿の配信についてはマイクロブログCGI側にRSSを吐き出す機能があるので、これを流用したら良いのではないかと考えている。
というか思ったけど、はてなブログもFediverse配信に対応したりしないかなって思ったり。