diff --git a/supabase/migrations/03-global-keywords.sql b/supabase/migrations/03-global-keywords.sql index f1f31ea..d885b00 100644 --- a/supabase/migrations/03-global-keywords.sql +++ b/supabase/migrations/03-global-keywords.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS keywords ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), keyword text NOT NULL, - interval_minutes int NOT NULL DEFAULT 60, + interval_minutes int NOT NULL DEFAULT 5, is_active boolean NOT NULL DEFAULT true, last_scraped_at timestamptz, initial_loaded boolean NOT NULL DEFAULT false, diff --git a/worker/src/bot.py b/worker/src/bot.py index f7f8046..553a43e 100644 --- a/worker/src/bot.py +++ b/worker/src/bot.py @@ -79,6 +79,7 @@ def register_handlers(app: Application) -> None: app.add_handler(CommandHandler("add", add_handler)) app.add_handler(CommandHandler("list", list_handler)) app.add_handler(CommandHandler("delete", delete_handler)) + app.add_handler(CommandHandler("interval", interval_handler)) app.add_handler(CommandHandler("stats", stats_handler)) app.add_handler(CommandHandler("adduser", adduser_handler)) app.add_handler(CommandHandler("removeuser", removeuser_handler)) @@ -99,6 +100,7 @@ async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N "/add — Subscribe to a keyword (shared across users)\n" "/list — List your subscriptions\n" "/delete — Unsubscribe from a keyword\n" + "/interval — Change scrape interval\n" "/stats — View your tracking statistics", ) @@ -127,7 +129,7 @@ async def add_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non if not existing: kw_id = str(uuid.uuid4()) await pool.execute( - "INSERT INTO keywords (id, keyword, interval_minutes, is_active) VALUES ($1, $2, 60, true)", + "INSERT INTO keywords (id, keyword, interval_minutes, is_active) VALUES ($1, $2, 5, true)", kw_id, keyword, ) @@ -171,7 +173,7 @@ async def list_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No pool = await get_pool() subscriptions = await pool.fetch( - "SELECT kw.id, kw.keyword, kw.is_active, kw.last_scraped_at, COUNT(ks2.user_id) AS subs " + "SELECT kw.id, kw.keyword, kw.is_active, kw.interval_minutes, kw.last_scraped_at, COUNT(ks2.user_id) AS subs " "FROM keyword_subscriptions ks " "JOIN keywords kw ON kw.id = ks.keyword_id " "LEFT JOIN keyword_subscriptions ks2 ON ks2.keyword_id = ks.keyword_id " @@ -194,12 +196,87 @@ async def list_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No f"{i}. \"{s['keyword']}\"\n" f" ID: {s['id']}\n" f" Status: {status}{subs}\n" + f" Interval: {s['interval_minutes']} min\n" f" Last scraped: {last}" ) await update.message.reply_text("\n\n".join(lines)) # type: ignore[union-attr] +# -- /interval ------------------------------------------------------------ + +async def interval_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + row = await _require_user(update) + if not row: + return + + text = (update.message or update.callback_query).text or "" # type: ignore[union-attr] + parts = text.split() + if len(parts) < 3: + await update.message.reply_text("Usage: /interval ") # type: ignore[union-attr] + return + + keyword_id = parts[1].strip() + try: + uuid.UUID(keyword_id) + except ValueError: + await update.message.reply_text("Invalid keyword ID format.") # type: ignore[union-attr] + return + + try: + minutes = int(parts[2]) + except ValueError: + await update.message.reply_text("Interval must be a number (minutes).") # type: ignore[union-attr] + return + + if minutes < 1 or minutes > 1440: + await update.message.reply_text("Interval must be between 1 and 1440 minutes.") # type: ignore[union-attr] + return + + pool = await get_pool() + + sub_check = await pool.fetchrow( + "SELECT 1 FROM keyword_subscriptions WHERE keyword_id = $1 AND user_id = $2", + keyword_id, + row["id"], + ) + if not sub_check: + await update.message.reply_text("You are not subscribed to this keyword.") # type: ignore[union-attr] + return + + kw_row = await pool.fetchrow( + "SELECT id, interval_minutes FROM keywords WHERE id = $1", + keyword_id, + ) + if not kw_row: + await update.message.reply_text("Keyword not found.") # type: ignore[union-attr] + return + + old_interval = kw_row["interval_minutes"] + await pool.execute( + "UPDATE keywords SET interval_minutes = $1 WHERE id = $2", + minutes, + keyword_id, + ) + + sub_count = await pool.fetchval( + "SELECT COUNT(*) FROM keyword_subscriptions WHERE keyword_id = $1", + keyword_id, + ) + + if sub_count > 1: + await update.message.reply_text( # type: ignore[union-attr] + f"Interval updated: {old_interval} min → {minutes} min\n" + f"(affects all {sub_count} subscriber(s))", + ) + else: + await update.message.reply_text( # type: ignore[union-attr] + f"Interval updated: {old_interval} min → {minutes} min", + ) + + logger.info("User %s changed interval for keyword '%s' to %d min", update.effective_user.id, keyword_id, minutes) + + # -- /delete -------------------------------------------------------------- async def delete_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: diff --git a/worker/src/main.py b/worker/src/main.py index 3f4d88b..f040201 100644 --- a/worker/src/main.py +++ b/worker/src/main.py @@ -96,7 +96,7 @@ async def scheduler_task(pool: object, bot: ExtBot) -> None: fields.get("main_image_url"), fields.get("postcode"), ad_uuid, ) - if not initial_loaded: + if initial_loaded: notify_fields = {**fields, "keyword": keyword} for tg_id in telegram_ids: await notify_new_ad(bot, tg_id, notify_fields)