Twitter API と GAS を用いた情報収集 - 2. GAS+OAuth 編

一連の記事では、TwitterAPI の情報を GAS 経由で取得し、Google スプレッドシートに録り溜めていく方法を、ステップバイステップで説明していきます。

[前回](https://qiita.com/anti-digital/items/5f085d8d7361785f7def)は Twitter Developer Account の取得について説明しました。
今回は GAS から基本的な TwitterAPI を呼ぶところまで説明します。

文章で書いてしまうと複雑で面倒そうに見えますが、実際に行うことはわずかで、15 分もあれば十分です。

# 1. Google アカウントの取得

分からない方はいないと思いますんで割愛します。

# 2. OAuth1 ライブラリの導入

TwitterAPI の実際の姿は、特定の URL で、そこにアクセスして情報を取得することになります。
例えば、タイムラインから 5 つツイートを取得するのは、次のような呼び出しとなります。

`https://api.twitter.com/1.1/statuses/home_timeline.json?count=5`

(詳細: https://developer.twitter.com/en/docs/twitter-api/v1/tweets/timelines/api-reference/get-statuses-home_timeline)

しかし、この URL をいきなり呼ぶことはできません。

OAuth をかませる必要があります。
GAS でこの処理を簡便化するライブラリ、[OAuth1](https://github.com/googleworkspace/apps-script-oauth1) がユーザーから提供されており、これを使用します。

次のステップでライブラリを導入します。

**2.(a) Google Drive に Google App Script を作ります。**
このスクリプトに OAuth の実装、TwitterAPI の呼び出しなど、全てを実装していく想定です。
<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/e78d0648-39d0-fa3d-765e-0b7571762b73.png">

**2.(b) 作ったスクリプトを開き、"Libraries" の横にある "+" を押します。**
<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/3be3cc70-f8f1-530e-0436-43baf4d9ff11.png">

**2.(c) Script ID として `1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s` を指定し、"Look up" します。**
すると、次のように OAuth1 ライブラリが見つかるはずなので、これを "Add" します。

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/1ef5ca2d-f1ea-d0fe-6c34-6749dc2320d1.png">

これで [OAuth1 ライブラリ](https://github.com/googleworkspace/apps-script-oauth1)が使用できる状態になりました。

# 3. Twitter Developer Portal でキーの生成

GAS から OAuth1 を経由して TwitterAPI を使用できるように、[Twitter Developer Portal](https://developer.twitter.com/en/portal) でキーを生成し、これを用いて OAuth1 からアクセスするための準備をします。
以下のステップで行います。

**3.(a) [Twitter Developer Portal](https://developer.twitter.com/en/portal) へ行き、Standalone Apps から Create App を押します。**

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/8e41de40-0fba-18d2-8dc0-66d299dcab04.png">

**3.(b) "Name Your App" で、適当な名前を入れます。ここでは SampleFetchTweet としました。**

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/d4bc91af-cc5c-42ec-5b7a-2592c9d27262.png">

**3.(c) Key が生成されるので、これらを控えた後、閉じます。(控えておかないと再表示できないので、その場合は再生成することになります)**
API Key と API Secret Key が OAuth1 ライブラリに後で設定する consumer API Key と consumer API Secret Key にそれぞれ相当します。

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/b42992ab-9d9c-967d-08d3-e9db64948907.png">

以上で、Twitter Developer Dashboard でのアプリのキーの生成は終わります。

# 4. GAS のコールバックを Twitter Developer Portal に設定

今度は GAS の方で OAuth の応答を受け取れるよう、先程登録した Twitter Developer Portal のアプリに GAS のコールバックを設定します。
以下のステップで行います。

**4.(a) GAS スクリプトに戻り、スクリプトの ID を確認します。**

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/1bcb316a-ca8b-817c-70d4-fa8ee6b08bc4.png">

この ID を用いた下記 URL が OAuth の応答を受け取るコールバックとなります。
`https://script.google.com/macros/d/{Script ID}/usercallback`
詳細については [GAS のリファレンス](https://developers.google.com/apps-script/migration/oauth-config)を参照してください。

**4.(b) 再び Twitter Developer Dashboard に作ったアプリの設定画面に行き、[Settings] ⇒ [Authentication settings] ⇒ [Enable 3-legged OAuth] を有効にした後、上記コールバック URL を指定します。**
設定を保存するには、[Website URL] も記入しないといけないので、妥当なものを入力して [Save] します。

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/72df7efe-1cf3-cd1a-4e7d-045b583ebf96.png">

以上で準備ができました。あとはコーディングです。

# 5. OAuth1 を用いて GAS と Twitter を連携させる

次のコードは、[OAuth1 ライブラリのリポジトリ](https://github.com/googleworkspace/apps-script-oauth1) の Usage に沿った、最小の構成のコードとなります。
Usage に書かれている方法よりさらにシンプルに、authorization URL をログに書き出すようにしています。
Google Apps Script ファイルを作った時にデフォルトで書かれている `myFunction()` の記述を含む全てを削除して、まるごとこのスクリプトで置き換えてください。
このスクリプトの中の `VAL_CONSUMER_API_KEY` と `VAL_CONSUMER_API_SECRET` には、Twitter Developer Dashboard で先程生成した API Key と API Secret Key の値をそれぞれ記入する必要があります。

```JavaScript
//let VAL_CONSUMER_API_KEY     = PropertiesService.getScriptProperties().getProperty("CONSUMER_API_KEY");
//let VAL_CONSUMER_API_SECRET  = PropertiesService.getScriptProperties().getProperty("CONSUMER_API_SECRET");
let VAL_CONSUMER_API_KEY     = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
let VAL_CONSUMER_API_SECRET  = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
function logOAuthURL() {
  var twitterService = getTwitterService();
  Logger.log(twitterService.authorize());
}
function getTwitterService() {
  // Create a new service with the given name. The name will be used when
  // persisting the authorized token, so ensure it is unique within the
  // scope of the property store.
  return OAuth1.createService('twitter')
      // Set the endpoint URLs.
      .setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
      .setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
      .setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
      // Set the consumer key and secret.
      .setConsumerKey(VAL_CONSUMER_API_KEY)
      .setConsumerSecret(VAL_CONSUMER_API_SECRET)
      // Set the name of the callback function in the script referenced
      // above that should be invoked to complete the OAuth flow.
      .setCallbackFunction('authCallback')
      // Set the property store where authorized tokens should be persisted.
      .setPropertyStore(PropertiesService.getUserProperties());
}
function resetTwitterService() {
    var twitterService = getTwitterService();
    twitterService.reset();
}
function authCallback(request) {
  var twitterService = getTwitterService();
  var isAuthorized = twitterService.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('Success! You can close this tab.');
  } else {
    return HtmlService.createHtmlOutput('Denied. You can close this tab');
  }
}
```
最上部のコメントアウトしている 2 行は、Key や Secret 値を GAS のプロパティとして外部に出して、そこから読む場合の記述です。
こうするとコードの中に Key や Secret 値をハードコードする必要がなくなりますが、ここではシンプルにハードコードで進めます。
コードができたら、次のステップで連携処理を行います。

**5.(a) GAS エディタ上で logOAuthURL() を実行させます。**

**5.(b) すると権限のレビューを行うようダイアログが表示されますので、"Review permissions" を押します。**
<img width=300 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/07992705-02ac-0665-1aa0-61e942e0ae71.png">

**5.(c) 危険なアプリダイアログが表示されますが、自分の作っているアプリ (スクリプト) なので "Advanced" を押し、先に勧めます。**

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/053bab9f-ab74-fad7-a3aa-3493b94bf3e5.png">

**5.(d) 続くダイアログで許可を行います。**

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/fbfcfae9-653b-80b9-6dc0-d7944449bf7d.png">

**5.(e) ついにスクリプトが実行され、Twitter 側からコールバックのレスポンスがログに残ります。**

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/be7b7450-f401-4076-01ad-fe9b7608e531.png">

**5.(f) ログに記載された URL をコピーして、ブラウザで開きます。**

すると、アプリケーションの連携でよく見る認証画面が表示されます。
<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/5b5dcd5f-443d-d799-9719-625dcf8c8355.png">

**5.(g) 上記画面で [連携アプリを認証] を押し、一連の処理が成功したことを確認します。**

"Success! You can close this tab." と表示されれば成功です。
<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/6676e6a3-254a-487b-c748-a40f90329f5a.png">


これまでのステップで、GAS からの Twitter API の呼び出しが許可され、Twitter API を呼び出すための準備が整いました。
以降 **logOAuthURL()** を呼び出す必要はありませんが、Key を再生成した場合などは、 **resetTwitterService()** を呼び出してリセット後、 **logOAuthURL()** を改めて呼び出す必要があります。

# 6. Twitter API の呼び出し

これまでのステップで、Twitter API を自由に呼べる状況となりました。
今回は簡単に Search API を呼んで、その結果を取得できることをログで確認して終わりたいと思います。
上記コードに付け足して次のようにします。

```JavaScript
//let VAL_CONSUMER_API_KEY     = PropertiesService.getScriptProperties().getProperty("CONSUMER_API_KEY");
//let VAL_CONSUMER_API_SECRET  = PropertiesService.getScriptProperties().getProperty("CONSUMER_API_SECRET");
let VAL_CONSUMER_API_KEY     = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
let VAL_CONSUMER_API_SECRET  = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
function logOAuthURL() {
  var twitterService = getTwitterService();
  Logger.log(twitterService.authorize());
}
function getTwitterService() {
  // Create a new service with the given name. The name will be used when
  // persisting the authorized token, so ensure it is unique within the
  // scope of the property store.
  return OAuth1.createService('twitter')
      // Set the endpoint URLs.
      .setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
      .setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
      .setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
      // Set the consumer key and secret.
      .setConsumerKey(VAL_CONSUMER_API_KEY)
      .setConsumerSecret(VAL_CONSUMER_API_SECRET)
      // Set the name of the callback function in the script referenced
      // above that should be invoked to complete the OAuth flow.
      .setCallbackFunction('authCallback')
      // Set the property store where authorized tokens should be persisted.
      .setPropertyStore(PropertiesService.getUserProperties());
}
function resetTwitterService() {
    var twitterService = getTwitterService();
    twitterService.reset();
}
function authCallback(request) {
  var twitterService = getTwitterService();
  var isAuthorized = twitterService.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('Success! You can close this tab.');
  } else {
    return HtmlService.createHtmlOutput('Denied. You can close this tab');
  }
}
// ⇓ ここから付け足した部分 ⇓
function twitterSearch(keyword, maxCount) {
    let encodedKeyword = encodeURIComponent(keyword);
    try {
        var twitterService = getTwitterService();
        if (!twitterService.hasAccess()) {
            Logger.log(twitterService.getLastError());
            return null;
        }
        var url = 'https://api.twitter.com/1.1/search/tweets.json?q='
            + encodedKeyword
            + '&result_type=recent&lang=ja&locale=ja&count='
            + maxCount;
        var response = twitterService.fetch(url, { method: "GET" });
        var json = JSON.parse(response);
        Logger.log(json);
        return json;
    }
    catch (ex) {
        Logger.log(ex)
        return null;
    }
}
function main() {
  twitterSearch("天気",50);
}
```

**twitterSearch()** という関数を用意し、それを **main()** 関数で呼び出します。
**twitterSearch()** は、検索キーワードと、取得最大ツイート数を引数にとります。結果の json は、今回はログにそのまま吐き出します。
キーワードは **encodeURIComponent()** を用いて、いわゆるパーセントエンコーディングしています。
また、TwitterAPI Search のパラメータとして、「最新」(`result_type=recent`)、「日本語」(`lang=ja`)、「日本国」(`locale=ja`)、と最大値 (`count`) を与えています。

実際に **main()** で呼び出す **twitterSearch()** に "天気" をハードコードで指定してスクリプトを実行させると、以下のようなログが取得でき、うまく動いていることが確認できました。

<img width=400 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1859956/8cb38f02-52d9-7e5f-ec0b-37a9a99476b0.png">

次回は、[今回用意した GAS を拡張して Google スプレッドシートと連携]((https://qiita.com/anti-digital/items/f7d6de42974066ad1f25)) させたいと思います。
GAS に詳しい人であれば、今回までの記事で必要な情報は得られたと思います。


---
* [Twitter API と GAS を用いた情報収集 - 1. Twitter Developer Account 編](https://qiita.com/anti-digital/items/5f085d8d7361785f7def)
* [Twitter API と GAS を用いた情報収集 - 2. GAS+OAuth 編](https://qiita.com/anti-digital/items/acbd70b3ecedc6ff0b38)
* [Twitter API と GAS を用いた情報収集 - 3. Google スプレッドシートとの連携 編](https://qiita.com/anti-digital/items/f7d6de42974066ad1f25)