【GAS】X(旧Twitter)に自動投稿する

GAS(Google Application Script)に戻る

目次

概要

スプレッドシートに記入した内容をX(旧Twitter)に投稿するようなシステムを作ろうと思った。
株価を取得したものを自動で投稿させたりしできたらいいと思い、今回実装した。

準備

まず、自動投稿のツールを使うには下準備をする必要がある。

スプレッドシート関連

まず、スプレッドシートを作成する。
手順は以下だ。

  • Googleスプレッドシートを開く
  • 「拡張機能」->「Apps Script」を選択してGASのエディタを開く
  • エディタの「ライブラリ」の「+」ボタンを押下する
  • スクリプトIDに以下を入力する
    1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
  • 検索ボタンを押下する
  • OAuth2が表示されていることを確認し、「追加」ボタンを押下する

スクリプトIDを入力して、検索

追加ボタンを押下する

X(旧Twitter)

  • X Developer Platform(https://developer.twitter.com/en/apps)を開く。
  • 投稿を行いたいアカウントでログインする
  • 以下はこちらの「事前準備:APIの取得手順」を参照
    • アカウント認証を求められるため、「認証する」ボタンを押下する
    • 画面右上の「Create an App」ボタンを押下する
    • 確認メッセージが表示されたら「Apply」ボタンを押下する
    • APIプランを選択する(今回は無料のプランのため、「Sign up for Free Account」を選択)
    • 利用目的を英語で記入、そして項目にチェックを入れて「Submit」ボタンを押下する
    • トップ画面へ遷移する

以下がトップ画面になる。

そして、画面右にあるDefault project-XXXXXXXXの下にある項目をクリックすると以下の画面になる。
その「User authentication settings」の「Set up」ボタンを押下する。

その後、「App permissions」は「Read and write」を選択

Type of Appは「Web App, Automated App or Bot」を選択。

App infoはWebsite URLは任意。

Callback URI / Redirect URLは以下を入力する。

https://script.google.com/macros/d/{スクリプトID}/usercallback

スクリプトIDは以下から取得できる。

  • エディタの左にある歯車アイコン(プロジェクトの設定)を押下する
  • 黒塗りにされている箇所にスクリプトIDが表示されている。

「CLIENT_ID」と「CLIENT_SECRET」を取得する。
この二つは以下から取得できる。

  • 画面右にあるDefault project-XXXXXXXXの下にある項目をクリックする
  • 「Key and tokens」タブを選択
  • CLIENT_IDは赤枠で囲われた黒塗り部分
  • CLIENT_SECRETはその下の「Client Secret」の「Regenerate」ボタンを押下する。

ここまで取得できれば準備は完了だ。

ソースコード

スプレッドシートの内容

GASのコード

const CLIENT_ID = '[CLIENT_ID]';
const CLIENT_SECRET = '[CLIENT_SECRET]';

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());
}

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

/**
 * Googleスプレッドシートからデータを取得します。
 * この関数は、スプレッドシートの全データを読み込み、それを返します。
 */
function getSpreadsheetData() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
  const range = sheet.getDataRange(); // データ範囲の取得
  console.log(range.getValues());
  return range.getValues(); // データの取得
}

/**
 * スケジュールされたツイートを投稿します。
 * この関数は、指定された時間にツイートを自動的に投稿するために使用されます。
 */
function postScheduledTweets() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
  const rows = sheet.getDataRange().getValues(); // 全行のデータを取得

  const now = new Date(); // 現在の日時を取得

  // 各行に対する処理
  for (let i = 1; i < rows.length; i++) {
    const [scheduledTime, tweetContent, status] = rows[i]; // 各列のデータを取得

    // スケジュールされた時間が現在時刻以前で、ツイート内容があり、まだ投稿されていない場合
    if (scheduledTime && tweetContent && new Date(scheduledTime) <= now && status !== "投稿済") {
      sendTweet(tweetContent); // ツイートを送信
      sheet.getRange(i + 1, 3).setValue("投稿済"); // ステータスを「投稿済み」に更新
    }
  }
}


/**
 * 指定された内容でツイートを送信します。
 * この関数は、Twitter APIを使用してツイートを投稿します。
 */
function sendTweet(tweetContent) {
  if (!tweetContent) {
    Logger.log("No tweet content provided"); // ツイート内容がない場合のログ出力
    return;
  }

  var service = getService(); // OAuth2サービスの取得
  if (service.hasAccess()) {
    var url = 'https://api.twitter.com/2/tweets'; // Twitter APIのURL
    var response = UrlFetchApp.fetch(url, {
      method: 'POST', // POSTリクエスト
      contentType: 'application/json', // コンテンツタイプ
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken() // 認証ヘッダー
      },
      muteHttpExceptions: true,
      payload: JSON.stringify({ text: tweetContent }) // ツイート内容をJSON形式で送信
    });

    var result = JSON.parse(response.getContentText()); // レスポンスの解析
    Logger.log(JSON.stringify(result, null, 2)); // レスポンスのログ出力
  } else {
    var authorizationUrl = service.getAuthorizationUrl(); // 認証URLの取得
    Logger.log('Open the following URL and re-run the script: %s', authorizationUrl); // 認証URLのログ出力
  }
}

詳細

スプレッドシート

まずはスプレッドシートを見ていこう。
今回のコードは今回のスプレッドシートの形式に則って書かれている。
プログラミングがわかる人ならアレンジしても問題ないだろう。

まず、一行目に項目名を、二行目以降に項目内容を書いていく。
そして、1列目は、2列目に投稿内容、3列目に投稿済みかどうかのフラグ

日時
投稿内容ここに記載した内容がXに投稿される。
投稿済みかどうかのフラグここに「投稿済み」と記載されていると自動投稿の対象から外れる。
そして、投稿されたタイミングで「投稿済み」と入力される。

GASコード

順番に見ていこう。
まずは「CLIENT_ID」と「CLIENT_SECRET」から。
それは準備の箇所に記載済み。

const CLIENT_ID = '[CLIENT_ID]';
const CLIENT_SECRET = '[CLIENT_SECRET]';

まずはX(旧Twitter)の認証を行う。
以下の部分を抜粋。mainメソッドを実行すると、ログにURLが出てくるのでそのWebページを開こう。

するとログイン画面へ遷移するため、ログインする。
ログイン済みの場合、console.log(“Already authorized”);の部分「Already authorized」が出力される。

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 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 main() {
  const service = getService();
  if (service.hasAccess()) {
    console.log("Already authorized");
  } else {
    const authorizationUrl = service.getAuthorizationUrl();
    console.log('Open the following URL and re-run the script: %s', authorizationUrl);
  }
}

そのあと、postScheduledTweetsメソッドを実行することで投稿ができる。

関係あるコードは以下になる。

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 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 postScheduledTweets() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
  const rows = sheet.getDataRange().getValues(); // 全行のデータを取得

  const now = new Date(); // 現在の日時を取得

  // 各行に対する処理
  for (let i = 1; i < rows.length; i++) {
    const [scheduledTime, tweetContent, status] = rows[i]; // 各列のデータを取得

    // スケジュールされた時間が現在時刻以前で、ツイート内容があり、まだ投稿されていない場合
    if (scheduledTime && tweetContent && new Date(scheduledTime) <= now && status !== "投稿済") {
      sendTweet(tweetContent); // ツイートを送信
      sheet.getRange(i + 1, 3).setValue("投稿済"); // ステータスを「投稿済み」に更新
    }
  }
}

function sendTweet(tweetContent) {
  if (!tweetContent) {
    Logger.log("No tweet content provided"); // ツイート内容がない場合のログ出力
    return;
  }

  var service = getService(); // OAuth2サービスの取得
  if (service.hasAccess()) {
    var url = 'https://api.twitter.com/2/tweets'; // Twitter APIのURL
    var response = UrlFetchApp.fetch(url, {
      method: 'POST', // POSTリクエスト
      contentType: 'application/json', // コンテンツタイプ
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken() // 認証ヘッダー
      },
      muteHttpExceptions: true,
      payload: JSON.stringify({ text: tweetContent }) // ツイート内容をJSON形式で送信
    });

    var result = JSON.parse(response.getContentText()); // レスポンスの解析
    Logger.log(JSON.stringify(result, null, 2)); // レスポンスのログ出力
  } else {
    var authorizationUrl = service.getAuthorizationUrl(); // 認証URLの取得
    Logger.log('Open the following URL and re-run the script: %s', authorizationUrl); // 認証URLのログ出力
  }
}

では、細かく見ていこう。
まずはpostScheduledTweetsメソッドから。

function postScheduledTweets() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
  const rows = sheet.getDataRange().getValues(); // 全行のデータを取得

  const now = new Date(); // 現在の日時を取得

  // 各行に対する処理
  for (let i = 1; i < rows.length; i++) {
    const [scheduledTime, tweetContent, status] = rows[i]; // 各列のデータを取得

    // スケジュールされた時間が現在時刻以前で、ツイート内容があり、まだ投稿されていない場合
    if (scheduledTime && tweetContent && new Date(scheduledTime) <= now && status !== "投稿済") {
      sendTweet(tweetContent); // ツイートを送信
      sheet.getRange(i + 1, 3).setValue("投稿済"); // ステータスを「投稿済み」に更新
    }
  }
}

以下の部分でまずはデータを取得。

const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
const rows = sheet.getDataRange().getValues();

そのデータを格納してsendTweetメソッドを呼び出す。
ただし、投稿する条件を満たした場合飲み。

// 各行に対する処理
for (let i = 1; i < rows.length; i++) {
  const [scheduledTime, tweetContent, status] = rows[i]; // 各列のデータを取得

  // スケジュールされた時間が現在時刻以前で、ツイート内容があり、まだ投稿されていない場合
  if (scheduledTime && tweetContent && new Date(scheduledTime) <= now && status !== "投稿済") {
    sendTweet(tweetContent); // ツイートを送信
    sheet.getRange(i + 1, 3).setValue("投稿済"); // ステータスを「投稿済み」に更新
  }
}

sendTweetメソッド内を見てみよう。

function sendTweet(tweetContent) {
  if (!tweetContent) {
    Logger.log("No tweet content provided"); // ツイート内容がない場合のログ出力
    return;
  }

  var service = getService(); // OAuth2サービスの取得
  if (service.hasAccess()) {
    var url = 'https://api.twitter.com/2/tweets'; // Twitter APIのURL
    var response = UrlFetchApp.fetch(url, {
      method: 'POST', // POSTリクエスト
      contentType: 'application/json', // コンテンツタイプ
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken() // 認証ヘッダー
      },
      muteHttpExceptions: true,
      payload: JSON.stringify({ text: tweetContent }) // ツイート内容をJSON形式で送信
    });

    var result = JSON.parse(response.getContentText()); // レスポンスの解析
    Logger.log(JSON.stringify(result, null, 2)); // レスポンスのログ出力
  } else {
    var authorizationUrl = service.getAuthorizationUrl(); // 認証URLの取得
    Logger.log('Open the following URL and re-run the script: %s', authorizationUrl); // 認証URLのログ出力
  }
}

まずはデータが格納されているかチェック。格納されていなければ今回の処理は行わない。

if (!tweetContent) {
  Logger.log("No tweet content provided"); // ツイート内容がない場合のログ出力
  return;
}

ここでOAuth2サービスが取得できるかチェック

var service = getService(); // OAuth2サービスの取得
if (service.hasAccess()) {

投稿のためのAPIを呼び出す。

var url = 'https://api.twitter.com/2/tweets'; // Twitter APIのURL

そして、投稿内容のデータはJSON形式にしてpayloadに格納。

var response = UrlFetchApp.fetch(url, {
    method: 'POST', // POSTリクエスト
    contentType: 'application/json', // コンテンツタイプ
    headers: {
      Authorization: 'Bearer ' + service.getAccessToken() // 認証ヘッダー
    },
    muteHttpExceptions: true,
    payload: JSON.stringify({ text: tweetContent }) // ツイート内容をJSON形式で送信
  });

そして、APIを呼び出した結果のデータは以下のように取得。
その結果をログに出力する。

var result = JSON.parse(response.getContentText()); // レスポンスの解析
Logger.log(JSON.stringify(result, null, 2)); // レスポンスのログ出力

ここまでで投稿のシステムはOK。次に自動投稿されるようにしよう。
そのためには「トリガー」を設定する。その設定は以下からできる。
まずは画面左にある「トリガー」を選択。

そして、右下の「トリガーを追加」ボタンを押下する。

すると、以下のポップアップが表示される。
設定内容は以下の画像を参照。

  • 実行する関数は「postScheduledTweets」
  • 実行するデプロイは「Head」
  • イベントのソースは「時間主導型」
  • 時間ベースのトリガータイプ、時間の間隔は任意。
    画像のように「時間ベース」にすると「時間(hour)単位」で設定できる。

そして、新しくトリガーが登録されたことを確認。

あとはスプレッドシートの概要箇所に「投稿済み」と記入されてないものがあればX(旧Twitte)を確認すれば投稿されてることが確認できる。

参考ページ

GAS(Google Application Script)に戻る