📨

【GAS】メッセージ受取/転送(投稿)/リアクションするSlack Appの作り方

2021/03/20に公開

https://zenn.dev/mokomoka/articles/6d281d27aa344e
前回の記事の続きのようなものになります。

前回の記事では、「1.4 Botの開発」「2.2 Event API使用のための準備」「2.4 Appの開発」の箇所で、具体的な実装については記載していなかったため、こちらの記事ではGoogle Apps Script(GAS)による実装にフォーカスして書いていきたいと思います。
(ちなみに、私が実装するときには前の記事の順番ではなく、2.2→1.4と2.4並行といった感じで実装したので、この記事でも実装を考えた順に説明します。)

どんなApp(Bot)を作るの?

特定のワードが入ったメッセージが投稿されたら、そのメッセージにリアクションを付けた後、そのメッセージのリンクを特定のチャンネルに投稿する(以降「転送」と呼ぶ)」Botを作ります。おまけ機能として、転送したメッセージの内容などをスプレッドシートに記録します。

このBotの用途として、「iOSに関する質問です」というワードを含むメッセージが投稿されたら#question-iosチャンネルに、「Androidに関する質問です」というワードを含むメッセージが投稿されたら#question-androidチャンネルに、「hoge(他の条件に当てはまらない文字列)に関する質問です」というワードを含むメッセージが投稿されたら#question-othersチャンネルにそれぞれのメッセージへのリンクを投稿するといった運用をしました。リアクションは転送済みの証、スプレッドシートの記録は後ほど使うかもしれないので念の為Slackから消えても残るようにしたかった…ような扱いです。

単純に言えば、「ユーザからのメッセージ投稿を受け取り、そのメッセージにリアクションを付ける&そのメッセージ内容に応じてBotもメッセージ投稿をする&スプレッドシートにメッセージの情報を記録する」というものですので、必要な部分だけ抜き出せば他の使用例にも応用しやすいかと思います。

コード全体は以下のGitHub Repositoryで公開しています。
基本的にはそちらのコードを参照し、この記事ではそのコードに至った考え方や補足などを確認していただければと思います。(この記事の文章中には、あまりコードが載っていません…)
https://github.com/mokomoka/SlackBots/blob/master/sources/ForwardBot.gs

ということで、以下の流れで作っていきます。

  1. Events APIやBotの情報を設定する(前の記事の2.2の内容を含む)
  2. 転送部分を作る
  3. リアクション部分を作る
  4. 履歴記録部分を作る
  5. メッセージ投稿を受け取る部分などを作る
    ※今回は、スプレッドシートを作成して「スクリプト エディタ」からGASのコードを書く前提で進めます

1. Events APIやBotの情報を設定する

1.1 スクリプトのプロパティ設定

Bot User OAuth Access TokenやAppのVerification Tokenは、コード内に載せるとオンラインで公開/管理する際によろしくないです。
GASの場合は、そういった値を「スクリプトのプロパティ」に設定します。
以前はUI上から設定可能だったのですが、私がこのBotを開発している時期くらいにGASのWeb UIが変わり、どうやらUI上で設定できなくなったようです。そのため、PropertiesService.getScriptProperties().setPropertyPropertiesService.getScriptProperties().setPropertiesなどでコード上で設定する方法しか現状見つかっていません。
プロパティ設定用の関数を作り、公開するときにはその部分を除外するのが良いでしょうか…?(このあたりの知見はあまりないので、良い方法を知っている方がいれば教えて下さい!)

とりあえず今回は以下のような感じで、Bot User OAuth Access Token, Verification Token, そしてワークスペースのURLの一部(xxx.slack.comのxxx部分)を設定しました。

function setProperties() {
  PropertiesService.getScriptProperties().setProperty("bot_token", "xoxb-xxx...");
  PropertiesService.getScriptProperties().setProperty("verification_token", "xxx...");
  PropertiesService.getScriptProperties().setProperty("workspace", "xxx...");
}

もちろん、Propertiesのほうを使ってまとめて設定しても良いですね。その場合は公式ドキュメントを参照すればできると思います。

そしてメインとなる関数であるdoPost(e)の序盤でsetProperties()を呼び出せば設定できます。

ちなみに、旧UIに戻して設定する場合は、ボタンポチポチからの入力で設定できます。

プロジェクトのプロパティ選択画面
スクリプトのプロパティ入力画面

1.2 Events API用の記述

「特定のワードが入ったメッセージが投稿されたら~」の部分の下地として、doPost(e)内でEvents APIを使うための記述をします。

  // Events APIからのPOSTを取得
  // 参考→https://api.slack.com/events-api
  const json = JSON.parse(e.postData.getDataAsString());
  
  // Events APIからのPOSTであることを確認
  if (prop.getProperty("verification_token") != json.token) {
    throw new Error("invalid token.");
  }
  
  // Events APIを使用する初回、サーバの存在確認?みたいなのがあるので、そのための記述
  if (json.type == "url_verification") {
    return ContentService.createTextOutput(json.challenge);
  } 

ほぼ前回記事の2.2と同じですが…
このような感じで、POSTを取得しパースしたあと、それがEvents APIからのPOSTであることを確認し、その後初回のurl_verificationに対処するための記述をしています。

2. 転送部分を作る

コード中の forwardMessage 関数にあたる部分です。

2.1 基本的なメッセージ投稿部分の記述について

コード中にコメントで記載している通り、メッセージを送るときはhttps://api.slack.com/methods/chat.postMessage を参照して必要な情報を用意しましょう。

上記サイトで必須(Required)と記載されているのはtokenchannelの2つのみですが、現実的にはtextも必要でしょう。
今回のコードでは、payload変数にtoken channel textを設定しています。
今回は、Events APIで取得したメッセージのリンクをlinkという定数に代入済みとして、

let payload = {
  "token" : token,
  "text" : link
};

こんな感じでpayloadの初期化時に渡しています。
channelはその後のfor文内if文の条件によってどのチャンネルに送るか決めた時にpayloadに渡しています。
(これを書いてから、「channelの値は適当な定数に代入しておいて、for文終わった後にpayloadの初期化自体をすれば、payloadも定数で良かったな…というかparamsに直接代入も可能だったな…」と気づきました。)

そして上記の情報をAPIエンドポイントにPOSTします。
GASのUrlFetchAppクラスのfetchメソッドを使います。(公式リファレンス

const params = {
  "method" : "post",
  "payload" : payload
};
const r = UrlFetchApp.fetch(forward_url, params);

こんな感じでpostと先程のpayloadparamsに代入し、UrlFetchApp.fetchの引数に渡せばOKです。
レスポンスは後ほど、このPOSTが成功したかの判定に使うことを見越して、rに代入してあります。

2.2 この用途独自の部分について

基本的にメッセージ投稿は2.1の内容でできますが、今回の用途のために記述した部分も念の為いくつか説明します。

今回のBotでは「特定のワード」に応じて「特定のチャンネル」にメッセージを投稿するため、そのワードとチャンネルの対応をkey/valueとしてchannelsに代入しています。(他に良い方法が思いつかなかった…)
そして、for文内で、受け取ったメッセージにchannelsのkeyのいずれかが含まれているかを判定し、含まれていれば転送するチャンネルのIDをpayloadに代入するようにしています。

channelsのvalueに使うチャンネルのIDは、以下のように、チャンネル名を右クリックから「リンクをコピー」で取得できます。
チャンネルIDの取得

また、今回Botで投稿するメッセージの内容は、ユーザが投稿したメッセージへのリンクです。
メッセージのリンクは
ワークスペースのURL + /archives/ + 投稿されたメッセージのチャンネルID + /p + メッセージのタイムスタンプ
といった文字列で構成されています。
ワークスペースとチャンネルとタイムスタンプでメッセージを一意に特定できるということですね!
ここで必要となるチャンネルIDとメッセージのタイムスタンプは、Events API経由で取得したユーザが投稿したメッセージの情報に含まれています。
タイムスタンプは.が含まれる形式で取得されるものの、URL上では.が使用されないため、今回のコードではreplace(".","")を用いて.を取り除いています。

3. リアクション部分を作る

コード中の addReaction 関数にあたる部分です。
コード中にコメントで記載している通り、メッセージに絵文字リアクションをつける場合はhttps://api.slack.com/methods/reactions.add を参照して必要な情報を用意します。

必須項目はtoken channel name timestampです。
nameは絵文字の名前です。(日本語が使えるかは未確認ですが、ユーザが登録した絵文字が日本語名しか付いていない場合もあると思うので、対応しているかな…?)
channeltimestampは、リアクションを付ける対象となるメッセージが投稿されたチャンネルとタイムスタンプを表しています。ちょうど2.2で説明したメッセージのリンクを構成するために必要な文字列と同じで、リアクション対象となるメッセージを一意に特定するための情報です。

それらの必須項目が用意できたら、あとはほぼメッセージ投稿(転送)と同じようにUrlFetchApp.fetchでPOSTすればOKです。

4. 履歴記録部分を作る

コード中の recordHistory 関数にあたる部分です。
この部分は3までと異なり、スプレッドシートの扱いについての説明になります。

まず、記録先のスプレッドシートを取得します。
let sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
次に、下に向かって追記していきたいため、スプレッドシートの最終行を取得します。
let lastrow = sheet.getLastRow();
(コード内では上記の2行をdoPost(e)内で実行していましたが、recordHistory内でやっても良かったかなと思います。)
最後に、最終行の次の行、書き込みたい列を指定してその箇所に書き込みたい文字列をセットします。(下記の例では最終行の次の行の3列目にtextを書き込む)
sheet.getRange(lastrow+1,3).setValue(text);

以上です。簡単ですね!

5. メッセージ投稿を受け取る部分などを作る

最後の仕上げとして、メインであるdoPost(e)に戻り、メッセージ投稿受け取り→各種関数呼び出しを実装します。

メッセージ投稿の受け取りは、1.2で用意したEvents APIを使用します。
コード中にコメントで記載している通り、https://api.slack.com/events/message.channelsを参照すると、どのような形式で情報が得られるかわかります。
今回は、(認証のためにtokenが必要なのは当然として、)event内のtype channel text tsが他の関数のために必要となります。
const json = JSON.parse(e.postData.getDataAsString()); でJSONをパースしたのち、json.event.typeなどの指定で必要な情報を取り出せます。

そしてメッセージの転送をするために、2で作成したforwardMessageを呼び、その結果(response)が正常だった場合はaddReaction recordHistoryを呼びます。

以上です。
今回の内容ができれば、他にも簡単な応答をしてくれるBotなら作れると思います!
実際に私は、「褒めてとメッセージ投稿したら、リアクションをしてからランダムに褒めるメッセージを投稿してくれるBot」も同様の形式で実装して、研究室のワークスペースで稼働させています。(褒めbot ver.2)

前回の記事と同様、質問や困ったことがあればお気軽にどうぞ!

Discussion