Slack API×AWS Lambda×Node.jsで作るチャットボット開発

# Slack API×AWS Lambda×Node.jsで作るチャットボット開発

Slack APIをLambdaと組み合わせると、様々なことができます。
「スマホからも操作できるSlackで、Lambda関数を実行できる」というだけで、その応用範囲は多岐にわたります。

今回、SlackとLambdaを使ってチャットボットのサンプルを作る機会があったため、そこで得た知識をまとめ、手順化してみました。
この内容を理解することで、以下のようなことができます。

- Slackのワークスペースにボットを配置する。
- ボットにDMを送ると、AWSのLambda関数を実行するように設定する。
- Lambda関数の処理で、Slackのワークスペースにメッセージを投稿する。

細かく手順を作ったので、初心者も手を付けやすいと思います。
ぜひここで基礎部分を学んでから、自分のやりたいことに応じて調べてみてください。

第9節までありますが、基礎の部分は4節までですし、アカウント作成などが無ければ1章あたり20分くらいでできると思います。

# 目次

- [概要](#概要)
  - [システム構成](#システム構成)
  - [章構成](#章構成)
  - [前提](#前提)
- [基礎編](#基礎編)
  - [1.Lambda関数を作る](#1lambda関数を作る)
  - [2.Amazon API GatewayとLambda関数を接続](#2amazon-api-gatewayとlambda関数を接続)
  - [3.新しいSlack Botを作成する](#3新しいslack-botを作成する)
  - [4.メッセージに返信する](#4メッセージに返信する)
- [Interactive Message編](#interactive-message編)
  - [5.Interactive Messageを使ってみる](#5interactive-messageを使ってみる)
    - [5-1.ボタン付きメッセージを送信する](#5-1ボタン付きメッセージを送信する)
    - [5-2.ボタンが押されたことを受け取る](#5-2ボタンが押されたことを受け取る)
    - [5-3.元のメッセージを変更する](#5-3元のメッセージを変更する)
- [Amazon CloudWatch編](#amazon-cloudwatch編)
  - [6.Amazon CloudWatchからLambda関数を実行する](#6amazon-cloudwatchからlambda関数を実行する)
- [Amazon DynamoDB編](#amazon-dynamodb編)
  - [7.Amazon DynamoDBを作成する](#7amazon-dynamodbを作成する)
  - [8.Lambda関数からDynamoDBを検索する](#8lambda関数からdynamodbを検索する)
- [Amazon Lambda Layer編](#amazon-lambda-layer編)
  - [9.AWS Lambda Layerを使う](#9aws-lambda-layerを使う)

# 概要

本章では、この本稿で説明するシステムの概要と章構成を紹介します。
ビジネス向けのチャットアプリ「Slack」は、Web APIを公開しているため、外部システムと連携させると様々なことができます。
今回、AWSと組み合わせてチャットボットを作成したため、その基本となる仕組みの部分を紹介します。

## システム構成

ざっくりしたシステム構成は以下の通りです。

![システム構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/d2953742-71a3-2295-cc01-587fa97b94c8.png)

- (1)自分のワークスペースで特定のアクションが起こったとき、Slack APIはAmazon API Gatewayにリクエストを送信します。
- (2)リクエストを受け取ったAmazon API Gatewayは、AWS Lambdaを実行します。
- (3)AWS Lambdaは、受け取ったリクエストを処理します。  
もし必要なら、Slack APIのメッセージ投稿APIへリクエストを送信します。
- (4)Amazon CloudWatchから、Lambda関数を実行します。  
これにより、特定の時刻・イベント時にSlackへメッセージを投稿できます。
- (5)AWS Lambdaから、Amazon DynamoDBにクエリを送信します。
- (6)クエリを受け取ったAmazon DynamoDBは、クエリを処理し、結果を返します。  
これにより、永続情報を持てないAWS Lambdaで、DBを使って情報の保存・読み込みができます。

## 章構成

本稿は主に、以下の5つからなります。

- [基礎編](#基礎編)  
システム構成図の(1)、(2)、(3)の部分を作成します。  
ワークスペースにSlack Botを導入します。  
ユーザーがボットにメッセージを投稿すると、その内容に応じて異なるメッセージをBotが投稿します。
- [Interactive Message編](#interactive-message編)  
Slack APIには、Interactive Messageと呼ばれるメッセージがあります。  
Interactive Messageを使うと、ボタンやメニュー付きのメッセージを投稿できます。  
本稿では、選択肢をボタン形式で表したメッセージを投稿し、選ばれた内容に応じたアクションを実施します。
- [Amazon CloudWatch編](#amazon-cloudwatch編)  
システム構成図の(4)の部分を作成します。  
Amazon CloudWatchからLambda関数をトリガーします。  
これが実現すると、例えば以下のようなタイミングでLambda関数を実行できます。
  - 特定の日時
  - 毎日・毎週等のタイミング
  - EC2のCPU使用率が50%を超えた時
- [Amazon DynamoDB編](#amazon-dynamodb編)  
システム構成図の(5),(6)の部分を作成します。  
Lambda関数は永続情報を持てないため、DynamoDBをデータベースとして利用します。
- [Amazon Lambda Layer編](#amazon-lambda-layer編)  
Amazon Lambda Layerを使って、Lambda関数のスクリプトで拡張モジュール「node-fetch」を利用します。

基礎編を終えた後は、ほかの項目はそれぞれ独立しているため、別々に見ることができます。

## 前提

- 以下は事前に用意しておいてください。
  - 色々いじれるAWSアカウント(Lambda、API Gateway、IAM、CloudWatchあたりは必須で、DynamoDB等も必要になります。admin権限があると嬉しい。ただし、ルートアカウントはやめましょう。)
  - 自由にいじれるSlackワークスペース
- 本稿のスクリプトではトークンなど、Httpリクエストに含まれる認証情報をCloudWatchのログに書き出しています。都合が悪ければ、console.logを全て消すなど、対応してください。

# 基礎編

## 1.Lambda関数を作る

本節では、Slack Botから送られてくるHTTPリクエストを受け取って、処理を実行するLambda関数を作成します。
ただし、この段階では中身はそこまで作り込まず、受け取ったリクエストをログに吐き出すだけとします。

①[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインイン。  
②Lambdaで検索。
  
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/0110c11b-4641-2ede-b0e3-572e4fd4674d.png)

③Lambda関数一覧で、「関数の作成」をクリック。 
 
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/f0e49081-6bd4-b230-c023-82ccb28b7b70.png)

④以下の設定で、「関数の作成」をクリック。
- 一から作成
- 関数名：SlackBotFunction(任意)
- ランタイム：Node.js系
- アクセス権：基本的なLambda アクセス権限で新しいロールを作成

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/f1dbeb74-a801-a00b-b8f3-ff46e48c6dd6.png)

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/d547c1b5-871e-ffd1-6586-d12a115656db.png)

⑤Lambda関数が作成されるので、スクリプトを下記に修正して、「保存」をクリック。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/6f454cc1-7e0c-69ee-8b4c-e5a50f83ed45.png)

```javascript:SlackBotFunction
exports.handler = async (event) => {

    // handle challenge
    const challenge = event.challenge;
    if(challenge){
        const body = {
                challenge: challenge
            };
        const response = {
            statusCode: 200,
            body: JSON.stringify(body)
        };
        return response;
    }

    // ログに書き込む
    console.log(JSON.stringify(event));

    // 200を返す。
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;

};
```

少しだけ解説します。

- handle challlenge  
Slack APIからリクエストを送る自作APIを登録するときは、  
「bodyにchallengeが指定されたら、ステータスコード200でchallengeを打ち返す」  
というAPIじゃないと登録できません。  
(参考：[url_verification event](https://api.slack.com/events/url_verification))  
一度登録したら、このhandle challengeはたぶん不要です。
- ログに書き込む  
ログへの書き込みは、console.logでできます。  
後で、Amazon CloudWatchからログの内容を確認できます。  
ここでは、リクエストの内容を表すeventの内容をログに書き出しています。
- 200を返す  
200を返します。bodyは適当。

以上で、Lambda関数の準備はおしまいです。  
次は、これをAmazon API Gatewayとつなげます。

## 2.Amazon API GatewayとLambda関数を接続

本節では、新しくAmazon API Gatewayを作成して、リクエストを受け取ったらLambda関数を実行するように設定します。

①[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインイン。  
②「API Gateway」で検索する。  

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/200cc54c-7d6f-b9a2-927e-b3a4f1452977.png)

③「REST API」の「構築」をクリックします。
(すでにAPIを構築したことがある方は、「APIを作成」をクリックし、「REST API」の「構築」をクリック。)  

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/c766d86e-a82b-ce13-acdf-5633a6f7c91b.png)

④以下の設定で、「APIの作成」をクリックします。

- プロトコル：REST  
(これから作るのはREST APIではないと思うけど...明らかにWeb Socketではないので。)
- 新しいAPIの作成：新しいAPI
- API名：SlackBotAPI(任意)
- 説明：任意
- エンドポイントタイプ：リージョン

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/52786384-a2ea-1870-8c9a-4f11776fcabf.png)

⑤作成したAPIの「リソース」画面へ移動します。  

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/74e42353-d9f3-6778-2f05-056e150fadf0.png)

⑥「/」と書いてあるリソースを選択し、アクションから「メソッドの作成」を選択し、「POST」メソッドを下記の設定で追加します。

- 統合タイプ：Lambda 関数
- Lambdaプロキシ統合の使用：チェックを外す
- Lambdaリージョン：Lambda関数と同じリージョン
- Lambda関数：2.の④で設定したLambda関数の名前(図はSlackBotSample)
- デフォルトタイムアウトの使用：任意(図ではチェック)

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/34cfe19b-ad5f-9f9b-d8af-6d0b6448631d.png)

⑦次に、このAPIをデプロイします。「アクション」の「APIのデプロイ」を選択し、以下の設定でデプロイします。  
※今後、APIの設定を変更した後は、デプロイすることを忘れずに！良く忘れて「この設定でなぜ動かない...」となります...

- デプロイされるステージ：新しいステージ
- ステージ名：SlackBotStage(任意)
- ステージの説明：任意
- デプロイメントの説明：任意

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/dd1d0031-e8d4-8052-bac3-15e0dfa4dd0c.png)

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/231533ca-69f4-c426-cd4d-63063fb509ea.png)

以上で、API Gatewayの設定は完了です。ステージ画面から、APIのURLを控えておきましょう。

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/1e148337-59b8-9ae3-13f4-b965dcd3caf0.png)

もし、[Advanced REST client](https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo/details?hl=ja-JP)を持っていたら、試しにアクセスしてみましょう。

![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/4ed1328d-e4fa-1d88-e550-55466628f3ad.png)

以上で、最低限のAWS側の設定はおしまいです。続いて、Slack側の設定を見ていきます。

## 3.新しいSlack Botを作成する

本節では、SlackのワークスペースにBotを追加して、DMにメッセージを受け取ったときにAPI Gatewayを実行するように設定します。

いよいよSlack側の設定です。流れとしては、「Botを作る」→「API Gatewayと接続する」→「ワークスペースへインストール」となります。

①[Slack App](https://api.slack.com/apps)にアクセスしてサインイン。
②「Create New App」をクリックして、下記の設定で「Create App」をクリックします。

- App Name：AwsSlackBot(任意)
- Development Slack Workspace：自分がいじっていいワークスペースを選択。  
※間違って他人も使うようなワークスペースを選ばないように。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/77ced804-f0e1-2090-cf59-103a9d0cb374.png)

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/dad97f92-99b7-7e9a-5e68-fd69721b30aa.png)

③作成した後の画面が、Slack Botのホーム画面のようなものです。ざっくりと機能紹介です。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/036ff012-1758-f334-21f0-2cce6e9603d7.png)

- Basic Information
  - Display Information  
  ワークスペースでユーザー向けに見えている部分(Botの名前とか)を設定できます。
- Install App  
このBotをワークスペースにインストールします。
- Interactivity & Shortcuts  
Interactive Messageの設定などができます。  
「[5.Interactive Messageを使ってみる](#5interactive-messageを使ってみる)」で利用します。
- OAuth & Permissions  
このBotの権限を編集します。
- Event Subscriptions  
Botがいるワークスペースで、"いつ"、"どこへ"リクエストを送信するか指定します。この後使います。

④Botのホーム画面から、左のサイドバーの「Event Subscriptions」を選択し、「Enable Events」をOnにします。  
⑤Request URLに、[2.](#2amazon-api-gatewayとlambda関数を接続)の最後で控えておいた、APIのURLを指定します。  
challengeに成功すれば、「Verified」と表示されます。エラーが出た場合は[url_verification event](https://api.slack.com/events/url_verification)を確認しましょう。[1.](#1lambda関数を作る)の⑤でhandle challengeをちゃんと入れていれば動くはずです。たぶん。。。  

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/3ab67084-0db7-68b6-ed94-66de825c839c.png)

⑥「Subscribe to bot events」に、DMが投稿された時にリクエストを送信する設定をします。「Add Bot User Event」をクリックして、「message.im」を検索し、選択します。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/6893863d-1405-bb9a-6cd6-a0328f2a6cff.png)

⑦「Save Changes」をクリックして設定を保存します。  
⑧最後に、左のサイドバーから「Install App」を選択して、「Install App to Workspace」をクリックします。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/4744655b-e26d-ff28-63ec-f05b1a5bb836.png)

以上で、Botの設定は完了です。Slackのワークスペースに行くと、Appの中に自分が作成したBotが登録されていると思います。

では、動作確認をしてみましょう。

⑨Slackのワークスペースから自分が作成したBotをクリックし、DMにメッセージを送信します。

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/366aa831-2d32-2b87-1b5a-73cd6d000523.png)

⑩[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインイン。  
⑪Cloud Watchで検索する。

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/27536f48-9eda-ed96-8197-659b489c41bd.png)

⑫サイドバーの「ロググループ」を選択します。  
⑬「/aws/lambda/Lambda関数名」というロググループを選択します。

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/c4141c98-0d72-b0f4-dcca-6501f595f7ec.png)

⑭ログストリームの中から、自分がメッセージを送った時刻のものを選択します。

![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/0800d605-d94d-ea42-5ca9-321e117ea5e9.png)

⑮以下の4つのログが確認できます。(challengeのログも一緒に表示されているかもしれません。)

- START  
デフォルトで出力してくれるログです。
- INFO  
[1.](#1lambda関数を作る)の⑤で作成したスクリプトの、console.log()のログです。受け取ったリクエストの内容が表示されます。
- END  
デフォルトで出力してくれるログです。
- REPORT  
デフォルトで出力してくれるログです。

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/c2abc026-4b2b-45fb-0dd6-f8f9c13d632a.png)

以上で、Slack側の設定です。ここまでで、「SlackのDMメッセージをトリガーとしてLambda関数を実行する」ことができています。  
ここまでできてしまえば、後は「そのLambda関数で何をしたいか？」ということに注力できます。

## 4.メッセージに返信する

本節では、Lambda関数を変更して、ユーザーのDMに返信できるようにします。  
流れとしては、Slackの設定をしてtokenを取得し、Lambda関数を修正してSlack APIにメッセージ投稿のリクエストを送信する処理を追加します。

①[Slack App](https://api.slack.com/apps)にアクセスしてサインイン。  
②[3.](#3新しいslack-botを作成する)で作成したBotを選択します。  
③サイドメニューから「OAuth & Permissions」をクリック。  
④「Scopes」の「Add an OAuth Scope」を選択し、「chat:write」を追加します。これで、このBotがチャンネルにメッセージを送信できます。[^1]

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/d9599df8-4bd8-7741-cc5b-615378bc4341.png)

⑤「Reinstall App」をクリックして、再度インストールします。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/d0f3b4d8-a3d9-7f09-327f-93d9be0dad5c.png)

⑥以上でSlack側の準備は完了です。後で使うため、「OAuth & Permissions」に記載されている「Bot User OAuth Access Token」を控えておいてください。なお、パスワードのようなものなので取り扱いは注意してください。  
⑦[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインインし、[1.](#1lambda関数を作る)で作成したLambda関数のページにアクセスします。  
⑧スクリプトを下記に修正します。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/91533b46-6e62-1dd0-1d8a-2cda854e2733.png)

```javascript:SlackBotFunction
exports.handler = async (event) => {

    // handle challenge
    const challenge = event.challenge;
    if (challenge) {
        const body = {
            challenge: challenge
        };
        const response = {
            statusCode: 200,
            body: JSON.stringify(body)
        };
        return response;
    }
    // ログに書き込む
    console.log(JSON.stringify((event)));

    // Bot自身が投稿したメッセージには、event.event.bot_idが存在する。
    // 後でメッセージ変更を行う時、それにも反応しないようにする。
    if (!event.event.bot_id && event.event.subtype != 'message_changed') {
        // メッセージを書き込む
        await postMessage('メッセージを受け取りました。\r\n' + event.event.text, event.event.channel);
    }

    // 200を返す。
    const response = {
        statusCode: 200,
        body: 'Hello from Lambda!',
    };
    return response;

};

// 指定したchannelに、メッセージを送信する。
async function postMessage(text, channel) {
    const headers = {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + process.env['SLACK_BOT_USER_ACCESS_TOKEN']
    };
    const data = {
        'channel': channel,
        'text': text
    };
    await sendHttpRequest(process.env['SLACK_POST_MESSAGE_URL'], 'POST', headers, JSON.stringify(data));
}

// Httpリクエストを送信する。
async function sendHttpRequest(url, method, headers, bodyData) {
    console.log('sendHttpRequest');
    console.log('url:' + url);
    console.log('method:' + method);
    console.log('headers:' + JSON.stringify(headers));
    console.log('body:' + bodyData);
    const https = require('https');
    const options = {
        method: method,
        headers: headers
    };
    return new Promise((resolve, reject) => {
        let req = https.request(url, options, (res) => {
            console.log('responseStatusCode:' + res.statusCode);
            console.log('responseHeaders:' + JSON.stringify(res.headers));
            res.setEncoding('utf8');
            let body = '';
            res.on('data', (chunk) => {
                body += chunk;
                console.log('responseBody:' + chunk);
            });
            res.on('end', () => {
                console.log('No more data in response.');
                resolve(body);
            });
        }).on('error', (e) => {
            console.log('problem with request:' + e.message);
            reject(e);
        });
        req.write(bodyData);
        req.end();
    });
}
```

ちょっとだけ解説します。

- 「exports.handler」内で、postMessage関数を呼び出すことで、「ユーザーからメッセージを受け取ったら返信する」ということを実現しています。
- ただ、そのまま実行すると「ユーザーが投稿→それに反応してBotが投稿→そのBotの投稿に反応してBotが投稿→...」と、無限ループになってしまうため、if文でBotの投稿には反応しないようにしています。
- また、[5.](#5interactive-messageを使ってみる)でメッセージを後から書き換えることをするのですが、その動作にも反応しないようにしています。
- 関数postMessageは、Slackが提供している下記のAPIへリクエストを送信することで、ユーザーに返信する関数です。
  - [chat.postMessage](https://api.slack.com/methods/chat.postMessage)
- 関数sendHttpRequestは、Node.jsの標準モジュール「https」を使ってリクエストを送信する非同期関数です。以下を参考に作りました。
  - [Node.js公式ドキュメント](https://nodejs.org/api/http.html#http_http_request_url_options_callback)
  - [Node.jsのhttpsモジュールを用いた通信処理をPromiseで書き直して解読してみた](https://ky-yk-d.hatenablog.com/entry/2018/07/16/011748)
- (※)[9.](#9aws-lambda-layerを使う)で、AWS Lambda Layerを使って、「node-fetch」モジュールを使い、もっと簡単にHttpリクエストができるようにしています。

⑨上のスクリプトでは、2つ環境変数を使っています。そこで、これを登録します。  
「環境変数」欄の「編集」をクリックし、環境変数を2つ追加して以下の設定とします。

| キー | 値 |
|:--|:--|
| SLACK_POST_MESSAGE_URL | <ttps://slack.com/api/chat.postMessage> |
| SLACK_BOT_USER_ACCESS_TOKEN | ⑥で控えておいた「Bot User OAuth Access Token」 |

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/4f99a8fb-0157-a3d7-4bb7-fcd17a3ab43e.png)

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/d02175fb-c287-9ff9-6b5b-21c1d0a044a4.png)

⑩以上で設定は完了です。最後に「保存」をクリックしてください。Slackにメッセージを投稿すると、返信してくれるはずです。  
もしうまくいかない場合は、CloudWatchからログを確認してみてください。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/411cb975-4365-0ef6-e742-01622aac605e.png)

# Interactive Message編

## 5.Interactive Messageを使ってみる

本節では、Slack APIのInteractive Messageを使って、ボタン付きメッセージを送ります。ユーザーがボタンを押すと、それに応じてメッセージを変更します。  
ボリュームが多いので、細かく分けて説明します。

- [5-1.ボタン付きメッセージを送信する](#5-1ボタン付きメッセージを送信する)  
[4.](#4メッセージに返信する)のスクリプトを修正して、ボタン付きのメッセージも送信します。
- [5-2.ボタンが押されたことを受け取る](#5-2ボタンが押されたことを受け取る)  
ボタンが押されたら、API Gatewayの別リソースにリクエストを送信するように設定し、そのリクエストをLambda関数で受け取ります。
- [5-3.元のメッセージを変更する](#5-3元のメッセージを変更する)  
[5-2](#5-2ボタンが押されたことを受け取る)でリクエストを受け取ったら、別のLambda関数を非同期で実行し、その中でメッセージの内容を書き換えます。

### 5-1.ボタン付きメッセージを送信する

①[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインインし、「Lambda」で検索します。  
②[1.](#1lambda関数を作る)で作成したLambda関数「SlackBotFunction」を選択し、スクリプトのトップレベルに、下記の関数を追加します。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/dbe07598-be87-3aeb-25c9-8fad8797fc7c.png)

```javascript:SlackBotFunction
async function postInteractiveMessage(channel){
    const headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + process.env['SLACK_BOT_USER_ACCESS_TOKEN']
    };
    const data = {
        'channel':channel,
        'blocks':[
            {
                'type':'section',
                'text':{
                    'type':'plain_text',
                    'text':'InteractiveMessageのテスト'
                }
            },
            {
                'type':'actions',
                'elements':[
                    {
                        'type':'button',
                        'text':{
                            'type':'plain_text',
                            'text':'select1'
                        },
                        'value':'click1'
                    },
                    {
                        'type':'button',
                        'text':{
                            'type':'plain_text',
                            'text':'select2'
                        },
                        'value':'click2'
                    },
                    {
                        'type':'button',
                        'text':{
                            'type':'plain_text',
                            'text':'select3'
                        },
                        'value':'click3'
                    },
                ]
            }
        ],
    }
    await sendHttpRequest(process.env['SLACK_POST_MESSAGE_URL'], 'POST', headers, JSON.stringify(data));
}
```

ちょっと解説…といっても、リクエストのdataにblockというものを追加して、中身を色々いじっただけです。この辺は、下記を参考にすると色々作れます。

- [Block Kit Builder](https://api.slack.com/tools/block-kit-builder)  
Blockの例を見たり、実際にいじってみてどうなるかをWeb上で確認できます。
- [Block Kit](https://api.slack.com/block-kit)  
このページを起点にすると、Blockについて色々調べられます。

②同じスクリプトの中で、exports.handler内のメッセージを書き込む部分を、下記のように変更します。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/456c2dde-7698-2eaf-5056-164f6c4a5bf4.png)

```javascript:SlackBotSample.js
    // Bot自身が投稿したメッセージには、event.event.bot_idが存在する。
    // 後でメッセージ変更を行う時、それにも反応しないようにする。
    if (!event.event.bot_id && event.event.subtype != 'message_changed') {
        // メッセージを書き込む
        await postMessage('メッセージを受け取りました。\r\n' + event.event.text, event.event.channel);
        // 【追加】InteractiveMessageを書き込む
        await postInteractiveMessage(event.event.channel);
    }
```

③以上で完成です。試しに、ワークスペースでBotのDMあてにメッセージを送ってみましょう。ボタン付きのメッセージが送られてくるはずです。ただし、ボタンを押しても何も反応しませんが…

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/503746fe-005a-24e7-4ac9-7057ef69a65f.png)

### 5-2.ボタンが押されたことを受け取る

ここでは、ボタンが押されたことを受け取るために、新しくLambda関数とAPIの受け口を作ります。

①[1.](#1lambda関数を作る)と同様にして、下記の設定でLambda関数を新たに作ります。

- 一から作成
- 関数名：SlackBotCatchInteractiveMessageFunction(任意)
- ランタイム：Node.js系
- アクセス権：基本的なLambda アクセス権限で新しいロールを作成

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/f31901de-08a3-db7f-f507-17499f06fa52.png)

②作成したLambda関数のスクリプトを下記に変更する。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/947d7981-15c3-a285-477e-9a1fde6c9d5f.png)

```javascript:SlackBotCatchInteractiveMessage.js
exports.handler = async (event) => {

    console.log(JSON.stringify(event));

    const querystring = require('querystring');
    const body = querystring.parse(event.body);
    const payload = JSON.parse(body['payload']);
    console.log(payload);

    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;

};
```

※Interactive Messageのリクエストはx-www-form-urlencodedで送られてきます。API Gatewayでも処理できますが、ここでは上記のJavaScriptで頑張って処理します。  
※なんでか分からないのですが、「body.payload」はだめでした。querstringの仕様？

[querystring.parse 公式ドキュメント](https://nodejs.org/api/querystring.html#querystring_querystring_parse_str_sep_eq_options)
>The object returned by the querystring.parse() method does not prototypically inherit from the JavaScript Object. This means that typical Object methods such as obj.toString(), obj.hasOwnProperty(), and others are not defined and will not work.

③API Gatewayにリソースを追加します。[2.](#2amazon-api-gatewayとlambda関数を接続)で作成したAPI Gateway「SlackBotAPI」の画面へ移動し、「アクション」の「リソースを作成」をクリックします。  

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/2080026f-7ebf-90c7-1ab6-3dc9d02d84e9.png)

④下記の設定で「リソースの作成」をクリックして、リソースを作成します。

- プロキシリリースとして設定する：チェックを外す
- リソース名：interactivemessage(任意)
- リソースパス：interactivemessage(任意)
- API Gateway CORSを有効にする：チェックを外す

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/4fa0ad70-303e-b734-27d1-07ead89a8ca8.png)

⑤作成したリソースを選択し、「アクション」の「メソッドを作成」をクリックします。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/3aaba9fa-6509-d059-61cd-3d57405aaebe.png)

⑥下記の設定で「保存」をクリックします。

- 結合タイプ：Lambda関数
- Lambdaプロキシ統合の使用：チェックをつける
- Lambdaリージョン：Lambda関数を作成したリージョン
- Lambda関数：SlackBotCatchInteractiveMessageFunction(①で作成したLambda関数)
- デフォルトタイムアウトの使用：チェックをつける

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/165afb48-1550-831a-4743-789c6da3b084.png)

⑦API Gatewayの設定を変更したので、デプロイします。「アクション」から「APIのデプロイ」を選択し、下記の設定でデプロイを押します。

- デプロイされるステージ：SlackBotStage([2.](#2amazon-api-gatewayとlambda関数を接続)の⑧で作成したステージ)
- デプロイメントの説明：任意

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/7da4069c-6263-61ae-bdd4-32f0177283a5.png)

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/f08dfe84-1a16-4e6a-0ee6-a5134c8bcb29.png)

⑧以上でAWS側の設定は終わりです。最後に、新しいリソースのURLを控えておいてください。新しいリソースのURLは、**「APIのURL」/「⑥のリソースパスに設定した値」**です。

⑨続いて、Slack側の設定に移ります。[Slack App](https://api.slack.com/apps)にアクセスしてサインインします。  
⑩サイドバーから「Interactivity & Shortcuts」を選択し、「Interactivity」をOnにします。  
⑪「Request URL」に、⑧で控えておいたInteractive Message用のリソースのURLを設定します。  

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/267f0144-468d-7b96-fb08-666f8ede90f8.png)

⑫最後に「Save Changes」をクリックしておしまいです。  
⑬ここからは、動作確認です。ワークスペースでBotにDMを送って、ボタン付きメッセージを送ってもらい、そのボタンをクリックします。

![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/cb9ec0f7-743e-3b49-ce04-67a9b7adf12b.png)

⑭[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインインし、「CloudWatch」で検索します。
⑮サイドバーの「ロググループ」を選択します。  
⑯「/aws/lambda/Lambda関数名」というロググループを選択する。  
⑰ログストリームの中から、自分がメッセージを送った時刻のものを選択します。  
⑱以下の5つのログが確認できます。

![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/d335c458-01f3-4194-2e15-dbb689402b3f.png)

- START  
デフォルトで出力してくれるログです。
- INFO  
②で作成したスクリプトの、console.log()のログです。event引数の内容が表示されます。(めちゃめちゃ長いですが、必要なのはbodyだけです。)
- INFO  
②で作成したスクリプトの、console.log()のログです。query形式のbodyを整形して目的のオブジェクト「payload」を抽出しています。「message」プロパティにはアクションのもととなるメッセージの情報が、「actions」にはユーザーがとったアクションの情報が載っています。
- END  
デフォルトで出力してくれるログです。
- REPORT  
デフォルトで出力してくれるログです。

### 5-3.元のメッセージを変更する

3つ目のLambda関数を作成して、ボタンクリックに対してこちらからレスポンスをします。
やり方は簡単です。[5-2.](#5-2ボタンが押されたことを受け取る)の「console.log(payload)」を見ると、その中に「response_url」というものがあります。このURLに変更したいテキストをbody`{"text":"変更したいテキスト"}`でリクエストするだけです。

このくらいの処理だったら、リクエストを受けたLambda関数内で処理しても問題ないです。
ただし、もっと重い処理をしたいケースではリクエストを受けた後はいったん200を返して非同期でリクエストを処理する必要があります。
今回は、リクエストを受けるLambda関数とは別に、リクエストの内容を処理（今回はresponse_urlに変更したいテキストを送信する部分）を行うLambda関数を作成します。
他にも、Amazon SQSを使うとかいろいろあるみたいなのですが、難しかったので断念しました。

参考：[Message responses](https://api.slack.com/interactivity/handling#message_responses)

①[1.](#1lambda関数を作る)と同様にして、下記の設定でLambda関数を新たに作ります。

- 一から作成
- 関数名：SlackBotHandleInteractiveMessageFunction(任意)
- ランタイム：Node.js系
- アクセス権：基本的なLambda アクセス権限で新しいロールを作成

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/207a4c6b-6b05-ada3-a02f-5261f2f6440d.png)

②作成したLambda関数のスクリプトを下記に変更する。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/01bb0e4e-023b-4964-e275-d4765ab184ab.png)

```javascript:SlackBotHandleInteractiveMessageFunction
exports.handler = async (event) => {

    console.log(event);
    await responseMessage(event.response_url, event.text);
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;

};

async function responseMessage(url,text){

    const headers = {
        'Content-Type': 'application/json'
    };
    const data ={
        'text':text
    };
    await sendHttpRequest(url,'POST',headers,JSON.stringify(data));

}

async function sendHttpRequest(url, method, headers, bodyData) {

    console.log('sendHttpRequest');
    console.log('url:' + url);
    console.log('method:' + method);
    console.log('headers:' + JSON.stringify(headers));
    console.log('body:' + bodyData);
    const https = require('https');
    const options = {
        method: method,
        headers: headers
    };

    return new Promise((resolve, reject) => {
        let req = https.request(url, options, (res) => {
            console.log('responseStatusCode:' + res.statusCode);
            console.log('responseHeaders:' + JSON.stringify(res.headers));
            res.setEncoding('utf8');
            let body = '';
            res.on('data', (chunk) => {
                body += chunk;
                console.log('responseBody:' + chunk);
            });
            res.on('end', () => {
                console.log('No more data in response.');
                resolve(body);
            });
        }).on('error', (e) => {
            console.log('problem with request:' + e.message);
            reject(e);
        });
        req.write(bodyData);
        req.end();
    });

}
```

かるく解説します。

- event引数で、textとresponse_urlを受け取ります。
- responseMessage関数は、response_urlにリクエストを送信して、テキストをtextに変更します。
- sendHttpRequest関数は[4.](#4メッセージに返信する)で紹介したものと同じです。

③後で利用するため、①で作成したLambda関数のARNを控えておいてください。ARNはLambda関数の画面右上から取得できます。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/57167c8c-5eb1-c1bc-4a67-26583de5b659.png)

④続いて、[5-2.](#5-2ボタンが押されたことを受け取る)で作成したInteractive Message受け取り用Lambda関数を編集します。スクリプトを、下記に変更します。ただし、スクリプト中の「★呼び出すLambda関数名(ARN)★」のところは、各自で変更してください。③で控えておいたARNです。

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/2c4eccc9-0698-4461-dd5c-2b111350f5b0.png)

```javascript:SlackBotCatchInteractiveMessageFunction
const AWS = require('aws-sdk');

exports.handler = async (event) => {

    console.log(JSON.stringify(event));

    const querystring = require('querystring');
    const body = querystring.parse(event.body);
    const payload = JSON.parse(body['payload']);
    console.log(payload);

    // lambda関数を非同期実行
    const lambda = new AWS.Lambda();

    const selectedValue = payload.actions[0].value;
    const response_url = payload.response_url;
    const requestEvent = {
        'text': selectedValue,
        'response_url':response_url
    }

    const functionName = 'arn:aws:lambda:******'; //★呼び出すLambda関数名(ARN)★
    await invokeLambda(functionName,'Event',JSON.stringify(requestEvent));

    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;

};

async function invokeLambda(functionName, invocationType, payload){
    const lambda = new AWS.Lambda();
    console.log('invoke lambda');
    console.log('functionName:' + functionName);
    console.log('invocationType:' + invocationType);
    console.log('payload:' + payload);
    const param = {
        FunctionName: functionName,
        InvocationType: invocationType,
        Payload: payload
    }
    return new Promise((resolve, reject) => {
        lambda.invoke(param, (err, data) => {
            if (err) {
                console.log(err, err.stack);
                reject(err);
            } else {
                console.log(data);
                resolve(data);
            }
        });
    });
}
```

難しいので自信がないのですが…解説します。

- invokeLambda関数はAWSが提供しているモジュールの機能を使いやすくするためにラップしたものです。  
(参考：[Lambda.invoke公式ドキュメント](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Lambda.html#invoke-property))
- invokeLambda関数の引数は、以下の意味です。
    - functionName = 'arn:aws:lambda:******'  
    実行するLambda関数の名前(ARN)です。自分で書き換えてください。
    - invocationType  
    文字列'Event'を指定すると、Lambdaを非同期で実行します。invokeLambdaはPromiseオブジェクトを返しますが、これは、「Lambda関数を実行してください」という要求を出し終わるまでは待ちますが、「実際にLambda関数を実行し、結果がわかる」までは待ちません。  
    'RequestResponse'を指定すると、「実際にLambda関数を実行し、結果がわかる」まで待ちます。
    - payload = JSON.stringify(requestEvent)  
    以下のkey-valueを持ったオブジェクトのJSON文字列です。実行するLambda関数の引数「event」に移ります。
        - text  
        変更後のテキスト。ここでは選ばれたボタンのvalue値を使っています。
        - response_url  
        テキストの変更を行うためのリクエスト送信先url。

⑤「保存」ボタンをクリックします。

⑥このままだと、lambda.invokeを実行する権限がないので、権限を変更します。「アクセス権限」をクリックして、ロール名のリンクをクリックします。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/82eaeae8-af0b-36ce-c9d1-a4e421bdbe99.png)

⑦「インラインポリシーの追加」をクリックします。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/e069f0ac-b04b-b464-da75-d8ff09374038.png)

⑧以下の設定で「ポリシーの確認」をクリックします。

- サービス：Lambda
- アクション：InvokeFunction
- リソース：どっちでも。「指定」の場合は、③で控えておいたARNを指定してください。
- リクエスト条件
  - MFAが必須：チェックを外す
  - 送信元IP：チェックを外す

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/a23f6494-77f4-5172-1541-3cab71107adb.png)

⑨以下の設定で「ポリシーの作成」をクリックします。

- 名前：MyInlinePolicyLambdaInvokeFunction(任意)

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/46ca5840-17a6-34f3-aab8-6b87eb96daa8.png)

以上で完了です。ワークスペースからメッセージを投稿し、ボタンを押してみましょう。メッセージが切り替われば成功です。(ちょっと時間がかかります。)

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/0a6f7e47-f03c-37b8-1faa-df466809f0f1.png)

![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/7b8e3a56-ac70-9815-3c05-cbd07205e496.png)

# Amazon CloudWatch編

## 6.Amazon CloudWatchからLambda関数を実行する

本節では、Amazon CloudWatchからLambda関数を実行して、10分ごとにワークスペースへメッセージを送る設定をします。  
その際、投稿先チャンネルのchannelIdが必要です。

[3.](#3新しいslack-botを作成する)の⑮のログなどで確認できると思うので、控えておいてください。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/fb6eb348-1330-52d3-4416-8f4b6593a9cf.png)

①[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインインし、「Lambda」で検索します。

②[1.](#1lambda関数を作る)と同様にして、以下の設定でLambda関数を作成します。

- 一から作成
- 関数名：任意(図はSlackBotPostMessage10Minutes)
- ランタイム：Node.js系
- アクセス権：基本的なLambda アクセス権限で新しいロールを作成

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/b327990f-d64f-e92d-ae6a-c3e2652495f7.png)

③スクリプトを、下記に変更。ただし、★各自変更の部分は、postMessageの第2引数を最初に控えておいたchannelIdに変更すること。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/caf8cb96-e7fa-be5f-6628-37af039cc6a4.png)

```javascript:SlackBotPostMessage10Minutes.js
exports.handler = async (event) => {
    await postMessage('10分送信テスト','AABBCCDD'); //★第2引数'AABBCCDD'を、各自で変更
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};

// 指定したchannelに、メッセージを送信する。
async function postMessage(text, channel) {
    const headers = {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + process.env['SLACK_BOT_USER_ACCESS_TOKEN']
    };
    const data = {
        'channel': channel,
        'text': text
    };
    await sendHttpRequest(process.env['SLACK_POST_MESSAGE_URL'], 'POST', headers, JSON.stringify(data));
}

// Httpリクエストを送信する。
async function sendHttpRequest(url, method, headers, bodyData) {
    console.log('sendHttpRequest');
    console.log('url:' + url);
    console.log('method:' + method);
    console.log('headers:' + JSON.stringify(headers));
    console.log('body:' + bodyData);
    const https = require('https');
    const options = {
        method: method,
        headers: headers
    };
    return new Promise((resolve, reject) => {
        let req = https.request(url, options, (res) => {
            console.log('responseStatusCode:' + res.statusCode);
            console.log('responseHeaders:' + JSON.stringify(res.headers));
            res.setEncoding('utf8');
            let body = '';
            res.on('data', (chunk) => {
                body += chunk;
                console.log('responseBody:' + chunk);
            });
            res.on('end', () => {
                console.log('No more data in response.');
                resolve(body);
            });
        }).on('error', (e) => {
            console.log('problem with request:' + e.message);
            reject(e);
        });
        req.write(bodyData);
        req.end();
    });
}
```

④[4.](#4メッセージに返信する)の⑨と同じ環境変数を設定する。

| キー | 値 |
|:--|:--|
| SLACK_POST_MESSAGE_URL | <https://slack.com/api/chat.postMessage> |
| SLACK_BOT_USER_ACCESS_TOKEN | [4.](#4メッセージに返信する)の⑥で控えておいた「Bot User OAuth Access Token」 |

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/663ca340-550d-efcd-7a38-290e08251373.png)

⑤[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)から「CloudWatch」で検索し、サイドバーから「イベント」欄の「ルール」を選択する。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/cc309dbb-3c8b-3f77-3c6b-90c020ef0f38.png)

⑥「ルールの作成」をクリックし、下記の設定を行う。

- スケジュール：チェック
- Cron式：チェック
  - Cron式の内容：`0/10 * * * ? *`  
  このCron式は、「0分をスタートとして10分ごとに実行」という意味。

⑦「ターゲットの追加」をクリックし、以下の設定をして「設定の詳細」をクリック

- Lambda関数を選択
- 関数：①で作成したLambda関数を選択する。
- バージョン/エイリアスの設定：デフォルト
- 入力の設定：任意(Lambda関数で使っていない)

![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/8e6ae7b4-77e3-55aa-af52-cae954e510bc.png)

⑧以下の設定で「ルールの作成」をクリック

- 名前：SlackBot10MinutesRule(任意)
- 説明：任意
- 状態：有効化にチェックをつける

![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/c3cda399-d02e-4831-24c3-899163680983.png)

以上で完了です。10分毎に自動でメッセージが届きます。止めたい場合は、④のイベント欄のルールを選択した状態から、作成したルールを選択して「アクション」の「無効化」又は「削除」を選択します。

![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/acc934e1-bb05-8dab-2eb0-df8913e6d8f5.png)

# Amazon DynamoDB編

本章では、Amazon DynamoDBをLambda関数のデータベースとして利用します。あらかじめDynamoDBにユーザー情報を登録しておき、Lambda関数から検索できるようにします。

## 7.Amazon DynamoDBを作成する

①[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインインし、「DynamoDB」で検索する。  
②「テーブルの作成」をクリックする。  
③以下の設定で「作成」をクリックする。

- テーブル名：slackbot_user(任意)
- プライマリキー：user_id(文字列を選択)
- デフォルト設定の使用：チェックをつける

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/15a1f514-2c71-741c-5df0-b650c44c00dc.png)

④作成したテーブルを選択し、「項目」タブから「項目の作成」をクリックします。

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/59f1124f-347e-e1a3-2fe3-c423a537ecf2.png)

⑤プラスボタンで3項目を追加して、以下の3ユーザーをそれぞれ登録します。(毎回カラムを2つ追加する必要があります。)

| user_id | user_name(string) | department(string) |
|:--|:--|:--|
| 001 | 太郎 | 部署A |
| 002 | 次郎 | 部署A |
| 003 | 三郎 | 部署B |

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/5fb74857-764b-6607-56ba-7db88a0536a7.png)

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/b8a6c459-91f5-ac0e-ad42-734c3c8c7e1f.png)

以上でDBの設定は完了です。続いて、Lambda関数からこのDBを検索します。

## 8.Lambda関数からDynamoDBを検索する

①[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)へサインインし、「Lambda」で検索する。

②[1.](#1lambda関数を作る)で作成したLambda関数を選択する。

③スクリプトを下記に変更する。ただし、「★各自で変更」というコメントがついている部分は、[7.](#Amazon-DynamoDBを作成する。)で作成したテーブル名に応じて変更する。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/9eb0481e-ae3c-7889-3657-e61a7421a9db.png)

```javascript:SlackBotFunction
const AWS = require('aws-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient({});

exports.handler = async (event) => {

    // handle challenge
    const challenge = event.challenge;
    if (challenge) {
        const body = {
            challenge: challenge
        };
        const response = {
            statusCode: 200,
            body: JSON.stringify(body)
        };
        return response;
    }
    // ログに書き込む
    console.log(JSON.stringify((event)));

    // Bot自身が投稿したメッセージには、event.event.bot_idが存在する。
    // 後でメッセージ変更を行う時、それにも反応しないようにする。
    if (!event.event.bot_id && event.event.subtype != 'message_changed') {
        // メッセージを書き込む
        // await postMessage('メッセージを受け取りました。\r\n' + event.event.text, event.event.channel);
        const channel = event.event.channel;
        const responseText = await makeResponseText(event.event.text);
        await postMessage(responseText, channel);
    }

    // 200を返す。
    const response = {
        statusCode: 200,
        body: 'Hello from Lambda!',
    };
    return response;

};

async function makeResponseText(receiveText){
    let responseText;
    if(receiveText.substring(0,6) === 'search'){
        const user_id = receiveText.substring(6);
        const searchResult = await searchUser(user_id);
        const user = searchResult.Item;
        if(user){
            responseText = `user_id:${user_id}\r\nuser_name:${user.user_name}\r\ndepartment:${user.department}`;
        } else {
            responseText = `user_id:${user_id}が存在しません。`;
        }
    } else {
        responseText = 'search[user_id]で検索ができます。';
    }
    return responseText;
}

async function searchUser(user_id){
    const tableName = 'slackbot_user' //★各自で変更
    const param = {
        TableName: tableName,
        Key:{
            user_id:user_id
        }
    };
    return new Promise((resolve,reject) => {
        dynamo.get(param, (err,data) => {
            if(err){
                console.log(err);
                reject(err);
            } else {
                console.log(data);
                resolve(data);
            }
        });
    });
}

// 指定したchannelに、メッセージを送信する。
async function postMessage(text, channel) {
    const headers = {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + process.env['SLACK_BOT_USER_ACCESS_TOKEN']
    };
    const data = {
        'channel': channel,
        'text': text
    };
    await sendHttpRequest(process.env['SLACK_POST_MESSAGE_URL'], 'POST', headers, JSON.stringify(data));
}

// Httpリクエストを送信する。
async function sendHttpRequest(url, method, headers, bodyData) {
    console.log('sendHttpRequest');
    console.log('url:' + url);
    console.log('method:' + method);
    console.log('headers:' + JSON.stringify(headers));
    console.log('body:' + bodyData);
    const https = require('https');
    const options = {
        method: method,
        headers: headers
    };
    return new Promise((resolve, reject) => {
        let req = https.request(url, options, (res) => {
            console.log('responseStatusCode:' + res.statusCode);
            console.log('responseHeaders:' + JSON.stringify(res.headers));
            res.setEncoding('utf8');
            let body = '';
            res.on('data', (chunk) => {
                body += chunk;
                console.log('responseBody:' + chunk);
            });
            res.on('end', () => {
                console.log('No more data in response.');
                resolve(body);
            });
        }).on('error', (e) => {
            console.log('problem with request:' + e.message);
            reject(e);
        });
        req.write(bodyData);
        req.end();
    });
}
```

ちょっとだけ解説。

- 冒頭でAWS.DynamoDB.DocumentClientのインスタンスを作成します。
- searchUser関数は、DynamoDBのテーブルを検索してユーザー情報を取得する関数です。使いやすくするためにPromiseで囲ってます。
- sendHttpRequest関数とpostMessage関数は、[4.](#4メッセージに返信する)で作成したものと同じです。

④DynamoDBへのアクセス権を設定します。「アクセス権」タブから実行ロール欄のロール名のリンクをクリックします。  

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/b804aae5-4fed-ce76-4128-da8a471752f3.png)

⑤「ポリシーをアタッチします」をクリックし、「AmazonDynamoDBReadOnlyAccess」で検索してチェックを入れ、「ポリシーのアタッチ」をクリックします。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/51d46926-6619-4359-2a02-a840ce8d4573.png)

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/e106b248-12f3-cb3b-e374-26f690e4be98.png)

⑥以上で完了です。以下のようなメッセージをワークスペースから投稿してみましょう。

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/4af4839c-f446-b6e4-cc7a-49e0c7077df7.png)

# Amazon Lambda Layer編

Lambdaでは通常のNode.js開発のように「npm install」で気軽にモジュールをインストールすることができません。また、複数のLambda関数で共通の関数を使いたいときに、Lambda関数ごとに書かなければならず、メンテナンスが大変です。

本章では、これらを解決できるLambda Layerについて紹介し、[4.](#4メッセージに返信する)のスクリプト中のHTTPリクエスト送信部分を「https」から「node-fetch」に変更します。

※最初は「request-promise」で検討していたのですが、なぜかrequireに3.5秒ほどかかるうえ、deprecatedになったらしいのでnode-fetchにします。

## 9.AWS Lambda Layerを使う

①ローカルで、「npm init -y」、「npm install node-fetch」を実行してnode_modulesを用意します。

②以下のjsファイルをローカルで作成します。

```javascript:SendHttpRequest.js
const fetch = require('node-fetch');
module.exports = async function (url, method, headers, bodyData) {
    console.log('sendHttpRequest');
    console.log('url:' + url);
    console.log('method:' + method);
    console.log('headers:' + JSON.stringify(headers));
    console.log('body:' + bodyData);
    const options = {
        method: method,
        headers: headers,
        body: bodyData
    };
    const response = await fetch(url, options);
    return response.json();
}
```

```javascript:PostMessage.js
const sendHttpRequest = require('./SendHttpRequest');
module.exports = async function (text, channel) {
    const headers = {
        'Content-Type': 'application/json; charset=UTF-8',
        'Authorization': 'Bearer ' + process.env['SLACK_BOT_USER_ACCESS_TOKEN']
    };
    const data = {
        'channel': channel,
        'text': text
    };
    await sendHttpRequest(process.env['SLACK_POST_MESSAGE_URL'], 'POST', headers, JSON.stringify(data));
}
```

③以下のフォルダ構成でnodejsフォルダごとzip化し、「nodejs.zip」を用意する。

- nodejs
  - node_modules  
    - ①でinstallしたモジュールが入ったnode_modulesを配置
  - SendHttpRequest.js
  - PostMessage.js

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/d369c9fd-a4b8-120a-2445-4915edc089d2.png)

④[AWSマネージメントコンソール](https://console.aws.amazon.com/console/home)にサインインして、「Lambda」で検索する。

⑤「レイヤー」から「レイヤーの作成」をクリックし、下記の設定で作成する。

- 名前：SlackBotLayer(任意)
- 説明：任意
- .zipファイルをアップロードにチェック
- アップロード：③で作成した「nodejs.zip」をアップロード
- 互換性のあるランタイム：Lambda関数作成時に選んだランタイムと同じものを選択。
- ライセンス：空欄

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/3f897a34-96c4-93c9-5ffd-16d22aa1add7.png)

⑥[1.](#1lambda関数を作る)で作成したLambda関数のスクリプトを、下記に修正する。

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/54655603-7e77-ebe2-5520-7056c06857e5.png)

```javascript:SlackBotFunction
const postMessageUseLayer = require('/opt/nodejs/PostMessage');
exports.handler = async (event) => {

    // handle challenge
    const challenge = event.challenge;
    if (challenge) {
        const body = {
            challenge: challenge
        };
        const response = {
            statusCode: 200,
            body: JSON.stringify(body)
        };
        return response;
    }
    // ログに書き込む
    console.log(JSON.stringify((event)));

    // Bot自身が投稿したメッセージには、event.event.bot_idが存在する。
    // 後でメッセージ変更を行う時、それにも反応しないようにする。
    if (!event.event.bot_id && event.event.subtype != 'message_changed') {
        await postMessageUseLayer('メッセージを受け取りました。\r\n' + event.event.text, event.event.channel);
    }

    // 200を返す。
    const response = {
        statusCode: 200,
        body: 'Hello from Lambda!',
    };
    return response;

};
```

⑦画面上側の「Layers」をクリックし、「レイヤーの追加」を選択します。

![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/818dc342-4d03-ef6d-e286-75025831057b.png)

⑧以下の設定で「追加」をクリックします。

- 名前：SlackBotLayer(⑤で設定した名前)
- バージョン：1

![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/0752e1f8-e822-b62e-d739-6202130300bd.png)

⑨BotにDMを送信し、戻ってくることを確認します。

![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/462024/8493e953-391e-79a2-ea62-f7f542850b84.png)

## 作業中に知った無関係な小ネタ

- 「Windows」 + 「Alt」 + 「PrintScreen」で、user\Videos\Capturesにアクティブウィンドウのキャプチャをpng保存する。
- [Qiita Markdownのページ内リンクの罠](https://qiita.com/hennin/items/7ee58dd7d7c013a23be7)

[^1]: 以前は何でもできるtokenがもらえたのですが、最近はBot User Access Tokenのみが与えられて、そのtokenで何ができるか（スコープ）をちゃんと決めないといけないらしいです。[ボットの OAuth スコープについて](https://api.slack.com/lang/ja-jp/understanding-oauth-scopes-bot)