地元サッカークラブのサポーターをしているのですが、マイナーなクラブだけにクラブ HP のリリースやニュースなどが Twitter 公式アカウントでツイートされないことが多く、それなら bot を作って情報発信のサポートもしようということで、非公式ながら運用開始してはや7年目のシーズンになります。
クラブ公式 HP のリリース更新をトリガーにタイトルとページリンク、画像があれば画像も含めて bot ツイートするだけです。
先日 Twitter API v1.1 のサポートが終了し、とうとう bot ツイートできなくなってしまいました。2分毎に動く自動起動バッチから大量のエラーメールが...
エラー履歴
日付 | 経緯 | 対応 |
---|---|---|
2023/05/23 | Twitter API が SUSPENDED になり bot ツイートでエラーとなる | 「プランが未選択」のため「Free」を選択したところ解決 ※参考 |
2023/07/19 | TwitterAPI v1.1 サポート終了につき再び bot ツイートでエラーとなる | Twitter API サイトで v2 用プロジェクトの作成とアプリ追加 ※参考 TwitterOAuth を使用し API v2 対応 |
エンドポイントサポート状況
Twitter API v1.1 と v2 のエンドポイントのサポート状況は以下の通りです(2023/07/31現在)。
Twitter API Endopoint | v1.1 | v2 |
---|---|---|
upload | 〇 | - |
tweets | × | 〇 |
今まで dg/twitter-api というライブラリを使用していましたが、すでに更新が止まっているため、Twitter API v2 対応のメジャーな Abraham/TwitterOAuth に乗り換えることにしました。
Twitter API v2 対応
初期化
<?php
require "vendor/autoload.php";
use Abraham/TwitterOAuth/TwitterOAuth;
$consumer_key = "your consumer key";
$consumer_secret = "your consumer secret";
$access_token = "your access token";
$access_token_secret = "your access token secret";
$twitter = new TwitterOAuth($consumer_key, $consumer_secret, $access_token, $access_token_secret);
アップロード
// media/upload v1.1
foreach ($media_url_list as $media_url) {
$stream = file_get_contents($media_url, false, $context);
$media_data = base64_encode($stream);
$parameters = array('media' => $media_data);
$twitter->setApiVersion('1.1');
$response = $twitter->upload('media/upload', $parameters); // エラーになる
if (isset($response->media_id_string)) {
$media_id = $response->media_id_string;
array_push($media_ids, $media_id);
}
}
Twitter API のアップロードのエンドポイントは未だ v1.1 で v2 を指定するとエラーになるため、明示的に 1.1 を指定しています。 setApiVersion() の 1.1 と 2 の違いは URLの末尾 に .json が付くか付かないかだけです。
ただこのコードでは残念ながら TwitterOAuth::upload() メソッドでエラーとなります。
今までクラブ HP のリリースページの画像URLを file_get_contents() で読み込んで一旦ファイルには落とさずそのままアップロードしていましたが、TwitterOAuth::upload() メソッドは Base64 エンコード文字列のアップロードをサポートしておらず、ファイルパス文字列のみサポートしていますのでこのままでは動作しません。Twitter API はサポートしているのですが、 TwitterOAuth が未サポートとなります。普通にメディアのパス文字列を指定するのであれば TwitterOAuth::upload() で問題ありません。
OK : $parameters = array('media' => '/path/to/media.jpg');
NG : $parameters = array('media' => $media_data);
またメモリを意識しないのかというご指摘には富豪的プログラミングということでご容赦ください。まあ NotChunked() を利用するのでしたらあまり変わらないかと。
2019 年あたりでは post() メソッドでサポートするような履歴 Issues#792, Support POST requests to the UPLOAD があったのですが、今も実装されていないです。
一旦ファイルには落としたくないので、イレギュラーですが、 private な http() メソッドを Closure::bind() で叩く方法で逃げました。隠蔽が無駄じゃんとかはなしで笑。まあ protected でもよいのではないかと。
$closure_http = function ($parameters) {
return $this->http('POST', 'https://upload.twitter.com', 'media/upload', $parameters, false);
};
foreach ($media_url_list as $media_url) {
$stream = file_get_contents($media_url, false, $context);
$media_data = base64_encode($stream);
$parameters = array('media' => $media_data);
$twitter->setApiVersion('1.1');
$closure_bind = Closure::bind($closure_http, $twitter, 'Abraham\TwitterOAuth\TwitterOAuth');
$response = $closure_bind($parameters);
if (isset($response->media_id_string)) {
$media_id = $response->media_id_string;
array_push($media_ids, $media_id);
}
}
ツイート
// tweets v2
$parameters = array('text' => $title);
if (!empty($media_ids)) {
$parameters['media'] = array('media_ids' => $media_ids);
}
$twitter->setApiVersion('2');
$response = $twitter->post('tweets', $parameters, true);
アップロード画像があれば media_ids 配列を parameters にセットして v2 を指定して POST します。 v1.1 の時は media_ids に media_id をカンマ区切りで指定しましたが、 v2 では media.media_ids に media_id 配列を指定します。また v1.1 では media_ids 値があってもなくてもツイートできていましたが、 v2 では media_ids 値が null や空の場合はエラーとなるようです。
Twitter API | v1.1 | v2 |
---|---|---|
Tweets Endopoint | POST statuses/update | POST /2/tweets |
v1.1 : $parameters = array('media_ids' => implode(',', $media_ids));
v2 : $parameters['media'] = array('media_ids' => $media_ids);
番外編 ~ TwitterOAuth の変更 ~
オリジナルなソースコードに手を加えるのはどうかとは思うのですが、当初は TwitterOAuth::uploadMediaNotChunked() メソッドを変更しようと思っていましたのでその破片も残しておきます。
オリジナル : TwitterOAuth.php.org
337 private function uploadMediaNotChunked(string $path, array $parameters)
338 {
339 if (
340 !is_readable($parameters['media']) ||
341 ($file = file_get_contents($parameters['media'])) === false
342 ) {
343 throw new \InvalidArgumentException(
344 'You must supply a readable file',
345 );
346 }
347 $parameters['media'] = base64_encode($file);
348 return $this->http(
349 'POST',
350 self::UPLOAD_HOST,
351 $path,
352 $parameters,
353 false,
354 );
355 }
以下のような感じで base64 エンコード文字列のアップロードもサポートしてくれるとよいのですが。。。
変更後 : TwitterOAuth.php
337 private function uploadMediaNotChunked(string $path, array $parameters)
338 {
339 if (
340 is_readable($parameters['media']) &&
341 ($file = file_get_contents($parameters['media'])) !== false
342 ) {
343 $parameters['media'] = base64_encode($file);
344 } elseif (preg_match('%^[a-zA-Z0-9/+]*={0,2}$%', $parameters['media']) &&
345 base64_encode(base64_decode($parameters['media'], true)) === $parameters['media']) {
346 // already base64 encoded
347 } else {
348 throw new \InvalidArgumentException(
349 'You must supply a readable file or base64 data',
350 );
351 }
352 return $this->http(
353 'POST',
354 self::UPLOAD_HOST,
355 $path,
356 $parameters,
357 false,
358 );
359 }
# diff -u TwitterOAuth.php.org TwitterOAuth.php
--- TwitterOAuth.php.org 2023-07-20 11:43:02.000000000 +0900
+++ TwitterOAuth.php 2023-07-25 12:29:53.000000000 +0900
@@ -337,14 +337,18 @@
private function uploadMediaNotChunked(string $path, array $parameters)
{
if (
- !is_readable($parameters['media']) ||
- ($file = file_get_contents($parameters['media'])) === false
+ is_readable($parameters['media']) &&
+ ($file = file_get_contents($parameters['media'])) !== false
) {
+ $parameters['media'] = base64_encode($file);
+ } elseif (preg_match('%^[a-zA-Z0-9/+]*={0,2}$%', $parameters['media']) &&
+ base64_encode(base64_decode($parameters['media'], true)) === $parameters['media']) {
+ // already base64 encoded.
+ } else {
throw new \InvalidArgumentException(
- 'You must supply a readable file',
+ 'You must supply a readable file or base64 data',
);
}
- $parameters['media'] = base64_encode($file);
return $this->http(
'POST',
self::UPLOAD_HOST,
おわりに
イーロン・マスクよ、勘弁してよ怒