Вебаппки «Телеграма» пишутся проще ботов — показываю (20 минут — на развёртывание)

Как только мы позвали вас, хабраюзеров, в бету ковырять наши контейнеры, выяснилось, что вам они очень часто нужны для телеграм-ботов и телеграмных же вебаппов. Потому что контейнер — идеальное размещение для такого: не надо много ресурсов, а нужно, чтобы бот висел и иногда отвечал на запросы, но при этом был готов выдерживать очень большое число запросов, если нам повезёт и он завирусится. Сам бэк тарифицируется по фактическому потреблению, то есть вполне спокойно это могут быть и 100 рублей в месяц за сервер с 2 vCPU и 4 Gb оперативки.
Показываю, как это происходит, и в конце сразу приложу готовый шаблон webapp для «Телеграма», который можно почти бесплатно запустить за пару минут.
На всякий случай напомню, что мы строим контейнерный хостинг, поэтому такое его применение кажется нам интересным кейсом, а главное, даёт преимущества которых невозможно найти у других. Сам кейс является начальным уровнем того, на что способна платформа.
Начну с того, что в «Телеграме» можно сделать webapp-приложение. По сути, это вебвьюха встроенная в мессенджер. Весь интерфейс можно делать точно так же, как на веб-фронте, на любом фреймворке (react, angular, vue…), то есть не ограничиваясь возможностями клиента. А главное, можно юзать API телефона и использовать авторизацию «Телеграма».
Немного — про архитектуру
Фронт — вебвью на NextJS. Бэк — rest API. База — Postgres. Взаимодействуют между собой, как обычное веб-приложение на базе трёх контейнеров. Телеграм-клиент взаимодействует только с фронтендом, выступая в роли оболочки и браузера. В отличие от обычного телеграм-бота меньше танцев с бубном при подключении, меньше геморроя с созданием интерфейса. Можем создать любой интерфейс для своего приложения. Деплоить всё будем в L1veStack — это наш контейнерный хостинг, который сегодня вышел в публичную бету, т. е. можно зайти и попробовать.
Забегая вперёд, скажу, что каркас бэка сделали тупо нейронкой, чтобы просто запустить приложение и оно работало, а фронт написали на NextJS.
В отличие от ботов, которые взаимодействуют через API «Телеграма» и могут тормозить при большой нагрузке, аппки работают с внешними вычислительными мощностями (то есть в данной ситуации — с вашими контейнерами). Это значит, что если у вас нет ограничений на потребление, то контейнер может масштабироваться так, как вам надо, и взаимодействие не будет лагать.
Естественно, если вам вдруг станет дешевле перейти с контейнера на обычную VPS-машину, то мы об этом предупредим.
Телеграмовские вебаппы на практике уже выдерживают миллионы пользователей. Справиться с нагрузкой — это на нашей стороне.
Теперь — про связку с «Телеграмом». В отличие от простого сайта, который открывается во фрейме, в webapp мы из коробки получаем авторизацию и работу с устройством пользователя. Например, сайт не может повибрировать вибромотором телефона, а вот приложение может. Соответственно, ваш бэк может сказать телеграм-клиенту, что пора помурчать, и телефон это сделает.
Чтобы отображать все пользовательские данные в нашем приложении, мы можем получить от «Телеграма» объект самого пользователя и объект «Телеграма», в котором будет доступ к функциям приложения. Сейчас всё это делается посредством использования библиотеки, но можно делать напрямую официальный скрипт.
Что делает наш бот из примера
Это опросник с промокодом в конце. Мы поняли, что хотим спрашивать пользователей, какие фичи им интересны в первую очередь, а взамен давать промокоды для пополнения баланса в L1vestack. Зарисовали — пошли делать…
Далее разберём особенности бэкенда и фронтенда, а потом по шагам распишем, как это всё запускается в контейнерах, минут за пять должны справиться.
Бэкенд
Если упрощённо, то для нашего бота понадобится бэкенд на шесть эндпоинтов (базовый каркас):
- POST /user
- GET /survey
- GET /survey/{surveyId}/
- POST /survey/{surveyId}/answer
- GET /code/{surveyId}
- POST /code/activate
Тут мы решили:, а почему бы не написать бэк на Скале? Не будем нанимать очень дорогого программиста и сгенерируем бэкенд с помощью нейронки (да простят меня Скала-разработчики, но даже при нашей зарплатной политике ваши требования по ЗП оставили шрамы отпечаток в моей душе).
Генерируем каркас на основе промпта
Write an OpenAPI specification for REST API of Survey service. The purpose of this service is provide back-end for Survey Telegram Mini App. In this app users can answer a question of survey and get a reward — promo-code, which can be activated in service and take discounts.
Methods:
- POST /user - Create new user
- GET /survey - List Active Surveys, available for User, according to Display Survey Logic. Return fields:id, title, description, progress. Progress is a calculated field for user.
- GET /survey/{surveyId}/ - View surveys fields, including questions and answer variants with
users answers (if provided).
- POST /survey/{surveyId}/answer - Accept users answer for Surveys question in JSON format
with two fields: questionId and answerVariantId.
- GET /code/{surveyId} - View promo-code, given to user as reward for completeion of survey.
- POST /code/activate - Method for external service to check promo-code validity and mark it as used (if vlid) or return an error, if not valid.
Data Structures (Complete):
- User: id, firstName, lastName, languageCode, profilePicture, username, allowsWriteToPM, creationDate
- Surevey: id, title, description, creationDate, isActive, codeValidEndDate
- Questions: id, surveyId, title, description, index- AnswerVariants: id, questionId, lable, value- Answers: id, userId, questionId, answerId, timestamp- Codes: id, userId, surveyId, isUse
Display Survey Logic:
There are many surveys in system. Some of the can be not Active (isActive: false). Application show to user all active surveys, and only not active surveys satisfying following conditions:- user completed this survey
- AND user’s code for this survey is not used. For active Surveys Progress fields shows percents of answered questions as Int value: 0 for new (not touched) survey, 100 for completed survey.
Получаем спецификацию OpenAPI. После небольших коррективов отдаём её на вход в следующем промпте:
Write complete code of Scala webserver based on Pekko HTTP with Slick PostgreSQL, providing REST API for Telegram Survey Mini App and satisfying following OpenAPI specification: …
На выходе получаем код, который после двух (!) итераций «ошибка — исправление» компилируется и работает.
Структура проекта:
Код — в одном файле (удобно для итераций исправления):
Бонус — методы для проверки (хотя можно попросить написать полноценные Spec-тесты или даже сначала написать тесты, а потом — код):
Нейронка отлично справилась с созданием каркаса нашего бэкенд-приложения. А финальную логику можно будет прокачать позже. Как это обычно бывает, концепт ещё предстоит проверить на жизнеспособность и получить апрув на продолжение разработки. Поэтому на старте главное — получить работоспособный API-каркас с возможностью расширения.
Добавляем Dockerfile и docker-compose.yaml. Для того чтобы сразу проверить наш бэкенд и обеспечить его работу у фронтендера, добавляем обвязку в виде Dockerfile и Docker compose-файлов.
Эти файлы также легко генерируются нейронкой и требуют только финальной отладки и проверки.
Вот результаты.
Фронтенд — вебапп
В лучших традициях рисования совы берём NextJS и получаем приложение. Как показывает опыт, по шагам повторять действия никто не будет, поэтому вот краткая памятка. А вот — репозиторий.
Примеры кода и лайфхаки
1. Поскольку приложение спроектировано так, чтобы контент не превышал высоту экрана, фиксируем экран для удобного взаимодействия при свайпах:
// layout.tsx
{children}
2. Инициализируем функции, которые предоставляет Telegram, в файле init.tsx:
import {backButton, viewport, miniApp, initData, $debug, init as initSDK, swipeBehavior, themeParams} from '@telegram-apps/sdk-react';
/** * Инициализируем приложение и настраиваем зависимости. */
export async function init(debug: boolean): Promise {
// Устанавливает режим отладки для @telegram-apps/sdk-react.
$debug.set(debug);
// Инициализируем специальные обработчики событий для Telegram Desktop, Android, iOS и т. д.
// Также настраиваем пакет.
initSDK();
// Кнопка назад.
if (backButton.isSupported()) {
backButton.mount();
}
// Определяем CSS-переменные, связанные с компонентами.
if (!miniApp.isMounted()) {
miniApp.mount();
}
// Создаём CSS-переменные вроде:
// --tg-bg-color: #aabbcc
// --tg-header-color: #aabbcc
if (miniApp.bindCssVars.isAvailable()) {
miniApp.bindCssVars();
}
if (!themeParams.isMounted()) {
themeParams.mount();
}
// Создаём CSS-переменные вроде:
// --tg-theme-button-color: #aabbcc
// --tg-theme-accent-text-color: #aabbcc
// --tg-theme-bg-color: #aabbcc
if (themeParams.bindCssVars.isAvailable()) {
themeParams.bindCssVars();
}
if (!viewport.isMounted() && !viewport.isMounting()) {
void viewport.mount().catch((e) => {
console.error("Something went wrong mounting the viewport", e);
}).then(() => {
// Переводим приложение в полноэкранный режим, если это возможно.
if (viewport.requestFullscreen.isSupported() && viewport.requestFullscreen.isAvailable() && !viewport.isFullscreen()) viewport.requestFullscreen() });
}
// Создаём CSS-переменные вроде:
// --tg-viewport-height: 675px
// --tg-viewport-width: 320px
// --tg-viewport-stable-height: 675px
if (viewport.bindCssVars.isAvailable()) {
viewport.bindCssVars();
}
// Отключаем вертикальные свайпы (чтобы пользователь случайно не свернул приложение)
if (!swipeBehavior.isMounted()) {
swipeBehavior.mount();
}
if (swipeBehavior.disableVertical.isAvailable()) {
swipeBehavior.disableVertical();
}
// Восстанавливаем initData
initData.restore();
}
3. Хендлер для управления кнопкой «Назад»:
'use client';
import { backButton } from '@telegram-apps/sdk-react';
import { PropsWithChildren, useEffect } from 'react';
import {useRouter} from "next/navigation";
export function BackButtonHandler({ children, back = true }: PropsWithChildren<{
/**
* True, если нужно отображать кнопку, иначе — false.
*/
back?: boolean
}>) {
const router = useRouter()
useEffect(() => {
if (back) {
backButton.show();
return backButton.onClick(() => {
router.back();
});
}
backButton.hide();
}, [back, router]);
return children;
}
Сборка и запуск
Сборка
Создадим новый приватный репозиторий на github. С большой долей вероятности вы не хотите делать своего бота опенсорсным, поэтому рассмотрим вариант с приватным репозиторием:
Склонируем себе полученный репозиторий и начнём работать.
Создадим директорию containers в корне проекта.
Скопируем себе код из репозитория бэкенда и удалим в ней директорию .git, чтобы не создавать субрепозиторий
```
git clone --depth=1 --branch=main https://github.com/H3LLO-CLOUD/survey-bot-openai-o1pro.git ./containers/backend
rm -rf ./containers/backend/.git
``
Скопируем себе код из репозитория фронта и тоже удалим в ней директорию .git, чтобы не создавать субрепозиторий
```
git clone --depth=1 --branch=main git@github.com:H3LLO-CLOUD/survey-bot-webapp.git ./containers/app
rm -rf ./containers/app/.git
```
Подобный метод может создать проблемы в продуктах JetBrains из-за автоматического обнаружения субрепозиториев. Просто надо удалить записи о новых репозиториях тут: Settings | Version Control | Directory Mappings.
Частично используем docker-compose.yaml из репозитория бэкенда и добавляем к нему фронтенд. В итоге получим следующее содержимое файла docker-compose.yml
```
services:
app:
build:
context: ./containers/app
dockerfile: Dockerfile
target: production
environment:
NEXT_PUBLIC_API_URL: http://backend:8080
ports:
- "80:3000"
backend:
build:
context: ./containers/backend
dockerfile: Dockerfile
target: production
environment:
POSTGRES_URL: "jdbc:postgresql://postgres:5432/survey_bot_db"
POSTGRES_USER: "survey_bot_user"
POSTGRES_PASSWORD: "L2eVC5aTUB1tzni3"
ports:
- "8080:8080"
postgres:
image: postgres:17.2
environment:
POSTGRES_DB: "survey_bot_db"
POSTGRES_USER: "survey_bot_user"
POSTGRES_PASSWORD: "L2eVC5aTUB1tzni3"
volumes:
- .:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
```
Теперь, нам нужно собирать наши контейнеры в образы. Будем делать это автоматически при пуше в репозиторий. Для этого создадим файл ./.github/workflows/action.yml
name: Publish branch image
on:
push:
branches:
- main # Собираем при пуше в main ветку
env:
GHCR_REGISTRY_HOST: ghcr.io
GHCR_REGISTRY_USERNAME: ${{ secrets.PACKAGES_USER }}
GHCR_REGISTRY_PASSWORD: ${{ secrets.PACKAGES_TOKEN }}
jobs:
create-latest-image:
name: Create branch docker image
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Create tag image
run: |
echo "GHCR_IMAGE_APP=${{ env.GHCR_REGISTRY_HOST }}/${GITHUB_REPOSITORY,,}-app:${GITHUB_REF##*/}" >> ${GITHUB_ENV}
echo "GHCR_IMAGE_BACKEND=${{ env.GHCR_REGISTRY_HOST }}/${GITHUB_REPOSITORY,,}-backend:${GITHUB_REF##*/}" >> ${GITHUB_ENV}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
-
name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.GHCR_REGISTRY_HOST }}
username: ${{ env.GHCR_REGISTRY_USERNAME }}
password: ${{ env.GHCR_REGISTRY_PASSWORD }}
-
# ${login}/${repository-name}-app
name: Build and push app
uses: docker/build-push-action@v4
with:
context: ./containers/app
platforms: linux/amd64
# platforms: linux/amd64,linux/arm64
push: true
# ${login}/${repository-name}-app
tags: |
${{ env.GHCR_IMAGE_APP }}
-
name: Build and push backend
uses: docker/build-push-action@v4
with:
context: ./containers/backend
platforms: linux/amd64
# platforms: linux/amd64,linux/arm64
push: true
# ${login}/${repository-name}-backend
tags: |
${{ env.GHCR_IMAGE_BACKEND }}
По идее всё должно пройти хорошо.
Чтобы всё заработало, нам нужно создать Personal Access Token в github на чтение и отдельный токен — на запись образов.
github.com/settings/tokens
New personal access token (classic)
Токен с правами write: packages используем в github actions.
1. Создаём имя токена:
2. Устанавливаем права:
3. Нажимаем «Создать токен»:
4. В репозитории в разделе github.com//settings/secrets/actions
создаём переменные:
PACKAGES_USER: "github login”
PACKAGES_TOKEN: "созданный токен”

Всё, теперь можно запушить проект и посмотреть образы в packages.
Берём адреса наших образов:
- ghcr.io/rieset/bot-app: main
- ghcr.io/rieset/bot-backend: main
Создаём токен на чтение образов, как в предыдущем шаге:
Добавляем инструкцию соединения с приватным docker registry (это чисто фича L1veStack, чтобы он мог забирать образы из приватных реп)
```
x-registry:
"ghcr.io":
username:
Password:
```
Итоговый файл docker-compose будет выглядеть так
services:
app:
image: ghcr.io//bot-app:main
build:
context: ./containers/app
dockerfile: Dockerfile
target: production
environment:
NEXT_PUBLIC_API_URL: http://backend:8080
ports:
- "80:3000"
backend:
image: ghcr.io//bot-backend:main
build:
context: ./containers/backend
dockerfile: Dockerfile
target: production
environment:
POSTGRES_URL: "jdbc:postgresql://postgres:5432/survey_bot_db"
POSTGRES_USER: "survey_bot_user"
POSTGRES_PASSWORD: "L2eVC5aTUB1tzni3"
ports:
- "8080:8080"
postgres:
image: postgres:17.2
environment:
POSTGRES_DB: "survey_bot_db"
POSTGRES_USER: "survey_bot_user"
POSTGRES_PASSWORD: "L2eVC5aTUB1tzni3"
volumes:
- .:/docker-entrypoint-initdb.d
ports:
- "5432:5432"
x-registry:
"ghcr.io":
username:
password:
```
И самое интересное — деплой. В другой ситуации здесь должна быть длинная простыня про запуск виртуалки, прописывание DNS, делегирование домена, установку зависимостей и т. д., но мы запилили L1veStack, и теперь деплоить можно вот так:
Собственно бот
- Создаём бота через botfather.
- Создаём вебапп через него же и указываем ссылку, полученную в l1vestack.ru.
FAQ
Какая библиотека используется для связи с «Телеграмом»?
Вот пакет для реакта, но есть и другие.
Где смотреть документацию по вебаппкам для Telegram?
core.telegram.org/bots/webapps
Где ссылка на бот?
t.me/h3PollBot
Буду признателен за обратную связь.
Что делает этот бот?
Опрос. За прохождение даёт купон на бонусы и скидки.
Что нужно сделать в самой «телеге», чтобы вебаппка заработала?
Есть стартовый шаблон, его нужно скопировать с гита и просто запустить. Там сразу будет небольшой проект, который демонстрирует возможности «Телеграма».
Где код?
- Ссылка на инструкцию webapp — teletype.in/@nvmxre/webapp-guide
- Ссылка на фронт — github.com/H3LLO-CLOUD/survey-bot-webapp
- Ссылка на бэк — github.com/H3LLO-CLOUD/survey-bot-openai-o1pro
Как видите, можно довольно быстро развернуть вебаппку, которая будет использовать ресурсы только при конкретном обращении пользователя к ней. Кажется, что контейнеры идеально подходят для таких задач.