slackアプリを作る機会があったので、ブログにしてみました。
チームのみんなにお寿司かピザを選択してもらい、それがリモートの自宅に届くすごく便利なFoods Bot
です。
流れは実に単純で
slack → api gateway → lambda → お寿司屋さんorピザ屋さん
とういうふうになってます。
Serverless Frameworkの具体的な説明やawsの詳しい仕様周りの説明はしません。それぞれチュートリアル程度はやっていないと、この説明では何がなんだかわからないと思います。
このslackアプリを作るうえであった方がいい前提知識があります。
まずはslack
が提供しているBoltです。
slackは日本人の開発メンバーもいるらしくドキュメントが日本語化されているので、この辺はわかりやすくていいですね。
入門ガイドや基本的な概念程度はまず手を動かしてやってみることをお勧めします。
公式からAWSへのデプロイ方法も紹介されているので、こちも参考になります。AWS Lambda へのデプロイ
あとは、slack api
slackのアプリのデザインをオンライン上で確認できるBlock Kit Builderなんかも使うのであらかじめ見てみることをお勧めします。
HelloWorld程度のものは公式から出ているので、こちらを参考にしましょう。
ちょっと説明不足な点といえばlayer
くらいですかね。
こちらはAWS Lambdaのlayerをserverless frameworkでつくる(node.js)が参考になると思います。
HelloWorld程度であればこれでいいですけど、実際は色々な処理が入ってきて分割したくなるのが普通なので、僕の場合はこうしてます。
まずはserverless.yml
です。
service: slack-api
frameworkVersion: '2'
plugins:
localPath: './layer-package/nodejs/node_modules'
modules:
- serverless-deployment-bucket
provider:
name: aws
runtime: nodejs14.x
region: ${opt:region, 'us-east-1'}
stage: ${opt:stage, 'development'}
lambdaHashingVersion: 20201221
deploymentBucket:
name: ${self:service}-architect-bucket-${self:provider.stage}
serverSideEncryption: AES256
environment:
SLACK_SIGNING_SECRET: 123456789
SLACK_BOT_TOKEN: token-123456789
resources:
# api/role
- ${file(./api/slack/role/operation-event.yml)}
package:
individually: true
patterns:
- '!*.md'
- '!api/**'
- '!*.json'
- '!node_modules/**'
- '!Makefile'
layers:
layerPackage:
path: layer-package
functions:
slackOperationEvent:
package:
patterns:
- api/slack/operation-event.js
handler: api/slack/operation-event.handler
events: ${file(./api/slack/slack.yml):slack_operation_event}
role: !GetAtt OperationEventRole.Arn
layers:
- !Ref LayerPackageLambdaLayer
僕の場合はrole
、api gateway
やlayer
なんかも分割して最初から作ってしまうので、こんな感じになってます。あとはSLACK_SIGNING_SECRET
とSLACK_BOT_TOKEN
の環境変数ですかね。これはlambdaでboltを使うときに必要になります。
api gateway
は以下のようにしてます。
slack.yml
slack_operation_event:
- http:
path: slack
method: post
slack apiのshort cutを使ってまずはslackのフォームを作成します。
Creating and handling shortcuts
フォームのレイアウトはBlock Kit Builderで作っていくと簡単にできます。
lambdaは以下のようになります。
// slack/bolt
const { App, AwsLambdaReceiver } = require('@slack/bolt');
// Initialize your custom receiver
const awsLambdaReceiver = new AwsLambdaReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET
});
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
receiver: awsLambdaReceiver,
// The `processBeforeResponse` option is required for all FaaS environments.
// It allows Bolt methods (e.g. `app.message`) to handle a Slack request
// before the Bolt framework responds to the request (e.g. `ack()`). This is
// important because FaaS immediately terminate handlers after the response.
processBeforeResponse: true
});
// foodsのショートカット
app.shortcut('foods', async ({ ack, body, client }) => {
// コマンドのリクエストを確認
await ack();
try {
const result = await client.views.open({
// 適切な trigger_id を受け取ってから 3 秒以内に渡す
trigger_id: body.trigger_id,
// view の値をペイロードに含む
view: {
type: 'modal',
// callback_id が view を特定するための識別子
callback_id: 'operation_view',
title: {
type: 'plain_text',
text: 'foods Bot'
},
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: 'Foods'
}
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '好きな食べ物を選んでください。'
}
},
{
type: 'divider'
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: '食べ物'
},
accessory: {
type: 'static_select',
placeholder: {
type: 'plain_text',
text: 'Select an item',
emoji: false
},
options: [
{
text: {
type: 'plain_text',
text: 'お寿司'
},
value: 'sushi'
},
{
text: {
type: 'plain_text',
text: 'ピザ'
},
value: 'pizza'
}
],
action_id: 'foods'
}
},
{
dispatch_action: true,
type: 'input',
element: {
type: 'plain_text_input',
action_id: 'email'
},
label: {
type: 'plain_text',
text: 'メールアドレス',
emoji: false
}
}
],
submit: {
type: 'plain_text',
text: 'Submit'
}
}
});
console.log(result);
} catch (error) {
console.error(error);
}
});
// Handle the Lambda function event
module.exports.handler = async (event, context, callback) => {
const handler = await app.start();
return handler(event, context, callback);
};
これでユーザーがショートカットを選択するとモーダルが出てきて、お寿司とピザの選択そしてEmailを入力するフォームが出来上がりました。
slackのack()
による応答は3秒以内に行う必要があります。今回の場合、セレクトボックスでお寿司かピザを選んだ瞬間にack()
で応答する必要があります。
正直、なくても動くのですが、CloudWatch
にエラーとしてログが残ります。
ERROR [ERROR] An incoming event was not acknowledged within 3 seconds. Ensure that the ack() argument is called
あと、slack側でも注意されるので、ちゃんと対応した方がいいでしょう。
今回はaction_id
がfoods
なので、これに対応します。
lambda
app.action('foods', async ({ ack }) => {
await ack();
});
これでエラーは出なくなりました。
モーダルから送られて情報を確認するためにapp.view
を使います。
参考イベントの確認
こちらを参考に作ったのが、こちら。社内で使う程度なのでチェックも最低限でいいと思ってあまり凝ったことはやりませんでした。
view.state.values
に値が入っているのですが、ユニークidの配下に値が入っていたのでObject.keys() メソッド
を使いました。
あとは実行したい処理をひたすら書くだけです。
app.view('operation_view', async ({ ack, body, view, client }) => {
// モーダルでのデータ送信イベントを確認
console.log('operation_view');
await ack();
console.log(`view ${view}`);
// 入力値を使ってやりたいことをここで実装
console.log(`view.state.values: ${JSON.stringify(view.state.values)}`);
const stateValues = view.state.values;
const objectKeys = Object.keys(stateValues);
// slackから送られたデータをJsonに整形する
async function setJson () {
const formatJson = {};
return new Promise((resolve) => {
for (const element of objectKeys) {
if (stateValues[element].foods) {
formatJson.foods = stateValues[element].foods.selected_option.value;
}
if (stateValues[element].email) {
formatJson.email = stateValues[element].email.value;
}
}
resolve(formatJson);
});
}
// emailの有効性チェック
async function emailValidates () {
const setJsonResults = await setJson();
return new Promise((resolve) => {
if (setJsonResults.email) {
// eslint-disable-next-line no-useless-escape
const isEmail = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
if (isEmail.test(setJsonResults.email)) {
setJsonResults.status = 'success';
resolve(setJsonResults);
} else {
setJsonResults.status = 'err';
setJsonResults.message = '入力されたemailが有効ではありません。';
resolve(setJsonResults);
}
} else {
setJsonResults.status = 'err';
setJsonResults.message = '入力されたemailが有効ではありません。';
resolve(setJsonResults);
}
});
}
// 実行したい様々な処理
// レスポンスを返す
async function sendMessageToUser () {
return result.message;
}
const msg = await sendMessageToUser();
console.log(msg);
const val = view.state.values;
const user = body.user.id;
const results = (user.input, val);
if (results) {
msg;
}
// ユーザーにメッセージを送信
try {
await client.chat.postMessage({
channel: user,
text: msg
});
} catch (error) {
console.error(error);
}
});
これで完成です。
slack apiはなかなか便利ですね。今回は社内むけの簡単なものしか作りませんでしたがawsと連携できるので新しいサービスにも応用が効きそうです。
ちなみに、自宅にお寿司やピザが届く制度はありません。ちょっと変えて書きました。hahaha。