コードの知識ゼロで完成!Googleスプレッドシート×GASでX(Twitter)自動投稿を作る完全ガイド

生成AI

こんにちは!この記事では、私が プログラミング知識ゼロ から実際に X(旧Twitter)の自動投稿ツール を作り上げた全過程解説します。

何度もつまずきましたが、最終的にちゃんと動きました!!

同じように「コードがわからないけど、Xの自動投稿をやってみたい!」という方の参考になれば嬉しいです。

この記事でできること

  • Googleスプレッドシートにツイート内容・投稿日時を入力
  • GASでX APIと連携し、自動投稿
  • 投稿が完了したらスプレッドシートに「投稿済み」とツイートURLが記録

全体の構成

必要なもの

  • Googleスプレッドシート
  • Google Apps Script (GAS)
  • X (Twitter) のDeveloperアカウント

STEP1: X (Twitter) APIの準備

1. X Developer Portal に登録

Twitter Developer Portal にログインし、開発者アカウントを申請。

申請するときは、特に気張らずに「自動化して効率的なSNS運用をしたいです!」みたいなことを所定の文字数以上かけて描けばOKです。

2. App Settings → User Authentication Settings

まず最初にXのDeveloper Portalで「Project」と「App」を作ります。
この設定項目を埋めないといけません。

この設定を埋めるために、 Callback URL を作りましょう。これは今回、GASを使うのでGASのスクリプトURLを設定します。

Callback URLの設定例

https://script.google.com/macros/d/YOUR_SCRIPT_ID/usercallback

YOUR_SCRIPT_ID は、GASのプロジェクトURLから取得できます!

3. Callback URIを取得したら、設定項目を埋めます

先ほど添付した画像にあった設定項目を埋めなければいけません。

Callback URIはスクリプトIDを埋めます。必須項目を埋め終わったら保存しましょう。

4. Client ID & Client Secret を取得

さて、3stepをこなしたら、自動的に表示されます!

この画面で、Client ID と Client Secret を取得します。

これを GASのスクリプト側にも設定しますんで保存しましょう!


STEP2: Googleスプレッドシートの準備

A列 (日付)B列 (時間)C列 (ツイート内容)D列 (ステータス)E列 (ツイートURL)
2025/03/2619:30こんにちは!投稿済みhttps://twitter.com/xxx/status/yyy

D列とE列は自動入力です。


STEP3: GASの設定

ファイル構成

  1. auth.gs → 認証関連
  2. post.gs → 投稿処理
  3. main.gs → 認証URL発行用

認証ファイル (auth.gs)

const CLIENT_ID = '取得したClient ID';
const CLIENT_SECRET = '取得したClient Secret';

function getService() {
  const userProps = PropertiesService.getUserProperties();
  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() {
  const userProps = PropertiesService.getUserProperties();
  if (!userProps.getProperty("code_verifier")) {
    let verifier = "";
    const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
    for (let i = 0; i < 128; i++) {
      verifier += possible.charAt(Math.floor(Math.random() * possible.length));
    }
    const sha256Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, verifier);
    const challenge = Utilities.base64Encode(sha256Hash).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    userProps.setProperty("code_verifier", verifier);
    userProps.setProperty("code_challenge", challenge);
  }
}

function authCallback(request) {
  const service = getService();
  const authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('✅ 認証成功しました!');
  } else {
    return HtmlService.createHtmlOutput('❌ 認証が拒否されました');
  }
}

function reset() {
  const service = getService();
  service.reset();
  PropertiesService.getUserProperties().deleteAllProperties();
  Logger.log("🔄 認証情報をリセットしました");
}

投稿ファイル (post.gs)

function postScheduledTweets() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
  const rows = sheet.getDataRange().getValues();
  const now = new Date();
  const USER_ID = 'あなたのXユーザーID';

  for (let i = 1; i < rows.length; i++) {
    const [datePart, timePart, tweetContent, status] = rows[i];

    if (datePart && timePart && tweetContent && status !== "投稿済") {
      const dateStr = Utilities.formatDate(new Date(datePart), Session.getScriptTimeZone(), "yyyy/MM/dd");
      let timeStr;
      if (Object.prototype.toString.call(timePart) === '[object Date]') {
        timeStr = Utilities.formatDate(new Date(timePart), Session.getScriptTimeZone(), "HH:mm");
      } else {
        timeStr = timePart;
      }

      const dateTimeString = `${dateStr} ${timeStr}`;
      const scheduledDateTime = new Date(dateTimeString);

      if (isNaN(scheduledDateTime.getTime())) {
        Logger.log(`⚠️ 日時形式エラー: 行 ${i + 1}`);
        continue;
      }

      const timeDiff = Math.abs(now - scheduledDateTime) / 60000;

      if (timeDiff <= 5) {
        const tweetId = sendTweet(tweetContent);
        if (tweetId) {
          sheet.getRange(i + 1, 4).setValue("投稿済");
          const tweetUrl = `https://twitter.com/${USER_ID}/status/${tweetId}`;
          sheet.getRange(i + 1, 5).setValue(tweetUrl);
          Logger.log(`✅ 投稿完了: ${tweetContent} | URL: ${tweetUrl}`);
        }
      }
    }
  }
}

function sendTweet(tweetContent) {
  const service = getService();
  if (service.hasAccess()) {
    const url = 'https://api.twitter.com/2/tweets';
    const response = UrlFetchApp.fetch(url, {
      method: 'POST',
      contentType: 'application/json',
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken()
      },
      muteHttpExceptions: true,
      payload: JSON.stringify({ text: tweetContent })
    });

    const result = JSON.parse(response.getContentText());
    if (response.getResponseCode() === 201) {
      return result.data.id;
    } else {
      Logger.log("❌ エラー: " + JSON.stringify(result));
      return null;
    }
  } else {
    Logger.log("❌ 認証が必要です");
    return null;
  }
}

認証用 (main.gs)

function main() {
  const service = getService();
  if (service.hasAccess()) {
    Logger.log("✅ すでに認証済み");
  } else {
    pkceChallengeVerifier();
    const authorizationUrl = service.getAuthorizationUrl();
    Logger.log(`🔑 このURLで認証: ${authorizationUrl}`);
  }
}

ライブラリの追加

このコードを動かすには、OAuth2ライブラリが必要です。

GASエディタ左側の「ライブラリ」の + を押し、以下のスクリプトIDを入力して「検索」→「追加」を押してください。

検索スペースにはこちらを入力してください。
OAuth2 ライブラリID: 1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMirs4KDBKz5ZZRubdXz

このライブラリも、GoogleWorkspaceで公開されているものなので安心して使ってください。

参考:https://github.com/googleworkspace/apps-script-oauth2

(↑よくわかんないので、開かなくていい!w)

STEP4: 自動実行(トリガー)の設定

コードを書いただけでは自動で動かないので、定期的(例えば1分おき)にチェックする設定を行います。

  1. GASエディタ左側の時計アイコン(トリガー)をクリック
  2. 「トリガーを追加」をクリック
  3. 以下のように設定して保存
    • 実行する関数:postScheduledTweets
    • イベントのソース:時間主導型
    • タイプ:分ベースのタイマー
    • 間隔:1分おき(または5分おき)

これで、スプレッドシートに書いた時間になると勝手に投稿されます!


実際に私が躓いたポイント

1. Callback URIの場所がわからない問題

「いや、Callback URIってなにやねん…」

これが一番最初の引っ掛かりポイント。しかもこれわかりようなかったですが、GASのURLを登録すればいったんOK。

あとはGAS側で必要な設定を完了させるのみ。

App details → User authentication settings → OAuth 2.0 Settings → Callback URLs で設定!

2. 認証エラー(code_verifierエラー)

  • リセット関数を実行してからmain()を必ず実行すること!
function reset() {
  const service = getService();
  service.reset();
  PropertiesService.getUserProperties().deleteAllProperties();
  Logger.log("🔄 認証情報をリセットしました");
}

3. スプレッドシートの時間形式が原因で投稿されない

B列がDate型かどうか判定し、フォーマット変換するよう修正を依頼

4. ツイートURLも残したい!

→ 投稿成功時、ツイートIDを組み合わせてURLをスプレッドシートに保存!を可能にコード側を直しました。


最後に

エラーが出たら ログを見る ことが大事。

ログが出たら、AIにエラーメッセージを送ることで解決できます。
コードに詳しくなくても、AIとひとつひとつ解決していけば、ちゃんと動きます!


ぜひこの記事を参考に、X自動投稿を自分の手で動かしてみてください!