diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..46aab2a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,16 @@ +# Agent Instructions + +## Standard Practice +- **Always use subagents** (`Task` tool) to implement code changes unless explicitly told otherwise by the user. This optimizes token usage and enables parallel work. +- Each subagent should handle a single, well-scoped task with clear acceptance criteria. +- After subagents complete, verify syntax, commit, and deploy. + +## Deployment Pattern +```bash +ssh tizona "cd /mnt/user/appdata/willhaben-tracker && docker compose down worker && git pull origin main && docker compose build worker --no-cache && docker compose up -d" +``` + +## Key Context +- Bot token: `8653489932:AAG_Ins2_z3sNHX8ZlGI4mhyzmUhWAWCZlg` (in `.env`) +- Admin TG ID: `298181113` +- PTB v21, asyncpg with Postgres 15 diff --git a/worker/src/bot.py b/worker/src/bot.py index 7c4adf3..eb0fe3c 100644 --- a/worker/src/bot.py +++ b/worker/src/bot.py @@ -180,7 +180,6 @@ def _admin_menu_keyboard() -> InlineKeyboardMarkup: [InlineKeyboardButton("➕ Add User", callback_data="admin_add"), InlineKeyboardButton("👥 List Users", callback_data="admin_list")], [InlineKeyboardButton("🗑 Remove User", callback_data="admin_remove")], - [InlineKeyboardButton("↩ Back", callback_data="menu:")], ]) @@ -384,6 +383,8 @@ async def text_input_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) context.user_data.clear() logger.info("Admin added user %d", tg_id) + await update.message.reply_text( # type: ignore[union-attr] + "⚙️ Admin Panel", parse_mode="HTML", reply_markup=_admin_menu_keyboard()) # ── admin: awaiting TG ID to remove user ────────────────────────── elif state == "admin_awaiting_tg_id_remove": @@ -428,6 +429,8 @@ async def text_input_handler(update: Update, context: ContextTypes.DEFAULT_TYPE) context.user_data.clear() logger.info("Admin removed user %d", tg_id) + await update.message.reply_text( # type: ignore[union-attr] + "⚙️ Admin Panel", parse_mode="HTML", reply_markup=_admin_menu_keyboard()) # ── callback router (handles ALL inline button clicks) ──────────────────── @@ -507,12 +510,16 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) - ) name = user["first_name"] or "there" - await context.bot.send_message( - chat_id=chat_id, - text=f"That was all your listings, {name}!\n\nAnything else I can help with?", - parse_mode="HTML", - reply_markup=_main_menu_keyboard(), - ) + summary_chat_id = update.effective_chat.id # type: ignore[union-attr] + try: + await context.bot.send_message( + chat_id=summary_chat_id, + text=f"That was all your listings, {name}!\n\nAnything else I can help with?", + parse_mode="HTML", + reply_markup=_main_menu_keyboard(), + ) + except Exception: + logger.exception("Failed to send summary message for user %s", user["id"]) elif sub == "stats": total_kw = await pool.fetchval( @@ -529,7 +536,12 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) - f"Ads indexed: {total_ads}\n" f"Notifications sent: {total_notifs}" ) - await query.edit_message_text(text=text, parse_mode="HTML") + await query.edit_message_text( + text=text, parse_mode="HTML", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("↩ Back", callback_data="back:admin")], + ]), + ) else: # Empty payload — "Back" button from sub-menus, show main menu @@ -601,7 +613,10 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) - await _edit_or_send( context.bot, chat_id, msg_id, # type: ignore[arg-type] - f'✅ Keyword added!\n\n
"{keyword_text}"
{extra}', + f'✅ Keyword added!\n\n"{keyword_text}"{extra}', + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("↩ Back", callback_data="menu:")], + ]), ) logger.info("User %s subscribed to '%s'", update.effective_user.id, keyword_text) @@ -789,13 +804,28 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) - # ADMIN FLOWS # ════════════════════════════════════════════════════════════════════ + elif action == "back": + if payload == "admin": + context.user_data.clear() + try: + await query.edit_message_text( + "⚙️ Admin Panel", parse_mode="HTML", + reply_markup=_admin_menu_keyboard()) + except Exception: + await query.answer(show_alert=True) + elif action == "admin_add": admin_row = await _require_admin(update) if not admin_row: return context.user_data["state"] = "admin_awaiting_tg_id_add" context.user_data["admin_action_msg_id"] = msg_id - await query.edit_message_text("Enter the Telegram user ID to add:", parse_mode="HTML") + await query.edit_message_text( + "Enter the Telegram user ID to add:", parse_mode="HTML", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("❌ Cancel", callback_data="back:admin")], + ]), + ) elif action == "admin_list": admin_row = await _require_admin(update) @@ -831,4 +861,9 @@ async def _handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) - return context.user_data["state"] = "admin_awaiting_tg_id_remove" context.user_data["admin_action_msg_id"] = msg_id - await query.edit_message_text("Enter the Telegram user ID to remove:", parse_mode="HTML") \ No newline at end of file + await query.edit_message_text( + "Enter the Telegram user ID to remove:", parse_mode="HTML", + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton("❌ Cancel", callback_data="back:admin")], + ]), + ) \ No newline at end of file diff --git a/worker/src/main.py b/worker/src/main.py index 7de4c60..337a9e8 100644 --- a/worker/src/main.py +++ b/worker/src/main.py @@ -79,7 +79,14 @@ async def scheduler_task(pool: object, bot: ExtBot) -> None: if initial_loaded: notify_fields = {**fields, "keyword": keyword} for tg_id in telegram_ids: - await notify_new_ad(bot, tg_id, notify_fields) + msg_id_val = await notify_new_ad(bot, tg_id, notify_fields) + if msg_id_val: + user_row = await pool.fetchrow("SELECT id FROM users WHERE telegram_id = $1", tg_id) + if user_row: + try: + await log_notification(pool, str(user_row["id"]), ad_uuid, msg_id_val) + except Exception: + logger.exception("Failed to log new ad notification") new_count += 1 else: ad_uuid = str(existing["id"])