【開発】LINEを用いたTodo管理Botの作成と仮運用

1. 本開発の目的について

 従来のToDo管理アプリは、
いちいち専用のアプリを開いて、
画面を開いて、
予定を入力して、
確認して、
削除して……と、
とにかく“アプリを触る”という行為が避けられなかった。

でも実際問題、
人類はもうアプリを増やしたくない。

なら LINE公式アカウントを使って、連絡のついでに予定管理できれば最強じゃね?
と思い、勢いのまま実際に作ってる。

しかし、ただ管理するだけじゃ面白くないので、

  • 時間になったら通知してくれるアラーム機能
  • グループLINEでも使える
  • 誰かが登録した予定もみんなで見れる
  • 通知したら予定が勝手に消える

うーむ。いい感じ。これはもうカレンダーも兼用。
というわけで今回の設計目標は以下の通り。

  • LINEだけでTodo/予定の登録ができること
    →アプリ不要で連絡ついでに管理可能
  • 予定が来たら自動で通知
    →人間の記憶など信用せず、アラームやカレンダーとしての機能も付随
  • 通知後は自動で予定を削除
    →消すのめんどい

今回はこのような目標を立てて開発を行う。

2. 開発環境(実験環境)

 本プロジェクトはとりあえず動かしたいという気持ちを軸に、ローカルPCとベースとした簡易な開発環境で構築を行った。
特別なクラウドやサーバーは使用せず、あくまで家のPCで実験し、LINEでの動作確認という流れである。
実際に使用した環境は以下の通り。

  • Windows11
  • Python 3.13.9
  • Flask(簡易ウェブサーバー構築)
  • line-bot-sdk (Messaging APIと接続)
  • ngrok(外部への公開用、無料プラン)
    ローカルポート8000より外部URLへ転送
ngrock http 8000

  これで生成された https://xxxx.ngrok-free.dev/callback をLINE DevelopersのWebhool URLとして設定
  Webhookの挙動確認は、LINE Developersの↓

  • Webhookテスター
  • 再送機能
  • エラー統計

  も活用して行った。

  • LINE 公式アカウント(Messaging API)
  • 実験用の個人LINE (小川琉盛本人のアカウントを使用)

  今回のBotは完全にFlask単体にngrokの構成のため、

  • Webサーバー
  • データベース
  • 常時稼働環境

等は現時点では未導入。予定データはPythonメモリ上で管理し、再起動でリセットされる。

2.1. Bot作成

以下に今回作成したプログラムを乗せる。尚、このプログラムはChatGPTを使用して開発した

// ================================
// LINE予定管理Bot(GAS版 完全動作品)
// ================================

// ★ 必ず自分のチャネルアクセストークンに変更
const CHANNEL_ACCESS_TOKEN = 'YOUR_ACCESS_TOKEN_HERE';

// ================================
// メイン処理(LINE → Webhook)
// ================================
function doPost(e) {
  const json = JSON.parse(e.postData.contents);
  const event = json.events[0];

  if (!event || event.type !== "message" || event.message.type !== "text") {
    return ContentService.createTextOutput("ok");
  }

  const userId = event.source.userId;
  const text = event.message.text.trim();

  // 「/」で始まらないメッセージには反応しない
  if (!text.startsWith("/")) {
    return ContentService.createTextOutput("ok");
  }

  const parts = text.split(" ");
  const command = parts[0];

  switch (command) {
    case "/登録":
      return replyMessage(event.replyToken, registerTodo(userId, parts));
    case "/一覧":
      return replyMessage(event.replyToken, listTodo(userId));
    case "/削除":
      return replyMessage(event.replyToken, deleteTodo(userId, parts));
    case "/コマンド":
      return replyMessage(event.replyToken, showCommands());
    default:
      return replyMessage(event.replyToken, "不明なコマンドです。\n「/コマンド」で使えるコマンドを確認できます。");
  }
}

// ================================
// コマンド一覧
// ================================
function showCommands() {
  return (
    "📌 使用できるコマンド\n\n" +
    "/登録 YYYY/MM/DD HH:MM 内容\n" +
    "/一覧\n" +
    "/削除 登録番号\n" +
    "/コマンド"
  );
}

// ================================
// 予定登録
// ================================
function registerTodo(userId, parts) {
  if (parts.length < 4) {
    return "形式:/登録 YYYY/MM/DD HH:MM 内容";
  }

  const date = parts[1];
  const time = parts[2];
  const content = parts.slice(3).join(" ");

  const sheet = getSheet();
  sheet.appendRow([userId, date, time, content, new Date(), "0"]);

  return (
    "予定を登録しました!\n" +
    `📆 日付: ${date}\n` +
    `⏰ 時間: ${time}\n` +
    `📝 内容: ${content}`
  );
}

// ================================
// 一覧表示
// ================================
function listTodo(userId) {
  const sheet = getSheet();
  const data = sheet.getDataRange().getValues();

  const list = data
    .map((row, i) => ({ index: i + 1, row }))
    .filter(item => item.row[0] === userId);

  if (list.length === 0) return "登録されている予定はありません。";

  let msg = "📌 登録中の予定\n\n";
  list.forEach(item => {
    msg += `${item.index}. ${item.row[1]} ${item.row[2]} ${item.row[3]}\n`;
  });

  return msg;
}

// ================================
// 削除
// ================================
function deleteTodo(userId, parts) {
  if (parts.length < 2) return "形式:/削除 番号";

  const num = Number(parts[1]);
  if (isNaN(num)) return "番号は数字で入力してください。";

  const sheet = getSheet();
  const data = sheet.getDataRange().getValues();

  if (num < 1 || num > data.length) {
    return "その番号の予定は存在しません。";
  }

  sheet.deleteRow(num);
  return `予定 ${num} を削除しました。`;
}

// ================================
// 通知処理(トリガーで1分ごと)
// ================================
function checkAndNotify() {
  const sheet = getSheet();
  const data = sheet.getDataRange().getValues();
  const now = new Date();

  for (let i = 0; i < data.length; i++) {
    const [userId, date, time, content, created, sent] = data[i];

    if (sent === "1") continue;

    const target = new Date(`${date} ${time}`);
    if (Math.abs(now - target) < 60000) {
      notifyUser(userId, `🔔 予定の時間です!\n${date} ${time}\n${content}`);
      sheet.getRange(i + 1, 6).setValue("1");
    }
  }
}

// ================================
// 通知送信
// ================================
function notifyUser(userId, message) {
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push", {
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    method: "post",
    payload: JSON.stringify({
      to: userId,
      messages: [{ type: "text", text: message }],
    }),
  });
}

// ================================
// 返信送信
// ================================
function replyMessage(token, message) {
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", {
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    method: "post",
    payload: JSON.stringify({
      replyToken: token,
      messages: [{ type: "text", text: message }],
    }),
  });
}

// ================================
// シート取得
// ================================
function getSheet() {
  return SpreadsheetApp.getActiveSpreadsheet().getSheetByName("todo");
}

このプログラムを用いて実験を行う。尚、以下にプログラム呼び出しコマンドを示す。

  1. 予定登録
    /予定登録 年 月 時間 分 内容
    例 /予定登録 2025 12 24 23 59 メリー苦しみます
  2. 登録済み予定の表示
    /予定表示
  3. 予定の削除
    /予定表示
  4. コマンド一覧表示
    /コマンド

これらを用いて、Todoリストの作成、管理を行う。尚、予定時刻になったら自動で削除される。

3. 実際の運用 

では実際に運用している様子をご覧くにいれよう。(YouTubeの限定公開を用いています)

ちなみに、この動画はプログラムを改良する前の動作の様子(このサイトで上げたプログラムが改良後)である為に、このサイト上のコマンドでは動かない。
実際に運用したい場合はサイト上のプログラムとサイト上のコマンドを用いて行うことを推奨する。
(Q : …なんで改良後のプログラムの動画が無いの? A : パソコンのデータを初期化してしまった為。)

4. 最後に

今回ではLINE公式アカウントとその他様々な物を用いてデモ運用を行ったが、結果としては「別にわざわざラインでやる必要はないのでは」という感想である。しかし、LINE内ですべてが完結するのでスマホの容量が少ない人や、他のアプリをあまり入れたくない人にとっては気休め程度にはなるのかもしれない。
今回は作業用PCで直接サーバー化したが、他のプロジェクトのためにN100ミニPCを調達予定であるため、機会があれば実際に皆さまが活用できるように作業したいと思う。
最後まで読んでくださりありがとうございました。

Back to top