Edited at

[PHP] ライブラリに頼らないTwitterAPI入門

More than 3 years have passed since last update.


前書き

「ライブラリで何してんのか分からないから気持ち悪いんじゃヴォエエエ」 って人は読んでみるといいと思うよ(震え声)

HTTP通信について全く知らない人は、先に以下のリンク先を読んでおいてください。

※ HTTP通信における改行コードは全て \r\n です。


GETリクエストヘッダーの例

GET /1.1/statuses/show.json?id=464526017030545409 HTTP/1.1

Host: api.twitter.com
Authorization: OAuth oauth_consumer_key="***", oauth_signature_method="HMAC-SHA1", oauth_timestamp="***", oauth_version="1.0a", oauth_nonce="***", oauth_signature="***", oauth_token="***"
Connection: close



通常のPOSTリクエストヘッダーの例

POST /1.1/statuses/update.json HTTP/1.1

Host: api.twitter.com
Authorization: OAuth oauth_consumer_key="***", oauth_signature_method="HMAC-SHA1", oauth_timestamp="***", oauth_version="1.0a", oauth_nonce="***", oauth_signature="***", oauth_token="***"
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 11

status=test



マルチパートのPOSTリクエストヘッダーの例

POST /1.1/statuses/update_with_media.json HTTP/1.1

Host: api.twitter.com
Authorization: OAuth oauth_consumer_key="***", oauth_signature_method="HMAC-SHA1", oauth_timestamp="***", oauth_version="1.0a", oauth_nonce="***", oauth_signature="***", oauth_token="***"
Connection: close
Content-Type: multipart/form-data; boundary=-----hogehoge
Content-Length: 232

-------hogehoge
Content-Disposition: form-data; name="status"

test
-------hogehoge
Content-Disposition: form-data; name="media[]"; filename="test.png"
Content-Type: application/octet-stream

臼NG........
-------hogehoge--



OAuth認証の流れ

ライブラリを使ってTwitter連携機能を実装したことのある人なら分かりきっていると思いますが、念のため分かりやすい図を@ITさんの記事より引用させていただきます。現状メインで使われているのはやはりOAuth1.0および1.0aなので、ここではそれに限定して説明していきます。

01.jpg


oauth_signature 生成のために必要なパラメータ

ここでは便宜上


  • 送信する情報の本体
    「ベース」

  • 送信する情報の正当性を保証する署名に使われる部分
    「キー」

と呼ぶことにします。


oauth/request_token の場合


ベース

要素名
説明

oauth_consumer_key
コンシューマーキー

oauth_signature_method
"HMAC-SHA1"

oauth_timestamp
タイムスタンプ

oauth_version
"1.0a"

oauth_nonce

bin2hex(openssl_random_pseudo_bytes(16)) で生成した乱数ハッシュなど

oauth_callback
認証後にリダイレクトして戻ってくるURL



  • oauth_callback は省略可能です。省略した場合はコンシューマーキー登録時のものが使用されます。


  • oauth_callback"oob" を指定した場合、リダイレクトする代わりにPIN入力になります。


キー

オフセット
説明

0
コンシューマーシークレット

1
"" (空文字列)


oauth/access_token の場合


ベース

要素名
説明

oauth_consumer_key
コンシューマーキー

oauth_signature_method
"HMAC-SHA1"

oauth_timestamp
タイムスタンプ

oauth_version
"1.0a"

oauth_nonce

bin2hex(openssl_random_pseudo_bytes(16)) で生成した乱数ハッシュなど

oauth_verifier
ベリファイア(PIN)

oauth_token
リクエストトークン


キー

オフセット
説明

0
コンシューマーシークレット

1
リクエストトークンシークレット


その他のリクエストの場合(通常)


ベース

※ 追加パラメータは oauth_signature 生成より後には除外されます。

要素名
説明

oauth_consumer_key
コンシューマーキー

oauth_signature_method
"HMAC-SHA1"

oauth_timestamp
タイムスタンプ

oauth_version
"1.0a"

oauth_nonce

bin2hex(openssl_random_pseudo_bytes(16)) で生成した乱数ハッシュなど

oauth_token
アクセストークン

パラメータ1の名称
パラメータ1の値

パラメータ2の名称
パラメータ2の値

パラメータ3の名称
パラメータ3の値

...
...


キー

オフセット
説明

0
コンシューマーシークレット

1
アクセストークンシークレット


その他のリクエストの場合(マルチパート)


ベース

要素名
説明

oauth_consumer_key
コンシューマーキー

oauth_signature_method
"HMAC-SHA1"

oauth_timestamp
タイムスタンプ

oauth_version
"1.0a"

oauth_nonce

bin2hex(openssl_random_pseudo_bytes(16)) で生成した乱数ハッシュなど

oauth_token
アクセストークン


キー

オフセット
説明

0
コンシューマーシークレット

1
アクセストークンシークレット


Authorization ヘッダー生成の手順

上記の 「その他のリクエストの場合(通常)」POST statuses/update を利用する場合の具体例を示します。


1. ベースとキーの準備

// 最後まで使われる基本パラメータ

$oauth_params = [
'oauth_consumer_key' => 'コンシューマーキー',
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0a',
'oauth_nonce' => bin2hex(openssl_random_pseudo_bytes(16)),
'oauth_token' => 'アクセストークン',
];

// oauth_signature生成まで使われる追加パラメータ
$additional_params = [
'status' => 'ツイート内容',
'in_reply_to_status_id' => 'リプライ先ステータスID',
];

// ベース
$base = $oauth_params + $additional_params;

// キー
$key = ['コンシューマーシークレット', 'アクセストークンシークレット'];


2. ベースを要素名自然順でソートする

通常のソートだと

a1, a10, a2, a3, a4, a5, a6, a7, a8, a9

となってしまうところを

a1, a2, a3, a4, a5, a6, a7, a8, a9, a10

とするのが自然順ソートです。以下のように記述します。

uksort($base, 'strnatcmp');

実際にこのような差異がつくパラメータ名はTwitterAPIには採用されていないので、

ksort($base);

と書いても特に問題ないかもしれません。


3. ベースをRFC3986に従ってクエリストリングにする

$base = http_build_query($base, '', '&', PHP_QUERY_RFC3986);


4. 所定の手順に従ってベースを再構成する

※ GETリクエストの場合、この「URL」にクエリストリングは含めません。

// リクエストメソッド、URL、ベースからなる配列を作る

$base = ['POST', 'https://api.twitter.com/1.1/statuses/update.json', $base];

// RFC3986に従ったURLエンコードを適用
$base = array_map('rawurlencode', $base);

// 「&」で結合する
$base = implode('&', $base);


5. 所定の手順に従ってキーを再構成する

// RFC3986に従ったURLエンコードを適用

$key = array_map('rawurlencode', $key);

// 「&」で結合する
$key = implode('&', $key);


6. oauth_signature を生成して基本パラメータに追加する

HMACとかSHA-1とか聞かれても答えられないので暗号アルゴリズムについて自力で模索するぐらい意識の高い人以外は おまじない として軽くスルーしてください。Twitterがこの方法で署名を作れとしているので、単にそれに従っているだけです。

$oauth_params['oauth_signature'] = base64_encode(hash_hmac('sha1', $base, $key, true));


7. ヘッダーの形に仕上げて完成

foreach ($oauth_params as $name => $value) {

$items[] = sprintf('%s="%s"', urlencode($name), urlencode($value));
}
$header = 'Authorization: OAuth ' . implode(', ', $items);

ここまでの手順を最短でまとめると以下のようになります。


PHP5.4以降のみ

uksort($base, 'strnatcmp');

$oauth_params['oauth_signature'] = base64_encode(hash_hmac(
'sha1',
implode('&', array_map('rawurlencode', [
'POST',
'https://api.twitter.com/1.1/statuses/update.json',
http_build_query($base, '', '&', PHP_QUERY_RFC3986)
])),
implode('&', array_map('rawurlencode', $key)),
true
));
foreach ($oauth_params as $name => $value) {
$items[] = sprintf('%s="%s"', urlencode($name), urlencode($value));
}
$header = 'Authorization: OAuth ' . implode(', ', $items);


PHP5.3以前でも可能な方法

uksort($base, 'strnatcmp');

$oauth_params['oauth_signature'] = base64_encode(hash_hmac(
'sha1',
implode('&', array_map('rawurlencode', array(
'POST',
'https://api.twitter.com/1.1/statuses/update.json',
str_replace(
array('+', '%7E'),
array('%20', '~'),
http_build_query($base, '', '&')
)
))),
implode('&', array_map('rawurlencode', $key)),
true
));
foreach ($oauth_params as $name => $value) {
$items[] = sprintf('%s="%s"', urlencode($name), urlencode($value));
}
$header = 'Authorization: OAuth ' . implode(', ', $items);


リクエストの準備

fsockopen 関数などを使ってリクエストヘッダーをガリガリ書くよりも cURL関数群 を利用したほうがはるかに容易なので、後者の方法で実装することにします。


GETリクエストの場合

生成済みの Authorization ヘッダー $header を利用して GET users/show にリクエストする場合の具体例を示します。

$url = 'https://api.twitter.com/1.1/users/show.json';

$params = ['screen_name' => 'mpyw'];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url . '?' . http_build_query($params, '', '&'),
CURLOPT_HTTPHEADER => [$header],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'gzip',
]);


POSTリクエストの場合(通常)

生成済みの Authorization ヘッダー $header を利用して POST statuses/update にリクエストする場合の具体例を示します。

$url = 'https://api.twitter.com/1.1/statuses/update.json';

$params = ['status' => 'test'];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params, '', '&'),
CURLOPT_HTTPHEADER => [$header],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'gzip',
]);


POSTリクエストの場合(マルチパート)

生成済みの Authorization ヘッダー $header を利用して POST statuses/update_with_media にリクエストする場合の具体例を示します。 http_build_query 関数を通さず、直接ポストフィールドに連想配列を渡すとマルチパートリクエストになります。


PHP5.5以降のみ

$url = 'https://api.twitter.com/1.1/statuses/update_with_media.json';

$params = [
'status' => 'test',
'media[]' => new CURLFile('/home/mpyw/test.png'),
];
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $params,
CURLOPT_SAFE_UPLOAD => true,
CURLOPT_HTTPHEADER => [$header],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => 'gzip',
]);

PHP5.4以前では CURLFile クラスや CURLOPT_SAFE_UPLOAD フラグが使えないため、少々面倒な方法を採ることになります。詳しくは以下を参照してください。


リクエストの実行・レスポンスのパース・例外処理


例外処理の前提

何らかのエラーが発生していた場合、以下のような形式で例外をスローすることにします。

throw new RuntimeException('エラーメッセージ', 'HTTPステータスコード');


奇形フォーマットへの対応

Twitterには歴史的な理由でさまざまなフォーマットが存在するので、その全てに対応しなければなりません。その際の Content-Type もアテにならないので、実際にそれぞれの形式でデコードしてみる必要があります。


リクエスト成功時のレスポンス


最も基本的なパターン

{

"id" : 123456,
"id_str" : "123456"
}


タイムライン取得など

[

{
"id" : 123456,
"id_str" : "123456"
},
{
"id" : 123456,
"id_str" : "123456"
}
]


トークン取得時

oauth_token=****&oauth_token_secret=****



リクエスト失敗時のレスポンス


最も基本的なパターン

{

"errors" : [
{
"code" : 123,
"message" : "ERROR"
}
]
}


公式リツイート失敗時

{

"errors" : "sharing is not permissible for this status (Share validations failed)"
}


許可の無いユーザのタイムラインを取得しようとしたとき

{

"request" : "\/1.1\/statuses\/user_timeline.json?screen_name=******",
"error" : "Not authorized."
}


トークン取得においてシグネチャが不正であったとき

Failed to validate oauth signature and token



アクセストークン取得においてリクエストトークンが有効期限切れのとき

<?xml version="1.0" encoding="UTF-8"?>

<hash>
<error>Invalid / expired Token</error>
<request>/oauth/access_token</request>
</hash>


ユーザーストリーム規制時

Exceeded connection limit for user



ユーザーストリーム取得失敗時

<html>\n<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>\n<title>Error 401 Unauthorized</title>

</head>
<body>
<h2>HTTP ERROR: 401</h2>
<p>Problem accessing '/1.1/user.json'. Reason:
<pre> Unauthorized</pre>
</body>
</html>


高負荷でサーバーにリクエストを拒絶されたとき

(空文字列)


XMLやHTMLを突然返してきたり、 \n という文字がそのまま返されてたりしてさんざんですね。


関数の実装

リクエストの準備を行ったcURLリソース $ch を渡し、リクエストを実行してレスポンスをデコードしてオブジェクトまたは配列で返す関数を実装してみます。なお、実際にストリーミングAPIを利用するときにこの関数は使わないので、これらに関する例外処理は行いません。

/**

* 通常のcURLリクエストを実行します。
*
* @param resource $ch cURLリソース
* @return stdClass|array デコード済みのメッセージ
* @throw RuntimeException
*/

function curl_exec_decode($ch)
{
// リクエストを実行
$response = curl_exec($ch);

// cURLのメタデータを取得
$info = curl_getinfo($ch);

// cURLの実行自体にエラーが発生しているか
if (curl_errno($ch)) {
throw new RuntimeException(curl_error($ch), $info['http_code']);
}

// レスポンスがJSON形式またはXML形式であるか
if (
null !== $obj = json_decode($response) or
false !== $obj = json_decode(json_encode(@simplexml_load_string($response)))
) {
// レスポンスが文字列形式のプロパティerrorを持っているか
if (isset($obj->error)) {
throw new RuntimeException($obj->error, $info['http_code']);
}
// レスポンスがプロパティerrorsを持っているか
if (isset($obj->errors)) {
if (is_string($obj->errors)) {
// 文字列形式の場合
throw new RuntimeException($obj->errors, $info['http_code']);
} else {
// 配列形式の場合
throw new RuntimeException($obj->errors[0]->message, $info['http_code']);
}
}
return $obj;
}

// クエリストリングとしてパース
parse_str($response, $obj);
$obj = (object)$obj;

// レスポンスが妥当な形式のクエリストリングであるか
if (isset($obj->oauth_token, $obj->oauth_token_secret)) {
return $obj;
}

// レスポンスはテキスト形式のエラーであるか
if (strip_tags($response) === $response) {
throw new RuntimeException(trim($response), $info['http_code']);
}

// 形式不明のレスポンスであった
throw new RuntimeException('Malformed response detected.', $info['http_code']);
}


ストリーミングAPIへの対応

ストリーミングAPIは無限にレスポンスを返し続けるので、 curl_exec の実行時間が無限大になります。通常の方法では対応できません。

生成済みの Authorization ヘッダー $header を利用して GET statuses/sample にリクエストする場合の具体例を示します。

$url = 'https://stream.twitter.com/1.1/statuses/sample.json';

// コールバック関数を作成する(内部でstatic変数を利用しているため、必ずクロージャでなければならない)
$callback = function ($ch, $str) {
// メッセージが蓄積されるバッファ
static $buffer = '';

// 新規メッセージを追記する
$buffer .= $str;

// デコード可能な状態であるか(末尾が改行コードであるか)
if ($buffer[strlen($buffer) - 1] === "\n") {
if (null !== $obj = json_decode($buffer)) {
$info = curl_getinfo($ch);
if (preg_match("@Reason:\n<pre>([^<]++)</pre>@", $buffer, $matches)) {
// レスポンスがHTML形式のエラーである場合
throw new RuntimeException(trim($matches[1]), $info['http_code']);
}
if (strip_tags($buffer) === $buffer) {
// レスポンスがテキスト形式のエラーである場合
throw new RuntimeException(trim($buffer), $info['http_code']);
}
// 形式不明のレスポンスであった
throw new RuntimeException('Malformed response detected.', $info['http_code']);
}
if (isset($obj->disconnect)) {
// レスポンスがJSON形式だが切断されたことを意味する場合
$info = curl_getinfo($ch);
throw new RuntimeException($obj->disconnect->reason, $info['http_code']);
};
// メッセージを使って目的の処理を行う(ここではダンプしている)
var_dump($buffer);
// バッファを空にする
$buffer = '';
}

// 追記された長さを返す
return strlen($str);
};

$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => [$header],
CURLOPT_ENCODING => 'gzip',
CURLOPT_TIMEOUT => 0, // 無限に実行し続ける
CURLOPT_WRITEFUNCTION => $callback, // 各メッセージを処理する関数として設定する
]);
curl_exec($ch); // エラーが発生するまでここで停滞する

// ここに到達したことはcURL自体のエラーを意味する
$info = curl_getinfo($ch);
throw new RuntimeException(curl_error($ch), $info['http_code']);


活用例


ライブラリの作成例

例外処理をガチガチに固めてあるので、これを使えば原因不明のエラーに悩まされることが無くなります。