aws-cdkを使って恋人(？)をLINE Botでつくってみた

## はじめに
みなさんいかがお過ごしでしょうか。早いもので今年もクリスマスが近づいていますね。
もしかすると1人でお家で凍えて寂しい思いをされている方がいるかもしれません。
そんな方のために恋人っぽく返事をくれるBotを作ってみました。

散々擦られたネタですが、最近はLINE Botに触れる機会が多くあり
aws cdkを使って構築してみたりもしたのでまとめてみることにしました。

## 構成
AWS上にaws-cdk(typescript)を使用して構築しています。
簡単な定型文の応答では面白くないので、Amazon Comprehendを使ってBotに送信されたメッセージの感情分析を行って、その内容に合わせて返答を行うようにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/07f67924-02a8-2d06-1301-aef49d463b85.png)

## LINEアカウントの準備
LINE Developersへ登録してLINEの公式アカウントを作成します。
以下の公式ページの手順に従って作成しましょう。

* [LINE Developersコンソールでチャネルを作成する](https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console)

ソースコード上にチャネルアクセストークンとチャネルシークレットを指定する必要があります。
以下のページの取得手順を参考に、それらの値をメモしておきましょう。

* [チャネルシークレット](https://developers.line.biz/ja/glossary/#channel-secret)
* [チャネルアクセストークン](https://developers.line.biz/ja/docs/messaging-api/channel-access-tokens/#long-lived-channel-access-tokens)

また、アカウント作成時点ではLINE Official Account上で自動応答メッセージが有効になっていると思います。
そのままだとなにかメッセージを送るたびに意図しないメッセージが返却されてしまう状態になってしまうので、オフに設定しておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/139d8f1f-f6e7-35d5-a9b7-20677907dfc5.png)


## 環境構築
環境構築をしていきます。
筆者はcloud9上で作業を行ってます。
構築した際のNodejsとnpmのバージョンは以下となります。

* Nodejs : v14.18.2
* npm : 6.14.15

### aws-cdk install
cdkをインストールしましょう。
cdkとは[こちら](https://aws.amazon.com/jp/cdk)で説明されているように、AWS上のリソースをJavascriptなどのプログラム言語で定義することができるフレームワークになります。

構築手順に関しては[こちら](https://summit-online-japan-cdk.workshop.aws/20-typescript/20-create-project/100-cdk-init.html)の公式手順が参考になると思います。
作業時点でのaws-cdkのバージョンは1.136.0でした。

* aws-cdkのインストール

```
npm install -g aws-cdk
cdk --version
```

* プロジェクトの作成

```
mkdir line-bot
cd line-bot
cdk init sample-app --language typescript
```

* ライブラリインストール

```
npm i @aws-cdk/aws-apigateway @aws-cdk/aws-lambda-nodejs @line/bot-sdk
```

作業時点でのインストールしたライブラリのバージョンは以下となります。
cdkで利用するライブラリはcdkのバージョンと同一にそろえておかないとエラーが発生しがちなので注意しましょう。

* @aws-cdk/aws-apigateway ： 1.136.0
* @aws-cdk/aws-lambda-nodejs ： 1.136.0
* @line/bot-sdk ： 7.4.0

### ディレクトリ構成
上述した手順より、テンプレートファイル群が生成されたと思います。
最終的には以下のようにファイルを配置します。

```
├── bin
│   └── line-bot.ts
├── lambda
│   └── app.ts
├── lib
│   └── line-bot-stack.ts
├── test
│   └── line-bot.test.ts
├── cdk.json
├── jest.config.js
├── package-lock.json
├── package.json
└── tsconfig.json
```

### API Gateway及びLambdaを定義
lib/line-bot-stack.tsへ以下のソースコードを記述します。
API GatewayとLambdaを定義し、Comprehendの実行権限を付与したロールをLambdaへ紐付けています。
※ **LINE_TOKEN**及び**LINE_CHANNEL_SECRET**にはLINE Developersコンソール上で取得したチャンネルアクセストークン及びチャネルシークレットをそれぞれ指定してください。

```typescript:lib/line-bot-stack.ts
import * as cdk from '@aws-cdk/core';
import * as apigateway from "@aws-cdk/aws-apigateway";
import * as iam from "@aws-cdk/aws-iam";
import { NodejsFunction } from '@aws-cdk/aws-lambda-nodejs';

export class LineBotStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Role
    const lineBotRole = new iam.Role(this, 'lineBotRole', {
      assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'service-role/AWSLambdaBasicExecutionRole'
        ),
        iam.ManagedPolicy.fromAwsManagedPolicyName('ComprehendFullAccess'),
      ],
    });

    // Lambda
    const lineBotFunction = new NodejsFunction(this, 'lineBotFunction', {
      entry: 'lambda/app.ts',
      handler: 'handler',
      role: lineBotRole,
      timeout: cdk.Duration.seconds(30),
      environment: {
        // line messaging api
        LINE_TOKEN: 'XXXX',
        LINE_CHANNEL_SECRET: 'XXXX',
      },
    });

    // APIGateway
    const apiRoot = new apigateway.LambdaRestApi(this, 'line-sample', {
      handler: lineBotFunction
    });
  }
}

```

### Lambdaの実装(Botの応答部分)

```typescript:lambda/app.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import * as line from '@line/bot-sdk'
import * as aws from "aws-sdk";

const client = new line.Client({
  channelAccessToken: process.env.LINE_TOKEN,
  channelSecret: process.env.LINE_CHANNEL_SECRET
})

// for signature line header
const crypto = require('crypto')

export async function handler(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> {
  console.log("processing line bot api...")
  // x-line-signature request headerを検証する
  // https://developers.line.biz/ja/reference/messaging-api/#signature-validation
  const signature = crypto.createHmac('SHA256', process.env.LINE_CHANNEL_SECRET).update(event.body).digest('base64');
  if (signature != event.headers['x-line-signature']) {
    console.error("fail signature, unauthorized.")
    return {
      statusCode:401,
      body: {
        message: "Unauthorized"
      }
    }
  }
  const requestBody: line.WebhookRequestBody = JSON.parse(event.body!)
  // 返信する
  for (let event of requestBody.events) {
    // メッセージのみ返信する
    if ( event.type == 'message') {
      // テキストのときのみ返信する
      if ( event.message.text !== undefined ) {
        // comprehends
        const resultComp = await comprehend(event.message.text)
        // メッセージ選択
        const returnText = createReturnText(resultComp)
        if (!returnText) {
          break
        }
        // replyMessage
        await client.replyMessage(event.replyToken, {
          type: "text",
          text: returnText
        })
        
      }
    } else {
      console.log("done. this request's type is not message.")
    }
  }
  return {
    statusCode: 200,
    headers: { "Content-Type": "text/plain" }
  }
}

/**
 * comprehendを使用してメッセージを分析する
 */
 async function comprehend(text: string) {
  const compClient = new aws.Comprehend();
  const request = {
    LanguageCode: "ja",
    Text: text,
  };
  try {
    const result = await compClient.detectSentiment(request).promise();
    console.log(result)
    return result.Sentiment
  } catch (err) {
    console.error("Comprehend Error: " + err)
    return false
  }
 }
 
 /**
  * comprehendの結果より応答メッセージを生成
  */
function createReturnText(sentiment: string) {
  let message: string, ary: string[]
  if ( sentiment == 'POSITIVE' ) {
    ary = ['すっごい！','さすがだねぇ','いいね！'];
    message = ary[Math.floor(Math.random() * ary.length)];
  } else if (sentiment == 'NEGATIVE') {
    ary = ['それはやばいね・・・', 'どんまい！', 'ひどいね、、'];
    message = ary[Math.floor(Math.random() * ary.length)];
  } else if (sentiment == 'NEUTRAL') {
    ary = ['うぇ〜い', 'うんうん', 'そうだね〜'];
    message = ary[Math.floor(Math.random() * ary.length)];
  } else {
    message = "ふーん"
  }
  return message
}
```

### デプロイ
以下のコマンドを実行し、デプロイを実行します。
デプロイが完了するとAPI Gatewayのエンドポイントが表示されるので、その値をコピーしておいてください。

```
cdk deploy
```

## LINE上でのWebhook設定
以下の公式ページを参考に、コピーしておいたAPI GatewayのエンドポイントをWebhook URLへ設定しましょう。
これにより、作成したLINEアカウントに対するメッセージ受信時などのイベント情報がWebhook先のURLへ通知されるようになります。

* [Webhook URLを設定する](https://developers.line.biz/ja/docs/messaging-api/building-bot/#setting-webhook-url)

## 動作確認

ではさっそく会話してみましょう！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/01da0588-58be-360e-e9ab-3185e0d979d9.png)

ちゃんと動いてくれてますね！

## まとめ
aws-cdkを使って、AWS上にComprehendを利用したLINE Botを作ってみました。
文脈から感情分析して応答してくれるので、それなりに動いてくれているように思えます。
ただ、恋人と言えるかどうかは微妙なところですね。。
時間があれば固有表現抽出とかやってもっと高度な応答ができるようにしていきたいです！