【サーバレス】LineMessagingAPIとAWS Lambdaでお天気LineBotを作ってみた。【LineBot】

# はじめに
* 近頃人工知能とかWatsonなどに興味があるので何か作ってみたい！ということで考えていたら、やっぱり人工知能はメッセンジャーのBotや音声認識などに相性がよさそうということで、まずはBotを調べてみることに。
* 簡単なBotということでLineBotでお天気を教えてくれるものを作ってみようと思いました。（メッセージ送信部分を変更したらSlackとかでも簡単にできると思います）
* この「[おもしろまじめなチャットボットをつくろう](https://www.amazon.co.jp/dp/4798051640/)」という本を読んでいたらサーバを用意してPHPで作るやり方が載っていたのですが、サーバを用意しなくてもLambdaでやればサーバレスで行けるんじゃないか？ということでLambdaで作成。
    * サーバを用意する場合はSSLを利用したAPIが作れればOK。
* 人工知能などの技術は搭載していません。文言認識をよくするために自然言語処理を入れたいところ。次の機会に。
* こちらの投稿にLambdaではなくIBM bluemixのNode-Red版も書いてますので興味があればどうぞ
    * http://qiita.com/hironaito/items/3398519abf8172fc2dd0

![line](https://qiita-image-store.s3.amazonaws.com/0/41761/ba72ca2e-880d-8b8b-e359-57e09b1600c2.gif)



# 利用する物
* LineMessagingAPI
* AWS
    * Lambda
    * API Gateway
* livedoorお天気API

# LineMessagingAPI
* 登録
    * ここから登録
        * https://business.line.me/ja/
    * 登録の流れは色んなところにも書いてあったりするので割愛。
* 注意点
    * ![line_api_attention2.png](https://qiita-image-store.s3.amazonaws.com/0/41761/26a8a4a3-514b-22c7-482b-717049b96377.png)



   * Botを利用しようとすると上記のような注意文言が出て来るけど、line business用の企業アカウントとかbotではないアカウントとして利用できなくなるよということ？
    * ビジネス用に別アカウントは発行できるようなので、新規発行した場合は切り替えても大丈夫そう。すでに使っているアカウントであれば別にビジネスアカウントを発行したらよさそう。

### Line@ MANAGER
* https://admin-official.line.me/
* 登録後Line@MANEGERからBot設定を少し変更。
* Bot設定から詳細設定のWebhook送信を利用する。自動応答メッセージを利用しないに変更。
![LINE@MANAGER.png](https://qiita-image-store.s3.amazonaws.com/0/41761/28ee40bf-0592-ac0a-6912-bb8fa08d84dc.png)


### LineDevelopers
* https://developers.line.me/ba/
    * 重要なのは「Channels > Basic information」に記載されている「Webhook URL」と「Channel Access Token」。後ほど利用。
* QRコードなどを読み込んでLineにBotを友達として追加してみる。Line@MANEGERの「友だち追加時あいさつ」が表示さる。必要であれば変更しておく。
* Messaging API リファレンス
    * https://devdocs.line.me/ja/#messaging-api

# AWS
## lambda作成
* とりあえずメッセージを投稿したらオウム返しするところまで実装
* ■Channel Access Token■に先ほどLineDevelopersでメモしたものを記載する。
    * まずはお試しで記載していますが、環境変数に設定した方がよいと思います。

```js:lineBotReply.js
const https = require('https');
const querystring = require('querystring');

exports.handler = (event, context, callback) => {
    let postData = {
        "replyToken": event.events[0].replyToken,
        "messages": [
            {
                "type": "text",
                "text": event.events[0].message.text
            }
        ]
    };
    
    const options = {
        hostname: 'api.line.me',
        path: '/v2/bot/message/reply',
        method: 'POST',
        headers: {
            "Content-Type": "application/json",
            "Authorization": "Bearer ■Channel Access Tokenを記載■"
        }
    };
    
    const req = https.request(options, (res) => {
        res.setEncoding('utf8');
        res.on('data', (d) => {
            process.stdout.write(d);
            callback(null,  JSON.parse(d));
        });
    });
    req.write(JSON.stringify(postData));
    req.end();
    
    req.on('error', (e) => {
      console.error(e);
    });
};

```

## API Gateway設定

* API GatewにPOSTで設定
    * ![API Gateway](https://qiita-image-store.s3.amazonaws.com/0/41761/728324d9-4ff5-fb1d-7527-cd763b5c345e.png)
    * 他を色々設定した方がよいですが、とりあえず動くので割愛
* Line Developersの「Webhook URL」にAWSのAPIを記載
    * 記載後 VERIFYをクリックしてsuccessと出ればOK。

![API Gateway](https://qiita-image-store.s3.amazonaws.com/0/41761/2e707f8d-b01e-c53d-3af9-f94587f26e56.png)

を下記Line Developersページに記載

![webhook.png](https://qiita-image-store.s3.amazonaws.com/0/41761/bdb9e0d0-c851-267d-3f6b-389387f2f4a9.png)


# 一旦動作確認
* メッセージをLineから送ってみて、オウム返しが来たらOK。

# お天気API実装

* livedoorお天気API
    * http://weather.livedoor.com/weather_hacks/webservice

* そのまま実装すると非同期なため、お天気情報が取得されるまえにlambdaが終了しちゃうのでpromiseを利用する。
* 天気という文字と、場所、明日、明後日の文言に反応するようにする。
    * ex) 明日の東京の天気
    * ex) 大阪の天気
* ファイルが長くなるので”lineBotReply.js”と”weather.js”と”weather-area.json”の３つに分割。
    * 完成したら１つにしてZIPアップロード。
* Channel Access Tokenなどは環境変数に記載するように変更
    * ![ACCESS_TOKEN.png](https://qiita-image-store.s3.amazonaws.com/0/41761/a915ca8b-d9b3-d753-75b6-f8d6c123c921.png)


```js:lineBotReply.js
'use strict';
const https = require('https');
const querystring = require('querystring');
const weather = require('weather');

exports.handler = (event, context, callback) => {
    let postData = {
        "replyToken": event.events[0].replyToken,
        "messages": [
            {
                "type": "text",
                "text": event.events[0].message.text
            }
        ]
    };
    // Message解析
    let text = event.events[0].message.text;
    // 天気の文言が含まれていたら
    if(text.match(/天気/gi)) {
      let nextDay = 0;
      // 明日・明後日の指示があるか判断
      if(text.match(/明日/gi) || text.match(/あした/gi)) {
        nextDay = 1;
      }
      if(text.match(/明後日/gi) || text.match(/あさって/gi)) {
        nextDay = 2;
      }
      // 地域名のみにテキストを出来る限り削除する（自然言語理解に置き換えたい）
      let cityName = text.replace( /[あ-ん]/g , "" )
        .replace( /天気/g , "" ).replace( /今日/g , "" ).replace( /明日/g , "" )
        .replace( /明後日/g , "" ).replace( /教/g , "" ).replace( /県/g , "" )
        .replace( /府/g , "" ).replace( /東京都/g , "東京" ).replace( /？/g , "" )
        .replace( /\u\l\d/g , "" ).replace( /\s/g , "" ).replace( /\n/g , "" );
      weather.getWeather(cityName, nextDay) //天気APIをコール
      .then( (data)=>{
        postData.messages = data;
        replyLineBotMessage(postData);
      });
      return;
    }
    // 何も無ければオウム返し
    replyLineBotMessage(postData);
    return;
};

// LineBotAPIコール
function replyLineBotMessage(postData) {
  const options = {
      hostname: "api.line.me",
      path: "/v2/bot/message/reply",
      method: 'POST',
      headers: {
          "Content-Type": "application/json",
          "Authorization": "Bearer " + process.env.ACCESS_TOKEN
      }
  };

  const req = https.request(options, (res) => {
      console.log('statusCode: ', res.statusCode);
      console.log('headers: ', res.headers);
      res.setEncoding('utf8');
      res.on('data', (d) => {
          process.stdout.write(d);
      });
  });
  req.write(JSON.stringify(postData));
  req.end();
}
```

```js:weather.js
// 天気APIコール
'use strict';
const http = require('http');
const querystring = require('querystring');
const weatherArea = require('weather-area');

exports.getWeather = (cityName, nextDay) => {
    let message = [{
      "type": "text",
      "text": ""
    }];
    let areaNumber = exports.getAreaNumber(cityName);
    return new Promise(function(resolve, reject) {
        let URL = 'http://weather.livedoor.com/forecast/webservice/json/v1?city='+areaNumber.num;
        http.get(URL, (res) => {
            let body = '';
            res.setEncoding('utf8');

            res.on('data', (chunk) => {
                body += chunk;
            });

            res.on('end', (res) => {
                res = JSON.parse(body);
                message[0].text = res.location.city + 'の' + res.forecasts[nextDay].dateLabel + 'の天気は' + res.forecasts[0].telop;
                if(areaNumber.err) {
                  message[0].text += '(ちゃんと理解できなかったので東京の天気だよ。）' ;
                }
                resolve(message);
            });
        }).on('error', (e) => {
            reject(new Error('エラー'));
        });

    });
};

exports.getAreaNumber = (cityName) => {
  let cityNumber = {
    num : '130010',
    err : false
  };
  cityNumber.num = weatherArea[cityName];
  if (!cityNumber.num) {
    cityNumber.num = '130010';
    cityNumber.err = true;
  }
  return cityNumber;
};
```


* エリア情報とIDを紐付けたJSONを作る。

```json:weather-area.json
{
  "北海道" : "016010",
  "稚内" : "011000",
  "旭川" : "012010",
  "留萌" : "012020",
  "網走" : "013010",
  "北見" : "013020",
  "紋別" : "013030",
  "根室" : "014010",
  "釧路" : "014020",
  "帯広" : "014030",
  "室蘭" : "015010",
  "浦河" : "015020",
  "札幌" : "016010",
  "岩見沢" : "016020",
  "倶知安" : "016030",
  "函館" : "017010",
  "江差" : "017020",
  "青森" : "020010",
  "むつ" : "020020",
  "八戸" : "020030",
  "岩手" : "030010",
  "盛岡" : "030010",
  "宮古" : "030020",
  "大船渡" : "030030",
  "宮城" : "040010",
  "仙台" : "040010",
  "白石" : "040020",
  "秋田" : "050010",
  "横手" : "050020",
  "山形" : "060010",
  "米沢" : "060020",
  "酒田" : "060030",
  "新庄" : "060040",
  "福島" : "070010",
  "小名浜" : "070020",
  "若松" : "070030",
  "茨城" : "080010",
  "水戸" : "080010",
  "土浦" : "080020",
  "栃木" : "090010",
  "宇都宮" : "090010",
  "大田原" : "090020",
  "群馬" : "100010",
  "前橋" : "100010",
  "埼玉" : "110010",
  "熊谷" : "110020",
  "秩父" : "110030",
  "千葉" : "120010",
  "銚子" : "120020",
  "館山" : "120030",
  "東京" : "130010",
  "大島" : "130020",
  "八丈島" : "130030",
  "父島" : "130040",
  "神奈川" : "140010",
  "横浜" : "140010",
  "小田原" : "140020",
  "新潟" : "150010",
  "長岡" : "150020",
  "高田" : "150030",
  "相川" : "150040",
  "富山" : "160010",
  "伏木" : "160020",
  "石川" : "170010",
  "金沢" : "170010",
  "輪島" : "170020",
  "福井" : "180010",
  "敦賀" : "180020",
  "山梨" : "190010",
  "甲府" : "190010",
  "河口湖" : "190020",
  "長野" : "200010",
  "松本" : "200020",
  "飯田" : "200030",
  "岐阜" : "210010",
  "高山" : "210020",
  "静岡" : "220010",
  "網代" : "220020",
  "三島" : "220030",
  "浜松" : "220040",
  "愛知" : "230010",
  "名古屋" : "230010",
  "豊橋" : "230020",
  "三重" : "240010",
  "津" : "240010",
  "尾鷲" : "240020",
  "滋賀" : "250010",
  "大津" : "250010",
  "彦根" : "250020",
  "京都" : "260010",
  "舞鶴" : "260020",
  "大阪" : "270000",
  "兵庫" : "280010",
  "神戸" : "280010",
  "豊岡" : "280020",
  "奈良" : "290010",
  "風屋" : "290020",
  "和歌山" : "300010",
  "潮岬" : "300020",
  "鳥取" : "310010",
  "米子" : "310020",
  "島根" : "320010",
  "松江" : "320010",
  "浜田" : "320020",
  "西郷" : "320030",
  "岡山" : "330010",
  "津山" : "330020",
  "広島" : "340010",
  "庄原" : "340020",
  "山口" : "350020",
  "下関" : "350010",
  "柳井" : "350030",
  "萩" : "350040",
  "徳島" : "360010",
  "日和佐" : "360020",
  "香川" : "370000",
  "高松" : "370000",
  "愛媛" : "380010",
  "松山" : "380010",
  "新居浜" : "380020",
  "宇和島" : "380030",
  "高知" : "390010",
  "室戸岬" : "390020",
  "清水" : "390030",
  "福岡" : "400010",
  "八幡" : "400020",
  "飯塚" : "400030",
  "久留米" : "400040",
  "佐賀" : "410010",
  "伊万里" : "410020",
  "長崎" : "420010",
  "佐世保" : "420020",
  "厳原" : "420030",
  "福江" : "420040",
  "熊本" : "430010",
  "阿蘇乙姫" : "430020",
  "牛深" : "430030",
  "人吉" : "430040",
  "大分" : "440010",
  "中津" : "440020",
  "日田" : "440030",
  "佐伯" : "440040",
  "宮崎" : "450010",
  "延岡" : "450020",
  "都城" : "450030",
  "高千穂" : "450040",
  "鹿児島" : "460010",
  "鹿屋" : "460020",
  "種子島" : "460030",
  "名瀬" : "460040",
  "沖縄" : "471010",
  "那覇" : "471010",
  "名護" : "471020",
  "久米島" : "471030",
  "南大東" : "472000",
  "宮古島" : "473000",
  "石垣島" : "474010",
  "与那国島" : "474020"
}
```

# 動作確認
* メッセージを送ってみて、お天気が帰ってきたらOK。

![line](https://qiita-image-store.s3.amazonaws.com/0/41761/ba72ca2e-880d-8b8b-e359-57e09b1600c2.gif)

# 感想
* ということでサーバがなくても簡単に作ることができました。
* まだメッセージ内容次第では判別が効かないので自然言語処理を入れて精度をあげたいところ。
* お天気APIではなくて他のAPIにしたら色々できそう。