Mastodonにアカウントとして認識されるActivityPubを実装してみる

Mastodonの検索エリアにURLを突っ込んだ際にアカウント化（？）させるにはどういう実装が必要なのか、気になって調べて実装してみた。

# 結局
- ActivityPub対応
- WebFinger対応
- HTTPS化

で受け入れられる。

# 参考
- [pylodon](https://github.com/rowanlupton/pylodon): PythonでのActivityPubの実装
- [Misskey](https://github.com/syuilo/misskey): Mastodon対応のActivityPubの実装

# Personを返すActivityPubの実装
GETアクセスした際に以下のJSONを返す。

```python
{
    '@context': 'https://www.w3.org/ns/activitystreams',
    'type': 'Person',
    'id': 'https://example.com/test', # Fediverseで一意
    'name': 'test', # 表示名
    'preferredUsername': 'test', # ユーザID
    'summary': 'my simple activitypub', # 概要
    'inbox': 'https://example.com/inbox', # このユーザへの宛先
    'outbox': 'https://example.com/outbox', # このユーザの発信元
    'url': 'https://example.com/test', # プロフィールページのURL
    'icon': {
        'type': 'Image',
        'mediaType': 'image/png', # mime type
        'url': 'https://example.com/icon.ong' # アイコン画像のURL
    }
}
```

Flaskで実装する例。

```
$ pip install flask
```

```python:api.py
import json
from flask import Flask, Response

app = Flask(__name__)

@app.route('/')
def person():
    response = {
        ...
    }
    return Response(json.dumps(response), headers={'Content-Type': 'application/activity+json'})

if __name__ == "__main__":
    app.run()
```

# WebFingerの実装
あまり記事で見かけないが、MastodonはWebFingerにも対応させる必要がある（インターネット界の暗黙的なルール？）。
ことの始まりとして`/.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://example.com/.well-known/webfinger?resource={uri}" />
</XRD>
```

のようなXMLを返す。これを受けてMasotodnは`template`のURLにアクセスしてくるので、

```python
{
    'subject': 'acct:test@example.com',
    'links': [
        {
            'rel':  'self',
            'type': 'application/activity+json',
            'href': 'https://example.com/test'
        },
    ]
}
```

を返すようにする。`{uri}`には求めているユーザの`subject`にあたる文字列が入っている。ユーザを複数扱う場合にはこれを参照して返すXMLを生成する。

Flaskで実装する例（１ユーザのみ版）。

```python:webfinger.py
import json
from xml.dom.minidom import parseString
from flask import Flask, jsonify, Response
from .app import app

@app.route('/.well-known/host-meta')
def webfinger_host_meta():
    xml_str = "<?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://example.com/.well-known/webfinger?resource={uri}\"/>\
        </XRD>"
    dom = parseString(xml_str)

    return Response(dom.toprettyxml(), headers={'Content-Type': 'application/xml'})

@app.route('/.well-known/webfinger')
def webfinger_resource():
    response = {
        'subject': 'acct:test@example.com',
        'links': [
            {
                'rel':  'self',
                'type': 'application/activity+json',
                'href': 'https://example.com/test'
            },
        ]
    }

    return jsonify(response)
```

api.pyから読み込む。

```python:api.py
...

import webfinger

if __name__ == "__main__":
    app.run()
```

Flaskをとりあえず動かしてみる。

```
$ python api.py
```
# 本番サーバでHTTPS化

本番サーバではuWSGIを使う。

```
$ pip install uwsgi
```

設定ファイルを作る。

```ini:app.ini
[uwsgi]
# 初期起動ファイル
module = api
# Flask(__name__) 実体先
callable = app
master = true
processes = 1
socket = /tmp/uwsgi.sock
chmod-socket = 666
vacuum = true
die-on-term = true
```

次のコマンドで本番サーバが立ち上がる。

```
uwsgi --ini flask.ini
```

これをnginxでHTTPS化して公開する。

```conf:flask.conf
server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443;
  server_name example.com;

  ssl_protocols TLSv1.2;
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        include uwsgi_params;
        uwsgi_pass unix:///tmp/uwsgi.sock;
    }
}
```

`https://example.com/test`をMastodonの検索エリアに突っ込むことでアカウントになって出てくると思う。

# bot化する上で必要な事
- Noteを返すActivityPubの実装
- フォロワーの管理
- Activity（発言とか）をフォロワーのINBOXへPOST

[続き](https://qiita.com/wakin/items/28cacf78095d853bfa67)