aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2026-05-06 21:46:21 +0300
committerJan Tuomi <jan@jantuomi.fi>2026-05-06 21:46:21 +0300
commit713ced9c68cd70a8e56e8ce5ed27cb9bbe3e55c4 (patch)
tree29f9997386b9f46db9f263ef63a5e01ca19db393 /src
parentcba8c3d49b53702a488eaff16109e29e49d94b6d (diff)
Add shopping list features, rework
Diffstat (limited to 'src')
-rw-r--r--src/bot.ts49
-rw-r--r--src/config.ts30
-rw-r--r--src/db.ts74
-rw-r--r--src/index.ts51
-rw-r--r--src/middleware.ts10
-rw-r--r--src/sheets.ts203
-rw-r--r--src/tasks.ts39
-rw-r--r--src/tg.ts119
8 files changed, 276 insertions, 299 deletions
diff --git a/src/bot.ts b/src/bot.ts
new file mode 100644
index 0000000..1de3a22
--- /dev/null
+++ b/src/bot.ts
@@ -0,0 +1,49 @@
+import { bot, broadcastMessage } from "./tg";
+import { getRecurringTasks, markTaskDone } from "./db";
+import { isThisWeek, today, now } from "./tasks";
+import { isMonday, getHours, format } from "date-fns";
+
+const CHECK_INTERVAL_MS = 60 * 1000;
+let lastRunKey: string | null = null;
+
+const runWeeklyTasks = async (): Promise<void> => {
+ try {
+ console.log("[weekly] Starting recurring tasks check");
+ const tasks = getRecurringTasks();
+ const dueTasks = tasks.filter(isThisWeek);
+
+ if (dueTasks.length > 0) {
+ 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) {
+ markTaskDone(t.id, todayStr);
+ }
+ } else {
+ await broadcastMessage("Tällä viikolla ei toistuvia hommia! 🎉");
+ }
+ console.log("[weekly] Done");
+ } catch (err) {
+ console.error("[weekly] Error:", err);
+ }
+};
+
+const checkAndRun = async (): Promise<void> => {
+ const current = now();
+ if (isMonday(current) && getHours(current) === 9) {
+ const key = format(current, "yyyy-MM-dd");
+ if (key !== lastRunKey) {
+ lastRunKey = key;
+ await runWeeklyTasks();
+ }
+ }
+};
+
+setInterval(checkAndRun, CHECK_INTERVAL_MS);
+console.log("[bot] Scheduled weekly tasks for Monday 09:00");
+
+bot.launch();
+console.log("[bot] Bot is running");
+
+process.once("SIGINT", () => bot.stop("SIGINT"));
+process.once("SIGTERM", () => bot.stop("SIGTERM"));
diff --git a/src/config.ts b/src/config.ts
index d8e3ca0..0384656 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,5 +1,4 @@
import dotenv from "dotenv";
-import fs from "fs";
import path from "path";
if (!process.env.SKIP_DOTENV) {
@@ -13,45 +12,20 @@ class ConfigError extends Error {
}
}
-const nodeEnv = process.env.NODE_ENV || "development";
-
const tgBotToken = process.env.TELEGRAM_BOT_TOKEN || null;
if (!tgBotToken) {
throw new ConfigError("Missing env var TELEGRAM_BOT_TOKEN");
}
-const sheetsSpreadsheetId = process.env.SHEETS_SPREADSHEET_ID || null;
-if (!sheetsSpreadsheetId) {
- throw new ConfigError("Missing env var SHEETS_SPREADSHEET_ID");
-}
-
-const sheetsRange = process.env.SHEETS_RANGE || null;
-if (!sheetsRange) {
- throw new ConfigError("Missing env var SHEETS_RANGE");
-}
+const tz = process.env.TZ || "Europe/Helsinki";
const sqlitePath =
process.env.SQLITE_PATH || path.join(process.cwd(), "data", "hommabot.db");
-const googleSaJsonB64 = process.env.G_SA_JSON_B64 || null;
-if (!googleSaJsonB64) {
- throw new ConfigError("Missing env var G_SA_JSON_B64");
-}
-
-const googleSaJsonBuff = Buffer.from(googleSaJsonB64, "base64");
-const googleSaJsonUtf8 = googleSaJsonBuff.toString("utf-8");
-const googleSaJson: Record<string, string> = JSON.parse(googleSaJsonUtf8);
-const googleSaJsonPath = path.join("sa_key.json");
-fs.writeFileSync(googleSaJsonPath, googleSaJsonUtf8);
-
const config = {
- nodeEnv,
tgBotToken,
- sheetsSpreadsheetId,
- sheetsRange,
+ tz,
sqlitePath,
- googleSaJson,
- googleSaJsonPath,
};
export default config;
diff --git a/src/db.ts b/src/db.ts
index 511b519..a2c5cee 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -51,6 +51,20 @@ const ddl = `
CREATE TABLE IF NOT EXISTS permitted_users (
id INTEGER PRIMARY KEY
) 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;
+
+ CREATE TABLE IF NOT EXISTS recurring_tasks (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ interval TEXT NOT NULL,
+ last_done TEXT
+ ) STRICT;
COMMIT;
`;
db.exec(ddl);
@@ -117,3 +131,63 @@ export const getPermittedUsers = async (): Promise<
const stmt = db.prepare("SELECT id FROM permitted_users");
return stmt.all() as PermittedUser[];
};
+
+/* Shopping list */
+
+interface ShoppingItem {
+ id: number;
+ item: string;
+}
+
+export const addShoppingItem = (item: string, addedBy: number): void => {
+ withWriteTx((db) => {
+ 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[];
+};
+
+export const removeShoppingItem = (id: number): void => {
+ withWriteTx((db) => {
+ db.prepare("DELETE FROM shopping_list WHERE id = ?").run(id);
+ });
+};
+
+/* Recurring tasks */
+
+export interface RecurringTask {
+ id: number;
+ name: string;
+ interval: string;
+ last_done: string | null;
+}
+
+export const addRecurringTask = (name: string, interval: string): void => {
+ withWriteTx((db) => {
+ 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[];
+};
+
+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);
+ });
+};
+
+export const markTaskDone = (id: number, date: string): void => {
+ withWriteTx((db) => {
+ db.prepare("UPDATE recurring_tasks SET last_done = ? WHERE id = ?").run(date, id);
+ });
+};
+
+export const removeRecurringTask = (id: number): void => {
+ withWriteTx((db) => {
+ db.prepare("DELETE FROM recurring_tasks WHERE id = ?").run(id);
+ });
+};
diff --git a/src/index.ts b/src/index.ts
deleted file mode 100644
index c6d92eb..0000000
--- a/src/index.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { buildSheetsClient } from "./sheets";
-import { broadcastMessage } from "./tg";
-
-const main = async (): Promise<void> => {
- const startTime = Date.now();
- console.log("[run] Starting recurring tasks notification script");
-
- const sheets = await buildSheetsClient();
-
- // 1. Fetch raw sheet data
- const sheetData = await sheets.fetchSheetData();
- console.log(`[run] Retrieved ${sheetData.length} raw rows from Sheets`);
-
- // 2. Process rows (compute next/last dates)
- const entries = sheets.processRows(sheetData);
- console.log(`[run] Processed ${entries.length} entries`);
-
- // 3. Filter only tasks due this week
- const thisWeeksEntries = sheets.filterOnlyThisWeek(entries);
- console.log(`[run] Found ${thisWeeksEntries.length} entries due this week`);
-
- // 4. Compose & broadcast Telegram message
- if (thisWeeksEntries.length > 0) {
- const tasks = thisWeeksEntries.map((e) => `${e.name} (${e.interval})`);
- const taskText = tasks.join("\n");
- const msg = `Tällä viikolla tehtävät hommat:\n${taskText}`;
- await broadcastMessage(msg);
- console.log("[run] Broadcasted weekly task message");
- } else {
- const msg = "Tällä viikolla ei toistuvia hommia! 🎉";
- await broadcastMessage(msg);
- console.log("[run] Broadcasted empty-week message");
- }
-
- // 5. Update "last done" column with new date stamps (only tasks done this week are advanced)
- const newDateStamps = sheets.updatedLastDoneDateStamps(entries);
- await sheets.updateSheetLastDoneColumn(newDateStamps);
- console.log("[run] Updated last done column in sheet");
-
- const durationMs = Date.now() - startTime;
- console.log(`[run] Completed successfully in ${durationMs}ms`);
-};
-
-main()
- .then(() => {
- process.exit(0);
- })
- .catch((err) => {
- console.error("[run] Fatal error:", err);
- process.exit(1);
- });
diff --git a/src/middleware.ts b/src/middleware.ts
index 589022f..6c56352 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,11 +1,5 @@
/**
* Telegram-only middleware utilities.
- *
- * HTTP server functionality has been removed, so any HTTP-specific middleware
- * (e.g. header-based auth) has been deleted.
- *
- * This module currently exposes:
- * tgAuth - Ensures that only permitted Telegram user IDs can invoke bot commands.
*/
import { Context } from "telegraf";
@@ -25,7 +19,7 @@ export const tgAuth = async (
ctx: Context,
next: () => Promise<void>,
): Promise<void> => {
- const userId = ctx.message?.from.id;
+ const userId = ctx.from?.id;
if (!userId) {
await ctx.reply("Käyttäjätunnusta ei voitu lukea (user id puuttuu).");
return;
@@ -43,6 +37,6 @@ export const tgAuth = async (
await next();
} catch (err) {
console.error("[tgAuth] Authorization check failed:", err);
- await ctx.reply("Odottamaton virhe valtuutuksessa.");
+ await ctx.reply("Odottamaton virhe luvituksessa.");
}
};
diff --git a/src/sheets.ts b/src/sheets.ts
deleted file mode 100644
index 3578198..0000000
--- a/src/sheets.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { google } from "googleapis";
-import {
- add,
- set,
- Duration,
- parse,
- format,
- getDay,
- subDays,
- addDays,
-} from "date-fns";
-
-import config from "./config";
-
-interface SheetsRawEntry {
- intervalStr: string;
- name: string;
- lastDoneDateStr: string;
-}
-
-interface SheetsProcessedEntry {
- name: string;
- nextDateStamp: string;
- nextDate: Date;
- lastDateStamp: string;
- lastDate?: Date;
- interval: string;
-}
-
-const INTERVAL_ABBREVS = {
- pv: 1,
- vk: 7,
- kk: 30,
- v: 365,
-} as const;
-
-type IntervalAbbrevKey = keyof typeof INTERVAL_ABBREVS;
-
-const strToDate = (s: string) => parse(s, "yyyy-MM-dd", new Date());
-const dateToStr = (d: Date) => format(d, "yyyy-MM-dd");
-
-const calculateDuration = (intervalStr: string) => {
- const match = /(\d+)(\w+)/.exec(intervalStr);
-
- if (!match) {
- throw new Error("Given interval string does not match regular expression");
- }
-
- const number = Number.parseInt(match[1]);
- const abbrev = match[2];
-
- if (number <= 0) {
- throw new Error(`"${number}" is an invalid interval number`);
- }
-
- if (!Object.keys(INTERVAL_ABBREVS).includes(abbrev)) {
- throw new Error(`"${abbrev}" is an invalid interval string`);
- }
-
- const days = INTERVAL_ABBREVS[abbrev as IntervalAbbrevKey];
-
- const totalDuration: Duration = {
- days: number * days,
- };
-
- return totalDuration;
-};
-
-const processRow = (row: SheetsRawEntry) => {
- const duration = calculateDuration(row.intervalStr);
-
- const calcValues = () => {
- if (row.lastDoneDateStr) {
- const lastDate = strToDate(row.lastDoneDateStr);
- const nextDate = add(lastDate, duration);
- const nextDateStamp = dateToStr(nextDate);
-
- return { lastDate, nextDate, nextDateStamp };
- } else {
- const lastDate = undefined;
- const nextDate = new Date();
- const nextDateStamp = dateToStr(nextDate);
-
- return { lastDate, nextDate, nextDateStamp };
- }
- };
-
- const processedEntry: SheetsProcessedEntry = {
- ...calcValues(),
- name: row.name,
- lastDateStamp: row.lastDoneDateStr,
- interval: row.intervalStr,
- };
-
- return processedEntry;
-};
-
-const isThisWeek = (entry: SheetsProcessedEntry): boolean => {
- const nextDate = entry.nextDate;
- const currentDateTime = new Date();
- const today = set(currentDateTime, {
- hours: 0,
- minutes: 0,
- seconds: 0,
- milliseconds: 0,
- });
- const dayOfWeek = (getDay(today) + 7 - 1) % 7; // getDay returns sunday = 0
- const currentWeekStart = subDays(today, dayOfWeek);
- const currentWeekEnd = addDays(currentWeekStart, 7);
-
- return nextDate >= currentWeekStart && nextDate < currentWeekEnd;
-};
-
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export const buildSheetsClient = async () => {
- const auth = new google.auth.GoogleAuth({
- // Scopes can be specified either as an array or as a single, space-delimited string.
- scopes: [
- "https://www.googleapis.com/auth/spreadsheets",
- "https://www.googleapis.com/auth/spreadsheets.readonly",
- ],
- keyFile: config.googleSaJsonPath,
- });
-
- google.options({ auth });
- const sheets = google.sheets({ version: "v4" });
-
- const fetchSheetData = async (): Promise<SheetsRawEntry[]> => {
- try {
- const res = await sheets.spreadsheets.values.get({
- spreadsheetId: config.sheetsSpreadsheetId,
- range: config.sheetsRange,
- });
-
- const entriesAsLists = res.data.values || [];
- const rawEntries: SheetsRawEntry[] = entriesAsLists.map((row) => ({
- intervalStr: row[0],
- name: row[1],
- lastDoneDateStr: row[2],
- }));
-
- console.log(`Fetched ${rawEntries.length} rows from Sheets.`);
- return rawEntries;
- } catch (err) {
- console.error(err);
- throw err;
- }
- };
-
- const getRightmostColumnInRange = (range: string): string => {
- const match = /(.+!)?([A-Z]+?)(\d+):([A-Z]+?)(\d+)/.exec(range);
-
- if (!match) {
- throw new Error("Given range does not match regular expression");
- }
-
- const sheetName = match[1];
- const topLeftRow = Number.parseInt(match[3]);
- const bottomRightCol = match[4];
- const bottomRightRow = Number.parseInt(match[5]);
-
- const rmCol = bottomRightCol;
- const rmRowStart = topLeftRow;
- const rmRowEnd = bottomRightRow;
-
- return `${sheetName}${rmCol}${rmRowStart}:${rmCol}${rmRowEnd}`;
- };
-
- const updateSheetLastDoneColumn = async (newDateStamps: string[]) => {
- const lastDoneColumnRange = getRightmostColumnInRange(config.sheetsRange);
-
- const body = {
- values: newDateStamps.map((nds) => [nds]),
- };
-
- await sheets.spreadsheets.values.update({
- spreadsheetId: config.sheetsSpreadsheetId,
- range: lastDoneColumnRange,
- valueInputOption: "RAW",
- requestBody: body,
- });
- };
-
- const processRows = (rows: SheetsRawEntry[]) => rows.map(processRow);
-
- const filterOnlyThisWeek = (
- entries: SheetsProcessedEntry[],
- ): SheetsProcessedEntry[] => entries.filter(isThisWeek);
-
- const updatedLastDoneDateStamps = (
- entries: SheetsProcessedEntry[],
- ): string[] =>
- entries.map((e) => (isThisWeek(e) ? e.nextDateStamp : e.lastDateStamp));
-
- return {
- fetchSheetData,
- getRightmostColumnInRange,
- updateSheetLastDoneColumn,
- processRows,
- filterOnlyThisWeek,
- updatedLastDoneDateStamps,
- };
-};
diff --git a/src/tasks.ts b/src/tasks.ts
new file mode 100644
index 0000000..b370aad
--- /dev/null
+++ b/src/tasks.ts
@@ -0,0 +1,39 @@
+import { add, parse, format, startOfWeek, addWeeks, isWithinInterval, Duration } from "date-fns";
+import { RecurringTask } from "./db";
+import config from "./config";
+
+const INTERVAL_ABBREVS: Record<string, number> = {
+ pv: 1,
+ vk: 7,
+ kk: 30,
+ v: 365,
+};
+
+export const now = (): Date => {
+ return new Date(new Date().toLocaleString("en-US", { timeZone: config.tz }));
+};
+
+export const parseInterval = (intervalStr: string): Duration => {
+ const match = /^(\d+)(pv|vk|kk|v)$/.exec(intervalStr);
+ if (!match) throw new Error(`Virheellinen intervalli: "${intervalStr}"`);
+ const num = parseInt(match[1]);
+ const days = num * INTERVAL_ABBREVS[match[2]];
+ return { days };
+};
+
+export const getNextDate = (task: RecurringTask): Date => {
+ if (!task.last_done) return now();
+ const lastDate = parse(task.last_done, "yyyy-MM-dd", now());
+ return add(lastDate, parseInterval(task.interval));
+};
+
+export const isThisWeek = (task: RecurringTask): boolean => {
+ const nextDate = getNextDate(task);
+ const weekStart = startOfWeek(now(), { weekStartsOn: 1 });
+ const weekEnd = addWeeks(weekStart, 1);
+ return isWithinInterval(nextDate, { start: weekStart, end: weekEnd });
+};
+
+export const formatDate = (d: Date): string => format(d, "yyyy-MM-dd");
+
+export const today = (): string => format(now(), "yyyy-MM-dd");
diff --git a/src/tg.ts b/src/tg.ts
index 4850c88..2e5e49d 100644
--- a/src/tg.ts
+++ b/src/tg.ts
@@ -1,11 +1,14 @@
-import { Telegraf } from "telegraf";
+import { Markup, Telegraf } from "telegraf";
import config from "./config";
-import { addActiveChat, getActiveChats, removeActiveChat } from "./db";
+import {
+ addActiveChat, getActiveChats, removeActiveChat,
+ addShoppingItem, getShoppingList, removeShoppingItem,
+ addRecurringTask, getRecurringTasks, updateRecurringTask, markTaskDone, removeRecurringTask,
+} from "./db";
import { tgAuth } from "./middleware";
+import { getNextDate, formatDate, parseInterval, today } from "./tasks";
-console.log(`Setting up Telegram bot with token ${config.tgBotToken}`);
const bot = new Telegraf(config.tgBotToken);
-
bot.use(tgAuth);
bot.command("/start", async (ctx) => {
@@ -18,13 +21,111 @@ bot.command("/stop", async (ctx) => {
await ctx.reply("HommaBot pysäytetty!");
});
+// --- Shopping list ---
+
+bot.command("add", async (ctx) => {
+ const item = ctx.message.text.replace(/^\/add\s*/, "").trim();
+ 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; }
+ 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.reply(`🛒 Ostoslista:\n${text}`, Markup.inlineKeyboard(buttons));
+});
+
+bot.action(/^del:(\d+)$/, async (ctx) => {
+ const id = Number(ctx.match[1]);
+ removeShoppingItem(id);
+ await ctx.answerCbQuery("Poistettu!");
+ const items = getShoppingList();
+ 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));
+});
+
+// --- Recurring tasks ---
+
+bot.command("newtask", async (ctx) => {
+ const args = ctx.message.text.replace(/^\/newtask\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");
+ return;
+ }
+ try {
+ parseInterval(match[1]);
+ } catch {
+ await ctx.reply("Virheellinen intervalli. Käytä: pv, vk, kk, v (esim. 2vk, 1kk)");
+ return;
+ }
+ addRecurringTask(match[2], match[1]);
+ await ctx.reply(`✅ Uusi tehtävä: ${match[2]} (joka ${match[1]})`);
+});
+
+bot.command("tasks", async (ctx) => {
+ const tasks = getRecurringTasks();
+ 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}`;
+ });
+ const buttons = tasks.map((t) => [
+ Markup.button.callback(`✅ ${t.name}`, `taskdone:${t.id}`),
+ Markup.button.callback(`🗑️`, `taskdel:${t.id}`),
+ ]);
+ await ctx.reply(`📋 Toistuvat tehtävät:\n\n${lines.join("\n\n")}`, {
+ parse_mode: "Markdown",
+ ...Markup.inlineKeyboard(buttons),
+ });
+});
+
+bot.command("edittask", async (ctx) => {
+ const args = ctx.message.text.replace(/^\/edittask\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");
+ return;
+ }
+ try {
+ parseInterval(match[2]);
+ } catch {
+ await ctx.reply("Virheellinen intervalli.");
+ return;
+ }
+ updateRecurringTask(Number(match[1]), match[3], match[2]);
+ await ctx.reply(`✏️ Päivitetty: ${match[3]} (${match[2]})`);
+});
+
+bot.command("done", async (ctx) => {
+ const idStr = ctx.message.text.replace(/^\/done\s*/, "").trim();
+ const id = Number(idStr);
+ if (!id) { await ctx.reply("Käyttö: /done <id>"); return; }
+ markTaskDone(id, today());
+ await ctx.reply("✅ Merkitty tehdyksi!");
+});
+
+bot.action(/^taskdone:(\d+)$/, async (ctx) => {
+ const id = Number(ctx.match[1]);
+ markTaskDone(id, today());
+ await ctx.answerCbQuery("Merkitty tehdyksi!");
+});
+
+bot.action(/^taskdel:(\d+)$/, async (ctx) => {
+ const id = Number(ctx.match[1]);
+ removeRecurringTask(id);
+ await ctx.answerCbQuery("Poistettu!");
+});
+
const broadcastMessage = async (msg: string): Promise<void> => {
const activeChats = await getActiveChats();
- const chatIds = activeChats.map(c => c.id);
-
- await Promise.all(chatIds.map(async chatId =>
- bot.telegram.sendMessage(chatId, msg),
- ));
+ await Promise.all(activeChats.map((c) => bot.telegram.sendMessage(c.id, msg)));
};
export { bot, broadcastMessage };