【GAS】SlackとTrelloを連携するBotを導入した話(メンション機能を有効化する)
「ふせん」のような感覚で使えるタスク管理ツール「Trello(トレロ)」。視覚的に見やすく、様々なプラグインも豊富で、非常に使いやすい。
私の会社でも、タスク管理にTrelloを用いている。また、ビジネスコラボレーションハブの「Slack」をメインツールとしている。SlackとTrelloを連携するには、Trelloの公式アプリを用いて簡単に実現することができる。
しかしながら、ここで問題が発覚した。
slackとTrelloのユーザー名が異なり、メンションされない。。。
Slackに通知する場合、@メンションがされていないと、情報が流れてしまい、見逃してしまうという問題が発生する。
また、公式アプリは(通知するだけなのでこの機能で十分ではあるが、)英語表記で何か味気ないw(この部分に関して、こだわるべきか否かは賛否両論あると思うが、個人的にはこだわりたいタイプであるw)
そこで、今回Google Apps Script(GAS)を用いてSlackに通知するBotを作成することとした。(今回は、公式アプリのように、Slackからカードの操作などを想定しておらず、通知のみで使うことを想定している。)なぜGASを選択したかというと、何と言っても無料で運用できることに尽きる。。。
システム概要
今回の構成は以下の通りである。
TrelloのWebhookで様々な更新情報を取得することができるが、実際には以下の項目のみをSlackに通知することとした。
- メンション付きのコメントがあった場合のみ、コメントを通知する
- カードの担当が割り振られた場合に、割り当てられたことを通知する
- Doingに移動した場合に、着手を通知する
- Reviewで割り当てられた人に、レビュー依頼を通知する
- Doneに移動した場合に、完了を通知する
実装に関しては、以下の手順で行った。
- SlackのIncoming Webhookを作成する
- GASのウェブアプリケーションの公開URLの準備(TrelloのCallback URLに設定するために必要)
- TrelloのWebhookの登録
- GASにSlackに通知するためのコードを実装
SlackのIncoming Webhookを作成する
Incoming Webhookは、生成されたIncoming Webhook URL に適切なpayloadをPOSTするだけで、任意のChannelにメッセージを送信することができる、とても便利なツールである。
作成手順は以下の通りである。(公式ドキュメントを参照してください。)
GASのウェブアプリケーションの公開URLの準備
続いて、TrelloのWebhookを設定する前に、TrelloのCallback URLを生成するために、先にGASの準備から行う。
今回は、ユーザーの変更があった際に、コードを変更するのではなく、スプレッドシートのユーザー一覧を変更することで、システムに反映させることができる設計とした。
- Googleにログインして、新規スプレッドシートを作成する
- ツール > スクリプトエディタ から、GASを起動する
- GAS起動時は、「無題のプロジェクト」という名前になっているので、適切な名前をつける(今回のプロジェクトは、「Slack&Trello連携Bot」とする。)
- 公開 > ウェブアプリケーションとして導入...を選択する
以下の設定として、「導入」を選択する
項目 設定 プロジェクトバージョン New 次のユーザーとしてアプリケーションを実行 自分 アプリケーションにアクセスできるユーザー 全員 現在のウェブアプリケーションのURLが表示されるので、コピーしておく
(※注意)
アプリケーションにアクセスできるユーザーを必ず「全員」とする。そうでないと、次のTrelloのWebhook登録時に403が返ってきて登録することができない。
下記を参考にした。本当に感謝。(このエラーにはかなり時間食ったw) Trello Webhook × GoogleAppsScript の連携してカンバンの分析を楽にして見る - Qiita
TrelloのWebHookの登録
Trello APIを用いて行う。
TrelloのAPIを使うために、まずAPIキーとTokenを取得する
Trelloにログインしている状態で、以下にアクセスするとAPIキーが表示される。
トークンは、同ページ内の
トークン
リンクから、 許可 を選択することで表示される。監視したいボードIDを取得する ボード一覧取得APIを用いてIDを取得する
curl --request GET \ --url 'https://api.trello.com/1/members/{id}/boards?key={API_key}&token={API_token}' \ --header 'Accept: application/json'
各パラメータは以下の通りである。
- id:Trelloのユーザー名(プロフィール画面で確認できる。または@メンション時に表示される名前)
- API_key:(1)で取得したAPI Key
- API_token:(1)で取得したAPI Token
返却された一覧の中から、監視したいボードのIDを控えておく
Webhookを登録する Webhook登録 for Token APIを用いて登録する
curl --request POST \ --url 'https://api.trello.com/1/tokens/{API_token}/webhooks?key={API_key}&callbackURL={GAS_URL}&idModel={boardId}?description={desc}' \ --header 'Accept: application/json'
各パラメータは以下の通りである。
- API_token:(1)で取得したAPI Token
- API_key:(1)で取得したAPI Key
- GAS_URL:GASで生成したURL
- boardId:(2)で取得した監視したいボードID
- desc:(任意)説明文を追加しておくことをオススメする。例:Webhook-For-GAS
レスポンスが200で返却されれば、登録完了である。
これで、GASとTrelloの連携が完了し、Trelloに変更があった場合は、GASのURLに変更情報がPOSTされる。
GASにSlackに通知するためのコードを実装
GASは以前までES6の書き方などができずに、少しコードを書くのが面倒であったが、Chrome V8が搭載されてES6で記載できるようになった。
GASの基本的な構成は以下である。
function doPost(e) { //TrelloでPOSTされた情報を受け取る const contents = JSON.parse(e.postData.contents); // bodyの中身 <--- Slackに通知すべきか判別 ---> <--- SlackにPOSTするメッセージを作成 ---> UrlFetchApp.fetch(postUrl, options) // SlackにメッセージをPOSTする }
また今回、スプレッドシートをDBとして利用し、ユーザー情報、SlackのWebhook URLやTrelloのAPI Key/Tokenを参照する。参考までに、私のスプレッドシートを共有する。(コピーを自分のドライブに作成し、手順の1から実行することで、同様の通知が可能となるので、お試しください。ただし、ボードの名前などが違う場合があるので、条件設定は再度お確かめください。)
SlackのユーザーID(メンバーID)は以下の方法で確認できる。
個別のユーザーのユーザー ID は、メンバーのプロフィールの (その他) アイコン をクリックし、 「メンバー ID をコピー」を選択して確認することができる。
また、Trelloのactionに関しては、以下のリンク先に記載されているように、かなり細かく分類されているため、通知が必要な部分のみを選定して適切なメッセージで通知できるようにするのがベストな運用と思われる。
(※注意※) 最後に、コードが完成して変更を反映させるには、以下を再度必ず実行する必要がある。(通常のコードでいう、デプロイ作業的なもの)
- 公開 > ウェブアプリケーションとして導入...を選択する
以下の設定として、「更新」を選択する
項目 設定 プロジェクトバージョン New 次のユーザーとしてアプリケーションを実行 自分 アプリケーションにアクセスできるユーザー 全員(匿名ユーザーを含む)
以降は、「全員(匿名ユーザーを含む) 」で運用する必要があるので、変更を忘れずに。 公開用URLが変更になるわけではないので、ご安心ください。
参考までに、GASのコードも掲載しておく。(コードの最適化がされていない部分はご了承ください。)
function doPost(e) { const contents = JSON.parse(e.postData.contents); // ユーザーデータ読み込み const users = setUserData(); // データ成形 const actionType = contents.action.type; const cardName = contents.action.data.card.name; const shortLink = 'https://trello.com/c/' + contents.action.data.card.shortLink; const operatorId = contents.action.memberCreator.username; const operator = users.filter(user => user.trelloId === operatorId)[0] let message = ""; // カードにコメントが追加された場合 if (actionType === 'commentCard') { const comment = contents.action.data.text; const userToMention = users.reduce((acc, user) => { let trelloId = '@' + user.trelloId; if (comment.includes(trelloId)) { acc += '<@' + user.slackId + '> '; } return acc }, "") if (!!userToMention) { message += userToMention + '\n'; message += ' *<' + shortLink + '|' + cardName + '>* のコメント確認お願いします。 by' + operator.firstName; const blocks = [ { type: "section", text: { type: "mrkdwn", text: message } }, { type: 'divider' },{ type: "section", text: { type: "mrkdwn", text: comment } } ] postSlack(message, blocks); } // カードが移動された場合 } else if (actionType === 'updateCard') { const toListName = contents.action.data.listAfter.name if (toListName === 'DOING') { message = '<!here>' + '\n *<' + shortLink + '|' + cardName + '>* のタスクに着手。 by ' + operator.firstName + '\n'; message += '期限厳守。報連相の徹底で。'; } else if (toListName === 'DONE') { message = '<!here>' + '\n *<' + shortLink + '|' + cardName + '>* のタスク完了。 by ' + operator.firstName + '\n'; message += 'お疲れ様でした。'; } postSlack(message); // カードに担当が割り振られた場合 } else if (actionType === 'addMemberToCard') { const addedUserId = contents.action.member.username; const addedUser = users.filter(user => user.trelloId === addedUserId)[0]; const userToMention = '<@' + addedUser.slackId + '>'; const cardId = contents.action.data.card.id; const listName = getListName(cardId) if (listName === 'REVIEW') { message = userToMention + '\n *<' + shortLink + '|' + cardName + '>* のレビューをお願いします。 by ' + operator.firstName + '\n'; } else { message = userToMention + '\n *<' + shortLink + '|' + cardName + '>* のタスクは任せた。 by ' + operator.firstName + '\n'; message += 'よろしく頼む!!PDCA回すぞ!!'; } postSlack(message); } return ContentService.createTextOutput(); } function setUserData() { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName('users'); const LastRow = sheet.getLastRow(); const LastColumn = sheet.getLastColumn(); const keys = sheet.getRange(1, 1, 1, LastColumn).getValues()[0]; const values = sheet.getRange(2, 1, LastRow - 1, LastColumn).getValues(); return values.map(row => { return keys.reduce((acc, key, index) => { acc[key] = row[index] return acc }, {}) }) } function getListName(cardId) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName('setting'); const trelloAPIKey = sheet.getRange(2, 2).getValue(); const trelloAPIToken = sheet.getRange(3, 2).getValue(); const url = 'https://api.trello.com/1/cards/' + cardId + '/list?key=' + trelloAPIKey + '&token=' + trelloAPIToken; const response = UrlFetchApp.fetch(url); const data = JSON.parse(response.getContentText()); return data.name } function postSlack(text, blocks=[]) { const ss = SpreadsheetApp.getActiveSpreadsheet(); const sheet = ss.getSheetByName('setting'); const postUrl = sheet.getRange(1, 2).getValue(); const data = blocks.length ? { blocks } : { text }; const options = { method: 'post', contentType: 'application/json', payload: JSON.stringify(data) }; UrlFetchApp.fetch(postUrl, options); }
Botの様子
以下のような感じで、Botから通知がきていい感じ!!
みなさんも素敵な開発ライフをお過ごしください!!!