refactor: remove local storing tg js

This commit is contained in:
3252a8
2026-04-28 23:33:24 +03:00
parent aabc0e312d
commit d7d5c2b1ef
10 changed files with 52 additions and 4304 deletions
+2
View File
@@ -29,3 +29,5 @@ __pycache__/
locales/ru_backup.json
locales/en_backup.json
db/models_old.py
config/tariffs.json
docker-compose-dev.yml
-13
View File
@@ -303,19 +303,6 @@ Web App запускается в том же контейнере, что и б
Для email-регистраций пользователь в панели Remnawave создается с анонимным username вида `em_<referral_code>`; email добавляется в описание пользователя панели и, если API панели принимает поле email, передается отдельным полем. Для Telegram-регистраций сохраняется существующая схема `tg_<telegram_id>`.
Шапка и модальные окна Web App учитывают Telegram safe area через `--tg-content-safe-area-inset-top` и `--tg-safe-area-inset-top`, а сверху добавлен повышенный дополнительный буфер, чтобы интерфейс не уезжал под панель Telegram при открытии из списка чатов.
Для настройки внешнего вида без запуска бота можно открыть файл `bot/app/web/templates/subscription_webapp.html` напрямую в браузере. Рядом с ним лежат `telegram-web-app.js`, `telegram-widget.js`, `subscription_webapp.css` и `subscription_webapp.js`, поэтому локальный предпросмотр работает без сервера и без внешнего CDN. При отдаче страницы через Web App сервер dev-mock автоматически вырезается и не попадает пользователям.
Локальная копия Telegram Web App SDK хранится в `bot/app/web/templates/telegram-web-app.js`, а локальная копия Telegram Login Widget - в `bot/app/web/templates/telegram-widget.js`. В контейнере обе копии автоматически обновляются при старте Web App и затем раз в 24 часа; если источник временно недоступен, используется уже сохраненная версия. У локального `telegram-widget.js` есть минимальная нормализация, чтобы при загрузке с вашего домена виджет все равно открывал iframe на Telegram-origin, а не на `/embed/...` вашего сайта. Для ручного обновления можно запустить команды:
```bash
python scripts/update_telegram_web_app_js.py
python scripts/update_telegram_widget_js.py
```
При необходимости оба скрипта принимают `--source-url` и `--target`, если нужно скачать файл в другое место или проверить альтернативный источник.
## Подробная инструкция для развертывания на сервере с панелью Remnawave
### 1. Клонирование репозитория
-146
View File
@@ -48,13 +48,7 @@ logger = logging.getLogger(__name__)
TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "subscription_webapp.html"
ASSET_DIR = TEMPLATE_PATH.parent
TELEGRAM_WEB_APP_SDK_URL = "https://telegram.org/js/telegram-web-app.js"
TELEGRAM_WEB_APP_SDK_PATH = ASSET_DIR / "telegram-web-app.js"
TELEGRAM_WIDGET_SDK_URL = "https://telegram.org/js/telegram-widget.js?23"
TELEGRAM_WIDGET_SDK_PATH = ASSET_DIR / "telegram-widget.js"
WEBAPP_LOGO_PROXY_PATH = "/webapp-logo"
_UNPATCHED_WIDGET_ORIGIN_SNIPPET = """ if (origin == 'https://telegram.org') {\n origin = default_origin;\n } else if (origin == 'https://telegram-js.azureedge.net' || origin == 'https://tg.dev') {\n origin = dev_origin;\n }\n"""
_PATCHED_WIDGET_ORIGIN_SNIPPET = """ if (origin == 'https://telegram.org') {\n origin = default_origin;\n } else if (origin == 'https://telegram-js.azureedge.net' || origin == 'https://tg.dev') {\n origin = dev_origin;\n } else {\n origin = default_origin;\n }\n"""
WEBAPP_CONFIG_PLACEHOLDER = "<!-- WEBAPP_CONFIG_SCRIPT -->"
WEBAPP_I18N_PLACEHOLDER = "<!-- WEBAPP_I18N_SCRIPT -->"
WEBAPP_JS_PLACEHOLDER = "<!-- WEBAPP_JS_SCRIPT -->"
@@ -164,8 +158,6 @@ def create_subscription_webapp_application(
def setup_subscription_webapp_routes(app: web.Application) -> None:
app.router.add_get("/", index_route)
app.router.add_get("/health", health_route)
app.router.add_get("/telegram-web-app.js", telegram_web_app_asset_route)
app.router.add_get("/telegram-widget.js", telegram_widget_asset_route)
app.router.add_get(WEBAPP_LOGO_PROXY_PATH, webapp_logo_route)
app.router.add_get("/subscription_webapp.css", css_asset_route)
app.router.add_get("/subscription_webapp.min.{asset_hash}.js", js_asset_route)
@@ -467,144 +459,6 @@ async def _enforce_webapp_rate_limit(
return None
async def telegram_web_app_asset_route(request: web.Request) -> web.Response:
if not TELEGRAM_WEB_APP_SDK_PATH.exists():
await refresh_telegram_web_app_sdk()
try:
response = await _serve_template_asset(
request,
"telegram-web-app.js",
"application/javascript",
)
except FileNotFoundError:
logger.exception(
"Telegram Web App SDK is unavailable at %s",
TELEGRAM_WEB_APP_SDK_PATH,
)
raise web.HTTPServiceUnavailable(text="telegram_web_app_sdk_unavailable")
response.headers["Cache-Control"] = "no-cache"
return response
async def telegram_widget_asset_route(request: web.Request) -> web.Response:
if not TELEGRAM_WIDGET_SDK_PATH.exists():
await refresh_telegram_login_widget_sdk()
try:
data = TELEGRAM_WIDGET_SDK_PATH.read_bytes()
except FileNotFoundError:
logger.exception(
"Telegram Login Widget SDK is unavailable at %s",
TELEGRAM_WIDGET_SDK_PATH,
)
raise web.HTTPServiceUnavailable(text="telegram_widget_sdk_unavailable")
data = _normalize_telegram_login_widget_sdk(data)
response = web.Response(body=data, content_type="application/javascript")
response.headers["Cache-Control"] = "no-cache"
return response
async def refresh_telegram_web_app_sdk() -> bool:
"""Best-effort refresh of the vendored Telegram Web App SDK."""
try:
session = await _get_shared_http_session()
async with session.get(TELEGRAM_WEB_APP_SDK_URL) as response:
if response.status != 200:
logger.warning(
"Telegram Web App SDK refresh returned HTTP %s; keeping the bundled copy.",
response.status,
)
return False
data = await response.read()
except Exception as exc:
logger.warning("Failed to refresh Telegram Web App SDK: %s", exc)
return False
try:
TELEGRAM_WEB_APP_SDK_PATH.parent.mkdir(parents=True, exist_ok=True)
existing_data = (
TELEGRAM_WEB_APP_SDK_PATH.read_bytes()
if TELEGRAM_WEB_APP_SDK_PATH.exists()
else None
)
if existing_data == data:
logger.info("Telegram Web App SDK is already up to date.")
return True
temp_path = TELEGRAM_WEB_APP_SDK_PATH.with_name(
f"{TELEGRAM_WEB_APP_SDK_PATH.name}.tmp"
)
temp_path.write_bytes(data)
temp_path.replace(TELEGRAM_WEB_APP_SDK_PATH)
logger.info(
"Telegram Web App SDK updated at %s (%d bytes).",
TELEGRAM_WEB_APP_SDK_PATH,
len(data),
)
return True
except Exception as exc:
logger.warning("Failed to store Telegram Web App SDK locally: %s", exc)
return False
async def refresh_telegram_login_widget_sdk() -> bool:
"""Best-effort refresh of the vendored Telegram Login Widget SDK."""
try:
session = await _get_shared_http_session()
async with session.get(TELEGRAM_WIDGET_SDK_URL) as response:
if response.status != 200:
logger.warning(
"Telegram Login Widget SDK refresh returned HTTP %s; keeping the bundled copy.",
response.status,
)
return False
data = await response.read()
except Exception as exc:
logger.warning("Failed to refresh Telegram Login Widget SDK: %s", exc)
return False
try:
data = _normalize_telegram_login_widget_sdk(data)
TELEGRAM_WIDGET_SDK_PATH.parent.mkdir(parents=True, exist_ok=True)
existing_data = (
TELEGRAM_WIDGET_SDK_PATH.read_bytes()
if TELEGRAM_WIDGET_SDK_PATH.exists()
else None
)
if existing_data == data:
logger.info("Telegram Login Widget SDK is already up to date.")
return True
temp_path = TELEGRAM_WIDGET_SDK_PATH.with_name(
f"{TELEGRAM_WIDGET_SDK_PATH.name}.tmp"
)
temp_path.write_bytes(data)
temp_path.replace(TELEGRAM_WIDGET_SDK_PATH)
logger.info(
"Telegram Login Widget SDK updated at %s (%d bytes).",
TELEGRAM_WIDGET_SDK_PATH,
len(data),
)
return True
except Exception as exc:
logger.warning("Failed to store Telegram Login Widget SDK locally: %s", exc)
return False
def _normalize_telegram_login_widget_sdk(data: bytes) -> bytes:
# Keep the vendored widget pointing to Telegram's OAuth host instead of the local origin.
text = data.decode("utf-8")
normalized = text.replace(
_UNPATCHED_WIDGET_ORIGIN_SNIPPET,
_PATCHED_WIDGET_ORIGIN_SNIPPET,
1,
)
return normalized.encode("utf-8")
async def js_asset_route(request: web.Request) -> web.Response:
asset_hash = request.match_info.get("asset_hash")
filename = (
@@ -19,7 +19,7 @@
font-display: swap;
}
</style>
<script src="./telegram-web-app.js"></script>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="./subscription_webapp.css">
</head>
+48 -25
View File
@@ -91,7 +91,8 @@ const MOCK = (() => {
const CFG = readJsonScript('webapp-config') || (MOCK && MOCK.config) || {};
const tg = window.Telegram && window.Telegram.WebApp ? window.Telegram.WebApp : null;
const TELEGRAM_LOGIN_WIDGET_URL = './telegram-widget.js';
const TELEGRAM_LOGIN_WIDGET_URL = 'https://telegram.org/js/telegram-widget.js?23';
const TELEGRAM_LOGIN_WIDGET_RENDER_TIMEOUT_MS = 8000;
const MANUAL_LOGOUT_FLAG_KEY = 'rw_webapp_manual_logout';
const state = {
token: MOCK ? 'local-preview' : (localStorage.getItem('rw_webapp_token') || ''),
@@ -241,7 +242,8 @@ const MOCK = (() => {
telegram_auth: 'Telegram auth',
telegram_auth_verifying: 'Проверяю вход...',
telegram_auth_failed: 'Не удалось подтвердить Telegram-вход. Попробуйте еще раз.',
telegram_auth_unavailable: 'Telegram Login Widget недоступен. Проверьте username бота и доступ к telegram.org.',
telegram_auth_unavailable: 'Сервер Telegram сейчас недоступен. Попробуйте позже или войдите по email.',
telegram_bot_unavailable: 'Telegram Login Widget недоступен. Попробуйте позже или войдите по email.',
telegram_auth_access_denied: 'Доступ запрещен.',
active: 'Активна',
inactive: 'Не активна',
@@ -381,7 +383,8 @@ const MOCK = (() => {
telegram_auth: 'Telegram auth',
telegram_auth_verifying: 'Verifying login...',
telegram_auth_failed: 'Could not verify Telegram login. Try again.',
telegram_auth_unavailable: 'Telegram Login Widget is unavailable. Check the bot username and access to telegram.org.',
telegram_auth_unavailable: 'Telegram server is unavailable right now. Try again later or sign in by email.',
telegram_bot_unavailable: 'Telegram Login Widget is unavailable. Try again later or sign in by email.',
telegram_auth_access_denied: 'Access denied.',
active: 'Active',
inactive: 'Inactive',
@@ -701,6 +704,32 @@ const MOCK = (() => {
await finalizeTelegramAuth(user, 'auth_data');
}
function appendTelegramLoginWidget(container, botUsername, callbackName, onUnavailable) {
const script = document.createElement('script');
let unavailableShown = false;
const showUnavailable = () => {
if (unavailableShown) return;
unavailableShown = true;
onUnavailable();
};
script.async = true;
script.src = TELEGRAM_LOGIN_WIDGET_URL;
script.setAttribute('data-telegram-login', botUsername);
script.setAttribute('data-size', 'large');
script.setAttribute('data-userpic', 'true');
script.setAttribute('data-request-access', 'write');
script.setAttribute('data-onauth', `${callbackName}(user)`);
script.onerror = showUnavailable;
script.onload = () => {
window.setTimeout(() => {
if (!container.contains(script) || container.querySelector('iframe')) return;
showUnavailable();
}, TELEGRAM_LOGIN_WIDGET_RENDER_TIMEOUT_MS);
};
container.appendChild(script);
}
function setAuthMode(mode) {
state.authMode = mode === 'telegram' ? 'telegram' : 'email';
renderAuthMode();
@@ -740,7 +769,7 @@ const MOCK = (() => {
container.innerHTML = '';
const botUsername = String(CFG.telegramLoginBotUsername || '').trim();
if (!botUsername) {
setAuthStatus(t('telegram_auth_unavailable'), true);
setAuthStatus(t('telegram_bot_unavailable'), true);
return;
}
@@ -750,16 +779,13 @@ const MOCK = (() => {
};
}
const script = document.createElement('script');
script.async = true;
script.src = TELEGRAM_LOGIN_WIDGET_URL;
script.setAttribute('data-telegram-login', botUsername);
script.setAttribute('data-size', 'large');
script.setAttribute('data-userpic', 'true');
script.setAttribute('data-request-access', 'write');
script.setAttribute('data-onauth', 'onTelegramAuth(user)');
script.onerror = () => setAuthStatus(t('telegram_auth_unavailable'), true);
container.appendChild(script);
setAuthStatus('');
appendTelegramLoginWidget(
container,
botUsername,
'onTelegramAuth',
() => setAuthStatus(t('telegram_auth_unavailable'), true)
);
}
function bindEmailLoginInput() {
@@ -1864,7 +1890,7 @@ const MOCK = (() => {
container.innerHTML = '';
const botUsername = String(CFG.telegramLoginBotUsername || '').trim();
if (!botUsername) {
setTelegramLinkStatus(t('telegram_auth_unavailable'), true);
setTelegramLinkStatus(t('telegram_bot_unavailable'), true);
return;
}
@@ -1872,16 +1898,13 @@ const MOCK = (() => {
await linkTelegramAccount(user);
};
const script = document.createElement('script');
script.async = true;
script.src = TELEGRAM_LOGIN_WIDGET_URL;
script.setAttribute('data-telegram-login', botUsername);
script.setAttribute('data-size', 'large');
script.setAttribute('data-userpic', 'true');
script.setAttribute('data-request-access', 'write');
script.setAttribute('data-onauth', 'onTelegramLinkAuth(user)');
script.onerror = () => setTelegramLinkStatus(t('telegram_auth_unavailable'), true);
container.appendChild(script);
setTelegramLinkStatus('');
appendTelegramLoginWidget(
container,
botUsername,
'onTelegramLinkAuth',
() => setTelegramLinkStatus(t('telegram_auth_unavailable'), true)
);
state.telegramLinkRendered = true;
}
File diff suppressed because it is too large Load Diff
-575
View File
@@ -1,575 +0,0 @@
(function(window) {
(function(window){
window.__parseFunction = function(__func, __attrs) {
__attrs = __attrs || [];
__func = '(function(' + __attrs.join(',') + '){' + __func + '})';
return window.execScript ? window.execScript(__func) : eval(__func);
}
}(window));
(function(window){
function addEvent(el, event, handler) {
var events = event.split(/\s+/);
for (var i = 0; i < events.length; i++) {
if (el.addEventListener) {
el.addEventListener(events[i], handler);
} else {
el.attachEvent('on' + events[i], handler);
}
}
}
function removeEvent(el, event, handler) {
var events = event.split(/\s+/);
for (var i = 0; i < events.length; i++) {
if (el.removeEventListener) {
el.removeEventListener(events[i], handler);
} else {
el.detachEvent('on' + events[i], handler);
}
}
}
function getCssProperty(el, prop) {
if (window.getComputedStyle) {
return window.getComputedStyle(el, '').getPropertyValue(prop) || null;
} else if (el.currentStyle) {
return el.currentStyle[prop] || null;
}
return null;
}
function geById(el_or_id) {
if (typeof el_or_id == 'string' || el_or_id instanceof String) {
return document.getElementById(el_or_id);
} else if (el_or_id instanceof HTMLElement) {
return el_or_id;
}
return null;
}
var getWidgetsOrigin = function(default_origin, dev_origin) {
var link = document.createElement('A'), origin;
link.href = document.currentScript && document.currentScript.src || default_origin;
origin = link.origin || link.protocol + '//' + link.hostname;
if (origin == 'https://telegram.org') {
origin = default_origin;
} else if (origin == 'https://telegram-js.azureedge.net' || origin == 'https://tg.dev') {
origin = dev_origin;
} else {
origin = default_origin;
}
return origin;
};
var getPageCanonical = function() {
var a = document.createElement('A'), link, href;
if (document.querySelector) {
link = document.querySelector('link[rel="canonical"]');
if (link && (href = link.getAttribute('href'))) {
a.href = href;
return a.href;
}
} else {
var links = document.getElementsByTagName('LINK');
for (var i = 0; i < links.length; i++) {
if ((link = links[i]) &&
(link.getAttribute('rel') == 'canonical') &&
(href = link.getAttribute('href'))) {
a.href = href;
return a.href;
}
}
}
return false;
};
function haveTgAuthResult() {
var locationHash = '', re = /[#\?\&]tgAuthResult=([A-Za-z0-9\-_=]*)$/, match;
try {
locationHash = location.hash.toString();
if (match = locationHash.match(re)) {
location.hash = locationHash.replace(re, '');
var data = match[1] || '';
data = data.replace(/-/g, '+').replace(/_/g, '/');
var pad = data.length % 4;
if (pad > 1) {
data += new Array(5 - pad).join('=');
}
return JSON.parse(window.atob(data));
}
} catch (e) {}
return false;
}
function getXHR() {
if (navigator.appName == "Microsoft Internet Explorer"){
return new ActiveXObject("Microsoft.XMLHTTP");
} else {
return new XMLHttpRequest();
}
}
if (!window.Telegram) {
window.Telegram = {};
}
if (!window.Telegram.__WidgetUuid) {
window.Telegram.__WidgetUuid = 0;
}
if (!window.Telegram.__WidgetLastId) {
window.Telegram.__WidgetLastId = 0;
}
if (!window.Telegram.__WidgetCallbacks) {
window.Telegram.__WidgetCallbacks = {};
}
function postMessageToIframe(iframe, event, data, callback) {
if (!iframe._ready) {
if (!iframe._readyQueue) iframe._readyQueue = [];
iframe._readyQueue.push([event, data, callback]);
return;
}
try {
data = data || {};
data.event = event;
if (callback) {
data._cb = ++window.Telegram.__WidgetLastId;
window.Telegram.__WidgetCallbacks[data._cb] = {
iframe: iframe,
callback: callback
};
}
iframe.contentWindow.postMessage(JSON.stringify(data), '*');
} catch(e) {}
}
function initWidget(widgetEl) {
var widgetId, widgetElId, widgetsOrigin, existsEl,
src, styles = {}, allowedAttrs = [],
defWidth, defHeight, scrollable = false, onInitAuthUser, onAuthUser, onUnauth;
if (!widgetEl.tagName ||
!(widgetEl.tagName.toUpperCase() == 'SCRIPT' ||
widgetEl.tagName.toUpperCase() == 'BLOCKQUOTE' &&
widgetEl.classList.contains('telegram-post'))) {
return null;
}
if (widgetEl._iframe) {
return widgetEl._iframe;
}
if (widgetId = widgetEl.getAttribute('data-telegram-post')) {
var comment = widgetEl.getAttribute('data-comment') || '';
widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev');
widgetElId = 'telegram-post-' + widgetId.replace(/[^a-z0-9_]/ig, '-') + (comment ? '-comment' + comment : '');
src = widgetsOrigin + '/' + widgetId + '?embed=1';
allowedAttrs = ['comment', 'userpic', 'mode', 'single?', 'color', 'dark', 'dark_color'];
defWidth = widgetEl.getAttribute('data-width') || '100%';
defHeight = '';
styles.minWidth = '320px';
}
else if (widgetId = widgetEl.getAttribute('data-telegram-discussion')) {
widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev');
widgetElId = 'telegram-discussion-' + widgetId.replace(/[^a-z0-9_]/ig, '-') + '-' + (++window.Telegram.__WidgetUuid);
var websitePageUrl = widgetEl.getAttribute('data-page-url');
if (!websitePageUrl) {
websitePageUrl = getPageCanonical();
}
src = widgetsOrigin + '/' + widgetId + '?embed=1&discussion=1' + (websitePageUrl ? '&page_url=' + encodeURIComponent(websitePageUrl) : '');
allowedAttrs = ['comments_limit', 'color', 'colorful', 'dark', 'dark_color', 'width', 'height'];
defWidth = widgetEl.getAttribute('data-width') || '100%';
defHeight = widgetEl.getAttribute('data-height') || 0;
styles.minWidth = '320px';
if (defHeight > 0) {
scrollable = true;
}
}
else if (widgetEl.hasAttribute('data-telegram-login')) {
widgetId = widgetEl.getAttribute('data-telegram-login');
widgetsOrigin = getWidgetsOrigin('https://oauth.telegram.org', 'https://oauth.tg.dev');
widgetElId = 'telegram-login-' + widgetId.replace(/[^a-z0-9_]/ig, '-');
src = widgetsOrigin + '/embed/' + widgetId + '?origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname) + '&return_to=' + encodeURIComponent(location.href);
allowedAttrs = ['size', 'userpic', 'init_auth', 'request_access', 'radius', 'min_width', 'max_width', 'lang'];
defWidth = 186;
defHeight = 28;
if (widgetEl.hasAttribute('data-size')) {
var size = widgetEl.getAttribute('data-size');
if (size == 'small') defWidth = 148, defHeight = 20;
else if (size == 'large') defWidth = 238, defHeight = 40;
}
if (widgetEl.hasAttribute('data-onauth')) {
onInitAuthUser = onAuthUser = __parseFunction(widgetEl.getAttribute('data-onauth'), ['user']);
}
else if (widgetEl.hasAttribute('data-auth-url')) {
var a = document.createElement('A');
a.href = widgetEl.getAttribute('data-auth-url');
onAuthUser = function(user) {
var authUrl = a.href;
authUrl += (authUrl.indexOf('?') >= 0) ? '&' : '?';
var params = [];
for (var key in user) {
params.push(key + '=' + encodeURIComponent(user[key]));
}
authUrl += params.join('&');
location.href = authUrl;
};
}
if (widgetEl.hasAttribute('data-onunauth')) {
onUnauth = __parseFunction(widgetEl.getAttribute('data-onunauth'));
}
var auth_result = haveTgAuthResult();
if (auth_result && onAuthUser) {
onAuthUser(auth_result);
}
}
else if (widgetId = widgetEl.getAttribute('data-telegram-share-url')) {
widgetsOrigin = getWidgetsOrigin('https://t.me', 'https://post.tg.dev');
widgetElId = 'telegram-share-' + window.btoa(widgetId);
src = widgetsOrigin + '/share/embed?origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname);
allowedAttrs = ['telegram-share-url', 'comment', 'size', 'text'];
defWidth = 60;
defHeight = 20;
if (widgetEl.getAttribute('data-size') == 'large') {
defWidth = 76;
defHeight = 28;
}
}
else {
return null;
}
existsEl = document.getElementById(widgetElId);
if (existsEl) {
return existsEl;
}
for (var i = 0; i < allowedAttrs.length; i++) {
var attr = allowedAttrs[i];
var novalue = attr.substr(-1) == '?';
if (novalue) {
attr = attr.slice(0, -1);
}
var data_attr = 'data-' + attr.replace(/_/g, '-');
if (widgetEl.hasAttribute(data_attr)) {
var attr_value = novalue ? '1' : encodeURIComponent(widgetEl.getAttribute(data_attr));
src += '&' + attr + '=' + attr_value;
}
}
function getCurCoords(iframe) {
var docEl = document.documentElement;
var frect = iframe.getBoundingClientRect();
return {
frameTop: frect.top,
frameBottom: frect.bottom,
frameLeft: frect.left,
frameRight: frect.right,
frameWidth: frect.width,
frameHeight: frect.height,
scrollTop: window.pageYOffset,
scrollLeft: window.pageXOffset,
clientWidth: docEl.clientWidth,
clientHeight: docEl.clientHeight
};
}
function visibilityHandler() {
if (isVisible(iframe, 50)) {
postMessageToIframe(iframe, 'visible', {frame: widgetElId});
}
}
function focusHandler() {
postMessageToIframe(iframe, 'focus', {has_focus: document.hasFocus()});
}
function postMessageHandler(event) {
if (event.source !== iframe.contentWindow ||
event.origin != widgetsOrigin) {
return;
}
try {
var data = JSON.parse(event.data);
} catch(e) {
var data = {};
}
if (data.event == 'resize') {
if (data.height) {
iframe.style.height = data.height + 'px';
}
if (data.width) {
iframe.style.width = data.width + 'px';
}
}
else if (data.event == 'ready') {
iframe._ready = true;
focusHandler();
for (var i = 0; i < iframe._readyQueue.length; i++) {
var queue_item = iframe._readyQueue[i];
postMessageToIframe(iframe, queue_item[0], queue_item[1], queue_item[2]);
}
iframe._readyQueue = [];
}
else if (data.event == 'visible_off') {
removeEvent(window, 'scroll', visibilityHandler);
removeEvent(window, 'resize', visibilityHandler);
}
else if (data.event == 'get_coords') {
postMessageToIframe(iframe, 'callback', {
_cb: data._cb,
value: getCurCoords(iframe)
});
}
else if (data.event == 'scroll_to') {
try {
window.scrollTo(data.x || 0, data.y || 0);
} catch(e) {}
}
else if (data.event == 'auth_user') {
if (data.init) {
onInitAuthUser && onInitAuthUser(data.auth_data);
} else {
onAuthUser && onAuthUser(data.auth_data);
}
}
else if (data.event == 'unauthorized') {
onUnauth && onUnauth();
}
else if (data.event == 'callback') {
var cb_data = null;
if (cb_data = window.Telegram.__WidgetCallbacks[data._cb]) {
if (cb_data.iframe === iframe) {
cb_data.callback(data.value);
delete window.Telegram.__WidgetCallbacks[data._cb];
}
} else {
console.warn('Callback #' + data._cb + ' not found');
}
}
}
var iframe = document.createElement('iframe');
iframe.id = widgetElId;
iframe.src = src;
iframe.width = defWidth;
iframe.height = defHeight;
iframe.setAttribute('frameborder', '0');
if (!scrollable) {
iframe.setAttribute('scrolling', 'no');
iframe.style.overflow = 'hidden';
}
iframe.style.colorScheme = 'light dark';
iframe.style.border = 'none';
for (var prop in styles) {
iframe.style[prop] = styles[prop];
}
if (widgetEl.parentNode) {
widgetEl.parentNode.insertBefore(iframe, widgetEl);
if (widgetEl.tagName.toUpperCase() == 'BLOCKQUOTE') {
widgetEl.parentNode.removeChild(widgetEl);
}
}
iframe._ready = false;
iframe._readyQueue = [];
widgetEl._iframe = iframe;
addEvent(iframe, 'load', function() {
removeEvent(iframe, 'load', visibilityHandler);
addEvent(window, 'scroll', visibilityHandler);
addEvent(window, 'resize', visibilityHandler);
visibilityHandler();
});
addEvent(window, 'focus blur', focusHandler);
addEvent(window, 'message', postMessageHandler);
return iframe;
}
function isVisible(el, padding) {
var node = el, val;
var visibility = getCssProperty(node, 'visibility');
if (visibility == 'hidden') return false;
while (node) {
if (node === document.documentElement) break;
var display = getCssProperty(node, 'display');
if (display == 'none') return false;
var opacity = getCssProperty(node, 'opacity');
if (opacity !== null && opacity < 0.1) return false;
node = node.parentNode;
}
if (el.getBoundingClientRect) {
padding = +padding || 0;
var rect = el.getBoundingClientRect();
var html = document.documentElement;
if (rect.bottom < padding ||
rect.right < padding ||
rect.top > (window.innerHeight || html.clientHeight) - padding ||
rect.left > (window.innerWidth || html.clientWidth) - padding) {
return false;
}
}
return true;
}
function getAllWidgets() {
var widgets = [];
if (document.querySelectorAll) {
widgets = document.querySelectorAll('script[data-telegram-post],blockquote.telegram-post,script[data-telegram-discussion],script[data-telegram-login],script[data-telegram-share-url]');
} else {
widgets = Array.prototype.slice.apply(document.getElementsByTagName('SCRIPT'));
widgets = widgets.concat(Array.prototype.slice.apply(document.getElementsByTagName('BLOCKQUOTE')));
}
return widgets;
}
function getWidgetInfo(el_or_id, callback) {
var e = null, iframe = null;
if (el = geById(el_or_id)) {
if (el.tagName &&
el.tagName.toUpperCase() == 'IFRAME') {
iframe = el;
} else if (el._iframe) {
iframe = el._iframe;
}
if (iframe && callback) {
postMessageToIframe(iframe, 'get_info', {}, callback);
}
}
}
function setWidgetOptions(options, el_or_id) {
var e = null, iframe = null;
if (typeof el_or_id === 'undefined') {
var widgets = getAllWidgets();
for (var i = 0; i < widgets.length; i++) {
if (iframe = widgets[i]._iframe) {
postMessageToIframe(iframe, 'set_options', {options: options});
}
}
} else {
if (el = geById(el_or_id)) {
if (el.tagName &&
el.tagName.toUpperCase() == 'IFRAME') {
iframe = el;
} else if (el._iframe) {
iframe = el._iframe;
}
if (iframe) {
postMessageToIframe(iframe, 'set_options', {options: options});
}
}
}
}
if (!document.currentScript ||
!initWidget(document.currentScript)) {
var widgets = getAllWidgets();
for (var i = 0; i < widgets.length; i++) {
initWidget(widgets[i]);
}
}
var TelegramLogin = {
popups: {},
options: null,
auth_callback: null,
_init: function(options, auth_callback) {
TelegramLogin.options = options;
TelegramLogin.auth_callback = auth_callback;
var auth_result = haveTgAuthResult();
if (auth_result && auth_callback) {
auth_callback(auth_result);
}
},
_open: function(callback) {
TelegramLogin._auth(TelegramLogin.options, function(authData) {
if (TelegramLogin.auth_callback) {
TelegramLogin.auth_callback(authData);
}
if (callback) {
callback(authData);
}
});
},
_auth: function(options, callback) {
var bot_id = parseInt(options.bot_id);
if (!bot_id) {
throw new Error('Bot id required');
}
var width = 550;
var height = 470;
var left = Math.max(0, (screen.width - width) / 2) + (screen.availLeft | 0),
top = Math.max(0, (screen.height - height) / 2) + (screen.availTop | 0);
var onMessage = function (event) {
try {
var data = JSON.parse(event.data);
} catch(e) {
var data = {};
}
if (!TelegramLogin.popups[bot_id]) return;
if (event.source !== TelegramLogin.popups[bot_id].window) return;
if (data.event == 'auth_result') {
onAuthDone(data.result);
}
};
var onAuthDone = function (authData) {
if (!TelegramLogin.popups[bot_id]) return;
if (TelegramLogin.popups[bot_id].authFinished) return;
callback && callback(authData);
TelegramLogin.popups[bot_id].authFinished = true;
removeEvent(window, 'message', onMessage);
};
var checkClose = function(bot_id) {
if (!TelegramLogin.popups[bot_id]) return;
if (!TelegramLogin.popups[bot_id].window ||
TelegramLogin.popups[bot_id].window.closed) {
return TelegramLogin.getAuthData(options, function(origin, authData) {
onAuthDone(authData);
});
}
setTimeout(checkClose, 100, bot_id);
}
var popup_url = Telegram.Login.widgetsOrigin + '/auth?bot_id=' + encodeURIComponent(options.bot_id) + '&origin=' + encodeURIComponent(location.origin || location.protocol + '//' + location.hostname) + (options.request_access ? '&request_access=' + encodeURIComponent(options.request_access) : '') + (options.lang ? '&lang=' + encodeURIComponent(options.lang) : '') + '&return_to=' + encodeURIComponent(location.href);
var popup = window.open(popup_url, 'telegram_oauth_bot' + bot_id, 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',status=0,location=0,menubar=0,toolbar=0');
TelegramLogin.popups[bot_id] = {
window: popup,
authFinished: false
};
if (popup) {
addEvent(window, 'message', onMessage);
popup.focus();
checkClose(bot_id);
}
},
getAuthData: function(options, callback) {
var bot_id = parseInt(options.bot_id);
if (!bot_id) {
throw new Error('Bot id required');
}
var xhr = getXHR();
var url = Telegram.Login.widgetsOrigin + '/auth/get';
xhr.open('POST', url);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (typeof xhr.responseBody == 'undefined' && xhr.responseText) {
try {
var result = JSON.parse(xhr.responseText);
} catch(e) {
var result = {};
}
if (result.user) {
callback(result.origin, result.user);
} else {
callback(result.origin, false);
}
} else {
callback('*', false);
}
}
};
xhr.onerror = function() {
callback('*', false);
};
xhr.withCredentials = true;
xhr.send('bot_id=' + encodeURIComponent(options.bot_id) + (options.lang ? '&lang=' + encodeURIComponent(options.lang) : ''));
}
};
window.Telegram.getWidgetInfo = getWidgetInfo;
window.Telegram.setWidgetOptions = setWidgetOptions;
window.Telegram.Login = {
init: TelegramLogin._init,
open: TelegramLogin._open,
auth: TelegramLogin._auth,
widgetsOrigin: getWidgetsOrigin('https://oauth.telegram.org', 'https://oauth.tg.dev')
};
}(window));
})(window);
+1 -25
View File
@@ -1,7 +1,6 @@
import hmac
import asyncio
import logging
from contextlib import suppress
from aiohttp import web
from aiogram import Bot, Dispatcher
@@ -17,8 +16,6 @@ class SecureSimpleRequestHandler(SimpleRequestHandler):
return False
return hmac.compare_digest(telegram_secret_token, self.secret_token)
TELEGRAM_WEB_APP_SDK_REFRESH_INTERVAL_SECONDS = 24 * 60 * 60
def _inject_shared_instances(
app: web.Application,
@@ -132,13 +129,8 @@ async def build_and_start_web_app(
f"AIOHTTP server started on http://{settings.WEB_SERVER_HOST}:{settings.WEB_SERVER_PORT}"
)
telegram_web_app_sdk_refresh_task = None
if settings.WEBAPP_ENABLED:
from bot.app.web.subscription_webapp import (
create_subscription_webapp_application,
refresh_telegram_login_widget_sdk,
refresh_telegram_web_app_sdk,
)
from bot.app.web.subscription_webapp import create_subscription_webapp_application
subscription_app = create_subscription_webapp_application(
dp,
@@ -161,25 +153,9 @@ async def build_and_start_web_app(
settings.WEBAPP_SERVER_PORT,
)
async def _refresh_telegram_web_assets_forever() -> None:
while True:
await refresh_telegram_web_app_sdk()
await refresh_telegram_login_widget_sdk()
await asyncio.sleep(TELEGRAM_WEB_APP_SDK_REFRESH_INTERVAL_SECONDS)
telegram_web_app_sdk_refresh_task = asyncio.create_task(
_refresh_telegram_web_assets_forever(),
name="TelegramWebAssetsRefreshTask",
)
try:
await asyncio.Event().wait()
finally:
if telegram_web_app_sdk_refresh_task is not None:
telegram_web_app_sdk_refresh_task.cancel()
with suppress(asyncio.CancelledError):
await telegram_web_app_sdk_refresh_task
for runner in reversed(runners):
try:
await runner.cleanup()
-57
View File
@@ -1,57 +0,0 @@
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from urllib.request import Request, urlopen
SOURCE_URL = "https://telegram.org/js/telegram-web-app.js"
TARGET_PATH = (
Path(__file__).resolve().parents[1]
/ "bot"
/ "app"
/ "web"
/ "templates"
/ "telegram-web-app.js"
)
def _download(source_url: str) -> bytes:
request = Request(
source_url,
headers={
"User-Agent": "Mozilla/5.0",
"Accept": "application/javascript,text/javascript,*/*;q=0.8",
},
)
with urlopen(request, timeout=30) as response:
return response.read()
def main() -> int:
parser = argparse.ArgumentParser(
description="Download the latest Telegram Web App SDK into the local templates directory."
)
parser.add_argument(
"--source-url",
default=SOURCE_URL,
help="Telegram Web App SDK URL to download from.",
)
parser.add_argument(
"--target",
type=Path,
default=TARGET_PATH,
help="Output path for the vendored SDK.",
)
args = parser.parse_args()
data = _download(args.source_url)
args.target.parent.mkdir(parents=True, exist_ok=True)
args.target.write_bytes(data)
print(f"Wrote {len(data)} bytes to {args.target}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
-70
View File
@@ -1,70 +0,0 @@
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from urllib.request import Request, urlopen
SOURCE_URL = "https://telegram.org/js/telegram-widget.js?23"
TARGET_PATH = (
Path(__file__).resolve().parents[1]
/ "bot"
/ "app"
/ "web"
/ "templates"
/ "telegram-widget.js"
)
_UNPATCHED_WIDGET_ORIGIN_SNIPPET = """ if (origin == 'https://telegram.org') {\n origin = default_origin;\n } else if (origin == 'https://telegram-js.azureedge.net' || origin == 'https://tg.dev') {\n origin = dev_origin;\n }\n"""
_PATCHED_WIDGET_ORIGIN_SNIPPET = """ if (origin == 'https://telegram.org') {\n origin = default_origin;\n } else if (origin == 'https://telegram-js.azureedge.net' || origin == 'https://tg.dev') {\n origin = dev_origin;\n } else {\n origin = default_origin;\n }\n"""
def _download(source_url: str) -> bytes:
request = Request(
source_url,
headers={
"User-Agent": "Mozilla/5.0",
"Accept": "application/javascript,text/javascript,*/*;q=0.8",
},
)
with urlopen(request, timeout=30) as response:
return response.read()
def _normalize_widget_sdk(data: bytes) -> bytes:
# Keep the vendored widget pointing to Telegram's OAuth host instead of the local origin.
text = data.decode("utf-8")
normalized = text.replace(
_UNPATCHED_WIDGET_ORIGIN_SNIPPET,
_PATCHED_WIDGET_ORIGIN_SNIPPET,
1,
)
return normalized.encode("utf-8")
def main() -> int:
parser = argparse.ArgumentParser(
description="Download the latest Telegram Login Widget SDK into the local templates directory."
)
parser.add_argument(
"--source-url",
default=SOURCE_URL,
help="Telegram Login Widget SDK URL to download from.",
)
parser.add_argument(
"--target",
type=Path,
default=TARGET_PATH,
help="Output path for the vendored SDK.",
)
args = parser.parse_args()
data = _normalize_widget_sdk(_download(args.source_url))
args.target.parent.mkdir(parents=True, exist_ok=True)
args.target.write_bytes(data)
print(f"Wrote {len(data)} bytes to {args.target}")
return 0
if __name__ == "__main__":
raise SystemExit(main())