はじめに
iPhoneのリマインダーを使っている。細かくリマインドを設定している。そして毎回無視している。
問題はリマインダーそのものではなく、「無視するコストがゼロ」であることだと気づいた。通知を見て、スワイプして消す。この動作が0.3秒で完了してしまう。抵抗がなさすぎる。
ならば、無視するたびに鬱陶しさがエスカレートしていくBotを作ればいいのでは、と思った。タスクをやるより無視し続けるほうが辛い設計にする。行動経済学でいう「ナッジ」の考え方だ。
この記事では、LINE Messaging APIとNode.js(TypeScript)を使ってそのBotを作った過程を書く。詰まったところも正直に書く。
作ったもの
LINEでタスクを登録すると、設定した時刻に通知が来る。無視すると30分後にまた来る。さらに無視すると口調がだんだん強くなっていく。「やった」と送ると完了になり、翌日リセットされる。
ユーザー → 「追加: 筋トレ 毎朝8時」
Bot → 「筋トレを登録しました。最初の通知:2026/5/13 8:00」
--- 翌朝8時 ---
Bot → 「筋トレの時間です。やりましたか?」
ユーザー → (無視)
--- 30分後 ---
Bot → 「筋トレ、まだですか?」
--- さらに30分後 ---
Bot → 「いい加減にしてください。筋トレやってください。」
ユーザー → 「やった」
Bot → 「筋トレ完了!お疲れ様です。次回は明日また声をかけます。」
技術スタック
| 役割 | 技術 |
|---|---|
| サーバー | Node.js + Express + TypeScript |
| DB | Turso(SQLiteのクラウド版) |
| ORM | Prisma 6 |
| 通知 | LINE Messaging API |
| 定期実行 | node-cron |
| ホスティング | Render(無料プラン) |
Claude APIでメッセージ生成することも考えたが、APIの課金が必要だったので今回はエスカレーションメッセージを固定文にした。十分鬱陶しい。
ディレクトリ構成
nag-bot/
├── prisma/
│ └── schema.prisma
├── src/
│ ├── index.ts # エントリーポイント
│ ├── webhook.ts # LINEのWebhook処理
│ ├── scheduler.ts # 定期チェック
│ ├── notifier.ts # LINE通知送信
│ └── taskParser.ts # コマンドのパース
├── .env
├── package.json
└── tsconfig.json
実装
DBスキーマ
model Task {
id Int @id @default(autoincrement())
userId String
title String
cronExpression String
ignoreCount Int @default(0)
nextNotifyAt DateTime
completedAt DateTime?
createdAt DateTime @default(now())
}
ignoreCountがエスカレーションのキー。無視するたびにインクリメントされ、メッセージの強度を決める。
エスカレーションロジック
const NAG_MESSAGES = [
(title: string) => `「${title}」の時間です。やりましたか?`,
(title: string) => `「${title}」、まだですか?`,
(title: string) => `いい加減にしてください。「${title}」やってください。`,
(title: string) => `本当にやる気あるんですか。「${title}」。`,
(title: string) => `…(呆れ)。「${title}」。早くしてください。`,
];
const ESCALATION_INTERVALS = [30, 30, 60, 120]; // 分
1分ごとにcronで未完了タスクをチェックし、nextNotifyAtを過ぎていたら通知を送る。送るたびにignoreCountを増やし、次回通知時刻を設定する。
Tursoへの接続
import { PrismaLibSql } from "@prisma/adapter-libsql";
import { PrismaClient } from "@prisma/client";
const adapter = new PrismaLibSql({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
});
export const prisma = new PrismaClient({ adapter });
コマンドのパース
LINEで「追加: 筋トレ 毎朝8時」と送るとタスクが登録される。自然言語をcron式に変換している。
export function parseAddCommand(text: string): ParseResult {
const match = text.match(/^追加[::]\s*(.+?)\s+(.+)$/);
if (!match) return null;
const title = match[1];
const scheduleText = match[2];
const cron = parseCronFromText(scheduleText);
if (!cron) return null;
return { title, cronExpression: cron.expression, nextNotifyAt: cron.nextDate };
}
現在対応しているのは「毎朝HH時」「毎晩HH時」「毎日HH時」の形式。
コマンド一覧
| コマンド | 動作 |
|---|---|
追加: タスク名 毎朝8時 |
タスク登録 |
やった |
直近タスクを完了 |
削除: タスク名 |
タスクを削除 |
リスト |
未完了タスク一覧 |
詰まったポイント
1. LINE Developers ConsoleのUI変更
以前はLINE Developers ConsoleからMessaging APIチャネルを直接作れたが、現在はLINE公式アカウントを先に作り、そこからMessaging APIを有効化する手順に変わっている。
手順:
- LINE Official Account Manager でアカウント作成
- 設定 → Messaging API → 「Messaging APIを利用する」
- するとLINE Developers Consoleにチャネルができるのでそこでトークンを発行
ドキュメントが追いついていない部分があるので注意。
2. Prisma 7の破壊的変更
npm install prismaすると最新のPrisma 7が入るが、Prisma 7ではschema.prisma内のurl指定が廃止されている。prisma.config.tsという新しいファイルで設定する必要がある。
今回はPrisma 6を明示的に指定してこの問題を回避した。
npm install prisma@6 @prisma/client@6
3. RenderでのTypeScript型エラー
ローカルでは@types/nodeと@types/expressがdevDependenciesに入っていても動くが、Renderのビルド時はdevDependenciesをインストールしないため型エラーになる。
package.jsonを直接編集してdependenciesに移動した。
4. LINE公式アカウントの自動応答
Botに友達追加した直後、メッセージを送ると「このアカウントでは個別のお問い合わせは受け付けておりません」という自動応答が返ってくる。
LINE Official Account Managerの「応答設定」→「応答メッセージ」をオフにすることで解決する。
5. RenderのSQLiteはサーバー再起動でリセットされる
Renderの無料プランはサーバーが再起動するたびにファイルシステムがリセットされる。SQLiteのDBファイルも消えるため、タスクが全部消えてしまう。
これを解決するためにTursoというSQLiteのクラウド版に移行した。Tursoは無料プランがあり、接続もPrismaのアダプター経由で簡単にできる。
# Turso CLIのインストール
brew install tursodatabase/tap/turso
# ログインとDB作成
turso auth login
turso db create nag-bot
# 接続情報の取得
turso db show nag-bot # URL
turso db tokens create nag-bot # 認証トークン
PrismaのアダプターはPrisma 6の@prisma/adapter-libsqlを使う。
npm install @prisma/adapter-libsql @libsql/client
注意点として、TursoはPrismaのmigrate deployコマンドが使えない。初回のテーブル作成はmigration.sqlを直接流す必要がある。
turso db shell nag-bot < prisma/migrations/XXXXXX_init/migration.sql
Renderのstart commandもnpx prisma migrate deploy && npm startからnpm startに変更する。
6. @prisma/adapter-libsqlをdependenciesに入れる
@prisma/adapter-libsqlと@libsql/clientをdevDependenciesに入れてしまうとRenderのビルド時に除外されてエラーになる。他の@types系パッケージと同様にdependenciesに入れること。
設計について
このBotの本質は「意志力に頼らない設計」だ。
一般的なリマインダーは「やるべきことを思い出させる」ことしかしない。それだけでは行動は変わらない。人間は面倒なことを後回しにするようにできている。
このBotは「無視し続けることの方が面倒」という状況を作り出す。ペインドリブンな設計だ。タスクをやるより無視する方が辛いなら、やった方が楽になる。
完璧な設計ではないし、Botをブロックすれば終わりだ。でも少なくとも自分には効いている。
今後やりたいこと
- 週次サボりレポート:「今週の無視回数ランキング」を月曜朝に送る
- Claude APIの導入:無視回数に応じて動的にメッセージを生成する
- 複数ユーザー対応:チームで使えるようにする
- スケジュール形式の拡充:「毎週月曜」「毎月1日」などに対応する
おわりに
自分の実際の困りごとを題材にした。
作ってみると意外と詰まるところが多く大変だった。
こういう躓きも糧にしていきたい。
今回はClaudeにだいぶ助けられた。
なんならこの記事も書いてもらっている。
AIってすごいですね。