LINEとGoogle Homeの双方向通信～GHKitとGoogle Apps Scriptを使って～

# 実現したこと

1. Google Homeに話したメッセージを、LINEグループに投稿する。
2. LINEグループに投稿したメッセージを、Google Homeで再生する。
3. Google Homeで聞き漏らしたメッセージを、もう一度再生する。

すでにたくさんの方が色々な方法で実現済みの内容ですが、ここでは[GHKit](http://ghkit.jp/)という買い物と、多くの人がアカウントを持っているであろうGoogleのサービスGoogle Apps Scriptを使って、**「お手軽に」**実装しようというのが狙いです。

# 経緯

我が家は、父ちゃん(私)、母ちゃん(かみさん)、子供(4歳)の3人家族です。まだ子供が独りで家に居るというシチュエーションはありませんが、小学生くらいになるとお留守番する可能性が出てきます。この時に、何かコミュニケーションを取れる手段が必要と考えていました。

「こいつを自由にしゃべらせられれば良いのに」と、天気予報とタイマーくらいしか活用していないリビングのGoogle Home Miniに目がとまり、方法を調べてみるものの「複雑…」とついて行けずに挫折。

が、ある時[GHKitが紹介された記事](https://robotstart.info/2018/01/12/tetotetote-ghkit-for-google-home.html)を読み、「これ使ったら、色々面倒なこと端折れるんじゃね？」と感じ(考えてない)、脊髄反射でポチ～。

その後、それなりに端折れてもどこかでプログラムを組む必要があることに気付き、ポチったことを後悔しながらも調べた結果、Google Apps Scriptで解決できそうなことが判明。開発を開始しました。

# 必要な知識、モノ、サービス

- **Google Apps Script**
    - ここで紹介する、キモとなるスクリプトです。
    - スクリプトはGoogleドライブに設置します。
    - Googleアカウントを持っていれば、無料で使用可能です。
    - JavaScriptの知識ゼロの人でも何とかなりました。

- **GHKit**
    - 買い物です。
    - 製品情報は[こちら](http://ghkit.jp/)をご覧ください。
    - これがないと話が始まりません。メーカーの手順通り初期設定を済ませてください。

- **LINE Messaging API**
    - アカウントを取って使える状態にしてください。
    - 一般的なLINE Botを作る手順で問題ありません。以下に挙げる参考文献にも載っていますので、ここでは詳しく触れません。
    - 違うのはWebhookのURLくらいです。

- **IFTTT**
    - アカウントを取って使える状態にしてください。(詳しくは触れません)
    - Google AssistantとWebhookのサービスを使用します。

# 参考文献

LINE <-> Google Home間の双方向通信が可能であることを知る。

- [Google Home mini で双方向の音声でのLINE送受信を実現する](https://qiita.com/ghoul_shana/items/7d62489240490f12f14a)
- [GoogleHomeとiftttとDialogflowとfirebaseとLINEBOTとラズパイを使って子供と音声によるLINE交換をしてみる（前編）](https://qiita.com/nk-tamago/items/b4039195ca4fc24a7f86)

LINE Messaging APIの入出力データを加工するのに、Google Apps Scriptが使えるという可能性に気付く。

- [【初心者向け】GASを使ってSlackへ自動通知](https://qiita.com/chihiro/items/c7b11abc78f5d806c3a8)
- [LINE Botをサーバーレスで開発！Google Apps ScriptとLINE Messaging APIを使ってチャットボットを作ってみた](http://inside.pixiv.net/entry/2016/11/14/150000)
- [LINE Messaging API と Google Apps Script で LINE BOT を作ってみる](http://kntmr.hatenablog.com/entry/2017/07/10/192927)

メッセージをGoogleスプレッドシートに保存すれば、Google Homeで聞き漏らしてももう一度再生できるかも、という野望に目覚める。

- [LINE Messaging API を使って、メールでラインができるbotを作成しました](http://freqmodu874.hatenadiary.com/entry/2017/04/06/171137)

GHKitのメッセージ型。

- [GHKitとIFTTTの連携手順](https://qiita.com/tetotetote/items/a7ba4d464e8574d452fd)

# 使い方とデータの流れ

## Google Home → LINE

Google Homeに話したメッセージを、LINEグループに投稿します。

![hometoline.jpg](https://qiita-image-store.s3.amazonaws.com/0/233188/61d657c5-3920-fcc3-a935-f4f5121d7d3b.jpeg)

"OK Google"に続くキーワード「LINE」は、IFTTTで自由に設定できます。後述の「コードと設定内容」をご覧ください。

## LINE → Google Home

LINEグループに投稿したメッセージを、Google Homeで再生します。

![linetohome.jpg](https://qiita-image-store.s3.amazonaws.com/0/233188/167ea150-100a-f46b-90be-995db7649c03.jpeg)

## Google Home → Google Home

Google Homeで聞き漏らしたメッセージを、もう一度再生します。

![hometohome.jpg](https://qiita-image-store.s3.amazonaws.com/0/233188/316e9848-1445-bdae-0a80-9867e3e860b0.jpeg)

「最後のLINE」の代わりに「最近のLINE」と入力すると、過去24時間以内のメッセージのうち直近の最大5件を再生します。

"OK Google"に続くキーワード「最後のLINE」「最近のLINE」は、IFTTTで自由に設定できます。後述の「コードと設定内容」をご覧ください。

# コードと設定内容

本題のGoogle Apps Scriptのコードと、周辺の設定です。

コード内で設定・変更が必要な箇所は、コメントに取得方法を記載しています。

その他、周辺の設定は、必要な内容のみをピンポイントで記載します。

## Google Apps Script

Googleドライブにて、「新規」→「その他」→「Google Apps Script」で開いたところに、以下のコードを入力してください。もし「Google Apps Script」が表示されない場合は、「アプリを追加」から追加してください。

```js
// GHKit
var postGHKit = 'http://ifttt.ghkit.jp/';
var GHKIT_ID  = '<GHKit ID>';  // GHKitの発送連絡メールに記載されているID

// LINE
var pushLINE  = 'https://api.line.me/v2/bot/message/push';
var CHANNEL_ACCESS_TOKEN = '<アクセストークン>';  // LINE Messaging APIのチャンネル設定画面で発行できるアクセストークン(ロングターム)

// LINE HomeグループID
    // LINE上で「Home」というグループを作成した。
    // グループのメンバーは、父ちゃん、母ちゃん、LINE Messaging APIのBotの三者。
    // このグループかつテキスト形式の投稿のみ、Google Homeで再生する。
var grpidHome = '<グループID>';

// LINE ID 一覧
    // 自分のIDは、LINE Messaging APIのチャンネル設定画面にあるYour user IDを参照。
    // 他人のIDは、一度投稿してもらって、スプレッドシートの記録から取得する。
var idUsers = {
  '<User ID 1>' : "父ちゃん",
  '<User ID 2>' : "母ちゃん"
};

// LINE -> Google Home メッセージ記録用のスプレッドシートID
    // Googleドライブで新規のスプレッドシートを作成する。(空でOK、共有設定も不要)
    // ブラウザのアドレスバーに表示される下記のようなURLから、スプレッドシートのIDを取得する。
    // https://docs.google.com/spreadsheets/d/<スプレッドシートのID>/edit#gid=0
var ssId = '<スプレッドシートのID>';

/*
 * POSTを受けるメインルーチン
 * LINE側からでも、Google Home側(IFTTT)からでもここに入る。
 */
function doPost(e) {
  // POST内容によって対応するアクションを仕分ける
  var actType = JSON.parse(e.postData.contents).events[0].type;
  
  /*
   * LINE側からPOSTされた場合
   * <アクション1> GHKitにメッセージを渡す (LINEのメッセージをGoogle Homeで再生する)
   */
  if(actType == 'message') {

    // グループのチェック
    var gid = JSON.parse(e.postData.contents).events[0].source.type;
    if(gid == 'group') {
      gid = JSON.parse(e.postData.contents).events[0].source.groupId;
    }
    if(gid == grpidHome) {    // Homeグループのみ対応
  
      // メッセージタイプのチェック
      var messageType = JSON.parse(e.postData.contents).events[0].message.type;
      if(messageType == 'text') {    // テキストメッセージのみ対応

        // メッセージの取得
        var messageText = JSON.parse(e.postData.contents).events[0].message.text;
  
        // 発言者の特定
        var id = JSON.parse(e.postData.contents).events[0].source.userId;
        if(id in idUsers) {
          var who = idUsers[id];
        } else {
          var who = '誰か';
        }
  
        // 内容をスプレッドシートに保存 (メッセージの問い合わせに対応するため)
        var ss = SpreadsheetApp.openById(ssId).getSheets()[0];  // スプレッドシートの選択
        if(who != '誰か') {
          ss.appendRow([new Date(), who, messageText]);     // 日時、発言者、メッセージを保存
        } else {
          ss.appendRow([new Date(), who, messageText, id]); // 不明なIDも保存(新規ユーザは手動でidUsersに追加する)
        }
  
        // GHKitへ送信
        var draft = 'ラインが来ました。' + who + 'から、' + messageText;
        sendGhkit(draft);

      } else {
        // テキストメッセージ以外の処理
      }
    } else {
      // Homeグループ以外の処理
    }

  }
  
  /*
   * Google Home側(IFTTT)からPOSTされた場合
   */
  else {
    
    // メッセージ本文を取得
    var messageText = JSON.parse(e.postData.contents).events[0].message;
    
    /*
     * <アクション2> LINEにメッセージを渡す (Google Homeで話したメッセージをLINEのトークに投稿する)
     */
    if(actType == 'googlehome_to_line') {
      
      // Homeグループへ送信
      sendLinePush(grpidHome, messageText);
      
    }
    
    /*
     * <アクション3> 再生済みメッセージをGHKitに返す (聞き逃したメッセージをGoogle Homeで再生する)
     */
    else if(actType == 'googlehome_request') {
      
      var ss = SpreadsheetApp.openById(ssId).getSheets()[0];  // スプレッドシートの選択
      var ssRows = ss.getLastRow();                           // 最終行番号の取得
      
      if(ssRows < 1) {                      // メッセージがない(スプレッドシートの記録が空)の場合
        sendGhkit('ラインはまだありません。');

      } else if(messageText == 'last') {    // <アクション3.1> 最後・最新(1件)のメッセージを再生する

        // スプレッドシートの最終行から日時、発言者、メッセージの取得
        var ssData = ss.getRange(ssRows, 1, 1, 3).getValues();

        // GHKitへ送信
        var draft = '最後のライン。' + getRequestMessage(ssData[0]);
        sendGhkit(draft);

      } else if(messageText == 'recent') {  // <アクション3.2> 最近(24時間以内の最大5件)のメッセージを再生する

        // スプレッドシートから最大5件の日時、発言者、メッセージの取得
        var recRows = ssRows;  // スプレッドシートの記録が5件以下の場合
        if(recRows > 5) {
          recRows = 5;         // スプレッドシートの記録が5件を超える場合
        }
        var ssData = ss.getRange(ssRows - recRows + 1, 1, recRows, 3).getValues();
        
        // 24時間以内のメッセージに絞る
        var nowDate = new Date();
        for(var i = 0; i < recRows; i ++) {
          var targetDate = new Date(ssData[i][0]);
          if((nowDate - targetDate) < (1000 * 60 * 60 * 24)) {  // ミリ秒単位
            break;
          }
        }
        
        if(i == recRows) {  // 24時間以内のメッセージがない場合
          sendGhkit('24時間以内のラインはありません。');
        } else {            // 24時間以内のメッセージがある場合
          // 対象となったメッセージをつなげる
          var recentMessage = ''
          for(var j = i, ct = 1; j < recRows; j ++, ct ++) {
            recentMessage += ct + '件目。' + getRequestMessage(ssData[j]) + '。'
          }

          // 件数などをまとめてGHKitへ一括送信
              // 1件ずつループで連続送信すると、最後の1件しか再生されなかったりするため。
              // どうやらGHKitは後から来た要求を優先するらしく、すでに再生中なら再生を止めてしまうように見える。
          ct --;
          var draft = '24時間以内に' + ct + '件のラインがありました。' + recentMessage + '以上です。'
          sendGhkit(draft);
        }

      } else {
        // Google Homeから不明なメッセージの問い合わせ処理
      }
    } else {
      // 不明なリクエストの処理
    }
  }
}

/*
 * サブルーチン群
 */

// スプレッドシートの記録から読み上げメッセージに変換
function getRequestMessage(dt) {
  return Utilities.formatDate(dt[0], 'JST', 'M月d日 H時m分') + '。' + dt[1] + 'から、' + dt[2];
}

// GHKitへ送信
function sendGhkit(message) {
  UrlFetchApp.fetch(postGHKit, {
    'headers': {
      'Content-Type' : 'application/json'
    },
    'method'  : 'POST',
    'payload' : JSON.stringify({
      'message' : GHKIT_ID + " " + message
    })
  });
}

// LINEへプッシュ (idはグループでもユーザでもプッシュ可能)
function sendLinePush(id, message) {
  UrlFetchApp.fetch(pushLINE, {
    'headers': {
      'Content-Type' : 'application/json',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
    },
    'method' : 'POST',
    'payload': JSON.stringify({
      'to'       : id,
      'messages' : [{
        'type' : 'text',
        'text' : message
      }]
    })
  });
}
```

入力が終わりましたら、メニューの「公開」→「ウェブアプリケーションとして導入」で公開します。

ハマる人は少ないと思いますが、「ウェブアプリケーションとして導入」ダイアログでは、「プロジェクトバージョン」を「新規作成」しましょう。忘れると変更が反映されません。(私はハマりました)

またこの時、「アプリケーションにアクセスできるユーザー」は「全員(匿名ユーザーを含む)」を指定する必要がありそうです。仕組みはよく分かっていません。(外部(LINE/IFTTT)から叩くから？)

最後に「ウェブアプリケーションとして導入」ダイアログで「更新」した時、表示されるURLをメモしておいてください。LINE Messaging APIやIFTTTで指定するWebhookのURLとなります。(一度発行されたURLは、スクリプトを修正しても変わらないようです)

## LINE Messaging API

LINE Messaging APIのチャンネル設定画面にあるWebhook URLに、Google Apps Scriptの公開時に表示されたURLを指定してください。

## IFTTT

以下の3つのアプレットを作成します。いずれのアプレットも、"this"にGoogle Assistantを、"that"にWebhookを指定しています。
WebhookのURL欄に、Google Apps Scriptの公開時に表示されたURLを指定してください。

### 1. LINEのHomeグループに送信

![toline.png](https://qiita-image-store.s3.amazonaws.com/0/233188/94c10856-6319-3858-6b8c-06128931c8af.png)

### 2. 最後のLINE(1件)の確認

![reqlast.png](https://qiita-image-store.s3.amazonaws.com/0/233188/3f38f508-d038-f08b-28ad-be262283b1d6.png)

### 3. 最近のLINE(最大5件)の確認

![reqrecent.png](https://qiita-image-store.s3.amazonaws.com/0/233188/d24b2d48-0a53-6617-03d5-69c2daa249e7.png)

# 最後に

**「お手軽に」**と言った割に、1画面で収まらないコードになってしまいました。問い合わせ機能を欲張ったからですね。

仕事でC言語を使い、perlを少しかじったくらいのスキルしか持っていませんが、GHKit到着でやらざるを得ない状況になってから1週間で、そこそこの形にまとめられたかと思います。(でもコードが正しいのかイマイチ分からない)

色々初めての経験ですので、何かございましたら優しく教えていただければ幸いです。