こんにちは!この記事では、私が プログラミング知識ゼロ から実際に 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/26 | 19:30 | こんにちは! | 投稿済み | https://twitter.com/xxx/status/yyy |
D列とE列は自動入力です。
STEP3: GASの設定
ファイル構成
- auth.gs → 認証関連
- post.gs → 投稿処理
- 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分おき)にチェックする設定を行います。
- GASエディタ左側の時計アイコン(トリガー)をクリック
- 「トリガーを追加」をクリック
- 以下のように設定して保存
- 実行する関数:
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自動投稿を自分の手で動かしてみてください!

