💳

【メモ】Google Apps ScriptとIFTTTを使って、LINE Payの使いすぎ防止botを作った話

2022/09/30に公開

概要

LINE Payヘビーユーザーでお金にだらしないので毎月の利用額がヤバい。
貯金がほぼ底をつきかけている。非常にまずい。
とはいえあらゆる節約や家計簿は全く続かなかった。どうしよう…

そうだ!自動でLINE Payの決済情報を記録&計上してくれて、いくら使っているかLINEでお知らせしてくれるアプリを作ればいいじゃないか!

というわけで、自動で記録する家計簿用のデータベースを作り、そこからLINE Payの使いすぎ防止の通知をしてくれるLINE botを作成した。

参考にしたサイト・作ったコードなど、備忘録としてこちらに記録する。

仕様

  • 動作環境:Andoroid 12 / Googleスプレッドシート
  • 使用言語:Google Apps Script(以下:GAS) / JavaScript
  • 使用ツール / API
    • Google スプレッドシート
    • Gmail
    • LINE
    • IFTTT
    • Messaging API

動作イメージ

LINEIFTTTGmailGASスプレッドシートMessaging APILINE
という順で動作するイメージ。

Gmailに転送されたメールの本文に含まれる金額情報をGASで処理して抜き出し、それをスプレッドシートに記述。
そこからBotに配信をするといった単純な作りになっている。

Gmailを元に情報を取ってくる仕様なので、メール、もしくはスマホへの通知に対応してない決済では使えない方法なのでご注意を。(もちろんGASを使うのでGmail限定である)

当初はLINEウォレットのトークから決済情報を取得しようと思ったが、公式アカウントのトークの取得は出来ず、カード会社側でもメールなどのお知らせ機能に対応していないため送られてくるのはLINEの通知とメッセージだけという半分詰みのような状態に陥った。

そこでIFTTTというツールを使うと、LINEの通知をGmailに転送するといったアプレットというタスクを作ることが出来るため、それを介してGmailへの転送を実現した。
バンサイIFTTT。大好きIFTTT。

実装

LINEの通知をIFTTTを使ってGmailに転送する

Createページから以下の設定する。

If this:トリガー

  • Android DeviceNotification received from a specific appを選択
    • App name → LINE
    • Filter by notification keyword → LINEウォレット

Then That:アクション

  • GmailSend yourself an emailを選択
    • Gmail account → GASを使うGoogleアカウントのアドレスで設定
    • タイトルやメッセージはいじらずにそのまま


https://www.takosuke.net/lineにメッセージが届いたら指定のメアドに通知す/

GASで金額情報を抽出したりスプレッドシードのDBに書き込む

DBに書き出すまでの処理の内容は以下の通り。

  1. シート・行数に関連する情報を取得
  2. メールを指定のクエリから検索
  3. 既存のメールIDを取得して配列化する
  4. 受信日・本文・メールidを取得
  5. DB上に記録がないメールの場合は、本文から金額が書いてある部分を抽出して[金額,発生日,id]で配列化
  6. DBシートと新着シートに書き出し

基本の作りは以下の連載を参考にさせていただいた。
https://tonari-it.com/gas-gmail-search-thread/

シート・行数に関連する情報を取得

まず定数に必要な情報を格納する。
以下のものはグローバルにした。
DBとついているものはDBシートへ、
Newとついているものは新着シートに影響が出る。

database.gs
// スクリプトに紐づくスプレッドシートの読み込み
  const ss = SpreadsheetApp.getActiveSpreadsheet();
//今回使用するシートと最終行たち
  const sheetDB = ss.getSheetByName('DB');
  const sheetNew = ss.getSheetByName('新着');
  const lastRowDB = sheetDB.getLastRow();
  const lastRowNew = sheetNew.getLastRow();
  const lastColDB = sheetDB.getLastColumn();
  const lastColNew = sheetNew.getLastColumn();

メールを指定のクエリから検索

ここからは関数「searchByMail()」内に記述を追加していく。

GmailApp.search()クラスを使用して、メールを検索。
引数に検索条件の(query)を設定する。
https://developers.google.com/apps-script/reference/gmail/gmail-app#search(String,Integer,Integer)

database.gs
function searchByMail() {
  //Gmailの検索と取得
  const query = 'LINE Pay お支払い -件の新規通知';
  const threads = GmailApp.search(query);
  const getMessages = GmailApp.getMessagesForThreads(threads);
  }

定型文で「LINE Pay お支払い」という文章が入ってくるのでそちらで検索をかけるが、たまに同じ決済で「〇〇件の新規通知」と入ったメールが届くことで、同じ決済情報がダブることがある。
そのため、頭にマイナスをつけて「-件の新規通知」と除外するようにしている。

既存のメールIDを取得して配列化する

この後ループ処理でメールの内容を取得してシートに書き出しをするが、
その前にすでに記録されているidをDBシートから取得して配列を作成する。
これを使うことで、過去にすでに計上した決済がダブって記録されることを防ぐことが出来る。

未読メールからも検索できるが、うっかり開けてしまうことも考えて、id単位でダブりを確認するやり方を採用した。

このあたりの処理は以下のページを参考にさせていただいた。
https://tonari-it.com/gas-add-contact-mail/

database.gs
function searchByMail() {
  //Gmailの検索と取得
  ~   中略  ~
  
  //書き出し用の配列
  const values = [];
  //過去に計上したものかどうかを判定するためのidリスト(配列)の作成
  const arryId = sheetDB.getRange(2, 3, lastRowDB).getValues().flat();
  }

本文・受信日・メールidを取得

Messageオブジェクト.getXXX()でメールに関する情報が取得できるため、そちらを利用して「メール本文」「受信日」「メールid」を取得する。

database.gs
function searchByMail() {
  //Gmailの検索と取得
  ~   中略  ~
  
  //書き出し用の配列
  ~   中略  ~
  //過去に計上したものかどうかを判定するためのidリスト(配列)の作成
  ~   中略  ~

  //メッセージがヒットした場合に繰り返し処理する
  for (const messages of getMessages) {
    //メッセージ情報の取得
    const body = messages[0].getPlainBody();
    const date = messages[0].getDate();
    const id = messages[0].getId();
    }
  }

取得した本文は以下のようになる。この後これを加工して金額部分だけ抜き出す。

メール本文
LINEウォレット
LINE Pay お支払い 1,000 円 お支払いが完了しました。 カード番号: ****** ***

LINE notification
Received September 01, 2022 at 00:00AM 

DB上に記録がないメールの場合は、本文から金額が書いてある部分を抽出して[金額,発生日,id]で配列化する

既存のメールIDを取得して配列化するで作成したarryId(idの配列)を使用して、取得したメールidがarryIdに含まれているかを判定。
含まれていない新たなメールのみ、[金額,発生日,id]という配列を作成してvaluesに格納する。

この際本文の中から金額を抜き出して変換する処理も行っている。

database.gs
function searchByMail() {
    //Gmailの検索と取得
    ~ 中略  ~

    //書き出し用の配列
    const values = [];
    //過去に計上したものかどうかを判定するためのidリスト(配列)の作成
    ~ 中略  ~

    //メッセージがヒットした場合に繰り返し処理する
    for (const messages of getMessages) {
        //メッセージ情報の取得
        ~ 中略  ~
      // IDがDB上に無い新しいメッセージの場合
      if (!arryId.includes(id)) {
            const formatDate = Utilities.formatDate(date, 'JST', 'yyyy/MM/dd HH:mm:ss').toString().split(/\s/);
            //メッセージから金額情報を抽出して3桁区切りを消して数値に変換
            let bodyFilter = body.match(/LINE Pay お支払い(.*)/);
            let splitBody = bodyFilter['1'].split(/\s/);
            const paydPriceStr = splitBody[1].replace(/,/g, "");
            const paydPrice = parseInt(paydPriceStr, 10);
            //[日付,金額,id]の配列を作る
            values.push([paydPrice, formatDate, id]);
        }
    }
}

テキストの加工は以下のように変換される。

  1. 本文から「LINE Pay お支払い」が入っている行を抜き出す
console.log(bodyFilter);
//let bodyFilter = body.match(/LINE Pay お支払い(.*)/);
[ 'LINE Pay お支払い 1,000 円 お支払いが完了しました。 カード番号: ****** *',
  ' 1,000 円 お支払いが完了しました。 カード番号: ****** *',
  index: 10,
  input: 'LINEウォレット\nLINE Pay お支払い 1,000 円 お支払いが完了しました。 カード番号: ****** *\n\nLINE notification\nReceived September 01, 2022 at 00:00AM\n',
  groups: undefined ]
  1. 抜き出したテキストを半角スペースごとに区切って配列化する
console.log(splitBody);
//let splitBody = bodyFilter['1'].split(/\s/);
  [ '', '1,000', '円', 'お支払いが完了しました。', 'カード番号:', '******', '*' ]
  1. 金額に該当する部分を抜き出して3桁区切りを消す
  2. 数値に変換する
console.log(paydPrice);
//const paydPriceStr = splitBody[1].replace(/,/g, "");
//const paydPrice = parseInt(paydPriceStr, 10);
  1000

「受信日」「メールid」と合わせて、最終的に以下のような配列が生成される

console.log(values);
   [ 1000, [ '2022/09/01', '00:00:00' ], '123456789ABCDEFG' ]

DBシートと新着シートに書き出し

ここまでで作成した配列をスプレッドシートに書き出す。
DBシートはリセットすることがないのでrastRowDBの次の行(最終行)に、新着シートはリセットをするため、タイトル下の行に追加するよう指定する。

database.gs
function searchByMail() {
    //Gmail関連の変数(条件で検索)
    ~ 中略  ~

  //書き出し用の配列
  const values = [];
    //過去に計上したものかどうかを判定するためのidリスト(配列)の作成
    ~ 中略  ~

  //メッセージがヒットした場合に繰り返し処理する
  for (const messages of getMessages) {
        //メッセージ情報の取得
        ~ 中略  ~
    // IDがDB上に無い新しいメッセージの場合
    if (!arryId.includes(id)) {
            ~ 中略  ~
    }
    }
    //新しい配列がある場合に新着シートとDBシートにそれぞれ書き出し
    if (values.length > 0) {
        sheetDB.getRange(lastRowDB + 1, 1, values.length, values[0].length).setValues(values);
        sheetNew.getRange(2, 1, values.length, values[0].length).setValues(values);
    }
}

その他の細かな動作設定

DBに書き込む処理が出来上がったので、Newシートをリセットする関数「clearSheet()」と一定時間で動作する「doTimerfunc()」を追加する。

「clearSheet()」は見出し以降の内容を.clearContent()で内容を削除する。また、Newシートが空の場合に.clearContent()でエラーが出てしまうため、セル「A2:C2」以降に要素が入っているかを判定した上で処理をする。

「doTimerfunc()」は「searchByMail()」と「clearSheet()」をまとめ、一定時間に一回トリガーを使って起動するように設定している。

database.gs
function searchByMail() {
  ~   中略  ~
}

function clearSheet() {
  sheetNew.getRange(2, 1, lastRowNew - 1, lastColNew).clearContent();
}

//タイマーで実行する関数のまとまり
function doTimerfunc(){
  const range = sheetNew.getRange('A2:C2');
  if(!range.isBlank()){
    clearSheet();
  }
  searchByMail();
}

ここまでがGASを使ったスプレッドシート上にGmailで受信した決済情報を記録する方法になる。
最終的にはこのような表として書き出される

全体のコードは以下の通り。

database.gs
// 参考:https://blog.synnex.co.jp/google/gmail-gas/
// 参考:https://tonari-it.com/gas-gmail-search-thread/

// スクリプトに紐づくスプレッドシートの読み込み
const ss = SpreadsheetApp.getActiveSpreadsheet();
//今回使用するシートたち
const sheetDB = ss.getSheetByName('DB');
const sheetNew = ss.getSheetByName('新着');
const lastRowDB = sheetDB.getLastRow();
const lastRowNew = sheetNew.getLastRow();
const lastColDB = sheetDB.getLastColumn();
const lastColNew = sheetNew.getLastColumn();

// メールから金額情報を抽出してスプレッドシートに書き出し
function searchByMail() {
    //Gmailの検索と取得
  const query = 'LINE Pay お支払い -件の新規通知';
  const threads = GmailApp.search(query);
  const getMessages = GmailApp.getMessagesForThreads(threads);

  //書き出し用の配列
  const values = [];
  //過去に計上したものかどうかを判定するためのidリスト(配列)の作成
  const arryId = sheetDB.getRange(2, 3, lastRowDB).getValues().flat();

  //メッセージがヒットした場合に繰り返し処理する
  for (const messages of getMessages) {
    //メッセージ情報の取得
    const body = messages[0].getPlainBody();
    const date = messages[0].getDate();
    const id = messages[0].getId();
    // IDがDB上に無い新しいメッセージの場合
    if (!arryId.includes(id)) {
     // 日付の形式を変換
      const formatDate = Utilities.formatDate(date, 'JST', 'yyyy/MM/dd HH:mm:ss').toString().split(/\s/);
      //メッセージから金額情報を抽出して3桁区切りを消して数値に変換
      let bodyFilter = body.match(/LINE Pay お支払い(.*)/);
      let splitBody = bodyFilter['1'].split(/\s/);
      const paydPriceStr = splitBody[1].replace(/,/g, "");
      const paydPrice = parseInt(paydPriceStr, 10);
      //[日付,金額,id]の配列を作る
      values.push([paydPrice, formatDate, id]);
    }
  }
  //新着シートとDBシートにそれぞれ書き出し
  if (values.length > 0) {
    sheetDB.getRange(lastRowDB + 1, 1, values.length, values[0].length).setValues(values);
    sheetNew.getRange(2, 1, values.length, values[0].length).setValues(values);
  }
  //動作確認と出力内容の確認
  console.log(values);
}

// セルのリセット(新着シートのみ)
function clearSheet() {
  sheetNew.getRange(2, 1, lastRowNew - 1, lastColNew).clearContent();
}

//タイマーで実行する関数のまとまり
function doTimerfunc(){
  const range = sheetNew.getRange('A2:C2');
  if(!range.isBlank()){
    clearSheet();
  }
  searchByMail();
}


LINEに「使いすぎ予報」として配信する

ほぼ以下の連載の手順で作成したため、細かな手順は省く。
https://tonari-it.com/gas-line-message-api-overview/

仕組みは、「LINE配信用情報」シートで「今月の利用額」「過去四時間の利用額」「今月の予算」「今月の残金」を記録・計算し、それを以下のコードで取得して本文のテキストを作成し、配信している。

先程までのスプレッドシートへの書き出しとは別でスクリプトファイルを作成している。
またAPIのトークンなどは、スクリプトプロパティに書き込んだものを呼び出す形で使用している。

コードはほぼ全体的にこちらの内容を参考にさせていただいた。
https://tonari-it.com/gas-line-message-script/

コードは以下の通り。

line_bot.gs
//今回使用するシート
const sheetLine = ss.getSheetByName('LINE配信用情報');

function postMessage() {
  const url = 'https://api.line.me/v2/bot/message/push';
  const scriptProp = PropertiesService.getScriptProperties();
  const lineToken = scriptProp.getProperty('XXXXX'); //トークン取得
  const lineUserId = scriptProp.getProperty('YYYYY'); //botのユーザーID

  //セル情報の取得
  const monthPerce = sheetLine.getRange(2, 6).getValue().toLocaleString() * 100;
  const monthPayd = sheetLine.getRange(2, 3).getValue().toLocaleString();
  const hourPayd = sheetLine.getRange(2, 2).getValue().toLocaleString();
  const monthBalance = sheetLine.getRange(2, 5).getValue().toLocaleString();

  console.log(monthPerce);

  const message = `【▲使いすぎ予報▲】
  
利用率:${monthPerce}%

過去4時間の利用額:${hourPayd}円
今月の利用額:${monthPayd}円
今月の残高:${monthBalance}`;

  console.log(message);

  const requestBody = {
    to: XXXXX, //ユーザーID
    messages: [
      {
        type: 'text',
        text: message
      }
    ]
  };

  const params = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      Authorization: 'Bearer ' + YYYYY
    },
    payload: JSON.stringify(requestBody)
  };

  UrlFetchApp.fetch(url, params);
}

トリガーで任意のタイミングに起動するように設定

最後に「トリガー」を使い、任意のタイミングで「doTimerfunc()」→「postMessage()」の順で起動するようにすれば完成。
今回は1時間に一回で設定してみた。

まとめ

最終的に以下のようなLINEメッセージが届く様になった。

まさかほぼJsのみでLINEのbotが作成できるとは思わなかった。
基本の作りは参考サイトのものがベースになっているが、そこから改造して自分が必要なbotを作るためにあらゆる情報を見に行ったので、いろんな知見が溜まったなぁという感想。

参照回数が多いのか、処理が重たい感じがするのでもう少し改良も加えたい。

これからもいろいろ作ってみたい。まずは節約を頑張ります。

Discussion