はじめに
nodejs初心者ですが、herokuを利用したなんやかんやいろいろ詰め込んだLine Botの作成をしました。
開発にしばらく間が空きそうなので備忘録がてら書き記しておきます。
主な機能
- テキストが送られてきたらDOCOMO雑談APIによって会話する。
- 特定の言葉ではAPIに渡さず、自サーバー上で返事を作成
- 数字だけなら因数分解を実行
- 画像が送られてきたらDOCOMO画像認識APIで解析する。
- 本・食品の場合はAPIから返ってくる情報をもとに商品情報・URLを返す。
- スタンプが送られてきたら適当にあしらう
参考にさせていただいた記事
必要なもの
- Windows10で開発
- node v6.10.0
- npm 4.5.0
- heroku
- Line Messaging API
- DOCOMO 雑談API + 画像認識API
nodeモジュール
- http
- express
- co
- request
コード
その1
main.js
const func = require("./functions.js");//処理のメインのスクリプトです。(後述)
const co = require("co");//非同期処理の要
const express = require("express");//サーバーの元
const http = require("http");
const bodyParser = require('body-parser');//JSONを扱うのを簡単にしてくれるらしい
const app = express();
const htdocs = 'htdocs'; //GETしてきたとき用のルートディレクトリ
const listening_port = 80; //Listeningポート
const server1 = http.createServer(app); //expressサーバーを設定
app.use(bodyParser.urlencoded({extended: true})); // JSONの送信を許可
app.use(bodyParser.json()); // JSONのパースを楽に(受信時)
app.get('/', function (req, res) {
res.set('Content-Type', 'text/html');
res.sendfile(htdocs + '/index.html');
});
app.use('/css', express.static(htdocs + '/css'));
app.use('/js', express.static(htdocs + '/js'));
app.get('/callback', function (req, res) {
res.set('Content-Type', 'text/html');
res.sendfile(htdocs + '/index.html');
});
app.post('/callback', function (req, res) {
co(function * () {
var FromLine = yield func.make_FromLine(req);
//処理1:LINEからのJSONを基にこっちで必要な情報だけを抜き取る -> FromLineへ格納
var contents = yield func.reply_manager(FromLine);
//処理2:FromLineを基に返信データを作成 -> contentsへ
var ToLine = yield func.make_message_data(FromLine.atesaki, contents);
//処理3:contentsを基に返信データを梱包 -> ToLine へ格納
yield func.Send_to_Line(ToLine);
//処理4:それを送る
}).catch(onerror);
});
process.on('unhandledRejection', console.dir);
server1.listen(process.env.PORT || listening_port); //expressサーバーを開始
console.log('\-----Server running-----\n');
function onerror(err) {
console.error(err);
}
要点
- 同期処理をしないと確実に失敗する -> co モジュール
- heroku上ではポートの指定が特別 -> [サーバー].listen(process.env.PORT || [指定ポート])
その2
functions.js
const request = require('request');
const DOCOMO_ENDPOINT = 'https://api.apigw.smt.docomo.ne.jp/';
const LINE_ENDPOINT = 'https://api.line.me/v2/bot/';
const LINE_HEADERS = {
'Content-type': 'application/json',
'Authorization': 'Bearer {' + [YOUR_LINE_CHANNEL_ACCESS_TOKEN] + '}'
};
const DOCOMO_HEADERS = {
'Content-type': 'application/json'
};
var IS_SRTR = false;//しりとりかどうか
var context = "";//contextコード
module.exports = {
//関数1
make_message_data: function (reply_token, contents) {
return new Promise(function (resolve, reject) {
var result = {
'replyToken': reply_token,
'messages': []
};
for (i = 0; i < contents.length; i++) {
result['messages'].push(contents[i]);
}
//replyToken と messages の入ったオブジェクトを返す
resolve(result);
});
},
//関数2
make_FromLine: function (req) {
return new Promise(function (resolve, reject) {
var JsonString = req.body;// 受信テキスト
var request_object = JsonString['events'][0];
// 送られてきた内容は[request_object]に格納されている
var FromLine = {
atesaki: request_object['replyToken'],
event_type: request_object['type'],
time: request_object['timestamp'],
userid: request_object['source']['userId'],
message: request_object['message'],
message_type: request_object['message']['type'],
messageId: request_object['message']['id']
};//必要な情報だけ抜き取る
resolve(FromLine);
});
},
//関数3
reply_manager: function (FromLine) {
return new Promise(function (resolve, reject) {
if (FromLine.message_type === 'text') {
//-------------------テキストデータの場合-----------------------
var string = FromLine.message['text'];
if (string.match(/^[0-9]{1,}$/g)) {
//数字だけで構成されているときは因数分解します。(無駄機能)
if (string.length < 12) {
//因数分解の処理 -> resultへ
var contents = [{
"type": "text",
"text": result
}];
resolve(contents);
}
} else if (string.match(/日付/g)) {
//「日付」の文字が含まれていたら無差別的に現在時刻を返す
//現在時刻の取得 -> timeへ
var contents = [{
"type": "text",
"text": time
}];
resolve(contents);
} else {
//その他の場合 -> 雑談APIへ
var message_data = {
'utt': string,
'mode': 'dialog',
'context': context,
};
if (IS_SRTR === true) {
message_data.mode = 'srtr';//しりとりモード
} else {
message_data.mode = 'dialog';//お話モード
}
var options = {
url: DOCOMO_ENDPOINT + 'dialogue/v1/dialogue?APIKEY=' + [YOUR_DOCOMO_APIKEY],
method: 'POST',
headers: DOCOMO_HEADERS,
json: message_data
};
request(options, function (err, response, body) {
if (err) {
reject(err);
}
response_json = body;//返信データ
if (response_json.mode === "srtr") {
IS_SRTR = true;
} else {
IS_SRTR = false;
}
context = response_json.context;//contextコードの格納
var reply_text = response_json.utt;//返信テキスト
var contents = [{
"type": "text",
"text": reply_text
}];
//contentsは配列。contentsのうち一つ一つのオブジェクトがLINE上で一つの吹き出しになる
resolve(contents);
});
}
} else if (FromLine.message_type === 'sticker') {
//-------------------スタンプを送ってきた場合-------------------
var contents = [{
"type": "text",
"text": 'スタンプをありがとう!'
}, {
"type": "sticker",
"packageId": "1",
"stickerId": Math.floor((Math.random() * 21 + 1))//ランダムでスタンプを返す。
}];
resolve(contents);
} else if (FromLine.message_type === 'image') {
//-------------------画像を送ってきた場合-------------------
//LINEから写真をGETしてくる
var options1 = {
url: LINE_ENDPOINT + "message/" + FromLine.messageId + "/content",
method: 'GET',
encoding: null,// <- 重要!
headers: LINE_HEADERS
};
request(options1, function (err, res, content) {
if (err || res.statusCode !== 200) {
reject(err);
} else {
//写真の取得がうまくいった場合
//DOCOMO画像認識APIに写真をPOST
var options2 = {
url: DOCOMO_ENDPOINT + 'imageRecognition/v1/recognize?APIKEY=' + [YOUR_DOCOMO_APIKEY] + '&recog=product-all',
method: 'POST',
body: content,//返ってきたデータ(バイナリ)をそのまま渡す
encoding: null,//重要
headers: {
'Content-type': 'application/octet-stream'
}
};
request(options2, function (err, res, body2) {
if (err) {
var body2_str = body2.toString();
var body2_obj = JSON.parse(body2_str);
console.log("ERROR:", body2_obj);
reject(err);
} else if (res.statusCode === 200) {
var body2_str = body2.toString();
var body2_obj = JSON.parse(body2_str);
//送られてきたものもすべてバイナリなので平文に戻す
if (!body2_obj['error']) {
if (!body2_obj["candidates"]) {
//エラーはないけれどもDOCOMO側で物体を特定できなかった場合
var contents = [{
"type": "text",
"text": 'なんだかよくわからないです...'
}];
} else {
var recog_data = body2_obj["candidates"];//配列
var item = recog_data[0];
if (item["score"] > 50) {//情報が確からしいもの※数値は適当です(実際の返信では0.0000...から2000ぐらいまであります。)
if (item["category"] === "book") {
//情報を取り出す処理 -> contentsへ
} else if (item["category"] === "food") {
//情報を取り出す処理 -> contentsへ
} else if (item["category"] === "product-all") {
//情報を取り出す処理 -> contentsへ
} else {
}
}
}
resolve(contents);
} else {
reject(err);
}
} else {
reject(err);
}
});
}
});
} else {
//-------------------それ以外が送られてきたとき-------------------
var contents = [{
"type": "text",
"text": 'なんとおっしゃったのかわかりません。'
}, {
"type": "sticker",
"packageId": "1",
"stickerId": "9"
}];
resolve(contents);
}
});
},
//関数4
Send_to_Line: function (MessageToLine) {
return new Promise(function (resolve, reject) {
try {
var options = {
url: LINE_ENDPOINT + 'message/reply',
method: 'POST',
headers: LINE_HEADERS,
json: MessageToLine
};
request(options, function (err, response, body) {
if (err) {
reject(err);
}
if (response.statusCode === 200) {
console.log("\------------Success!-------------\n");
resolve();
} else {
console.log("\-----Line_Response_Error-----\n" + response.statusCode,body);
}
});
} catch (e) {
console.log(e);
reject(e);
}
});
}
};
要点
- Nodejsで画像のバイナリを通信するとき、encoding:null にしないとうまくいかないです(ハマッたポイント)
- encoding:null で送られてきた情報を見るときも、当たり前ですが平文に復元しないと読めないです。 -> [バイナリデータ].toString()を使うなど
- いままでの情報では Server IP Whitelist に登録する必要があるとかなんとかありましたが、なくても大丈夫なようです。(2017年5月時点、自分のところは問題ないです。)
注意
※上のコードではエラー処理や一部の処理が省略されているので注意してください。
リファレンスなど
Line API Reference
docomo Developer support
さいごに
バカみたいに長いコードですみませんm(_ _)m
何かツッコミがございましたら、お気軽にごツッコミください!