Google Apps ScriptからTwitter APIをOAuth2.0認証で使う【GAS】

イーロン・マスクに買収されてしまったTwitter。其の結果、Twitter APIもいくつか大きな変更があり、前回の記事のコードだと動かないというケースが出てきました。それがOAuth2.0認証とツイートする部分。

前回のコードはOAuth1.0認証で、Twitter API v2を動かしていたのですが、今回はOAuth2.0認証をしてTwitter API v2を動かしています。前回の記事もできれば参照してみてください。

Google Apps ScriptからTwitter API v2を使ってツイートする【GAS】

今回使用するスプレッドシート

今回のファイルはOAuth1.0の処理は入っていません。また、OAuth2.0認証を行う仕組みとなっているため、前回とは異なる部分がいくつかあります。また、認証用のライブラリとしてOAuth2 library for Google Apps Scriptを利用しています。

前回のコードは以下のようなエラーメッセージが出てしまっており、Botも止まってしまっていました。故にOAuth1.0をやめて、OAuth2.0の認証に乗り換えてトライするのが目的です。

Uncaught  at Service_.getRequestToken_ (Service:344)
 at Service_.authorize (Service:245)

図:こんな感じでエラーが出てたのでこれをクリアする

サードパーティ締め出しの件について

概要

2023年1月13日付近から、Twitterクライアントのいくつかで接続出来なかったり、認証がそもそも出来ないなどの不具合がいくつも報告されている。有名どころのクライアントですら繋がらない状況。再認証が必要ですと出たまま認証も出来ずに使えないというのが主な現象。ただ、このページにあるようなWebAppの場合そのような報告が無いので、Standalone Appに関して起きてる様子。

このBAN祭り?とも言える現象についてまとめたスプレッドシートが有志から公開されている。

この件の続報として、どうやら広告収入に貢献しないので意図的に遮断してるという内部情報があるとかないとか。その理屈で行くと、将来的にTwitter API v2自体、使い続ける為には何らかの大きな制限が課せられるのではないかという憶測もある。

図:こんな感じで認証自体出来ない

買収後の履歴

  • 2023/1/20 - 続報として公式で明確にTwitterクライアントの禁止が明示されました。一斉にAppストアやPlayストアからもクライアントが消えてなくなりました。API自体はまだ廃止ということではないので、ウェブで使う分には問題なさそうです。
  • 2023/2/2 - Twitterから正式にTwitter APIの有料化が宣言されました。また、サードパーティクライアントの完全終了も宣言されています。よって、今後Twitter APIで手軽にBotを作成したり、ツイート内容を取得するといったことは課金しなければ出来ません。
  • 2023/2/14 - TwitterからAPIの有償化が再延期が発表。迷走しています。
  • 2023/2/18 - Twitterの2段階認証に於いてSMS認証が有償化しました。Twitter BlueでなければAuthenticatorアプリGoogle Titan Keyなどを使っての二段階認証が必要になりました。
  • 2023/3/10 - Twitter APIの無償プランを廃止し、非常に高額なプランが策定中との情報。事実上のTwitter APIの終焉ではないかと思います。
  • 2023/3/30 - 公式発表で新APIが発表されて、Free, Basic($100/月), Enterprise(価格不明)で落ち着いたようです。Freeは残りましたが、既存のプランは全廃で移行が必須のようです。
  • 2023/4/6 - 今回のスクリプト再認証を実行してツイートを読み取ってみましたが、手直し無しで今は稼働中
  • 2023/4/11 - Twitterという社名が消え、x.corpという社名に変更。旧無償プランが完全廃止。Standaloneが影響受けていて、OAuth2.0認証は影響受けていない様子(このスクリプトもReadでは動いています)
  • 2023/6/23 - Freeプランで動いていたOAuth2.0のAPIについて、Tweetデータの取得が完全に出来なくなりました。

新APIの仕様について

3/30発表の新Twitter APIですが、以下のような仕様になっている模様。

  • 旧APIは30日後に廃止される(現在のStandard, Essential, Elevated, Premiumというプランがコレ)
  • Freeプランで利用できるのは1ID1アプリのみ。また書き込みは1500回/月と制限される(読み取りは出来ない模様)
  • Basicプランで利用できるのは1ID2アプリまで。書き込みは1ユーザ3000回/月、1アプリ50000回/月まで。読み取り上限は10000回/月まで。
  • この回数はリツイートやDM、ブックマーク登録の動作も1回とカウントされてしまう
  • API経由での投稿は過去に全く同じ投稿をしてる場合エラーとなり投稿出来ない(You are not allowed to create a tweet with duplicate contentとなる。マスクがこの手の宣伝目的Botを嫌った為でしょう。)。何か投稿内容にちょっとした工夫が必要です。
  • 認証〜ツイートまでのコードはこのページで紹介してるOAuth2.0認証でV2 APIを叩くのと同じコードで行けるので修正は不要

1アプリ1IDとなると、複数のGASから投稿したい場合コールバックURLで引っかかってしまうので投稿用のGASのライブラリを作り、Access Tokenなどは1箇所で管理するようにプロジェクトに工夫が必要。もしくは複数アカウント作成してそれぞれでClient IDを取るなどが必要になります。

図:Bot運用は出来なくなります

Client_Forbiddenについて

2023年6月23日、ついに自分のAPIについてリクエストを投げたところ、「Client_Forbidden」と出てメッセージとして、「When authenticating requests to the Twitter API v2 endpoints, you must use keys and tokens from a Twitter developer App that is attached to a Project. You can create a project via the developer portal.」と表示され、Twitterデータが取得出来なくなりました。

つまりこれは、Freeのアカウントではアクセスレベルが足らないので、リクエストを処理出来ないということ。こちらにアクセスレベルのページがあるのですが、FreeについてはTwitter API v2 へのアクセスについては、月額100ドルを払わないとツイートの取得が出来なくなったことを表しています(ツイートは出来る模様)

よって、ツイートデータを収集する事がFreeアカウントでは不可能になりました。

図:事実上のAPIの終焉です

事前準備

GAS側の事前準備

ライブラリの追加

以下の手順でOAuth2 library for Google Apps Scriptライブラリを追加しましょう。

  1. スクリプトエディタを開きます。
  2. サイドバーよりより「ライブラリ」の+ボタンをクリック
  3. ライブラリを追加欄に「1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF」を追加します。
  4. 今回はバージョンは43を選択してみます。
  5. 保存ボタンを押して完了

これで、OAuth2.0認証にまつわる様々な関数を手軽に利用できるようになります。

図:ライブラリを追加した様子

コールバックURLを取得する

コールバックURLとは、認証を完了しAccess Tokenを取得したら戻るべきURLを指定するものです。これは、スクリプトIDをもとに作られているので、スクリプトIDを取得して組み立てます。

  1. スクリプトエディタのサイドバーより「プロジェクトの設定」を開く
  2. 情報の中にある「スクリプトID」を控えておく。
  3. https://script.google.com/macros/d/スクリプトID/usercallback として組み立てる。これがコールバックURLとなる。

図:スクリプトIDはファイル毎に異なるのです。

Twitter側の事前準備

アプリの作成

  1. Twitter DevelopersのDashboardにアクセスする
  2. 下のほうにある「Create App」をクリックする
  3. AppNameを指定します。今回は「tweetgasdata2」と命名しました。同じ名前は利用出来ないので、誰かが取ってると使えません。
  4. 作成が完了したら一旦dashboardに戻ると生成されてるのでクリックする
  5. User authentication settingsのところのeditをクリックする
  6. App permissionsRead and Writeを選択
  7. Type of AppWeb App, Automated App or Botを選択する
  8. Callback URI / Redirect URLには、前述のコールバックURLを入れる
  9. 自身のwebsite urlを入れる
  10. Saveをクリックする

元の画面に戻って、tweetgasdata2のプロジェクトページにある「Keys and tokens」を開き、OAuth 2.0 Client ID and Client Secretという項目を見ましょう。Client IDはすでに生成されてるのでこれを控えておきます。Client SecretはRegenerateをクリックすると生成されるので控えておきます。

注意点として、最初の1個目はstandardアプリとして作れますが、2個目以降はstand aloneアプリとなり、こちらではGASからはツイートは出来ません(403エラーになります

図:設定画面が結構変わっていた

図:ClientIDとSecret

利用制限

※この項目は3/30の新API登場により変更されています。前述の新APIの仕様をご覧ください。

Twitter API v2のQuotaを調べて見ると、以下のような制限があります。

  • ツイートは15分、1アプリ当たり300件(ユーザ単位だと900件まで) - v1エンドポイントの場合
  • ツイートは15分、1アプリ当たり200件 - v2エンドポイントの場合
  • また、v2エンドポイントでの投稿の場合、3時間以内にユーザ・アプリ毎に300リクエストまでという追加規制があります。
  • いいねは、24時間当たり1000リクエストまで
  • ツイートの取得は1リクエスト100件まで(100件を超える部分は、next tokenでの取得が必要)

他にも細かな項目ごとにリミットが掛けられているので、利用する場合には意識する必要があります。特に同じアプリを複数で共有する場合、あっという間にリミットに達してしまうので要注意。応答ヘッダーにその内容がx-rate-limitとして含まれているので、エラー処理を加えたい場合には考慮すると良いでしょう。超過すると429エラー(Too Many Request)が返ってきます。

また、API KeyはPermissionを変更すると使えなくなるので、再度生成する必要性があります。

同じようなキーワードでの検索の場合、リアルタイムでなければスプレッドシートにキャッシュしておいて、それを返すようにしておくと、リクエスト数を減らす事が可能(キャッシュはトリガー使用で1時間ごとに1回取得などにしておくと良いでしょう)。また、連続して大量のツイートを取得するようなケースでは、リミットに抵触しないように、Utilities.sleepでスリープ処理を入れるべきでしょう。

申請不要で使えるようになりました

※この項目は3/30の新API登場により変更されています。前述の新APIの仕様をご覧ください。

2021年11月15日、Twitter API v2が正式なAPIとしてv1から移行され、同時にこれまで煩雑な申請が必要であったAPI利用申請ですが、「Essensial」というレベルであれば、申請不要で利用可能になりました。その上位の「Elevated」というレベルでは申請が必要です。既にAPI利用申請を出して取得済みの人は、自動的にElevatedレベルになります(Elevated+やAcademic Researchという研究用レベルも用意されています。)

  • Essential : 1アプリケーション、50万ツイート/月の取得まで
  • Elevated : 3アプリケーション、200万ツイート/月の取得まで

となっています。詳細はこちらのURLより

図:自動的にElevatedになってた

OAuth2.0認証を行う処理を作る

ここまでで事前準備が完了し、あとは認証を実行してAccess Tokenを取得すればツイートの実行準備はすべて完了になります。認証部分のコードを作成しましょう。なお、Access Tokenの寿命は2時間ですが、ライブラリが自動的にRefresh Tokenで更新してくれるのでRefresh Tokenで更新するようなコードは不要です。

GAS側コード

大きく変更されたのは、Scopeという項目が増えた点。ここで明示的に利用するScopeを指定しておく必要があります。ツイートを投稿する場合でもtweet.writeとtweet.readの2つが必要になるので注意。また、OAuth2.0ではoffline.accessも必要となります。

さらに大きな変更として、code_challenge_methodとcode_challenge、setTokenHeaderの部分。

code_challengeRFC7636の既定に基づき、48〜128文字のランダムな文字列を指定したcode_verifier(コード中のpossibleという変数の部分)というものをcode_challenge_methodで算出した値を入れる必要がある。この計算用の関数をこちらのサイトの方が作っていらっしゃったので利用いたしました(pkceChallengeVerifierという関数がそれになります。)。

code_challenge_methodはS256を指定します。

setTokenHeaderでは、client_idとclient_secretをbase64encodeしたBasic認証を作ってヘッダに加える必要があります。この辺りの下りはここのドキュメントに記載されています。

//認証用の各種変数
var appid = 'ここにクライアントIDを入れる';
var appsecret='ここにクライアントシークレットを入れる';
var scope = "tweet.write tweet.read users.read offline.access"
var authurl = "https://twitter.com/i/oauth2/authorize"
var tokenurl = "https://api.twitter.com/2/oauth2/token"

function startoauth(){
  //UIを取得する
  var ui = SpreadsheetApp.getUi();
  
  //認証済みかチェックする
  var service = checkOAuth();
  if (!service.hasAccess()) {
    //認証画面を出力
    var output = HtmlService.createHtmlOutputFromFile('template').setHeight(450).setWidth(500).setSandboxMode(HtmlService.SandboxMode.IFRAME);
    ui.showModalDialog(output, 'OAuth2.0認証');
  } else {
    //認証済みなので終了する
    ui.alert("すでに認証済みです。");
  }
}

//アクセストークンURLを含んだHTMLを返す関数
function authpage(){
  var service = checkOAuth();
  var authorizationUrl = service.getAuthorizationUrl();

  console.log(authorizationUrl)


  var html = "<center><b><a href='" + authorizationUrl + "' target='_blank' onclick='closeMe();'>アクセス承認</a></b></center>"
  return html;
}

//認証チェック
function checkOAuth() {
  pkceChallengeVerifier();
  const prop = PropertiesService.getUserProperties();

  return OAuth2.createService("twitter")
    .setAuthorizationBaseUrl(authurl)
    .setTokenUrl(tokenurl + '?code_verifier=' + prop.getProperty("code_verifier"))
    .setClientId(appid)
    .setClientSecret(appsecret)
    .setScope(scope)
    .setCallbackFunction("authCallback") //認証を受けたら受け取る関数を指定する
    .setPropertyStore(PropertiesService.getScriptProperties())  //スクリプトプロパティに保存する
    .setParam("response_type", "code")
    .setParam('code_challenge_method', 'S256')
    .setParam('code_challenge', prop.getProperty("code_challenge"))
    .setTokenHeaders({
      'Authorization': 'Basic ' + Utilities.base64Encode(appid + ':' + appsecret),
      'Content-Type': 'application/x-www-form-urlencoded'
    })
}

//認証コールバック
function authCallback(request) {
  var service = checkOAuth();
  var isAuthorized = service.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput("認証に成功しました。ページを閉じてください。");
  } else {
    return HtmlService.createHtmlOutput("認証に失敗しました。");
  }
}

//ログアウト
function reset() {
  checkOAuth().reset();
  SpreadsheetApp.getUi().alert("ログアウトしました。")
}

function pkceChallengeVerifier() {
  var prop = PropertiesService.getUserProperties();
  if (!prop.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(/=+$/, '')
    prop.setProperty("code_verifier", verifier)
    prop.setProperty("code_challenge", challenge)
  }
}
  • 取得したAPI Key、API Secretをそれぞれコード内に記述します。
  • 今回利用するv2用のエンドポイントは、https://api.twitter.com/2/tweetsとなっています
  • startoauthを実行して認証を実行すれば、スクリプトプロパティにAccess Tokenが格納されます。
  • 認証時に次項のTemplate.htmlが呼び出され、認証完了するとCallback URLに指定したURLにアクセスされて、authCallBackが実行されます。
  • reset関数はログアウトされて、再度認証ができるようになります。

HTML側コード

template.htmlというダイアログ用のファイルを用意します。ここでアクセス承認を実行し、ログインをすると、アクセストークンその他が取得可能になります。

<link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
<script type="text/javascript" src="https://apis.google.com/js/api.js"></script>
<script>
  google.script.run.withSuccessHandler(onSuccess).authpage();
  
  function onSuccess(data){
    document.getElementById("kinoko").innerHTML = data;
  }

</script>

<style type="text/css">
  /* --- ボックス --- */
  div.section {
    width: 480px; /* ボックスの幅 */
    background-color: #ffffff; /* ボックスの背景色 */
    border: 1px #c0c0c0 solid; /* ボックスの境界線 */
    font-size: 100%; /* ボックスの文字サイズ */
  }
  
  /* --- 見出し --- */
  div.section h3 {
    margin: 0; /* 見出しのマージン */
    padding: 6px 10px; /* 見出しのパディング(上下、左右) */
    background-color: #f5f5f5; /* 見出しの背景色 */
    border-bottom: 1px #c0c0c0 solid; /* 見出しの下境界線 */
    font-size: 120%; /* 見出しの文字サイズ */
  }
  
  /* --- ボックス内の段落 --- */
  div.section p {
    margin: 1em 10px; /* 段落のマージン(上下、左右) */
  }
</style>

<div class='section'>
<img border="0" src="https://officeforest.org/library/oauth2.png" alt="oauth2">
<h3 id='header'>OAuth認証の許可が必要です。</h3>
<hr>
<div id="info">
<p>
このスクリプトは、Twitter APIにアクセスするために、特別なログイン処理を利用しています。<br>
既に特別なログインに関する設定はなされており、承認がされるとプログラムを実行することが出来ます。この承認がなされない場合、プログラムの実行に制限が掛かり、
処理が続行できません。<br><br>

<div id="kinoko"></div>
</div>
<p>
<script>
function closeMe(){
  if(google && google.script && google.script.host){
      google.script.host.close();
  } else if(window && window.close){
      window.close();
  } 
}
</script>
</div>
  • 実際にこれらのコードで、startoauthを実行すると、スプレッドシート上で認証用のダイアログが出ます。
  • 認証でtwitterにログインします
  • 取得したAccess Tokenほかはスクリプトプロパティのtwitterという項目にガッツリ値が格納されます。

図:認証実行のダイアログがこちら

ツイート関係の処理

エンドポイントの情報は以下の通りです

//Tweet Endpoint
var endpoint2 = "https://api.twitter.com/2/tweets";
var getpoint = "https://api.twitter.com/2/tweets/search/recent?query="; //ツイートを検索取得する為のエンドポイント

認証の実行

スプレッドシートを開き直して、メニューより「OAuth認証」を開くと、「認証の実行」があるので、クリックして実行します。ダイアログが出てくるので、ちょっと待つとアクセス認証の文字が出てくるのでクリックし、Twitterの認証画面が出てくるので、アプリにアクセスを許可をクリックします

実行するとAccess Tokenが取得されてスクリプトプロパティに格納され、認証が完了します。

図:認証を実行してみた

取得したTokenを確認

スクリプトエディタのスクリプトプロパティには取得すたAccess Token等の情報が格納されているのでそれで確認が可能です。

図:無事取得出来てるのを確認する

ツイートの実行

エンドポイントに対してPOSTでリクエストをする必要があります。メッセージはJSON.stringifyでくくってpayloadとして送り込むことになります。注意点として同じツイートを連続して送ることが出来ません。エラーとなります(スパム対策と思われる)。

//テストツイートする
function testtweet2(){
  let ui = SpreadsheetApp.getUi();

  //トークン確認
  var service = checkOAuth();

  if (service.hasAccess()) {   
    //message本文
    var message = {
      text: 'GASからツイートを実行してみた@v2 API'
    }

    //リクエストヘッダ
    let header = {
      Authorization: 'Bearer ' + service.getAccessToken()
    }

    //リクエスト実行
    const response = UrlFetchApp.fetch(endpoint2, {
      method: "post",
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken()
      },
      muteHttpExceptions: true,
      payload: JSON.stringify(message),
      contentType: "application/json"
    });

    //リクエスト結果を取得する
    const result = JSON.parse(response.getContentText());

    //リクエスト結果を表示
    console.log(JSON.stringify(result, null, 2))
  } else {
    ui.alert("認証が実行されていませんよ。");
  }
}

図:GASから実行した結果

ツイートを取得してみる

エンドポイントURLに対して、GETメソッドで投げる必要性があります。リクエストパラメータが変更されていて、しかも公式ドキュメントが非常に読みにくい物になっています。今回はクエリの指定にて最新の40件だけ取得するようにしています。

キーワード検索で「Google Apps Script」に関するものに対して、リツイートを除外し日本語だけのものをkeywordに指定しています。1度に20件ずつ取得が可能で、next_tokenを使って次の20件を取得する事が可能です。

//特定ワードでツイートを取得する
function gettweet(){
  let ui = SpreadsheetApp.getUi();

  //トークン確認
  var service = checkOAuth();

  //サービス確認後
  if (service.hasAccess()) {   
    //検索オプション
    var option = "&tweet.fields=author_id,id,text,created_at&max_results=20";

    //検索キーワード(URI Encodeする)
    var keyword = encodeURIComponent("\"google apps script\" -is:retweet lang:ja")

    //検索キーワードとURL構築
    var url = getpoint + keyword + option;

    //リクエスト実行
    var response = UrlFetchApp.fetch(url, {
      method: "GET",
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken()
      },
      muteHttpExceptions: true,
      contentType: "application/json"
    });

    var result = JSON.parse(response.getContentText());

    //リクエスト結果からnext tokenを取得する
    var next = result.meta.next_token;

    //dataの中身を配列に書き出す
    var array = [];
    var data = result.data;
    var length = data.length;

    for(var i = 0;i<length;i++){
      //一時配列を用意
      var temparr = [];

      //レスポンスデータを配列に書き出す
      temparr.push(data[i].author_id);
      temparr.push(data[i].text);
      temparr.push(data[i].created_at);

      //書込み用配列に追加
      array.push(temparr);
    }

    //次の20件を取得する(nexttokenを使用する)
    option = option + "&next_token=" + next;

    //URL構築
    url = getpoint + keyword + option;

    //リクエスト実行
    response = UrlFetchApp.fetch(url, {
      method: "GET",
      headers: {
        Authorization: 'Bearer ' + service.getAccessToken()
      },
      muteHttpExceptions: true,
      contentType: "application/json"
    });

    result = JSON.parse(response.getContentText());

    //スリープを10秒入れる
    Utilities.sleep(10 * 1000);

    //次の20件のデータをarrayに追加
    data = result.data;
    length = data.length;

    for(var i = 0;i<length;i++){
      //一時配列を用意
      var temparr = [];

      //レスポンスデータを配列に書き出す
      temparr.push(data[i].author_id);
      temparr.push(data[i].text);
      temparr.push(data[i].created_at);

      //書込み用配列に追加
      array.push(temparr);
    }

    //arrayをスプレッドシートに上書きで書込み
    var ss = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1");
    var colcnt = array[0].length;
    var rowcnt = array.length;
    ss.getRange(2,1,rowcnt,colcnt).setValues(array);

    //完了メッセージ
    ui.alert("ツイートデータの取得が完了しました。")

  }else{
    ui.alert("認証が実行されていませんよ。");
  }
}

図:Tweetを取得することが出来ました。

実装事例サンプル

今回のOAuth2.0認証を装備し直したTwitter API v2を叩くGoogle Apps Scriptのコードで、GAS最新情報にて利用してるRSSリーダーを実装し直しました。現在Version 5.0としてスプレッドシートも公開しています。

GAS最新情報

関連リンク

Google Apps ScriptからTwitter APIをOAuth2.0認証で使う【GAS】” に対して2件のコメントがあります。

  1. YANMMER より:

    はじめまして
    すいませんが教えていただきたいことがありコメントします。
    BOTではなくシート上の内容を手動でツイートしたいのですが
    その場合の記述はどうなりますでしょうか
    よろしくお願いいたします。

    1. officeの杜 より:

      YANMMERさん

      officeの杜管理人です。
      結局のところ、testtweet2関数にテキストの引数を渡し、messageの部分に入れてツイートすることになるので、予めスプレッドシートの塊を取得し、ループで回して、必要なものだけtesttweet2関数に渡して上げれば良いです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)