Enhance Telegram bot functionality and improve error handling

- Introduced a new function to set the default menu button for the Telegram bot's Web App.
- Updated the initData validation process to provide detailed error messages for authorization failures.
- Refactored the validate_init_data function to return both username and reason for validation failure.
- Enhanced the web application to handle access denial more gracefully, providing users with hints on how to access the calendar.
- Improved the README with additional instructions for configuring the bot's menu button and Web App URL.
- Updated tests to reflect changes in the validation process and error handling.
This commit is contained in:
2026-02-17 19:08:14 +03:00
parent 1948618394
commit dd960dc5cc
11 changed files with 171 additions and 59 deletions

View File

@@ -1,4 +1,8 @@
(function () {
const FETCH_TIMEOUT_MS = 15000;
const RETRY_DELAY_MS = 800;
const RETRY_AFTER_ACCESS_DENIED_MS = 1200;
const MONTHS = [
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь",
"Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"
@@ -34,16 +38,33 @@
return new Date(d.getFullYear(), d.getMonth(), diff);
}
/** Get tgWebAppData value from hash when it contains unencoded & and = (URLSearchParams would split it). Value runs from tgWebAppData= until next &tgWebApp or end. */
function getTgWebAppDataFromHash(hash) {
var idx = hash.indexOf("tgWebAppData=");
if (idx === -1) return "";
var start = idx + "tgWebAppData=".length;
var end = hash.indexOf("&tgWebApp", start);
if (end === -1) end = hash.length;
var raw = hash.substring(start, end);
try {
return decodeURIComponent(raw);
} catch (e) {
return raw;
}
}
function getInitData() {
var fromSdk = (window.Telegram && window.Telegram.WebApp && window.Telegram.WebApp.initData) || "";
if (fromSdk) return fromSdk;
var hash = window.location.hash ? window.location.hash.slice(1) : "";
if (hash.indexOf("tgWebAppData=") === 0) {
if (hash) {
var fromHash = getTgWebAppDataFromHash(hash);
if (fromHash) return fromHash;
try {
return decodeURIComponent(hash.substring("tgWebAppData=".length));
} catch (e) {
return hash.substring("tgWebAppData=".length);
}
var hashParams = new URLSearchParams(hash);
var tgFromHash = hashParams.get("tgWebAppData");
if (tgFromHash) return decodeURIComponent(tgFromHash);
} catch (e) { /* ignore */ }
}
var q = window.location.search ? new URLSearchParams(window.location.search).get("tgWebAppData") : null;
if (q) {
@@ -59,9 +80,9 @@
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.indexOf("tgWebAppData=") === 0;
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 + " симв." : "нет") + ", query: " + (queryHasData ? "да" : "нет");
return "SDK: " + (sdk ? "да" : "нет") + ", hash: " + (hashHasData ? hash.length + " симв." : (hash ? "есть, без tgWebAppData" : "нет")) + ", query: " + (queryHasData ? "да" : "нет");
}
function isLocalhost() {
@@ -69,7 +90,19 @@
return h === "localhost" || h === "127.0.0.1" || h === "";
}
function hasTelegramHashButNoInitData() {
var hash = window.location.hash ? window.location.hash.slice(1) : "";
if (!hash) return false;
try {
var keys = Array.from(new URLSearchParams(hash).keys());
var hasVersion = keys.indexOf("tgWebAppVersion") !== -1;
var hasData = keys.indexOf("tgWebAppData") !== -1 || getTgWebAppDataFromHash(hash);
return hasVersion && !hasData;
} catch (e) { return false; }
}
function showAccessDenied() {
var debugStr = getInitDataDebug();
if (headerEl) headerEl.hidden = true;
if (weekdaysEl) weekdaysEl.hidden = true;
calendarEl.hidden = true;
@@ -78,7 +111,13 @@
errorEl.hidden = true;
accessDeniedEl.hidden = false;
var debugEl = document.getElementById("accessDeniedDebug");
if (debugEl) debugEl.textContent = getInitDataDebug();
if (debugEl) debugEl.textContent = debugStr;
var hintEl = document.getElementById("accessDeniedHint");
if (hintEl) {
hintEl.textContent = hasTelegramHashButNoInitData()
? "Откройте календарь через кнопку меню бота (⋮ или «Календарь»), а не через «Открыть в браузере» или прямую ссылку."
: "Откройте календарь из Telegram.";
}
}
function hideAccessDenied() {
@@ -96,21 +135,19 @@
const headers = {};
if (initData) headers["X-Telegram-Init-Data"] = initData;
var controller = new AbortController();
var timeoutId = setTimeout(function () { controller.abort(); }, 15000);
var timeoutId = setTimeout(function () { controller.abort(); }, FETCH_TIMEOUT_MS);
try {
var res = await fetch(url, { headers: headers, signal: controller.signal });
clearTimeout(timeoutId);
if (res.status === 403) {
throw new Error("ACCESS_DENIED");
}
if (res.status === 403) throw new Error("ACCESS_DENIED");
if (!res.ok) throw new Error("Ошибка загрузки");
return res.json();
} catch (e) {
clearTimeout(timeoutId);
if (e.name === "AbortError") {
throw new Error("Не удалось загрузить данные. Проверьте интернет.");
}
throw e;
} finally {
clearTimeout(timeoutId);
}
}
@@ -206,6 +243,25 @@
}
}
/** If allowed (initData or localhost), call onAllowed(); otherwise show access denied. When inside Telegram WebApp but initData is empty, retry once after a short delay (initData may be set asynchronously). */
function requireTelegramOrLocalhost(onAllowed) {
var initData = getInitData();
var isLocal = isLocalhost();
if (initData) { onAllowed(); return; }
if (isLocal) { onAllowed(); return; }
if (window.Telegram && window.Telegram.WebApp) {
setTimeout(function () {
initData = getInitData();
if (initData) { onAllowed(); return; }
showAccessDenied();
loadingEl.classList.add("hidden");
}, RETRY_DELAY_MS);
return;
}
showAccessDenied();
loadingEl.classList.add("hidden");
}
function setNavEnabled(enabled) {
if (prevBtn) prevBtn.disabled = !enabled;
if (nextBtn) nextBtn.disabled = !enabled;
@@ -229,7 +285,7 @@
setNavEnabled(true);
if (window.Telegram && window.Telegram.WebApp && !window._initDataRetried) {
window._initDataRetried = true;
setTimeout(loadMonth, 1200);
setTimeout(loadMonth, RETRY_AFTER_ACCESS_DENIED_MS);
}
return;
}
@@ -250,5 +306,9 @@
loadMonth();
});
runWhenReady(loadMonth);
runWhenReady(function () {
requireTelegramOrLocalhost(function () {
loadMonth();
});
});
})();