作りました。
LINE Messaging APIとGoogle Apps Script で初心者でも簡単に作れました。
追記:この記事にあるコードだと遅いので早くしました。
作ったGASがとっても遅かったので早くした
追記:家計簿の集計グラフをLINEに送付する機能を追加しました。
GASとLINEで家計簿-グラフの表示
概要
LINEのBotを作ります。
そのBotでMessaging APIを利用します。
Googleスプレッドシートを開きます。
そのスプレッドシートからGASプロジェクトを作ります。
(GASはスプレッドシートから作るコンテナバインド型じゃないと面倒)
LINE DevelopersにGASのwebアプリケーションとしてのURLをwebhookをとして登録。
同じページでLINEのMessageing APIのチャネルアクセストークン(長期)を取得します。
あとはGASを書いてLINEから届いたメッセージをスプレッドシートに書き出したり取得してLINEに返してあげるだけです。
仕組みとしては、
LINEに送信されたメッセージをMessaging APIを使ってGASのwebhookに投げます。
メッセージを抽出して処理した後、メッセージの中にあるreplyToken宛にGASで作ったメッセージを投げてあげます。
最後にGASが自分に’contet’: ‘post ok’ を返してあげると動きます。
はまったところ
BOTを作るまでに色々面倒な登録作業やら設定がありましたがネットの情報を見ながら難なく作成できました。
ところが、Googleスプレッドシートと連携するためのwebhook登録(検証)がうまくいかない。
結局、何がダメなのかわからないまま時間が解決してくれました・・・
サーバー側の反映に時間がかかったのかな?
コードを書いている段階ではjavascriptが分からないのでいろいろ躓きました。メッセージをそのままスプレッドシートに吐き出すようにしてどんなデータが来てるのか見ながらやったのですが、GASっていちいちnewで「webアプリケーションとして公開」をしなければいけないのが不便でした。
REST APIを受けるとき(送るとき)のデバッグだとconsole.log();が使えなかったので・・・
もっと簡単にメッセージデバッグする方法ありますよね?
LINEメッセージにはtimestampがエポックタイムでついてくるのですが、ずっと10桁(分まで)と思って処理を書いていて、本当は13桁ということに気付かず暫く呻っている時間がありました。yyyy/mm/ddに変換すると人類滅亡後みたいな年が表示されていましたよ。
正規表現はいつも苦戦するんですが(すぐ調べればいいんですが)、月を指定するメッセージを判定するときの書き方に時間を取られました。
結局 /^(0?[1-9]|1[012])月$/ こんな正規表現になりました。
細かいところでいうと、改行の処理についてもちょっと迷いました。
REST APIは\nが改行文字なのですが、スプレッドシートは\r\nなんですよね。GASを書いているときなぜかスプレッドシートの方に引っ張られてメッセージ中の改行を\r\nで処理しようとしちゃいました。
どう使っていくか?
LINE BOTを招待したグループに特定のルールでメッセージを送信するとGoogleスプレッドシートにその内容が記録されていくという感じです。
例えば、「食費 1300円」 とメッセージを送ると対応するスプレッドシートに
日時,送信者,カテゴリ,金額
みたいに記録されていきます。
また、「いくら?」とメッセージすれば今月の今までの出費を教えてくれるようにしたので時たま振り返りながらお金と向き合うことができます。
そのグループ内の人達で何でいくら出費があったのかをLINEで簡単に登録、確認していこうという魂胆です。
GASのトリガーがとても便利で、月一で記録を別シートに移して記録シートを真っ新にする関数を設定すればあとはメンテフリーなので嬉しいですね。
コード
人様の書いた関数をそのままコピペした箇所もあったりするので、まずいことあったらすぐに消しますのでお知らせください。
勢いのまま書いていたので繰り返しや冗長な部分がかなりありますが、勢いがないと書けない&動いたら勢い消える趣味コードなのでご容赦を・・・
将来見直して「自分成長したなぁ・・・」と思いたいのでこのまま載せます。
var ACCESf_TOKEN = '<LINE Developersから取得するチャネルアクセストークン(長期)を入れる>';
var SHEET_KEY = '<コンテナバインドのスプレッドシートキー>';
function doPost(e) {
var event = JSON.parse(e.postData.contents).events[0];
var userMessage = event.message.text;
var message = "";
if ( userMessage === "ID" ) {
message = tellID(event);
}
else if (userMessage.match(/^食費\n*/)){
message = food(event);
}
else if (userMessage.match(/^日用品\n*/)){
message = daily_necessities(event);
}
else if (userMessage.match(/^いくら?$|今いくら?$/)){
message = ikura();
}
else if (userMessage.match(/^(0?[1-9]|1[012])月いくら?$/)){
message = month_ikura(event.message.text);
}
else if (userMessage.match(/^おしえて|^教えて|^どうやるの*|help|usage/)){
message = '次のようにメッセージを送ってください。😁\n';
message += '\n①食費の登録\n'
message += '◇食費<改行>金額◇\n(,や円はつけない数字だけ)\n';
message += '\n②日用品の登録\n'
message += '◇日用品<改行>金額◇\n(,や円はつけない数字だけ)\n';
message += '\n③今月の現在までの出費確認(食費と日用品)\n';
message += '◇いくら?◇\nまたは\n◇今いくら?◇\n'
message += '\n④指定した月の出費確認(食費と日用品)。1カ月分ずつ聞いてください。\n';
message += '◇<1-12>月いくら?◇'
}
}
replyMessage(event.replyToken, message);
return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
function costFood(event){
// 食費シートにメッセージを書き込む
var food_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('食費');
var last_row = food_sheet.getLastRow() + 1;
var user = event.source.userId;
var split_txt = event.message.text.split('\n');
var value = split_txt[1];
var timestamp = timestampToTime(event.timestamp);
food_sheet.getRange(last_row,1).setValue(timestamp);
food_sheet.getRange(last_row,2).setValue(user);
food_sheet.getRange(last_row,3).setFormula('=vlookup(B' + last_row + ',userId!A1:B3,2,FALSE)');
food_sheet.getRange(last_row,4).setValue(value);
var message = "食費を記録しました";
return message;
}
function costDailyNecessities(event){
// 日用品シートにメッセージを書き込む
var daily_necessities_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('日用品');
var last_row = nitiyouhin_sheet.getLastRow() + 1;
var user = event.source.userId;
var split_txt = event.message.text.split('\n');
var value = split_txt[1];
var timestamp = timestampToTime(event.timestamp);
daily_necessities_sheet.getRange(last_row,1).setValue(timestamp);
daily_necessities_sheet.getRange(last_row,2).setValue(user);
daily_necessities_sheet.getRange(last_row,3).setFormula('=vlookup(B' + last_row + ',userId!A1:B3,2,FALSE)'); //userIdシートにuserIdと名前のテーブルを作成している
daily_necessities_sheet.getRange(last_row,4).setValue(value);
var message = "日用品の出費を記録しました";
return message;
}
}
function confirmRecord(){
// 各自いくら使ってるのかだす
var food_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('食費');
var daily_necessities_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('日用品');
var f_a_sum = food_sheet.getRange("G1").getValue();
var f_b_sum = food_sheet.getRange("I1").getValue();
var f_sum = food_sheet.getRange("K1").getValue();
var d_a_sum = daily_necessities_sheet.getRange("G1").getValue();
var d_a_sum = daily_necessities_sheet.getRange("I1").getValue();
var d_sum = daily_necessities_sheet.getRange("K1").getValue();
var message = '今月の現在までの記録\n'
message += '\n';
message += '食費は...'+ f_sum + '円!\n';
message += '(内訳...)\n'
message += 'Aさん:' + f_a_sum + '円\n';
message += 'Bさん:' + f_b_sum + '円\n';
message += '\n';
message += '日用品は...' + d_sum + '円!\n'
message += '(内訳...)\n'
message += 'Aさん:' + d_a_sum + '円\n';
message += 'Bさん:' + d_b_sum + '円\n';
message += 'でした。';
return message;
}
function month_confirmRecord(text){
// 月指定で各自いくら使ってるのかだす
var food_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('食費');
var daily_necessities_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('日用品');
var aggregatiod_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('月集計');
var split_txt = text.split('いくら?');
var month = split_txt[0];
var target_row = findRow(aggregatiod_sheet, month, 1);
var f_a_sum = aggregatiod_sheet.getRange("C"+ target_row).getValue();
var f_b_sum = aggregatiod_sheet.getRange("C" + (target_row + 1)).getValue();
var f_sum = aggregatiod_sheet.getRange("E" + target_row).getValue();
var d_a_sum = aggregatiod_sheet.getRange("F"+ target_row).getValue();
var d_b_sum = aggregatiod_sheet.getRange("F" + (target_row + 1)).getValue();
var d_sum = aggregatiod_sheet.getRange("H" + target_row).getValue();
var message = month + 'の記録\n';
message += '\n';
message += '食費は...'+ f_sum + '円!\n';
message += '(内訳...)\n'
message += 'Aさん:' + f_a_sum + '円\n';
message += 'Bさん:' + f_b_sum + '円\n';
message += '\n';
message += '日用品は...' + d_sum + '円!\n'
message += '(内訳...)\n'
message += 'Aさん:' + d_a_sum + '円\n';
message += 'Bさん:' + d_b_sum + '円\n';
message += 'でした。';
return message;
}
function delete_contents(){
//毎月1日0時にトリガーを設定。記録を月集計シートにコピーし、内容をクリアする
var food_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('食費');
var daily_necessities_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('日用品');
var aggregatiod_sheet = SpreadsheetApp.openById(SHEET_KEY).getSheetByName('月集計');
var f_a_sum = food_sheet.getRange("G1").getValue();
var f_b_sum = food_sheet.getRange("I1").getValue();
var d_a_sum = daily_necessities_sheet.getRange("G1").getValue();
var d_b_sum = daily_necessities_sheet.getRange("I1").getValue();
var now = new Date();
var month = now.getMonth() + 1 + '月';
var to_copy_row = findRow(aggregatiod_sheet, month, 1);
aggregatiod_sheet.getRange(to_copy_row,3).setValue(f_a_sum);
aggregatiod_sheet.getRange((to_copy_row + 1),3).setValue(f_b_sum);
aggregatiod_sheet.getRange(to_copy_row,6).setValue(d_a_sum);
aggregatiod_sheet.getRange((to_copy_row + 1),6).setValue(d_b_sum);
var f_last_row = food_sheet.getLastRow() + 1;
var d_last_row = daily_necessities_sheet.getLastRow() + 1;
food_sheet.getRange("A2:D" + f_last_row).clear();
daily_necessities_sheet.getRange("A2:D" + d_last_row).clear();
}
function findRow(sheet,val,col){
var dat = sheet.getDataRange().getValues(); //受け取ったシートのデータを二次元配列に取得
for(var i=0;i<dat.length;i++){ // iは0からじゃないとダメ。スプレッドシートは列を0から、行は1から数える。
if(dat[i][col-1] === val){
return i+1;
}
}
return 0;
}
// 引数 timestamp は13桁
const timestampToTime = (timestamp) => {
const date = new Date(timestamp);
const yyyy = `${date.getFullYear()}`;
// .slice(-2)で文字列中の末尾の2文字を取得する
// `0${date.getHoge()}`.slice(-2) と書くことで0埋めをする
const MM = `0${date.getMonth() + 1}`.slice(-2); // getMonth()の返り値は0が基点
const dd = `0${date.getDate()}`.slice(-2);
const HH = `0${date.getHours()}`.slice(-2);
const mm = `0${date.getMinutes()}`.slice(-2);
const ss = `0${date.getSeconds()}`.slice(-2);
return `${yyyy}/${MM}/${dd} ${HH}:${mm}:${ss}`;
}
function replyMessage(token, message) {
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + ACCESf_TOKEN,
},
'method': 'post',
'muteHttpExceptions': true,
'payload': JSON.stringify({
'replyToken': token,
'messages': [{
'type': 'text',
'text': message,
}],
}),
});
return response.getResponseCode();
}
function pushMessage(to, message) {
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/push', {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + ACCESf_TOKEN,
},
'method': 'post',
'muteHttpExceptions': true,
'payload': JSON.stringify({
'to': to,
'messages': [{
'type': 'text',
'text': message,
}],
}),
});
}
function tellID(event) {
// ID
var userID = event.source.userId;
var talkID = "";
if (event.source.type === "group") {
talkID = event.source.groupId;
} else if (event.source.type === "room") {
talkID = event.source.roomId;
}
var message = "あなたのID: " + userID;
if (talkID != "") {
message += "\nこのチャットのID: " + talkID;
}
return message;
}
まとめ
実はこのコード、ゴミ出しの曜日をLINEで通知しようという記事をもとに書いています。
その記事がなければこれも書けなかったと思います。
無料で有用な記事、コードを出してくださる方に感謝。
ところで元ネタの方はきれいなコードだったのですが、、、
私のコードは汚いのでだいぶ長いですが、きっと半分くらいまで短くできるコードなのでしょう。
あとはサボらず家計簿機能使っていけばいつか整理したい欲も出てくると思うので機会があればコードの整理します。では。
コメント
[…] LINEから出費を記録できる仕組みを作ったので、そこにスプレッドシートで作成したグラフをLINEに送る機能を追加しようとしたのです。結論から言うと、GAS側のバグによって実現できま […]
[…] この記事でGASとLINEで作る家計簿をご紹介していますが、そこにグラフが送信できたらかっこいいと思って実装しました。さっきまでグラフを画像にするところではまっていましたが、解 […]