はじめに
本記事はNTTテクノクロス Advent Calendar 2024のシリーズ1 10日目の記事です。
こんにちは。NTTテクノクロスの関口です。
今回の記事では、業務外での活動(= 遊んでみた 作ってみた)ことについて記事にしていこうと思います。
この記事には試行錯誤の過程が含まれています。
こうするといいよ、などあたたかいコメントをいただけると幸いです。
やってみたこと
我が家では建て替たお金を1円単位できっちりと精算する文化なのですが、件数が多いとなかなか面倒....
チャットもところどころに精算用のメッセージが挟まり、追うのも大変だったので、使って精算用のBotを作ってみました。
(駆け出しエンジニアに向けて)試行錯誤する過程も含めて紹介できればと思います。
動作環境
Google Apps Script (GAS)
サーバレスで動かしたかったのでGoogle Apps Script (以降GAS)を使用しています。ほぼJavaScriptの感覚で書けます。
GASは無料で使用することができますが、一部機能の回数に制限があります。
制限を超えた後は、その機能が使用できなくなるらしい。
Fetch関数は20,000件/日使用可能 ≒ 20,000通/日メッセージを送れるらしい。
個人で使ってたらそんなに送らないよ ※他のGASプロジェクトとの合計
その他の制約はこちら
LINE Messaging API
また、チャットアプリのbotに、LINE Messaging APIを使用しています。
いわゆるLINE公式アカウント(LINEのBot)としてメッセージのやり取りができるサービスです。
LINE Messaging APIには無料枠があります。
Botから無料でメッセージを送ることができるメッセージ数は200通/月ですが、
返信は何度でも無料です。
今回作るBotは返信を使用しているので、実質無限です。
有料メッセージの料金はこちら
機材や環境などを自分で準備しないでいいので、すごく楽です。
便利なものはありがたく使わせていただきます。
まずはオウム返しできるBotを作る
まずは動いているところが見えるとワクワクしますよね。
と言うことで、LINEとつながる部分を作ってみましょう。
全部作って動かしてみて動かなかったら悲しいので、 まずはオウム返しをするようBotのプログラムを作成します。
プロジェクトの準備
プロジェクトの作成
Googleにログインした状態で、GASを開くと、自分のプロジェクト一覧が出てきます。
「新しいプロジェクト」から新しいプロジェクトを作成します。
スクリプトプロパティの設定と呼び出し
プロジェクトができたら、チャネルトークンを安全な場所に保管しておきましょう。
左下の歯車アイコン、プロジェクト設定をクリックし、スクリプトプロパティの項目を探します。
スクリプトプロパティに下記のように値を入れ、スクリプトプロパティを保存を押下します。
プロパティ | 値 |
---|---|
token | 【準備の際に発行済みの長期チャネルアクセストークン】 |
保存できたら、左側<>アイコンのエディタをクリックし、コードを書く画面に戻ります。
保存できているかを確認するために、一度コンソール上に設定した値を出してみましょう。
新規作成した後のプロジェクトには空の関数、myFunction()があるので、そこにtokenを出すコードを追記してみます。
function myFunction() {
let token = PropertiesService.getScriptProperties().getProperty("token");
console.log(token);
}
コードが書けたら、実行してみましょう。
一度Ctrl+S(Macの場合はcommand+S)でプロジェクトを保存し、実行ボタンを押します。
実行ログの中にtokenとして設定した値が出てきました。
オウム返しをする機構を作ってみる
メッセージが来ると、Webhookを通じて、メッセージ内容を含むJSONが送られてきます。
送られてくるJSONに入っている情報はMessaging APIリファレンスにわかりやすく書かれています。また、Webhookの設定は後ほど解説をします。
今回取り出したいデータは、「誰が」「どんな内容の」メッセージを送ってきたか?なので、それらを取り出す準備をしていきます。(あとは返信をするための応答トークンも。)
※今回はテキストメッセージにのみ反応させます。
GASでは、PostでURLが叩かれた時にdoPost関数が呼ばれます。
新しくdoPost関数を作ってみましょう。
function doPost(e){
//送られてきたJSONをオブジェクトに変換する
var contentlist = JSON.parse(e.postData.contents);
//eventは複数送られてくることがあるので各イベントごとに処理を実施
for (event of contentlist.events){
if(event.type == "message"){
//誰が
let userId = event.source.userId;
//どんな内容のメッセージ
let recievedMessage = event.message.text;
//応答トークン
let replyToken = event.replyToken;
//返信用の変数。今回はオウム返しなので、受信メッセージをそのまま入れる。
var replyMessage = recievedMessage;
//送信用のあれこれ。URLに返信用の情報を入れ込んでいる。
let url = 'https://api.line.me/v2/bot/message/reply';
let token = PropertiesService.getScriptProperties().getProperty("token");
var res=UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + token,
},
'method': 'post',
'payload': JSON.stringify({
'replyToken': replyToken,
'messages': [{
'type': 'text',
'text': replyMessage,
}],
}),
});
}
}
//無事に届いたことをレスポンスする。200番台のステータスコードが返れば正直なんでも良い。
return ContentService.createTextOutput(
JSON.stringify({'content': 'post ok'}))
.setMimeType(ContentService.MimeType.JSON);
}
仕上げの連携作業
GASのURLを発行
保存をしたら、MessagingAPIと連携してみましょう。
右上のデプロイボタンより、新しいデプロイを選択し、デプロイのメニューを開きます。
メニューが出てきたら、種類の選択の右側歯車アイコンより、ウェブアプリを選択します。
右側に詳細の設定が出てくるので、アクセスできるユーザを全員に変更し、デプロイを押します。初めてのデプロイなので、外部と繋がるための承認が必要となるので、アクセスを承認してあげましょう。
アクセスを承認するため、アカウントを選択する画面が出てくるので、今回GASを触っているアカウントを選択します。
このアプリケーションはGoogleが認証していないから危険かもしれないよ、と怒られますが、自分自身が作っているものなので、Advancedから詳細画面を表示した上で、Go to 無題のプロジェクト(作成したプロジェクト名)を選択します。unsafeとか言われると安全なはず、と思っていてもドキッとする...
ここまできて、ようやくアプリケーションの外部アクセスを承認する画面が出現します。外とつながるために、allowで許可してしまいましょう。
ここまでできたら、URLの発行が完了です。ウェブアプリのURLをコピーしておきます。
LINE側にURLを設定
LINE Developersよりログインし、作成済みの公式アカウントを選択することで、設定画面に入りましょう。Messaging API設定より、Webhookの設定を探します。WebhookURLに先ほどコピーしたリンクを貼り付け、更新ボタンを押します。また、合わせてWebhookの利用をONにしておきましょう。
動かしてみる
スプレッドシートと連携する
スプレッドシートを作成する
おうむ返しができたので、動くものができて、満足、、、してきたところで当初の目的であるスプレッドシート連携をしていきます。
ベースとなる以下のようなスプレッドシートを作成しました。
項目を追加したら、A~C列に1行ずつ記録されていき、精算をしたらこれらを全て削除する動きをさせてみようと思います。
スプレッドシートのIDを取得する。
GASで、どのスプレッドシートを操作するかを伝えるため、スプレッドシートのIDをコピーしていきます。
スプレッドシートのIDはURLのこの部分となっています。
IDがコピーできたら、スクリプトプロパティにスプレッドシートのIDを格納します。
プロパティ | 値 |
---|---|
token | 【準備の際に発行済みの長期チャネルアクセストークン】 |
sheet | 【スプレッドシートのID】 |
スプレッドシートに値を追加してみる・削除してみる
値の追加
一旦Botからは離れて、スプレッドシートに追記をするコードを書いてみましょう。
確認のためにメッセージを送るのは面倒大変なので、値を追加する関数を書いてみます。
ついでに動くか確かめる関数も追加します。
function addRecord(detail,amount){
let sheetId = PropertiesService.getScriptProperties().getProperty("sheet");
//sheetIdのスプレッドシートを開く
var spreadsheet = SpreadsheetApp.openById(sheetId);
var sheet = spreadsheet.getActiveSheet();
//最終行の位置を格納
var lastRow = sheet.getLastRow();
//今日の日付、明細、金額を最終行の次の行に書き込み
sheet.getRange(lastRow + 1, 1).setValue(new Date());
sheet.getRange(lastRow + 1, 2).setValue(detail);
sheet.getRange(lastRow + 1, 3).setValue(amount);
return detail+" (¥"+amount+")を追加しました。";
}
function test(){
addRecord("牛乳","100");
}
コードを書いたら、保存した上で、testを実行していきたいと思います。
実行する関数を、myFunctionから、testに変更した上で、実行していきます。
初めてスプレッドシートを触るときは、権限の追加が求められますので、権限を確認より許可してあげます。
値の確認
スプレッドシートを見に行くと、無事、追加されていることがわかります。
値の削除
先ほどと同様に削除をする関数も作ってみましょう。
function clearRecord(){
let sheetId = PropertiesService.getScriptProperties().getProperty("sheet");
//sheetIdのスプレッドシートを開く
var spreadsheet = SpreadsheetApp.openById(sheetId);
var sheet = spreadsheet.getActiveSheet();
//最終行の位置
var lastRow = sheet.getLastRow();
//データがない場合、ヘッダが消えてしまうことを防止するため、lastRowが1より大きいの時のみクリアする
if(lastRow>1){
var lastBalance = sheet.getRange("E1").getValue();
var range = sheet.getRange("A2:C" + lastRow);
range.clearContent();
return "¥"+lastBalance+"の精算が完了しました。";
}
return "精算するものはありません。"
}
同様に保存し、clearRecordを実行します。
値の確認
スプレッドシートを見に行くと、無事、値が削除されていることがわかります。
この状態でもう一度clearRecordを実行し、ヘッダが消えないかも確認していきます。
実行し、スプレッドシートを見にいきましたが、大丈夫そうですね。
金額を確認する機能を追加する
金額を確認し、メッセージに適した形へ変換していく機構もここで先に作っておきます。
function outputRecord(){
let sheetId = PropertiesService.getScriptProperties().getProperty("sheet");
//sheetIdのスプレッドシートを開く
var spreadsheet = SpreadsheetApp.openById(sheetId);
var sheet = spreadsheet.getActiveSheet();
//最終行の位置
var lastRow = sheet.getLastRow();
if(lastRow>1){
// 2行目から最終行までの値を取得
let range = sheet.getRange("A2:C"+lastRow);
let data = range.getValues();
//メッセージを追記していく
var replyMessage = "";
for (var i = 0; i < data.length; i++) {
var date = Utilities.formatDate(new Date(data[i][0]), "Asia/Tokyo", "MM/dd");
var detail = data[i][1];
var amount = data[i][2];
replyMessage += date + " " + detail + " (¥" + amount + ")\n";
}
//合計金額も含める
replyMessage += "-------------\n合計 ¥";
replyMessage += sheet.getRange("E1").getValue();
return replyMessage;
}
return "現在精算するものはありません。";
}
オウム返しBotとスプレッドシートをつなげる
どんなメッセージを送ってもらうか
最後に、すでにできているbotと計算するスクリプトをつなげていきます。
今回は、下記の条件で動くようにし、それ以外はエラーを出すようにします。
- 文字の区切りは改行とする。明細[改行]金額の形式で送られてきたものを
スプレッドシートに追記する - 「いくら?」と送信された場合、スプレッドシートの内容と合計金額を返信する
- 「精算済み」と送信された場合、スプレッドシートをクリアにする
メッセージによって処理内容を分岐する
Botの要である、メッセージを振り分ける機構も新たな関数にしてしまいます
キーワードを忘れても良いように、エラー文にキーワードを書いておきます。
function parseMessage(message){
if(message == "いくら?") return outputRecord();
if(message == "精算済み") return clearRecord();
let sentence = message.split("\n");
if(sentence.length == 2){
return addRecord(sentence[0],sentence[1])
}
return "入力形式が誤っています。\nいくら?\n精算済み\nまたは【品目名】[改行]【値段】\nと話しかけてください。"
}
doPostをアップデートする
ここまで糊付けしてきたものをBotに反映させていきます。
最初に作ったものにはなるべく手を出さない形で作ったので1行変えるだけで問題ありません。
//(省略)
if(event.type == "message"){
//誰が
let userId = event.source.userId;
//どんな内容のメッセージ
let recievedMessage = event.message.text;
//応答トークン
let replyToken = event.replyToken;
- //返信用の変数。今回はオウム返しなので、受信メッセージをそのまま入れる。
- var replyMessage = recievedMessage;
+ //返信用の変数。受け取ったメッセージをparseMessageに投げた結果を入れる。
+ var replyMessage = parseMessage(recievedMessage);
//送信用のおまじない。URLに返信用の情報を入れ込んでいる。
//(省略)
編集が完了したら忘れずに保存をしましょう。
デプロイ
修正できたら、botに反映させていきます。
デプロイより、デプロイを管理を選択します。
最初にデプロイしたものが出てくるので、鉛筆アイコンの 編集ボタンをクリックします。
バージョンを選択し、新バージョンを選択します。これで、最新の状態が選ばれている状態になりました。
最後にデプロイを押せば、オウム返しBotは精算Botに生まれ変わっているはずです。
動作確認
おわりに
本記事では、GASを使って、精算Botを作ってみました。
手元に環境がなくても動くものが作れるって素晴らしいですね!
最後までお読みいただき、ありがとうございました。
NTTテクノクロス Advent Calendar 2024、明日は @Daha さんの2回目の記事です!ぜひご覧ください!!
-
推奨されているチャネルアクセストークンは、「任意の有効期間を指定できるチャネルアクセストークン(チャネルアクセストークンv2.1)」ですが、今回は1度発行すると値が変わらない「長期のチャネルアクセストークン」を使用しています。推奨されているものは動的にトークンを生成するコードを別途書く必要があり、難易度が跳ね上がるため、今回は固定の長期のチャネルアクセストークンを使用しています。
認証・検証周りは難しい。。。↩