From 5cfc699c3d8b4a5e0b0d63e7ac613fe9d746b9d2 Mon Sep 17 00:00:00 2001 From: Nikolay Tatarinov Date: Tue, 17 Feb 2026 19:50:08 +0300 Subject: [PATCH] Refactor Telegram bot and web application for improved functionality - Disabled the default menu button in the Telegram bot, allowing users to access the app via a direct link. - Updated the initData validation process to ensure URL-decoded values are used in the data-check string. - Enhanced error handling in the web application to provide more informative access denial messages. - Removed unnecessary debug information from the access denied section in the web app. - Cleaned up the web application code by removing unused functions and improving CSS styles for hidden elements. --- api/telegram_auth.py | 6 ++++-- api/test_telegram_auth.py | 4 ++-- handlers/commands.py | 21 ++++----------------- main.py | 3 ++- webapp/app.js | 37 +++++++++++++++++-------------------- webapp/index.html | 2 -- webapp/style.css | 11 +++++------ 7 files changed, 34 insertions(+), 50 deletions(-) diff --git a/api/telegram_auth.py b/api/telegram_auth.py index 4c8e043..0910088 100644 --- a/api/telegram_auth.py +++ b/api/telegram_auth.py @@ -7,7 +7,7 @@ import time from urllib.parse import unquote # Telegram algorithm: https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app -# Data-check string must use the same key=value pairs as received (sorted by key); we preserve raw values. +# Data-check string: sorted key=value with URL-decoded values, then HMAC-SHA256(WebAppData, token) as secret. def validate_init_data( @@ -44,7 +44,9 @@ def validate_init_data_with_reason( if not hash_val: return (None, "no_hash") data_pairs = sorted(params.items()) - data_string = "\n".join(f"{k}={v}" for k, v in data_pairs) + # Data-check string: key=value with URL-decoded values (per Telegram example) + data_string = "\n".join(f"{k}={unquote(v)}" for k, v in data_pairs) + # HMAC-SHA256(key=WebAppData, message=bot_token) per reference implementations secret_key = hmac.new( b"WebAppData", msg=bot_token.encode(), diff --git a/api/test_telegram_auth.py b/api/test_telegram_auth.py index 1bd97c5..598f8d0 100644 --- a/api/test_telegram_auth.py +++ b/api/test_telegram_auth.py @@ -3,7 +3,7 @@ import hashlib import hmac import json -from urllib.parse import quote +from urllib.parse import quote, unquote from api.telegram_auth import validate_init_data @@ -21,7 +21,7 @@ def _make_init_data( if auth_date is not None: params["auth_date"] = str(auth_date) pairs = sorted(params.items()) - data_string = "\n".join(f"{k}={v}" for k, v in pairs) + data_string = "\n".join(f"{k}={unquote(v)}" for k, v in pairs) secret_key = hmac.new( b"WebAppData", msg=bot_token.encode(), diff --git a/handlers/commands.py b/handlers/commands.py index a1a4e67..be687dd 100644 --- a/handlers/commands.py +++ b/handlers/commands.py @@ -1,9 +1,9 @@ -"""Command handlers: /start, /help; /start registers user and shows Calendar button.""" +"""Command handlers: /start, /help; /start registers user.""" import asyncio import config -from telegram import Update, WebAppInfo, InlineKeyboardButton, InlineKeyboardMarkup +from telegram import Update from telegram.ext import CommandHandler, ContextTypes from db.session import get_session @@ -42,27 +42,14 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: await asyncio.get_running_loop().run_in_executor(None, do_get_or_create) text = "Привет! Я бот календаря дежурств. Используй /help для списка команд." - if config.MINI_APP_BASE_URL: - keyboard = InlineKeyboardMarkup( - [ - [ - InlineKeyboardButton( - "📅 Календарь", - web_app=WebAppInfo(url=config.MINI_APP_BASE_URL + "/app/"), - ) - ], - ] - ) - await update.message.reply_text(text, reply_markup=keyboard) - else: - await update.message.reply_text(text) + await update.message.reply_text(text) async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: if update.message: await update.message.reply_text( "Доступные команды:\n" - "/start — Начать и открыть календарь\n" + "/start — Начать\n" "/help — Показать эту справку" ) diff --git a/main.py b/main.py index f56ae0f..19e133a 100644 --- a/main.py +++ b/main.py @@ -61,7 +61,8 @@ def _run_uvicorn(web_app, port: int) -> None: def main() -> None: - _set_default_menu_button_webapp() + # Menu button (Календарь) and inline Calendar button are disabled; users open the app by link if needed. + # _set_default_menu_button_webapp() app = ApplicationBuilder().token(config.BOT_TOKEN).build() register_handlers(app) diff --git a/webapp/app.js b/webapp/app.js index 68f3f76..d34028b 100644 --- a/webapp/app.js +++ b/webapp/app.js @@ -77,14 +77,6 @@ return ""; } - function getInitDataDebug() { - var sdk = !!(window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData); - var hash = window.location.hash ? window.location.hash.slice(1) : ""; - var hashHasData = !!(hash && (getTgWebAppDataFromHash(hash) || new URLSearchParams(hash).get("tgWebAppData"))); - var queryHasData = !!(window.location.search && new URLSearchParams(window.location.search).get("tgWebAppData")); - return "SDK: " + (sdk ? "да" : "нет") + ", hash: " + (hashHasData ? hash.length + " симв." : (hash ? "есть, без tgWebAppData" : "нет")) + ", query: " + (queryHasData ? "да" : "нет"); - } - function isLocalhost() { var h = window.location.hostname; return h === "localhost" || h === "127.0.0.1" || h === ""; @@ -101,8 +93,8 @@ } catch (e) { return false; } } - function showAccessDenied() { - var debugStr = getInitDataDebug(); + /** @param {string} [serverDetail] - message from API 403 detail (unused; kept for callers) */ + function showAccessDenied(serverDetail) { if (headerEl) headerEl.hidden = true; if (weekdaysEl) weekdaysEl.hidden = true; calendarEl.hidden = true; @@ -110,14 +102,6 @@ loadingEl.classList.add("hidden"); errorEl.hidden = true; accessDeniedEl.hidden = false; - var debugEl = document.getElementById("accessDeniedDebug"); - if (debugEl) debugEl.textContent = debugStr; - var hintEl = document.getElementById("accessDeniedHint"); - if (hintEl) { - hintEl.textContent = hasTelegramHashButNoInitData() - ? "Откройте календарь через кнопку меню бота (⋮ или «Календарь»), а не через «Открыть в браузере» или прямую ссылку." - : "Откройте календарь из Telegram."; - } } function hideAccessDenied() { @@ -138,7 +122,18 @@ var timeoutId = setTimeout(function () { controller.abort(); }, FETCH_TIMEOUT_MS); try { var res = await fetch(url, { headers: headers, signal: controller.signal }); - if (res.status === 403) throw new Error("ACCESS_DENIED"); + if (res.status === 403) { + var detail = "Доступ запрещён"; + try { + var body = await res.json(); + if (body && body.detail !== undefined) { + detail = typeof body.detail === "string" ? body.detail : (body.detail.msg || JSON.stringify(body.detail)); + } + } catch (parseErr) { /* ignore */ } + var err = new Error("ACCESS_DENIED"); + err.serverDetail = detail; + throw err; + } if (!res.ok) throw new Error("Ошибка загрузки"); return res.json(); } catch (e) { @@ -281,7 +276,7 @@ renderDutyList(duties); } catch (e) { if (e.message === "ACCESS_DENIED") { - showAccessDenied(); + showAccessDenied(e.serverDetail); setNavEnabled(true); if (window.Telegram && window.Telegram.WebApp && !window._initDataRetried) { window._initDataRetried = true; @@ -298,10 +293,12 @@ } document.getElementById("prevMonth").addEventListener("click", function () { + if (!accessDeniedEl.hidden) return; current.setMonth(current.getMonth() - 1); loadMonth(); }); document.getElementById("nextMonth").addEventListener("click", function () { + if (!accessDeniedEl.hidden) return; current.setMonth(current.getMonth() + 1); loadMonth(); }); diff --git a/webapp/index.html b/webapp/index.html index ec8141a..801e7ee 100644 --- a/webapp/index.html +++ b/webapp/index.html @@ -22,8 +22,6 @@ diff --git a/webapp/style.css b/webapp/style.css index de429af..7c25900 100644 --- a/webapp/style.css +++ b/webapp/style.css @@ -36,6 +36,11 @@ body { margin-bottom: 12px; } +.header[hidden], +.weekdays[hidden] { + display: none !important; +} + .nav { width: 40px; height: 40px; @@ -166,12 +171,6 @@ body { font-weight: 600; } -.access-denied .debug { - font-size: 0.75rem; - margin-top: 12px; - word-break: break-all; -} - .access-denied[hidden] { display: none !important; }