背景
ごはん会とごはん会ポイント
僕の住むシェアハウスでは、不定期にごはん会が開催されます。
いつも各自が自炊するハウスメイトたちが、一緒に楽しく美味しいごはんを作り、普段交わらない組み合わせのメンバが集まったりすると新たな化学反応が生まれたりして、楽しく交流できるとても良いイベントです。
シェアハウスの運営陣が支援してくれているのか、このごはん会を盛り上げるための**「ごはん会ポイント」**という制度が存在します。
ごはん会ポイントはある一定数のハウスメンバーが集まってごはん会を開催するとポイントが加算されます。
ポイントが貯まるとたこ焼き器やミキサー等、ごはん会をより盛り上げるための景品と交換してもらえます。
普通にお得^^
現状のポイントカウント方法の課題
ごはん会後に、開催したことを証明する記録用紙に誰かが記録します。
しかし、ごはん会後は後片付け/食器洗いをしているメンバーと、片付け組からはみ出たリビングでくつろいでいるメンバーの大きく2組に別れ、後者は基本スマホを眺めていることが多いです。
これが重なると、ごはん会を開催しているのにポイントが加算されずに放置され、貯まるはずのポイントが貯まらないので損をしてしまいます。
じゃあ、そもそも紙で書いて証明する必要がなさそうだし、
LINEにbot仕込んでそいつにポイント管理させればスマホで簡単に記録できるし把握もできるんじゃね?
と思いはじめました。
LINEで完結すれば、紙で書くめんどくささもなくなるし、
片付け組からはみ出たリビングでくつろいでいるメンバー
記録するときはこの人たちにお願いすればよさそうだなとか思ったりしました笑
出来上がったbot
使い方
**「ご飯会したよ」**で現在のポイントから開催分のポイントを加算してくれます

不正防止で、 同日に「ご飯会したよ」って言うとBotに注意されます。
基本的にごはん会が同日に開催されることってないのでこの仕様にしています。

**「ポイント教えて」**で現在のポイントを確認できます。
ポイントで何と交換する〜?って話し合うときにこれで外出しているときも使えますね。

**「ポイント編集 [適当な数値]」**でポイントを更新します。
ただし、権限付きで、LINEグループでは僕だけがこの機能を使えます。

仕組み
使ったサービスは
- LINE Messaging API
- GAS(Google Action Script)
- Google Sheets(スプレッドシート)
LINEでのユーザの発言をもとにGASでリプライの処理をさせます。
スプレッドシートで、ごはん会の開催日時とポイントを管理します。
下準備
GASに処理を書いていく前に、必要な物を準備しましょう。
LINEのBotのアカウント作成とスプレッドシートの作成が必要になるのでそれをやっていきます。
LINE Botの作成にはLINE Developersに登録している必要がありますが、そこは省略させていただきます。
LINE Botのアカウント作成
LINE Developersの登録が完了したらLINE Developersコンソールを開いてチャネルを作成します。
必要な情報を入力して作成します。
チャネルの種類という項目はMessaging APIにして、他の必須項目は自分で任意の情報を入力してください。
チャネルを作成したら設定をしていきます。
Messaing API設定の項目に行くとQRコードが表示されるので自分の端末に読み込んで友達登録しておきましょう。

LINE公式アカウント機能の項目は、今回はシェアハウスのLINEグループに導入するので以下で設定します。

そのすぐ下にあるチャネルアクセストークンの発行ボタンを押して、トークンを発行しましょう。
このトークンはGASで実装する際に使うのでコピーしておきましょう。

これでBotアカウントの準備は完了です。
スプレッドシートの準備
Googleドライブで任意の場所でスプレッドシートを作成します。
スプレッドシートにはこれだけしか用意していません。

ついでにGASのエディタも開いてしまいましょう
「ツール」 > 「スクリプト エディタ」で開きます。

実装
ポイントの更新・確認・編集をやっていきます。
ヘルプ機能は、言ってしまえば文字列を返すだけなので省略させてもらいます。
ひとまずBotにおうむ返しさせてみる
下記のコードで実現できます。
CHANNEL_ACCESS_TOKENは先ほどLINE Developersのチャネル設定で発行したトークンのことです。
// Messaging APIのチャネルアクセストークン
const CHANNEL_ACCESS_TOKEN = '作成したトークン'
// 応答メッセージ用のAPI URL
const url = 'https://api.line.me/v2/bot/message/reply';
function doPost(e) {
// WebHookで受信した情報
const event = JSON.parse(e.postData.contents).events[0];
// WebHookで受信した応答用Token
const replyToken = event.replyToken;
// ユーザからのメッセージ
const userMessage = event.message.text;
UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
},
'method': 'post',
'payload': JSON.stringify({
'replyToken': replyToken,
'messages': [{
'type': 'text',
'text': userMessage,
}],
}),
});
return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
デプロイと確認
コードを書き終わったらエディタを開いている画面右上に青色のデプロイボタンがあるのでそれを押します。
このようなビューが表示されるので、「種類の選択」からウェブアプリを選択します。

説明にはどういうデプロイなのかを説明する文を入れます。(入れなくても良いですが、元に戻したい時にたくさんのデプロイバージョンがあると戻したいバージョンがわからなくなるので入れておいた方が良さげ)
アクセスできるユーザは全員を選択します。
最後にデプロイを押します。

デプロイが更新されると下記のビューが表示されるので、ウェブアプリのURLをコピーします

LINE Officail Account Managerの自分の作成したBotアカウントの設定ページに行き、Messaging APIのWebhook URL欄にコピーしたURLをペーストして、保存します。

保存が完了したらLINEを開いて適当に文字をBotに送ってみましょう。
おうむ返しされるようになります。

これ以降、デプロイするときは毎回この流れを取ってもらえればOKです。
ポイント確認をする
ユーザからのメッセージがポイント教えてだったらポイントを確認するような処理を書いていきます。
用意したスプレッドシートのB2にごはん会ポイントが記録されているのでその情報を取得するようにします。
SpreadsheetApp.openById()の括弧内のスプレッドシートIDは用意したスプレッドシートの、
https://docs.google.com/spreadsheets/d/XXXXXX/edit#gid=0とあった場合のXXXXXX部分です。
また、spreadSheet.getSheetByName()の括弧内のシート名は、文字通りスプレッドシートのシート名を指定してあげればOKです。
デフォルトだとsheet1みたいになっている部分ですね。
// スプレッドシート
const spreadSheet = SpreadsheetApp.openById('スプレッドシートID');
function doPost(e) {
// 省略
const reply = replyMessage(userMessage);
// 特定の文言ではない限りリプライは送らない
if (reply) {
UrlFetchApp.fetch(url, {
// 省略
'messages': [{
'type': 'text',
'text': reply, //userMessageだったけどreplyに変更
}],
// 省略
});
}
}
function replyMessage(message) {
const sheet = spreadSheet.getSheetByName('シート名');
const point = sheet.getRange('B2').getValue();
if (message == 'ポイント教えて') {
return '今のご飯会ポイントは\n' + point + 'pt です';
}
}
ポイント更新
ごはん会したよと報告が来たらポイントを更新するような処理を書いていきます。
今回のコードでは、報告を受けたら+300ptするようにしています。
処理は確認とほとんど変わりません。
スプレッドシートをgetしていましたが、setしてあげれば良いです。
説明の簡略化のため、確認時にも使ったreplyMessageの中身を書き換えます。
function replyMessage(message) {
const sheet = spreadSheet.getSheetByName('シート名');
const point = sheet.getRange('B2').getValue();
if (message == 'ごはん会したよ') {
const newPoint = point + 300;
//スプレッドシートのデータを更新しておく
sheet.getRange('B2').setValue(newPoint);
return 'ご飯会ポイントは' + newPoint + 'ptになりました';
}
}
不正防止
同じ日にごはん会したよと来たらポイントが連続で増え続けてしまいますのでそれを防ぎましょう。
最終更新の日付の情報はスプレッドシートのC2に記録していたので、そこを参照します。
スプレッドシートから取得した日付とDate()で取得した現在の日付のデータがフォーマットが統一されておらず、そのままだと日付が同じかどうかが比較できないので、フォーマットを合わせる必要があります。
Moment.jsという日付操作系ライブラリもあるのでそっちのが楽かなと思いましたが、今回はライブラリ使わない縛りでやっていたのでこのような形になりました。
function updateMessage(sheet, point) {
const date = sheet.getRange('C2').getValue();
const today = new Date();
const formattedDate= Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd');
const formattedToday= Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy/MM/dd');
const shouldUpdate = (formattedToday != formattedDate);
// すでに同日に更新がされている場合は更新を行わない
if (shouldUpdate) {
// 更新する処理をして、文言を返す
} else {
// 更新できない旨の文言を返す
}
}
ポイント編集(一部ユーザのみ)
ポイント編集 [任意の数値]で権限のあるユーザだけポイントをいじれるような処理を書きます。
まずはコード内の(1)でwebhookで受信した情報からユーザのIDを取得しておきます。
(2)で(1)で取得したユーザIDと、もともと定めていた権限のあるユーザのIDと一致するかを確認します。
このときちょっと癖があるのが、文字列の部分一致になるところです。
そこでmatch()を使用しました。
(3)では変更するポイントの数値を取りたいので、半角スペースを区切りとみなし数値部分だけを取得し、その下のisFinite()で数値かどうかを判定するようにしました。
// ユーザIDを格納するための変数
let userId;
function doPost(e) {
// WebHookで受信した情報
const event = JSON.parse(e.postData.contents).events[0];
// 省略
userId = event.source.userId; //(1)
// 省略
}
function replyMessage(message) {
const sheet = spreadSheet.getSheetByName('ご飯会pt');
const point = sheet.getRange('B2').getValue();
// 編集権限ユーザ、かつ編集するための文字列かを判定
if (message.match('ポイント編集') && (userId == '編集しても良いユーザのID')) { //(2)
const newPoint = Number(message.split(' ')[1]); //(3)
// 数値か無効な値かを判定
if (isFinite(newPoint)) {
sheet.getRange('B2').setValue(newPoint);
return 'ご飯会ポイントが' + point + 'ptから' + newPoint + 'ptに更新されました';
} else {
return '無効な値が入力されています'
}
}
}
デバッグ
下記の記事をそのまま参考にしてもらえると良いかと思います。
スプレッドシートにデバッグしたい情報を出力するようにしているので大変役立ちました。
コード全体
まだ汚いのですが💦、すこしだけまとめました。
長いので折りたたみました。▶︎を押すと開きます
// Messaging APIのチャネルアクセストークン
const CHANNEL_ACCESS_TOKEN = "取得したトークン";
// 応答メッセージ用のAPI URL
const url = 'https://api.line.me/v2/bot/message/reply';
// スプレッドシート
const spreadSheet = SpreadsheetApp.openById('スプレッドシートID');
// 権限持つ人のID
const Author_ID = '権限持たせたいユーザID';
// ユーザID
let userId;
// ユーザから来るメッセージの種類
const UserMessageType = {
check : 'ポイント教えて',
report : 'ご飯会したよ',
edit : 'ポイント編集 ',
help : 'ヘルプ'
}
// 受信した情報をもとにリプライを送信
function doPost(e) {
// WebHookで受信した情報
const event = JSON.parse(e.postData.contents).events[0];
const replyToken = event.replyToken;
const userMessage = event.message.text;
userId = event.source.userId;
const reply = replyMessage(userMessage);
// 特定の文言ではない限りリプライは送らない
if (reply) {
UrlFetchApp.fetch(url, {
'headers': {
'Content-Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
},
'method': 'post',
'payload': JSON.stringify({
'replyToken': replyToken,
'messages': [{
'type': 'text',
'text': reply,
}],
}),
});
return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
}
// リプライメッセージの作成
function replyMessage(message) {
const sheet = spreadSheet.getSheetByName('シート名');
const point = sheet.getRange('B2').getValue();
switch (message) {
case UserMessageType.check:
return '今のご飯会ポイントは\n' + point + 'pt です';
case UserMessageType.report:
return updateMessage(sheet, point);
case UserMessageType.help:
return '適当な文言';
default:
return editMessage(sheet, message, point);
}
}
// ご飯会報告時のリプライ文言を作成
function updateMessage(sheet, point) {
const date = sheet.getRange('C2').getValue();
const today = new Date();
const formattedDate= Utilities.formatDate(date, 'Asia/Tokyo', 'yyyy/MM/dd');
const formattedToday= Utilities.formatDate(today, 'Asia/Tokyo', 'yyyy/MM/dd');
const shouldUpdate = (formattedToday != formattedDate);
// すでに同日に更新がされている場合は更新を行わない
if (shouldUpdate) {
const newPoint = point + 300;
//スプレッドシートのデータを更新しておく
sheet.getRange('B2').setValue(point);
sheet.getRange('C2').setValue(date);
return 'ご飯会ポイントは' + newPoint + 'ptになりました';
} else {
return 'もうご飯会はやったことを伝えるメッセージ';
}
}
// 編集時のリプライ文言を作成
function editMessage(sheet, message, point) {
// 編集権限を持っており、かつ編集するための文字列かを判定
if (message.match(UserMessageType.edit) && (userId == Author_ID)) {
const newPoint = Number(message.split(' ')[1]);
// 数値か無効な値かを判定
if (isFinite(newPoint)) {
sheet.getRange('B2').setValue(newPoint);
return 'ご飯会ポイントが' + point + 'ptから' + newPoint + 'ptに更新されました';
} else {
return '無効な値が入力されています'
}
}
}
おわりに
動かすだけなら2時間くらい、慣れないgasのコーディングで汚くなったソースコードを綺麗にしたりグループに導入したりするのにちょっと詰まったりしたものの、合計で約6時間くらいで完成しました。(コードがまだ綺麗になりそうな気がしているので、アドバイスがある方はコメントをいただけると幸いです🙇♂️)
個人利用くらいの規模感で使うならMessaging APIを無料で使えるので、お金もかけずに生活の一部を自動管理して便利にできそうですね。
