この記事はモチベーションクラウドシリーズアドベントカレンダー2020の4日目の記事です。
どうも初めまして、リンクアンドモチベーションでエンジニアをしている伊藤と申します。
完全に別件の宣伝ですが、2020/12/18に弊社がスポンサーにもなっている【オンライン開催】銀座Rails#28 で「モチベーションクラウドを支える非同期処理の変遷」というテーマで発表します。
今回のゲストスピーカーはあのまつもとゆきひろ(@yukihiro_matz)さんなので興味があればぜひ!!
はじめに
業務効率化のために slack apiと google app scriptを使ったslack appを作りました。
なぜ作ったか から作成の手順を3つのステップに分けて解説します。
こんなことありませんか?
〜オンライン会議にて〜
上司 「おい、若手!全員いるか確認しろ!」
自分 「はい!」
...
自分 「(顔だけじゃわかんないし、人数的には足りないけど誰いないかわかんないよ...)」
上司 「誰いない?いない人声かけて!」
自分 「...(いないフリ)」
上司 「ん???」
自分 🥺
新しい生活様式になり、ガラリと変わった会議の形 = オンライン会議 での若手のあるある・イライラの一つである、
「オンライン会議、誰がいないか全然わからない問題」
を解決するために!
出欠を報告してもらう
slack用アプリ (スラッシュコマンド)
を作成しました!
若手でも作れます!!
何を作ったか
使い方は簡単!!
(「出欠確認して」と言われる前にやっておくこと!) ← こういうのが若手のうちはとっても大事
- 全員がいるslackチャンネルで
/attendance
と入力する - 参加者がボタンをおす
- 名前が残っている人を読み上げて、参加しているか確認する
ほら、いない人が一目瞭然!!!
もうこれで
「誰がいないか画面とにらめっこ」
なんてしなくていいですね!!
今回はslack apiを活用しながら、Google Apps Script (以降 gas) で実装していきます。
いいね!と思った人はぜひLGTMお願いします!!
作り方
解説はいくつか飛ばしますがgasは活用記事が多かったり、本家のReferenceも整備されているのでそちらも合わせて参照ください。
全体像
/attendance
を実行してから、欠席者の名前が出るまでのフローは以下のようになっています
なので
- slack appの設定
- スラッシュコマンドの作成
参加
ボタン用apiの作成
の3つに分けて説明していきます!
1. slack appの設定
まずはslack appを登録していきます
Slack APIへアクセスし下記の「Start Building」をおします
すると次のようなポップアップが出てくるので
- App Nameはお好みで (出欠確認bot とか)
- Development Slack Workspace は導入する予定のslackのworkspaceを選択
早速アプリが作成できました!
さらに、Settings > Basic Information > Display Information
より、作ったアプリの見た目をいい感じにいじることができます
次にbotとして発信もさせたいので、botの登録をします
Features > App Home > Your App's Presence in Slack
より Editをおします
ポップアップが出てくるのでいい感じに名前を付けます
これで完了です!
アプリのインストールとtokenの確認
Features > OAuth & Permissions > OAuth Tokens & Redirect URLs
で 「Install to Workspace」を押して、許可するを押しましょう
するとFeatures > OAuth & Permissions > OAuth Tokens & Redirect URLs
に
OAuth Access Token
が表示されるので、メモしておいてください (あとで使用します)
(このtokenはセキュリティ的に重要なので、扱いには気をつけてください)
また、Settings > App Credentials
より
Verification Token
もメモしてください (こちらもあとで使用します)
これで一旦slack appの設定は完了です
このあとちょくちょくslack側での設定を行うので、このslack appのページは閉じないで下さい
2. スラッシュコマンドの作成
次はslackのスラッシュコマンドをgasで作成します
スラッシュコマンドって?
slack上で使える簡単なコマンドです
/remind
とかは使ったことのあるひとも多いのではないでしょうか?
/remind ごみを捨てる tommorow
と送信すると...
ってできるやつです!
参考
ここでは /attendance
って入力するとメンバーの一覧が投稿される機能を作っていきます!
今回の作り方
流れはこのような形になっています
なので、手順としては、
- google スプレッドシートにメンバーリストを用意する
- スプレッドシートにメンバーリストを送信するスクリプトを用意する
- slackからメンバーリストを使ってボタン付き出欠確認メッセージを送信できるようにする
の流れで作っていきます
google スプレッドシートにメンバーリストを用意する
gasを始める前に会議に参加する人の一覧を作っておきましょう
- 新しいスプレッドシートは http://spreadsheet.new/ で作れます
- 今回は「ユーザー一覧 (users)」 と 「所属一覧 (groups)」の2つのシートを作ります
- ユーザー一覧には、
slack id
,名前
,所属 (グループ名とか)
の3列を用意します - 所属一覧には、
所属一覧
の1列を用意します - slack idの調べ方はこちらなどを参考にcsvなどでどかっととってきましょう
シートのイメージ
この時以下の2点に注意してください
- ユーザー一覧シートは
- 一行目に 、
slack id
,名前
,所属
をいれる - シートの名前を
users
にする
- 一行目に 、
- 所属一覧シートは
- 一行目に 、
所属一覧
をいれる - シートの名前を
groups
にする
- 一行目に 、
スプレッドシートにメンバーリストを送信するスクリプトを用意する
まずはgasを開きましょう
先ほどのシートの ツール > スクリプトエディタ
を選択します
するとこんなコードエディターが開きます
ここに以下をコピペして保存します (コードの詳しい解説はskipします!!)
**コード.gs(ここをクリックして展開)**
const slackToken = PropertiesService.getScriptProperties().getProperty("slack_token")
const verificationToken = PropertiesService.getScriptProperties().getProperty("verification_token")
const NotVerificateMessage = "許可されていません"
const usersSheetName = "users" // ユーザー一覧シート名
const groupsSheetName = "groups" //所属一覧シート名
const buttonValue = "attend"
// 以下の変数は適宜変更してください
const buttonText = "参加"
const notAttendComment = "まだ参加していない方↓"
const defaultMessage = "参加していますか?"
const responseData = "出欠確認をしました"
const textMessage = "出欠をとっています"
function doPost(e) {
const params = e.parameter
const token = params.token
if (token != verificationToken) {
return ContentService.createTextOutput(NotVerificateMessage).setMimeType(ContentService.MimeType.JSON);
}
const userName = params.user_name
const channelId = params.channel_id
let message = params.text
if (!message) {
message = defaultMessage
}
postAttendaceMessage(channelId, message, userName)
return ContentService.createTextOutput(responseData).setMimeType(ContentService.MimeType.JSON);
}
function postAttendaceMessage(channelId, message) {
// userのリストをとってきて
const usersHash = usersHashByGroupname()
// blockを作成して
const blocks = makeBlockFromUsersHash(usersHash, message)
// 決められたchannelに投稿する
return postSlackByChannelwithBlocks(channelId, blocks)
}
// return {group_name: [user_id, user_id], group_name: [user_id,...], ..}
function usersHashByGroupname() {
const rawSheetValues = getSpreadSheetByName(usersSheetName)
let groupedHash = {}
const rowGroupSheetValues = getSpreadSheetByName(groupsSheetName)
rowGroupSheetValues.forEach(function (value) {
if (value[0]) {
groupedHash[value[0]] = []
}
})
rawSheetValues.forEach(function (value) {
if (value[0] && value[1] && value[2]) {
groupedHash[value[2]].push("<@" + value[0] + ">")
}
})
return groupedHash
}
function getSpreadSheetByName(sheetName) {
//spreadsheetの値を返す
const ss = SpreadsheetApp.getActiveSpreadsheet(); //スプレッドシートを取得
const sheet = ss.getSheetByName(sheetName); //シート名でシートを取得
const lastRow = sheet.getLastRow(); //最終行取得
const lastCol = sheet.getLastColumn(); //最終行取得
const firstRow = 2
const firstCol = 1
const range = sheet.getRange(firstRow, firstCol, lastRow, lastCol)
const values = range.getValues()
return values
}
function postSlackByChannelwithBlocks(channelId, blocks) {
const postMessageurl = "https://slack.com/api/chat.postMessage"
const payload = {
"token": slackToken,
"channel": channelId,
"text": textMessage,
"blocks": blocks
}
const params = {
methods: 'post',
payload: payload
}
return UrlFetchApp.fetch(postMessageurl, params)
}
function makeBlockFromUsersHash(usersHash, message) {
const head = {
"type": "section",
"text": {
"type": "mrkdwn",
"text": message
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"emoji": true,
"text": buttonText
},
"value": buttonValue
}
}
const divider = {
"type": "divider"
}
const notAttendMessage = {
"type": "section",
"text": {
"type": "mrkdwn",
"text": notAttendComment
}
}
const mainBlocks = makeMainBlocksbyUserHash(usersHash)
let blocks = []
blocks.push(head)
blocks.push(divider)
blocks.push(notAttendMessage)
blocks = blocks.concat(mainBlocks)
blocks.push(divider)
return JSON.stringify(blocks)
}
function makeMainBlocksbyUserHash(usersHash) {
let mainBlocks = []
Object.keys(usersHash).forEach(function (groupName) {
if (usersHash[groupName].length > 0) {
const context = {
"type": "context",
"elements": [{
"type": "mrkdwn",
"text": groupName
}]
}
const usersSection = {
"type": "section",
"text": {
"type": "mrkdwn",
"text": usersHash[groupName].join(" ")
}
}
mainBlocks.push(context)
mainBlocks.push(usersSection)
}
})
return mainBlocks
}
保存したら ファイル > プロジェクトのプロパティ
を開きます
さらに スクリプトのプロパティ
のタブに二つの値を設定します
プロパティ | 値 |
---|---|
slack_token |
xoxp- から始まる先ほどメモした OAuth Access Token
|
verification_token | 先ほどメモした Verification Token |
設定したら保存を押します
最後にapiとして公開します
公開 > ウェブアプリケーションとして導入
を選択します
Deploy as web app
というポップアップが出てくるので、以下のように設定します
Project version | New (ここをNewにしないと最新の更新が反映されないので注意!!) |
---|---|
Execute the app as | Me |
Who has access to the app | Anyone, even anonymous |
そしてDeployを押すと初回は権限を確認されるので許可を確認を押します
アカウント選択画面が出るので、自分のアカウントを選択します
すると警告メッセージが出ますが、詳細
(1) を押すと script名 (安全ではないページに移動)
(2) が出てくるのでクリックします
script名 が Google アカウントへのアクセスをリクエストしています
と表示されるので 許可
を押します
すると公開が完了します
完了後、Deploy as web app
というポップアップと共に今回作成したapiのurlが表示されるのでこれをメモします
これでapiの作成が完了しました!!
slackからメンバーリストを使ってボタン付き出欠確認メッセージを送信できるようにする
次にslackにスラッシュコマンドを用意します
slack apiのFeatures > Slash Commands
より、Create New Commandsを押します
そしたらそれぞれこんな感じで埋めていき、保存します
- Command :
/attendance
- Request URL : さっき公開したURL
- Short Description, Usage Hint : いい感じに!!
これでスラッシュコマンドの登録が完了です!
↓この段階でこんな感じのスラッシュコマンドが使えるようになっているはずです
3.参加
ボタン用apiの作成
さて残るはボタンを押した時のアクションのみです
参加ボタンを押すとslackのメッセージが変わるアクションを作っていきます
ボタンを押してからメッセージが更新される流れはこんな感じです
apiの作成
ボタンアクション用のapiもgasで作っていきます
先ほどとは別の新しいscriptを用意しましょう => http://script.new/
ここに以下をコピペして保存します (コードの詳しい解説はskipします!!)
**コード.gs(ここをクリックして展開)**
const slackToken = PropertiesService.getScriptProperties().getProperty("slack_token")
const verificationToken = PropertiesService.getScriptProperties().getProperty("verification_token")
const replaceEmoji = ":white_check_mark:" // 好きな絵文字に置き換える!
const attendButtonValue = "attend"
function doPost(e) {
const payload = JSON.parse(e["parameter"]["payload"])
const token = payload.token
if (token != verificationToken) {
return
}
const action = payload.actions[0]["value"]
if (action === attendButtonValue) {
return clickedAttendButton(payload)
}
}
function clickedAttendButton(payload) {
const originalBlocks = JSON.stringify(payload.message.blocks)
const messageTs = payload.container.message_ts
const channelId = payload.container.channel_id
const userId = payload.user.id
const user = "<@" + userId + ">"
const replacedBlocks = originalBlocks.replace(user, replaceEmoji)
const chatUpdateUrl = "https://slack.com/api/chat.update"
const updatePayload = {
"token": slackToken,
"channel": channelId,
"ts": messageTs,
"blocks": replacedBlocks
}
const updateParams = {
methods: 'post',
payload: updatePayload
}
UrlFetchApp.fetch(chatUpdateUrl, updateParams)
const params = {
"replace_original": false,
"text": ""
}
return ContentService.createTextOutput(JSON.stringify(params)).setMimeType(ContentService.MimeType.JSON);
}
保存したら先ほどと同様にtokenの設定を行います
ファイル > プロジェクトのプロパティ
を開きます
さらに スクリプトのプロパティ
のタブに二つの値を設定します
プロパティ | 値 |
---|---|
slack_token |
xoxp- から始まる先ほどメモした OAuth Access Token
|
verification_token | 先ほどメモした Verification Token |
完了したらスラッシュコマンド作成時と同様にapiとして公開してください
(この記事の 2. スプレッドシートにメンバーリストを送信するスクリプトを用意する
を参考にしてください)
slack apiへの登録
早速作成したapiをslackに登録していきます
Features > Interactivity & Shortcuts
のページにいきます
このInteractivity
を Off → On にするといろいろなメニューが展開されます
そしてInteractivity
のRequest URL
に先ほど作成したapiのurlを貼り付けます
そして、右下の Save Changesを押せば完了です!!
これで参加ボタンを押すと自分の名前が ✅ に変更されるようになりました!!
完成!!
長かったですね...
お疲れ様です!!
応用編
毎週月曜日の朝9時に出欠をとりたい!
この記事が参考になるかと思います
今回作成した、/attendance
用コードを応用しましょう
例えば、下記のような関数を作成し、上記の記事のようにtriggerを設定してあげれば定例オンライン会議の度に/attendance
をしなくてもよくなります!! (さらなる無駄の解消)
function postAttendaceMessageEveryMonday() {
const channelId = "channel id"
const message = "月曜日の定例へ参加お願いします! https://meet.google.com/xxx-xxxx-xxx"
postAttendaceMessage(channelId, message)
}
出欠確認メッセージをもっとモチベーションがあがる形にしたい!
slackが用意している Block Kit Builderを使えばアウトプットをイメージしながら作ることができます!
ボタンを押したらもっと可愛い絵文字になるようにしたい!
自分のワークスペースでカスタム絵文字を設定して見ましょう
参加
ボタン用api のソース内部の replaceEmoji
という変数を追加したカスタム絵文字にすることでより愛着が湧きます
参考:
↓弊社ではなぜか参加
ボタンを押すと自分が坊主になります...???
終わりに
書き終わって見ると、この記事が想像の3倍の量になって驚いています...
slack appだったり、google app scriptはリファレンスを読んでいると、「あれ、面倒だったこれもアプリにやらせるんじゃないか??」がいろいろ湧いてきて楽しいですね。
面倒なルーティーンはアプリに任せて、自分は新しいことを勉強する時間に使っていきたいですね!!