前置き
LINEのリマインくんってご存知でしょうか?
LINEでリマインダー設定ができて、繰り返し通知なんかもできるシンプルなbotなのですが、めちゃくちゃ便利です。
スマホのリマインダー使えば良いじゃん?と思うかもしれませんが、スマホのリマインダー、なぜかあんまりリマインドとしては自分的に機能しなくて(簡単に無視できてしまうというか)、LINEだとメッセージとして来るので、ちゃんと自分にはリマインドになるのが好きなんですよ。
ただ残念なのが、定期的なリマインダーの設定はできないので、一回限りでしかリマインドを設定できないことや、同じリマインダー設定を家族で共有できないことがネックだなーと思っていました。
多分今ならManusとかを使えばいけるのかもしれませんが、この方法だとGoogle Spreadsheetに通知先(自分の場合は家族リマインダーとして使っているので、夫・自分・両方に通知可能)、通知時間、通知内容、繰り返しなどを設定できるので割と使いやすいです。
LINEに家族のリマインダーを通知したい方はぜひ使ってみてくださいー。
基本仕様
・リマインダー、と送信すると、リマインダーの形式をbotが教えてくれる
・このbotの言うとおりの形式でTODOを設定するとGoogle Spreadsheetに登録される
(直接自分でリマインダーをスプシに記入してもOK)
・時間になるとリマインドメッセージを送ってくれる。
取り上げないこと
こちらの記事には以下の方法は書いていませんが、必要にはなるので、必要に応じて特化の別記事などを読んでください。
・自分以外のLINEのuser ID取得方法
・LINE botの設定方法
・GASのWebhook公開、設定方法
・GASのトリガースケジュールの設定方法
コード
Google Spreadsheetにはこのような感じで設定しています。
最初の3行はテストで作ったので気にしないでくださいw
また、コードとは別にこのGASコードを30分単位でトリガーする設定を入れています。
// 設定値を取得する関数
function getConfig() {
const properties = PropertiesService.getScriptProperties();
return {
lineAccessToken: properties.getProperty('LINE_CHANNEL_ACCESS_TOKEN'),
myUserId: properties.getProperty('MY_USER_ID'),
husbandUserId: properties.getProperty('HUSBAND_USER_ID')
};
}
// LINEに通知を送信する関数(送信先を指定可能に)
function sendLineNotify(mailBody, targetUserId) {
var payload = {
"to": targetUserId,
"messages": [{
"type": "text",
"text": mailBody
}]
};
try {
const response = UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", {
"method": "post",
"contentType": "application/json",
"headers": {
"Authorization": "Bearer " + getConfig().lineAccessToken,
},
"payload": JSON.stringify(payload),
"muteHttpExceptions": true
});
console.log("成功!!! レスポンスコード: " + response.getResponseCode() + " (送信先: " + targetUserId + ")");
} catch (e) {
console.error("エラーが発生しました: " + e.message);
}
}
// LINE Webhookを受信する関数
function doPost(e) {
try {
const json = JSON.parse(e.postData.contents);
const events = json.events;
// 検証リクエストの場合は200を返す
if (!events || events.length === 0) {
return ContentService.createTextOutput(JSON.stringify({'status': 'ok'}))
.setMimeType(ContentService.MimeType.JSON);
}
const config = getConfig();
events.forEach(event => {
// メッセージイベントのみ処理
if (event.type === 'message' && event.message && event.message.type === 'text') {
const replyToken = event.replyToken;
const userId = event.source.userId;
const messageText = event.message.text;
let replyText = '';
// 「リマインダー」というメッセージへの応答
if (messageText.trim() === 'リマインダー' || messageText.trim() === 'りまいんだー') {
replyText = getHelpMessage();
} else {
// リマインダー追加コマンドの処理
replyText = handleReminderCommand(userId, messageText);
}
// Reply APIで返信
const replyPayload = {
'replyToken': replyToken,
'messages': [{
'type': 'text',
'text': replyText,
}],
};
const options = {
'method': 'post',
'contentType': 'application/json; charset=UTF-8',
'headers': {
'Authorization': 'Bearer ' + config.lineAccessToken,
},
'payload': JSON.stringify(replyPayload),
'muteHttpExceptions': true
};
UrlFetchApp.fetch('https://api.line.me/v2/bot/message/reply', options);
}
});
return ContentService.createTextOutput(JSON.stringify({'status': 'ok'}))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
console.error('doPost error: ' + error);
return ContentService.createTextOutput(JSON.stringify({'status': 'error', 'message': error.toString()}))
.setMimeType(ContentService.MimeType.JSON);
}
}
// ヘルプメッセージを返す関数
function getHelpMessage() {
return '📝 リマインダーを設定してね!\n\n【フォーマット】\n以下の5行を改行区切りで送信してください:\n\n1️⃣ 日付タイプ\n ・特定日\n ・毎週\n ・毎日\n\n2️⃣ 日付または曜日\n ・特定日の場合: 2025/11/15\n ・毎週の場合: 月曜日\n ・毎日の場合: -\n\n3️⃣ 時間(HH:MM形式)\n 例: 09:00\n\n4️⃣ Todo内容\n 例: 会議の準備\n\n5️⃣ 送信先\n ・私\n ・夫\n ・両方\n\n【例1: 毎週のリマインダー】\n毎週\n月曜日\n10:00\n週報作成\n私\n\n【例2: 毎日のリマインダー】\n毎日\n-\n07:00\nおはよう!\n両方\n\n【例3: 特定日のリマインダー】\n特定日\n2025/12/25\n09:00\nプレゼント準備\n夫';
}
// リマインダー追加コマンドを処理する関数
function handleReminderCommand(userId, messageText) {
const config = getConfig();
// メッセージを改行で分割
const lines = messageText.split('\n').map(line => line.trim()).filter(line => line);
// 5行必要(日付タイプ、日付/曜日、時間、Todo内容、送信先)
if (lines.length !== 5) {
return '❌ 入力形式が正しくありません。\n\n「リマインダー」と送信すると、詳しい使い方が表示されます。';
}
const dateType = lines[0];
const dateOrDay = lines[1];
const time = lines[2];
const todoContent = lines[3];
const target = lines[4];
// バリデーション
if (!['特定日', '毎週', '毎日'].includes(dateType)) {
return '❌ 日付タイプは「特定日」「毎週」「毎日」のいずれかを指定してください。';
}
if (!['私', '夫', '両方'].includes(target)) {
return '❌ 送信先は「私」「夫」「両方」のいずれかを指定してください。';
}
// 時間の形式チェック
if (!/^\d{1,2}:\d{2}$/.test(time)) {
return '❌ 時間は「HH:MM」形式で入力してください(例: 09:00)';
}
// 毎週の場合の曜日チェック
if (dateType === '毎週') {
const validDays = ['月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日', '日曜日'];
if (!validDays.includes(dateOrDay)) {
return '❌ 毎週の場合は曜日を正しく入力してください(例: 月曜日)';
}
}
// 特定日の場合の日付チェック
if (dateType === '特定日') {
const datePattern = /^\d{4}\/\d{1,2}\/\d{1,2}$/;
if (!datePattern.test(dateOrDay)) {
return '❌ 特定日の場合は日付を「YYYY/MM/DD」形式で入力してください(例: 2025/11/15)';
}
}
// スプレッドシートに追加
try {
addReminderToSheet(dateType, dateOrDay, time, todoContent, target);
let confirmMessage = '✅ リマインダーを追加しました!\n\n';
confirmMessage += '📅 日付タイプ: ' + dateType + '\n';
if (dateType !== '毎日') {
confirmMessage += '📆 ' + (dateType === '特定日' ? '日付' : '曜日') + ': ' + dateOrDay + '\n';
}
confirmMessage += '⏰ 時間: ' + time + '\n';
confirmMessage += '📝 内容: ' + todoContent + '\n';
confirmMessage += '👤 送信先: ' + target;
return confirmMessage;
} catch (error) {
return '❌ リマインダーの追加中にエラーが発生しました: ' + error.message;
}
}
// スプレッドシートにリマインダーを追加する関数
function addReminderToSheet(dateType, dateOrDay, time, todoContent, target) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
// 毎日の場合は日付/曜日を「-」にする
const dateValue = dateType === '毎日' ? '-' : dateOrDay;
// 通知済みフラグ(特定日の場合はFALSE、それ以外は空)
const notified = dateType === '特定日' ? false : '';
// 新しい行を追加
sheet.appendRow([
dateType,
dateValue,
time,
todoContent,
true, // 有効フラグ(デフォルトで有効)
target,
notified // 通知済みフラグ
]);
console.log('リマインダーを追加: ' + [dateType, dateValue, time, todoContent, target].join(', '));
}
// リマインダーをチェックして通知する関数
function checkAndSendReminders() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
const config = getConfig();
const now = new Date();
const currentDay = now.getDay();
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
const currentTime = currentHour * 60 + currentMinute;
const dayNames = ['日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'];
for (let i = 1; i < data.length; i++) {
const dateType = data[i][0];
const dateOrDay = data[i][1];
const timeStr = data[i][2];
const todoContent = data[i][3];
const isEnabled = data[i][4];
const target = data[i][5];
const notified = data[i][6]; // 通知済みフラグ
if (!isEnabled) continue;
// 特定日で既に通知済みの場合はスキップ
if (dateType === '特定日' && notified === true) {
continue;
}
let targetHour, targetMinute;
if (typeof timeStr === 'string' && timeStr.includes(':')) {
const timeParts = timeStr.split(':');
targetHour = parseInt(timeParts[0]);
targetMinute = parseInt(timeParts[1]);
} else if (timeStr instanceof Date) {
targetHour = timeStr.getHours();
targetMinute = timeStr.getMinutes();
} else {
continue;
}
const targetTime = targetHour * 60 + targetMinute;
if (Math.abs(currentTime - targetTime) > 5) continue;
let shouldNotify = false;
if (dateType === '特定日') {
let targetDate;
if (dateOrDay instanceof Date) {
targetDate = dateOrDay;
} else if (typeof dateOrDay === 'string') {
targetDate = new Date(dateOrDay);
} else {
continue;
}
if (now.getFullYear() === targetDate.getFullYear() &&
now.getMonth() === targetDate.getMonth() &&
now.getDate() === targetDate.getDate()) {
shouldNotify = true;
}
} else if (dateType === '毎週') {
const targetDayName = typeof dateOrDay === 'string' ? dateOrDay : '';
const targetDayIndex = dayNames.indexOf(targetDayName);
if (targetDayIndex === currentDay) {
shouldNotify = true;
}
} else if (dateType === '毎日') {
shouldNotify = true;
}
if (shouldNotify) {
const message = '🔔 Reminder\n\n' + todoContent;
if (target === '私') {
sendLineNotify(message, config.myUserId);
} else if (target === '夫') {
sendLineNotify(message, config.husbandUserId);
} else if (target === '両方') {
sendLineNotify(message, config.myUserId);
sendLineNotify(message, config.husbandUserId);
} else {
console.log('警告: 不明な送信先「' + target + '」がスキップされました');
}
// 特定日の場合は通知済みフラグをTRUEに更新
if (dateType === '特定日') {
// 行番号は i + 1 (ヘッダー行があるため)
sheet.getRange(i + 1, 7).setValue(true); // G列(7列目)を更新
console.log('特定日リマインダーを通知済みに設定: ' + todoContent);
}
}
}
}
// トリガーを設定する関数(初回のみ実行)
function setupTrigger() {
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => ScriptApp.deleteTrigger(trigger));
ScriptApp.newTrigger('checkAndSendReminders')
.timeBased()
.everyMinutes(10)
.create();
Logger.log('トリガー設定完了');
}
// テスト用関数
function testNotification() {
const config = getConfig();
sendLineNotify('テスト通知です!(私宛)', config.myUserId);
sendLineNotify('テスト通知です!(夫宛)', config.husbandUserId);
}
// ヘルプメッセージのテスト
function testHelpMessage() {
console.log(getHelpMessage());
}
自分の場合はリマインくんっぽく、作成したLINE botにリッチメニューを設定し、「リマインダー」という文字を送信すると、リマインドの形式などを教えてくれるようにしています。
