13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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

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

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

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

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

目次

概要

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

システム構成

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

システム構成

  • (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編
    Slack APIには、Interactive Messageと呼ばれるメッセージがあります。
    Interactive Messageを使うと、ボタンやメニュー付きのメッセージを投稿できます。
    本稿では、選択肢をボタン形式で表したメッセージを投稿し、選ばれた内容に応じたアクションを実施します。
  • Amazon CloudWatch編
    システム構成図の(4)の部分を作成します。
    Amazon CloudWatchからLambda関数をトリガーします。
    これが実現すると、例えば以下のようなタイミングでLambda関数を実行できます。
    • 特定の日時
    • 毎日・毎週等のタイミング
    • EC2のCPU使用率が50%を超えた時
  • Amazon DynamoDB編
    システム構成図の(5),(6)の部分を作成します。
    Lambda関数は永続情報を持てないため、DynamoDBをデータベースとして利用します。
  • 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マネージメントコンソールへサインイン。
②Lambdaで検索。

1.png

③Lambda関数一覧で、「関数の作成」をクリック。

2.png

④以下の設定で、「関数の作成」をクリック。

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

3.png

4.png

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

5.png

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)
    一度登録したら、このhandle challengeはたぶん不要です。
  • ログに書き込む
    ログへの書き込みは、console.logでできます。
    後で、Amazon CloudWatchからログの内容を確認できます。
    ここでは、リクエストの内容を表すeventの内容をログに書き出しています。
  • 200を返す
    200を返します。bodyは適当。

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

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

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

AWSマネージメントコンソールへサインイン。
②「API Gateway」で検索する。

1.png

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

2.png

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

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

3.png

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

4.png

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

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

6.png

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

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

7.png

8.png

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

9.png

もし、Advanced REST clientを持っていたら、試しにアクセスしてみましょう。

10.png

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

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

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

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

Slack Appにアクセスしてサインイン。
②「Create New App」をクリックして、下記の設定で「Create App」をクリックします。

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

1.png

2.png

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

3.png

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

④Botのホーム画面から、左のサイドバーの「Event Subscriptions」を選択し、「Enable Events」をOnにします。
⑤Request URLに、2.の最後で控えておいた、APIのURLを指定します。
challengeに成功すれば、「Verified」と表示されます。エラーが出た場合はurl_verification eventを確認しましょう。1.の⑤でhandle challengeをちゃんと入れていれば動くはずです。たぶん。。。

4.png

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

5.png

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

6.png

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

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

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

7.png

AWSマネージメントコンソールへサインイン。
⑪Cloud Watchで検索する。

8.png

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

9.png

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

10.png

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

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

11.png

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

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

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

Slack Appにアクセスしてサインイン。
3.で作成したBotを選択します。
③サイドメニューから「OAuth & Permissions」をクリック。
④「Scopes」の「Add an OAuth Scope」を選択し、「chat:write」を追加します。これで、このBotがチャンネルにメッセージを送信できます。1

1.png

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

2.png

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

3.png

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.でメッセージを後から書き換えることをするのですが、その動作にも反応しないようにしています。
  • 関数postMessageは、Slackが提供している下記のAPIへリクエストを送信することで、ユーザーに返信する関数です。
  • 関数sendHttpRequestは、Node.jsの標準モジュール「https」を使ってリクエストを送信する非同期関数です。以下を参考に作りました。
  • (※)9.で、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

5.png

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

6.png

Interactive Message編

5.Interactive Messageを使ってみる

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

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

AWSマネージメントコンソールへサインインし、「Lambda」で検索します。
1.で作成したLambda関数「SlackBotFunction」を選択し、スクリプトのトップレベルに、下記の関数を追加します。

1.png

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
    Blockの例を見たり、実際にいじってみてどうなるかをWeb上で確認できます。
  • Block Kit
    このページを起点にすると、Blockについて色々調べられます。

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

2.png

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

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

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

1.と同様にして、下記の設定でLambda関数を新たに作ります。

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

1.png

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

2.png

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 公式ドキュメント

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.で作成したAPI Gateway「SlackBotAPI」の画面へ移動し、「アクション」の「リソースを作成」をクリックします。

3.png

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

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

4.png

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

5.png

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

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

6.png

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

  • デプロイされるステージ:SlackBotStage(2.の⑧で作成したステージ)
  • デプロイメントの説明:任意

7.png

8.png

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

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

9.png

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

10.png

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

11.png

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

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

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

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

参考:Message responses

1.と同様にして、下記の設定でLambda関数を新たに作ります。

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

1.png

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

2.png

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.で紹介したものと同じです。

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

3.png

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

4.png

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公式ドキュメント)
  • 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

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

6.png

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

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

7.png

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

  • 名前:MyInlinePolicyLambdaInvokeFunction(任意)

8.png

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

9.png

10.png

Amazon CloudWatch編

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

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

3.の⑮のログなどで確認できると思うので、控えておいてください。

1.png

AWSマネージメントコンソールへサインインし、「Lambda」で検索します。

1.と同様にして、以下の設定でLambda関数を作成します。

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

2.png

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

3.png

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.の⑨と同じ環境変数を設定する。

キー
SLACK_POST_MESSAGE_URL https://slack.com/api/chat.postMessage
SLACK_BOT_USER_ACCESS_TOKEN 4.の⑥で控えておいた「Bot User OAuth Access Token」

5.png

AWSマネージメントコンソールから「CloudWatch」で検索し、サイドバーから「イベント」欄の「ルール」を選択する。

6.png

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

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

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

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

7.png

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

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

8.png

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

9.png

Amazon DynamoDB編

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

7.Amazon DynamoDBを作成する

AWSマネージメントコンソールへサインインし、「DynamoDB」で検索する。
②「テーブルの作成」をクリックする。
③以下の設定で「作成」をクリックする。

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

1.png

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

2.png

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

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

3.png

4.png

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

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

AWSマネージメントコンソールへサインインし、「Lambda」で検索する。

1.で作成したLambda関数を選択する。

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

1.png

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.で作成したものと同じです。

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

2.png

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

3.png

4.png

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

5.png

Amazon Lambda Layer編

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

本章では、これらを解決できるLambda Layerについて紹介し、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ファイルをローカルで作成します。

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();
}
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

AWSマネージメントコンソールにサインインして、「Lambda」で検索する。

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

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

2.png

1.で作成したLambda関数のスクリプトを、下記に修正する。

3.png

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

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

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

5.png

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

6.png

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

  1. 以前は何でもできるtokenがもらえたのですが、最近はBot User Access Tokenのみが与えられて、そのtokenで何ができるか(スコープ)をちゃんと決めないといけないらしいです。ボットの OAuth スコープについて

13
10
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?