JavaScriotでツイートしたいなと思って、いろいろ試していたら、
30秒以上動画つきツイートが結構めんどくさかったので、その時の備忘録。
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なのでドキュメントを参照。
画像とか動画とかメディアつきだとちょっとめんどくさく...
ツイートと一緒に画像をアップロードできないので、
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や動画はアップロードできない...
動画やGIFをアップロードしたい場合は、Chunked media uploadという形でアップロードする必要がある。
この方法は、大きく3ステップに分かれている
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秒を超える動画は、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点。
このあたり、ドキュメントに詳しい説明がなくて、かなりハマった...
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点
{ responseType: "arraybuffer" }
でcreateしないといけないTwitter APIむずい...
以上!!
積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました!
積読ハウマッチは、Nuxt.js+Firebaseで開発してます!
もしよかったら、遊んでみてくださいヽ(=´▽`=)ノ
要望・感想・アドバイスなどあれば、
公式アカウント(@MemoryLoverz)や開発者(@kira_puka)まで♪
Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。
また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!
こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?
コメント