GoogleHomeに話しかけてLINEのトークルームへメッセージを送信する LINE Messaging API + Google Apps Script


実現した事

  1. LINEでの会話内容をGoogleHomeで読み上げる
  2. GoogleHomeに話しかけてLINEメッセージを送信
  3. GoogleHomeに直近メッセージの再読み上げさせる

経緯

娘が小学校にあがり、新しい環境になったことでいろいろなトラブルが起こりました。
・勘違いから学童に行かず授業後そのまま自宅に帰る。
・校門の前で待ち合わせをしたものの待ち合わせの意味がわからず先に自宅に帰る。

携帯電話を持たせていないこともあり、子供との連絡手段がない事に不安を感じて解決策を検討した結果
「GoogleHomeに話しかけて連絡を取ることができれば1年生の娘にも出来るんではないか!?」 という
結論に至り、今回のサービスの作成を行いました。

今回のサービスの仕組みに用いた技術・機材

  • GoogleHome
  • LINE Messaging API
  • Google Apps Script
  • IFTTT
  • RaspberryPi

RaspberryPiについては、以前に子供にお手伝いをさせる事を目的に「がんばりポイント」の累計値を問い合わせる
サービスを作成しており、その際にセットアップした環境を流用(google-home-notifierとngrokをセットアップ済み)

データの流れのイメージ

LINE→GoogleHome

GoogleHome→LINE

直近メッセージの読み上げ

コードや設定内容

LINE Messaging API

以下の内容でChannelを作成

基本情報
- アプリ名:LINEBotの名称(トークメンバーの名称)
- アプリ説明:LINEBotの説明
- ChannelID/ChannelSecret:今回は使用しない 他サービスとの連携時に使用する
- アプリタイプ:BOT(MessagingAPIを利用しLINEとサービスの双方向コミュニケーションを実現する)
メッセージ送受信設定
- アクセストークン(ロングターム):ここで発行したトークンをGoogle Apps Scriptの<アクセストークン>に貼り付ける

サービスからLINEへメッセージを送信する際に必要
- Webhook送信:LINEMessagingAPIからGoogle Apps Scriptへメッセージを送信する際の宛先
    Google Apps Script公開時に発行されるアドレスを設定する
- Botのグループトーク参加:利用する(パパ&ママのトークルームに参加させるため)
LINE@機能の利用
- 自動応答メッセージ:利用しない
- 友達追加時あいさつ:利用しない
LINEアプリへのQRコード:パパ・ママがLINEBotと会話をするために、友達登録を行っておく必要がある。
QRコードを使用して友達登録を行う。

Google Apps Script

// LINE
var pushLINE  = 'https://api.line.me/v2/bot/message/push';
var CHANNEL_ACCESS_TOKEN = '<アクセストークン>';  // LINE Messaging APIで発行したアクセストークンを貼り付け

// LINE トークルームID
    // メッセージを送信する宛先(グループIDまたはトークルームID)
    // LINEの場合グループチャット、複数人チャット、個別チャットによりチャットのtypeが変わる
    // グループチャットの場合 type:group 複数人チャット(トークルーム)の場合 type:room 個別チャットの場合type:user
    // 今回は、パパ、ママ、LINE Messaging APIのBotの三者の複数人チャットで会話を行う。
    // このトークルームでのテキスト形式の投稿のみ、GoogleHomeで読み上げを行う。
var grpidHome = '<トークルームID>';

// LINE ID 一覧
    // BotのIDは、LINE Messaging APIのチャンネル設定画面にあるYour user IDを参照。
    // LINEよりメッセージ送信時にSpreadSheetに書き込む際idUsersのリストに不在の場合userIdを書き出す。
var idUsers = {
  '<パパのユーザID>' : "パパ",
  '<ママのユーザID>' : "ママ"
};

// LINE -> GoogleHome メッセージ記録用のスプレッドシートID
    // Googleドライブで新規のスプレッドシートを作成し
    // https://docs.google.com/spreadsheets/d/<スプレッドシートのID>/edit#gid=0
    // スプレッドシートのIDを取得する。
var ssId = '<スプレッドシートID>';

// mgrokのアドレス
var url = "https://<発行されたngrokのID>.ngrok.io/google-home-notifier";

// テスト用発話関数
function msgtest(){
  sendHttpPost("テストです。")
}

// GoogleHomeを喋らせる関数
function sendHttpPost(message){
   var payload =
   {
     "text" : message
   };

   var options =
   {
     "method" : "post",
     "payload" : payload
   };

   UrlFetchApp.fetch(url, options);
}

//SPREAD_SHEETはスプレッドシートのURL
function writeSpread(who, messageText, id){
  // 内容をスプレッドシートに保存 (直近メッセージ読み上げ用データとして)
  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に追加する)
  }
}

function doPost(e) {
  // POST内容によって対応するアクションを仕分ける
  var actType = JSON.parse(e.postData.contents).events[0].type;

  /*
   * LINE側からPOSTされた場合
   * <アクション1> LINEのメッセージをGoogleHomeで発話
   */
  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;
    } else if(gid == 'room'){
      gid = JSON.parse(e.postData.contents).events[0].source.roomId;
    }

    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 = '不明';
        }
        // スプレッドシートにメッセージを書き込む
        writeSpread(who, messageText, id)

        // google-home-notifierへ送信しGoogleHomeで発話する
        var message_ = 'ラインが来ました。' + who + 'から、' + messageText;
        sendHttpPost(message_);

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

  }

  /*
   * GoogleHome側(IFTTT)からPOSTされた場合
   */
  else {

    // メッセージ本文を取得
    var messageText = JSON.parse(e.postData.contents).events[0].message;

    /*
     * <アクション2> LINEにメッセージを送信 (GoogleHomeで話したメッセージをLINEのトークに投稿する)
     */
    if(actType == 'googlehome_to_line') {

      // Homeグループへ送信
      sendLinePush(grpidHome, messageText);

    }

    /*
     * <アクション3> 再生済みメッセージを読み上げる (聞き逃したメッセージをGoogleHomeで発話する)
     */
    else if(actType == 'googlehome_request') {

      var ss = SpreadsheetApp.openById(ssId).getSheets()[0];  // スプレッドシートの選択
      var ssRows = ss.getLastRow();                           // 最終行番号の取得

      if(ssRows < 1) {                      // メッセージがない(スプレッドシートの記録が空)の場合
        sendHttpPost('ラインはまだありません。');

      } 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時間以内のメッセージがない場合
          sendHttpPost('24時間以内のラインはありません。');
        } else {            // 24時間以内のメッセージがある場合
          // 対象となったメッセージをつなげる
          var recentMessage = ''
          for(var j = i, ct = 1; j < recRows; j ++, ct ++) {
            recentMessage += ct + '件目。' + getRequestMessage(ssData[j]) + '。'
          }

          // 件数などをまとめてGoogleHomeへ一括送信
              // 1件ずつループで連続送信すると、最後の1件しか再生されなかったりするため。
          ct --;
          var message_= '24時間以内に' + ct + '件のラインがありました。' + recentMessage + '以上です。'
          sendHttpPost(message_);
        }

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

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

// 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
      }]
    })
  });
}

IFTTT

・ GoogleHomeに話しかけてLINEメッセージを送信



THENの設定
- Google Assistant のSay a phrase with a text ingredient を選択
- 「OK Google」に続けてサービスを呼び出す言葉を登録
  (話しかけた内容のうち"$" に該当する部分が送信されるメッセージとなる)
  今回の設定では「OK Google ライン ただいま」 と話しかけると 「ただいま」がトークルームに送信されます。
THATの設定
- Google Apps Scriptのサービス公開時に発行されたURLを設定
- POSTのMethodを用いて、サービスへJSON形式でBody(optional)に設定した情報(メッセージ)を送信する。
- これによりサービス側のdoPost関数が実行されます。
- typeに指定した文言を判定しサービスの処理を決定する(googlehome_to_lineではLINEのトークルームにメッセージを送信する)
- TextField部は{{TextField}}と入力することでTHENの部分で"$"に該当していた言葉が連携される(例の「ただいま」の部分)

・ GoogleHomeに直近メッセージの再読み上げさせる



THENの設定
- Google Assistant のSay a simple phrase を選択
- 「OK Google」に続けてサービスを呼び出す言葉を登録
THATの設定
- Google Apps Scriptのサービス公開時に発行されたURLを設定
- POSTのMethodを用いて、サービスへJSON形式でBody(optional)に設定した情報(メッセージ)を送信する。
- これによりサービス側のdoPost関数が実行されます。
- typeに指定した文言を判定しサービスの処理を決定する(googlehome_requestでは24時間以内の最新の5件のメッセージを読み上げる)

使用してみた感想

子供は順応するのが早いこともあり、抵抗なくGoogleHomeに話しかけておりました。
ただ、「最近ってナニ?」といった質問が飛び出すなど違った意味で発見することもあり、今後の改善課題が出てきました。
また、妻については自宅にいる時にLINEに気づかないということがあるため、メッセージを読み上げてもらえれば少しは楽になるのでは?
といった観点で使用感を聞いてみました。
その結果、「読み上げてほしいものと、そうでないものはあるので全部読み上げるのは使い勝手としては良くない!」と言った意見をもらい
これもまた、改善課題として今後対応が必要となりました。