🤖

「GASでRSSを読んでXに投稿したい」とAIに丸投げしたら、簡単に作れたよという話(※ただし1回失敗あり)

に公開

🤖 はじめに

ちょっといろいろありまして、X(旧Twitter)にZennのブログのRSSを読み取って自動で投稿するような機能を作りたいなと思っていました。

ただ、、、
APIの認証だとか、XMLのパースだとか、
そういう「ちょっと面倒な処理」を前にすると、
つい腰が重くなってしまいますよね。

でも、

「AI(Gemini)に丸投げすれば、一瞬で作れるんじゃない?」

この記事は、
そんな軽いノリで生成AIに雑なプロンプトを投げたら、
AIとの「リアルな試行錯誤」を経て、
本当に実用的なGASアプリのコードが数分(と、数往復の対話)で完成してしまった、
という私の体験談です。

「AIって本当に使えるの?」
「AIの答えが間違ってたらどうするの?」

そう思っているあなたの背中を、そっと押してくれるかもしれません。

💬 私がAIに投げた「雑なプロンプト」

私がGemini(PRO)に投げたのは、本当にこれだけです。

GASを利用して、とあるRSSから最新の内容を読み込んでXに自動投稿するアプリを作成したいのでGASのコードで実装してください。

どうですか?
かなり「雑」ですよね。

どのRSSなのかも指定していませんし、
X(旧Twitter)への投稿形式も細かく指示していません。

数秒後、Geminiは意気揚々とコードを生成しました。
しかし、それは一部は合っていて一部は間違った内容でした。

🤯 AIとの「リアルな」試行錯誤

「はい、完璧なコードです!」とAIが最初に出してきた答えは、
残念ながら、そのままでは動かないもの でした。

1. 1回目の回答:古いライブラリ(OAuth 1.0a)

AIが最初に提示してきたのは、
twitter-lib-gas というサードパーティライブラリを使った方法でした。

// (AIが最初に生成したコードのイメージ)
// ※このコードは古いため動きません

// ライブラリ 'twitter-lib-gas' (11e9B8nbv2G2njsm...) を追加して...
// 4つのキー (API Key, Secret, Access Token, Secret) を使って...
const twitterService = Twitter.init(API_KEY, API_SECRET, ...);

// v2 APIを呼び出す
twitterService.postV2('tweets', payload);

一見すると完璧に見えました。
「へぇ、こんな便利なライブラリがあるんだ」と。

しかし、実際にそのライブラリIDをGASの「ライブラリ」で検索しても、
見つからない 、あるいは古すぎて最新のX API (v2) に対応していない 可能性が高いことに気づきました。

そうです。
AIは「過去のインターネットの情報」を学習しているため、
APIの仕様変更(特にX APIのような激しい変更)に追随できていないことがある んです。

2. 私のフィードバック:「この方法じゃないと動かないのでは?」

ここで諦めてはいけません。
私は、Googleでとある記事を見つけました。

https://zenn.dev/spacemarket/articles/9fbff59b801631

その記事では、OAuth 1.0aではなく、
より新しく複雑な OAuth 2.0 (PKCE対応) と、
OAuth2 という別のGAS標準ライブラリを使う方法が紹介されていました。

私はAIに、その参考記事のURL(あるいは「OAuth 2.0 と OAuth2 ライブラリを使う方法で」ということ)を伝えて、こうフィードバックしました。

以下の記事内容を確認して再考してもらってもよいでしょうか。

3. 2回目の回答:AIがフィードバックを元に生成した「本当に動くコード」

AI(Gemini)は、
「ご指摘ありがとうございます。確かに現在のX API v2ではOAuth 2.0が必要です」
と、素直に間違い(古さ)を認め
私が提示したZenn記事の認証ロジック(OAuth 2.0)を完全に理解 した上で、
私の最初の要望(RSSの読み込み)と完璧にマージ させたコードを再生成したのです。

// (AIが再生成したコードの骨格)
// ※こっちは本当に動くやつ

// ▼▼▼ ユーザー設定項目 ▼▼▼

// 1. 取得したいRSSフィードのURL
const RSS_URL = 'https://example.com/rss.xml'; // あなたが取得したいRSSのURLに変更してください

// 2. X APIのキー(OAuth 2.0)
// ※これらのキーは、下の「setupCredentials」関数を実行して保存します。
const CLIENT_ID = 'YOUR_CLIENT_ID';       // あなたのClient ID
const CLIENT_SECRET = 'YOUR_CLIENT_SECRET'; // あなたのClient Secret

// ▲▲▲ ユーザー設定項目 ▲▲▲

// -----------------------------------------------------------------------------
// 【初回に1回だけ実行】APIキーを安全に保存する関数
// -----------------------------------------------------------------------------
function setupCredentials() {
  const scriptProps = PropertiesService.getScriptProperties();
  scriptProps.setProperties({
    'CLIENT_ID': CLIENT_ID,
    'CLIENT_SECRET': CLIENT_SECRET
  });
  Logger.log('Client ID と Client Secret をスクリプトプロパティに保存しました。');
}

// -----------------------------------------------------------------------------
// メインの処理:RSSを読み込んでXに投稿する (OAuth 2.0版)
// -----------------------------------------------------------------------------
function postLatestRssToX_v2() {
  const service = getService();
  if (!service.hasAccess()) {
    Logger.log("Xの認証がされていません。先に 'runFirstAuth' 関数を実行してください。");
    Logger.log("認証URL: " + service.getAuthorizationUrl());
    return;
  }

  const props = PropertiesService.getScriptProperties();
  const LAST_POSTED_GUID_KEY = 'LAST_POSTED_GUID';

  try {
    // 1. RSSフィードを取得
    const response = UrlFetchApp.fetch(RSS_URL);
    const xml = response.getContentText();
    const document = XmlService.parse(xml);
    const root = document.getRootElement();
    const channel = root.getChild('channel');
    const items = channel.getChildren('item');

    if (items.length === 0) {
      Logger.log('RSSフィードにアイテムが見つかりません。');
      return;
    }

    // 2. 最新のアイテムを取得
    const latestItem = items[0];
    const title = latestItem.getChild('title').getText();
    const link = latestItem.getChild('link').getText();
    let guid = latestItem.getChild('guid') ? latestItem.getChild('guid').getText() : link;

    // 3. 最後に投稿した記事と比較
    const lastPostedGuid = props.getProperty(LAST_POSTED_GUID_KEY);

    if (lastPostedGuid === guid) {
      Logger.log('新しい記事はありません。前回の投稿: ' + guid);
      return;
    }

    // 4. 投稿するテキストを作成
    let tweetText = `【新着記事】\n${title}\n${link}`;
    if (tweetText.length > 280) {
      const availableLength = 280 - link.length - 6;
      tweetText = `【新着記事】\n${title.substring(0, availableLength)}...\n${link}`;
    }

    // 5. X (Twitter) に投稿 (API v2 / OAuth 2.0)
    const payload = {
      'text': tweetText
    };
    
    const postResponse = UrlFetchApp.fetch("https://api.twitter.com/2/tweets", {
      method: "post",
      contentType: "application/json",
      headers: {
        Authorization: "Bearer " + service.getAccessToken() // OAuth 2.0 の Bearer トークン
      },
      payload: JSON.stringify(payload),
      muteHttpExceptions: true
    });

    const result = JSON.parse(postResponse.getContentText());
    Logger.log('API Response: ' + JSON.stringify(result));

    if (result.data && result.data.id) {
      Logger.log(`投稿成功: ${tweetText}`);
      // 6. 成功したら、今回の記事IDを保存
      props.setProperty(LAST_POSTED_GUID_KEY, guid);
    } else {
      Logger.log('投稿に失敗しました。レスポンスを確認してください。');
    }

  } catch (e) {
    Logger.log('エラーが発生しました: ' + e.toString());
  }
}


// -----------------------------------------------------------------------------
// OAuth 2.0 認証関連 (Zenn記事 参考)
// -----------------------------------------------------------------------------

/**
 * OAuth2 サービスを取得
 */
function getService() {
  pkceChallengeVerifier();
  const userProps = PropertiesService.getUserProperties();
  const scriptProps = PropertiesService.getScriptProperties();
  const clientId = scriptProps.getProperty('CLIENT_ID');
  const clientSecret = scriptProps.getProperty('CLIENT_SECRET');

  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(clientId)
    .setClientSecret(clientSecret)
    .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(clientId + ':' + clientSecret),
      'Content-Type': 'application/x-www-form-urlencoded'
    });
}

/**
 * 認証コールバック
 */
function authCallback(request) {
  const service = getService();
  const authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('認証に成功しました。タブを閉じてください。');
  } else {
    return HtmlService.createHtmlOutput('認証に失敗しました。');
  }
}

/**
 * PKCE(コード検証)の値を生成・保存
 */
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);
  }
}

/**
 * 【重要】コールバックURL(リダイレクトURI)を表示する
 * Xの開発者ポータルに設定するURLを確認するために使います。
 */
function logRedirectUri() {
  var service = getService();
  Logger.log(service.getRedirectUri());
}

/**
 * 【初回認証用】この関数を実行して認証を開始する
 */
function runFirstAuth() {
  const service = getService();
  if (service.hasAccess()) {
    Logger.log("すでに認証済みです。");
  } else {
    const authorizationUrl = service.getAuthorizationUrl();
    Logger.log('(重要)Xの開発者ポータルで「Callback URI / Redirect URL」に以下のURLを設定してください:');
    Logger.log(service.getRedirectUri());
    Logger.log('------------');
    Logger.log('設定が完了したら、以下のURLにアクセスして認証を行ってください: %s', authorizationUrl);
  }
}

このコードを見て、私は作業を続けることができました。
実際に上記のコードの必要な部分だけを更新し後は手順に沿って作業をするだけでした。

ちなみに上記コードを利用した際の手順は以下の通りで作業を進められます!

作業手順

実行方法(OAuth 2.0版)
手順がOAuth 1.0aと少し異なります。

ステップ1:CLIENT_ID と SECRET の保存

  1. コード内の ▼▼▼ ユーザー設定項目 ▼▼▼ に、X開発者ポータルで取得した CLIENT_ID と CLIENT_SECRET を貼り付けます。

  2. GASエディタで関数 setupCredentials を選択し、「実行」します。(権限を求められたら許可します)

  3. ログに「保存しました」と出ればOKです。(コード上のキーは削除しても構いません)

ステップ2:コールバックURLの確認と設定

  1. Xの開発者ポータルでの設定(OAuth 2.0設定)には、「Callback URI / Redirect URL」の登録が必須です。

  2. GASエディタで関数 logRedirectUri を選択し、「実行」します。

  3. ログに https://script.google.com/macros/d/(...あなたのスクリプトID...)/usercallback のようなURLが表示されます。

  4. このURLをコピーし、X開発者ポータルの「Callback URI / Redirect URL」欄に貼り付けて保存します。

ステップ3:初回認証(必須)

  1. GASエディタで関数 runFirstAuth を選択し、「実行」します。

  2. ログに https://twitter.com/i/oauth2/authorize?(...) のような長いURLが表示されます。

  3. このURLをコピーしてブラウザで開きます。

  4. Xの認証画面が表示されるので、「Authorize app(アプリを連携)」をクリックします。

  5. 「認証に成功しました。」と表示されれば、GASとXの連携は完了です。

ステップ4:RSS投稿のテスト実行

  1. RSS_URL をあなたの監視したいRSSに変更します。

  2. GASエディタで関数 postLatestRssToX_v2 を選択し、「実行」します。

  3. ログを確認し、エラーが出ていないか、Xに投稿されているかを確認します。

ステップ5:自動化(トリガー)の設定

  1. GASエディタの左側「トリガー」(時計アイコン)を開きます。

  2. 「トリガーを追加」をクリックします。

  3. 実行する関数を postLatestRssToX_v2 に設定します。

  4. イベントのソースを「時間主導型」にし、「1時間おき」などに設定して保存します。

🤖 AIの本当のすごさと気を付けないといけない点

AIを使っていて便利な点と気を付けないといけない点がまさに以下です。

便利な点:面倒な「RSSパース」は最初から完璧だった

これは1回目も2回目も共通ですが、
私が一番やりたくなかった「RSS (XML) をパースする」という面倒な処理(UrlFetchAppXmlService を使う部分)は、AIが完璧に実装してくれていました。
これだけでも、数時間の節約です。
AIを使う際のプロンプトで、正しい設計で投げてあげれば数秒~数分で欲しいコードを作成してくれます。

気を付けないといけない点:AIは常に正しい答えを返すとは限らない

AIは、
「はい、コピペで動きますよ」と嘘(古い情報)をついた後、
人間から 「こっちの記事が正しいよ」とフィードバックされる と、
その記事(OAuth 2.0)のロジックを瞬時に理解し、
自分の間違っていた部分(OAuth 1.0a)を捨て、
全く新しいアプローチでコードを「再設計」した のです。

ここがAIを使う点で気を付けないといけない点で、
常にAIは正しい答えを返すとは限らない( ハルシネーションが起こる )ので、
AIの回答の検証や確認は必ず実施しないといけないのです。

でもAIを上手に利用することで、「優秀なアシスタント」にもなり得るのです。

💡 AIは「完璧な答え」ではなく「最高のたたき台」をくれる

これで、AIとの付き合い方が明確になったかと思います。

重ねてになりますが、AIは必ずしも100点満点の「完璧な答え」を最初からくれるわけではありません
特に、情報の鮮度が重要なAPI周りでは、平気で古い情報を提示してきます。

しかし、AIがくれるのは、
90点の「最高のたたき台(プロトタイプ)」 でした。

私たち人間に残された仕事は、
その「たたき台」を見て、
「ここ、仕様が古いよ」
「ここのロジック、もっとこうして」
と、的確なフィードバック(ツッコミ)を入れること だけです。

AIが「一番時間のかかる9割」を終わらせてくれるので、
私たちは「一番重要な1割(レビューと修正指示)」に集中できる。

これって、ものすごい革命だと思いませんか?

😌 おわりに

「プログラミングは、専門知識を持った人だけのもの」
「自動化は、難しくて時間がかかるもの」

そんな時代は、もう終わりつつあります。

生成AIという「超優秀だけど、たまに古い情報を出す同僚」がいれば、
「こんなことできたらいいな」というアイデアを、
驚くほどのスピードで形にできます。

AIの答えが間違っていても、落ち込む必要はありません。

「お、AIくん、ここ間違ってるよ」
「こっちの資料(URL)読んで、書き直してみて」

と、気軽に「対話」してみてください。

きっと、数分後には、
あなたの「本当に欲しかったもの」が、
目の前に現れているはずです。

📚 参考出典

Discussion