嫁と話すLineBotをつくる

[新卒専用 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/fresh)　の12日目です。

#作ろうと思ったきっかけ
菅原のびすけさんの[投稿記事](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)を読んで、自分でも作ってみたくなった。
という訳で、Botを作っていきたいと思います。

#どんなBotをつくる？
タイトルにもある通り嫁と話せるbotを作ろうと思います。
はいそこ！気持ち悪いとか言わない！:rage:
自分の理想の嫁を想定してもいいし、2次元のキャラでも全然オッケーですｗ:v:
恥を捨てて、自分好みの嫁を作りましょう！


#用意するもの
菅原のびすけさんの[投稿記事](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)を参考に以下のものを用意します。各種の細かい設定や詳細説明はそちらの記事を参考にしていただきたいと思います。

- Botのアカウント
    - [ここのページ](https://developers.line.me/ja/)から作成出来ます。自分のLineアカウントに登録しているメールアドレスとパスワードが必要です。
- Node.jsとnpm
    - [ここ](https://nodejs.org/ja/)からダウンロード出来ます。`nvm`があればなお良いと思います。自分のバージョンは、 node.jsが`8.9.0`で、npmが`5.5.1` で開発しています。
- ngrok
    - トンネリングするために必要なツールです。動作テストだけでデプロイしたくない場合に使います。ダウンロードは[こちら](https://ngrok.com/download)

いろいろと準備するものは多いですが頑張りましょう:blush:

#実装したい機能
- 「おはよう」の発言で挨拶を返してくれる
    - 返事に日にちと時間、さらに一言返すようにできればベスト
- 「おやすみ」の発言で挨拶を返してくれる
    - 返事とさらに一言返すようにできればベスト
- 「行ってきます」の発言で返事してくれる
    - 仕事が頑張れる一言を用意したい
- 「ただいま」の発言で返事してくれる
    - 帰ってきて良かったと思える発言をしたい
- その他何かあれば適宜追加

#コード
以下のコードが今回のベースになります

```server.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const PORT = process.env.PORT || 3000;

const config = {
    channelAccessToken: '', // アクセストークン
    channelSecret: '' // チャンネルシークレット
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  return client.replyMessage(event.replyToken, {
    type: 'text', // 返事は普通のテキスト
    text: event.message.text //自分の発言内容を返事とする
  });
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);
```
この時点はまだオウム返しをするだけです。これにどんどんコードを追記していきます。

で、実際に機能を追加したコードが以下です。
この後一つずつ説明していきます。

```server.js
'use strict';

const express = require('express');
const line = require('@line/bot-sdk');
const axios = require('axios');
const PORT = process.env.PORT || 3000;

const config = {
    channelAccessToken: process.env.CHANNEL_ACCESS_TOKEN,
    channelSecret: process.env.CHANNEL_SECRET
};

const app = express();

app.post('/webhook', line.middleware(config), (req, res) => {
    console.log(req.body.events);
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

function handleEvent(event) {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return Promise.resolve(null);
  }

  let repMes = ''; //返却用メッセージ
  let sendMes = event.message.text; // 送信されたメッセージ

  if (/おはよう/.test(sendMes)){
    // 発言に「おはよう」が含まれていた時のリプライ
    let date = getDate();
    repMes = 'おはようございます。あなた♪\n' + 
              `本日は ${date} です。\n` + 
              '今日も一日頑張りましょう！';
  } else if (/おやすみ/.test(sendMes) || /お休み/.test(sendMes)) {
    // 発言に「おやすみ」または「お休み」が含まれていた時のリプライ
    repMes = 'おやすみなさい。あなた♪\nまた明日。';
  } else if (/行ってきます/.test(sendMes) || /いってきます/.test(sendMes)) {
    // 発言に「行ってきます」または「いってきます」が含まれていた時のリプライ
    repMes = '行ってらっしゃい、あなた♪\nお仕事頑張ってくださいね。';
  } else if (/ただいま/.test(sendMes)) {
    // 発言に「ただいま」が含まれていた時のリプライ
    repMes = 'おかえりなさい、あなた♪\n今日もお仕事お疲れ様。';
  } else if (/今日の天気/.test(sendMes)) {
    // 発言に「今日の天気」が含まれていた時のリプライ
    repMes = '今日の天気ですね？\nちょっと待ってて下さい';
    getWeather(event.source.userId, 0);
  } else if (/明日の天気/.test(sendMes)) {
    // 発言に「明日の天気」が含まれていた時のリプライ
    repMes = '明日の天気ですね?\nちょっと待ってて下さい';
    getWeather(event.source.userId, 1);
  } else if (/ありがとう/.test(sendMes)) {
    repMes = 'ふふっ♪\nどういたしまして。';
  } else if (/好きです/.test(sendMes)) {
    repMes = '私も好きですよ。あなた♡';
  } else {
    // それ以外は発言をそのまま返す
    repMes = sendMes;
  }

  return client.replyMessage(event.replyToken, {
    type: 'text',
    text: repMes
  });
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);

/**
 * 現在の日付を取得する。
 * 
 * @return 日付 [月/日(曜日)]
 */
const getDate = () => {
  let today = new Date(); //今日の日付データ
  
  // 月日取得
  let month = today.getMonth() + 1;
  let day = today.getDate();
  let week = today.getDay();

  // 曜日格納用配列
  let weekDay = new Array("日", "月", "火", "水", "木", "金", "土");

  let formatDate = `${month}/${day} ${weekDay[week]}曜日`;

  return formatDate;
}

/**
 * 天気情報を取得し、ユーザにプッシュする
 * @param {*} userId プッシュ先のユーザID 
 * @param {*} dayId　いつの情報を取得するかを決める(0: 今日, 1: 明日)
 */
const getWeather = async (userId, dayId) => {
  // 天気情報(json形式)をAPIから取得
  const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=130010');
  const item = res.data;
  
  // 日付と地域名と天気を取得
  const city = item.location.city;
  const dayLabel = item.forecasts[dayId].dateLabel;
  const weather = item.forecasts[dayId].telop;
  const date = item.forecasts[dayId].date;
  
  await client.pushMessage(userId, {
    type: 'text',
    text: `${dayLabel}(${date})の${city}の天気は\n${weather}です。`
  });
}
```

##「おはよう」、「おやすみ」、「いってきます」、「ただいま」の発言に挨拶を返す
`反応するワード`に対して`testメソッド`を利用しています。このメソッドの引数の中に`反応するワード`が含まれていれば`true`と判定してくれます。
これで、自分の発言に特定の挨拶が含まれていれば、それに対応した返事をしてくれます。

```server.js
  let repMes = ''; //返却用メッセージ
  let sendMes = event.message.text; // 送信されたメッセージ

  if (/おはよう/.test(sendMes)){
    // 発言に「おはよう」が含まれていた時のリプライ
    let date = getDate();
    repMes = 'おはようございます。あなた♪\n' + 
              `本日は ${date} です。\n` + 
              '今日も一日頑張りましょう！';
  } else if (/おやすみ/.test(sendMes) || /お休み/.test(sendMes)) {
    // 発言に「おやすみ」または「お休み」が含まれていた時のリプライ
    repMes = 'おやすみなさい。あなた♪\nまた明日。';
  } else if (/行ってきます/.test(sendMes) || /いってきます/.test(sendMes)) {
    // 発言に「行ってきます」または「いってきます」が含まれていた時のリプライ
    repMes = '行ってらっしゃい、あなた♪\nお仕事頑張ってくださいね。';
  } else if (/ただいま/.test(sendMes)) {
    // 発言に「ただいま」が含まれていた時のリプライ
    repMes = 'おかえりなさい、あなた♪\n今日もお仕事お疲れ様。';
  } else if (/今日の天気/.test(sendMes)) {
    // 発言に「今日の天気」が含まれていた時のリプライ
    repMes = '今日の天気ですね？\nちょっと待ってて下さい';
    getWeather(event.source.userId, 0);
  }
```

###「おはよう」に対する返事に本日の日付を加える
「おはよう」に対する返事には今日の日時を取得するメソッド`getDate`を動作させている。
特に難しい処理を行っておらず、ただ日時を取得してフォーマットして返しているだけです。

```server.js
/**
 * 現在の日付を取得する。
 * 
 * @return 日付 [月/日(曜日)]
 */
const getDate = () => {
  let today = new Date(); //今日の日付データ
  
  // 月日取得
  let month = today.getMonth() + 1;
  let day = today.getDate();
  let week = today.getDay();

  // 曜日格納用配列
  let weekDay = new Array("日", "月", "火", "水", "木", "金", "土");

  let formatDate = `${month}/${day} ${weekDay[week]}曜日`;

  return formatDate;
}
```
## 今日、明日の天気を教えてくれる
「今日の天気」「明日の天気」が発言に含まれていると「ちょっと待って」という返事をして、天気を取得する処理`getWeather`を動作させます。
「今日の天気」、「明日の天気」では`getWeather`を呼び出すときの引数が違います。理由は後述します。

```server.js
 else if (/今日の天気/.test(sendMes)) {
    // 発言に「今日の天気」が含まれていた時のリプライ
    repMes = '今日の天気ですね？\nちょっと待ってて下さい';
    getWeather(event.source.userId, 0);
  } else if (/明日の天気/.test(sendMes)) {
    // 発言に「明日の天気」が含まれていた時のリプライ
    repMes = '明日の天気ですね?\nちょっと待ってて下さい';
    getWeather(event.source.userId, 1);
  }

```
こちらが`getWeather`の処理です。天気の情報を取得できるAPIを使用しています。
APIは`Weather Hacks`にあるAPIを利用しています。[詳細はこちら](http://weather.livedoor.com/weather_hacks/webservice)
天気情報はjsonで返ってくるので、必要な所だけ抜き取っています。
返ってくるjsonでは、`forecasts[0]`に今日の天気情報、`forecasts[1]`に明日の天気情報が格納されているので、`getWeather`を呼び出すときの第2引数に数字を入れて指定しています。

```server.js
/**
 * 天気情報を取得し、ユーザにプッシュする
 * @param {*} userId プッシュ先のユーザID 
 * @param {*} dayId　いつの情報を取得するかを決める(0: 今日, 1: 明日)
 */
const getWeather = async (userId, dayId) => {
  // 天気情報(json形式)をAPIから取得
  const res = await axios.get('http://weather.livedoor.com/forecast/webservice/json/v1?city=130010');
  const item = res.data;
  
  // 日付と地域名と天気を取得
  const city = item.location.city;
  const dayLabel = item.forecasts[dayId].dateLabel;
  const weather = item.forecasts[dayId].telop;
  const date = item.forecasts[dayId].date;
  
  await client.pushMessage(userId, {
    type: 'text',
    text: `${dayLabel}(${date})の${city}の天気は\n${weather}です。`
  });
}
```

#動作確認
実際に発言をしてみます。
因みにアイコンは、今友人に依頼して作成してもらっている最中です:confounded:

### おはよう
![ohayo.gif](https://qiita-image-store.s3.amazonaws.com/0/185028/deb15d21-c9ea-6604-9ac0-9fe9a9b879f3.gif)

### おやすみ
![oyasumi.gif](https://qiita-image-store.s3.amazonaws.com/0/185028/0fec8a21-9a83-4f07-2761-97125394de48.gif)

### いってきます
![ittekimasu.gif](https://qiita-image-store.s3.amazonaws.com/0/185028/cfa261cc-73fd-0151-7639-56560abaf496.gif)

### ただいま
![tadaima.gif](https://qiita-image-store.s3.amazonaws.com/0/185028/9d7fe585-5fa2-445b-943f-43ed575a50d1.gif)

### 今日の天気は
![todayWeather.gif](https://qiita-image-store.s3.amazonaws.com/0/185028/9c4904b3-ba26-f789-79c1-2c32b1afd50b.gif)

### 明日の天気は
![tomorrowWeather.gif](https://qiita-image-store.s3.amazonaws.com/0/185028/5c16b384-2b97-facb-eb2e-f34ed4f9bc40.gif)

どうやらしっかり動いてくれてるみたいです。やったぜ。:v::relaxed::v:


# デプロイ
now を使ってデプロイしました。
デプロイすると無料プランの場合、隠蔽しなくてはならないアクセストークンやチャンネルシークレットが丸見えなので、必ずデプロイ時に隠蔽するようにしてください！
隠蔽の方法は[こちら](https://qiita.com/hirorin/items/2ff28306711fe19b0b33)

QRコードを公開していましたが、私が管理しているbotアカウントなので私が好き勝手にいじれてしまいます。そのため、載せるのはやめました。
下のgithubからフォークして自分の環境で動かして下さい。


# github
今回組んだプログラムはgithubに上げました。
[こちら](https://github.com/Hirorin495/MyLineBot)

# まとめ
Node.js を利用して、嫁と話すLINEBotを作ることが出来ました。
仕事があって多くの時間が割けなかったので、機能は少ないですが、ここまで形に出来ただけでも嬉しいです。
準備するものは多めですが、どれも使い方は簡単で詰ることなくスムーズに開発に入ることができました。これなら他のbotを作る際も苦労しなさそうです。

悔やむのは、天気の情報の取得で動的に取得地域を設定出来ていないところです。現在のコードだと東京固定です。
「嫁と話すLineBot」ということをコンセプトに作っていたので、人物と話しているような形式で情報を取得したいと思っていました。なのでただ位置情報を送るのではなく、「今日の○○の天気どう？」みたいな発言で取得させるのが理想でした。
時間があれば実装を進めていこうと思っています。