JavaScriotでツイートしたいなと思って、いろいろ試していたら、
30秒以上動画つきツイートが結構めんどくさかったので、その時の備忘録。
Node.jsでTwitter APIを使う
Node.jsでTwitter APIを使うときは、desmondmorris/node-twitterを使うのが良さそう
インストール
$ npm install twitter
ツイートしてみる
文字だけをツイートするのは、こんな感じ。
import Twitter from "twitter"; // 初期化 const client = new Twitter({ consumer_key: TWITTER_CONSUMER_KEY, consumer_secret: TWITTER_CONSUMER_SECLET, access_token_key: ACCESS_TOKEN_KEY, access_token_secret: ACCESS_TOKEN_SECRET }); // 文字だけをツイート async function tweet(text: string) { const tweet = await client.post("statuses/update", { status: text }); } tweet("ツイート").then();
Twitterクラスに.post()
や、.get()
が用意されているので、
Twitter APIのドキュメントを見ながら、呼び出していく感じ。
ツイートするのはPOST statuses/updateなのでドキュメントを参照。
画像つきでツイートしてみる
画像とか動画とかメディアつきだとちょっとめんどくさく...
ツイートと一緒に画像をアップロードできないので、
- 最初に画像をアップロードしてからmediaIdを取得し、
- mediaIdと一緒に
statuses/update
でツイート
という段階的な感じになる。
import Twitter from "twitter"; const client = // 略 async function tweetWithImage(text: string, filePath: string) { const data = require('fs').readFileSync(filePath); // 画像をアップロード const media = await client.post('media/upload', {media: data}); // mediaIdをパラメタに追加して、ツイート const params = { status: text, media_ids: media.media_id_string }; const tweet = await client.post("statuses/update", params); } tweetWithImage("ツイート", "./imange.jpg").then();
複数の画像をつけたい場合は、それぞれアップロードして、
media_ids
にカンマ区切りでmediaIdを指定する。
ただ、このmedia/upload
を1度だけ呼び出すシンプルな方法には制限があり、
GIFや動画はアップロードできない...
30秒以下の動画付きツイートをしてみる
動画やGIFをアップロードしたい場合は、Chunked media uploadという形でアップロードする必要がある。
この方法は、大きく3ステップに分かれている
- 初期化: command=INIT
- アップロード: command=APPEND
- 完了: commaind=FINALIZE
import Twitter from "twitter"; const client = // 略 async function tweetWithChunkedMedia(text: string, filePath: string) { const mediaType = 'video/mp4'; const mediaData = require('fs').readFileSync(filePath); const mediaSize = require('fs').statSync(filePath).size; // 動画をアップロード: INIT const media = await client.post('media/upload', { command : 'INIT', total_bytes: mediaSize, media_type : mediaType }); // INITでmediaIdが発行されるので、取得しておく const mediaId = media.media_id_string; // 動画をアップロード: UPLOAD await client.post('media/upload', { command : 'APPEND', media_id : mediaId, media : mediaData, segment_index: 0 }); // 動画をアップロード: FINALIZE await client.post('media/upload', { command : 'FINALIZE', media_id: mediaId }); // mediaIdをパラメタに追加して、ツイート const params = { status: text, media_ids: mediaId }; const tweet = await client.post("statuses/update", params); } tweetWithChunkedMedia("ツイート", "./video.mp4").then();
動画やGIFのような大きいサイズのメディアは、分割してアップロードできるこの仕組みを使うっぽい。
ただ、30秒以上の動画や1MB(チャンクサイズ上限)を超える場合は、
INIT時にmedia_category
を指定して、非同期アップロードをしないといけない。
30秒を超える動画付きツイートをしてみる
30秒を超える動画は、media_categoryをつけ、非同期アップロードで対応しないといけない。
media_categoryは、tweet_image
, tweet_gif
, tweet_video
を指定できるので、
アップロードするメディアに合わせて指定する。
また、チャンクサイズの上限が1MBなので、APPENDでデータをPOSTする際には注意。
1MB以上の場合は、1MB以下になるように分割し、segment_indexでindexを指定する。
import Twitter from "twitter"; const client = // 略 async function tweetWithChunkedMedia(text: string, filePath: string) { const mediaType = 'video/mp4'; const mediaData = require('fs').readFileSync(filePath); const mediaSize = require('fs').statSync(filePath).size; // 動画をアップロード: INIT const media = await client.post('media/upload', { command : 'INIT', total_bytes: mediaSize, media_type : mediaType, media_category: "tweet_video" // media_categoryを指定 }); const mediaId = media.media_id_string; // 動画をアップロード: UPLOAD await client.post('media/upload', { command : 'APPEND', media_id : mediaId, media : mediaData, segment_index: 0 }); // 動画をアップロード: FINALIZE await client.post('media/upload', { command : 'FINALIZE', media_id: mediaId }); // 動画をアップロード: STATUS while(true) { // アップロードのステータスをポーリング const status = await client.get('media/upload', { command : 'STATUS', media_id: mediaId }); if (status.processing_info.state == "succeeded") { // 完了したら、ポーリングを終了 break; } else if (status.processing_info.state == "failed") { // エラーになったら、例外を投げる throw new Error(status.processing_info.error.message); } else { // 処理中(in_progress)の場合は、指定された秒数分待つ await sleep(status.processing_info.check_after_secs + 1); } } // mediaIdをパラメタに追加して、ツイート const params = { status: text, media_ids: mediaId }; const tweet = await client.post("statuses/update", params); } function sleep(time: number) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), time * 1000); }); } tweetWithChunkedMedia("ツイート", "./video.mp4").then();
かなりハマったのが以下の2点。
- STATUSはFINALIZEしてからじゃないと、404が返ってくる
- FINALIZEのレスポンスにもprocessing_infoがあるが、
STATUSをしないと永遠にpending状態。(STATUSを呼ぶと処理が始まる)
このあたり、ドキュメントに詳しい説明がなくて、かなりハマった...
axiosを使って外部URLのメディアをツイートする
Cloud Storageにある画像/動画を含めてツイートしたかったので、
axiosを使って外部URLを取得する処理を加えてみたのがこれ。
new TwitterApi().postTweet("ツイート", ["https://..."]);
みたいに呼び出すと、ダウンロード/アップロード/ツイートできる。(はず...)
import Twitter from "twitter"; import axios from "axios"; /** * スリープ処理 * @param time スリープする秒数 */ function sleep(time: number) { return new Promise((resolve, reject) => { setTimeout(() => resolve(), time * 1000); }); } export default class TwitterApi { private client: Twitter; constructor() { this.client = new Twitter({ consumer_key: // TWITTER_CONSUMER_KEY, consumer_secret: // TWITTER_CONSUMER_SECLET, access_token_key: // ACCESS_TOKEN_KEY, access_token_secret: // ACCESS_TOKEN_SECRET }); } /** * ツイートするメイン処理 * @param text ツイート文 * @param medias 添付する外部URLのリスト */ public async postTweet(text: string, medias: string[] = []) { let mediaIds: string[] = []; if (medias.length > 0) { // メディアファイルがあれば、アップロードしてmediaIdを取得 mediaIds = await Promise.all( medias.map(async v => await this.uploadMedia(v.url)) ); } const res = await this.tweet(text, mediaIds); } /** * メディアのアップロード処理 * @param url メディアのURL */ private async uploadMedia(url: string) { // axiosを使って、メディアのデータを取得 const res = await axios.create({ responseType: "arraybuffer" }).get(url); const mediaData: ArrayBuffer = res.data; const mediaSize = res.headers["content-length"]; const mediaType = res.headers["content-type"]; // INIT: mp4かgifなら、media_categoryを指定する const initParams = { command: "INIT", total_bytes: mediaSize, media_type: mediaType }; if (mediaType == "video/mp4") { initParams["media_category"] = "tweet_video"; } else if (mediaType == "image/gif") { initParams["media_category"] = "tweet_gif"; } const data = await this.client.post("media/upload", initParams); const mediaId = data.media_id_string; // APPEND: 500Bくらいにチャンクを分けてアップロードする const chunkSize = 500000; const chunkNum = Math.ceil(mediaSize / chunkSize); for (let index = 0; index < chunkNum; index++) { const chunk = mediaData.slice(chunkSize * index, chunkSize * (index + 1)); const resAppend = await this.client.post("media/upload", { command: "APPEND", media_id: mediaId, media: mediaData.slice(chunkSize * index, chunkSize * (index + 1)), segment_index: index }); } // FINALIZE const resFinalize = await this.client.post("media/upload", { command: "FINALIZE", media_id: mediaId }); if (!resFinalize.processing_info) { // media_categoryをしていないと、processing_infoがない return mediaId; } else if (resFinalize.processing_info.state == "succeeded") { return mediaId; } else if (resFinalize.processing_info.state == "failed") { throw new Error(resFinalize.processing_info.error.message); } // STATUS while (true) { const resStatus = await this.client.get("media/upload", { command: "STATUS", media_id: mediaId }); if (resStatus.processing_info.state == "succeeded") { return mediaId; } else if (resStatus.processing_info.state == "failed") { throw new Error(resStatus.processing_info.error.message); } else { await sleep(resStatus.processing_info.check_after_secs + 1); } } } /** * ツイート処理 * @param text ツイート文 * @param mediaIds メディアIDのリスト */ private async tweet(text: string, mediaIds: string[] = []) { const params = { status: text }; if (mediaIds.length > 0) params["media_ids"] = mediaIds.join(","); const tweet = await this.client.post("statuses/update", params); return tweet; } }
若干ハマったのが、以下の2点
- axiosで取得する場合は、
{ responseType: "arraybuffer" }
でcreateしないといけない - cloudStrageでデータを取得できないので、downloadURLを取得しておかないといけない
以上!!
参考にしたサイト様
- TwitterAPIのアップロード系エンドポイントまとめ (140秒動画対応) - Qiita
- Uploading media — Twitter Developers
- Media best practices — Twitter Developers
- Chunked media upload — Twitter Developers
- Downloading images with node.js - Stack Overflow
- GET media/upload (STATUS) — Twitter Developers
- [axios] 画像データのレスポンスを取得する際にハマった話 - Qiita