Nodejs初心者によるherokuを利用したなんやかんやいろいろ詰め込んだLine Botの作成

##はじめに
nodejs初心者ですが、herokuを利用したなんやかんやいろいろ詰め込んだLine Botの作成をしました。
開発にしばらく間が空きそうなので備忘録がてら書き記しておきます。

###主な機能
   - テキストが送られてきたらDOCOMO雑談APIによって会話する。
      - 特定の言葉ではAPIに渡さず、自サーバー上で返事を作成
      - 数字だけなら因数分解を実行
   - 画像が送られてきたらDOCOMO画像認識APIで解析する。
      - 本・食品の場合はAPIから返ってくる情報をもとに商品情報・URLを返す。
   - スタンプが送られてきたら適当にあしらう

###参考にさせていただいた記事

[Nodejs+LINE BOTでレストラン検索](http://qiita.com/enta0701/items/45d5295e023e24e9ff0b)

##必要なもの
1. Windows10で開発 
2. node v6.10.0 
3. npm 4.5.0
4. heroku
5. Line Messaging API
6. DOCOMO 雑談API + 画像認識API

####nodeモジュール
 - http
 - express
 - co
 - request

##コード

###その1

```js: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);
        }

```
####要点
 1. 同期処理をしないと確実に失敗する -> co モジュール
 2. heroku上ではポートの指定が特別 -> [サーバー].listen(process.env.PORT || [指定ポート])

###その2

```js: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);

                }
            });
        }
    };

```
####要点
1. Nodejsで画像のバイナリを通信するとき、encoding:null にしないとうまくいかないです(ハマッたポイント)
2. encoding:null で送られてきた情報を見るときも、当たり前ですが平文に復元しないと読めないです。 -> [バイナリデータ].toString()を使うなど
3. いままでの情報では Server IP Whitelist に登録する必要があるとかなんとかありましたが、なくても大丈夫なようです。(2017年5月時点、自分のところは問題ないです。)

#####注意
※上のコードではエラー処理や一部の処理が省略されているので注意してください。

###リファレンスなど
[Line API Reference](https://devdocs.line.me/ja/)
[docomo Developer support](https://dev.smt.docomo.ne.jp)

##さいごに
バカみたいに長いコードですみませんm(_ _)m
何かツッコミがございましたら、お気軽にごツッコミください!