AWSとLINEBotを繋いでみる

AWSとLINEから提供されるMessagingAPI、これらを使う事で様々な事をLINEBotでできるようになります。
LINEBotにメッセージを送ることで、そのメッセージに応じた処理をさせたり、何か通知をLINEBotから送信したりできますし、メッセージ送って処理させるならDBも使うことでユーザー毎に権限の制御もできるようになります。

送信:LINEBot(Webhook) -> AWS APIGateway -> Lambda
受信:Lambda(request) -> LINEBot(MessagingAPI)
というのが基本的なデザインになります。
![LINE-AWS-Design.png](https://qiita-image-store.s3.amazonaws.com/0/249725/c6bddcbf-37e4-dac7-0890-d2c020412e3c.png)

例えばユーザー毎に権限を管理するにはLINEWebhookから送られてくるペイロードに、ユーザー固有IDが含まれているので、それをDynamoDBで管理をすると、ServerlessFrameworkでマネージできるようになります。
![LINE-Dynamo.png](https://qiita-image-store.s3.amazonaws.com/0/249725/0a20bb32-eaeb-fb35-01f8-7a72731867c7.png)
今回はユーザー毎に状態を設定して、Botからユーザーによって異なるメッセージを送信するところまで作ります。
近い内に、LambdaがAPIを呼んで僕の家の鍵を開けるBotを作って仕組みを公開します。(予定)

#実装
毎度おなじみServerlessFrameworkを使っていきます。
今回は単純に、Dynamoにユーザー登録をして許可されたユーザーのみにオウム返しするBotを作ります。
リージョンは東京リージョンで、開発言語はNode.js8.10を使っていきます。
また、requestモジュールを使っているので、事前に`npm install request`を実行しておいてください。

```yaml:serverless.yml
service: sls-linebot

frameworkVersion: ">=1.1.0 <2.0.0"

provider:
  name: aws
  stage: ${opt:stage, 'dev'}
  runtime: nodejs8.10
  region: ap-northeast-1
  environment:
    DYNAMODB_TABLE: ${self:service}-${self:provider.stage}
    CHANNEL_ID: ${file(LINE.${self:provider.stage}.json):CHANNEL_ID}
    CHANNEL_SECRET: ${file(LINE.${self:provider.stage}.json):CHANNEL_SECRET}
  iamRoleStatements:
    - Effect: Allow
      Action:
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:DeleteItem
      Resource: "arn:aws:dynamodb:ap-northeast-1:*:table/${self:provider.environment.DYNAMODB_TABLE}"

functions:
  linebot:
    handler: index.line
    events:
      - http:
          path: line/apis/
          method: post
          cors: true

resources:
  Resources:
    DynamoDbTable:
      Type: 'AWS::DynamoDB::Table'
      DeletionPolicy: Retain
      Properties:
        AttributeDefinitions:
          -
            AttributeName: userid
            AttributeType: S
        KeySchema:
          -
            AttributeName: userid
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1
        TableName: ${self:provider.environment.DYNAMODB_TABLE}
```

今回はLINEDeveloperから発行されたクライアントIDとシークレットキーはymlに環境変数としてjsonファイルから渡しています。

```javascript:index.js
'use strict';

const request = require('request');
const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();

let lineMessagePush = (token, userId, messages) => {
    return new Promise((resolve, reject) => {
        let pushMessageParams = {
            url: 'https://api.line.me/v2/bot/message/push',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + token
            },
            json: true,
            body: {
                'to': userId,
                'messages': [messages]
            }
        };
        request(pushMessageParams, (err, response, body) => {
            if(err) reject(err);
            else resolve(response);
        });
    });
};

let lineGetAccessToken = () => {
    return new Promise((resolve, reject) => {
        let getAccessTokenParams = {
            url: 'https://api.line.me/v2/oauth/accessToken',
            method: 'POST',
            headers: {
                'Content-Type':'application/x-www-form-urlencoded'
            },
            form: {
                "grant_type":"client_credentials",
                "client_id":process.env["CHANNEL_ID"],
                "client_secret":process.env["CHANNEL_SECRET"]
            }
        };
        request(getAccessTokenParams, (err, response, body) =>  {
            if(err) reject(err);
            else resolve(JSON.parse(body).access_token);
        });
    });
};

let lineEvent = (json, callback) =>     {
    let type = json.events[0].type;
    let userId = json.events[0].source.userId;
    let messages;
    if(type === "follow") {
        messages = {
            "type": "text",
            "text": "フォローありがとうございます。\nオウム返しボットです!"
        };
    }
    else if(type === "message") {
        let text = json.events[0].message.text;
        messages = {
            "type": "text",
            "text": text
        };
    }
    lineGetAccessToken()
    .then(token => lineMessagePush(token, userId, messages))
    .then(response => callback(null, {statusCode:200, body:response}));
};

module.exports.line = (event, context, callback) => {
    let json = JSON.parse(event.body);
    let userAgent = event.requestContext.identity.userAgent;
    if(~userAgent.indexOf("LineBotWebhook")) lineEvent(json, callback);
    else {
        console.log("LINE Webhook以外からのアクセスです。");
        callback("Invalid UserAgent");
    }
};
```
これで単純なオウム返しBotができました。

lineEvent関数で、何のイベントで呼ばれたか判定し、メッセージ文を作成しています。こちらでは新規友達登録時と、ユーザーからのメッセージ送信時の時に反応できるようにしています。
lineGetAccessToken関数は名前の通り、アクセストークンを取得する関数です。取得にはクライアントの認証情報が必要なので環境変数から取得しています。
lineMessagePush関数も名前の通り、取得したアクセストークンや、lineEvent関数で取得したuserIDや作成したメッセージ文などを使ってLINEMessagingAPIを呼んでいます。
lineハンドラは、単純にAPIの呼び出し元がLineBotWebhookかどうかを判定しています。

ではここに、新規登録時にDynamoDBへユーザー登録し、DynamoDBの値を直接変更し、許可されたユーザーのみにオウム返しできるようにコードを書き換えていきます。
まず、lineEvent関数内で、友達登録時にそのユーザーをDynamoDBに追加しましょう。

```javascript
let lineEvent = async (json, callback) =>     {
    let type = json.events[0].type;
    let userId = json.events[0].source.userId;
    let messages;
    if(type === "follow") {
        await db.put({
            TableName: process.env.DYNAMODB_TABLE,
            ConsistentRead: true,
            Item: {
                userid: userId,
                auth: false
            }
        }).promise();
        messages = {
            "type": "text",
            "text": "フォローありがとうございます。\nオウム返しボットです!"
        };
    }
    else if(type === "message") {
        let text = json.events[0].message.text;
        await db.get({
            TableName: process.env.DYNAMODB_TABLE,
            Key: { userid: userId },
        },(err,data) => {
            if(err) console.log(err);
            messages = {
                "type": "text",
                "text": (data.Item.auth)?text:"返しません"
            };
        }).promise();
    }
    else if(type === "unfollow") {
        await db.delete({
            TableName: process.env.DYNAMODB_TABLE,
            Key: { userid: userId, },
        }).promise();
    }
    else console.log(type);
    lineGetAccessToken()
    .then(token => lineMessagePush(token, userId, messages))
    .then(response => callback(null, {statusCode:200, body:response}));
};
```
友達登録が解除されたユーザーはDynamoから削除するようにしています。
これで簡単なオウム返しをするLINEBotが作れました。
最終的なプログラムは以下のようになります。

```javascript:index.js
'use strict';

const request = require('request');
const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();

let lineMessagePush = (token, userId, messages) => {
    return new Promise((resolve, reject) => {
        let pushMessageParams = {
            url: 'https://api.line.me/v2/bot/message/push',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + token
            },
            json: true,
            body: {
                'to': userId,
                'messages': [messages]
            }
        };
        request(pushMessageParams, (err, response, body) => {
            if(err) reject(err);
            else resolve(response);
        });
    });
};

let lineGetAccessToken = () => {
    return new Promise((resolve, reject) => {
        let getAccessTokenParams = {
            url: 'https://api.line.me/v2/oauth/accessToken',
            method: 'POST',
            headers: {
                'Content-Type':'application/x-www-form-urlencoded'
            },
            form: {
                "grant_type":"client_credentials",
                "client_id":process.env["CHANNEL_ID"],
                "client_secret":process.env["CHANNEL_SECRET"]
            }
        };
        request(getAccessTokenParams, (err, response, body) =>  {
            if(err) reject(err);
            else resolve(JSON.parse(body).access_token);
        });
    });
};

let lineEvent = async (json, callback) =>     {
    let type = json.events[0].type;
    let userId = json.events[0].source.userId;
    let messages;
    if(type === "follow") {
        await db.put({
            TableName: process.env.DYNAMODB_TABLE,
            ConsistentRead: true,
            Item: {
                userid: userId,
                auth: false
            }
        }).promise();
        messages = {
            "type": "text",
            "text": "フォローありがとうございます。\nオウム返しボットです!"
        };
    }
    else if(type === "message") {
        let text = json.events[0].message.text;
        await db.get({
            TableName: process.env.DYNAMODB_TABLE,
            Key: { userid: userId },
        },(err,data) => {
            if(err) console.log(err);
            messages = {
                "type": "text",
                "text": (data.Item.auth)?text:"返しません"
            };
        }).promise();
    }
    else if(type === "unfollow") {
        await db.delete({
            TableName: process.env.DYNAMODB_TABLE,
            Key: { userid: userId, },
        }).promise();
    }
    else console.log(type);
    lineGetAccessToken()
    .then(token => lineMessagePush(token, userId, messages))
    .then(response => callback(null, {statusCode:200, body:response}));
};

module.exports.line = (event, context, callback) => {
    let json = JSON.parse(event.body);
    let userAgent = event.requestContext.identity.userAgent;
    if(~userAgent.indexOf("LineBotWebhook")) lineEvent(json, callback);
    else {
        console.log("LINE Webhook以外からのアクセスです。");
        callback("Invalid UserAgent");
    }
};
```

次回は家のスマートロックAPIを叩いてみます。
自分がBotに鍵開けてと送れば鍵が開いて、他の誰かが鍵を開けようとした時は時間帯によって開けるかどうか判断、というのをやる予定です。


--追記
CLIENT_ID,CLIENT_SECRETはLINEDeveloperのサイトでChannel ID,Channel Secretと表記されているものです。