AWS Lambdaを使用したLINEbotの作成

# ①LINE botの作成
LINE botを作成するには[LINE Developers](https://developers.line.biz/ja/)への登録が必要です。
公式サイトにて登録を完了すると下記のような画面になります。ここから3つの作業を行います。
![line_1.png](https://qiita-image-store.s3.amazonaws.com/0/372054/596f2d56-3d80-e026-b179-4d26b9295e68.png)


### ① 新規プロバイダの作成
* 画面左側メニューから、新規プロバイダの作成を選択し、プロバイダを作成します。

### ② 新規チャネルの作成
* 作成したプロバイダをクリックし、新しくチャネルを作成します。（使用するAPIはMessaging API）

### ③ 外部連携の設定
* ChannelSecretの取得
    * Lambdaとの連携に必要なので、ChannelSecretをメモしておきます。
![line_3.png](https://qiita-image-store.s3.amazonaws.com/0/372054/36a9a688-2426-ee8c-8c43-c9a65fc3a607.png)

* アクセストークン・Webhook送信の利用許可設定
    * 今回の連携ではWebhook送信を利用ため、Webhook送信を「利用する」に設定します。
    * アクセストークンもメモしておきます。
![line_2.png](https://qiita-image-store.s3.amazonaws.com/0/372054/054dc4a0-9bfb-aba4-6dab-91f6ad12bc05.png)

# ②Lambdaで動かすコードの作成
LINEで受けとったデータをAWS Lambdaで処理するためのコードを記述していきます。

## 作成したコード
* LINEとの受け答え部分のwebhook.jsに記述し、一部の関数はutils.jsに分離させています。
    * ※コードは[おじさんLINEごっこBOTを作ってみた - PPT Developers' Blog](https://persol-pt.github.io/posts/line-ojisan-bot/)を参考にさせていただきました。
* 環境はNode.js 6以上を想定しています。

```js:webhook.js
const utils = require('utils');
const line = require('@line/bot-sdk');
//const request = require('request-promise');
const Line = new line.Client({
  channelAccessToken: process.env.LINE_ACCESS_TOKEN,
  channelSecret: process.env.LINE_CHANNEL_SECRET,
});
const AWS = require('aws-sdk');

exports.handler = (event, context, callback) => {
  try {
    let body = null;
    if (typeof event.body === "string" || Buffer.isBuffer(event.body)) {
      body = utils.validateSignature(
        event.body,
        process.env.LINE_CHANNEL_SECRET,
        event.headers['X-Line-Signature']
      );
    }

    if (body === null) {
      throw new Error('body parsing failed');
    }

    body.events.forEach((webhookData) => {
      const replyToken = webhookData.replyToken;
      const msgEvtType = webhookData.type;
      const timeStamp = webhookData.timestamp;
      const userId = webhookData.source.userId;

      switch (msgEvtType) {
        case 'follow': // 友達追加時
          Line.getProfile(userId).then((profileData) => {
            const displayName = profileData.displayName;
            const replyMessage = utils.welcomeMessage(displayName);
            Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
          });
          break;
        case 'unfollow': // ブロック時
          break;
        case 'message': // メッセージ送信時
          Line.getProfile(userId).then((profileData) => {
            const displayName = profileData.displayName;
            const messageType = webhookData.message.type;
            let replyMessage = "";

            switch (messageType) {
              // switch文で送信メッセージの種類ごとにBOTの処理を分けられる
              case 'text': // テキストメッセージ
                replyMessage = webhookData.message.text;
                break;
              case 'image': // 画像
                replyMessage = '画像ですね。';
                Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
                break;
              case 'video': // 動画
                replyMessage = '動画ですね。';
                Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
                break;
              case 'audio': // 音声
                replyMessage = '音声ですね。';
                Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
                break;
              case 'file': // ファイル
                replyMessage = 'ファイルですね。';
                Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
                break;
              case 'location': // 位置情報
                replyMessage = '位置情報ですね。';
                Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
                break;
              case 'sticker': // スタンプ
                replyMessage = 'スタンプですね。';
                Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
                break;
            }
          });
          break;
        case 'postback': // ポストバックイベント受信時
          Line.getProfile(userId).then((profileData) => {
            const displayName = profileData.displayName;
            const replyToken = webhookData.replyToken;
            const pbData = JSON.parse(webhookData.postback.data);
            let replyMessage = "";

            const pbType = pbData.PBType;
            switch (pbType) {
              case 'SurveyAnswer': // サーベイ回答依頼に対する顧客からの回答
                switch (pbData.Satisfy) {
                  case 'Yes':
                    replyMessage = pbData.Customer + '様。ご満足いただきありがとうございました。';
                    Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
                    break;
                  case 'No':
                    replyMessage = pbData.Customer + '様。ご不満の点について今後改善して参ります。';
                    Line.replyMessage(replyToken, { type: 'text', text: replyMessage });
                    break;
                }
                break;
            }
          });
          break;
      }
    });
  } catch (e) {
    // console.log('throw exception:');
    throw e;
  }
  // console.log('Return: 200');
  callback(null, { statusCode: 200 });
};
```

```js:utils.js
const emojione = require('emojione');
const JSONParseError = require('@line/bot-sdk').JSONParseError;
const validateSignature = require('@line/bot-sdk').validateSignature;
const SignatureValidationFailed = require('@line/bot-sdk').SignatureValidationFailed;

// X-Line-Signatureリクエストヘッダーに含まれる署名を検証
module.exports.validateSignature = function (body, secret, signature) {
  if (!validateSignature(body, secret, signature)) {
    throw new SignatureValidationFailed("signature validation failed", signature)
  }
  const strBody = Buffer.isBuffer(body) ? body.toString() : body;
  try {
    return JSON.parse(strBody);
  } catch (err) {
    throw new JSONParseError(err.message, strBody);
  }
};

// 返答メッセージに入っているプレースホルダを任意の文字列に変換
module.exports.convertPlaceholders = function (message, placeholderString, replaceValue) {
  const placeholder = `<${placeholderString}>`;
  const convertedMessage = message.replace(new RegExp(placeholder, 'g'), replaceValue);
  return convertedMessage;
};

// 友達追加時メッセージ
module.exports.welcomeMessage = function (displayName) {
  return emojione.shortnameToUnicode("はじめまして、${displayName}さん:exclamation::smiley:");
};
```

### 上記のコードについて
* 以下の項目については個別のパラメータを設定してください。
    * LINEのアクセストークン
    * LINEのChannelSecret
    * 呼び出すAmazon Connectの設定(インスタンスID、リージョン、コンタクトフローID、取得した電話番号)
    * Amazon Connect Salesforce lambda packageをAPI GWを介してAPI化した際のURL
    * 手配する修理サービスの電話番号

<!-- 
* 今回作成したコードは[]()で作成しているシナリオのために、位置情報送信後に以下の処理を行っています。
    * SFDCからお客様情報を取得、ケースへの登録
    * Amazon Connectのコンタクトフローを呼び出し、修理業者へ電話をかける部分。
        * コンタクトフローIDやリージョン、AmazonConnectのインスタンスIDを入れる必要があります。
-->

# ③デプロイ

* デプロイはSAMを利用します。AWS CLIを使用するので設定が必要です。

## AWS CLI
* コマンドベースでAWSの設定を行うために、AWS CLIを使用します。Pythonベースのためpipでインストールします。
    * WindowsであればMSI形式のインストーラも利用可

### CLIの設定方法
* Python3をインストールします。（[公式サイト](https://www.python.org/)）
    * Pathを通した後、コマンドプロンプトからPython、pipが使えるかバージョン表示のコマンドで確認
        * `$ python -V`
        * `$ pip -V`

* Pythonのインストールが終わったら、pipでAWS CLIをインストールします。
    * `$ pip install awscli`

## AWS SAMの使い方

* 以下のサイトを参考にさせていただきました。
    * [【AWS SAM 入門①】セットアップから簡単なLambda Functionの作成まで](https://www.ketancho.net/entry/2017/12/25/030849)
    * [【AWS SAM 入門②】Lambda詳細編](https://www.ketancho.net/entry/2017/12/30/022511)

* 以下の作業が必要です。
    * 中間生成物を置くためのS3の準備
    * Lambdaに入れたいコードの準備
    * SAM使用のためのテンプレートの作成
    * パッケージング（S3に中間生成物が置かれる）
    * パッケージングされたものをAWSにデプロイ

### 1. S3バケットの作成

* AWS CLIを使用して下記のコマンドを打ち込みます。
    * `$ aws s3 mb s3://入れるバケット名 --profile CLIのプロファイル名`
    * `$ aws s3 ls --profile CLIのプロファイル名`で作成を確認

### 2. コードの準備

* デプロイするコード（webhook.js等）、外部モジュールをLambda関数ごとに1つのzip形式ファイルとしてまとめます。
    * 外部モジュールはnpmコマンドであらかじめローカルにインストールしておき、node_modules以下のファイルもzipにまとめます。

### 3. テンプレートの作成

* デプロイ情報をYAML形式で作成します。

```yaml:template.yaml
# バージョンの設定
AWSTemplateFormatVersion: '2010-09-09'
# 説明文
Description: Create Lambda function by using AWS SAM.
# Lambdaをサーバレスにする
Transform: AWS::Serverless-2016-10-31

# 複数の設定を行う際に共通する設定はGlobalsに記載
Globals:
  Function:
# 使用言語・バージョンを記載
    Runtime: nodejs8.10
# タイムアウト時間を秒で記載
    Timeout: 60
# メモリサイズの記載
    MemorySize: 256
    Environment:
      Variables:
# 変数を作成し、中にデータを入力
        HOGEHOGE: 36
        HUGAHUAG: abc
Resources:

# 作成するLambda関数・ロールの名前を記載
  LineChatWebhookReceiverSample:
# 上記が何者なのか種類を記載 ロールの場合はAWS::IAM::Role
    Type: AWS::Serverless::Function
# これ以降はプロパティ
    Properties:
# !GetAttとすることでこのファイル内で作成したロールを指定することができます。
      Role: !GetAtt LineChatIamRoleSample.Arn
# 上記で記載したZIPファイルの相対座標を記載しています。(このファイルから見た)
      CodeUri: 'functions/webhook.zip'
# このハンドラ名の前半がファイル名ドットの後の公判が呼び出す関数名となってます。
# ZIPしない場合にはfunctions/webhook.handler等で対応
      Handler: webhook.handler
    Events:
# Amazon API GWを同時に作る場合に記載
      Api:
        Type: Api
        Properties:
          Path: /webhooks/line
          Method: post
# 同様にログプロセッサーを使用する場合に記載
      LogsProcessor:
        Type: CloudWatchLogs
        Properties:
# !Refとしてこのファイル下部で作成したものを選択する
          LogGroupName: !Ref LineChatLogGroupSample


  LineChatLogGroupSample:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${LineChatWebhookReceiverSample}
      RetentionInDays: 14

# 上記で作成した関数に付与するロールの作成
  LineChatIamRoleSample:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: "sts:AssumeRole"
      Policies:
        -
          PolicyName: "LineChat-lambda-sample"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              -
                Effect: "Allow"
                Action: "cloudwatch:*"
                Resource: "*"
              -
                Effect: "Allow"
                Action: "s3:*"
                Resource: "*"
              -
                Effect: "Allow"
                Action: "logs:*"
                Resource: "*"
              -
                Effect: "Allow"
                Action: "dynamodb:*"
                Resource: "*"
              -
                Effect: "Allow"
                Action: "connect:*"
                Resource: "*"

```

### 4. パッケージングとデプロイ
* テンプレートとコードは以下のディレクトリ構成にします。

```
./
|-- ./functions
|   |--webhook.zip
|-- template.yaml

```

* 下記コマンドを実行し、パッケージングとデプロイを行います。 
  * 作成したYAML形式のテンプレート（template.yaml）、S3バケット、スタック名を指定します。
    * `aws cloudformation package`でパッケージングすることによりpackaged-template.yamlが生成される（はず）

```bash
$ aws cloudformation package --template-file template.yaml --s3-bucket S3バケット名 --output-template-file packaged-template.yaml --profile プロファイル名

$ aws cloudformation deploy --template-file packaged-template.yaml --stack-name スタック名 --capabilities CAPABILITY_IAM --profile プロファイル名

```

# ◆参考ページ
* [おじさんLINEごっこBOTを作ってみた - PPT Developers' Blog](https://persol-pt.github.io/posts/line-ojisan-bot/)
* [【AWS SAM 入門①】セットアップから簡単なLambda Functionの作成まで](https://www.ketancho.net/entry/2017/12/25/030849)
* [【AWS SAM 入門②】Lambda詳細編](https://www.ketancho.net/entry/2017/12/30/022511)