Bolt + Serverless Framework でお手軽 Slack アプリ開発

# 作るもの

----

## 背景
- とあるチームで、「週に1件以上ビジネスアイデアを Slack に投稿しよう！」という取り組みを作った
- しばらくは真面目に投稿していたが、忙しさにかまけて徐々に投稿が減る事態に・・・
- 「ボットに催促してもらおう！」ということに


----
## 機能＆構成
- アイデアの投稿があれば褒めてくれる
- 週に1度のアイデア投稿がなければ催促する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/26f3939d-f9b0-a4e9-879c-64a75dbeed0b.png)

----

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/383cb806-32e6-4f69-d866-a30498ead1f1.png)


----

# 手順
## Slack アプリの準備
基本的には、[Bolt 入門ガイド](https://slack.dev/bolt-js/ja-jp/tutorial/getting-started) に従って準備しますが、いくつか記載のない手順も必要だったので、改めてまとめておきます😃

----

### 1. アプリを作成する
slack api ダッシュボードの [Your Apps](https://api.slack.com/apps) で `Create New App` をクリックします。
画像のようなダイアログが開くので、ここでは `From scratch` を選択しておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/52da741d-0cdf-8f56-44e6-fb74dc4d46c1.png)

アプリ名を入力し、テストに使うワークスペースを選択します。
アプリ名は後から変更可能なので、適当に入力しておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/f243a18d-a22b-d7a4-1d4b-f9e957693729.png)

----

### 2. トークンとアプリのインストール
トークンには `ユーザートークン` `ボットトークン` `アプリレベルトークン` の3種類が存在するようですが、今回はボットを作りたいのでボットトークンを作成します。

----

#### 2-1. ボット名の設定
作成したアプリの管理画面サイドメニューの　`Features` > `App Home` にある `Your App’s Presence in Slack` 欄の `Edit` をクリックします。
アプリ名とは別にボットの表示名とユーザー名が必要なようなので、設定しておきます。
この手順を抜かすと、後続手順のワークスペース追加ができませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/b44722d1-bf38-0750-4dc9-a8dae26576db.png)

----

#### 2-2. Slash コマンドの作成
メニューの `Features` > `Slash Commands` から、 `Create New Command` を押してコマンド登録します。
デプロイ前でまだ URL が決まっていないので、 Request URL は `example.com` にしておきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/73c8b5af-c478-1712-03b7-a6c6300aec82.png)

----

#### 2-3. 権限設定
サイドメニューの　`Features` > `OAuth & Permissions` から必要な権限を追加します。
2-2の手順でで `commands` は登録されていたので、 `chat:write` を追加しておきました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/ddb510f3-997d-3e93-54e1-d5e656158fb0.png)

最後に、同じ画面上部の `Install to Workspace` をクリックして、ワークスペース側からアプリへの認可を与えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/6fd87280-74cb-29d3-dd7b-17dba924240b.png)

----

## 3. Bolt on Serverless Framework の環境作り

以下の手順で構築しましたが、利用したテンプレートの依存バージョンが古く、解決するのに苦労しました。。

諸々解決した状態でシンプルなサンプルアプリを公開しているで、面倒な人はよろしければ→[bolt-serverless-node-example](https://github.com/tomoeine/bolt-serverless-node-example) をクローンして使ってください。
（これもいずれ古くなると思いますが😅）

----

## 3-1. テンプレートから　Serverless アプリを新規作成する

```shell
npm i -g serverless

# Serveless 用のテンプレートを利用して新規作成（色々と古いので要注意）
serverless create \
  --template-url https://github.com/seratch/serverless-slack-bolt-aws/tree/master \
  --path idea-reminder
```

その後、諸々のパッケージのバージョンを上げ、結果としてエラーになった箇所を修正したり、 TypeScript を入れたり `Serverless Offline が使えるようにしました。（詳細略）

----

### 3-2. Slash コマンドをとりあえず動かしてみる
2-2 で登録したコマンドの `Edit` を押し、 Request URL を ngrok で公開した以下の内容に変更します。

```
https://xxxxxxxx.ngrok.io/dev/slack/events
```

app を追加したチャンネルで `/idea ほげほげ` と打つと、 ボットが `ほげほげ` と返してくれるやまびこアプリができました。

----

#### 3-3. イベント購読設定
チャンネルにメッセージが投稿されたら受け取れるようにしておきます。
サイドメニューの　`Event Subscription` > `Enable Events` を On にし、 `Request URL` に `https://<ngrok のドメイン>/dev/slack/events` を設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/21457937-37c0-d1c1-964a-018f81edd76f.png)

`Subscribe to bot events` に `message.channels` を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/e9f86642-80af-687c-0f04-6275dc9349f4.png)

----

## 4. 機能を実装
### 4-1. 特定の文字列が含まれるメッセージに対して返信をする

「【アイデア】」という文字列が含まれる投稿を受信し、スレッドでリプライします。
（TS の型エラーが出てしまうので、本来はよくない書き方なのかもしれない・・・）

```typescript:handler.ts
boltApp.message('【アイデア】', async ({
  message, logger, say,
}) => {
  try {
    // @ts-ignore
    await say({ text: `<@${message.user}>さん、 アイデアの投稿ありがとう！`, thread_ts: message.ts });
  } catch (e) {
    logger.error(e);
  }
});
```

----

### 4-2. 今週のアイデア投稿をカウントする

`/idea` というスラッシュコマンドを入力したら、そのユーザーの「【アイデア】」という文字列が含まれる今週の投稿をカウントします。
TS の型推論が効かない箇所を補いつつ。

```typescript:handler.ts
boltApp.command('/idea', async ({
  command, logger, ack, say,
}) => {
  const thisMonday = dayjs().day(1).startOf('day');
  try {
    const res = await boltApp.client.conversations.history({
      token: process.env.SLACK_BOT_TOKEN,
      channel: command.channel_id,
      oldest: `${String(thisMonday.unix())}.000000`,
      limit: 100, // 週に100件を超えるアイデアがない前提
    });
    const messages = res.messages as { [key: string]: unknown }[];
    const count = messages
      .filter((message) => message.user === command.user_id)
      .filter((message) => (message.text as string).includes('【アイデア】'))
      .length;
    let text = `<@${command.user_id}>さんの今週のアイデアはまだ投稿されていません。`
      + `${thisMonday.add(6, 'day').format('M月D日(日)')}までに1件のアイデアを投稿しましょう！`;
    if (count > 0) {
      text = `<@${command.user_id}>さんは、${thisMonday.format('M月D日(月)')}から今日までの間に${count}件のアイデアを投稿しました！この調子！`;
    }
    await say(text);
    await ack();
  } catch (e) {
    logger.error(e);
    await ack(`:x: Failed to post a message (error: ${e})`);
  }
});
```

`/idea` を送ると以下のようにメッセージが返ってくるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/135266/742eeac4-138a-af48-e313-c59eb44b0293.png)

----

## 5. AWS にデプロイ

### 5-1. デプロイ

- AWS のダッシュボードから、デプロイ用の IAM ユーザーを作成し、 `AdministratorAccess` 権限を付与します
- `~/.aws/credentials` に上記の IAM ユーザーを設定します
```credentials
  [servelessUser]
  aws_access_key_id=AKIAXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  aws_secret_access_key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

----

ひとまず dev stage にデプロイしてみます
```shell
sls deploy --verbose --stage dev --aws-profile serverlessUser
```

デプロイが成功すると以下のようなログが表示され、自動的に生成された API Gateway 上のエンドポイントも表示されます

```
✔ Service deployed to stack idea-reminder-dev (164s)

endpoint: POST - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/slack/events
functions:
  app: idea-reminder-dev-app (3.5 MB)
```

----

### 5-2. エンドポイントを変更

- 先ほど一時的に設定していた ngrok のエンドポイントを、5-1. で表示された URL に変更しておきます
  - Slash コマンドの Request URL
  - Event Subscriptions の Request URL

----

# まとめ
- 後で書く