2018-02-24

【GAS】相席ラウンジの混雑情報を、LINE Botに聞くと教えてくれる相席コンシェルジュを作ってみた

これまでの経緯


Google Apps Script上で、相席居酒屋の在席情報を一時間おきに取得し、そのデータをスプレッドシート上に保存していました。そしてそれらの値をリアルタイムでWebサイトに更新する方法を、いままで挑戦していました。


これをLINE BOT化して、どんなユーザーでもつかいやすいようにしてみました。

創ったもの


相席コンシェルジュという、LINE Botを作成しました。


Oriental Lounge Eveという相席居酒屋の、混雑状況を分析できるLine Botを作りました。QRコードから登録できます。なう→現在の混雑状況、ぶんせき→時系列データの分析ページ、あくせす→公式ページのリンクを出します。渋谷、新宿、町田店に対応しています。

以下のQRコードから友達追加することができます。






やりたいこと


実装したい機能としてはLineから特定のメッセージを送信すると、その内容をGoogle Apps Scriptで受信、その値を読み込み特定のアクションをLine上で行うように指示。

  • 「なう」 → 現在の空席状況と、住所データ、電話番号をLINE Botが教えてくれる
  • 「ぶんせき」 → 現在・過去の空席状況と、お店の包括的な情報が載っているページのURLを出してくれる
  • 「あくせす」 → 公式ホームページのURLを送信する
みたいな感じにしたいと思います。


アーキテクチャ


前回までの記事では、Google Apps Script上でここまでやりました。

  • www.oriental-lounge.com に、各店舗の混雑状況データが存在
  • それをGoogle SpreadsheetでIMPORTXML関数を実行し、データをスクレイピング
  • Google Spreadsheet上で、それらのデータを1時間おきに取るように、Google Apps Scriptでプログラミング
  • Google Spreadsheetで、現在の混雑データと、時系列データをグラフ化し、HTMLに変換
  • 混雑データ・時系列データをWebアプリ上でディプロイ


今回はさらに


  • LINEからのメッセージを、Google Apps Scriptで受信
  • Google Apps Scriptで受信したメッセージを加工し、その内容を再度LINEに送信
という処理を行えるようにします。

このアプリケーションのソースコード


さきに全体像が見えたほうがよいと思うので、ソースコードを公開します。


// プロパティ取得
var PROPERTIES = PropertiesService.getScriptProperties();//ファイル > プロジェクトのプロパティから設定した環境変数的なもの

//スプレッドシート情報
var SP_ID = PROPERTIES.getProperty('SP_ID') //プロパティに設定した、スプレッドシートのID

// LINE情報
var LINE_ACCESS_TOKEN = PROPERTIES.getProperty('LINE_ACCESS_TOKEN');
var line_endpoint = 'https://api.line.me/v2/bot/message/reply'; 
var WEB_HOCK_URL = PROPERTIES.getProperty("WEB_HOCK_URL") //Google Apps Scriptで作成したアプリのURL

function doGet() {
  return HtmlService.createTemplateFromFile("相席アナライザー").evaluate();
}


//LINEからWebhock URLあてに、HTTP POSTリクエストが送られてきたときに実行される関数
function doPost(e) {
  var json = JSON.parse(e.postData.contents);

  //返信するためのトークン取得
  var reply_token= json.events[0].replyToken;
  if (typeof reply_token === 'undefined') {
    return;
  }

  //送られたLINEメッセージを取得
  var user_message = json.events[0].message.text;  

  //返信する内容を作成
  var reply_messages;
  if ('なう' == user_message) { //「なう」と送信されたら、現在の混雑状況の画像を返す
    reply_messages = ["https://docs.google.com/spreadsheets/d/e/2PACX-1vR5UzfrOLwJ5he4CA1r8VBaFWN2QWro01ok2GP3D2p24EsVtMj6Pm1lGQb9Yjv_BQNnzjpBiQcYXb59/pubchart?oid=442255862&format=image"];//グラフ画像を返信
  } else if ('ぶんせき' == user_message) {   //分析ページのURLを返します。
    reply_messages = ["https://script.google.com/macros/s/AKfycbx0bke55aKe1TD4UqAyR2FfdJWMJ4-R12fTYCwJiEpeChU2Kvfc/exec"];
  } else if ('あくせす' == user_message) {    //ホームページのURLを返します
    reply_messages = ["www.oriental-lounge.com"];
  }else {
    //説明を返す
    reply_messages = ['「なう」で現在の空席状況、「ぶんせき」で分析ページのURL、「あくせす」で詳細情報をお送りします。'];
  }

  // メッセージを返信
  var messages;
  if ('なう' == user_message) { //「なう」の時だけ画像を返す
    messages = reply_messages.map(function (v) {
      return {
        'type': 'image', 
        'originalContentUrl': v, 
        "previewImageUrl":v}; 
      } 
    );
  } else { 
    messages = reply_messages.map(function (v) {
    return {'type': 'text', 'text': v}; 
    });    
  }

  //HTTP POSTリクエストをLINE BOTに送信
  UrlFetchApp.fetch(line_endpoint, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': messages,
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

//スプレッドシートを取得
var File = SpreadsheetApp.openById(SP_ID);
var baseSheet = File.getSheets()[0]; 

function MainFrame() {

  //TODO: スプレッドシートの決まった箇所に決まった値を挿入
  setManFemaleNumList(1,getMenFemaleArray(17,4,1,2)); //D17 2行分取得
  setManFemaleNumList(2,getMenFemaleArray(18,4,1,2));  
  setManFemaleNumList(3,getMenFemaleArray(19,4,1,2));  
}

//TODO:スプレッドシートに、相席ラウンジ 銀座店の現在の男女の数をリスト形式で返す
function setManFemaleNumList(sheetNum, manFemaleArray){
  var Sheet = File.getSheets()[sheetNum];
  Sheet.appendRow(manFemaleArray);
}


//TODO:指定範囲のセルから、男女の数をリスト形式に取得する

function getMenFemaleArray(row,col,rowNum,colNum){
  var array = baseSheet.getRange(row,col,rowNum,colNum).getValues()[0];
  var date = new Date();
  array.unshift(date)
  return array;
}



LINE BOTのアカウントの開設

まず、BOTを作る前に、アカウント等を作成する必要があります。ほかにアカウントの作成方法について述べている記事はたくさんあるので、あえてこの記事では解説しません。
こちらに詳しいセットアップマニュアルがあるので、これを参考にアカウントを開設してください。

LINE BOTの作り方を世界一わかりやすく解説(1)【アカウント準備編】 - Qiita

はじめに Messaging API (通称 LINE BOT) は Line が提供するサービスです。 こちらの Messaging API を使用して、ユーザーと自動的にやりとりができるLINE BOTを作成することがゴールです。 PushとReplyのAPIによる連携機能が提供されており、Pushの方は有料で使用できるようになっています。 料金体系は色々あるのですが、まず Developer Trial を使用されることをお勧めします。 De...

LINE Messaging APIとGoogle Apps Scriptで何ができるの?


さて、LINEからGoogle Apps Scirptを連携させてみようと思います。
これらの仕組みを実現するためには、Line Messaging APIと、Google Apps Scriptを利用してみたいと思います。

Line Messaging APIと、Google Apps Scriptを連携させることで、以下のようなことができるようになります。

  1. LINE上でユーザーがメッセージをBotに返信
  2. それがトリガーに(Webhock)、指定されたGoogle Apps ScriptのWebアプリにPOSTリクエストを送信
  3. Google Apps ScriptでそのPOSTリクエストからメッセージを受信し、データをもとに返すメッセージ・画像などを決定
  4. LINE Botにメッセージを返す
LINE公式のAPIドキュメントでは、以下の図のように解説しております。



LINE上でBOT作成に必要な設定を行う


LineでBotを作成し、そのBotにメッセージを送ります。
まず、Botを作るためには、アカウントの登録が必要です。


アカウントの取得はLINEのビジネス向けポータルサイト「LINE BUSINESS CENTER」 から、利用登録を行います。


ビジネスアカウントの登録とMessaging APIの利用登録が終わったら「LINE BUSINESS CENTER」アカウントリストから「LINE@MANAGER」を選び、Messaging APIの設定を行います。

アカウント設定 > bot設定 でまずは「APIを利用する」を選択します。

Google Apps Scriptで作成したWebアプリのURLを、Webhook URLに登録します。
そのWebhook URLと、Channel Access Tokenをメモしておきます。


注意点としては、Webhook URLはSSLで通信できないとダメなようです。


LINEからのメッセージ送信がトリガーに(Webhock)、指定されたGoogle Apps ScriptのWebアプリにPOSTリクエストを送信


LINEからメッセージの送信が送られると、指定したWebhock URLあてにPOSTリクエストが送られます。POSTリクエストの中身は、messageオブジェクトが入っています。
LINE API公式ドキュメントによると以下のようになっています。

プロパティタイプ説明
typeStringmessage
replyTokenStringイベントへの応答に使用するトークン
messageObjectメッセージの内容を含むオブジェクト。メッセージには以下のタイプがあります。

今回作成したアプリで使うのは、テキストメッセージ・画像データのみなので、テキストメッセージの中身だけ見てみましょう。送信元から送られたテキストを含むmessageオブジェクトの中身は、以下のようになっています。

プロパティタイプ説明
idStringメッセージID
typeStringtext
textStringメッセージのテキスト


Google Apps ScriptでそのPOSTリクエストからメッセージを受信し、データをもとに返すメッセージ・画像などを決定


LINEからWebhock URLをGoogle Apps Scriptに設定し、メッセージを送信すると、Google Apps ScriptにPOSTリクエストで通信が行われます。Google Apps Script上では、そのPOSTリクエストの中から、メッセージを取り出し、処理を行います。


今回の場合だと、「なう」で画像データ、「ぶんせき」、「あくせす」でURLを返します。POSTリクエストが送られると起動する関数doPost関数以下の内容が処理されます。

function doPost(e) {
  var json = JSON.parse(e.postData.contents);

  //返信するためのトークン取得
  var reply_token= json.events[0].replyToken;
  if (typeof reply_token === 'undefined') {
    return;
  }

  //送られたLINEメッセージを取得
  var user_message = json.events[0].message.text;  

  //返信する内容を作成
  var reply_messages;
  if ('なう' == user_message) { //「なう」と送信されたら、現在の混雑状況の画像を返す
    reply_messages = ["https://docs.google.com/spreadsheets/d/e/2PACX-1vR5UzfrOLwJ5he4CA1r8VBaFWN2QWro01ok2GP3D2p24EsVtMj6Pm1lGQb9Yjv_BQNnzjpBiQcYXb59/pubchart?oid=442255862&format=image"];//グラフ画像を返信
  } else if ('ぶんせき' == user_message) {   //分析ページのURLを返します。
    reply_messages = ["https://script.google.com/macros/s/AKfycbx0bke55aKe1TD4UqAyR2FfdJWMJ4-R12fTYCwJiEpeChU2Kvfc/exec"];
  } else if ('あくせす' == user_message) {    //ホームページのURLを返します
    reply_messages = ["www.oriental-lounge.com"];
  }else {
    //説明を返す
    reply_messages = ['「なう」で現在の空席状況、「ぶんせき」で分析ページのURL、「あくせす」で詳細情報をお送りします。'];
  }


Google Apps Scriptから、LINE Messaging APIのエンドポイントにデータを送信


さて、POSTリクエストで取得した値から、返すメッセージや画像のURLを設定しました。今度は、これをBOTに返してあげる必要があります。

LINEの返信のAPIエンドポイントは

var line_endpoint = 'https://api.line.me/v2/bot/message/reply'; 

にあります。このエンドポイント向けに、POSTリクエストを送信します。
HTTPの通信のヘッダーには、Content-TypeとAuthorizationの値を、以下のように指定してあげます。channel access tokenは、さきほどメモしておいたあれです。

リクエストヘッダー説明
Content-Typeapplication/json
AuthorizationBearer {channel access token}

リクエストボディには、

function doPost(e) {
  var json = JSON.parse(e.postData.contents);

  //返信するためのトークン取得
  var reply_token= json.events[0].replyToken;

で取得したreplyTokenと、メッセージオブジェクトを返してあげます。

プロパティタイプ必須説明
replyTokenString必須Webhookで受信する応答トークン
messagesメッセージオブジェクトの配列必須送信するメッセージ
最大件数:5

messsageオブジェクトの詳細についてみていきます。
現在の空席情報が分かる画像と、過去の在席情報が分かる分析ページを返すので、textオブジェクトとimageオブジェクトを返します。

textオブジェクト

プロパティタイプ必須説明
typeString必須text
textString必須メッセージのテキスト。以下の絵文字を含めることができます。
最大文字数:2000


imageオブジェクト

プロパティタイプ必須説明
typeString必須image
originalContentUrlString必須画像のURL(最大文字数:1000)
HTTPS
JPEG
最大画像サイズ:1024×1024
最大ファイルサイズ:1MB
previewImageUrlString必須プレビュー画像のURL(最大文字数:1000)
HTTPS
JPEG
最大画像サイズ:240×240
最大ファイルサイズ:1MB

これらの処理を行っているのが、下記のコードです。

//HTTP POSTリクエストをLINE BOTに送信
  UrlFetchApp.fetch(line_endpoint, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': messages,
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);



その他参考


他人からなるべく見えないようにしたい値(Access Token)は、ファイル > プロジェクトのプロパティに設定しています。(Herokuの環境変数的なあれ)


var PROPERTIES = PropertiesService.getScriptProperties();//ファイル > プロジェクトのプロパティから設定した環境変数的なもの

 PropertiesService.getScriptProperties() でプロパティオブジェクトを呼び出し

//スプレッドシート情報
var SP_ID = PROPERTIES.getProperty('SP_ID') //プロパティに設定した、スプレッドシートのID

// LINE情報
var LINE_ACCESS_TOKEN = PROPERTIES.getProperty('LINE_ACCESS_TOKEN');

getProperty('SP_ID') のようにすることで、指定した値を呼び出すことができるようになっています。


取得しているスプレッドシートのURL
https://docs.google.com/spreadsheets/d/1zfy9Np2E7sB4tj570YaTap5o8VM_wC7IoCaRCU19CLw/edit#gid=738727068

注目の投稿

 PythonのTweepyを利用して、Twitter APIを利用している。 その中で、ハマったポイントをメモしておく。 まず、Searchに関して。 Twitter検索は、クライアントアプリ側では、全期間の検索が可能になっている。 一方で、APIを利用する際は、過去1週間しか...