Fix keyboard WebApp identity and sendData

This commit is contained in:
Codex 2026-09-03 19:32:44 +00:00
parent 727a9b3793
commit 96e7068755
4 changed files with 55 additions and 20 deletions

View file

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

View file

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

View file

@ -8,7 +8,7 @@
<title>Пожитки</title>
<link rel="stylesheet" href="./styles.css" />
<script src="./telegram-web-app.js"></script>
<script src="./app.js" defer></script>
<script src="./app.js?v=2" defer></script>
</head>
<body>
<main class="shell">

View file

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