LoginSignup
7
6

More than 1 year has passed since last update.

Qiitadon の Streaming API を受信する時の注意点

Last updated at Posted at 2018-05-15

Mastodon のストリーミング API を使いたいが、Qiitadon で動かない。

TL; DR

ホストを streaming.qiitadon.com、ポートを 4000 に変更しましょう。

Qiita の マストドン・インスタンス「Qiitadon」は、Pawoo などと Mastodon Streaming API のエンドポイントが以下のように異なっています。

項目 備考
ホスト名 streaming.qiitadon.com qiitadon.comは CDN を通すので、パススルー用にストリーミング専用のサブ・ドメインが用意されているようです。
ポート 4000 一般的には 443

PHP のパッケージを作った

他のインスタンス(Mastodon サーバ)でも、もっと簡単に Mastodon Streaming API が使えるようにクラスにしてみたのでよろしかったら使ってみて下さい。Packagist にも公開しているので、composer でインストールできます。

動作の仕組み、詳細や Pure PHP で自分で実装したい場合は TS; DR 参照

composerで
composer require keinos/mastodon-streaming-api-listener
使い方
<?php

require_once __DIR__ . '/../vendor/autoload.php';

// サーバーの URL を指定するだけでポートなども自動で検知します。
$conf = [
    'url_host' => 'https://qiitadon.com/',
    'type_stream' => 'local', // LTL を指定。指定しない場合は 'public'(デフォルト)に設定されます。
];

$listener = new \KEINOS\MSTDN_TOOLS\Listener\Listener($conf);

foreach($listener as $event_name => $data_payload) {
    echo 'Event name: ' . $event_name . PHP_EOL;
    echo 'Data: '. PHP_EOL;
    print_r(json_decode($data_payload));
}

TS; DR

この Qiitadon の Streaming API のアドレス streaming.qiitadon.com とポート 4000 は、ソースを覗いて見つけました。view-source:https://qiitadon.com/web/timelines/public の 26 行目です。

しかし、Mastodon API の仕様をよくよく読むと instance というエンドポイント(API のメソッドを実行する URL)がありました。その API を叩くとサラリと情報が JSON 形式で返ってきました。なんということでしょう。

インスタンス(Mastodon サーバー)情報の API エンドポイント

Qiitadonのinstanceエンドポイント(Mastodon情報取得URL)
https://qiitadon.com/api/v1/instance

instance
Informational endpoint to discover information about a Mastodon website.
instance | API Methods @ docs.joinmastodon.org より)

(筆者訳)
instance
Mastodon の Web サイトに関する情報を得るためのエンドポイント。

以下のリクエストすると、Qiitadon のインスタンス(サーバー)情報を取得できます。API を叩いて JSON から urls 要素を抜き出すと、ちゃんとポートが 4000 であることがわかります。プロトコルwss であることがわかります。つまり SSL/TLS 通信の WebSocket プロトコルということです。

$ curl -s https://qiitadon.com/api/v1/instance | jq .urls
{
  "streaming_api": "wss://streaming.qiitadon.com:4000"
}

🐒   WebSocket ライブラリ/パッケージを使って HTTP/1.1 401 Unauthorized のレスポンスが返ってくる場合は、アクセス・トークンを Qiitadon で発行して、リクエストのヘッダーに以下を加えてみてください。これはプロトコルを HTTP から WebSocket にアップグレードする際に、Mastodon はアクセス・トークンを必要とするからです。

Authorization: Bearer [アクセストークン]

PHP でストリーミングを受信するサンプル

Pure PHP

WebSocket のパッケージを使ってもいいのですが、ここでは Pure PHP + fsockopen() でソケットから生の情報を取得してみたいと思います。WebSocket を使う場合はアクセス・トークンを必要とするからです。

以下は、簡単な動作を確認する PHP スクリプトです。実行後、数秒待つとストリームが始まります。

sample.php
<?php

const CRLF="\r\n";

// ソケットの準備
$hostname = 'ssl://streaming.qiitadon.com';
$port     = 4000;
$timeout  = 5; //sec
$errno    = '';
$errstr   = '';

// ソケットオープン
$fp = fsockopen($hostname, $port, $errno, $errstr, $timeout);

if (! $fp) {
    die("Error: {$errstr} ({$errno})");
}

// リクエストヘッダの準備
$method     = 'GET';
$endpoint   = '/api/v1/streaming/public';
$host       = 'qiitadon.com'; //streaming.qiitadon.com でない
$user_agent = 'qithub-bot';

$req = [
    "{$method} {$endpoint} HTTP/1.1",
    "Host: {$host}",
    "User-Agent: {$user_agent}",
];

$glue = CRLF;
$req  = implode($glue, $req) . CRLF . CRLF;

// GETリクエストを送信
fwrite($fp, $req);

// ストリーミングの読み込み
while (! feof($fp)) {
    $read = fgets($fp);
    print_r($read);
    // バッファを出力(画面更新)
    @ob_flush();
    @flush();
}

fclose($fp);

上記スクリプトの簡易説明ですが、基本的に以下を行っています。

  1. Qiitadon と通信できるように fsockopen ソケットを作り、ファイルとして読み書きできるようにファイル・ポインタを取得する。
  2. その際の接続先アドレスを streaming.qiitadon.com、ポートを 4000 番、通信プロトコルを ssl に設定。(ssl://streaming.qiitadon.com
  3. ソケットに GET メソッドでデータを書き込む。(fwrite(<ソケットのファイル・ポインタ>,<データ>)
  4. EOF(データの終端)が送られて来るまでソケットを読みこむ。(fgets(<ソケットのファイル・ポインタ>)

TIPS

LTL を受信する

FTL(連合タイムライン)でなく、LTL(ローカル・タイムライン)のストリーミングが欲しい場合はエンドポイントは以下になります。public:local つまり /public/public/local になります。

  • /api/v1/streaming/public/local
    • streaming | timelines | methods @ Mastodon documentation

流れてくるデータ

WebSocket を使わない場合は、Server-Sent Events として送信されてきます。

SSL ポートにソケットを接続・オープンして、エンドポイントに GET リクエストすると HTTP ヘッダー情報(ハンドシェイク後のレスポンス・ヘッダー)が流れた後、「データサイズ」(chunk) → 「情報」(event or data)→ 空行 の順で流れてきます。

chunk size は「情報」の長さで、情報 + "\n" のバイト長になります。なお、「情報」のタイプが data: の場合、本体データを payload と呼びます。

情報タイプが"event"の場合のフォーマット
[chunk size]
event: [name]

イベントの例
e  ← 下記 "event: update\n" のバイト数
event: update

情報タイプが"data"の場合のフォーマット
[chunk size]
data: [payload]

データの例(payloadがチャンクでない/分割されてない場合)
588  ← 下記 "data: {"id":...}\n" のバイト数
data: {"id":...}

「情報」が長い場合は、分割されて送信されます。その場合も「データサイズ」→「情報」で流れてきます。受け手は分割されたデータを結合して使う必要があります。

データの例(payloadがチャンク/分割されてる場合)
5a3  ← 下記行のバイト数
data: {"id":"10 ... ssing.png
9a   ← 下記行のバイト数
","followe ... "tags":[],"emojis":[]}


注意すべきは改行コードです。基本的に CRLF\r\n)ですが、「情報」がチャンクでない(分割されていない)場合は LF\n)になります。また、その時「情報」のタイプが data であった場合は、さらに \n(LF, x0a)が追加されます。

そのため、条件による改行の組み合わせが複雑なので、受け取ったデータは CRLFLF に統一すると楽です。

このようにソケットの生情報をゴリゴリ解析して実装するのも楽しいのですが、やはり WebSocket の PHP クライアントのライブラリを使う方が楽です。しかし、WebSocket の場合は、アクセストークンが必須なので、ライブラリを作ってみました。(注意:サーバが "whitelist-mode" になっている場合は、

動作確認済み環境

参考文献

7
6
0

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
7
6