🍎

Xのapiを使って画像・動画付きのポストを自動化する(GAS)

に公開

はじめに

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

Xの管理者ポータルまで環境設定

まずAPIを使用するにあたって設定をしなければいけないのですが、この記事通りにやればすぐに完了しました。手順は全く同じなのでここでは割愛します。
https://zenn.dev/gas/articles/37b6d7de715251

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を指定しているのでポストが自動でされるようになります。

以上です。結構前に書いたコードだったのですが、問題なく動いたので記事にしました。自分が調べるのに少し苦労したので参考になれば幸いです。

参考にさせてもらった記事

https://qiita.com/k7a/items/e6a456bec26b4e667c47

NCDC テックブログ

Discussion