diff --git a/app/bot.py b/app/bot.py index 55bfaa9..debc983 100644 --- a/app/bot.py +++ b/app/bot.py @@ -19,6 +19,10 @@ logger = logging.getLogger(__name__) CATEGORY_PATTERN = re.compile(r"^[^\x00-\x1f]{1,40}$") +def webapp_url_for_user(settings: Settings, user_id: int) -> str: + return f"{settings.webapp_url}?user_id={user_id}&mode=keyboard" + + def parse_transaction(raw: str) -> TransactionInput: try: payload: dict[str, Any] = json.loads(raw) @@ -65,12 +69,14 @@ class TelegramBotService: self.thread: Thread | None = None self._register_handlers() - def _keyboard(self) -> types.ReplyKeyboardMarkup: + def _keyboard(self, user_id: int) -> types.ReplyKeyboardMarkup: keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True) keyboard.add( types.KeyboardButton( text="Открыть Пожитки", - web_app=types.WebAppInfo(url=self.settings.webapp_url), + web_app=types.WebAppInfo( + url=webapp_url_for_user(self.settings, user_id) + ), ) ) return keyboard @@ -99,7 +105,7 @@ class TelegramBotService: prefix + "Пожитки считают доходы, расходы и процент накоплений. " "Нажмите кнопку ниже, чтобы открыть приложение.", - reply_markup=self._keyboard(), + reply_markup=self._keyboard(user.id), ) @self.bot.message_handler(content_types=["web_app_data"]) @@ -115,7 +121,7 @@ class TelegramBotService: self.bot.send_message( message.chat.id, f"Не получилось сохранить операцию: {exc}", - reply_markup=self._keyboard(), + reply_markup=self._keyboard(user.id), ) return @@ -123,15 +129,19 @@ class TelegramBotService: self.bot.send_message( message.chat.id, f"{operation} сохранён в категории «{item.category}». Откройте приложение снова, чтобы увидеть обновлённую статистику.", - reply_markup=self._keyboard(), + reply_markup=self._keyboard(user.id), ) @self.bot.message_handler(commands=["help"]) def help_message(message: types.Message) -> None: + user = message.from_user + if user is None: + return + self._save_user(user) self.bot.send_message( message.chat.id, "Добавляйте операции через WebApp. Ссылка «Стать друзьями» находится на вкладке рейтинга.", - reply_markup=self._keyboard(), + reply_markup=self._keyboard(user.id), ) def _save_user(self, user: types.User) -> None: diff --git a/static/app.js b/static/app.js index 211f3b8..4d6e021 100644 --- a/static/app.js +++ b/static/app.js @@ -5,7 +5,8 @@ const tgUser = tg?.initDataUnsafe?.user; const query = new URLSearchParams(window.location.search); const previewUserId = Number(query.get("user_id")) || 0; - const user = tgUser || (previewUserId ? { id: previewUserId, first_name: "Предпросмотр" } : null); + const keyboardLaunch = query.get("mode") === "keyboard"; + const user = tgUser || (previewUserId ? { id: previewUserId } : null); const userId = Number(user?.id) || 0; const state = { kind: "income", board: "all", summary: null, config: null }; @@ -18,11 +19,14 @@ function initTelegram() { if (!tg) return; - tg.ready(); - tg.expand(); - tg.enableClosingConfirmation?.(); - if (tg.isVersionAtLeast?.("6.1")) tg.setHeaderColor("secondary_bg_color"); - if (tg.isVersionAtLeast?.("7.10")) tg.setBottomBarColor("secondary_bg_color"); + const safely = (action) => { + try { action(); } catch (error) { console.warn("Telegram WebApp method failed", error); } + }; + safely(() => tg.ready()); + safely(() => tg.expand()); + safely(() => tg.enableClosingConfirmation?.()); + if (tg.isVersionAtLeast?.("6.1")) safely(() => tg.setHeaderColor("secondary_bg_color")); + if (tg.isVersionAtLeast?.("7.10")) safely(() => tg.setBottomBarColor("secondary_bg_color")); } function showToast(message) { @@ -32,8 +36,10 @@ window.setTimeout(() => toast.classList.remove("show"), 2600); } - function setIdentity() { - const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ") || "Гость"; + function setIdentity(profile = user) { + const name = profile?.nickname + || [profile?.first_name, profile?.last_name].filter(Boolean).join(" ") + || (userId ? "Загрузка…" : "Откройте из бота"); $("#userName").textContent = name; $("#userInitial").textContent = name.slice(0, 1).toUpperCase(); } @@ -97,12 +103,18 @@ showToast("Запись получилась слишком длинной"); return; } - if (!tg?.sendData || !tg.initData) { - showToast("Сохранение доступно при открытии из Telegram"); + const isTelegramClient = Boolean(tg?.platform && tg.platform !== "unknown"); + if (!tg?.sendData || !isTelegramClient || !keyboardLaunch) { + showToast("Отправьте /start боту и откройте приложение кнопкой под сообщением"); return; } - tg.HapticFeedback?.notificationOccurred("success"); - tg.sendData(encoded); + try { + tg.HapticFeedback?.notificationOccurred("success"); + tg.sendData(encoded); + } catch (error) { + console.error("Telegram sendData failed", error); + showToast("Telegram не принял запись. Обновите приложение и попробуйте снова"); + } } async function fetchJson(url) { @@ -115,6 +127,7 @@ if (!userId) return; try { state.summary = await fetchJson(`../api/users/${userId}/summary`); + setIdentity(state.summary); renderSummary(state.summary); } catch (error) { console.warn("Summary is not available", error); diff --git a/static/index.html b/static/index.html index bb472c4..3f56ea5 100644 --- a/static/index.html +++ b/static/index.html @@ -8,7 +8,7 @@ Пожитки - +
diff --git a/tests/test_core.py b/tests/test_core.py index 00e661e..7ab764b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2,7 +2,8 @@ from pathlib import Path import pytest -from app.bot import parse_transaction +from app.bot import parse_transaction, webapp_url_for_user +from app.config import Settings from app.db import Database @@ -73,3 +74,14 @@ def test_filtered_leaderboard(tmp_path: Path) -> None: def test_rejects_invalid_webapp_payload(payload: str) -> None: with pytest.raises(ValueError): parse_transaction(payload) + + +def test_personalized_keyboard_webapp_url(tmp_path: Path) -> None: + settings = Settings( + telegram_token="token", + base_app_url="https://example.com/", + data_dir=tmp_path, + ) + assert webapp_url_for_user(settings, 123456) == ( + "https://example.com/app/?user_id=123456&mode=keyboard" + )