Пишем WebApp в Telegram

Привет, vc.ru! Telegram WebApps существуют уже пару лет, но многие разработчики (неясно почему) до сих пор делают ботов по старинке - через бесконечные иерархии кнопок и команду /start.

Недавно я разрабатывал WhitePrism VPN - сервис, где, как вам несложно догадаться, важен удобный UI. Пользователь не должен гадать, какую кнопку нажать, чтобы просто включить VPN или продлить подписку. В этой статье я расскажу, как я упаковал полноценный SPA (Single Page Application) внутрь Telegram, прикрутил оплату Telegram Stars и почему это на голову выше обычных ботов.

Зачем вообще WebApp?

Обычный бот - это диалог. WebApp - это приложение. Разница в UX колоссальная. В обычном боте каждое действие создает новое сообщение, засоряя чат, навигация всегда сводится к кнопкам «Назад», а оплата часто выкидывает пользователя во внешнюю платежную систему.

В WebApp интерфейс меняется мгновенно без спама, доступен полноценный HTML/CSS/JS, навигация привычная (табы, модальные окна), а оплата происходит в два клика, однако из-за ограничений Telegram все же перенаправляет в сам чат бота для оплаты.

Технический стек

Я не стал усложнять и взял классический (как я называю, деревенский) стек, который идеально подходит для MVP и средних нагрузок:

  • Язык: Python 3.10+
  • Бот: pyTelegramBotAPI (Telebot)
  • Бэкенд: Flask (для обработки API запросов от WebApp)
  • База: SQLite
  • VPN ядро: Marzban (управление протоколами VLESS/Reality)
  • Фронтенд: Vanilla JS + HTML/CSS

Я не парился и все сунул в один код...

Одна из фишек моего подхода - я отдаю HTML-страницу прямо из Python-кода (переменная WEBAPP_HTML). Это может показаться странным для такого то проекта, но для деплоя бота одной кнопкой - идеально. К тому же, это защищает от простых атак: нельзя просто так открыть ссылку в браузере и увидеть интерфейс, не пройдя валидацию Telegram(про защиту могу написать отдельную статью).

Пользователь нажимает кнопку «Открыть WebApp», Telegram открывает встроенный браузер, и фронтенд отправляет initData (подписанные данные от Telegram) на наш Flask-бэкенд. Бэкенд проверяет криптографическую подпись (HMAC SHA-256). Без нее никак.

Важный момент по безопасности: Никогда не верьте данным, которые приходят с фронтенда без проверки. Любой школьник может открыть консоль браузера и отправить запрос «начисли мне миллион». Я использую валидацию initData (как раз про нее я и говорил): берем хеш, который прислал Telegram, и сверяем его с хешем, который генерируем сами на основе токена бота. Если они совпадают - значит, запрос реальный.

def validate_init_data(init_data):
parsed = dict(parse_qsl(init_data))
check_hash = parsed.pop("hash")
# Сортируем и подписываем данные своим токеном
data_str = "\n".join(f"{k}={v}" for k, v in sorted(parsed.items()))
secret = hmac.new(b"WebAppData", BOT_TOKEN.encode(), hashlib.sha256).digest()
calc_hash = hmac.new(secret, data_str.encode(), hashlib.sha256).hexdigest()
return json.loads(parsed['user'])['id'] if calc_hash == check_hash else None

Оплата через Telegram Stars (XTR)

Здесь я наступил на грабли, о которых мало пишут. В старых гайдах по платежам (Payments 2.0) требовался параметр payload и токен провайдера (например, ЮКассы). Косяк лично мой, читал левую справку. Для Stars всё иначе:

  1. Provider Token должен быть пустой строкой "".
  2. Валюта строго "XTR".
  3. Параметр payload в новых версиях библиотек (например, Telebot 4.x) заменен на invoice_payload - из-за этого код просто падает с ошибкой.

Правильный код создания инвойса для Звезд выглядит так:

bot.send_invoice(
chat_id=user_id,
title=f"Пополнение {stars}★",
description="Оплата подписки VPN",
invoice_payload=f"topup_{user_id}_{stars}", # Наши внутренние данные
provider_token="", # ВАЖНО: Для Stars оставляем пустым!
currency="XTR",
prices=[types.LabeledPrice(label=f"{stars} Stars", amount=stars)]
)

Интеграция с VPN (Marzban)

Для управления серверами я использую панель Marzban, у которой отличный API. Когда пользователь покупает подписку в боте, скрипт стучится в Marzban, создает юзера (или продлевает срок действия), генерирует новую ссылку vless://... и подписку и QR-код. QR-код мы генерируем прямо в Python (библиотека qrcode) и отдаем в WebApp в формате base64. Пользователю остается только навести камеру другого телефона для импорта в V2RayTun.

Итоги

Переход на WebApp дал неожиданные результаты. Во-первых, выросла конверсия в покупку - визуальный выбор тарифов и «прозрачная» оплата Звездами работают лучше, чем сухой текст. Во-вторых, снизилась нагрузка на поддержку: в WebApp мы добавили статус подключения (Online/Offline) и дату окончания, поэтому вопросы «а когда у меня кончится подписка?» исчезли, да хотя их и не было особо. Ну и, конечно, работает вау-эффект (немножко).

На этом все!

2
Начать дискуссию