1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LINE botで定期リマインダーを作る

Posted at

前置き

LINEのリマインくんってご存知でしょうか?
LINEでリマインダー設定ができて、繰り返し通知なんかもできるシンプルなbotなのですが、めちゃくちゃ便利です。

スマホのリマインダー使えば良いじゃん?と思うかもしれませんが、スマホのリマインダー、なぜかあんまりリマインドとしては自分的に機能しなくて(簡単に無視できてしまうというか)、LINEだとメッセージとして来るので、ちゃんと自分にはリマインドになるのが好きなんですよ。

ただ残念なのが、定期的なリマインダーの設定はできないので、一回限りでしかリマインドを設定できないことや、同じリマインダー設定を家族で共有できないことがネックだなーと思っていました。

多分今ならManusとかを使えばいけるのかもしれませんが、この方法だとGoogle Spreadsheetに通知先(自分の場合は家族リマインダーとして使っているので、夫・自分・両方に通知可能)、通知時間、通知内容、繰り返しなどを設定できるので割と使いやすいです。

LINEに家族のリマインダーを通知したい方はぜひ使ってみてくださいー。

基本仕様

・リマインダー、と送信すると、リマインダーの形式をbotが教えてくれる
・このbotの言うとおりの形式でTODOを設定するとGoogle Spreadsheetに登録される
(直接自分でリマインダーをスプシに記入してもOK)
・時間になるとリマインドメッセージを送ってくれる。

取り上げないこと

こちらの記事には以下の方法は書いていませんが、必要にはなるので、必要に応じて特化の別記事などを読んでください。

・自分以外のLINEのuser ID取得方法
・LINE botの設定方法
・GASのWebhook公開、設定方法
・GASのトリガースケジュールの設定方法

コード

Google Spreadsheetにはこのような感じで設定しています。

image.png

最初の3行はテストで作ったので気にしないでくださいw

また、コードとは別にこのGASコードを30分単位でトリガーする設定を入れています。

index.js
// 設定値を取得する関数
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にリッチメニューを設定し、「リマインダー」という文字を送信すると、リマインドの形式などを教えてくれるようにしています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?