ディズニーの待ち時間を教えてくれるLINE botを作ってみた

# はじめに
ディズニーの待ち時間をリアルタイムで教えてくれるサイトやアプリはよく見かけていたのですが、
LINEでのBotはあまり聞かないなーと思い、今回作ってみました。

![memo.jpg](https://qiita-image-store.s3.amazonaws.com/0/203817/c19b73dd-2e3b-ddeb-e189-07bf7e9a6a0e.jpeg)


個人的意見ですが、LINEの利用側として嬉しいのは、スマホが速度制限にかかっていても強いところかなと思います。
ですので、今回はカルーセルタイプ等は使わず文字列操作のみ、とにかく手軽に使えるよう機能は最小限に抑えてみました。

ユーザ操作は以下の通り。
1. botを開くと、リッチメニューが表示される
2. 「ランド」か「シー」か選ぶ
3. 選んだ方の現在の待ち時間が一覧表示される

シンプルですね。ユーザが行うことは2つのボタンのどっちかをポチるのみです。

今回は以下のサイトで待ち時間の情報がまとめられていたので、
こちらをスクレイピングして情報を頂かせてもらいました。

[ﾃﾞｨｽﾞﾆｰﾗﾝﾄﾞのﾘｱﾙﾀｲﾑ待ち時間](http://tokyodisneyresort.info/smartPhone/realtime.php?park=land&order=wait)

サーバ側の処理は以下の通り。
1. リッチメニュータップ時に固定メッセージを送信(ランドorシー)
2. 上記URLをスクレイピングし、情報取得
3. 文字列操作・必要な情報の整形
4. LINEへのメッセージ返却

# 作成したLINE Bot
先に作成したLINE Botを掲載しておきます。
よかったら友達登録して、速度制限時のディズニーなどで使ってみてください。

![d-suke-botQR.png](https://qiita-image-store.s3.amazonaws.com/0/203817/b8b8fb85-f6a6-d088-75b8-791c9cb55ee3.png)

[LINE友達追加](https://line.me/R/ti/p/%40mqf4207d)

# 開発環境
- npm 5.6.0
- Node.js v8.1.3
- LINE Messaging API
- Heroku

# 作成過程

## LINEbotアカウントの作成(Messaging API)
まずはLINE DevelopersでBotアカウントを作成します。
[LINE Developers](https://developers.line.me/console/register/messaging-api/provider/)
初回の方は新規プロバイダー作成からプロバイダーの設定を先に行ってください。

Messaging APIには、pushタイプとreplyタイプの2種類のメッセージ送信方法がありますが、
今回はreplyのみですので、フリーのプランにしています。

一通り登録が終わると、以下のような画面になるかと思います。

![linebot.png](https://qiita-image-store.s3.amazonaws.com/0/203817/f5fac38f-d38b-a3f5-dddf-2898e3bb63cb.png)


この際、Webhookの利用は「利用する」にし、Webhook先を今から作成するアプリのURLにしておきましょう。
また、
- Channel ID
- Channel Secret
- アクセストークン

は後ほど使うのでメモしておいて下さい。

## LINE@の設定変更
このBotでは、デフォルトのリッチメニューも利用したいため、LINE@の管理画面から
それらの設定も行っていきます。

[LINE@管理画面](https://admin-official.line.me/)

こちらも登録がまだの場合は先に済ませて下さい。

![lineatto.png](https://qiita-image-store.s3.amazonaws.com/0/203817/07118a22-b099-1636-6fcf-891fd17602a7.png)


作成し、LINE Developersで作成したアカウントに飛ぶと、上記のような画面になります。
ここから、「リッチコンテンツ作成」→「新規作成」と進みます。

表示設定を「反映する」にし、タイトル、表示期間等を設定します。
このBotでは、
初期表示メニュー：表示あり
テンプレート選択：画像で作成
としています。

![linecontent.png](https://qiita-image-store.s3.amazonaws.com/0/203817/acbe6054-5190-6b48-3f41-a39316ac300b.png)


下側にあるコンテンツ設定でエリアタップ時の動作を
- URL
- テキスト
- キーワード
から選択できます。

今回はテキストで、左半分だと「>Dランド」、右半分だと「>Dシー」と
テキストを返すよう設定しました。

## Herokuの設定
### Herokuとは
Paasの一つで、クレジットカードなどの登録がなくとも利用可能なお安いサービスです。
DBやcronなどのアドオンを利用したい場合にはクレジットカードの登録が必要ですが、今回は無しで大丈夫です。

### Herokuへのデプロイ
Herokuのリポジトリはgitでの管理となるため、CUIから簡単にデプロイできます。
`https://git.heroku.com/{リポジトリ名}.git` といったgit URLになります。

このURLをそのまま使うのであれば、

```
git clone https://git.heroku.com/{リポジトリ名}.git
```

アプリ作成後、

```
git add {変更したファイル}
git commit -m 'コメント'
git push https://git.heroku.com/{リポジトリ名}.git
```

でデプロイできます。

では、実際に作っていきましょう。

## botの作成
### ディレクトリ構成
構成は以下のようになっています。

```
┠ node_modules/
┠ routes/
┃  ┗ lineBot.js
┠ index.js
┠ package.json
┗ package-lock.json
```

まずは必要なものをインストールします。
LINEが出しているNode.jsのSDKがあるのですが、
元々慣れていたライブラリがあったので今回はそちらを利用してBotを作成します。

```
npm init
npm install --save express linebot body-parser cheerio-httpcli
```

※`cheerio-httpcli` というのは後ほどスクレイピングで利用するライブラリです。

package.jsonのscriptsには `node index.js` を追記しておいて下さい。

### ルーティング設定
まずはindex.jsから書いていきます。

```js:index.js
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const lineBot = require('./routes/lineBot');

app.set('port', (process.env.PORT || 8000));
app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json({
    verify(req, res, buf) {
        req.rawBody = buf
    }
}));
app.use(bodyParser.urlencoded({ extended: false }));

app.use('/',lineBot);

app.listen(app.get('port'), function() {
    console.log('Node app is running');
});
```

`app.use('/',lineBot);` の部分と、LINE Developersのwebhookが一致するように
webhook URLを設定しましょう。

この例では、800番ポートでlistenし、
このアプリのドメインのルートディレクトリにリクエストが来た際、
`routes`ディレクトリ配下の`lineBot.js`にイベントを渡しています。

### 各種イベントの反映
次に、lineBot.jsを見ていきます。

```js:lineBot.js
const express = require('express');
const linebot = require('linebot');
const router = express.Router();
const bodyParser = require('body-parser');
const bot = linebot({
    channelId: process.env.LINE_CHANNEL_ID,
    channelSecret: process.env.LINE_CHANNEL_SECRET,
    channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN
});

const parser = bodyParser.json({
    verify: (req, res, buf, encoding) => {
        req.rawBody = buf.toString(encoding);
    }
});

router.post('/', parser, (req, res, next) => {
    if (req.body.events === '') {
        return;
    }
    if (!bot.verify(req.rawBody, req.get('X-Line-Signature'))) {
        return res.sendStatus(400);
    }
    bot.parse(req.body);
    res.set('Content-Type', 'text/plain');
    res.status(200).end();
});

// 友達追加
bot.on('follow', (event) => {
    console.log('follow event');
});

// ブロック
bot.on('unfollow', (event) => {
    console.log('unfollow event');
});

//メッセージイベント
bot.on('message', (event) => {
    console.log('message event');
});

module.exports = router;
```

ここでは、
はじめに`body-parser`を用いてリクエストボディのパースを行い、
`linebot` ライブラリを用いて、LINEからの各イベントを非同期にタイプごとで
分けて受け止めるようにしています。

上記の例では、フォロー・ブロック・メッセージ時の
イベントのみ記載しました。

### WEBスクレイピング
次に、最初にお伝えしたWEBサイトから待ち時間の情報を拾っていきます。
まずは最上部に`cheerio-httpcli`を追加します。

```js
const express = require('express');
const linebot = require('linebot');
const router = express.Router();
const bodyParser = require('body-parser');
const cheerio = require('cheerio-httpcli');
```

先ほどのメッセージイベントを以下のように変更しましょう。

```js
bot.on('message', async (event) => {
    console.log('message event');

    if(event.message.type !== 'text') {
        return;
    }
    let replyMessage;
    if(event.message.text.indexOf('Dランド') !== -1) {
        replyMessage = await getWaitingTime("land");
    }else if(event.message.text.indexOf('Dシー') !== -1){
        replyMessage = await getWaitingTime("sea");
    }else {
        replyMessage = "待ち時間を取得するには、メニューからボタンをお選び下さい。";
    }
    event.reply(replyMessage);
});
```

スクレイピングするためのfunctionを1つ追加します。

```js
async function getWaitingTime(name) {
    const cheerioObject= await cheerio.fetch('http://tokyodisneyresort.info/smartPhone/realtime.php', {park: name, order: "wait"});
    let replyMessage = "";
    let lists = cheerioObject.$('li').text();

    lists = lists.trim().replace(/\t/g, "").replace(/\n+/g, ",").split(",");
    lists.forEach((list) => {
        if (list.indexOf("更新") !== -1){
            replyMessage += list;
        } else if (/FP|中|分|情報なし|案内/.test(list)){
            replyMessage += "\n" + list;
        } else {
            replyMessage += "\n\n" + list;
        }
    });
    return replyMessage;
}
```

最初のcheerio.fetchでGETのリクエストパラメータを渡せます。
その結果から、jQueryのように要素を指定して値を拾うことができます。

URLのDOM構造や拾い方次第でやり方は色々あると思うのですが、
自分にはいい取り方が思いつかなかったので、すべてのアトラクション名・待ち時間だけでなく
間のタブや改行まで全部ひっくるめて取る強引な形で取得しました。

力技感が否めないですが、その後の文字列操作で無理くり整形し、配列として格納しています。
そして最後に一つの文字列として改行を入れて調整しました。

ここまで行うと、以下のような形になっているかと思います。

```
19時52分現在のディズニーランド
スペース・マウンテン

60分[20:42更新]
（FP： 発券終了 ）
ビッグサンダー・マウンテン

60分[20:52更新]
（FP： 発券終了 ）
スプラッシュ・マウンテン
ｳｯﾄﾞﾁｬｯｸ･ｸﾞﾘｰﾃｨﾝｸﾞﾄﾚｲﾙ（ﾄﾞﾅﾙﾄﾞ）
30分[19:48更新]
ｳｯﾄﾞﾁｬｯｸ･ｸﾞﾘｰﾃｨﾝｸﾞﾄﾚｲﾙ（ﾃﾞｲｼﾞｰ）
:
:
```

あとはデプロイして完了です。

# github
Botのソースは以下に置いています。
https://github.com/daitasu/d-suke-bot

# まとめ
・無理くりでも文字列操作でスクレイピングはできる
・機能がシンプルでもBotはできる

今回はとにかくシンプルを目標にしましたが、
そのうちリッチメニューAPIかボタンタイプ等でエリア別の取得等はできるようにしたいですね。

あと、Herokuだと30分間アクセスがないとインスタンスがスリープしてしまい、
LINEのリッチメニュータップから返信まで5-8秒程度かかってしまいます。
もしEC2やLambda等が使える環境であればそちらの方が良さそうな印象です。

このBotでは、待ち時間の取り方がゴリ押しになってしまったので、
なにか良い案があればご教授お願い致します。