From 7f92c60ed89e3495f9dd660755a68af647fd8ce2 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Mon, 11 May 2026 13:40:31 +0300 Subject: WIP --- src/bot.ts | 4 +++- src/db.ts | 52 +++++++++++++++++++++++++++++------------- src/tasks.ts | 15 +++++++++--- src/tg.ts | 74 +++++++++++++++++++++++++++++++++++++++++++++++------------- 4 files changed, 109 insertions(+), 36 deletions(-) (limited to 'src') diff --git a/src/bot.ts b/src/bot.ts index 1de3a22..bda8ace 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -13,7 +13,9 @@ const runWeeklyTasks = async (): Promise => { const dueTasks = tasks.filter(isThisWeek); if (dueTasks.length > 0) { - const taskText = dueTasks.map((t) => `• ${t.name} (${t.interval})`).join("\n"); + const taskText = dueTasks + .map((t) => `• ${t.name} (${t.interval})`) + .join("\n"); await broadcastMessage(`📋 Tällä viikolla tehtävät hommat:\n${taskText}`); const todayStr = today(); for (const t of dueTasks) { diff --git a/src/db.ts b/src/db.ts index a2c5cee..c0c7d1a 100644 --- a/src/db.ts +++ b/src/db.ts @@ -38,7 +38,7 @@ const db = new Database(DB_FILE, { db.pragma("journal_mode = WAL"); db.pragma("busy_timeout = 5000"); db.pragma("synchronous = NORMAL"); -db.pragma("cache_size = 1000000000"); +db.pragma("cache_size = 10000"); db.pragma("foreign_keys = ON"); db.pragma("temp_store = MEMORY"); @@ -46,25 +46,25 @@ const ddl = ` BEGIN IMMEDIATE; CREATE TABLE IF NOT EXISTS active_chats ( id INTEGER PRIMARY KEY - ) STRICT; + ) STRICT; CREATE TABLE IF NOT EXISTS permitted_users ( id INTEGER PRIMARY KEY - ) STRICT; + ) STRICT; CREATE TABLE IF NOT EXISTS shopping_list ( id INTEGER PRIMARY KEY AUTOINCREMENT, item TEXT NOT NULL, added_by INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) - ) STRICT; + ) STRICT; CREATE TABLE IF NOT EXISTS recurring_tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, interval TEXT NOT NULL, last_done TEXT - ) STRICT; + ) STRICT; COMMIT; `; db.exec(ddl); @@ -79,14 +79,14 @@ function ensurePath() { } } +const begin = db.prepare("BEGIN IMMEDIATE"); +const commit = db.prepare("COMMIT"); +const rollback = db.prepare("ROLLBACK"); + /** * Wrap a write operation in a BEGIN IMMEDIATE transaction. */ function withWriteTx(fn: (db: BetterSqliteDb) => T): T { - const begin = db.prepare("BEGIN IMMEDIATE"); - const commit = db.prepare("COMMIT"); - const rollback = db.prepare("ROLLBACK"); - begin.run(); try { const result = fn(db); @@ -141,12 +141,17 @@ interface ShoppingItem { export const addShoppingItem = (item: string, addedBy: number): void => { withWriteTx((db) => { - db.prepare("INSERT INTO shopping_list (item, added_by) VALUES (?, ?)").run(item, addedBy); + db.prepare("INSERT INTO shopping_list (item, added_by) VALUES (?, ?)").run( + item, + addedBy, + ); }); }; export const getShoppingList = (): ShoppingItem[] => { - return db.prepare("SELECT id, item FROM shopping_list ORDER BY id").all() as ShoppingItem[]; + return db + .prepare("SELECT id, item FROM shopping_list ORDER BY id") + .all() as ShoppingItem[]; }; export const removeShoppingItem = (id: number): void => { @@ -166,23 +171,38 @@ export interface RecurringTask { export const addRecurringTask = (name: string, interval: string): void => { withWriteTx((db) => { - db.prepare("INSERT INTO recurring_tasks (name, interval) VALUES (?, ?)").run(name, interval); + db.prepare( + "INSERT INTO recurring_tasks (name, interval) VALUES (?, ?)", + ).run(name, interval); }); }; export const getRecurringTasks = (): RecurringTask[] => { - return db.prepare("SELECT id, name, interval, last_done FROM recurring_tasks ORDER BY id").all() as RecurringTask[]; + return db + .prepare( + "SELECT id, name, interval, last_done FROM recurring_tasks ORDER BY id", + ) + .all() as RecurringTask[]; }; -export const updateRecurringTask = (id: number, name: string, interval: string): void => { +export const updateRecurringTask = ( + id: number, + name: string, + interval: string, +): void => { withWriteTx((db) => { - db.prepare("UPDATE recurring_tasks SET name = ?, interval = ? WHERE id = ?").run(name, interval, id); + db.prepare( + "UPDATE recurring_tasks SET name = ?, interval = ? WHERE id = ?", + ).run(name, interval, id); }); }; export const markTaskDone = (id: number, date: string): void => { withWriteTx((db) => { - db.prepare("UPDATE recurring_tasks SET last_done = ? WHERE id = ?").run(date, id); + db.prepare("UPDATE recurring_tasks SET last_done = ? WHERE id = ?").run( + date, + id, + ); }); }; diff --git a/src/tasks.ts b/src/tasks.ts index b370aad..6d89ada 100644 --- a/src/tasks.ts +++ b/src/tasks.ts @@ -1,4 +1,11 @@ -import { add, parse, format, startOfWeek, addWeeks, isWithinInterval, Duration } from "date-fns"; +import { + add, + parse, + startOfWeek, + addWeeks, + isWithinInterval, + Duration, +} from "date-fns"; import { RecurringTask } from "./db"; import config from "./config"; @@ -34,6 +41,8 @@ export const isThisWeek = (task: RecurringTask): boolean => { return isWithinInterval(nextDate, { start: weekStart, end: weekEnd }); }; -export const formatDate = (d: Date): string => format(d, "yyyy-MM-dd"); +export const formatDate = (d: Date): string => + d.toLocaleDateString("en-CA", { timeZone: config.tz }); -export const today = (): string => format(now(), "yyyy-MM-dd"); +export const today = (): string => + new Date().toLocaleDateString("en-CA", { timeZone: config.tz }); diff --git a/src/tg.ts b/src/tg.ts index fd62af4..afa0477 100644 --- a/src/tg.ts +++ b/src/tg.ts @@ -1,9 +1,17 @@ import { Markup, Telegraf } from "telegraf"; import config from "./config"; import { - addActiveChat, getActiveChats, removeActiveChat, - addShoppingItem, getShoppingList, removeShoppingItem, - addRecurringTask, getRecurringTasks, updateRecurringTask, markTaskDone, removeRecurringTask, + addActiveChat, + getActiveChats, + removeActiveChat, + addShoppingItem, + getShoppingList, + removeShoppingItem, + addRecurringTask, + getRecurringTasks, + updateRecurringTask, + markTaskDone, + removeRecurringTask, } from "./db"; import { tgAuth } from "./middleware"; import { getNextDate, formatDate, parseInterval, today } from "./tasks"; @@ -25,16 +33,24 @@ bot.command("/stop", async (ctx) => { bot.command("add", async (ctx) => { const item = ctx.message.text.replace(/^\/add(@\w+)?\s*/, "").trim(); - if (!item) { await ctx.reply("Käyttö: /add "); return; } + if (!item) { + await ctx.reply("Käyttö: /add "); + return; + } addShoppingItem(item, ctx.message.from.id); await ctx.reply(`Lisätty: ${item}`); }); bot.command("list", async (ctx) => { const items = getShoppingList(); - if (items.length === 0) { await ctx.reply("Ostoslista on tyhjä."); return; } + if (items.length === 0) { + await ctx.reply("Ostoslista on tyhjä."); + return; + } const text = items.map((i, idx) => `${idx + 1}. ${i.item}`).join("\n"); - const buttons = items.map((i) => [Markup.button.callback(`❌ ${i.item}`, `del:${i.id}`)]); + const buttons = items.map((i) => [ + Markup.button.callback(`❌ ${i.item}`, `del:${i.id}`), + ]); await ctx.reply(`🛒 Ostoslista:\n${text}`, Markup.inlineKeyboard(buttons)); }); @@ -43,10 +59,18 @@ bot.action(/^del:(\d+)$/, async (ctx) => { removeShoppingItem(id); await ctx.answerCbQuery("Poistettu!"); const items = getShoppingList(); - if (items.length === 0) { await ctx.editMessageText("Ostoslista on tyhjä."); return; } + if (items.length === 0) { + await ctx.editMessageText("Ostoslista on tyhjä."); + return; + } const text = items.map((i, idx) => `${idx + 1}. ${i.item}`).join("\n"); - const buttons = items.map((i) => [Markup.button.callback(`❌ ${i.item}`, `del:${i.id}`)]); - await ctx.editMessageText(`🛒 Ostoslista:\n${text}`, Markup.inlineKeyboard(buttons)); + const buttons = items.map((i) => [ + Markup.button.callback(`❌ ${i.item}`, `del:${i.id}`), + ]); + await ctx.editMessageText( + `🛒 Ostoslista:\n${text}`, + Markup.inlineKeyboard(buttons), + ); }); // --- Recurring tasks --- @@ -55,13 +79,17 @@ bot.command("newtask", async (ctx) => { const args = ctx.message.text.replace(/^\/newtask(@\w+)?\s*/, "").trim(); const match = /^(\d+(?:pv|vk|kk|v))\s+(.+)$/.exec(args); if (!match) { - await ctx.reply("Käyttö: /newtask \nEsim: /newtask 2vk Imurointi\n\nIntervallit: pv, vk, kk, v"); + await ctx.reply( + "Käyttö: /newtask \nEsim: /newtask 2vk Imurointi\n\nIntervallit: pv, vk, kk, v", + ); return; } try { parseInterval(match[1]); } catch { - await ctx.reply("Virheellinen intervalli. Käytä: pv, vk, kk, v (esim. 2vk, 1kk)"); + await ctx.reply( + "Virheellinen intervalli. Käytä: pv, vk, kk, v (esim. 2vk, 1kk)", + ); return; } addRecurringTask(match[2], match[1]); @@ -70,11 +98,14 @@ bot.command("newtask", async (ctx) => { bot.command("tasks", async (ctx) => { const tasks = getRecurringTasks(); - if (tasks.length === 0) { await ctx.reply("Ei tehtäviä. Lisää: /newtask"); return; } + if (tasks.length === 0) { + await ctx.reply("Ei tehtäviä. Lisää: /newtask"); + return; + } const lines = tasks.map((t) => { const next = formatDate(getNextDate(t)); const done = t.last_done || "—"; - return `*${t.id}.* ${t.name}\n ↻ ${t.interval} | edellinen: ${done} | seuraava: ${next}`; + return `*${t.id}.* ${t.name}\n ↻ ${t.interval} | edellinen: ${done} | seuraava: ${next}`; }); const buttons = tasks.map((t) => [ Markup.button.callback(`✅`, `taskdone:${t.id}`), @@ -90,7 +121,9 @@ bot.command("edittask", async (ctx) => { const args = ctx.message.text.replace(/^\/edittask(@\w+)?\s*/, "").trim(); const match = /^(\d+)\s+(\d+(?:pv|vk|kk|v))\s+(.+)$/.exec(args); if (!match) { - await ctx.reply("Käyttö: /edittask \nEsim: /edittask 3 1kk Ikkunoiden pesu"); + await ctx.reply( + "Käyttö: /edittask \nEsim: /edittask 3 1kk Ikkunoiden pesu", + ); return; } try { @@ -106,7 +139,10 @@ bot.command("edittask", async (ctx) => { bot.command("done", async (ctx) => { const idStr = ctx.message.text.replace(/^\/done(@\w+)?\s*/, "").trim(); const id = Number(idStr); - if (!id) { await ctx.reply("Käyttö: /done "); return; } + if (!id) { + await ctx.reply("Käyttö: /done "); + return; + } markTaskDone(id, today()); await ctx.reply("✅ Merkitty tehdyksi!"); }); @@ -125,7 +161,13 @@ bot.action(/^taskdel:(\d+)$/, async (ctx) => { const broadcastMessage = async (msg: string): Promise => { const activeChats = await getActiveChats(); - await Promise.all(activeChats.map((c) => bot.telegram.sendMessage(c.id, msg))); + for (const chat of activeChats) { + try { + await bot.telegram.sendMessage(chat.id, msg); + } catch (err) { + console.error("[broadcastMessage] Failed to send message:", err); + } + } }; export { bot, broadcastMessage }; -- cgit v1.3