A note of a person who is learning programming, SakaTaQ

ロック好きのプログラミング学習

Twitter × GASで検索したキーワードをslackに定期投稿する

前回の記事で開発者としてのアカウント申請を終えましたので、今回は実装したTwitter botについて再び手順を残していこうと思います。
...色々あって当初予定していたものとは違うものを実装することにしました。

アプリケーションの作成

ヘッダーバーに公式ドキュメントがあります。
まずはTwitter Developerの自分のアカウント名のプルダウンメニューから「Apps」を選択。
f:id:saka20taku43:20200626115657p:plain
何も作ってないのでこの画面が出ます。「Create an app」でアプリケーションの作成を行います。
f:id:saka20taku43:20200626144745p:plain
作成するアプリケーションの詳細を入力していきます。
どこかの記事で日本語だと申請が通りにくい、みたいな感じの書き込みをみた気がしたので、念のため英語で入力。

  • App name(必須、最大32文字):アプリの名前
  • Application description(必須、10〜200文字):アプリの説明
    • ユーザーに表示されるアプリの説明文。どういった用途の物なのかを書いておくと良い。

実際に書いた文章 => It is an application that regularly posts the results of searching with fixed keywords on Twitter to Slack.
(前回記事同様、自分の言葉が望ましいようです。日本語で書いて英語に翻訳)
f:id:saka20taku43:20200626162623p:plain

  • Website URL(必須):ウェブサイトのURL
    • [原文訳]ウェブサイトのURLは、アプリによって作成されたツイートのアトリビューションのソースとして使用され、ユーザー向けの承認画面に表示されます。
      • ブログの新着記事投稿の自動ツイートであればブログのURLでいいんでしょうが、今回は参考サイトを例に作成する予定のGASスクリプトURL(現状は空のGAS)を新規作成し、そのURLを記載。
  • Allow this application to be used to sign in with Twitter:このアプリケーションを使用してTwitterでサインインすることを許可するか(チェックボックス)
    • 意味不明だったので詳細リンクに助けを求める←
      • オンにするとTwitterのサインインボタンが設置できるとあるので、認証を楽にするための何かっぽいのですが、それくらいしか分からん。後、今回のアプリは自動で検索したキーワードをSlackに投稿するものなのでその度にサインインするの?ってなわけで無視。
  • Callback URLs:コールバックした時に表示するURL
    • これも言葉だけではサッパリ意味不明でした。が、Webアプリでたまに見かける○○を使ってサインイン(or ログイン)の時に変遷したページから戻ってくる時のURLを入力する、ってくらいは分かりました。今回はログインもクソもないので無視。

  • Terms of Service URL:利用規約のURL
  • Privacy policy URL:プライバシーポリシーのURL
  • Organization nam:組織名
  • Organization website URL:組織のウェブサイトURL

この4項目は特にないので無視。必須でもない。

f:id:saka20taku43:20200626165422p:plain

  • Tell us how this app will be used(必須、100文字以上):アプリの使用方法
    • この部分はTwitter者の従業員のみが見る部分らしいです。このアプリの使用方法と、これを使用することによる自分と自分の顧客が何をすることを可能にするのか?とあります。
      アプリの要件さえ決まっていれば何となくかけます。勿論、翻訳は使いまくりですが💦

一通り必須項目を入力したら、Createを押す。 f:id:saka20taku43:20200626171710p:plain とりあえず注意事項っぽいので再び翻訳。 f:id:saka20taku43:20200626171739p:plain

同じ名前のアプリケーションは作れない、URLが長い、とあり失敗したので記入し直す。
で、アプリ名はすぐに通るものにできたけど、URLは流石に代用できないので困った...。 websiteの指定先としての定義みたいなものを色々探してみたところこちらの記事を参考にさせていただました。
とりあえず自分のTwitterアカウントのURLを入力。

作成完了したら詳細一覧が表示されます。
ここの「keys and tokens」でアプリケーションで使用するConsumer API keysが生成され、Access tokenの発行ができるようです。
また、PermissionsタブのAccess permissions はツイートの読み込みだけの場合は Read onlyになってるか確認する必要があるようです。
自動ツイート(書き込み)も行う場合は「Read and write」するみたいですね。

この時点でTwitter APIを呼び出す準備は整っているようです。
Googleアカウント(GAS)とSlackアカウントは既に準備済みなので省略します。
これ以降の手順として

  • Slackのwebhookの口(Incoming webhook)を用意
  • GASからそのwebhookを呼び出すスクリプトを書く
  • それを定期実行させるトリガーをGASに設定させる

のようにしていくようです、順番にやっていきます。

Slack側の設定

検索した結果をメッセージとして送信した時の投稿先のチャンネル開設。
Slack での Incoming Webhook の利用を参考に設定をしていく。
f:id:saka20taku43:20200627143537p:plain
メッセージを投稿するワークスペースでAppを選択 f:id:saka20taku43:20200627150939p:plain
検索して出てきた項目を選択し、
f:id:saka20taku43:20200627151119p:plain
一応、ヘッダー右上のワークスペース名を確認。
投稿するチャンネルをセレクトボックスから選択。
選択後は設定画面に飛ぶ。ページ上部の有効/無効で有効になっているか確認。下の方にはJSONデータでテキストを投稿する際の送信先Webhook URLなどが記載されている。
このアプリ設定ページへのアクセスはヘッダーバー「管理」の「カスタムインテグレーション」にある。

後は、ワークスペースを一度再読み込みしてみて、追加したチャンネルに連携した旨の投稿がされていればOK。

GASスクリプトファイルの準備

Googleドライブから開くやつかその他で直接用意する。
Slack API 公式ドキュメントを参考に、SlackのWebhook URLを呼び出すコードを書いていく

var slackWebhookUrl = 'https://hooks.slack.com/services/XXXXXXXXXXX/YYYYYZZZZZAAAAABBBBBCCCCCDDDDDEEEEEE'function firstApp() {
  var data = {
    'text': 'Hello, this message is from my GAS App',
  };
  
  var options = {
    'method': 'post', 
    'contentType': 'application/json', 
    'payload': JSON.stringify(data)
  };
  
  UrlFetchApp.fetch(slackWebhookUrl, options);
}

'payload'はslack appの設定ページに出てきましたが、webhook URLにJSON payloadでテキストを送信する、とあるのでこの形式になるようです。
LINE APIの時もそうでしたが、GASからwebhook URLを呼び出すにはUrlFetchAppclassが持っているfetchを使い、引数には投稿先であるwebhook URLとHTTPヘッダの内容やテキストなどをJSON形式に変換したオブジェクトにして渡す。

記述が完了したら、GASスクリプトを実行して投稿できているかを確認する。
実行したい関数を選択肢、再生マークのボタンで実行。
f:id:saka20taku43:20200627161600p:plain
※ 初めて送信する場合は、前回と同様に送信することをGoogleアカウントが許可するか別窓で出てくる。詳細からプライベートページに行くように促すと、今回はブラウザではないので外部サービスへ直接送信するか許可するか聞かれる。のでこれを承認する。

ランタイムV8が悪さしてエラー吐くかもしれないと見かけたので、無効にして送信→成功。
続けて有効にして送信→成功。
f:id:saka20taku43:20200627161719p:plain
送信自体は問題なくできるようです。

トリガーの設定

一定時間ごとに動かす設定をコードで書く必要はないみたい。
先ほどのGASのツールバーの再生ボタンの左にある時計みたいなアイコンをクリック。
f:id:saka20taku43:20200627162118p:plain
トリガーを追加をクリックすると、モーダル状の設定画面が出てくる。
f:id:saka20taku43:20200627162415p:plain
イベントのソースを選択を押すと「時間主導型」「カレンダーから」など出てくるが、ここで時間主導を選択。
特定の時間や日付ベースを選んだりすることで、そのカテゴリーによって何日とか何時とか何時間おきに、とか選べる。
今回はお昼頃に検索をかけて定期的に投稿をして欲しいので f:id:saka20taku43:20200627162725p:plain
のように選択肢、保存。
実際に来るか確認したいなら1時間おきなどにしておいて確認しておいたほうがいいかもしれない。
f:id:saka20taku43:20200628132643p:plain
キチンと日付を跨いで投稿されているのが確認できました。

ひとまずGASとSlackで連動させる作業は一旦終了。

TwitterとSlackを連携させる

Slack側のドキュメントです。
まずSlackのワークスペースのサイドバー上部の「App」を押して出てくる画面の右上の「Appディレクトリ」をクリック。
立ち上がった別窓の検索欄にそのまま「Twitter」と入れて検索
f:id:saka20taku43:20200628144514p:plain
Slackに追加→Twitterインテグレーションの追加→連携アプリを認証(Authorize app)をクリック
今回はTwitter上でGASを利用して自動検索されたツイートをSlackに投稿するので、ツイート追跡は特に書きません。投稿するチャンネルは既に作ってあるのでそれを指定。
インテグレーションが発言をした際の明示として、「自動キーワード検索」のような形で名前をつけておきます。最後に設定を保存(Save Setting)。

Twitter Bearerトークン取得

特定のアカウントのデータにアクセスする場合(ユーザー情報読み込み、ユーザーとしての書き込み)にはアクセストークン認証が必要。
公開されている情報にアプリケーションとしてのアクセスを行う場合にはアプリケーション認証であるBearerトークンが必要とのこと。
(Bearerトークン = ベアラートークン = 無記名トークン)
今回の要件では個人としての、ではなくてアプリケーション上で公開されている情報にアクセスするためBearerトークンを用いる。
認証処理の流れはドキュメントの図が利用する構文もついているので見やすいです。

とりあえず公式のApplication-only authentication and OAuth 2.0 Bearer Tokenを見ます。

  • Bearerトークンを使用するとユーザーコンテキストなしでアプリケーションに代わってAPIリクエストを行うことができる、これはアプリケーションのみの認証と呼ばれる。
  • Bearerトークンは、oauth2 / invalidate_tokenを使用して無効化できます。Bearerトークンが無効になると、新しい作成の試行で別のBearerトークンが生成され、以前のトークンの使用は許可されなくなります。
  • アプリケーションに対して未処理で存在することができるBearerトークンは1つだけであり、このメソッドへのリクエストが繰り返されると、無効になるまで同じ既存のトークンが生成されます。
  • 成功した応答には、授与されたBearerトークンを説明するJSON構造が含まれます。
  • このメソッドによって受信されたトークンはキャッシュされるべきです。試行回数が多すぎる場合、リクエストはコード99のHTTP 403で拒否されます。

    公式ドキュメントより翻訳して引用

1回きりの使い捨てできるトークンが使えるみたいなものかな。
Accessトークンは免許証、Bearerトークンは映画のチケットや駐車券みたいなイメージらしいっす。

Twitterリファレンス、GASドキュメントを参考にTwitterのBearerトークンを取得するプログラムを書く。

// Twitter Developer でアプリを作成した時に生成されたcomsumer-keyとsecret_keyのキーペアを入力(機密です、注意)
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; 

// Todo:Twitter 検索APIの呼び出し

// Todo:Slackに通知  Incoming Webhook

}

STEP1:まず、コンシューマとシークレットキーを用意し、その2つのキーをURLエンコードする必要があるそうです。
で、連結する際に:で繋げるとあります。Utilities.newblob()構文とUtilities.base64Encode('blob obj' + getBytes())GASでbase64にエンコードするを参考に作る。

STEP2:STEP1で用意したエンコードされた文字列は、POST oauth2/tokenにリクエスト発行することにより、Bearerトークンと交換する必要がある...専門用語だけでは分かりませんが、何をすべきかは書いてあります。

  • リクエストはHTTP POSTであること。
  • リクエストはSTEP1で用意した{Basic エンコードの値}を含むAuthorization:ヘッダーを含める。(Basicの後に半角のスペースが必要なので注意)
  • リクエストはapplication/x-www-form-urlencoded;charset=UTF-8の値を持つcontentTypeヘッダーを含める。
  • リクエストの本文'payload'はgrant_type=client_credentialsであること。

今回のコードではoptionsの内容はそれぞれ上記の項目を一つのオブジェクトとしてまとめたもの。

GASからURLを呼ぶ時の書き方は上記の通り。今回のリソース先はAPIリファレンス POST oauth/tokenを参考にhttps://api.twitter.com/oauth2/tokenと記入し、ヘッダーはoptionsで用意したJSON形式のものを引数に渡している。
次のJSON.parse()では文字列をJSONとして解析し、オブジェクトを構築する構文。引数にはリソース先のURLにFetchした際のレスポンスを渡している。

HTTP/1.1 200 OK
Status: 200 OK
Content-Type: application/json; charset=utf-8
...
Content-Encoding: gzip
Content-Length: 140

{"token_type":"bearer","access_token":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2FAAAAAAAAAAAAAAAAAAAA%3DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}

アプリケーションは、返されたオブジェクトのtoken_typeキーに関連付けられた値が無記名であることを確認する必要があります。 access_tokenキーに関連付けられている値は、Bearerトークンです。

公式リファレンスより引用

このレスポンスの中から.access_tokenと書くことで、最初に連結してエンコードされたBearerトークンを取得できるようです。accessトークンを渡しているならそのままaccessトークンを取得できる...と解釈。
デバッグとして関数最下にLogger.log(bearer_token);と記述し、関数を実行。ツールバーの「表示(View)」→「ログ(Logs)」で確認できる。
f:id:saka20taku43:20200628190102p:plain

Search Tweets(Twitter検索API)

Search Tweetsにはプランが3種類あって無料使用できるのはStandard search APIのみ。
f:id:saka20taku43:20200628190824p:plain
翻訳。 f:id:saka20taku43:20200628190707p:plain

無料版は過去7日文迄しか遡れないようですが...まぁ十分。

参照するページがあっちこっち行って分かりにくいですが、Application-only authentication and OAuth 2.0 Bearer TokenのSTEP 3(ようは続き)を見ていくと、
「Bearer Tokenを使用して、アプリケーションのみの認証をサポートするAPIエンドポイントにリクエストを発行できます。 Bearer Tokenを使用するには、通常のHTTPSリクエストを作成し、AuthorizationヘッダーにBearer <手順2のbase64 bearer token value>の値を含めます。署名は必要ありません。」(引用を翻訳し、抜粋)
とあるので、'Authorization': 'Bearer ' + bearer_tokenの形でヘッダーに渡してあげてリクエストをする。

  // 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=10',
    { 'headers': bearer_auth_header });
  result = JSON.parse(search_response);

このコードでは検索URLの準備と、それを利用してのリクエストを行っている。 URLではエンコードされた文字を使用する必要があるようです。

search_keywordに入れてある文字列は検索用語で、Using the standard search endpoint公式ドキュメントのテーブルの辺りに書き方が書いてありますね。
タグを検索したい場合、「#」は検索演算子として使用する場合は「%23」のように書く、また複数検索する場合の区切りとして使用するキーワード間のスペースは「%20」か「+」で書くようです。
-rtリツイートを検索対象から外すことができるようです。

最後には検索APIのリクエストをしている。URLはドキュメントのBest Practices にいくつか例が載っているのでそちらを参考にする。

  • #superbowl で検索APIを使用する際のURL
    https://api.twitter.com/1.1/search/tweets.json?q=%23superbowl&result_type=recent
  • さらに、特定の言語(今回は日本語)のみを検索結果としてリクエストする場合
    https://api.twitter.com/1.1/search/tweets.json?q=%23superbowl&lang=ja&result_type=recent

これらlangresult_typeなどの使用できるパラメータ(公式リファレンス)について
recentは最新の投稿の取得。countはデフォルトでは15件、最大100件。今回は毎日定刻に検索を行うので10件を指定。

Twitterの検索結果をSlackに投稿する

Twitter検索を行った場合の結果の取得についてはExample Response(公式リファレンス)

statusesにオブジェクトが入っているのでkeyを使って取り出すことができる。
今回の検索結果はresultに代入されているのでそちらからstatusesで取り出してあげればいいようです。

また、GASからSlackに投稿するコードは前述したコードが利用できそうです。

  // Slackに通知  Incoming Webhook
  result.statuses.forEach(function(status) {    
    var data = { 
      'text': '-----------------------------------------\n' +
      status.text + 
      '\n----------------------------------------\n' +
      status.created_at + 
      '\n ' + 
    };

    var options = {
      'method' : 'post',
      'contentType': 'application/json',
      'payload' : JSON.stringify(data)
    };

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

dataの部分は投稿される本文になりますが、今回はstatusesには複数データが入っているので、forEach文を使用して1件ずつ繰り返して投稿するような流れになっています。

最後にトリガーで定期実行するように設定。前回の関数は今後使用しないため、こちらをそのまま書き換えていく。

検索キーワード 'rails+%23Google Apps Script -rt'
実行してみたが反応せず。Logger.logでデバッグしたらnullだったので取得すら失敗してるみたい。
検索URLにエンコード以外でスペース入れてるのが問題なのかもと思い、'%23GoogleAppsScript -rt'で検索 (本当はrailsでもやってみたのですが災害情報ばっかり引っかかるので変えました(笑)) f:id:saka20taku43:20200629151330p:plain
リツイートが表示されている...とりあえず公式リファレンスを参考に記事、実装コード共に修正。
後、意外と一つの単語に対して調べる場合、大分前まで遡ってるので件数は3件くらいでもいいのかも?
もしくは検索用語増やすか。

  • var search_responsecount=3に修正
  • var search_keyword'%23GoogleAppsScript -filter:retweets'に修正
  • 細かいが区切り線を変数postSplitなどの名前として用意 f:id:saka20taku43:20200629152658p:plain

何か格言みたいなのばっかりになったな...


参考にさせていただいた記事
公式ドキュメント/Twitter APIリファレンス一覧
GASを使って、Twitter検索した結果をSlackへ定期投稿してみた
Google Apps ScriptからSlackへ定期投稿してみた
Slack API 公式ドキュメント
Twitter REST APIの使い方
Twitterの検索結果のAPIが返すオブジェクトの公式リファレンス


先人にただただ感謝!! APIの利用は構文の組み立て方とか、どこのドキュメント見ればいいかとか、正直難しかったけども普段使いしているアプリと直結させたりできるので使い方次第ではとても便利。
LINE API共々慣れていければいいな...

したらな❗️ 👋