Skip to content

Instantly share code, notes, and snippets.

@okapies
Last active September 5, 2021 11:39
  • Star 46 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save okapies/60d62d0df0163bbfb4ab09c1766558e8 to your computer and use it in GitHub Desktop.
Mastodon OStatus API の叩き方

Mastodon が他のインスタンスと情報交換をする OStatus API の使い方。使ってるだけのユーザは知る必要がない裏側の話。

host-meta

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 華やかなりし頃の化石のような仕様がバンバン出てきて面白い)

webfinger

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通り:

Atom フィード

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">&lt;p&gt;管理人さんそろそろ寝た方が良さげ。&lt;/p&gt;</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>
  ...

つまり、個々人のタイムラインをブログのフィードとみなした情報が提供されるということ。ブラウザで開けばフィードリーダーが出てくるし、購読することもできる.

Pubsubhubbub

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}" \

Callback URL の検証

Mastodon インスタンスは、自身に登録された callback が本当に有効なものか確認するために、実際にその URL へアクセスして検証する. また、購読解除の際も同様に検証が行われる.

Mastodon では、他の OStatus サーバに対して https://pawoo.net/api/subscriptions/{id}id はユーザ名ではなく内部の登録番号)を callback として登録するので、この URL に検証要求が来た時に応答を返すようになっている. 検証要求には、クエリパラメータとして以下のパラメータが指定される:

  • hub.mode (subscribe or unsubscribe)
  • hub.topic: 購読時に指定した topic URL
  • hub.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

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 準拠を謳っている実装はいろいろあるが、実際のところの互換性は微妙な部分もありそうな….

Magic Signature

TODO

Atom Activity Streams / Threading Extensions

TODO

参考

@j5ik2o
Copy link

j5ik2o commented Apr 23, 2017

@okapies インスタンスリストってどこから取得するんだろう。
あ、これを見ればいいんですかね?
https://instances.mastodon.xyz/instances.json

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment