Google HomeにLINE@経由でクックパッドのレシピの工程を読んでもらう

[Ateam Hikkoshi Samurai Inc. Advent Calendar 2017](https://qiita.com/advent-calendar/2017/hikkoshi) 18日目は[エイチーム引越し侍](http://hikkoshi.a-tm.co.jp/)の飲んだくれおっさんエンジニア @hironey が担当します。

時代の流れに乗って、Google Home の活用例として、クックパッドのレシピを LINE@ を使って読み上げさせる、というものを作ってみました。

# 背景

今年の Qiita Advent Calendar の記事を書こうと思っていた方々のあるあるだと思うのですが、ご多分に漏れずウチにも Echo の招待が来ず、記事ネタの当てが外れました…
途方にくれていたところでひょんなことから Google Home を借りることができたので、2017年 Qiita Advent Calendar のレッドオーシャンの様相を呈してきている Google Home ネタを恐れ多くもぶっこみます。

AIスピーカーの活用のアイデアを考えたときに、まっさきに浮かぶのは家電との連携ですが、Google Home であれば [IFTTT](https://ferret-plus.com/7940) で、Echo であればスキルでいわゆるスマートリモコンにつなぐ環境がすでに用意されているので先人の方々の記事を参考にすればさくっとできそうです…
うちにもスマート家電コントローラーなるもの（[REX-WFIREX2](http://manual.ratocsystems.com/wfirex/#page3)）があるのですが、Alexa のみの対応で IFTTT には対応してませんでした。Echo が我が家に来るのを待ちます。
「アレクサ、ちょっと暑い」「はい、エアコンの温度を下げますね」ってのをやりたい。

で、他にAIスピーカーが活きるのはどんな時か、と考えていくと、手が使えない時に声だけで操作できたらうれしいな、と思い、それはどんな時か考えていったときに、ひとつ思い出しました。

週末に料理をする際に、クックパッドにお世話になることが多いのですが、料理中はどうしても手が汚れていたり、濡れていることが多く、スマホの操作がめんどい、というか、小麦粉がつきまくったり、危うく水没しかけたり、、、というリスキーな状況です。

そんな状況で**声だけでレシピの手順を読み上げてくれたら！**というのが今回のお題です。

# 設計

まずは何の料理を作るかのレシピ探しですが、これは今まで通りスマホでの操作が良いです。AIスピーカーとのやり取りの方がまどろっこしい。手汚れてないし。

というわけで、レシピの決定までは普通にクックパッドで探して、それを Google Home に読み上げさせたい。
がっつりやるには [Actions on Google](https://developers.google.com/actions/) で Google Assistant と連携させるみたいなんだけど、IFTTTでさくっとやりたい。
ただ、IFTTT だと動的にテキストを返してそれを Google Home に話させることはできなさそう。任意の文章を話させるには、家の中のネットワークにいる Node.js からキックしてテキストを渡す必要がある。

ふむ。どうしようか。。。
（飲みながら）小1時間考えて決定したフローは以下の通り。

レシピ書き込み

1. クックパッドで作るレシピを探して、「レシピを共有」から URL を LINE で送る
2. それを LINE@ の bot で受け取って webhook でプログラムを呼び出す
3. 呼び出されたプログラムでその URL からクックパッドのページを取得してスクレイピングしてレシピの手順を取得し Google Spreadsheet に書き込む

ここまでが書き込み側で、以下が Google Home に読ませる側

4. Google Home に「レシピを読んで」と呼びかける
5. IFTTT 経由で Google Spreadsheet の特定のセルを更新
6. Spreadsheet の更新イベントで Google Apps Script（以下GAS）を起動
7. GAS のプログラムがレシピの手順のテキストを [ngrok](https://qiita.com/kitaro729/items/44214f9f81d3ebda58bd) 経由で Node.js へPOST
8. 家のMacbookで起動している Node.js がテキストを受け取って、google-home-notifier 経由で Google Home へ投げる
9. Google Home がレシピの手順を読み上げる
10. 「次の手順を読んで」「前の手順を読んで」で手順を前後させる機能も必要。

Google Home との一回一回完結される会話では状態を持てないんだけど、「7」の機能を実現するために、状態を Spreadsheet 側に持たせたのが今回のポイント。

# やったこと

実装の手順を逐一書いていくと一記事としてはものすごい量になりそうなのでありがたい先人へのリンクでほぼすませます。すません。

## まずは書き込み側
何経由で spreadsheet にレシピを伝えるのが良いか考えていた時にたまたま話してたグループ会社のエイチームブライズの @kakakaori830 が「やっぱ使い慣れた LINE で送るのが良いよね。LINE@ 経由の bot でいけるんじゃない？」と言い、翌日には「できたよ」と超絶スピードで実現してきたのでありがたくほぼそのまま利用させて頂きました。ありがとう！

### LINE@のアカウントを作成
@kakakaori830 の[こちらの記事](https://qiita.com/kakakaori830/items/d2c44a4469be39a51bc7)を参考に LINE@ の Developer アカウントを作り、「Messaging API設定」で Messaging API を有効にします。それから LINE Developers へ移り、「Messaging API をはじめる」で新規channel（＝ボットアプリ）を作成します。

ボットができたら、「Webhook送信」を「利用する」にして、「Webhook URL」に後ほど作成する AWS Cloud9 のURLをセットします。Webhook は https が必須になっているので注意。
この設定ページにあるQRコードを自分のスマホで読むなりして、このボットと友達になっておきます。

また、ボットに話しかけた際に、なんらか返答するのであれば Webhook の設定の上にある「アクセストークン（ロングターム）」が必要なのでコピペしておきます。

これで、ボットに話しかけると、そのテキストを webhook で外部URLに POST するところまでいけました。
ここではコードは何も書いてません！

### AWS Cloud9 のアカウントを作成
次にPOSTされる側を作ります。
上記記事では cloud9 で作ったプログラムを heroku に deploy して動かしていますが、今回はそのまま cloud9 で動かしました。

AWS のアカウントがあればそのまま使えると思っていたら、どうやら別で登録が必要っぽい。
[このあたりの記事](https://www.sejuku.net/blog/385)を参考に。

### Google Spreadsheet への外部からの書き込み準備
外部から Google API を呼び出して Spreadsheet を更新する方法は[こちらの記事](http://www.sharkpp.net/blog/2016/09/22/how-to-use-google-spreadsheets-api-for-php.html)を参考にさせてもらいました。今回は PHP で。

認証情報を json 形式のファイルでダウンロードしておきます。

### 書き込みプログラム
上記で発行した Cloud9 で作っていきます。
今回は PHP で作成したので、Google API は以下の公式のライブラリを使っています。
https://github.com/google/google-api-php-client/
（前項の Spreadsheet の記事に記載があります）
DOM操作には[PHP Simple HTML DOM Parser](https://so-zou.jp/web-app/tech/programming/php/library/simplehtmldom/)を利用させて頂きました。

LINEのボットから送られてきたメッセージからクックパッドのURLを抜き出し、ページのHTMLを取得します。
DOM操作でレシピの手順のところを抜き出すのですが、CSSで「#steps」というIDの中の「.step_text」という class の innerText を順にとってくるだけ、でいけました。ありがたや。
（勝手にスクレイピングとかしてごめんなさい。プレミアムサービス会員なのでお許しを…）
こんな感じ。エラー処理とか諸々は省いてます。

```php
// 受信したメッセージ情報を取得
$raw = file_get_contents('php://input');
$receive = json_decode($raw, true);

$event = $receive['events'][0];
$reply_token  = $event['replyToken'];
$messageText = $event['message']['text'];
$messageType = $event['message']['type'];

// cookpad のURLを抜き出す
preg_match('(https://cookpad.com/recipe/\d+)', $messageText, $results);
if (!$results[0]) exit;

// ページ取得
$html  = str_get_html(file_get_contents($results[0]));

// 手順だけ抜き出し
foreach ($html->find('#steps .step_text') as $step) {
  $steps[] = html_entity_decode(strip_tags($step->innertext));
}
```
次にスプレッドシートに書き込みます。
例えば、B列の2行目から順に手順のテキストを書き込む場合はこんな感じ。
なお、`CREDENTIALS_PATH` は前項でダウンロードした認証情報のファイル名を事前に定義してあるものです。

```php
putenv('GOOGLE_APPLICATION_CREDENTIALS='.dirname(__FILE__).'/'.CREDENTIALS_PATH);

$client = new Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope(Google_Service_Sheets::SPREADSHEETS);
$client->setApplicationName('test');

$service = new Google_Service_Sheets($client);
$value = new Google_Service_Sheets_ValueRange();

// スプレッドシートに書き込む
foreach ($steps as $key => $step) {
  $value->setValues([ 'values' => [ $step ] ]);
  $response = $service->spreadsheets_values->update(SPREADSHEET_ID, 'シート1!B'.($key+2), $value, [ 'valueInputOption' => 'USER_ENTERED' ] );
}
```
これで無事にスプレッドシートに書き込まれてるはずですが、動いたかどうか分からないので、返事をしてあげます。
`#accessToken` はLINE@の項で出てきた「アクセストークン（ロングターム）」です。

```php
// LINEで返信する
$message = array('type' => 'text',
                 'text' => '登録したよ');
$headers = array('Content-Type: application/json',
                 'Authorization: Bearer ' . $accessToken);
                
$body = json_encode(array('replyToken' => $reply_token,
                          'messages'   => array($message)));
$options = array(CURLOPT_URL            => 'https://api.line.me/v2/bot/message/reply',
                 CURLOPT_CUSTOMREQUEST  => 'POST',
                 CURLOPT_RETURNTRANSFER => true,
                 CURLOPT_HTTPHEADER     => $headers,
                 CURLOPT_POSTFIELDS     => $body);

$curl = curl_init();
curl_setopt_array($curl, $options);
curl_exec($curl);
curl_close($curl);
```
レシピの準備はこれで完了。
作りたいレシピが決まったら、Cookpadのアプリから「レシピを共有」ボタンを押して、LINEを選択し、友達になっておいたボットを選んで送信すると、Spreadsheet に書き込みが行われ、「登録したよ」と返事してくれます！

## 次に読み込み側

### スマホへの Google Home アプリインストール
まずは Google Home の初期設定をアプリ経由で行います。Wifi への接続とか。
このアプリ経由で後ほど必要な Google Home のIPアドレスを確認します。

### google-home-notifier の構築
Google Home を喋らすためのライブラリ[google-home-notifier](https://github.com/noelportugal/google-home-notifier)を稼働させます。
Node.js で動くので、本当は最近めっきりホコリを被っているだけのラズパイでやりたいところなんですが、借り物の Google Home だしとりいそぎ Macbook でいきます。

Node.js の導入
https://qiita.com/ebisennet/items/15c3c9fdb04e66db6323

```bash
$ curl -L git.io/nodebrew | perl - setup
下記の行を .bashrc に入れ、.bash_profile から .bashrc を呼び出すようにする
export PATH=$HOME/.nodebrew/current/bin:$PATH
$ nodebrew install-binary stable
$ nodebrew ls
$ nodebrew use v8.9.3
$ node -v
```
google-home-notifier をインストール

```bash
$ npm install google-home-notifier
```

Google Home のIPアドレスをスマホのアプリで調べて`example.js`に設定すれば大丈夫ですが、日本語を喋らせたいので言語を指定しておきます。
`example.js`の先頭の方で以下のように設定します。

```
var deviceName = 'Google Home';
var lang = 'ja';
googlehome.device(deviceName, lang);
googlehome.ip('192.168.xxx.xxx', lang);
```

下記コマンドで起動できます。

```bash
$ node ./example.js
```
ちなみに、起動すると、ngrok のURLが表示されるので、それをGASの呼び出し先ホスト名として使います。
ngrok を使うと、node.js の起動のたびにホスト名が変わるので注意。

### IFTTT アカウント作成
Google Home にコマンドを指定し、外部サービスとの連携をするために[IFTTT（イフト）](https://ifttt.com)でアカウント発行します。
IFTTTの初期導入の説明は[ここ](https://ferret-plus.com/7940)が丁寧でした。

今回は「New Applet」で
「This」は「Google Assistant」の「Say a simple phrase」を、
「That」は「Google Sheets」の「Update cell in spreadsheet」を選択。

ここで2箇所ハマったんですが、一つ目は、Google Home に対する IFTTT での「What do you want to say?」の部分。つまり、コマンド発火の発言内容。
「手順」がまーまーの確率で「お役にたてない」と言われる…
IFTTT の「Say a simple phrase」では一つのコマンドに対して２つの代替発言を登録できるので、「手順」ではなく「工程」と入れたり、「を」などの助詞を省いたものを登録することで認識ミスを減らしました。

もう一点は That 側の「Update cell in spreadsheet」で、path が何を指しているのか分からなかった。
今回は先に Spreadsheet を作成してそれを IFTTT から読ませようとしてハマったけど、IFTTTでなければ勝手にファイルが作られることに気がついて、IFTTTで作成されたものにGASを埋め込むことで解決しました。ちなみに、path を指定しなければ「IFTTT」というフォルダ以下にファイルが作られます。
また、「Spreadsheet name」というのがファイル名です。

今回は、固定のセル（例えば A1）にコマンドごとに特定の文字列を入れることで、GAS側でその文字列を判定して希望の動作をさせることをしました。

What do you want to say?
→ レシピを読んで
What do you want the Assistant to say in response?
→ はい、読みます
Which cell?
→ A1
Value
→ cur

今回は上記に加えて以下の２つの simple phrase な applet を登録しました。
「次の手順を読んで」でセルA1 に「next」を、
「前の手順を読んで」でセルA1 に「prev」という文字列を入れる。


### Spreadsheet 変更での GAS 起動

[この記事](https://tonari-it.com/gas-trigger-changed/
)を参考に。
Google Spreadsheet のスクリプトエディタのメニューの「リソース」→「現在のプロジェクトのトリガー」からウィンドウを開いて、「新しいトリガー」をクリックしてシート全体へのトリガーで実行される function を登録。

その function でごにょごにょする。
今回はこんな感じ。
IFTTTからのコマンド入力用のA1と、読み上げる現在行を保持する C1 と、手順のテキストが書き込まれている B列があります。

```javascript
function myFunction() {
  var sheet = SpreadsheetApp.getActiveSheet(); //シートを取得
  var cell = sheet.getActiveCell(); //アクティブセルを取得
 
  if(cell.getColumn() == 1 && cell.getRow() == 1){ //アクティブセルが A1 かを判定
    var rowCell = sheet.getRange('C1');
    var curRow = rowCell.getValue(); // 現在の読み上げ行を取得
    
    var command = cell.getValue();
    switch (command) { // コマンドにより読み上げ行を変更
      case 'cur':
        break;
      case 'prev':
        curRow--;
        break;
      case 'next':
        curRow++;
        break;
    }
    rowCell.setValue(curRow);
    var readCell = sheet.getRange(curRow, 2); // B列の読み上げる行のセルを取得
    var txt = readCell.getValue();
    if (txt == '') {
      txt = 'もう出来上がってます'; // 工程が終了している場合
      rowCell.setValue(2);
    }      
    cell.setValue('');
    sendGoogleHome(txt);
  }
}
```

1. IFTTT からは A1 セルに cur, next, prev の３つのうちのどれかの文字列を書き込む。
2. 更新トリガーで起動したGASで、アクティブセルが A1 かどうかをチェックする。A1 だったら、
3. その文字列をチェックして、その値により C1 の現在行の数字を加減する。
4. 「現在行」の数字の行のB列のテキストを取得。（デフォルトは２行目）
5. そのテキストを後述の ngrok 経由で Node.js へ POST する。sendGooglehome()関数部分。

```javascript
function sendGoogleHome(message){
  var payload =
  {
    "text" : message
  };
  var options =
  {
    "method" : "post",
    "payload" : payload,
  };
  var response = UrlFetchApp.fetch("https://xxxxxxx.ngrok.io/google-home-notifier", options);
  for(i in response) {
    Logger.log(i + ": " + response[i]);
  }
}
```
`xxxxxxx.ngrok.io`の部分はご自身のホスト名に読み替えてください。

この GAS から ngrok 経由で Node.js を叩くのが何度やってもうまくいかなくてハマったんですが、権限を付与していなかったことが原因でした。
一度、スクリプトを手動で実行したら「外部サービスへの接続」の権限追加のダイアログが出て、そこで許可ボタンを押したらそれ以降、更新イベントによる自動実行でも呼び出せるようになったのでご参考までに。

### Google Home を喋らす
Google Home を外部からPOSTして渡したテキストを喋らすだけであれば、google-home-notifier 付属の example.js でそのままいけます。
前述の node で example.js を指定して起動しておくだけ。

以上！

# 結果
(・∀・)ｲｲ!!
こちらの手が汚れてる時に一生懸命レシピを読み上げてくれるのはありがたい。

ありがたいんだけど、ちょっとタイムラグが…

「読んで」と言ってから IFTTT の実行が終わるのが2秒ほど。
Spreadsheet への変更を検知して GAS が起動して、手元の Node.js へリクエストが来るまでが 4秒。
そこから喋りだすまでに2秒程度かかって、合計で 8秒ちょいかかる感じ。
まどろっこしい。
調理中はたいてい急いでるので、レスポンスはなんとかしたいところ。
しかも、なんだか訛ってるので気になってレシピが頭に入ってこないのも致命的ｗ

まどろっこしいフローにしちゃったのでレスポンスが悪くなっちゃったのだと思うけど、おかげでいろんな初めてのサービスを触ることができたので結果的には良かったな、と。
触っているうちにどんどん楽しくなっていきました！
それに、ちょっと訛ったような Google Home の喋り方が愛おしくなってきた :-)

次は Echo 触りたい！

# 最後に
以上、[Ateam Hikkoshi Samurai Inc. Advent Calendar 2017](https://qiita.com/advent-calendar/2017/hikkoshi) 18日目でした。最後まで読んで頂いてありがとうございました。いかがでしたでしょうか。
明日は[エイチーム引越し侍](http://hikkoshi.a-tm.co.jp/)で一記事あたりの獲得いいね数の最高記録を持つ @shgkt が今日の記事でも活用した GAS 関連の記事を書くそうです。お楽しみに！

株式会社エイチーム引越し侍では、一緒にWebサービスを、事業を、会社を創っていく、そして成長させていく仲間を募集しています。私のように40越えたおっさんエンジニアでも、がしがしコード書いてますので、プログラマ35歳定年説に疑問や不安をもってる方も、エイチームグループに興味を持った方も是非、以下のリンクをクリック！
> [エイチームグループ採用サイト（Web開発エンジニア職）](http://hikkoshi.a-tm.co.jp/recruit/request.php?ea=hironey_googlehome)