🐕

Bolt + lambda を使って Slack に通知メッセージを送る API を作る

2021/05/31に公開2

Boltを利用してWebアプリと連携し、Slackワークスペースに所属するユーザーに応じて通知を出し分けるAPIを作ってみたので知見として書きます。

https://slack.dev/bolt-js/ja-jp/tutorial/getting-started

当記事で書くこと

  • Slack Appの設定
  • Bolt + serverless によるSlackBotのAPI実装
    • emailからユーザーとのDMチャンネルを検索
    • 取得したDMチャンネルIDへのメッセージ送信
  • lambdaへのデプロイ

当記事で書かないこと

  • Boltを利用したOAuth周りの認証設定
    • 複数ワークスペースでSlackAppを利用するため
    • 今後別記事にて投稿予定

手順

【SlackApp側の設定】

Slack App 作成 ←のリンクから Slack App を作成する

App Home タブにてアプリの DM に表示するタブを設定

  • "App Display Name" の Edit ボタンから、好きな display name を設定して保存する
  • Messages Tab
    • ここのチェックをtrueにすると Messages Tab でユーザーがメッセージを送信できるようになる
      Allow users to send Slash commands and messages from the messages tab

OAuth & Permissions タブにて

  • Slack API の利用に必要な以下の権限を設定する
"channels:read",
"chat:write:bot",
"groups:read",
"im:read",
"mpim:read",
"users:read",
"users:read.email"

以下のAPIのWorks withに必要なscopeが書いてあります

https://api.slack.com/methods/users.lookupByEmail

https://api.slack.com/methods/conversations.list

https://api.slack.com/methods/chat.postMessage

  • Install to WorkSpace する

【Slack App開発】

サンプルコードは以下のリポジトリで公開しています

https://github.com/SotaYamaguchi/serverless-bolt-js-dev-slack

まずはサーバーレスアプリケーションを開発、デプロイするためのツールをインストールします

こちらの記事で紹介されている事前準備を行ってください

https://www.wantedly.com/companies/forstartups/post_articles/279817

Lambdaの作成

下記コマンドを実行してNode.js用の作業ディレクトリとLambdaの定義ファイル作成します。

terminal
$ serverless create --template aws-nodejs --path myService

以下を実行して初期設定を行います

terminal
$ yarn init
myService
├── .npmignore
├── handler.js
├── package.json
└── serverless.yml

以下のコマンドで必要なパッケージをインストールします

terminal
$ yarn add @slack/bolt @vendia/serverless-express multiparty
$ yarn add -D serverless serverless-offline

package.jsonに以下のscriptを追記します
これでyarn devすることでlocalでデバックできるようになりました

package.json
{
	...
	"scripts": {
		"dev": "sls offline",
		"deploy": "npx serverless deploy"
	},
}

Serverlessの設定ファイルを以下の内容に変更します

serverless.yml
service: serverless-bolt-js
frameworkVersion: "2"
provider:
  name: aws
  runtime: nodejs12.x
  region: ap-northeast-1
  environment:
    SLACK_SIGNING_SECRET: ${env:SLACK_SIGNING_SECRET}
    SLACK_BOT_TOKEN: ${env:SLACK_BOT_TOKEN}
functions:
  slack:
    handler: app.handler
    events:
      - http:
          method: ANY
          path: /{any+}
useDotenv: true
plugins:
  - serverless-offline
package:
  patterns:
    - '!.git/**'
    - '!README.md'

あわせて環境変数を追加します

.env
SLACK_SIGNING_SECRET="xxx"
SLACK_BOT_TOKEN="xoxb-xxx"

準備ができたら処理を書いていきます

handler.js
const { App, ExpressReceiver } = require("@slack/bolt");
const serverlessExpress = require("@vendia/serverless-express");
const multiparty = require("multiparty");

const accessToken = process.env["SLACK_BOT_TOKEN"];

const expressReceiver = new ExpressReceiver({
  signingSecret: process.env["SLACK_SIGNING_SECRET"],
  processBeforeResponse: true,
});

const app = new App({
  token: accessToken,
  receiver: expressReceiver,
});

// /slack/events/massegesへのpostリクエストのエンドポイント作成
app.receiver.router.post("/slack/events/masseges", async (req, res) => {
  // req から fields を抽出する
  const data = await new Promise((resolve, reject) => {
    const form = new multiparty.Form();
    form.parse(req, (err, fields, files) => {
      resolve(fields);
    });
  });

  // validation
  if (!data.email) {
    res.status(400).send("error: no_email");
    return;
  }
  if (!data.text) {
    res.status(400).send("error: no_text");
    return;
  }

  const userEmailList = data.email.find((_, i) => i === 0).split(",");
  const massege = data.text.find((_, i) => i === 0);

  let userIds = [];
  for (const email of userEmailList) {
    try {
      // reqパラメーターのemilがワークスペースに存在するか確認
      const user = await app.client.users.lookupByEmail({
        token: accessToken,
        email: email,
      });
      if (user) {
        userIds = [...userIds, user.user.id];
      }
    } catch (error) {
      res.status(400).send(`error: user is Not Found. ${email}`);
      return;
    }
  }

  // DMチャンネル一覧を取得
  const conversationsList = await app.client.conversations.list({
    token: accessToken,
    types: "im",
  });
  const channels = conversationsList.channels;

  if (!!userIds.length) {
    // メールアドレスから取得したユーザーの DM チャンネルのみにフィルター
    channels
      .filter((x) => {
        return userIds.some((y) => {
          return y === x.user;
        });
      })
      .forEach((x) => {
        // メッセージ送信
        app.client.chat.postMessage({
          token: accessToken,
          channel: x.id,
          blocks: massege,
        });
      });
  }
  res.status(200).send("success!!");
});

// Handle the Lambda function event
module.exports.handler = serverlessExpress({
  app: expressReceiver.app,
});

デプロイ

以下コマンドでlambdaへデプロイします

terminal
yarn deploy

これで完成です!

試してみる

APIエンドポイントに対してcurlでリクエストを送ってみる
(email が一致したユーザーは Slack の DM にメッセージが送信される)

aws

通知メッセージを作成する

paramater

  • email : カンマ( , )区切りで通知を送信したいユーザーのメールアドレスを渡す
  • text : メッセージの block を作成しパラメーターに渡す
curl --location --request POST 'https://xxxxxxxxxx.amazonaws.com/dev/slack/events/masseges' \
--form 'email="taro@hoge.com,jiro@hoge.com"' \
--form 'text="[
   {
       \"type\": \"section\",
       \"text\": {
           \"type\": \"mrkdwn\",
           \"text\": \"*twitterフォローしてね!*\"
       }
   },
   {
       \"type\": \"section\",
       \"fields\": [
           {
               \"type\": \"mrkdwn\",
               \"text\": \"https://twitter.com/Area029S\"
           }
       ]
   }
]"'

送信できました!

screeen_shot

あとがき

以上でSlackワークスペースに所属するユーザーに応じて通知を出し分けるAPIを作成することができました。
今後はBoltの認証機能を利用したマルチワークスペース対応の実装例を紹介できればと思います。
ちなみにシンプルに通知のみを実行したいのであればBoltを使わないでサービス側で直接SlackAPIを叩いてしまう方が低コストに実現できます。

参考にした記事

https://zenn.dev/yuta_saito/articles/8b543a1957c375593ee5

Discussion

ニツオニツオ

Slack bot を作るのにNext.jsとVercelってどうですかね?
ExpressとLamdaは直接的には使ったことないのですが、そっちの方が楽ですか?
APIエンドポイントを楽に取得できるという意味でNext.jsを使ってやってるのですが、いまいちうまくいかない部分もあり・・

sota_yamaguchisota_yamaguchi

ご質問ありがとうございます!
Express 単体というよりは Bolt が Slack の APIを諸々内包してくれているので型の補完も効くし楽という印象でした。

Next.js + Vercel で Slack の API を直接叩く方法でも全然有りだと思います!

私が使うなら用途に応じてアプリケーションの機能の一部として使うなら API を直接叩く方法にします。逆に Slack bot を作るならBoltを使ってLambdaとかCloud Functionsとかにデプロイするかなぁという個人の感想でした。

なにかしらの参考になれたらうれしいです。