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
# 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(),

View File

@@ -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(),

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 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 — Показать эту справку"
)

View File

@@ -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)

View File

@@ -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();
});

View File

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

View File

@@ -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;
}