GASでTwitterAPIを使ったら快適だった件

2024/04/05に公開

APIが有料化する前はTweepyを使いAPIを使ってあれこれをしていたのですが
APIが有料化(しかも1万5000円と高い)になってからはツイートのみしかできない

そのためあまり使っていなかったのですが、ツイートだけでも価値あるかな?と思い
もう一度チャレンジすることにした。しかしタスクスケジューラを使うにはPCを常時起動しなくてはいけないので、GCPでも使うか?と思っていたらGASというものがあることを知った。

もともとExcelと連動させていたのでGASと連携させるのは非常に使い勝手よく使うことができた。

ただAPIが有料化して1年経過したのに未だに不便なこともあった。
画像付きのツイートするには1.1認証で画像をアップロードして
2.0認証でツイートしなければならないという手間のかかるところ
※まだこれ治らないの??イーロンってまじ何しているんですかね?

結構頑張って調べてようやく作成することに成功したぜ
初投稿ということで色々書いてみます。

function main() {
  const service = getService();
  if (service.hasAccess()) {
    Logger.log("Already authorized");
  } else {
    const authorizationUrl = service.getAuthorizationUrl();
    Logger.log('Open the following URL and re-run the script: %s', authorizationUrl);
  }
}

function getService() {
  pkceChallengeVerifier();
  const userProps = PropertiesService.getUserProperties();
  const scriptProps = PropertiesService.getScriptProperties();
  return OAuth2.createService('twitter')
    .setAuthorizationBaseUrl('https://twitter.com/i/oauth2/authorize')
    .setTokenUrl('https://api.twitter.com/2/oauth2/token?code_verifier=' + userProps.getProperty("code_verifier"))
    .setClientId(CLIENT_ID)
    .setClientSecret(CLIENT_SECRET)
    .setCallbackFunction('authCallback')
    .setPropertyStore(userProps)
    .setScope('users.read tweet.read tweet.write offline.access')
    .setParam('response_type', 'code')
    .setParam('code_challenge_method', 'S256')
    .setParam('code_challenge', userProps.getProperty("code_challenge"))
    .setTokenHeaders({
      'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET),
      'Content-Type': 'application/x-www-form-urlencoded'
    })
}

function authCallback(request) {
  const service = getService();
  const authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('Success!');
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}

function pkceChallengeVerifier() {
  var userProps = PropertiesService.getUserProperties();
  if (!userProps.getProperty("code_verifier")) {
    var verifier = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";

    for (var i = 0; i < 128; i++) {
      verifier += possible.charAt(Math.floor(Math.random() * possible.length));
    }

    var sha256Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, verifier)

    var challenge = Utilities.base64Encode(sha256Hash)
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '')
    userProps.setProperty("code_verifier", verifier)
    userProps.setProperty("code_challenge", challenge)
  }
}

function logRedirectUri() {
  var service = getService();
  Logger.log(service.getRedirectUri());
}

// OAuth1認証
function getServiceOld() {
  return OAuth1.createService( "Twitter" )
  .setAccessTokenUrl( "https://api.twitter.com/oauth/access_token" )
  .setRequestTokenUrl( "https://api.twitter.com/oauth/request_token" )
  .setAuthorizationUrl( "https://api.twitter.com/oauth/authorize" )
  .setConsumerKey( CONSUMER_API_KEY )
  .setConsumerSecret( CONSUMER_API_SECRET )
  .setAccessToken( ACCESS_TOKEN, ACCESS_TOKEN_SECRET )
  .setCallbackFunction('authCallback'); // コールバック関数名 
}

// OAuthコールバック
function authCallback(request) {
  const service = getTwitterService();
  const authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('Success!');
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}
function uploadMediaFromDrive(folderId) {
    var folder = DriveApp.getFolderById(folderId);
    var files = folder.getFiles();
    var fileList = [];
    var mediaIds = [];
    var service = getServiceOld();
  
    // ファイルをリストに追加
    while (files.hasNext()) {
      var file = files.next();
      fileList.push(file);
    }
  
    // ファイル名でソート
    fileList.sort(function(a, b) {
      return a.getName().localeCompare(b.getName());
    });
  
    // ソートされたファイルを処理
    for (var i = 0; i < fileList.length; i++) {
      var file = fileList[i];
      var blob = file.getBlob();
      var mimeType = blob.getContentType();
  
      // 画像ファイルの場合
      if (mimeType.startsWith("image/")) {
        var encodedImage = Utilities.base64Encode(blob.getBytes());
        var img_option = {
          'method': "POST",
          'payload': {
            'media_data': encodedImage
          }
        };
        var endPointMedia = 'https://upload.twitter.com/1.1/media/upload.json';
        var image_upload = JSON.parse(service.fetch(endPointMedia, img_option));
        mediaIds.push(image_upload['media_id_string']);
      }
      // 画像ファイルでない場合はログに記録して次へ
      else {
        Logger.log("Not an image file: " + file.getName());
        continue; // 次のファイルへ
      }
    }
  
    console.log(mediaIds);
    return mediaIds;
  }

// URLからフォルダIDを抽出する関数
function extractFolderIdFromUrl(url) {
  var match = url.match(/folders\/([\w-]+)/);
  if (match) {
    return match[1];
  }
  return null;
 }

こちらが画像をアップする関数

function sendTweet() {
  result = getContentText()
  tweet = result.tweet
  media = result.media

  var payload = {
    text: tweet
  }

  if (media != ""){
    folderId = extractFolderIdFromUrl(media)

    mediaIds = uploadMediaFromDrive(folderId);

    if (mediaIds.length > 0) {
      payload["media"] = {"media_ids": mediaIds};
    }
  }
  var service = getService();
  if (service.hasAccess()) {
    var url = `https://api.twitter.com/2/tweets`;
    var response = UrlFetchApp.fetch(url, {
      method: 'POST',
      'contentType': 'application/json',
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken()
      },
      muteHttpExceptions: true,
      payload: JSON.stringify(payload)
    });
    var result = JSON.parse(response.getContentText());
    Logger.log(JSON.stringify(result, null, 2));
  } else {
    var authorizationUrl = service.getAuthorizationUrl();
    Logger.log('Open the following URL and re-run the script: %s',authorizationUrl);
  }
}

これがツイートする関数です。

getContentTextはスプシの情報を取得する関数です。

あとはGASのスケジューラを設定すると毎日スプシのツイートが画像付きでツイートされます。
これで完全自動化ツイート完成ですね。

Discussion