LINEのMessagingAPIとgoogle-home-notifierを使ってGoogleHomeを喋らせてみた

去年、某家電店でGoogleHomeMiniを購入してIFTTT(If this,then that)と連携させていろいろ遊んでいたのですが、今では「OK、Google。天気教えて」「ねぇグーグル、タイマー5分」とGoogleHomeMiniが申し訳なさそうにしています...
何が原因かを考えた結果、意外とGoogleHomeって他のサービスと連携させるのって難しいんですよね(技術的な面をあげてますが、アイデアの側面もあり)。IFTTTではThis(=トリガー)にはGoogleHomeが対応していますが、That(=アクション)は対応していないので、GoogleHomeに話しかけて(能動的)何かをすることは出来ても、GoogleHomeに話してもらう(受動的)にはひと工夫が必要です。
そこで手始めに、多くの人が使っているLINEを使ってGoogleHomeに喋らせてやることで我が家のGoogleHomeのプレゼンスを高めてやりたいと思います。

# 実現すること
* LINE Messaging APIを使って、LINEのアプリを作成。作成したLINEアプリ(トーク)に投稿したテキストをGoogleHomeに喋らせる
* 我が家のGoogleHomeのプレゼンスを向上させる

# 利用シーン 
* 「仕事が終わったので今から家に帰ります」という報告
* 「買ってくるものある？」という問いかけ
    * IFTTTを使えば簡単にGoogleHome→LINEにテキストメッセージ送れるので双方向でコミュニケーション取れます
* 「わたしメリー、今あなたの後ろにいるの」という驚かし

# 筆者について
仕事は組み込み、組み込みLinuxに近い仕事
趣味でWeb系触ったり、自宅にサーバ立てたり…
今回Node.jsを使いますが、Node.jsはドットインストールを1周した程度
つまづくところもありましたが環境構築の方です(そこが辛い)

# 前提
* Google Home(Miniも可)を持っている
* Node.jsをインストール済み
* ある程度Linuxコマンド使える

# 流れ
1. LINE Developersに登録してLINEアプリを作成
2. GoogleHomeと同じサブネットにSSLに対応したWebサーバを立てる(SSL証明書はLet's Encrypt、サーバはNode.js)
    - 自宅サーバが無理なら、VPS借りてVPNを使えば解決(こっちの方が敷居が高いかも)
3. Node.jsでアプリを作る
    - LINE Messaging APIのPOSTリクエストから投稿内容をパース
    - パースした内容をGoogle-home-notifierを使って喋らせる

# 1.  LINE Developersに登録してLINEアプリを作成
ここは割愛し、参考にしたページを貼ります。
この記事を読ませて頂いたときに「あ、出来そう」と思いました。
[LINE Messaging API を使用して、会話の「くっころ」という言葉に反応して「くっころ」してしまうBOTを作成する方法](https://qiita.com/k_nakayama/items/18a437e9c837ec11da6d)

# 2.  SSLに対応したWebサーバを立てる(Node.jsで)
まずは以下のページを参考にして下さい(やる気あるのか)
[Let's Encrypt 総合ポータル](https://letsencrypt.jp/usage/)
[ExpressでもLet's Encryptしたい](https://qiita.com/insight3110/items/68a7256da4da203cc9ab)

筆者はここで一度躓きました。
Lets's encryptが提供しているSSL証明書の発行をリクエストするcertbotですが、既にWebサーバが稼働している環境とそうでない環境でオプションが違います。筆者はWebサーバの稼働を止めて、--webrootのオプションを使用してしまい苦しんでいるとリクエスト数の制限に引っかかってしまいタイムロスしました。

* 既にWebサーバが稼働している場合

```bash:bash
certbot certonly --webroot -w /var/www/html -d example.jp
```

* Webサーバが稼働していない場合

```bash:bash
certbot certonly --standalone -d example.jp
```

# 3. アプリを作る
ここまででLINE Messaging APIを使ってアプリを作るための環境が整いました。
Node.jsでRESTを簡単に扱いたいので、Expressというフレームワークを使いました。

* Node.jsを使ってプロジェクトの初期化とExpressのインストールまで

```bash:bash
mkdir your_project
cd your_project
npm init # Node.jsを使ったプロジェクトの初期化。プロンプトに回答していく。
npm install express # expressのインストール
```

* google-home-notifier依存ライブラリのインストール

ちゃんとドキュメントを読んでおらず、google-home-notifierのインストールでタイムロス。

```bash:bash
sudo apt-get install git-core libnss-mdns libavahi-compat-libdnssd-dev
```

* google-home-notifierのインストール

```bash:bash
npm install google-home-notifier
```
もしくはgitから取ってくる。

```bash:bash
git clone https://github.com/noelportugal/google-home-notifier
cd google-home-notifier
npm install
```

## app.jsを編集
コードは以下の通り

```javascript:app.js
const express = require('express');
const path = require('path');
const favicon = require('serve-favicon');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const crypto = require("crypto");
const fs = require('fs');
const conf = require('./conf');
const https = require('https');
const googlehome = require('google-home-notifier');


// load config
const env = process.env.NODE_ENV || "development";
const config = require(path.join(__dirname, './', 'config.json'))[env];

const lineUrl = config.lineUrl;
const lineChannelAccessToken = config.lineChannelAccessToken;
const lineChannelSecret = config.lineChannelSecret;
const serverPort = config.serverPort;
const googleHomeIp= '192.168.x.x';

// load letsencrypt files.(build https server)
var options = {
  key: fs.readFileSync(conf.key),
  cert: fs.readFileSync(conf.fullchain)
}


var app = express();
// initialization
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// google-home config
var deviceName = 'xxx'; // これ不要かも
var language = 'ja';
googlehome.device(deviceName, language); // これ不要かも
googlehome.ip(googleHomeIp, language); // これだけあればGoogleHome検知可能?

var urlencodedParser = bodyParser.urlencoded({
  extended: false
});

// run server
var server = https.createServer(options, app);


console.log('Server is online. Port:' + serverPort);

// Validate signature
const validate_signature = function(signature, body) {
  return signature == crypto.createHmac('sha256', lineChannelSecret).update(new Buffer(JSON.stringify(body), 'utf8')).digest('base64');
};

// recieve POST message. Endpoint /webhook
app.post('/webhook', urlencodedParser, function(req, res) { // /webhook はLINE Messaging APIのPOSTリクエストの設定に合わせること
  // Validate signature LINEアプリからのメッセージであることの検証
  if (!validate_signature(req.headers['x-line-signature'], req.body)) {
    console.log("Error:Validation");
    res.status(401);
    return null;
  }
  // POSTリクエストから必要なテキストメッセージを取り出す
  var webhookEventObject = req.body.events[0];
  var text = webhookEventObject.message.text;
  console.log("text:" + text);
  if (text) {
    try {
      // GoogleHomeにリクエスト
      googlehome.notify(text, function(notifyRes) {
        console.log("Success");
        console.log(notifyRes);
        console.log(deviceName + ' will say: ' + text);
        res.send(deviceName + ' will say: ' + text + '\n');
      });
    } catch (err) {
      console.log("Error");
      console.log(err);
      res.sendStatus(500);
      res.send(err);
    }
  } else {
    console.log("else phase");
    res.send('Please POST "text=Hello Google Home"');
  }
  // response OK
  res.writeHead(200, {
    "Content-type": "text/plain"
  });
  res.end();
});
```

設定ファイル群

```bash:server.conf
exports.key='/etc/letsencrypt/live/your.domain.path/privkey.pem';
exports.cert='/etc/letsencrypt/live/your.domain.path/cert.pem';
exports.chain='/etc/letsencrypt/live/your.domain.path/chain.pem';
exports.fullchain='/etc/letsencrypt/live/your.domain.path/fullchain.pem';
```

```bash:config.json
{
  "development": {
    "lineUrl": "https://api.line.me/v2/bot/message/reply",
      "lineChannelAccessToken": "xxx", # 自分の環境に合わせて変更する
      "lineChannelSecret": "xxx", # 自分の環境に合わせて変更する
      "serverPort":9000 # 自分の環境に合わせて変更する
  }
}
```
筆者のPOST先は以下の通り
![cap.PNG](https://qiita-image-store.s3.amazonaws.com/0/37268/ad47d372-98a7-c468-4046-abed8e808d1d.png)

## 動作確認

LINEで作成したLINEアプリを登録する。筆者はDevelopersページからQRコードを読み取って追加。
サーバを起動して、LINEアプリにテキストを投稿してGoogleHomeが喋ることを確認する。

```bash:bash
sudo node app.js # アプリの立ち上げ

# 正常に動作することを確認したらnohupをつけてログアウト後も動くように
sudo nohup node app.js
```

## 残っている疑問
筆者がハマった箇所はリクエストのパースの部分です。
とりあえずリクエストの中身を見ようとしてログに出したんですが、req.bodyでは取り出したいtextの箇所が「undefined」となってしまっていて必要なデータが取り出せず数時間ほどハマりました。
Wiresharkまで使って確認すると、値は入っていました。


```javascript
app.post('/webhook', urlencodedParser, function(req, res) {
  if (!validate_signature(req.headers['x-line-signature'], req.body)) {
    console.log("Error:Validation");
    res.status(401);
    return null;
  }
  console.log(req.body);
~中略~
```
LINE Messaging APIを解説しているQiita記事を見ながら、試しにreq.body.events[0]すると必要な値が取れました。
すごくもやもやしていたのですが、数時間ロスしてたので先に進めました。
※この記事書きながらまたもやもや。調査して分かり次第加筆します

```javascript
app.post('/webhook', urlencodedParser, function(req, res) {
  // Validate signature LINEアプリからのメッセージであることの検証
  if (!validate_signature(req.headers['x-line-signature'], req.body)) {
    console.log("Error:Validation");
    res.status(401);
    return null;
  }
  // POSTリクエストから必要なテキストメッセージを取り出す
  var webhookEventObject = req.body.events[0];
  var text = webhookEventObject.message.text;
```


リファクタたくさん出来そうなので、githubは後ほど公開したいと思います。
次回は、GASを使ってGoogleHomeに話す内容をスケジューリングしたいと思います。



# 最後に
今回初めての投稿だったので、いろんな方の記事を読み自分にスッと入る記事を参考にしながら作成しました。
皆さん分かりやすいイラスト等を貼られていて、どうやっているんだろう？何を使ってるんだろう？と(そこは調べなかった)
結局皆さんの超えられそうになくて参考文献の山になりましたm( .. )m
自由にアウトプットして、不特性多数の方の役に立つかもしれないことって仕事でなかなか無かったりするので、これからも投稿できたらなと思います。(この記事書くのに3時間かかりました。早く慣れよう…)