diff --git a/src/bot/index.ts b/src/bot/index.ts new file mode 100644 index 0000000..05978f5 --- /dev/null +++ b/src/bot/index.ts @@ -0,0 +1,44 @@ +import { Telegraf } from 'telegraf'; +import type { Database } from 'better-sqlite3'; +import { handleLog } from './commands/log'; +import { handleSkip } from './commands/skip'; +import { handleSnooze } from './commands/snooze'; +import { handleStatus } from './commands/status'; +import { handleHistory } from './commands/history'; +import { handleStreak } from './commands/streak'; +import { handleSettings } from './commands/settings-cmd'; +import { handleProgress } from './commands/progress'; +import { handleNext } from './commands/next'; +import { handleReschedule } from './commands/reschedule'; +import { handleChat } from './commands/chat'; +import { handleSetup, handleSetupReply } from './commands/setup'; + +export function createBot(token: string, chatId: string, db: Database): Telegraf { + const bot = new Telegraf(token); + + bot.command('log', handleLog(db)); + bot.command('skip', handleSkip(db)); + bot.command('snooze', handleSnooze(db, chatId)); + bot.command('status', handleStatus(db)); + bot.command('history', handleHistory(db)); + bot.command('streak', handleStreak(db)); + bot.command('settings', handleSettings(db)); + bot.command('progress', handleProgress(db)); + bot.command('next', handleNext(db)); + bot.command('reschedule', handleReschedule(db)); + bot.command('chat', handleChat(db)); + bot.command('setup', handleSetup(db)); + + const setupReply = handleSetupReply(db); + bot.on('text', async (ctx) => { + const text = (ctx.message as { text?: string })?.text ?? ''; + if (text.startsWith('/')) return; + await setupReply(ctx); + }); + + bot.catch((err) => { + console.error('Bot error:', err); + }); + + return bot; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..b6c736f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,22 @@ +import { config } from './config'; +import { getDb } from './db'; +import { createBot } from './bot'; +import { startScheduler } from './scheduler'; + +async function main(): Promise { + const db = getDb(config.dbPath); + const bot = createBot(config.telegramToken, config.telegramChatId, db); + + startScheduler(bot, db, config.telegramChatId); + + await bot.launch(); + console.log('Training assistant bot started.'); + + process.once('SIGINT', () => bot.stop('SIGINT')); + process.once('SIGTERM', () => bot.stop('SIGTERM')); +} + +main().catch((err) => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/src/scheduler/index.ts b/src/scheduler/index.ts new file mode 100644 index 0000000..7178e4f --- /dev/null +++ b/src/scheduler/index.ts @@ -0,0 +1,34 @@ +import cron from 'node-cron'; +import type { Telegraf } from 'telegraf'; +import type { Database } from 'better-sqlite3'; +import { getSetting } from '../db/settings'; +import { config } from '../config'; +import { sendMorningReminder, sendEveningNudge } from './reminders'; + +function parseCronTime(hhmm: string): string { + const [hh, mm] = hhmm.split(':'); + return `${mm} ${hh} * * *`; +} + +export function startScheduler(bot: Telegraf, db: Database, chatId: string): void { + const morningTime = getSetting(db, 'morning_reminder') ?? config.morningReminder; + const eveningTime = getSetting(db, 'evening_reminder') ?? config.eveningReminder; + + cron.schedule(parseCronTime(morningTime), async () => { + try { + await sendMorningReminder(bot, db, chatId); + } catch (err) { + console.error('Morning reminder failed:', err); + } + }); + + cron.schedule(parseCronTime(eveningTime), async () => { + try { + await sendEveningNudge(bot, db, chatId); + } catch (err) { + console.error('Evening nudge failed:', err); + } + }); + + console.log(`Reminders scheduled: morning=${morningTime}, evening=${eveningTime}`); +} diff --git a/src/scheduler/reminders.ts b/src/scheduler/reminders.ts new file mode 100644 index 0000000..87ee111 --- /dev/null +++ b/src/scheduler/reminders.ts @@ -0,0 +1,34 @@ +import type { Telegraf } from 'telegraf'; +import type { Database } from 'better-sqlite3'; +import { todayString } from '../utils/date'; +import { getScheduleByDate } from '../db/schedule'; +import { getSessionByDate } from '../db/sessions'; +import { formatDuration } from '../utils/duration'; + +export async function sendMorningReminder(bot: Telegraf, db: Database, chatId: string): Promise { + const today = todayString(); + const scheduled = getScheduleByDate(db, today); + if (!scheduled || scheduled.status !== 'pending') return; + + const session = getSessionByDate(db, today); + if (session) return; + + await bot.telegram.sendMessage( + chatId, + `Good morning! You have a ${formatDuration(scheduled.planned_min)} session planned today.\n\nUse /log when done, /skip if you can't make it, or /snooze to remind you later.` + ); +} + +export async function sendEveningNudge(bot: Telegraf, db: Database, chatId: string): Promise { + const today = todayString(); + const scheduled = getScheduleByDate(db, today); + if (!scheduled || scheduled.status !== 'pending') return; + + const session = getSessionByDate(db, today); + if (session) return; + + await bot.telegram.sendMessage( + chatId, + `Still time for today's session (${formatDuration(scheduled.planned_min)})!\n\nUse /log to record it, or /skip to mark it as skipped.` + ); +}