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:
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 — Показать эту справку"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user