キャンセル待ち戦争から抜きんでよう!Google Apps Scriptを利用してキャンプ場予約サイトからLINEに転送する仕組みを作ってみた

2022.03.22

データアナリティクス事業本部@札幌の佐藤です。

季節も3月となり、北海道は少しずつ春がやってきています。
春が近づき冬季閉鎖中のキャンプ場の予約もちらほらと始まってきており、早速ゴールデンウィークの予約を入れようとしましたが、 さすが他のキャンパーは行動が速く、直近予約できるキャンプ場は予約できず、キャンセル待ちになっていました。

キャンセル待ちをするのはいいのですが、キャンセルが発生したときにいち早く情報をゲットしたい……。
そんなわけで、キャンプ場からの通知をLINEに通知する機能を作成しました。

outputイメージ

outputイメージとしてはそんなに難しいことをやっているわけではなく、単純にメールの内容をLINE転送しているだけです。

今回の構成

今回のアーキテクチャ実装背景

そもそも必要だった背景・目的については最初に書いた通りです。

もう少し細かいことを書くと、キャンプ場サイトは携帯電話のメールアドレス宛に予約情報やキャンセルの情報が送信されるのですが、普段携帯メールをほとんど見ないため、その情報を見逃すことが多く、すでにほかの人が予約されているということが多かったです。
チャンスを逃していることになるので、改善したいと思っていました。

ここからビジネス要件として、今回は以下をスコープにすると定めました。

  • キャンセル発生を即時通知、すぐに対応できるようなシステムの構築
  • 予約情報がすぐに確認できるシステムの構築

また、非機能要件的な側面からでは、利用者は私だけであるため今後の利用者の増加もなく、たまにしか連携されてこず、連携数も数件であるため冗長性などを考える必要もありません。

アーキテクチャ構成

そもそもですが、キャンプ場からの連絡はメールアドレス限定であるので、メールアドレス以外の対応が必要です。
すぐに対応できるシステム=普段利用しているコミュニケーションツールでの通知が良いということで、LINEを使った通知にしています。
また、メールアドレスの情報は特に保持する必要がないため、単純にメールの内容を通知するという方針としています。

極力費用を抑えるということで、今回のサービスのレベルであれば無料で構築できます。

実装について

LINEのMessaging APIの準備

今回の実装は概要図にあるようにGoogle Apps Script(GAS)とLINEのMessaging APIを利用し、GASからMessaging APIにPUSH通知させています。
そのため、まずはMessaging APIの準備を行う必要があります。

Messaging API

作成方法についてはLINE側が提供しているドキュメントが非常にわかりやすいので、以下ドキュメントを見ながら作業するのが良いと思います。

Messaging APIを始めよう

ところでこれを読まれている方はキャンプをしているアニメといえば何を思い出すでしょうか。

キャンプといえばあのアニメですよね?

そう

『アイカツ!』ですね!

第71話「キラめきはアクエリアス」で霧矢あおいがクリスタルアクエリアスコーデのPRドレスの披露ライブの話をされましたが自信がなく、山籠もり(という名のキャンプ)をすることになりましたよね。
第2シーズン序盤は自信を失っている描写がちょいちょいあるので、ここは転機といえる、霧矢あおい好きにとっては素晴らしい回でしたね。

というわけで、Messaging APIを作成しました。

ここまで来たらあとは通知側を作っていくだけです。

GAS側の準備

実装としては単純で、以下3つの処理を行っているだけです。

  • Gmail上のメール上位10件を確認し、その中から該当のドメインから来ていて、お気に入りになっていないメールをチェック
  • メールのタイトルと、本文をMessaging APIへPUSH
  • 処理済みのメールをお気に入り登録する

メールのドメインにしているのは、単純に件名が動的に変わるという点や、運用保守的にもドメインにしたほうがいいからです。
Messaging APIへの通知についてはREPLYとPUSHの2種類ありますが、今回は単純に一方的な通知だけ行いたいのでPUSHを選択しています。

https://developers.line.biz/ja/reference/messaging-api/#messages

Messaging APIへのPUSHを行うためには、Messaging API上の「Channel access token (long-lived)」とBasic setting上の「Your user ID」がそれぞれ必要になりますので、事前にGAS側へ登録しておきPropertiesService.getScriptProperties().getProperty()で取得しています。

新レイアウトでは一度setPropaty()が必要になりますので、事前に処理を実行してセットしておくか、旧レイアウトからセットしておく必要があります。(旧レイアウトが有効であれば、旧レイアウトから入れるのが楽だと思います)

GAS側実装

//固定値
// Messaging APIの「Channel access token (long-lived)」を設定
const channel_token = PropertiesService.getScriptProperties().getProperty("CHANNEL_TOKEN");
// Messaging APIの「Your user ID」を設定
const user_id = PropertiesService.getScriptProperties().getProperty("USER_ID");
const url = "https://api.line.me/v2/bot/message/push";

function myFunction() {
  var threads = GmailApp.search("from:'@nap-camp.com'' ", 0, 10);

  // 取得したメール分処理を実行する
  threads.forEach(function(thread){
    var messages = thread.getMessages();

    if(!messages[0].isStarred()){

      Logger.log(messages[0].getSubject());
      Logger.log(messages[0].getPlainBody());

      // メール本文の加工
      if (messages[0].getSubject().match("キャンセル発生")){
        // キャンセル待ちメールはレイアウトが崩れるので、必要な箇所のみ通知
        // getPlainBodyだとURLが消えるため、getBodyで取得し加工
        var array = messages[0].getBody().split("<br />")
        // substrは非推奨なので、substringを利用
        var msg_head = array[0].substring(array[0].indexOf("<p>")+3, 1000).trim();
        var msg_url = array[5].substring(array[5].indexOf("https"), array[5].indexOf("予約")-2).trim();
        var msg = msg_head + "\r " + msg_url
      }else{
        // レイアウトが崩れないので通常通り通知
        var msg = messages[0].getPlainBody()
      }

      var res = send_linemessages(messages[0].getSubject(), msg)
      Logger.log(res.getResponseCode());

      //処理が終わったらお気に入りにする
      messages[0].star()
    }
  });
}

function send_linemessages(mail_Subject, mail_body) {

    var postData = {
      "to": user_id,
      "messages": [{
        "type": "text",
        "text": mail_Subject,
      },
      {
        "type": "text",
        "text": mail_body,
      }]
    };

    var headers = {
      "Content-Type": "application/json",
      'Authorization': 'Bearer ' + channel_token,
    };

    var options = {
      "method": "post",
      "headers": headers,
      "payload": JSON.stringify(postData)
    };

    // PUSHして結果を返却
    return response = UrlFetchApp.fetch(url, options);
}

実装時に発生した課題

実装時には以下の課題が発生し、方針を検討する必要がありました。

  • 処理が失敗した場合の通知先
  • キャンセル通知をLINE通知した場合にレイアウトが崩れる

処理が失敗した場合の通知先

処理が失敗するというのは大きく分けでこの2パターンです。

  • この処理(仕組み)自体がハングった場合の通知
  • Messaging APIでのPUSH通知結果が正常(200)以外だった場合の通知

今回の要件は全然確認しないメールアドレスから、すぐに確認できるLINEに変更することです。
この2つの処理が失敗した場合についてもLINEで通知するのか?というのがひとつのポイントかなと思います。

非機能要件の可用性という観点で考えると、失敗してしまう場合の影響はあります。
ですが、アーキテクチャ構成にも記載の通り、処理は1分単位で実行しています。
失敗しても次の便で取り込まれるため、充分可能性を担保できているとみなして、システム的に対応しないという方針にしました。

ただし、何らかの障害によって長期間通知されない可能性もあったため、バックアップ扱いとして携帯メールにもメールを送信する方針としました。

その場合の課題として、Gmailと携帯メール両方メールを送信するのか?という問題が発生します。
今回は通信事業者側のサービスとして、携帯メールを別のメールアドレスに転送することが可能であるため、そのシステムを利用することして解決しています。

キャンセル通知をLINE通知した場合にレイアウトが崩れる

テスト実施中に気付いたこととして、実はキャンセル通知とそれ以外の通知でキャンプ場側メール送信システムが違うらしく、キャンセル通知だけレイアウトが崩れることが分かりました。

多少のレイアウトが崩れるのであれば許容できますが、URL情報が消えてしまう問題もあったため、システム的に解決する方針としました。

具体的には、キャンセル通知メールのみメールの文章をparseして、必要な要素だけ送信というものです。
必要な要素だけに絞ったのは、parseするのであれば、ビジネス要件的にもすぐにアクセスできるのがよいだろうという判断からです。

コメントにも記載していますが、getPlainBody()だとURL情報が消えてしまうため、getBody()でHTMLタグ情報を含めて取得し加工しています。
メール本文のレイアウト変更に非常に弱い実装になっていますが、実際のお客さんではないので調整はできないため、仕方ないという感じです。

なお、これもコメントに書いていることですが、substr()は非推奨なので、substring()を利用しています。
GASでこの情報を検索をすると、明示的に書いていないサイトが多く見られました。

おそらくコンソールで表示されるのでわかるだろということなのかなと思いますが、 @deprecated — A legacy feature for browser compatibility と非推奨であり、レガシー機能であることが書いてありますので、今後のことも考えてsubstr()は利用しないのが良いと思います。

通知トリガー

アーキテクチャ構成図にも記載している1分おきの事項についてはトリガーを追加している形としています。

特に難しいことはなく実装できるのが良いところかなと思いました。

対応に当たりクォータについてもチェックをしましたが、今の構築であればリスクがないということも分かったというのも難しくなかった背景になります。

Quotas for Google Services

最後に

基本の実装自体は難しくなく、色々なサイトが解説していることであったので簡単に実装することができました。
GASは比較的とっつきやすい言語だなと思いましたので、非エンジニアの方も使っていけるなという印象でした。

早速この仕組みを使ってキャンセルされたキャンプ場を予約できたので、構築の費用対効果はありそうだなという印象です。
この程度のサービスであってもかなり楽になるので、改善活動としてはいいなと思いました。