AWS LambdaとLINE BOT APIで友達になったユーザーをDynamoDBで管理する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、せーのです。
先日LINE BOTを作りまして、それ以来私の携帯にDevelopers.ioの新着のお知らせが届いてきているわけですが

一方的に通知されるだけであまりBOTらしい動きをしていないな、ということで、今日は少し実践的に複数のユーザーに対応するAPIを書いてみたいと思います。

仕様を決める

今回やってみるのは

  • くらめそちゃんBOTと友達になった人全員にDevelopers.ioの新着をお知らせする

です。

NAT GatewayからNATインスタンスへ

前回も書きましたが、NAT Gatewayはこのソリューションを組むには少々オーバースペックですのでt2.nanoでEC2を一つ立て、それをNATとして使いたいと思います。

NATの立て方はとても簡単です。AWSのMarcketplaceにNAT用のインスタンス、というのがあるのでそれを使ってEC2を立て

line_bot_re1

送信先/送信元のソースチェックを外し

line_bot_re2

ルートテーブルで対象となるサブネットからの通信に立てたEC2を指定すればOKです。

line_bot_re3

前回NAT Gatewayを立てた時に作ったElastic IPをそのままこのEC2につけてやればLINE側の登録を変える必要もありません。

line_bot_re4

さて、では実装してみましょう。

友達になったユーザーのMIDを管理する

BOTに対して友達になったユーザーがいると、BOTに対してこのようなリクエストが飛びます。

line_bot_re7

この[opType]が4の場合は友達になった、8の場合はブロックされた、ということを表します。そして飛んできたユーザーのmidを元に

ヘッダ:

  • X-Line-ChannelID: Channel ID
  • X-Line-ChannelSecret: Channel secret
  • X-Line-Trusted-User-With-ACL: Channel MID(BOTのMID。相手ユーザーのではない)

をつけて

TARGET URL: https://trialbot-api.line.me/v1/profiles?mids=[相手ユーザーのMID。複数ある場合はカンマでつなぐ]

に対してGETでリクエストを飛ばすとユーザーの情報が取得できます。

それではユーザーの情報を取得するまでを書いてみましょう。今回は飛んできたリクエストを処理するので前回書いたSignatureによるValidationもキチンと行いましょう。

console.log('Loading function');

var request = require('request');
var crypto = require('crypto');

const CHANNEL_SECRET = 'b142571c3d1daa02ab99ff2f90c7856c';

var getprofileurl = "https://trialbot-api.line.me/v1/profiles";
var receiveOptions = {
        url: "",
        headers: {
            'X-Line-ChannelID':'0000000000',
            'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX',
            'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX'
        },
        json: true
    };

var sendOptions = {
        url: "https://trialbot-api.line.me/v1/events",
        method: 'POST',
        headers: {
            'Content-Type':'application/json',
            'X-Line-ChannelID':'0000000000',
            'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX',
            'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX'
        },
        json: true,
        body: ''
    };

exports.handler = (event, context, callback) => {
    var signature = event.CHANNELSIGNATURE;
    var eventbody =  new Buffer(JSON.stringify(event.body), 'utf8');

    var hash = crypto.createHmac('sha256', CHANNEL_SECRET).update(eventbody).digest('base64');

    if (hash != signature){
        context.fail("Signature validation failed.");
    }

    if (event.body.result[0].content.opType){
        if (event.body.result[0].content.opType == 4){
            getprofileurl += "?mids=" + event.body.result[0].content.params[0];
            console.log("url: " + getprofileurl);

            receiveOptions.url = getprofileurl;
            request.get(receiveOptions, function(error, response, body){
              if (!error) {
                console.log(JSON.stringify(body, null, 2));
                console.log('send to LINE to get profile.');

                context.succeed('done.');

              } else {
                console.log('error: ' + JSON.stringify(error));
              }
            });
        }
    }
};

これでBOTに対して友達になるアクションをすると

chat_bot11

キチンと動くとprofile取得のリクエストのresponseに

{
  "contacts": [
    {
      "displayName": "つよし",
      "mid": "XXXXXXXXXXXXXXXXXXXXXXXXXX",
      "pictureUrl": "http://dl.profile.line-cdn.net/XXXXXXXXXXXXXXXXXXXXXXXXXX",
      "statusMessage": "今年のテーマは「安定」"
    }
  ],
  "count": 1,
  "display": 1,
  "pagingRequest": {
    "start": 1,
    "display": 1,
    "sortBy": "MID"
  },
  "start": 1,
  "total": 1
}

このようなJSONが入ってきます。このmidとdisplayNameをDynamoDBに保管することでユーザーの管理ができます。試しに名前を呼んで返事してみましょう。

console.log('Loading function');

var request = require('request');
var crypto = require('crypto');

const CHANNEL_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXX';

var getprofileurl = "https://trialbot-api.line.me/v1/profiles";
var receiveOptions = {
        url: "",
        headers: {
            'X-Line-ChannelID':'0000000000',
            'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX',
            'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX'
        },
        json: true
    };

var sendOptions = {
        url: "https://trialbot-api.line.me/v1/events",
        method: 'POST',
        headers: {
            'Content-Type':'application/json;charset=utf-8',
            'X-Line-ChannelID':'0000000000',
            'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX',
            'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX'
        },
        json: true,
        body: ''
    };

var senddata={
    'to': [],
    'toChannel':1383378250,
    'eventType':"138311608800106203",
    'content':{
        'contentType': 1,
        'toType': 1,
        'text': 'くらめそちゃんだよ!'
    }
};

exports.handler = (event, context, callback) => {
    var signature = event.CHANNELSIGNATURE;
    var eventbody =  new Buffer(JSON.stringify(event.body), 'utf8');

    var hash = crypto.createHmac('sha256', CHANNEL_SECRET).update(eventbody).digest('base64');

    if (hash != signature){
        context.fail("Signature validation failed.");
    }

    if (event.body.result[0].content.opType){
        if (event.body.result[0].content.opType == 4){
            getprofileurl += "?mids=" + event.body.result[0].content.params[0];
            console.log("url: " + getprofileurl);

            receiveOptions.url = getprofileurl;
            request.get(receiveOptions, function(error, response, body){
              if (!error) {
                console.log(JSON.stringify(body, null, 2));
                console.log('send to LINE to get profile.');

                var usermid = body.contacts[0].mid;
                var username = body.contacts[0].displayName;
                senddata.to.push(usermid);
                senddata.content.text = 'くらめそちゃんだよ!' + username + 'さん、これからもよろしくね!';
                sendOptions.body = senddata;

                request.post(sendOptions, function(error, response, body){
                    if (!error) {
                        console.log(JSON.stringify(response));
                        console.log(JSON.stringify(body));
                        console.log('send to LINE.');

                        context.succeed('done.');

                    } else {
                        console.log('error: ' + JSON.stringify(error));
                    }
                });

              } else {
                console.log('error: ' + JSON.stringify(error));
              }
            });
        }
    }
};

line_bot_re8

DynamoDBを使って管理

ここまで出来れば後はDynamoDBにデータを突っ込んで先日書いたRSS配信のLambda FunctionにDynamoDBからMIDデータを引っ張ってくれば完成です。

まずはDynamoDBのテーブルを一つつくります。hashキーがmidですね。

line_bot_re10

LambdaにつけていたIAM RoleにDynamoDBの操作権限を追加します。

line_bot_re11

コードにDynamoDBへのPutを追加します。

console.log('Loading function');
var request = require('request');
var crypto = require('crypto');

const CHANNEL_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXX';

var getprofileurl = "https://trialbot-api.line.me/v1/profiles";
var receiveOptions = {
        url: "",
        headers: {
            'X-Line-ChannelID':'0000000000',
            'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX',
            'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX'
        },
        json: true
    };

var sendOptions = {
        url: "https://trialbot-api.line.me/v1/events",
        method: 'POST',
        headers: {
            'Content-Type':'application/json;charset=utf-8',
            'X-Line-ChannelID':'0000000000',
            'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX',
            'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX'
        },
        json: true,
        body: ''
    };

var senddata={
    'to': [],
    'toChannel':1383378250,
    'eventType':"138311608800106203",
    'content':{
        'contentType': 1,
        'toType': 1,
        'text': 'くらめそちゃんだよ!'
    }
};

var doc = require('dynamodb-doc');
var dynamo = new doc.DynamoDB();

var dbparams = {};
dbparams.TableName = "linebotusers";

exports.handler = (event, context, callback) => {
    var signature = event.CHANNELSIGNATURE;
    var eventbody =  new Buffer(JSON.stringify(event.body), 'utf8');

    var hash = crypto.createHmac('sha256', CHANNEL_SECRET).update(eventbody).digest('base64');

    if (hash != signature){
        context.fail("Signature validation failed.");
    }

    if (event.body.result[0].content.opType){
        if (event.body.result[0].content.opType == 4){
            getprofileurl += "?mids=" + event.body.result[0].content.params[0];
            console.log("url: " + getprofileurl);

            receiveOptions.url = getprofileurl;
            request.get(receiveOptions, function(error, response, body){
              if (!error) {
                console.log(JSON.stringify(body, null, 2));
                console.log('send to LINE to get profile.');

                var usermid = body.contacts[0].mid;
                var username = body.contacts[0].displayName;

                dbparams.Item = {
                    mid: usermid,
                    name: username
                };

                dynamo.putItem(dbparams, function(err, data) {
                    if (err) {
                        console.log(err, err.stack);
                    } else {
                        console.log('send to DynamoDB.');

                        console.log(data);
                        senddata.to.push(usermid);
                        senddata.content.text = 'くらめそちゃんだよ!' + username + 'さん、これからもよろしくね!';
                        sendOptions.body = senddata;

                        request.post(sendOptions, function(error, response, body){
                            if (!error) {
                                console.log(JSON.stringify(response));
                                console.log(JSON.stringify(body));
                                console.log('send to LINE.');

                                context.succeed('done.');

                            } else {
                                console.log('error: ' + JSON.stringify(error));
                            }
                        });
                            }
                        });
              } else {
                console.log('error: ' + JSON.stringify(error));
              }
            });
        }
    }
};

これで友達が追加されたらDynamoDBに値が格納されます。

line_bot_re9

あとは先日書いたRSSを送信するLambdaFunctionをDynamoDBから取ってくるように書き換えます。

console.log('Loading function');

var FeedParser = require('feedparser');
var request = require('request');
var feed = 'https://dev.classmethod.jp/feed/';
var options = {
        url: "https://trialbot-api.line.me/v1/events",
        method: 'POST',
        headers: {
            'Content-Type':'application/json',
            'X-Line-ChannelID':'0000000000',
            'X-Line-ChannelSecret':'XXXXXXXXXXXXXXXXXXXXXXXXXX',
            'X-Line-Trusted-User-With-ACL':'XXXXXXXXXXXXXXXXXXXXXXXXXX'
        },
        json: true,
        body: ''
    };


var senddata={
    'to': [],
    'toChannel':1383378250,
    'eventType':"138311608800106203",
    'content':{
        'contentType': 1,
        'toType': 1,
        'text': ''
    }
};

var doc = require('dynamodb-doc');
var dynamo = new doc.DynamoDB();

var dbparams = {};
dbparams.TableName = "linebotusers";

exports.handler = (event, context, callback) => {
    var req = request(feed);
    var feedparser = new FeedParser({});

    var items = [];
    var pubdate = "";

    req.on('response', function (res) {
      this.pipe(feedparser);
    });

    feedparser.on('meta', function(meta) {
      console.log('==== %s ====', meta.title);
    });

    feedparser.on('readable', function() {

      while(item = this.read()) {
        //console.log("item.pubdate: " + item.pubdate + '   ' + item.title);
        pubdate = item.pubdate.getTime();
        now = new Date().getTime();
        if (now - pubdate < 900000){
            items.push(item);
        }
      }

    });

    feedparser.on('end', function() {
        console.log("article is " + items.length);
        if (items.length == 0){
            context.succeed("No publish articles.");

        }

        dynamo.scan(dbparams, function(err, data) {
            if (err) {
                console.log(err, err.stack);
            } else {
                console.log(data);
                data.Items.forEach(function(val){
                    senddata.to.push(val.mid);
                });
                console.log('mids set.');

                  items.forEach(function(item) {

                    senddata.content.text = '- ' + item.author + 'が書いた、[' + item.title + ']' + '(' + item.link + ')がアップされましたよー☆';
                    options.body = senddata;
                    console.log('options: ' + JSON.stringify(options));

                    request.post(options, function(error, response, body){
                      if (!error) {
                        console.log(JSON.stringify(response));
                        console.log(JSON.stringify(body));
                        console.log('send to LINE.');

                        if( item == items.length - 1 ){
                            context.succeed('sending function done.');
                        }
                      } else {
                        console.log('error: ' + JSON.stringify(error));
                      }
                    });
                  });
            }
        });
    });


};

line_bot_re12

まとめ

いかがでしたでしょうか。普段APIを触っている方であれば簡単ではないでしょうか。ただ段々処理が増えてコールバック地獄の釜が開いてきているので、次に処理を足すときには一旦整理しないと厳しいですね。