【GAS(Google Apps Script)× Slack】アイデア投稿Slackアプリ実装 - 後編|Offers Tech Blog
概要
こんにちは、Offers を運営している株式会社 overflow のバックエンドエンジニアの shun です。今回は、前回の 【GAS(Google Apps Script)× Slack】アイデア投稿Slackアプリ実装 - 前編 で作成した Slack アプリを育て、アイデア投稿 Slack アプリを全て実装していきます。早速やっていきましょう。
本格的にアイデア投稿Slackアプリを実装
前回の記事にて Slack アプリの作成ができたので、あとは GAS で機能を実装 -> 検証 -> 修正を繰り返していくだけです。残りのやることとしては以下になります。
- スラッシュコマンド投稿したらアイデア入力フォームを立ち上げる
- 投稿内容をスプレッドシートへ出力
- 投稿内容を Slack へ通知
ではサクサク残りやってしまいましょう!
Slackアプリが行うScopeを設定する
Slack アプリで行うこととしては以下なので、それぞれに必要となる Scope を付与していきます。
Slack アプリのページのサイドメニューから、OAuth & Permissions を選択し、Scopes セクションから以下の 2 つを追加します。
行うこと | Scope |
---|---|
Slackへの投稿 | chat:write |
投稿者の情報取得 | users:read |
Scope の設定が完了したら、ページ上部の Reinstall to Workspace を実行し、再度インストールします。
インストールが完了すると、Bot User OAuth Token なるものが出現します。この値を GAS で利用していきます。
スラッシュコマンド投稿したらアイデア入力フォームを立ち上げる
GAS の doPost
メソッドは、デプロイしたエンドポイント URL に対して POST リクエストした時に発火します。なのでスラッシュコマンドを打ち込んだ時にもこの POST リクエスト送られるため、doPost 内部にあれこれ書く必要があります。
App/Main.gs
を以下のように書き換えます。
const doPost = (e) => {
const parameter = e.parameter
try {
// 自分が作成したSlackアプリからのリクエストでない場合はエラー
if (SLACK_TOKEN != parameter.token) throw new Error(parameter.token)
// モーダルを開くようにSlackへリクエスト
return openModal(parameter)
} catch(error) {
return ContentService.createTextOutput(403)
}
}
続いて、新規で App/ModalView.gs
を作成します。
/**
* モーダルOpen
*/
const openModal = payload => {
const modalView = generateModalView()
const viewData = {
token: SLACK_ACCESS_TOKEN,
trigger_id: payload.trigger_id,
view: JSON.stringify(modalView)
}
const postUrl = 'https://slack.com/api/views.open'
const viewDataPayload = JSON.stringify(viewData)
const options = {
method: "post",
contentType: "application/json",
headers: { "Authorization": `Bearer ${SLACK_ACCESS_TOKEN}` },
payload: viewDataPayload
}
UrlFetchApp.fetch(postUrl, options)
return ContentService.createTextOutput()
}
/**
* モーダルBlocks
*/
const generateModalView = () => {
return {
"type": "modal",
"title": {
"type": "plain_text",
"text": "アイデア投稿App",
"emoji": true
},
"submit": {
"type": "plain_text",
"text": "投稿する",
"emoji": true
},
"close": {
"type": "plain_text",
"text": "キャンセル",
"emoji": true
},
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*アイデア募集中!*"
}
},
{
"type": "divider"
},
{
"block_id": "idea_title",
"type": "input",
"element": {
"type": "plain_text_input",
"multiline": true,
"action_id": "idea_title_id"
},
"label": {
"type": "plain_text",
"text": "アイデア",
"emoji": true
}
},
{
"block_id": "idea_detail",
"type": "input",
"element": {
"type": "plain_text_input",
"multiline": true,
"action_id": "idea_detail_id"
},
"label": {
"type": "plain_text",
"text": "背景・詳細",
"emoji": true
}
}
]
}
}
続いて、Settings.gs
を新規作成し、以下のように設定データを格納します。
const SLACK_SIGNING_SECRET = 'SlackアプリのTop画面にある Signing Secret'
const SLACK_ACCESS_TOKEN = 'Bot User OAuth Tokenに記述されている値'
const SLACK_TOKEN = 'SlackアプリのTop画面にある Verification Token'
const SLACK_CHANNEL = '回答内容を通知したいチャネル名(例)#general'
ここで、再度デプロイをしましょう。
再度Slackからスラッシュコマンド打ってみる
Slack の任意のチャンネルにて /ideapost
を打ち込んでみます。
ペンギン GoodJob
現状はこのフォームに値を入力して投稿するボタンを押してもローディングが続いてエラーになります。
続いて 回答 をできるようにしましょう。
投稿内容をスプレッドシートへ出力
App/Main.gs
を以下のように書き換えます。
スラッシュコマンドでもこの doPost
は呼ばれ、投稿時にも doPost
は呼ばれます。
以下の特徴があるので、プログラムに分岐として適応します。
Action | 特徴 |
---|---|
スラッシュコマンド実行 | e.parameter.payloadが存在しない |
モーダルから回答実行 | e.parameter.payloadが存在する |
const doPost = (e) => {
const parameter = e.parameter
try {
if(parameter.payload) {
const payload = JSON.parse(decodeURIComponent(parameter.payload))
if (SLACK_TOKEN != payload.token) throw new Error(payload.token)
return appendIdeaAnswer(payload) // 回答時はスプレッドシートへ出力
} else {
if (SLACK_TOKEN != parameter.token) throw new Error(parameter.token)
return openModal(parameter) // スラッシュコマンドの時はモーダルをOpen
}
} catch(error) {
return ContentService.createTextOutput(403)
}
}
App/ModalView.gs
に以下のメソッドを追加します。
/**
* モーダル入力サブミットした時の処理
* ログシートへ出力のみ
*/
const appendIdeaAnswer = payload => {
const book = SpreadsheetApp.getActiveSpreadsheet()
const sheet = book.getSheetByName('シート1') // 投稿内容を出力するシート名
const formValues = payload.view.state.values
const ideaTitle = formValues.idea_title.idea_title_id.value
const ideaDetail = formValues.idea_detail.idea_detail_id.value
const userId = payload.user.id
const today = Utilities.formatDate(new Date(), 'Asia/Tokyo', "yyyy/MM/dd")
const appendData = [ideaTitle, ideaDetail, userId, today]
const lastRow = sheet.getLastRow()+1
sheet.getRange(lastRow, 1).setFormula('=row()-1')
sheet.getRange(lastRow, 2, 1, appendData.length).setValues([appendData])
return ContentService.createTextOutput()
}
再度、デプロイ -> スラッシュコマンド・Interactivity 設定更新を行いましょう。
その後、再度検証してみます。
投稿する ボタンクリックします。
ローディングが走り、モーダルが閉じます。
ここで、出力先のスプレッドシートを確認してみましょう。
無事に出力が確認できました!!ペンギン GoodJob!
投稿内容をSlackへ通知
いよいよ最後のステップです。
スプレッドシートに出力された投稿内容を、定期ジョブにて Slack へ通知しましょう。
重複で通知が行かないように、スプレッドシートの isNotified?
列にてステータスを随時更新するようにします。スプレッドシートの最終行に isNotified?
を追加しましょう。
次に、新規で Jobs/NotifyPostedIdea.gs
ファイルを作成します。
/**
* 投稿内容を匿名でSlackへPOST
* 5分おきに実行
*/
const notifyPostedIdeas = () => {
const book = SpreadsheetApp.getActiveSpreadsheet()
const sheet = book.getSheetByName('シート1') // 投稿内容を出力するシート名
const sheetData = sheet.getDataRange().getValues()
const header = sheetData[0]
sheetData.map((data, index) => {
if(index > 0 && data[header.indexOf("isNotified?")] === '') {
return {
data: data,
rowNum: index+1
}
}
}).filter(a => a).forEach(target => { // map内にif分使用すると、elseに入った場合はundifinedになるため、filterで潰します
postToSlack(target, sheet, header)
updateSheetFlag(target, sheet, header)
})
}
/**
* Slackへ通知
*/
const postToSlack = (data, sheet, header) => {
// 重複投稿防ぎ
sheet.getRange(data.rowNum, header.indexOf("isNotified?")+1).setValue("投稿中...")
const postRequestUrl = "https://slack.com/api/chat.postMessage" // see: https://api.slack.com/methods/chat.postMessage
const blocks = generateNotifyBlockMessage(data, header)
const message = {
token: SLACK_ACCESS_TOKEN,
blocks: blocks['blocks'],
channel: SLACK_CHANNEL
}
const messagePayload = JSON.stringify(message)
const messageOptions =
{
method: "post",
contentType: "application/json",
payload: messagePayload,
headers: { "Authorization": `Bearer ${SLACK_ACCESS_TOKEN}` }
}
UrlFetchApp.fetch(postRequestUrl, messageOptions)
}
/**
* 通知完了した行の isNotified? 列をYESに更新
* これをすることで、再度 notifyPostedIdeas が走った時に重複通知がされないようになります
*/
const updateSheetFlag = (data, sheet, header) => {
sheet.getRange(data.rowNum, header.indexOf('isNotified?')+1).setValue('YES')
return true
}
/**
* Slack通知用のメッセージ作成
*/
const generateNotifyBlockMessage = (data, header) => {
return {
"blocks": [
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":sparkles:*新しいアイデア発掘!*:sparkles:"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":bulb:*タイトル*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `${data.data[header.indexOf('Title')]}`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":bulb:*詳細・背景*"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `${data.data[header.indexOf('Description')]}`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "これ賛同!というものだったら:+1:(+1)スタンプを押してください!"
}
}
]
}
}
そして、こちらを参考 に、notifyPostedIdeas
を 5 分おきに動くように設定すれば、以下のような通知が Slack へ飛ぶようになります。
ペンギンお疲れ様でした。
実際に社内に導入してみてどうだったか
アイデア投稿は導入後、いろんな視点でのアイデアがありました。
以前ご紹介した BreakingTime という施策もこのアイデア投稿アプリ経由で実現され、そろそろ 1 年記念日を迎えます。
プロダクトに対するアイデアや、社内制度に対するアイデアなど、多くのアイデアが実行されていきました。 「あ、アイデア出したらみんな吟味してくれるじゃん」 という感覚を得られるのは結構嬉しいものです。
余談ですが、私が良かれと思って毎週月曜日にペンギン経由で「アイデアください!!」的なメッセージを投稿していたら多少ウザがられたので、やり過ぎには注意ですね笑
使いたい時、そこにあることが重要だと思います。
まとめ
今回は Slack・GAS(Google Apps Script)・スプレッドシートを用いて、アイデア投稿 Slack アプリの実装方法についてまとめました。
前編含めだいぶ長くなりましたが、最後まで読んで頂き、ありがとうございました。「いいね」していただけると記事執筆の励みになりますので、参考になったと思った方は是非よろしくお願いします!
次回は、社内でみんなが気持ちよく利用できるような GAS 利用時の注意点などを書こうと思います。
関連記事
副業転職の Offers 開発チームがお送りするテックブログです。【エンジニア積極採用中】カジュアル面談、副業からのトライアル etc 承っております💪 jobs.overflow.co.jp
Discussion