Mastodon が他のインスタンスと情報交換をする OStatus API の使い方。使ってるだけのユーザは知る必要がない裏側の話。
Mastodon インスタンスに対して、RFC6415 が規定する /.well-known/host-meta
というパスを要求すると以下の XML が返ってくる.
<?xml version="1.0"?>
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
<Link rel="lrdd" type="application/xrd+xml" template="https://[MASTODON_HOST]/.well-known/webfinger?resource={uri}"/>
</XRD>
- "lrdd" は Link-based Resource Descriptor Document の略で、こちらも Internet draft が提出されている.
- "webfinger" も RFC7033 で規定された仕様で、要はメールアドレスの代わりにネット上で個人を識別するアドレスとして使えるようなものを目指しているらしい.
(XML 華やかなりし頃の化石のような仕様がバンバン出てきて面白い)
host-meta のレスポンスで指示されている https://[MASTODON_HOST]/.well-known/webfinger?resource={uri}
に GET リクエストを送ると、以下のように指定したユーザの情報が JSON で返ってくる:
{
"subject": "acct:okapies@[MASTODON_HOST]",
"aliases": [
"https://[MASTODON_HOST]/@okapies"
],
"links": [
{
"rel": "http://webfinger.net/rel/profile-page",
"type": "text/html",
"href": "https://[MASTODON_HOST]/@okapies"
},
{
"rel": "http://schemas.google.com/g/2010#updates-from",
"type": "application/atom+xml",
"href": "https://[MASTODON_HOST]/users/okapies.atom"
},
{
"rel": "salmon",
"href": "https://[MASTODON_HOST]/api/salmon/5166"
},
{
"rel": "magic-public-key",
"href": "data:application/magic-public-key,RSA.oFvhFmN-Cb3o2xmyXvJxUfB-Xz1iB_4A9B_FPi7O8HVO7w4Wa1hcby1x5sQFGfoH4bhZsyP215a8mWWyzwLGbq1IwZ4PZV8whDW4aXthuGUsXYdjtXrkZbPyr9WY0FtEMFYEgXhc3pARh4L-OgWsCt_M4x3mFET4lX4rNAxBU91JZyINb-RfAmi2ahQE2SKgOo5MLUJf0NkMRlxtNebxcihquI0MmS_Yf4EDd1nY4xZ60_guoS4nAYHtGaaUUmdDSI42VI9pa94MQVLboelrZDxC7_QQqvYYZTQwO86nL4oMMd6AG3h5d26mUXm4j5tmfW5Ss4OI6PGtiBPq84IApw==.AQAB"
},
{
"rel": "http://ostatus.org/schema/1.0/subscribe",
"template": "https://[MASTODON_HOST]/authorize_follow?acct={uri}"
}
]
}
${uri}
の指定方法は以下の2通り:
acct:[USER_NAME]@[MASTODON_HOST]
(e.g. acct:okapies@pawoo.net)https://[MASTODON_HOST]/@[USER_NAME]
(e.g. https://pawoo.net/@okapies)
Webfinger のレスポンスで示されている https://[MASTODON_HOST]/users/[USER_NAME].atom を取得すると、以下のような Atom フィードが返ってくる:
<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:media="http://purl.org/syndication/atommedia" xmlns:ostatus="http://ostatus.org/schema/1.0" xmlns:mastodon="http://mastodon.social/schema/1.0">
<id>https://pawoo.net/users/okapies.atom</id>
<title></title>
<subtitle></subtitle>
<updated>2017-04-15T05:01:11Z</updated>
<logo>https://img.pawoo.net/accounts/avatars/000/005/166/original/496aedb9c97090b8.png?1492232471</logo>
<author>
<id>https://pawoo.net/users/okapies</id>
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
<uri>https://pawoo.net/users/okapies</uri>
<name>okapies</name>
<email>okapies@pawoo.net</email>
<summary></summary>
<link rel="alternate" type="text/html" href="https://pawoo.net/@okapies"/>
<link rel="avatar" type="image/png" media:width="120" media:height="120" href="https://img.pawoo.net/accounts/avatars/000/005/166/original/496aedb9c97090b8.png?1492232471"/>
<link rel="header" type="" media:width="700" media:height="335" href="/headers/original/missing.png"/>
<poco:preferredUsername>okapies</poco:preferredUsername>
<mastodon:scope>public</mastodon:scope>
</author>
<link rel="alternate" type="text/html" href="https://pawoo.net/@okapies"/>
<link rel="self" type="application/atom+xml" href="https://pawoo.net/users/okapies.atom"/>
<link rel="next" type="application/atom+xml" href="https://pawoo.net/users/okapies.atom?max_id=268963"/>
<link rel="hub" href="https://pawoo.net/api/push"/>
<link rel="salmon" href="https://pawoo.net/api/salmon/5166"/>
<entry>
<id>tag:pawoo.net,2017-04-16:objectId=710068:objectType=Status</id>
<published>2017-04-16T15:48:26Z</published>
<updated>2017-04-16T15:48:26Z</updated>
<title>New status by okapies</title>
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<content type="html"><p>管理人さんそろそろ寝た方が良さげ。</p></content>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
<mastodon:scope>public</mastodon:scope>
<link rel="alternate" type="text/html" href="https://pawoo.net/users/okapies/updates/365353"/>
<link rel="self" type="application/atom+xml" href="https://pawoo.net/users/okapies/updates/365353.atom"/>
</entry>
...
つまり、個々人のタイムラインをブログのフィードとみなした情報が提供されるということ。ブラウザで開けばフィードリーダーが出てくるし、購読することもできる.
Mastodon は、インスタンス間でステータスの更新情報をやり取りするために Pubsubhubbub を使う.
(実際に購読する所まではやってないので間違ってたらコメントください)
以下の Url を叩くと、その Mastodon インスタンス内の指定ユーザがステータスを更新した際に呼び出す callback URL を登録できる. topic には、指定したユーザに対応する上記の Atom フィードの URL を指定する.
MASTODON_HOST=...
CALLBACK_URL=...
USER_NAME=okapies
$ curl -v -X POST -Ss https://${MASTODON_HOST}/api/push \
-d "hub.mode=subscribe" \
-d "hub.topic=https://${MASTODON_HOST}/users/${USER_NAME}.atom" \
-d "hub.callback=${CALLBACK_URL}" \
-d "hub.lease_seconds=???"
-d "hub.secret=???"
その逆に、購読解除は hub.mode=unsubscribe
を指定する:
$ curl -v -X POST -Ss https://${MASTODON_HOST}/api/push \
-d "hub.mode=unsubscribe" \
-d "hub.topic=https://${MASTODON_HOST}/users/${USER_NAME}.atom" \
-d "hub.callback=${CALLBACK_URL}" \
Mastodon インスタンスは、自身に登録された callback が本当に有効なものか確認するために、実際にその URL へアクセスして検証する. また、購読解除の際も同様に検証が行われる.
Mastodon では、他の OStatus サーバに対して https://pawoo.net/api/subscriptions/{id}
(id
はユーザ名ではなく内部の登録番号)を callback として登録するので、この URL に検証要求が来た時に応答を返すようになっている. 検証要求には、クエリパラメータとして以下のパラメータが指定される:
hub.mode
(subscribe
orunsubscribe
)hub.topic
: 購読時に指定した topic URLhub.challenge
: 登録先のインスタンスがランダムに生成するチャレンジ値. 応答時にこの文字列をボディとするhub.lease_seconds
: 購読の有効期限切れまでの秒数
購読元の Mastodon インスタンスは、指定されたユーザのステータスが更新される度に登録済みの callback URL(この場合は https://pawoo.net/api/subscriptions/{id}
)を呼び出す. ボディには Atom 形式で更新フィードが入っている(全文が入っているわけではない?).
また Mastodon は、通知に HTTP_X_HUB_SIGNATURE
という独自の HTTP ヘッダを付与する. これは、購読時に共有した secret
属性の値をキーとしてボディを SHA1 で要約(ハッシュ化)した値になっており、これを比較することで更新情報の真正性を保証する. 具体的な生成方法は以下の通り(参考):
hmac = OpenSSL::HMAC.hexdigest('sha1', @secret, content)
(つまり、購読ごとに現在の secret を保存しておく必要があるということですね. lease が切れる度に更新することで、漏れた場合でも偽情報を食わされるリスクを限定できる. ただ、この用途で SHA1 はそろそろどうなんだろう…)
Salmon は、一言で言うとブログ記事にコメントを付けるためのプロトコルで、特に、あるサイトの記事を別のサイトで共有している時に、そこについたコメントをどのサイトでも集約して見ることができるようにする. OStatus では、まさに複数のインスタンスに跨ってツイートが共有されている状況で「返信」ができるようにする.
Salmon で返信するには、Salmon API のエンドポイントを取得する必要がある. このエンドポイントは、上記で取得した WebFinger や Atom フィードの中に書かれている:
{
"rel": "salmon",
"href": "https://[MASTODON_HOST]/api/salmon/5166"
}
<link rel="salmon" href="https://[MASTODON_HOST]/api/salmon/5166"/>
返信先を指定するには、Atom フィードからツイートの id
を取得する. ツイートの id は Tag URI スキームで表現される:
<entry>
<id>tag:pawoo.net,2017-04-16:objectId=700365:objectType=Status</id>
TODO: MagicKey を使ったリクエストの作り方
<?xml version='1.0' encoding='UTF-8'?>
<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
<me:data type='application/atom+xml'>
PD94bWwgdmVyc2lvbj0nMS4wJyBlbmNvZGluZz0nVVRGLTgnPz4KPGVudHJ5IHhtbG5zPS
dodHRwOi8vd3d3LnczLm9yZy8yMDA1L0F0b20nPgogIDxpZD50YWc6ZXhhbXBsZS5jb20s
MjAwOTpjbXQtMC40NDc3NTcxODwvaWQ-ICAKICA8YXV0aG9yPjxuYW1lPnRlc3RAZXhhbX
BsZS5jb208L25hbWUPHVyaT5hY2N0OmpwYW56ZXJAZ29vZ2xlLmNvbTwvdXJpPjwvYXV0a
G9yPgogIDx0aHI6aW4tcmVwbHktdG8geG1sbnM6dGhyPSdodHRwOi8vcHVybC5vcmcvc3l
uZGljYXRpb24vdGhyZWFkLzEuMCcKICAgICAgcmVmPSd0YWc6YmxvZ2dlci5jb20sMTk5O
TpibG9nLTg5MzU5MTM3NDMxMzMxMjczNy5wb3N0LTM4NjE2NjMyNTg1Mzg4NTc5NTQnPnR
hZzpibG9nZ2VyLmNvbSwxOTk5OmJsb2ctODkzNTkxMzc0MzEzMzEyNzM3LnBvc3QtMzg2M
TY2MzI1ODUzODg1Nzk1NAogIDwvdGhyOmluLXJlcGx5LXRvPgogIDxjb250ZW50PlNhbG1
vbiBzd2ltIHVwc3RyZWFtITwvY29udGVudD4KICA8dGl0bGUU2FsbW9uIHN3aW0gdXBzdH
JlYW0hPC90aXRsZT4KICA8dXBkYXRlZD4yMDA5LTEyLTE4VDIwOjA0OjAzWjwvdXBkYXRl
ZD4KPC9lbnRyeT4KICAgIA
</me:data>
<me:encoding>base64url</me: <me:alg>RSA-SHA256</me:alg>
<me:sig>
EvGSD2vi8qYcveHnb-rrlok07qnCXjn8YSeCDDXlbhILSabgvNsPpbe76up8w63i2f
WHvLKJzeGLKfyHg8ZomQ
</me:sig>
</me:env>
Mastodon は、インスタンスをまたぐ返信だけではなく、様々なリモート操作の通知を activity:verb
として表現している:
- follow
- request_friend
- authorize
- reject
- unfollow
- favorite
- unfavorite
- post
- share
- delete
- block
- unblock
このように、Salmon で送信するメッセージの形式については必ずしも OStatus で全てが規定されているわけではなく、実装依存の部分も大きい模様(Gargron さんのツイートより)). OStatus 準拠を謳っている実装はいろいろあるが、実際のところの互換性は微妙な部分もありそうな….
TODO
TODO
@okapies インスタンスリストってどこから取得するんだろう。
あ、これを見ればいいんですかね?
https://instances.mastodon.xyz/instances.json