Xのapiを使って画像・動画付きのポストを自動化する(GAS)
はじめに
スプレッドシートにポストしたい内容をリストで書いておいて、あとはそれをランダムでポストするスクリプトを作ってみました。
画像のurlはwebの画像アドレスか、googledriveのパスを指定すればそこからアップロードするようにしました。



Xの管理者ポータルまで環境設定
まずAPIを使用するにあたって設定をしなければいけないのですが、この記事通りにやればすぐに完了しました。手順は全く同じなのでここでは割愛します。
GASとXを繋げる認証
まずはgasとxを繋げる認証を行なっていきます。
まずxのapi設定画面からConsumer Keysを取得してください。画像のRegenerateボタンから取得できます。

続いて、スプレッドシートに戻って拡張設定タブ→Apps scriptで実際にスクリプトを書いていきます。
var TW_CONSUMER_KEY = "取得したAPI Key";
var TW_CONSUMER_SECRET = "取得したAPI Key Secret";
// 認証ページのURLをログに表示
function printAuthUrl() {
const twitter = buildTwitterAuth();
Logger.log(twitter.authorize());
}
// Twitter 用 OAuth1 サービスを作成
function buildTwitterAuth() {
return OAuth1.createService('twitterLogin')
.setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
.setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
.setAuthorizationUrl('https://api.twitter.com/oauth/authenticate')
.setConsumerKey(TW_CONSUMER_KEY)
.setConsumerSecret(TW_CONSUMER_SECRET)
// 認証後に戻ってくる処理
.setCallbackFunction('twitterCallback')
// アクセストークンの保存先
.setPropertyStore(PropertiesService.getUserProperties());
}
// 認証後に呼ばれるコールバック
function twitterCallback(e) {
const twitter = buildTwitterAuth();
const ok = twitter.handleCallback(e);
if (ok) {
return HtmlService.createHtmlOutput('認証成功');
} else {
return HtmlService.createHtmlOutput('認証失敗');
}
}
OAuth1というライブラリを使用するのでライブラリの+ボタンをクリックして、スクリプトIDに「1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s」と入力して追加します。

そして上部タブでprintAuthUrl関数を選択して実行をクリックします。

すると実行ログにurlが表示されるのでそれをそのままブラウザに貼り付けます。
少しだけxの画面が開き、「認証成功しました」という画面が表示されれば認証完了になります。

ツイートする箇所
ツイートする関数等
//ツイートする行を選定する関数
function point_target_row(selectedSheet){
let lastRow = selectedSheet.getRange(TWEET_TEXT_ROW, 1).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow();
var pending_row;
for (let i=1; i<= lastRow; i++){
//未完了の場合はarrayへ追加
if(selectedSheet.getRange(i, CHECK_ROW).getValue() == ''){
pending_row=i
}
}
if(pending_row){
return pending_row
}
else{
return -1
}
}
// ツイートする関数
function sendTweet() {
try{
// 対象のツイート行をランダムに選択
var activeSheet = SpreadsheetApp.getActiveSpreadsheet().getSheets();
var selectedSheet = activeSheet[0]
let targetRow = point_target_row(selectedSheet)
if(targetRow==-1){
Logger.log('ツイートする行が見つかりませんでした')
return
}
//tweet文の作成
let tweetText = selectedSheet.getRange(targetRow, TWEET_TEXT_ROW).getValue()
if(tweetText.length>=140){
Logger.log('ツイートの文字数が140文字以上です')
selectedSheet.getRange(targetRow, CHECK_ROW).setValue('ツイートの文字数が140文字以上でツイートできませんでした。');
return
}
var service = getTwitterService();
if (service.hasAccess()) {
//ファイルのアップロードidを取得
let img_ids = uploadFile(service, selectedSheet, targetRow)
if(!img_ids){
return
}
var payload = {
text: tweetText
};
if (img_ids.length > 0) {
payload.media = {
media_ids: img_ids
};
}
var response = service.fetch(
'https://api.twitter.com/2/tweets', {
method: "post",
muteHttpExceptions: true,
payload: JSON.stringify(payload),
contentType: "application/json"
});
var result = JSON.parse(response.getContentText());
let today = new Date();
let todayStr = Utilities.formatDate(today, 'JST', 'yy-MM-dd');
selectedSheet.getRange(targetRow, CHECK_ROW).setValue(todayStr+'に投稿済み');
Logger.log(result)
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',authorizationUrl);
}
}
catch(e){
Logger.log('エラーを検知しました。');
Logger.log('エラー内容:'+e.message);
}
}
画像ファイルのアップロード
//画像ファイルをアップロードする関数
function uploadFile(service, selectedSheet, targetRow){
var img_ids = [];
var urlPattern = /^(https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i;
for(i=0;i<4;i++){
var file_name = selectedSheet.getRange(targetRow, i+FILE_NAME_ROW_START).getValue(); //画像ファイル名を設定
if (file_name ==''){
break
}
//webからの画像を取得
if(file_name.match(urlPattern)){
var response = UrlFetchApp.fetch(file_name);
var content = response.getContent();
}
//driveからの画像を取得
else {
if (!DriveApp.getFilesByName(file_name).hasNext()){
Logger.log(targetRow+'行目の'+file_name+'のファイル名が無効です。')
selectedSheet.getRange(targetRow, CHECK_ROW).setValue(file_name+'のファイル名が無効でツイートできませんでした');
return ;
}
var file_temp = DriveApp.getFilesByName(file_name).next();//GoogleDriveから画像を取得
var file_type = file_temp.getMimeType()
if(file_type.match('image/')){
var content = file_temp.getBlob().getBytes();
}
else if(file_type=='video/mp4'){
img_ids[i] = upload_movie(service,file_temp)
continue
}
else {
Logger.log(targetRow+'行目の'+file_name+'のファイル形式が無効です')
selectedSheet.getRange(targetRow, CHECK_ROW).setValue(file_name+'のファイル形式が無効でツイートできませんでした');
return ;
}
}
var resp_64 = Utilities.base64Encode(content);
var image_upload = service.fetch(
'https://upload.twitter.com/1.1/media/upload.json',{
'method' : 'POST',
'payload': { 'media_data': resp_64 }
});
img_ids[i] = JSON.parse(image_upload).media_id_string;
}
return img_ids
}
動画ファイルのアップロード
//動画ファイルをアップロードする関数
function upload_movie(service,file_temp) {
var movie_blob = file_temp.getBlob();//GoogleDriveから動画を取得
var file_size = movie_blob.getBytes().length
var movie_64 = Utilities.base64Encode(movie_blob.getBytes())
var movie_64_file_size = movie_64.length
const endpoint_media = 'https://upload.twitter.com/1.1/media/upload.json'
var request_data = {
'command': 'INIT',
'media_type': 'video/mp4',
'total_bytes': file_size,
'media_category': 'tweet_video'
}
var video_upload = service.fetch(endpoint_media,{
'method' : 'POST',
'payload': request_data
});
var movie_init = JSON.parse(video_upload)
//APPEND
const segment_index = 0;
const bytes_sent = 0;
const chunk_size = 1000000;
const chunk_num = Math.ceil(movie_64_file_size / chunk_size);
for (let index = 0; index < chunk_num; index++) {
const chunk = movie_64.slice(chunk_size * index, chunk_size * (index + 1));
const movie_append_option = {
'method' : "POST",
"muteHttpExceptions" : true,
'payload': {
'command':'APPEND',
'media_data':chunk,
'media_id':movie_init['media_id_string'],
'segment_index':index
}
};
service.fetch(endpoint_media, movie_append_option);
}
//FINALIZE
const movie_finalize_option = {
'method' : "POST",
"muteHttpExceptions" : true,
'payload': {
'command':'FINALIZE',
'media_id':movie_init['media_id_string']
}
};
const movie_finalize = JSON.parse(service.fetch(endpoint_media, movie_finalize_option));
// STATUS
while (true) {
var movie_status_option = { 'method':"GET" };
var movie_status = JSON.parse(service.fetch(endpoint_media+"?command=STATUS&media_id="+movie_init['media_id_string'], movie_status_option));
Logger.log(movie_status)
if (movie_status["processing_info"]["state"] == "succeeded") {
break;
} else if (movie_status["processing_info"]["state"] == "failed") {
sheet.getRange(i, 14).setValue(movie_status["processing_info"]["error"]["message"]);
throw new Error(movie_status["processing_info"]["error"]["message"]);
return
} else {
Utilities.sleep(movie_status["processing_info"]["check_after_secs"] + 1);
Utilities.sleep(100)
}
};
return movie_init.media_id_string
}
コードを書いたら試しにスプレッドシートに文章を用意して、sendTweetを選択して実行します。問題なくポストできていたらOKです。
トリガーの設定
コードを用意しましたら、トリガーを設定します。

例えばこのように12時間おきと設定すると、その通りに選択した関数が実行されます。今回はsendTweetを指定しているのでポストが自動でされるようになります。

以上です。結構前に書いたコードだったのですが、問題なく動いたので記事にしました。自分が調べるのに少し苦労したので参考になれば幸いです。
参考にさせてもらった記事
NCDC株式会社( ncdc.co.jp/ )のテックブログです。 主にエンジニアチームのメンバーが投稿します。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください!
Discussion