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.
This commit is contained in:
2026-02-17 19:50:08 +03:00
parent dd960dc5cc
commit 5cfc699c3d
7 changed files with 34 additions and 50 deletions

View File

@@ -7,7 +7,7 @@ import time
from urllib.parse import unquote from urllib.parse import unquote
# Telegram algorithm: https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app # 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( def validate_init_data(
@@ -44,7 +44,9 @@ def validate_init_data_with_reason(
if not hash_val: if not hash_val:
return (None, "no_hash") return (None, "no_hash")
data_pairs = sorted(params.items()) 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( secret_key = hmac.new(
b"WebAppData", b"WebAppData",
msg=bot_token.encode(), msg=bot_token.encode(),

View File

@@ -3,7 +3,7 @@
import hashlib import hashlib
import hmac import hmac
import json import json
from urllib.parse import quote from urllib.parse import quote, unquote
from api.telegram_auth import validate_init_data from api.telegram_auth import validate_init_data
@@ -21,7 +21,7 @@ def _make_init_data(
if auth_date is not None: if auth_date is not None:
params["auth_date"] = str(auth_date) params["auth_date"] = str(auth_date)
pairs = sorted(params.items()) 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( secret_key = hmac.new(
b"WebAppData", b"WebAppData",
msg=bot_token.encode(), msg=bot_token.encode(),

View File

@@ -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 asyncio
import config import config
from telegram import Update, WebAppInfo, InlineKeyboardButton, InlineKeyboardMarkup from telegram import Update
from telegram.ext import CommandHandler, ContextTypes from telegram.ext import CommandHandler, ContextTypes
from db.session import get_session from db.session import get_session
@@ -42,19 +42,6 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await asyncio.get_running_loop().run_in_executor(None, do_get_or_create) await asyncio.get_running_loop().run_in_executor(None, do_get_or_create)
text = "Привет! Я бот календаря дежурств. Используй /help для списка команд." 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)
@@ -62,7 +49,7 @@ async def help_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.message: if update.message:
await update.message.reply_text( await update.message.reply_text(
"Доступные команды:\n" "Доступные команды:\n"
"/start — Начать и открыть календарь\n" "/start — Начать\n"
"/help — Показать эту справку" "/help — Показать эту справку"
) )

View File

@@ -61,7 +61,8 @@ def _run_uvicorn(web_app, port: int) -> None:
def main() -> 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() app = ApplicationBuilder().token(config.BOT_TOKEN).build()
register_handlers(app) register_handlers(app)

View File

@@ -77,14 +77,6 @@
return ""; 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() { function isLocalhost() {
var h = window.location.hostname; var h = window.location.hostname;
return h === "localhost" || h === "127.0.0.1" || h === ""; return h === "localhost" || h === "127.0.0.1" || h === "";
@@ -101,8 +93,8 @@
} catch (e) { return false; } } catch (e) { return false; }
} }
function showAccessDenied() { /** @param {string} [serverDetail] - message from API 403 detail (unused; kept for callers) */
var debugStr = getInitDataDebug(); function showAccessDenied(serverDetail) {
if (headerEl) headerEl.hidden = true; if (headerEl) headerEl.hidden = true;
if (weekdaysEl) weekdaysEl.hidden = true; if (weekdaysEl) weekdaysEl.hidden = true;
calendarEl.hidden = true; calendarEl.hidden = true;
@@ -110,14 +102,6 @@
loadingEl.classList.add("hidden"); loadingEl.classList.add("hidden");
errorEl.hidden = true; errorEl.hidden = true;
accessDeniedEl.hidden = false; 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() { function hideAccessDenied() {
@@ -138,7 +122,18 @@
var timeoutId = setTimeout(function () { controller.abort(); }, FETCH_TIMEOUT_MS); var timeoutId = setTimeout(function () { controller.abort(); }, FETCH_TIMEOUT_MS);
try { try {
var res = await fetch(url, { headers: headers, signal: controller.signal }); 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("Ошибка загрузки"); if (!res.ok) throw new Error("Ошибка загрузки");
return res.json(); return res.json();
} catch (e) { } catch (e) {
@@ -281,7 +276,7 @@
renderDutyList(duties); renderDutyList(duties);
} catch (e) { } catch (e) {
if (e.message === "ACCESS_DENIED") { if (e.message === "ACCESS_DENIED") {
showAccessDenied(); showAccessDenied(e.serverDetail);
setNavEnabled(true); setNavEnabled(true);
if (window.Telegram && window.Telegram.WebApp && !window._initDataRetried) { if (window.Telegram && window.Telegram.WebApp && !window._initDataRetried) {
window._initDataRetried = true; window._initDataRetried = true;
@@ -298,10 +293,12 @@
} }
document.getElementById("prevMonth").addEventListener("click", function () { document.getElementById("prevMonth").addEventListener("click", function () {
if (!accessDeniedEl.hidden) return;
current.setMonth(current.getMonth() - 1); current.setMonth(current.getMonth() - 1);
loadMonth(); loadMonth();
}); });
document.getElementById("nextMonth").addEventListener("click", function () { document.getElementById("nextMonth").addEventListener("click", function () {
if (!accessDeniedEl.hidden) return;
current.setMonth(current.getMonth() + 1); current.setMonth(current.getMonth() + 1);
loadMonth(); loadMonth();
}); });

View File

@@ -22,8 +22,6 @@
<div class="error" id="error" hidden></div> <div class="error" id="error" hidden></div>
<div class="access-denied" id="accessDenied" hidden> <div class="access-denied" id="accessDenied" hidden>
<p>Доступ запрещён.</p> <p>Доступ запрещён.</p>
<p class="muted" id="accessDeniedHint">Откройте календарь из Telegram.</p>
<p class="debug" id="accessDeniedDebug"></p>
</div> </div>
</div> </div>
<script src="https://telegram.org/js/telegram-web-app.js"></script> <script src="https://telegram.org/js/telegram-web-app.js"></script>

View File

@@ -36,6 +36,11 @@ body {
margin-bottom: 12px; margin-bottom: 12px;
} }
.header[hidden],
.weekdays[hidden] {
display: none !important;
}
.nav { .nav {
width: 40px; width: 40px;
height: 40px; height: 40px;
@@ -166,12 +171,6 @@ body {
font-weight: 600; font-weight: 600;
} }
.access-denied .debug {
font-size: 0.75rem;
margin-top: 12px;
word-break: break-all;
}
.access-denied[hidden] { .access-denied[hidden] {
display: none !important; display: none !important;
} }