Mastodon의 API를 Misskey에서도 사용할 수 있게 해주면...

처음 뵙겠습니다. 저는 Cyber Rex라고 합니다.저는 고등학생으로 취미가 넓습니다.
Zenn에서 기사를 쓴 것은 이번이 처음이다.잘 부탁드립니다.

발동기를 켜다


Misskey에서 NowPlaying 4Droid를 사용하고 싶습니다.
NowPlaying 4Droid는 현재 듣는 곡을 공유할 수 있는 안드로이드 앱이다.
Mastodon에 대한 자동 투고는 지원되지만 Misskey는 지원되지 않습니다.

말하자면 미스키가 뭐냐면요.


Misskey는 ActivityPub을 따르는 웨이보 플랫폼입니다.

문제점


Activity Pub은 지원되지만 Mastodon과 API가 호환되지 않기 때문에 Mastodon의 API 응용 프로그램과 서비스로 Misskey에 대응하려면 중간 서버로 전환해야 합니다.
지금 해야 할 일을 그림으로 표현하는 것이 바로 이런 느낌이다.

애플리케이션 요구 사항 보기


먼저 애플리케이션이 액세스하는 API를 알아야 합니다.
Flash 서버를 404까지 적절하게 시작할 수도 있고 액세스할 수도 있기 때문입니다.
그래서 이런 기록이 내려왔다.x.x.x.x - - [06/Nov/2021 00:00:00] "POST /api/v1/apps HTTP/1.0" 404 -먼저 /api/v1/apps에서 POST를 진행하고 응용 프로그램을 등록합니다.
이 JSON 데이터가 전송되었습니다.
{
	"client_name": "NowPlaying4Droid",
	"redirect_uris": "...",
	"scopes": "read write follow"
}
규격은 Mastodon 공식 문서에 있습니다.
https://docs.joinmastodon.org/methods/apps/
서버는 프로그램이 등록된 것처럼 보이도록 이 요청을 처리할 수 있습니다.

이루어지다


애플리케이션 등록


대답할 때 프로그램의 ID와 비밀 키에 답장을 해야 합니다.
여기에 ID를 적용하면 UIDv4에서 적절하게 생성되어 사용자에게 전달됩니다.
ID는 Misskey와의 공동 작업이 필요하므로 데이터베이스에 저장됩니다.
    uid = str(uuid.uuid4())
    with db.cursor() as cur:
        cur.execute('INSERT INTO oauth_pending(session_id, client_name, scope, redirect_uri, instance_domain) VALUES(%s, %s, %s, %s, %s)',
        (uid, request.form['client_name'], request.form['scopes'], request.form['redirect_uris'], None))
        cur.execute('commit')
    return json.dumps({
        'id': 'fakeoauthid001',
        'client_name': request.form['client_name'],
        'redirect_uri': request.form['redirect_uris'],
        'client_id': uid,
        'client_secret': 'fake123'
    })
id、client_시크릿이 사용되지 않았기 때문에 적당한 값을 되돌려줍니다.

OAuth


응용 프로그램 등록이 성공했다는 것을 알았을 때 /oauth/authorizeclient_id, scope, redirect_uri의 파라미터를 추가하여 사용자에게 접근합니다.client_id 방금 만든 ID를 추가해야 하기 때문에 데이터베이스를 조회하고 존재하면 통과한다.
미스키는 분산 SNS이기 때문에 특정 영역만 활용되는 게 아니라 무수히 존재한다.따라서 입력 영역의 화면을 가질 수 있습니다.
    client_id = request.args['client_id']
    scope = request.args['scope']
    redirect_uri = request.args['redirect_uri']
    return render_template('select_instance.html', session_id=client_id)
사용자가 도메인을 입력한 후 서버에 보내고 MiAuth(Misskey만의 OAuth 인증 시스템)로 다시 지정하십시오.
    res = make_response('', 302)
    res.headers['Location'] = f'https://{instance_domain}/miauth/{session_id}?' + build_query(
        name=sesinfo['client_name']+' (via MaMi Integration)',
        callback=f'https://{request.host}/oauth/integration_callback/{instance_domain}',
        permission='read:account,write:notes,write:drive,read:drive'
    )
    return res
MiAuthhttps://{instance_domain}/miauth/{session_id}?name=ApplicaionName&callback=...&permission=...에서 URL을 사용하여 인증하고 session_id 중복되지 않으면 OK.permission에서 지정한 값은 Mastodon의 scope와 달리 상세한 항목을 설정합니다.최소한 계정 정보를 읽고 투고와 드라이브를 읽을 수 있다.
호출은 /oauth/integration_callback/{instance_domain}입니다.
데이터베이스에 기록되지 않은 이유는 코드를 최대한 줄이려고 하기 때문이다(뒤에 말한 바와 같이 영패를 검증하고 있다.
사용자 허가 후, 방금 전의 회신 URL은 첨부 ?session=SESSION_ID 형식으로 되돌아옵니다.Misskey에서 제공하는 세션 ID를 조회하여 적법성을 확인하고 액세스 토큰을 받습니다.https://{instance_domain}/api/miauth/{session_id}/check전화로 문의드립니다.
    oauth_req = requests.post(f'https://{domain}/api/miauth/{args["session"]}/check')
    if oauth_req.status_code != 200:
        return make_response('セッションが正当なものであることを確認できませんでした。(MIAUTH_FAILED_'+str(oauth_req.status_code)+')', 500)
    data = oauth_req.json()
    if not data['ok']:
        return make_response('セッションが無効です。(MIAUTH_INVALID_SESSION)', 403)
API에서 다음 응답을 반환합니다.
{
	"ok": true,
	"token": "...",
	"user": {...}
}
세션 ID가 유효하면 ok 진짜가 되고 방문 영패가token에 들어갑니다.
토큰은 임시 가상 Mastodon의 ID와 연결된 형태로 데이터베이스에 저장됩니다.
Misskey 토큰을 받으면 지정된 콜백 URL이 적용됩니다.
NowPlaying 4Droid라면 사용자 정의 방안으로 프로그램을 되돌려주는 규격이 됩니다.
이때 URI 뒤에 ?code=CODE를 붙인다.이것code은 이후에 사용할 것이고 이것도 UIDv4로 새로 제작하여 데이터베이스에 저장할 것이다.
응용 프로그램으로 돌아가면 마지막/oauth/tokenPOST를 통해 비행할 수 있습니다.
파라미터client_id,code,grant_type가 있습니다.
client_아이디는 제일 먼저 나온 아이디인데 코드는 아까 만든 아이디일 거예요.
grant_typeauthorization_code은 고정되어 있습니다.
세션 ID의 존재와 code 일치 여부를 확인하면 Mastodon 응용 프로그램의 영패를 발행하고 데이터베이스에 로그인하여 응답합니다.
    data = request.form
    session_id = data['client_id']
    with db.cursor(dictionary=True) as cur:
        cur.execute('SELECT * FROM oauth_pending WHERE session_id = %s', (session_id,))
        sesinfo = cur.fetchone()
        if not sesinfo:
            return make_response('No such session id', 400)
        if sesinfo['authcode'] != data['code']:
            return make_response('Invalid code', 400)
    # ベアラートークン登録
    access_token = str(uuid.uuid4())
    with db.cursor() as cur:
        cur.execute('INSERT INTO oauth(misskey_token, mstdn_token, instance_domain) VALUES (%s, %s, %s)'
            , (sesinfo['misskey_token'], access_token, sesinfo['instance_domain']))
        cur.execute('commit')
    # 応答
    return json.dumps({
        'access_token': access_token,
        'token_type': 'bearer',
        'scope': 'read write',
        'created_at': int(time.time())
    })
그리고 한 번은 POST가 응용 프로그램에서 /api/v1/accounts/verify_credentials로 던졌다.데이터베이스에서 찾으면 200과 401을 돌려줄게.
그리고 계좌가 정확하면 계좌 정보를 돌려줘야 한다.
따라서 미스키의 영패를 이용해 미스키의 계좌 정보를 얻고 마스토돈의 계좌 정보로 전환해 응답한다.
미스키 API에 대한 문의는 유주라이오 선생의 작품을 사용했다Misskey.py.
    from misskey import Misskey
    # Misskeyインスタンスに問い合わせ
    m = Misskey(address=oauth_info['instance_domain'] ,i=oauth_info['misskey_token'])
    try:
        profile = m.i()
    except:
        return make_response('Misskey API error', 500)
    # プロフィールデータ再構成
    profile_mastodon = {
        'id': profile['id'],
        'username': profile['username'],
        'acct': profile['username'],
        'display_name': profile['name'],
        'avatar': profile['avatarUrl'],
        'avatar_static': profile['avatarUrl'],
        'header': profile['bannerUrl'],
        'header_static': profile['bannerUrl'],
        'note': profile['description'],
        'url': profile['url'],
        'locked': profile['isLocked'],
        'bot': profile['isBot'],
        'followers_count': profile['followersCount'],
        'following_count': profile['followingCount'],
        'statuses_count': profile['notesCount'],
        'fields': profile['fields']
    }

    res = make_response(json.dumps(profile_mastodon), 200)
    res.headers['Content-Type'] = 'application/json'
    return res
이렇게 하면 OAuth를 실현할 수 있다.이 부분은 매우 길다.

미디어 업로드


매스톤에 언론에 올릴 때 /api/v1/media 쳐요.
업로드된 데이터를 Misskey에게 직접 전달하여 드라이브에 업로드합니다.
    # Misskeyのドライブにアップロード
    m = Misskey(address=oauth_info['instance_domain'] ,i=oauth_info['misskey_token'])
    try:
        # streamはBinaryIOクラス
        drive_file = m.drive_files_create(request.files['file'].stream)
    except Exception as e:
        print(e)
        return make_response('Misskey API error', 500)
파일 정보를 Mastodon으로 변환합니다.
Misskey에서는 파일 ID가 영문 숫자를 사용하고 Mastodon에서는 숫자만 사용하기 때문에 메모리에 변환표를 만든다.
    # Mastodonは数字のメディアIDのみ受け付けるため、変換テーブルに登録して投稿用に備える
    fake_drive_id = f'{time.time():.0f}{random.randint(0,999999)}'
    drive_mediaid_table[fake_drive_id] = drive_file['id']


    res = make_response(json.dumps({
        'id': fake_drive_id,
        'type': 'image',
        'url': drive_file['url'],
        'preview_url': drive_file['thumbnailUrl'],
        'remote_url': None,
        'text_url': None,
        'meta': {},
        'description': None,
        'blurhash': 'UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}'
    }))
    return res
이렇게 미디어 업로드가 이루어졌다.

비고 투고


마지막으로 나는 필기를 너에게 투고할 것이다.
마스토돈에서 /api/v1/statuses 던지면 타투가 가능할 것 같아요.
공개 설정 등을 사용하지 않기 때문에 미스키의 설정에 맞춰야 한다.
미디어를 추가할 수도 있습니다.메모리에 있는 변환표라면 첨부합니다.
모든 항목을 설정할 수 없습니다.이제 본문, 미디어 첨부파일, 공개 범위를 설정할 수 있습니다.
    data = request.form
    text = data.get('status')
    sensitive = data.get('sensitive') # unused
    visibility = data.get('visibility')
    if visibility == 'private':
        visibility = 'followers'
    elif visibility == 'unlisted':
        visibility = 'home'
    media_ids = data.getlist('media_ids[]')
    if not text:
        return make_response('No text', 400)
    # ...省略
    
    drive_ids = []
    for mid in media_ids:
        if mid in list(drive_mediaid_table.keys()):
            drive_ids.append(drive_mediaid_table[mid])
	    
    # Misskeyにノート投稿
    m = Misskey(address=oauth_info['instance_domain'] ,i=oauth_info['misskey_token'])
    try:
        if drive_ids:
            note_d = m.notes_create(text, file_ids=drive_ids, visibility=visibility)
        else:
            note_d = m.notes_create(text, visibility=visibility)
    except Exception as e:
        print(e)
        return make_response('Misskey API error', 500)
    
    note = note_d['createdNote']

    toot_data = {
        'id': note['id'],
        'created_at': note['createdAt'],
        'in_reply_to_id': None,
        'in_reply_to_account_id': None,
        'sensitive': sensitive,
        'spoiler_text': None,
        'visibility': visibility,
        'language': None,
        'content': text,
        'reblog': None
    }
    return make_response(json.dumps(toot_data), 200)
이렇게 하면 노트에 투고할 수 있다.

실제로 사용해볼게요.


NowPlaying 4Droid에서 Mastodon 인스턴스 주소를 입력한 위치에 변환 시스템의 주소mami.cyberrex.jp를 입력합니다.
그래서 선택 화면이 잘 나왔다.

계속 버튼을 누르면 Misskey 인증 화면으로 이동합니다.

라이센스를 사용하여 다양한 작업을 수행할 수 있으며 성공적으로 등록되었습니다.

바로 재생곡을 시도해 보자.그래서 잘 투고했습니다.공개 범위가 관심자에 한정된 것도 반영됐다.(오른쪽 위에 있는 키 아이콘)

동작 확인 완료.

끝말


NowPlaying 4Droid는 개방원으로, 실제로는 GiitHub에서 Issue를 받았어요.인 것 같다.아마도 거기서 Feature Request를 진행하는 것이 비교적 빠를 것이다.
그래도 공부를 조금 할 수 있어서 좋았어요.
이 전환 서비스는 마미mami.cyberrex.jp라는 이름으로 실행되고 있으니 꼭 시도해 보세요.

좋은 웹페이지 즐겨찾기