Fix keyboard WebApp identity and sendData
This commit is contained in:
parent
727a9b3793
commit
96e7068755
4 changed files with 55 additions and 20 deletions
22
app/bot.py
22
app/bot.py
|
|
@ -19,6 +19,10 @@ logger = logging.getLogger(__name__)
|
||||||
CATEGORY_PATTERN = re.compile(r"^[^\x00-\x1f]{1,40}$")
|
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:
|
def parse_transaction(raw: str) -> TransactionInput:
|
||||||
try:
|
try:
|
||||||
payload: dict[str, Any] = json.loads(raw)
|
payload: dict[str, Any] = json.loads(raw)
|
||||||
|
|
@ -65,12 +69,14 @@ class TelegramBotService:
|
||||||
self.thread: Thread | None = None
|
self.thread: Thread | None = None
|
||||||
self._register_handlers()
|
self._register_handlers()
|
||||||
|
|
||||||
def _keyboard(self) -> types.ReplyKeyboardMarkup:
|
def _keyboard(self, user_id: int) -> types.ReplyKeyboardMarkup:
|
||||||
keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
|
keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True)
|
||||||
keyboard.add(
|
keyboard.add(
|
||||||
types.KeyboardButton(
|
types.KeyboardButton(
|
||||||
text="Открыть Пожитки",
|
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
|
return keyboard
|
||||||
|
|
@ -99,7 +105,7 @@ class TelegramBotService:
|
||||||
prefix
|
prefix
|
||||||
+ "Пожитки считают доходы, расходы и процент накоплений. "
|
+ "Пожитки считают доходы, расходы и процент накоплений. "
|
||||||
"Нажмите кнопку ниже, чтобы открыть приложение.",
|
"Нажмите кнопку ниже, чтобы открыть приложение.",
|
||||||
reply_markup=self._keyboard(),
|
reply_markup=self._keyboard(user.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.bot.message_handler(content_types=["web_app_data"])
|
@self.bot.message_handler(content_types=["web_app_data"])
|
||||||
|
|
@ -115,7 +121,7 @@ class TelegramBotService:
|
||||||
self.bot.send_message(
|
self.bot.send_message(
|
||||||
message.chat.id,
|
message.chat.id,
|
||||||
f"Не получилось сохранить операцию: {exc}",
|
f"Не получилось сохранить операцию: {exc}",
|
||||||
reply_markup=self._keyboard(),
|
reply_markup=self._keyboard(user.id),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -123,15 +129,19 @@ class TelegramBotService:
|
||||||
self.bot.send_message(
|
self.bot.send_message(
|
||||||
message.chat.id,
|
message.chat.id,
|
||||||
f"{operation} сохранён в категории «{item.category}». Откройте приложение снова, чтобы увидеть обновлённую статистику.",
|
f"{operation} сохранён в категории «{item.category}». Откройте приложение снова, чтобы увидеть обновлённую статистику.",
|
||||||
reply_markup=self._keyboard(),
|
reply_markup=self._keyboard(user.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
@self.bot.message_handler(commands=["help"])
|
@self.bot.message_handler(commands=["help"])
|
||||||
def help_message(message: types.Message) -> None:
|
def help_message(message: types.Message) -> None:
|
||||||
|
user = message.from_user
|
||||||
|
if user is None:
|
||||||
|
return
|
||||||
|
self._save_user(user)
|
||||||
self.bot.send_message(
|
self.bot.send_message(
|
||||||
message.chat.id,
|
message.chat.id,
|
||||||
"Добавляйте операции через WebApp. Ссылка «Стать друзьями» находится на вкладке рейтинга.",
|
"Добавляйте операции через WebApp. Ссылка «Стать друзьями» находится на вкладке рейтинга.",
|
||||||
reply_markup=self._keyboard(),
|
reply_markup=self._keyboard(user.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _save_user(self, user: types.User) -> None:
|
def _save_user(self, user: types.User) -> None:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
const tgUser = tg?.initDataUnsafe?.user;
|
const tgUser = tg?.initDataUnsafe?.user;
|
||||||
const query = new URLSearchParams(window.location.search);
|
const query = new URLSearchParams(window.location.search);
|
||||||
const previewUserId = Number(query.get("user_id")) || 0;
|
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 userId = Number(user?.id) || 0;
|
||||||
|
|
||||||
const state = { kind: "income", board: "all", summary: null, config: null };
|
const state = { kind: "income", board: "all", summary: null, config: null };
|
||||||
|
|
@ -18,11 +19,14 @@
|
||||||
|
|
||||||
function initTelegram() {
|
function initTelegram() {
|
||||||
if (!tg) return;
|
if (!tg) return;
|
||||||
tg.ready();
|
const safely = (action) => {
|
||||||
tg.expand();
|
try { action(); } catch (error) { console.warn("Telegram WebApp method failed", error); }
|
||||||
tg.enableClosingConfirmation?.();
|
};
|
||||||
if (tg.isVersionAtLeast?.("6.1")) tg.setHeaderColor("secondary_bg_color");
|
safely(() => tg.ready());
|
||||||
if (tg.isVersionAtLeast?.("7.10")) tg.setBottomBarColor("secondary_bg_color");
|
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) {
|
function showToast(message) {
|
||||||
|
|
@ -32,8 +36,10 @@
|
||||||
window.setTimeout(() => toast.classList.remove("show"), 2600);
|
window.setTimeout(() => toast.classList.remove("show"), 2600);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setIdentity() {
|
function setIdentity(profile = user) {
|
||||||
const name = [user?.first_name, user?.last_name].filter(Boolean).join(" ") || "Гость";
|
const name = profile?.nickname
|
||||||
|
|| [profile?.first_name, profile?.last_name].filter(Boolean).join(" ")
|
||||||
|
|| (userId ? "Загрузка…" : "Откройте из бота");
|
||||||
$("#userName").textContent = name;
|
$("#userName").textContent = name;
|
||||||
$("#userInitial").textContent = name.slice(0, 1).toUpperCase();
|
$("#userInitial").textContent = name.slice(0, 1).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
@ -97,12 +103,18 @@
|
||||||
showToast("Запись получилась слишком длинной");
|
showToast("Запись получилась слишком длинной");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!tg?.sendData || !tg.initData) {
|
const isTelegramClient = Boolean(tg?.platform && tg.platform !== "unknown");
|
||||||
showToast("Сохранение доступно при открытии из Telegram");
|
if (!tg?.sendData || !isTelegramClient || !keyboardLaunch) {
|
||||||
|
showToast("Отправьте /start боту и откройте приложение кнопкой под сообщением");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tg.HapticFeedback?.notificationOccurred("success");
|
try {
|
||||||
tg.sendData(encoded);
|
tg.HapticFeedback?.notificationOccurred("success");
|
||||||
|
tg.sendData(encoded);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Telegram sendData failed", error);
|
||||||
|
showToast("Telegram не принял запись. Обновите приложение и попробуйте снова");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJson(url) {
|
async function fetchJson(url) {
|
||||||
|
|
@ -115,6 +127,7 @@
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
try {
|
try {
|
||||||
state.summary = await fetchJson(`../api/users/${userId}/summary`);
|
state.summary = await fetchJson(`../api/users/${userId}/summary`);
|
||||||
|
setIdentity(state.summary);
|
||||||
renderSummary(state.summary);
|
renderSummary(state.summary);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("Summary is not available", error);
|
console.warn("Summary is not available", error);
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<title>Пожитки</title>
|
<title>Пожитки</title>
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
<script src="./telegram-web-app.js"></script>
|
<script src="./telegram-web-app.js"></script>
|
||||||
<script src="./app.js" defer></script>
|
<script src="./app.js?v=2" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="shell">
|
<main class="shell">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
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
|
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:
|
def test_rejects_invalid_webapp_payload(payload: str) -> None:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
parse_transaction(payload)
|
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"
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue