GASを使って、Twitter検索した結果をSlackへ定期投稿してみた

# この記事について
Google Apps Script（以下、GAS）を使って、情報収集のために、ある固定のキーワードで定期的に検索を行い、Slackへ自動投稿する、ということをやってみた備忘録です。
[GASから定期的に固定メッセージを自動投稿する](https://qiita.com/nobu09/items/a098e2c2345fd17c4b71)というのを前回実施したので、その続きです。
（ちなみに、TwitterとSlackの連携は、[この手順](https://slack.com/intl/ja-jp/help/articles/205346227-twitter-%E3%81%A8-slack-%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B)ですぐできます）


# 手順
大まかな手順です。

1. Twitter Developer Accountの申請
2. Twitter Appの作成
3. GASスクリプト作成
    4. Twitter Bearerトークン取得
    5. Twitter 検索
    6. Twitterの検索結果をSlackに通知
7. GASトリガー設定（定期的実行）

# 手順詳細
## 1. Twitter Developer Accountの申請
Twitterの検索APIを呼ぶためには、Twitter Developer Accountの取得が必要です。
[Twitter Developer](https://developer.twitter.com/)のサイトから申請し、承認される必要があります。
右上の[Apply]から申請を開始できます。
<img width="700" alt="01_twitterDevAccount.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/401d342f-9ceb-23db-5290-dbd425e17e58.png">

申請時、Twitterアカウントでログインを求められるので、作成するアプリで利用するTwitterアカウントが必要です。私は、既存の普段使っているアカウントを使用しました。

また、Twitter APIを利用する予定のアプリケーションについて、その内容や目的などを英語で書く必要がありました。Google翻訳を利用しながら埋めました。
申請には30分〜1時間くらい時間がかかったので、時間に余裕があるときに取り組むとよさそうです。
Twitter Developerの登録については既に色々な記事が書かれているので、気をつけるべきポイントを事前に確認できました。

承認されると、このようなメールアドレス確認のメールが届きました。
<img width="500" alt="02_twitterDevAccountConfirm.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/5ecc9319-0dcb-bd11-e75b-3019f9d67866.png">

メールアドレスを確認すると、無事にDeveloper Accountが取得できました！ :tada: 
<img width="700" alt="03_twitterDevAccountComplete.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/c3f70c3b-5784-caf8-bd2c-ccbc33681cce.png">

私の場合、申請してから承認されるのに1時間もかからなかった記憶があるのですが、このあたりは申請内容に拠るのだと思います。既出の記事だと、数日かかる場合もあるようでした。

## 2. Twitter Appの作成
Twitterの公式ドキュメントのGetStartedに従いながら進めます。
[Twitter Developer Account / GetStarted](https://developer.twitter.com/en/account/get-started)

Twitter APIを使用するには、OAuth認証スキームの一部として、アプリを作成する必要があります。
アプリを作成することで、API呼び出しに必要なAPI keysが発行されます。
Twitter Developerサイトの[Apps](https://developer.twitter.com/en/apps)ページへ行き、[Create an app](https://developer.twitter.com/en/apps/create)から必要な項目を入力し、Appを作成します。
<img width="700" alt="04_twitter_apps.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/82258841-89b5-10e8-5891-1044b354df64.png">
<img width="700" alt="05_twitter_create_app.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/d7617026-f16b-f38e-da90-a0a912f61555.png">
<img width="700" alt="09_create_app_form.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/aaba9195-7b5f-d7d8-a693-f96f6a80999c.png">

アプリが作成され、`Consumer API keys`が生成されました。
`Website URL`が必須項目だったので、作成する予定のGASスクリプトのURLを登録しました。（URLを指定するために、空のGASスクリプトを作成しておきました。）
[Keys and tokens]の注意書きで書かれているように、2020年1月20日以降は、初回に`access token`と`access token secret`を生成した以降はこのページで表示されなくなるので、必要なら書き留めておく必要がありそうです。またはいつでもこのサイトから再生成できるようです。
<img width="700" alt="06_twitter_create_app.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/8fc29706-cda2-4677-2bb3-cdfee5bef485.png">
<img width="700" alt="07_twitter_api_keys.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/c3a6ff77-3ffd-c124-b44b-d460aeec5494.png">

アプリの`Access permission`は、ツイートを読み込むだけでツイートの書き込みはしないので、`Read-only`に設定しました。
<img width="700" alt="08_twitter_permission.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/b6db99f0-12e4-66d3-92da-735ab0171398.png">

参考になりそうな公式ドキュメントはこちら :point_down: です。

- [Twitter Developer Account / GetStarted](https://developer.twitter.com/en/account/get-started)
- [Docs]
(https://developer.twitter.com/en/docs)

これで、Twitter APIを呼び出す準備が整いました。

## 3. GASスクリプト作成
ここから、GASのスクリプトを書いていきます。
[この手順](https://qiita.com/nobu09/items/a098e2c2345fd17c4b71#3-gas%E3%81%A7%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E4%BD%9C%E6%88%90)の1-3を参考に、GASのスクリプトを用意します。

### 1. Twitter Bearerトークン取得
Twitter APIを呼ぶには認証が必要です。

特定のアカウントのデータにアクセスする（ユーザの情報を読み取ったり、そのユーザとして書き込みする）にはアクセストークンでの認証が必要です。公開されている情報にアプリケーションとしてアクセスするにはアプリケーション認証（Bearerトークン）を行います。
今回は、公開されている情報にアクセスすれば十分なので、Bearerトークンを使ったアプリケーション認証を行います。
認証の処理の流れは、こちらの図がわかりやすいです。
<img width="500" alt="10_application_auth.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/720d5b77-3dbb-1449-62b1-fd01f184e81b.png">
出典：[Application-only authentication](https://developer.twitter.com/en/docs/basics/authentication/oauth-2-0/application-only)

Twitter認証について参考になりそうなページはこちら :point_down: です。

- [Twitter Developer Docs Autentication overview](https://developer.twitter.com/en/docs/basics/authentication/overview)
- [Twitter REST APIの使い方](https://syncer.jp/Web/API/Twitter/REST_API/)
- [OAuth 2.0 Bearer Token](https://developer.twitter.com/en/docs/basics/authentication/oauth-2-0)
- [Application-only authentication](https://developer.twitter.com/en/docs/basics/authentication/oauth-2-0/application-only)

GASで、TwitterのBearerトークンを取得するプログラムを書いていきます。
[Twitterのドキュメント（Application-only authentication）](https://developer.twitter.com/en/docs/basics/authentication/oauth-2-0/application-only)に、アプリケーション認証の方法が書かれているので、それに沿って進めます。

`POST oauth2/token`に、以下のようなリクエストを行う必要があります。

- `Authorization`ヘッダに、`Consumer key`、コロン[:]、`Consumer secret`を連結してBase64エンコードした文字列を`Basic xxxxxxxxxxx`の形式で設定
- `Content-Type`ヘッダに`application/x-www-form-urlencoded;charset=UTF-8`を設定
- リクエストボデイに`grant_type=client_credentials`を設定

（ドキュメントでは、最初に`Consumer key`と`Consumer secret`を`RFC1738`に沿ってURLエンコードする必要があると書いてありました。この手順は`Consumer key`と`Consumer secret`の値を変更しないが、これらの値の形式が将来変更される場合に備えてこの手順を実行する必要があるということです。今回は、現時点で値は変更されないので省略しました。）

参考にしたURLです。

- [Class Utilities（Google Developers）](https://developers.google.com/apps-script/reference/utilities/utilities#base64Encode(Byte))
    - GASでBase64エンコードする方法
- [POST oauth2/token（Twitter Docs）](https://developer.twitter.com/en/docs/basics/authentication/api-reference/token)
    - TwitterのBearerトークン取得のAPIリファレンス
- [UrlFetchApp（Google Developers）](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app#fetchurl)
    - GASでPOSTする方法

Bearerトークンを取得するコードです。

```js
var consumer_key = 'xxxxxxxxxxxxxxxxx';
var consumer_secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

function searchTweetsApps() { 
  // ①Twitter Bearerトークンの取得（検索APIの呼び出しに必要）
  // POST oauth2/token  https://developer.twitter.com/en/docs/basics/authentication/api-reference/token
  var blob = Utilities.newBlob(consumer_key + ':' + consumer_secret);
  var credential = Utilities.base64Encode(blob.getBytes());

  var formData = {
    'grant_type': 'client_credentials'
  };
  
  var basic_auth_header = {
    'Authorization': 'Basic ' + credential
  };
  
  var options = {
    'method': 'post',
    'contentType': 'application/x-www-form-urlencoded;charset=UTF-8',
    'headers':  basic_auth_header,
    'payload': formData,
  };
  
  var oauth2_response = UrlFetchApp.fetch('https://api.twitter.com/oauth2/token', options);  
  var bearer_token = JSON.parse(oauth2_response).access_token; 
```

ちゃんとBearerトークンが取得できているか見たいので、GASでログに書き出してみます。

```js
Logger.log(bearer_token);
```

GASの[View]->[Logs]から見ると、Bearerトークンが参照できたので問題なさそうです。

<img width="700" alt="11_bearer_token.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/7a1fea72-ae86-15ac-1786-fb7cc994cd2a.png">

### 2. Twitter 検索
やっと、本題のTwitter検索です。
Twitterの検索APIは、こちらのドキュメント[Search Tweets API overview](https://developer.twitter.com/en/docs/tweets/search/overview)を見ていただけるとわかるのですが、Standard, Premium, Enterpriseの3種類があります。検索対象の期間、有料/無料などが異なり、できることが異なっています。
今回は、無料で7日前までのツイートを検索できるStandardを使います。

APIリクエストの`Authorization`ヘッダに、前述の手順で取得したBearerトークンを、`Bearer xxxxxxxxxxxxx`の形式で設定します。
リファレンスはこちら :point_down: です。

- [Standard search API](https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets)
    - Standard search APIのリファレンス。
- [Tweet objects](https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/tweet-object)
    - Twitter検索結果のAPIが返すツイートのオブジェクトのリファレンス。
- [Using Standard Search](https://developer.twitter.com/en/docs/tweets/search/guides/standard-operators)
    - クエリの組み立て方についてのリファレンス。

GASで書いたコードです。
変数`search_keyword`には、[Using Standard Searchのドキュメント]に沿って、ツイートを検索したい文字列を設定します。
私は検索結果からリツイートを除外したかったので、`-rt`を指定しています。
また、検索件数を`count`で指定できるので、3件を指定しました。

```js
  // ②Twitter 検索APIの呼び出し 
  // GET https://api.twitter.com/1.1/search/tweets.json
  var search_keyword = 'xxxxx -rt';

  var bearer_auth_header = {
    'Authorization': 'Bearer ' + bearer_token
  };
  
  var search_response = UrlFetchApp.fetch(
    'https://api.twitter.com/1.1/search/tweets.json?q=' + search_keyword + '&lang=ja&result_type=recent&count=3',
    { 'headers': bearer_auth_header });
  result = JSON.parse(search_response);
```

### 3. Twitterの検索結果をSlackに通知
Twitterで検索結果の文字列を、Slackに通知します。
[こちら](https://qiita.com/nobu09/items/a098e2c2345fd17c4b71#3-gas%E3%81%A7%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E4%BD%9C%E6%88%90)の手順4を参考に、SlackのWebhook URLを呼び出すコードを書きます。

こんなコードになりました。
検索結果が3件なので、`forEach`で１件ずつSlackにPOSTしています。
投稿結果がわかりやすいように、ツイートの投稿日時（created_at)やユーザ名(user.name）、区切り線を入れてみました。

```js
  // ③Slackに通知  Incoming Webhook
  result.statuses.forEach(function(status) {    
    var data = { 
      'text': '-----------------------------------------\n' +
      status.text + 
      '\n----------------------------------------\n' +
      status.created_at + 
      '\nby ' + 
      status.user.name,
    };
  
    var options = {
      'method' : 'post',
      'contentType': 'application/json',
      'payload' : JSON.stringify(data)
    };

    UrlFetchApp.fetch(slackWebhookUrl, options);
```

これでGASを実行すると、このように検索結果がSlack通知されました。
（`GAS%20Google Apps Script -rt`で検索した結果です）

<img width="500" alt="12_result.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245454/ba2a77ed-8d5c-4ac5-73aa-81461f6a3fa3.png">


## 4. GASトリガー設定（定期的実行）
あとは、定期実行のために、GASでトリガーを設定します。
[こちら](https://qiita.com/nobu09/items/a098e2c2345fd17c4b71#5-gas%E3%81%AE%E3%83%88%E3%83%AA%E3%82%AC%E3%83%BC%E3%81%AE%E8%A8%AD%E5%AE%9A)の手順を参考に、GASのトリガーを設定します。
これで、定期的にTwitterを固定キーワードで検索して、その結果をSlackに投稿することができました！ :tada: 

最後に、全体のコードを載せておきます。

```js

var slackWebhookUrl = 'https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

var consumer_key = 'xxxxxxxxxxx';
var consumer_secret = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

var search_keyword = 'xxxxxxxx -rt';

function searchTweetsApps() { 
  // ①Twitter Bearerトークンの取得（検索APIの呼び出しに必要）
  // POST oauth2/token  https://developer.twitter.com/en/docs/basics/authentication/api-reference/token
  var blob = Utilities.newBlob(consumer_key + ':' + consumer_secret);
  var credential = Utilities.base64Encode(blob.getBytes());

  var formData = {
    'grant_type': 'client_credentials'
  };
  
  var basic_auth_header = {
    'Authorization': 'Basic ' + credential
  };
  
  var options = {
    'method': 'post',
    'contentType': 'application/x-www-form-urlencoded;charset=UTF-8',
    'headers':  basic_auth_header,
    'payload': formData,
  };
  
  var oauth2_response = UrlFetchApp.fetch('https://api.twitter.com/oauth2/token', options);  
  var bearer_token = JSON.parse(oauth2_response).access_token; 
  
  // ②Twitter 検索APIの呼び出し 
  // GET https://api.twitter.com/1.1/search/tweets.json
  var bearer_auth_header = {
    'Authorization': 'Bearer ' + bearer_token
  };
  
  var search_response = UrlFetchApp.fetch(
    'https://api.twitter.com/1.1/search/tweets.json?q=' + search_keyword + '&lang=ja&result_type=recent&count=3',
    { 'headers': bearer_auth_header });
  result = JSON.parse(search_response);
  
  // ③Slackに通知  Incoming Webhook
  result.statuses.forEach(function(status) {    
    var data = { 
      'text': '-----------------------------------------\n' +
      status.text + 
      '\n----------------------------------------\n' +
      status.created_at + 
      '\nby ' + 
      status.user.name,
    };
  
    var options = {
      'method' : 'post',
      'contentType': 'application/json',
      'payload' : JSON.stringify(data)
    };

    UrlFetchApp.fetch(slackWebhookUrl, options);    
  });
}

```

# やってみた感想
TwitterのAPIを触るのがはじめてだったので、Developer Accountの申請、APIの認証方式などについて調べるのに手間取りましたが、Twitterからの定期的な情報収集という目的が果たせたのはよかったです。
Slackへの投稿も装飾したりできそうなので、広げてやってみたらおもしろいかもしれないと思っています。