aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2026-05-11 13:40:31 +0300
committerJan Tuomi <jan@jantuomi.fi>2026-05-11 13:40:31 +0300
commit7f92c60ed89e3495f9dd660755a68af647fd8ce2 (patch)
tree106d801001c7b60e99658b50d3f135a932cf89ef /src
parentf7e272ab8726ed6a205b46d6723cc91aa9d63be1 (diff)
Diffstat (limited to 'src')
-rw-r--r--src/bot.ts4
-rw-r--r--src/db.ts52
-rw-r--r--src/tasks.ts15
-rw-r--r--src/tg.ts74
4 files changed, 109 insertions, 36 deletions
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<void> => {
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<T>(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 <tuote>"); return; }
+ if (!item) {
+ await ctx.reply("Käyttö: /add <tuote>");
+ 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 <intervalli> <nimi>\nEsim: /newtask 2vk Imurointi\n\nIntervallit: pv, vk, kk, v");
+ await ctx.reply(
+ "Käyttö: /newtask <intervalli> <nimi>\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 <id> <intervalli> <nimi>\nEsim: /edittask 3 1kk Ikkunoiden pesu");
+ await ctx.reply(
+ "Käyttö: /edittask <id> <intervalli> <nimi>\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 <id>"); return; }
+ if (!id) {
+ await ctx.reply("Käyttö: /done <id>");
+ 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<void> => {
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 };