この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、せーのです。 先日LINE BOTを作りまして、それ以来私の携帯にDevelopers.ioの新着のお知らせが届いてきているわけですが
一方的に通知されるだけであまりBOTらしい動きをしていないな、ということで、今日は少し実践的に複数のユーザーに対応するAPIを書いてみたいと思います。
仕様を決める
今回やってみるのは
- くらめそちゃんBOTと友達になった人全員にDevelopers.ioの新着をお知らせする
です。
NAT GatewayからNATインスタンスへ
前回も書きましたが、NAT Gatewayはこのソリューションを組むには少々オーバースペックですのでt2.nanoでEC2を一つ立て、それをNATとして使いたいと思います。
NATの立て方はとても簡単です。AWSのMarcketplaceにNAT用のインスタンス、というのがあるのでそれを使ってEC2を立て
送信先/送信元のソースチェックを外し
ルートテーブルで対象となるサブネットからの通信に立てたEC2を指定すればOKです。
前回NAT Gatewayを立てた時に作ったElastic IPをそのままこのEC2につけてやればLINE側の登録を変える必要もありません。
さて、では実装してみましょう。
友達になったユーザーのMIDを管理する
BOTに対して友達になったユーザーがいると、BOTに対してこのようなリクエストが飛びます。
この[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に対して友達になるアクションをすると
キチンと動くと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));
}
});
}
}
};
DynamoDBを使って管理
ここまで出来れば後はDynamoDBにデータを突っ込んで先日書いたRSS配信のLambda FunctionにDynamoDBからMIDデータを引っ張ってくれば完成です。
まずはDynamoDBのテーブルを一つつくります。hashキーがmidですね。
LambdaにつけていたIAM RoleにDynamoDBの操作権限を追加します。
コードに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に値が格納されます。
あとは先日書いた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));
}
});
});
}
});
});
};
まとめ
いかがでしたでしょうか。普段APIを触っている方であれば簡単ではないでしょうか。ただ段々処理が増えてコールバック地獄の釜が開いてきているので、次に処理を足すときには一旦整理しないと厳しいですね。