LoginSignup
23
14

More than 5 years have passed since last update.

Mastodonからフォロー出来て投稿が反映されるActivityPubを実装してみる

Last updated at Posted at 2018-06-18

前回の内容に引き続きさらにMastodonとやり取りが出来るようなActivityPubを書いてみる。

Noteを返すActivityPubの実装

Personを返すActivityPubと同様の流れで実装するだけ。

{
    '@context': 'https://www.w3.org/ns/activitystreams',
    'type': 'Note',
    'id': 'https://example.com/test/1', # Fediverseで一意
    'attributedTo': 'https://example.com/test', # 投稿者のPerson#id
    'content': '<p>投稿内容</p>' # XHTMLで記述された投稿内容
    'published': '2018-06-18T12:00:00+09:00' # ISO形式の投稿日
    'to': [ # 公開範囲
        'https://www.w3.org/ns/activitystreams#Public', # 公開(連合?)
        'https://example.com/test/follower', # フォロワー
    ]
}

flaskで実装する例(前回の続き)。

api.py
import json
from flask import Flask, Response

app = Flask(__name__)

...

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

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

Mastodonの検索エリアに https://example.com/note と入れるとトゥートとして返ってくると思う。

フォローを受け付ける処理の実装

前回の実装のままでも、Mastodonからフォローボタンを押せばフォロワーのカウント数が増えてフォローしているように見える。しかしフォローワー一覧等には反映されない。これはフォロー処理が完了していないためである。フォロー処理を完了させるためにはFollowのアクティビティに対してAcceptアクティビティを返す必要がある。

INBOXの実装

まずはアクティビティを受け付けるINBOXを実装する。Personのinboxパラメータに指定したパスへのPOSTを処理できるようにする。

flaskでの実装例。

api.py
import json
from flask import Flask, Response, request

app = Flask(__name__)

...

@app.route('/inbox', methods=['POST'])
def inbox():
    if request.headers['Content-Type'] != 'application/activity+json':
        return Response(status=400)

    jsn = request.json
    if type(jsn)!=dict or 'type' not in jsn:
        return Response(status=400)
    elif jsn['type'] == 'Follow':

        # Follow処理を書く

        # Acceptを返す処理を書く

        return Response(status=200)
    elif jsn['type'] == 'Undo':
        obj = jsn['object']
        if type(obj)!=dict or 'type' not in obj:
            return Response(status=400)
        elif obj['type'] == 'Follow':

            # Unfollow処理を書く

            # Acceptを返す処理を書く

            return Response(status=200)

    return Response(status=501)

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

Acceptアクティビティ

Followの処理後、アクティビティを受け付けたことを示す以下のようなAcceptを、Followを投げてきたアカウントのINBOXに返す。

{
    '@context': 'https://www.w3.org/ns/activitystreams',
    'type': 'Accept',
    'actor': 'https://example.com/test', # フォローを受け付けるアカウントのPersonのid
    'object': {
        # FollowアクティビティのJSON
    }
}

INBOXへPOSTする

MastodonのINBOXへアクティビティを届けるためには、公開鍵暗号方式による認証処理が必要になる。

INBOXの解決

INBOXの解決は、アクティビティのactorへの問い合わせで行う。Mastodonでは次のようにしてJSONを要求するとPersonが返ってくる。この要求には認証処理は必要でない。

lib.py
import requests

def actor_inbox(actor):
    response = requests.get(actor+".json")
    if response.status_code >= 400 and response.status_code < 600:
        # エラー処理
    return response.json()['inbox']

公開鍵、秘密鍵の生成

RSA暗号で公開鍵と秘密鍵を生成する。

$ pip install pycrypto
lib.py
from Crypto.PublicKey import RSA
from Crypto import Random

...

def generate_key_pair():
    rsa = RSA.generate(2048, Random.new().read)
    return [rsa.exportKey().decode('utf-8'), rsa.publickey().exportKey().decode('utf-8')]

公開鍵をPersonのJSONに付加する。

{
    '@context': 'https://www.w3.org/ns/activitystreams',
    'type': 'Person',
    ...
    'publicKey': { # Keyアクティビティ
        '@context': 'https://www.w3.org/ns/activitystreams',
        'type': 'Key',
        'id': 'https://example.com/test/pubkey', # keyのid(?)
        'owner': 'https://example.com/test', # Personのid
        'publicKeyPem': '-----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY-----' # PEM形式の公開鍵
    }
}

アカウント解決時にこのPersonを返す事でMastodon側に公開鍵が保持される。

キーペアを用いた署名の生成

MastodonのINBOXへのPOST時に行われる認証処理は、このキーペアで生成した署名で行われる。

pip install httpsig
lib.py
from httpsig import HeaderSigner
from datetime import datetime

...

def sign_headers():
    sign = HeaderSigner(
        'https://example.com/test', # keyId Personのid
        '-----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----', # 秘密鍵
        algorithm='rsa-sha256',
        headers=['(request-target)', 'date']
    ).sign({'Date': datetime.now().isoformat()}, method=method, path=path)
    auth = sign.pop('authorization')
    sign['Signature'] = auth[len('Signature '):] if auth.startswith('Signature ') else ''
    return sign

AcceptをINBOXへPOSTする実装(署名付きヘッダー)

pip install requests
lib.py
import requests

...

def accept(inbox, activity):
    jsn = {
        ...
        'object': activity
    }
    headers = sign_headers()
    response = requests.post(inbox, json=jsn, headers=headers)
    if response.status_code >= 400 and response.status_code < 600:
        # エラー処理
api.py
import json
from flask import Flask, Response
from httpsig import HeaderSigner
from datetime import datetime
from .lib import actor_inbox, accept

app = Flask(__name__)

@app.route('/inbox', methods=['POST'])
def inbox():
    if request.headers['Content-Type'] != 'application/activity+json':
        return Response(status=400)

    jsn = request.json
    if type(jsn)!=dict or 'type' not in jsn:
        return Response(status=400)
    elif jsn['type'] == 'Follow' and 'actor' in jsn:

        # Follow処理を書く

        inbox = actor_inbox(jsn['actor'])
        accept(inbox, jsn)

        return Response(status=200)
    elif jsn['type'] == 'Undo':
        obj = jsn['object']
        if type(obj)!=dict or 'type' not in obj:
            return Response(status=400)
        elif obj['type'] == 'Follow' and 'actor' in obj:

            # Unfollow処理を書く

            inbox = actor_inbox(obj['actor'])
            accept(inbox, jsn)

            return Response(status=200)

    return Response(status=501)

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

Acceptを返すことでフォロー処理が完了し、Mastodon側でもフォロー関係が反映される。

投稿をMastodonに反映させる

新しい記事の投稿をMastodonに反映させるには、フォロワーのINBOXに対して以下のCreateアクティビティをPOSTする。

{
    `@context`: 'https://www.w3.org/ns/activitystreams',
    'type': 'Create',
    `object`: {
        # 投稿するNoteアクティビティのJSON
    }
}

これをフォロワーのINBOXへ、AcceptのPOSTと同様に署名付きヘッダーを付けて送ればMastodonのフォロワーのHTLやインスタンスのFTLに反映される。

成果物をGithubに公開しています。

23
14
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
14