0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

X API Freeプランで予約投稿を実現する:GAS + Firestoreによる代替アーキテクチャ

0
Posted at

環境情報

項目 バージョン/プラン
X API v2(Freeプラン・月500投稿枠)
認証方式 OAuth 1.0a(User Context)
実行基盤 Google Apps Script(V8ランタイム)
データストア Cloud Firestore(Sparkプラン無料枠)
トリガー GAS Time-driven Trigger(10分間隔)

はじめに

X(旧Twitter)の運用自動化を個人開発で進めていたところ、API v2の scheduled_at パラメータがFreeプランでは利用不可という壁にぶつかりました。

公式ドキュメントの隅にしか記載がなく、実装後にエラーで初めて気づくパターンです。本記事では、この制約を回避するために構築した「GAS + Firestore による疑似予約投稿アーキテクチャ」を、ハマりポイントとコード例つきで解説します。

ハマったポイント:scheduled_at が通らない

OAuth 1.0a での認証は通り、即時投稿は成功していました。次のステップとして以下のようなリクエストを送ったところ、403 Forbidden が返却されます。

// ❌ Freeプランでは失敗する
const payload = {
  text: "テスト投稿です",
  scheduled_at: "2026-04-29T07:00:00Z"  // この時点でNG
};

返却されるエラー:

{
  "title": "Unsupported Authentication",
  "detail": "Scheduled tweets are only available on Basic tier or higher.",
  "type": "about:blank",
  "status": 403
}

Basicプラン(月額$200)にアップグレードすれば解決しますが、個人運用ではROIが合いません。月500投稿枠で十分なので、Freeプランのまま予約機能を実現する方法を検討しました。

プラン別の機能比較

機能 Free Basic ($200/月) Pro ($5,000/月)
月間投稿数 500 3,000 300,000
即時投稿 (POST /tweets)
予約投稿 (scheduled_at)
投稿の検索
Analytics取得 制限あり

→ Freeで使えるのは「即時投稿」のみ。これを軸に設計します。

代替アーキテクチャの全体像

[Discord承認]
    ↓
[Firestore: scheduled_post_time を保存]
    ↓
[GAS Time Trigger: 10分おき]
    ↓
[時刻到来チェック → POST /2/tweets で即時投稿]
    ↓
[Firestoreステータス: 予約済 → 投稿済]

ポイントは 「予約API」ではなく「予約風に動く Cron」 で代替することです。

実装ステップ

Step 1. Firestoreに予約スロットを保存する

承認済み投稿を Firestore に保存する際、scheduled_post_time フィールドに ISO 8601 形式で投稿予定時刻を持たせます。

// Firestoreドキュメント例
{
  "text": "投稿本文…",
  "status": "予約済",
  "scheduled_post_time": "2026-04-29T07:00:00+09:00",
  "approved_at": "2026-04-28T22:30:00+09:00",
  "self_reply_text": "補足のリプライ本文…"
}

Step 2. 10分おきのトリガーを仕込む

GASエディタから一度だけ以下を実行します。

function setupScheduledPostTrigger() {
  // 既存トリガーをクリア(重複防止)
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === 'cronCheckScheduledPosts') {
      ScriptApp.deleteTrigger(t);
    }
  });

  ScriptApp.newTrigger('cronCheckScheduledPosts')
    .timeBased()
    .everyMinutes(10)
    .create();
}

Step 3. 時刻到来チェックと投稿実行

function cronCheckScheduledPosts() {
  const now = new Date();
  const docs = fetchFirestore('posts', {
    where: [
      ['status', '==', '予約済'],
      ['scheduled_post_time', '<=', now.toISOString()]
    ]
  });

  docs.forEach(doc => {
    try {
      const tweetId = postTweet(doc.text);  // OAuth 1.0a で即時投稿
      updateFirestore('posts', doc.id, {
        status: '投稿済',
        posted_at: now.toISOString(),
        tweet_id: tweetId
      });
    } catch (e) {
      Logger.log(`投稿失敗 [${doc.id}]: ${e.message}`);
      notifyDiscord(`⚠️ 投稿失敗: ${doc.id}\n${e.message}`);
    }
  });
}

Step 4. OAuth 1.0a での即時投稿

function postTweet(text) {
  const url = 'https://api.x.com/2/tweets';
  const oauth = buildOAuth1Header('POST', url);

  const res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: { Authorization: oauth },
    payload: JSON.stringify({ text }),
    muteHttpExceptions: true
  });

  const code = res.getResponseCode();
  if (code !== 201) throw new Error(`HTTP ${code}: ${res.getContentText()}`);
  return JSON.parse(res.getContentText()).data.id;
}

ハマりポイントと対策

1. 10分のズレが発生する

GASのTime Triggerは「ちょうど10分ごと」ではなく、10分間隔のウィンドウ内のどこかで実行されます。秒単位の正確性が必要な場合は、Cloud Scheduler + Cloud Functions への移行も検討の余地ありです。

2. GASの実行時間上限(6分)

1回のトリガーで処理する投稿数が多いと6分の制約に抵触します。fetchFirestore 時に limit: 10 を入れて、1回あたりの処理上限を設けると安全です。

3. タイムゾーンの罠

scheduled_post_time を JST のまま <= 比較すると、UTC換算で9時間ズレます。保存時にUTC(ISO 8601 + Zサフィックス or +09:00明示)で統一するのが鉄則です。

4. 重複投稿の防止

トリガー発火直後に処理が長引き、次のトリガーが先に動くと二重投稿のリスクがあります。Firestore 側で status処理中 に先に書き換えてから投稿する Optimistic Lock を入れています。

FAQ

Q. なぜ Cloud Scheduler ではなく GAS なのか?
A. 個人開発でランニングコストをゼロに抑えたかったため。GAS + Firestore (Spark) はどちらも無料枠で完結します。

Q. 月500投稿枠で足りるか?
A. 1日2投稿 + 自己リプライ2回 = 4投稿/日 → 月120投稿。本体投稿に加えて運用枠を残しても十分です。

Q. Basicプランに上げるべきタイミングは?
A. ① 投稿数が月500を超える、② 検索APIが必要になる、③ Analytics をフル活用したい、のいずれかが発生したタイミングです。

まとめ

X API Freeプランの scheduled_at 制約は、**「Cronで定期チェック → 即時投稿」**で完全に代替可能です。

  • 予約API: ❌ Free不可
  • 即時投稿API: ✅ Freeで使える
  • → 「いつ投稿するか」をDB側で持ち、GAS Cronが時刻到来を検知して即時投稿する

API制約に直面した時、上位プランへの課金以外にも設計で乗り越えられる選択肢は意外と多いです。同じく X API で詰まっている方の参考になれば幸いです。


この記事を書いた人

BENTEN Web Works — 業務自動化・システム開発のフリーランスエンジニアです。

GAS / Python / RPA を使った業務自動化や、Web制作・システム開発のご相談を承っています。
「こんなこと自動化できる?」というご質問だけでもお気軽にどうぞ。

👉 業務自動化サービス — 詳細・お問い合わせはこちら
🐦 X(旧Twitter) — 日々の知見を発信中

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?