From 7b53566dad590e7b7f44c8f0fe84433c31fbbf34 Mon Sep 17 00:00:00 2001 From: anten-ka Date: Fri, 10 Apr 2026 13:00:04 +0300 Subject: [PATCH] fix(bot): promo auto-delete + neutralize fake install/change stubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two user-reported bugs: 1. Promo spam: daily promo message from /start and the Promo menu button stayed in chat forever. Now they auto-delete after 30s. - new helper _delete_message_after() - cmd_start schedules deletion of the daily promo - cb_menu_promo sends promo as a SEPARATE ephemeral message (instead of editing the main menu in place), so the menu stays intact and only the promo self-destructs in 30s. 2. CRITICAL: 'install/change template from bot' was a stub that silently corrupted /opt/gotelegram/config.json. cb_lite_domain and cb_pro_confirm wrote a fake minimal config ({mode,template, port,installed_at}) without secret/domain/mask_host, and showed '[OK] installed!' — while never invoking install.sh, never downloading the template, never touching nginx. Proxy link generation then broke because secret was gone. User symptom: 'устанавливал другой шаблон через бота и всё повисло'. Fix: both callbacks now refuse cleanly and route the user to the CLI ('gotelegram' command → menu 1 → 1 or 7). Configuration is NOT touched. Full non-interactive install/change from the bot is left as future work. Tested live on VPS: - bot.py syntax OK (ast.parse on VPS) - gotelegram-bot restarted, active - corrupted config.json on VPS rebuilt from telemt TOML ground truth (mode=lite, secret, port, mask_host=google.com) --- gotelegram-bot/bot.py | 137 ++++++++++++++++++++++-------------------- 1 file changed, 71 insertions(+), 66 deletions(-) diff --git a/gotelegram-bot/bot.py b/gotelegram-bot/bot.py index 89f0683..1d63852 100644 --- a/gotelegram-bot/bot.py +++ b/gotelegram-bot/bot.py @@ -288,6 +288,17 @@ async def safe_edit_message(query, text: str, reply_markup=None, parse_mode=None raise # Re-raise unexpected BadRequest +async def _delete_message_after(message, delay: int = 30) -> None: + """Delete a Telegram message after `delay` seconds. Errors are swallowed + (message may already be deleted by the user). Used for ephemeral content + like promo blocks that should auto-cleanup.""" + try: + await asyncio.sleep(delay) + await message.delete() + except Exception as e: + logger.debug(f"_delete_message_after: {e}") + + async def check_service_status(service: str) -> bool: """Check if systemd service is running.""" code, _, _ = await sh("systemctl", "is-active", service) @@ -465,12 +476,13 @@ async def cmd_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: welcome, reply_markup=get_main_menu(user_id), parse_mode="HTML" ) - # Промо раз в сутки + # Промо раз в сутки — сообщение само удаляется через 30 секунд if should_show_promo_bot(): mark_promo_shown_bot() - await update.message.reply_text( - get_promo_text(), parse_mode="HTML" + promo_msg = await update.message.reply_text( + get_promo_text(), parse_mode="HTML", disable_web_page_preview=True ) + asyncio.create_task(_delete_message_after(promo_msg, 30)) async def cmd_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -796,7 +808,17 @@ async def cb_install_mode_lite(update: Update, context: ContextTypes.DEFAULT_TYP async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Lite domain selection callback.""" + """Lite domain selection callback — safe stub. + + Historic bug (v2.4.1): this callback used to overwrite + /opt/gotelegram/config.json with a minimal fake config (no secret, no + mask_host, no engine), silently corrupting the live proxy and showing a + fake "✅ Lite mode installed!" — while never actually invoking install.sh. + + Installing/changing modes non-interactively from the bot is not yet wired + up. Until it is, refuse cleanly and route the user to the CLI instead of + damaging the working configuration. + """ query = update.callback_query data = query.data try: @@ -807,36 +829,19 @@ async def cb_lite_domain(update: Update, context: ContextTypes.DEFAULT_TYPE) -> return await query.answer() - await safe_edit_message(query,f"⏳ Installing with domain: {domain}...") - # Simulate installation (in real scenario, call install script) - config = { - "mode": "lite", - "domain": domain, - "port": 443, - "installed_at": datetime.now().isoformat(), - } - - if save_json(GOTELEGRAM_CONFIG, config): - text = ( - f"✅ Lite mode installed!\n\n" - f"Domain: {domain}\n" - f"Mode: Lite\n\n" - f"Service starting... Check status in 10 seconds." - ) - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]] - ) - await safe_edit_message(query, - text, reply_markup=keyboard, parse_mode="HTML" - ) - else: - await safe_edit_message(query, - "❌ Failed to save configuration", - reply_markup=InlineKeyboardMarkup( - [[InlineKeyboardButton("« Back", callback_data="menu_install")]] - ), - ) + text = ( + "⚠️ Установка из бота пока не поддерживается\n\n" + f"Выбранный домен: {html.escape(domain)}\n\n" + "Чтобы установить или переключить режим Lite, запустите на сервере:\n" + "gotelegram\n\n" + "Затем: 1) Прокси → 1) Установить/Обновить → Lite.\n\n" + "Существующая конфигурация не была изменена." + ) + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]] + ) + await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML") async def cb_install_mode_pro(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: @@ -1106,41 +1111,39 @@ async def cb_pro_template(update: Update, context: ContextTypes.DEFAULT_TYPE) -> async def cb_pro_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Confirm and install pro template.""" + """Confirm Pro template selection — safe stub. + + Historic bug (v2.4.1): this callback used to overwrite + /opt/gotelegram/config.json with a fake {"mode":"pro","template":..., + "port":443} blob — no secret, no domain, no mask_host — silently + corrupting the live proxy so link generation and status broke. At the + same time the user saw "✅ Pro mode installed!" although install.sh was + never invoked, no template was actually downloaded, nginx was not + reconfigured and certbot was not touched. + + Non-interactive Pro install/change from the bot is not yet implemented. + Until it is, refuse cleanly and route the user to the CLI instead of + damaging the working configuration. + """ query = update.callback_query data = query.data tpl_id = data.removeprefix("pro_confirm_") await query.answer() - await safe_edit_message(query,"⏳ Installing template...") - config = { - "mode": "pro", - "template": tpl_id, - "port": 443, - "installed_at": datetime.now().isoformat(), - } - - if save_json(GOTELEGRAM_CONFIG, config): - text = ( - f"✅ Pro mode installed!\n\n" - f"Template: {html.escape(tpl_id)}\n" - f"Mode: Pro\n\n" - f"Service starting... Check status in 10 seconds." - ) - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]] - ) - await safe_edit_message(query, - text, reply_markup=keyboard, parse_mode="HTML" - ) - else: - await safe_edit_message(query, - "❌ Failed to save configuration", - reply_markup=InlineKeyboardMarkup( - [[InlineKeyboardButton("« Back", callback_data="menu_install")]] - ), - ) + text = ( + "⚠️ Установка шаблона из бота пока не поддерживается\n\n" + f"Выбранный шаблон: {html.escape(tpl_id)}\n\n" + "Чтобы установить или сменить шаблон, запустите на сервере:\n" + "gotelegram\n\n" + "Затем: 1) Прокси → 1) Установить/Обновить → Pro " + "(или 7) Сменить режим/шаблон для смены текущего).\n\n" + "Существующая конфигурация не была изменена." + ) + keyboard = InlineKeyboardMarkup( + [[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]] + ) + await safe_edit_message(query, text, reply_markup=keyboard, parse_mode="HTML") # ============================================================================ @@ -1799,14 +1802,16 @@ def mark_promo_shown_bot() -> None: async def cb_menu_promo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Promo information — always shown from menu.""" + """Promo information — shown as a separate ephemeral message that + auto-deletes after 30s so it does not clutter the chat. The main menu + message stays intact (we don't edit it in place).""" query = update.callback_query await query.answer() - keyboard = InlineKeyboardMarkup( - [[InlineKeyboardButton(_t(_uid(update), "btn_back"), callback_data="menu_main")]] + promo_msg = await query.message.reply_text( + get_promo_text(), parse_mode="HTML", disable_web_page_preview=True ) - await safe_edit_message(query, get_promo_text(), reply_markup=keyboard, parse_mode="HTML") + asyncio.create_task(_delete_message_after(promo_msg, 30)) async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: