Привет, друзья! Ваш теплый отклик на мою прошлую статью о разработке Telegram-ботов с использованием технологии MiniApp вдохновил меня на создание нового проекта. Сегодня мы создадим Telegram-бота, который умеет:

  • Сканировать QR-коды в реальном времени через камеру.

  • Распознавать QR-коды на загруженных изображениях.

  • Создавать новые QR-коды с возможностью кастомизации.

Как и в прошлый раз, наш новый бот будет работать на базе FastAPI. Мы подробно рассмотрим настройку вебхуков на FastAPI для обеспечения быстрого и эффективного функционирования бота. Важное преимущество проекта — вся его логика будет реализована в рамках одного FastAPI-приложения, что значительно упрощает структуру и поддержку.

Для разработки бота мы будем использовать Aiogram 3, который отлично справляется с взаимодействием с API Telegram. Я покажу, как легко обмениваться информацией между WebApp и ботом без сложностей.

В конце статьи я продемонстрирую, как за несколько минут выполнить удалённый запуск бота на сервере. Для этого мы воспользуемся сервисом Amvera Cloud, который я выбрал по следующим причинам:

  1. Бесплатный HTTPS-домен. Для ботов, использующих вебхуки и MiniApp, HTTPS-домен необходим. На Amvera его можно получить всего за пару кликов, и он автоматически привяжется к вашему проекту.

  2. Быстрая и удобная развертка проекта. Для запуска нужно подготовить простой файл с инструкциями (буквально пять строк, где указывается версия Python и команда для запуска), а затем загрузить файлы проекта на сервис. После этого проект будет готов к запуску буквально за пару минут.

Для доставки файлов можно использовать веб-интерфейс Amvera или стандартные команды GIT. Читайте статью до конца, чтобы убедиться, что деплой за 5 минут — это реальность.

План проекта

Для удобства обозначим основные этапы создания нашего проекта:

  1. Подготовка. Создадим пустой проект, сгенерируем токен для бота, установим зависимости и настроим туннель для тестирования. Всё как обычно при разработке Telegram-ботов на вебхуках.

  2. Структурирование. Создадим структуру проекта с пустыми файлами, которые позже заполним кодом. Микросервисы вроде базы данных и миграций уберём, чтобы сосредоточиться на основной задаче. Если вас интересует интеграция базы данных с SQLAlchemy, обратитесь к моей предыдущей статье.

  3. Фронтенд. Займёмся разметкой (HTML + CSS), пока не углубляясь в логику на фронте (JavaScript). В этом нам поможет WebSimAI, который создаст базовую заготовку интерфейса, которую мы адаптируем и оживим с помощью JavaScript.

  4. Бэкенд бота. Напишем весь код бэкенда, который будет работать через вебхуки на FastAPI.

  5. API-методы. Опишем API-методы FastAPI для отправки QR-кодов в бота после их генерации в MiniApp и для передачи отсканированных данных (как с камеры, так и с загруженных изображений).

  6. Логика фронтенда. Реализуем логику фронтенда на JavaScript, подключив библиотеки для генерации и сканирования QR-кодов. Основное внимание уделим методам взаимодействия с Telegram. Полный код проекта можно будет найти в моём Telegram-канале «Легкий путь в Python».

  7. Деплой проекта. Развернём проект на сервере Amvera Cloud.

Работы сегодня предостаточно. Приготовьте крепкий кофе, запустите своё любимое IDE и начнём!

Доступ к глобальной сети для разработки

Для разработки Telegram-бота с WebApp и вебхуками необходимо обеспечить приложению доступ к глобальной сети. Это можно сделать с помощью сервисов для создания туннелей. В качестве примера рассмотрим настройку туннеля с помощью Ngrok на Windows, но вы можете выбрать любой другой сервис, например, LocalTunnel, Xtunnel, Tuna и т.д.

Принцип работы туннеля следующий: мы запускаем FastAPI-приложение на локальном порту (например, 8000), а затем создаем туннель к этому порту. В результате мы получаем временный HTTPS-домен, который можно использовать в качестве основного домена для нашего приложения.

Шаги по настройке Ngrok

  1. Зарегистрируйтесь на сайте Ngrok и авторизуйтесь в своём аккаунте.

  2. Скачайте версию Ngrok для вашей операционной системы.

  3. Откройте скачанный файл и выполните команду для добавления токена авторизации:

          ngrok config add-authtoken ваш_токен
  4. Запустите туннель на нужный порт. Например, для порта 5050 выполните команду:

    ngrok http 8000
  5. Если всё сделано правильно, в окне терминала появится временный HTTPS-домен. Скопируйте эту ссылку — она понадобится для дальнейшей работы с ботом и вебхуками.

Пример ссылки на 8000-м порту
Пример ссылки на 8000-м порту

Создание Telegram-бота с привязкой к MiniApp

Создать Telegram-бота с привязкой к MiniApp и Ngrok-домену можно в несколько шагов:

1. Создание бота в Telegram

  • Откройте чат с BotFather в Telegram и отправьте команду /new_bot.

  • Придумайте имя для вашего бота (можно на русском языке).

  • Укажите логин бота — только латинские буквы, без пробелов и специальных символов. Логин должен заканчиваться на BOT, bot или Bot.

2. Активация MiniApp

  • Перейдите в список ботов и выберите вашего нового бота.

  • Откройте раздел SETTINGS (настройки) и найдите опцию Configure MiniApp.

  • Нажмите кнопку Enable MiniApp, чтобы активировать функцию.

  • Введите ссылку, сгенерированную Ngrok, в качестве временного URL для MiniApp. После деплоя мы заменим её на постоянный URL, который будет предоставлен Amvera.

3. Привязка MiniApp к командному меню (опционально)

  • Вернитесь в настройки бота и выберите раздел Menu Button.

  • Укажите текст кнопки, который будет отображаться в меню, и прикрепите ссылку на MiniApp. Это позволит пользователям быстро переходить на главную страницу вашего веб-приложения с командного меню. Я укажу "СКАНЕР".

Теперь ваш бот настроен, туннель через Ngrok создан, и у вас есть временный HTTPS-домен для использования в MiniApp и в веб-хуках.

Подготовка проекта

Теперь создадим новый проект в любимом IDE (в моём случае это PyCharm) и сразу добавим файлы requirements.txt и .env.

Файл requirements.txt

В файле requirements.txt пропишем следующие зависимости:

aiogram==3.13.1
fastapi==0.115.0
pydantic==2.9.2
uvicorn==0.31.0
jinja2==3.1.4
pydantic_settings==2.5.2

Краткое описание зависимостей:

  1. aiogram==3.13.1 — Библиотека для создания Telegram‑ботов на Python. Обеспечивает удобный интерфейс для взаимодействия с Telegram API и поддерживает вебхуки, поллинг, обработку сообщений и событий.

  2. fastapi==0.115.0 — Быстрый веб‑фреймворк для создания API на Python. Использует асинхронные функции и Pydantic для валидации данных, предоставляет простоту и скорость разработки веб‑приложений.

  3. pydantic==2.9.2 — Библиотека для проверки и сериализации данных. Она позволяет создавать схемы данных, проверять их валидность и конвертировать их в нужные форматы.

  4. uvicorn==0.31.0 — Высокопроизводительный ASGI‑сервер для запуска FastAPI‑приложений. Поддерживает асинхронные запросы и идеален для использования с FastAPI.

  5. jinja2==3.1.4 — Популярный шаблонизатор для Python, используемый для рендеринга HTML‑страниц. Применяется для генерации динамических веб‑страниц с использованием шаблонов.

  6. pydantic_settings==2.5.2 — Расширение для Pydantic, предоставляющее инструменты для управления конфигурациями приложений с использованием моделей и валидации. Удобно для работы с переменными окружения и конфигурационными файлами.

Обязательно создайте этот файл, так как по другому не получится выполнить деплой проекта.

Файл .env

Создайте файл .env в корневой директории вашего проекта и добавьте в него следующие переменные окружения:

BOT_TOKEN=7702023022:FakeToken
BASE_SITE=https://my_url.ru
ADMIN_ID=9124843734

Описание переменных:

  • BOT_TOKEN — Токен вашего Telegram-бота, который вы получили от BotFather. В примере используется фиктивный токен для разработки, но для реальной работы замените его на настоящий.

  • BASE_SITE — URL вашего веб-приложения. Это может быть домен, полученный через Ngrok, или ваш основной домен.

  • ADMIN_ID — ID администратора бота, который будет получать важные уведомления или иметь доступ к расширенным функциям. Например, админ может получать сообщения о том, что бот включился или отключился.

Загрузка переменных окружения

Эти переменные мы загрузим в наше приложение с помощью библиотеки pydantic-settings, что позволит легко управлять конфигурациями и использовать переменные окружения в коде.

Организация проекта

Приступим к организации проекта. Мы будем оформлять его как стандартное FastAPI-приложение, в котором одним из микросервисов будет выступать наш бот. В корне проекта создадим папку app, где разместим все файлы приложения.

Шаг 1: Создание конфигурационного файла

Начнем с создания файла конфигурации. В папке app создаём файл config.py и заполняем его следующим образом:

import os
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    BOT_TOKEN: str
    BASE_SITE: str
    ADMIN_ID: int

    model_config = SettingsConfigDict(
        env_file=os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".env")
    )

    def get_webhook_url(self) -> str:
        """Возвращает URL вебхука с кодированием специальных символов."""
        return f"{self.BASE_SITE}/webhook"

settings = Settings()

Описание кода:

  1. Settings-класс — Мы создаем класс Settings, который наследуется от BaseSettings (из pydantic-settings). Внутри класса определяем переменные окружения BOT_TOKEN, BASE_SITE и ADMIN_ID, которые подтягиваются из .env файла.

  2. Путь к .env — Для загрузки переменных окружения указываем путь к файлу .env, находящемуся в корне проекта.

  3. Метод get_webhook_url — В качестве примера добавлен метод для генерации URL вебхука с учётом заданного BASE_SITE.

  4. Экземпляр класса — Создаем объект settings, чтобы обращаться к переменным и методам класса через точку, что делает работу с конфигурацией более удобной.

Шаг 2: Подготовка структуры проекта

Теперь создадим файл main.py в папке app. Этот файл будет служить точкой входа для сборки и запуска FastAPI-приложения и Telegram-бота с поддержкой вебхуков. Пока оставим его пустым:

app/
    config.py
    main.py

Шаг 3: Создание необходимых директорий

В папке app создадим следующие папки:

  • api — здесь будут описаны API-методы приложения.

  • bot — микросервис для работы с Telegram-ботом.

  • pages — эндпоинты для рендеринга HTML-страниц MiniApp.

  • templates — HTML-шаблоны для генерации динамических страниц.

  • static — статические файлы (например, изображения, стили и JS-скрипты).

Структура проекта будет выглядеть так:

app/
    api/
    bot/
    pages/
    templates/
    static/
    config.py
    main.py

Шаг 4: Начало разработки микросервисов

Теперь, когда базовая структура проекта готова, начнем последовательно реализовывать наши микросервисы: API-методы, бота, страницы для MiniApp и шаблоны.

В следующем шаге мы займемся наполнением каждого из этих разделов кодом и функциональностью.

Пишем Aiogram-часть (бэкенд телеграмм бота)

Работать мы будем с папкой bot. Сразу внутри папки bot давайте создадим файл create_bot.py. В этом файле мы пропишем функции, которые будут вызываться при запуске бота, при его остановке, а также инициируем два самых важных объекта в Aiogram: объект бота и объект диспетчера.

from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode

from app.config import settings

bot = Bot(token=settings.BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dp = Dispatcher()

async def start_bot():
    try:
        await bot.send_message(settings.ADMIN_ID, 'Я запущен?.')
    except:
        pass

async def stop_bot():
    try:
        await bot.send_message(settings.ADMIN_ID, 'Бот остановлен. За что??')
    except:
        pass

Из дополнительного я добавил по умолчанию ParseMode.HTML, чтобы мы могли свободно форматировать сообщения в боте HTML-тегами.

Теперь в папке bot создадим папку keyboards, а внутри файл kbs.py. В этом файле опишем клавиатуру, которая будет использоваться в нашем боте. Да, клавиатура будет всего одна, просто потому что я решил не перегружать вас информацией сегодня.

from aiogram.types import WebAppInfo, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from app.config import settings

def main_keyboard() -> InlineKeyboardMarkup:
    generate_url = settings.BASE_SITE
    kb = InlineKeyboardBuilder()
    kb.button(text="? Сканировать", web_app=WebAppInfo(url=f"{generate_url}/scan"))
    kb.button(text="? Загрузить QR", web_app=WebAppInfo(url=f"{generate_url}/upload"))
    kb.button(text="✨ Создать QR", web_app=WebAppInfo(url=f"{generate_url}"))
    kb.adjust(1)
    return kb.as_markup()

Как вы видите, я использую инлайн-клавиатуру с тремя кнопками WebAppInfo. Первая будет запускать страничку для сканирования, вторая — для загрузки фото (для считывания QR-кода), а третья — для генерации QR-кода.

Особенности использования WebApp

Я использовал инлайн-клавиатуру потому, что при переходе в MiniApp с такой клавиатуры на странице MiniApp видна информация о пользователе, в частности — TelegramID (то, что нам нужно). Такая же логика будет работать при переходе в MiniApp с командного меню.

По непонятной мне логике, при переходе с текстовой кнопки на странице WebApp не видна информация о пользователе, но при этом появляется возможность «штатными» методами вернуть с MiniApp информацию в Telegram.

Что интересно, отправить информацию штатными способами при переходе с инлайн-клавиатуры или с командного меню обратно нельзя.

Таким образом, я принял решение: для входа в MiniApp используем инлайн-кнопки и командное меню, а для возврата данных в бота — методы FastAPI. Это получится элегантно и удобно.

Для тех, кому нужна информация о пользователе с текстовой клавиатуры

Если вам очень нужно получать информацию о пользователе с текстовой клавиатуры, есть один способ: передавать нужные данные (например, TelegramID) в качестве параметра запроса:

https://021f-80-85-143-232.ngrok-free.app?user_id=12345

Далее, на стороне MiniApp, мы можем извлекать значение нужной информации через параметр запроса:

<script src="https://telegram.org/js/telegram-web-app.js"></script>

<script>
async function getUserID() {
    const tg = window.Telegram.WebApp;
    if (tg.initDataUnsafe?.user?.id) {
        console.log("User data from Telegram:", JSON.stringify(tg.initDataUnsafe.user));
        return tg.initDataUnsafe.user.id;
    }

    const urlParams = new URLSearchParams(window.location.search);
    const userID = urlParams.get('user_id');
    if (userID) {
        console.log("User ID from URL:", userID);
        return userID;
    }

    throw new Error("User ID not available");
}
</script>
  • Сначала она пытается получить ID из объекта Telegram Web App, что работает для командного меню и инлайн‑кнопок.

  • Если первый метод не сработал, функция ищет параметр 'user_id' в URL, что позволяет передавать ID через текстовые кнопки (не забудьте только подставить параметр запроса, чтоб получилась ссылка такого вида: https://your_site.ru?user_id=12 345).

  • Если оба метода не дали результата, функция выбрасывает ошибку.

Но это мы отвлеклись. Продолжим.

Опишем хендлер бота

Теперь нам осталось описать хендлер (тоже один), который просто будет срабатывать при выполнении команды /start (например, при входе в бота). Для этого в папке bot создадим папку handlers и внутри нее файл user_router.py.

Заполним файл:

from aiogram import Router
from aiogram.filters import CommandStart
from aiogram.types import Message
from app.bot.keyboards.kbs import main_keyboard

user_router = Router()


@user_router.message(CommandStart())
async def cmd_start(message: Message) -> None:
    welcome_text = (
        "? Привет! Добро пожаловать в мир QR-кодов! ?\n\n"
        "Я ваш помощник по работе с QR-кодами. Вот что я умею:\n\n"
        "? Сканировать QR-коды в реальном времени\n"
        "? Распознавать QR-коды на загруженных фото\n"
        "✨ Создавать новые QR-коды\n\n"
        "Выберите нужную функцию в меню ниже и начните работу! ?"
    )
    await message.answer(welcome_text, reply_markup=main_keyboard())

Логика тут максимально простая. Мы вешаем роутер, а затем пишем обработчик команды /start, используя встроенные фильтры третьей версии aiogram.

Сама функция возвращает текст с короткой информацией и нашу клавиатуру.

Тут, конечно, все можно было писать в одном файле, например, в create_bot.py, но я решил для порядка показать вам как можно логически разбивать файлы бота внутри FastApi приложения (бывает файлов очень много).

Подключение в main-файл и запуск

Теперь подключим файлы бота в main.py и выполним первый запуск нашего бота.

Заполним файл main.py (в папке app):

import logging
from contextlib import asynccontextmanager
from app.bot.create_bot import bot, dp, stop_bot, start_bot
from app.bot.handlers.user_router import user_router
from app.config import settings
from fastapi.staticfiles import StaticFiles
from aiogram.types import Update
from fastapi import FastAPI, Request

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

@asynccontextmanager
async def lifespan(app: FastAPI):
    logging.info("Starting bot setup...")
    dp.include_router(user_router)
    await start_bot()
    webhook_url = settings.get_webhook_url()
    await bot.set_webhook(url=webhook_url,
                          allowed_updates=dp.resolve_used_update_types(),
                          drop_pending_updates=True)
    logging.info(f"Webhook set to {webhook_url}")
    yield
    logging.info("Shutting down bot...")
    await bot.delete_webhook()
    await stop_bot()
    logging.info("Webhook deleted")

app = FastAPI(lifespan=lifespan)

@app.post("/webhook")
async def webhook(request: Request) -> None:
    logging.info("Received webhook request")
    update = Update.model_validate(await request.json(), context={"bot": bot})
    await dp.feed_update(bot, update)
    logging.info("Update processed")

Логика работы

У FastAPI есть такая технология, как жизненный цикл (lifespan). Описывается он в эндпоинте:

@asynccontextmanager
async def lifespan(app: FastAPI)
  • До yield выполняется во время запуска, после — при завершении работы приложения.

Во время запуска приложения мы:

  1. Включаем user_router бота.

  2. Выполняем функцию, которая должна выполниться при запуске бота.

  3. Вешаем вебхук.

При остановке приложения:

  1. Удаляем вебхук.

  2. Выполняем функцию, которая должна выполниться при завершении работы бота.

Более подробно особенности связки жизненного цикла FastApi c Aiogram 3 для работы через вебхуки с MiniApp я рассказывал в статье "Telegram Web App, FastAPI и вебхуки в одном приложении: Создаем Telegram-бот с веб-интерфейсом для приема заявок".

Первый запуск

Теперь выполним первый запуск бота. Для этого с корня проекта (на один уровень выше app) выполним команду для запуска FastAPI через uvicorn:

uvicorn app.main:app --reload

После этого, если все прошло корректно, то вы должны будете получить сообщение о том что бот запущен.

После введем команду /start, чтоб проверить подгружается ли наше приветственное сообщение с клавиатурой.

 Не обращайте внимание на логотип бота, это я установил за кадром.
Не обращайте внимание на логотип бота, это я установил за кадром.

Надеюсь, что вы сейчас, как и я, видите кнопку «СКАНЕР» вместо командного меню, которую мы установили на этапе создания бота и инлайн клавиатуру с WebInfo кнопками. Если это так, то поздравляю. Двигаемся дальше!

Опишем API-методы

Теперь займемся небольшим блоком кода, а именно, подготовим два API-метода:

  • Метод для отправки сгенерированного QR-кода в Telegram пользователя

  • Метод для отправки отсканированной с QR-кода информации в Telegram пользователю

Для этого в папке app/api создадим два файла:

  • schemas.py: тут мы опишем простые Pydantic-схемы, чтобы у нас все было по фэн-шую

  • router.py: тут опишем наши API-методы (эндпоинты)

Начнем с моделей Pydantic или, как их в народе называют, со схем. Напоминаю, что недавно я написал большую и подробную статью про Pydantic 2, в которой я подробно рассмотрел, что это за технология такая и как с ней работать (и главное, зачем).

from pydantic import BaseModel

class QRBase(BaseModel):
    user_id: int

class QRCodeRequest(QRBase):
    qr_code_url: str

class QRCodeScanner(QRBase):
    result_scan: str

Очень простые схемы:

  • QRBase: тут я вынес универсальный аргумент – Telegram ID пользователя

  • QRCodeRequest: схема для эндпоинта по отправке сгенерированного QR-кода пользователю в Telegram

  • QRCodeScanner: схема для отправки отсканированной информации пользователю в Telegram

Теперь настроим импорты и роутер для наших API-методов:

import base64
from aiogram.types import BufferedInputFile
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse
from app.api.schemas import QRCodeRequest, QRCodeScanner
from app.bot.create_bot import bot
from app.bot.keyboards.kbs import main_keyboard
  • base64 — стандартный модуль для кодирования/декодирования данных в формате Base64 (преобразование изображения в строку и обратно)

  • BufferedInputFile — класс из aiogram для отправки файлов в Telegram, например, QR-кода в виде изображения

  • APIRouter, HTTPException — инструменты FastAPI для создания маршрутов и обработки ошибок

  • JSONResponse — возвращает ответы в формате JSON

  • QRCodeRequest, QRCodeScanner — Pydantic-схемы для обработки запросов с QR-кодами

  • bot — объект Telegram-бота для взаимодействия через Telegram API

  • main_keyboard — инлайн-клавиатура для взаимодействия с пользователями

Настраиваем роутер:

router = APIRouter(prefix='/api', tags=['API'])

APIRouter используется для организации маршрутов FastAPI. Здесь создается маршрутизатор с префиксом '/api' и тегом 'API', чтобы группировать связанные с API маршруты.

Код с отправкой QR-кодов в Telegram

Здесь мы реализуем два метода для отправки QR-кодов и результатов сканирования в Telegram через нашего бота.

Метод отправки QR-кода

Первый метод занимается отправкой сгенерированного QR-кода. Что происходит:

  1. Получаем base64 строку. Мы принимаем на вход base64 закодированное изображение. Это значит, что картинка передаётся не как файл, а как строка символов

  2. Чистим строку. Если в строке есть префикс data:image/ — удаляем его, оставляем только "чистое" изображение

  3. Декодируем base64 в байты, то есть превращаем строку обратно в картинку

  4. BufferedInputFile — используем этот класс, чтобы отправить изображение в Telegram в виде файла. В отличие от стандартного InputFile, который работает с обычными файлами, BufferedInputFile позволяет передавать данные в виде байтов

  5. Создаём сообщение с QR-кодом. Отправляем картинку с текстом, в котором говорим пользователю, что всё готово, и предлагаем ему отсканировать код

  6. Если что-то идёт не так — ловим ошибку и возвращаем соответствующий ответ

Вот как выглядит метод:

@router.post("/send-qr/", response_class=JSONResponse)
async def send_qr_code(request: QRCodeRequest):
    try:
        # Получаем base64 строку из запроса
        base64_data = request.qr_code_url

        # Удаляем префикс MIME, если он есть
        if base64_data.startswith('data:image/'):
            base64_data = base64_data.split(',', 1)[1]

        # Декодируем base64 изображение в байты
        image_data = base64.b64decode(base64_data)

        # Создаем BufferedInputFile для отправки изображения
        image_file = BufferedInputFile(file=image_data, filename="qr_code.png")

        caption = (
            "? Ваш QR-код успешно создан и отправлен!\n\n"
            "? Вы можете отсканировать его, чтобы проверить содержимое.\n"
            "? Поделитесь этим QR-кодом с другими или сохраните его для дальнейшего использования.\n\n"
            "Что бы вы хотели сделать дальше? ?"
        )

        # Используем BufferedInputFile для отправки изображения
        await bot.send_photo(
            chat_id=request.user_id,
            photo=image_file,  # Передаем BufferedInputFile
            caption=caption,
            reply_markup=main_keyboard()
        )

        return JSONResponse(content={"message": "QR-код успешно отправлен"}, status_code=200)
    except Exception as e:
        print(f"Ошибка при отправке QR-кода: {str(e)}")
        raise HTTPException(status_code=500, detail=f"Ошибка при отправке QR-кода: {str(e)}")

Метод отправки результата сканирования QR-кода

Второй метод просто отправляет результат сканирования QR-кода:

  1. Получаем результат сканирования. В result_scan приходит текст или ссылка, которые были зашифрованы в QR-коде

  2. Формируем сообщение. Отправляем результат пользователю, сообщая ему, что сканирование прошло успешно

  3. Отправляем сообщение с помощью bot.send_message()

@router.post("/send-scaner-info/", response_class=JSONResponse)
async def send_qr_code(request: QRCodeScanner):
    try:
        text = (
            f"? QR-код успешно отсканирован!\n\n"
            f"? Результат сканирования:\n\n"
            f"<code><b>{request.result_scan}</b></code>\n\n"
            f"? Если это ссылка, вы можете перейти по ней.\n"
            f"? Если это текст, вы можете скопировать его для дальнейшего использования.\n\n"
            f"Что бы вы хотели сделать дальше? ?"
        )
        await bot.send_message(chat_id=request.user_id, text=text, reply_markup=main_keyboard())
        return JSONResponse(content={"message": "QR-код успешно просканирован, а данные отправлены в Telegram"},
                          status_code=200)
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

Оба метода похожи по логике. Один работает с сгенерированным QR-кодом и отправляет его пользователю в виде картинки, а другой — с результатами сканирования, отправляя текст или ссылку.

Регистрация роутера и подключение статических файлов

Для работы методов необходимо зарегистрировать роутер в main.py файле:

from app.api.router import router as router_api

app.include_router(router_api)

Также подключим статические файлы:

from fastapi.staticfiles import StaticFiles

app.mount('/static', StaticFiles(directory='app/static'), name='static')

Этим мы создаём маршрут /static, который будет служить для доступа к статическим файлам, расположенным в директории app/static.

Готовим фронт (HTML + CSS)

Для того чтобы не тратить время на отрисовку макета в Figma с его последующей версткой, я просто воспользовался услугами бесплатного сервиса WebSimAI. Там я подготовил промпт (запрос), в результате которого получил примерно тот результат, который хотел.

После еще несколькими запросами я откорректировал фронт до состояния, с которым уже можно работать. Подробное описание этого сервиса с фишками по работе с ним я делал в этой статье.

К сожалению, тот JavaScript, который предложил мне WebSim, мне не подошел, и пришлось описывать свой, но до этого мы ещё дойдем. Пока разложим скачанный проект из WebSim на статические файлы стилей и HTML-шаблоны.

Работа с HTML-шаблонами

Давайте приступим к работе с HTML-шаблонами в папке app/templates. Для начала создадим универсальный шаблон base.html, который станет основой для всех остальных страниц. В этом шаблоне мы будем собирать общие элементы интерфейса, такие как шапка, подвал и общие стили, чтобы упростить дальнейшую разработку фронтенда.

Самая важная часть этого файла – это размещение там объекта Telegram, который после будет передаваться на все страницы приложения. Это позволит нам получать информацию о пользователе на какой бы странице тот ни находился (благодаря тому, что точки входа в MiniApp через командное меню и инлайн-кнопки).

app/templates/base.html

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Приложение QR-кодов{% endblock %}</title>
    <link rel="stylesheet" href="/static/style/style.css">
    <script src="https://telegram.org/js/telegram-web-app.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/easyqrcodejs@4.4.13/dist/easy.qrcode.min.js"></script>
    <script>
        const tg = window.Telegram.WebApp;
        tg.ready()
    </script>
    {% block extra_head %}{% endblock %}
</head>
<body>
<div class="container">
    {% block content %}{% endblock %}
</div>

<nav class="nav-bar">
    <a href="/" class="nav-item {% if active_tab == 'generate' %}active{% endif %}">
        <img src="/static/img/generate.svg" alt="Генерация" class="nav-icon">
        Генерация
    </a>
    <a href="/scan" class="nav-item {% if active_tab == 'scan' %}active{% endif %}">
        <img src="/static/img/scan.svg" alt="Сканировать" class="nav-icon">
        Сканировать
    </a>
    <a href="/upload" class="nav-item {% if active_tab == 'upload' %}active{% endif %}">
        <img src="/static/img/upload.svg" alt="Загрузить" class="nav-icon">
        Загрузить
    </a>
</nav>
{% block extra_scripts %}{% endblock %}
</body>
</html>

Давайте подробно остановимся на важных моментах.

JavaScript-библиотеки

В шаблоне используются три JavaScript-библиотеки:

  1. https://telegram.org/js/telegram-web-app.js — основная библиотека, которая превращает наше веб-приложение в Telegram-MiniApp. С её помощью мы получаем доступ к объекту window.Telegram.WebApp, что даёт возможность взаимодействовать с функционалом Telegram прямо в нашем приложении. Также эта библиотека подтягивает специфические стили для улучшения пользовательского интерфейса в рамках MiniApp.

  2. https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js — библиотека для сканирования QR-кодов. Она поддерживает сканирование как через камеру устройства в реальном времени, так и через загруженные изображения с QR-кодом.

  3. https://cdn.jsdelivr.net/npm/easyqrcodejs@4.4.13/dist/easy.qrcode.min.js — библиотека для генерации QR-кодов. Она гибкая и позволяет легко кастомизировать вид QR-кодов.

Инициализация Telegram WebApp

const tg = window.Telegram.WebApp;
tg.ready();
  1. Получаем объект Telegram WebApp:

    • Создаём переменную tg, которая ссылается на объект window.Telegram.WebApp.

    • Этот объект предоставляет доступ к API Telegram.

  2. Инициализируем приложение:

    • Метод tg.ready() сообщает приложению о готовности к работе.

    • Это обязательный шаг для корректного взаимодействия с MiniApp.

Стили и структура приложения

В заголовке также присутствует импорт стилей:

<link rel="stylesheet" href="/static/style/style.css">

Полный код стилей не рассматривается в данном материале из-за его объема. Полную версию кода, включая стили и JS-скрипты, можно найти в моем телеграм-канале «Легкий путь в Python», где также публикуется дополнительный контент, не представленный на Хабре.

Далее в коде используется стандартный синтаксис Jinja2 для разметки страницы, включающий блоки для:

  • Импорта JavaScript

  • Подключения стилей

  • Размещения основного контента

Навигационный блок

В приложении используется простой навигационный блок на основе <nav>-элемента:

<nav class="nav-bar">
    <a href="/" class="nav-item {% if active_tab == 'generate' %}active{% endif %}">
        <img src="/static/img/generate.svg" alt="Генерация" class="nav-icon">
        Генерация
    </a>
    <a href="/scan" class="nav-item {% if active_tab == 'scan' %}active{% endif %}">
        <img src="/static/img/scan.svg" alt="Сканировать" class="nav-icon">
        Сканировать
    </a>
    <a href="/upload" class="nav-item {% if active_tab == 'upload' %}active{% endif %}">
        <img src="/static/img/upload.svg" alt="Загрузить" class="nav-icon">
        Загрузить
    </a>
</nav>

Особенности навигации:

Навигационные ссылки

  • Страница генерации QR-кодов (URL: /)

  • Страница сканирования (URL: /scan)

  • Страница загрузки изображений с QR-кодом (URL: /upload)

Активный элемент

  • Используется условие if active_tab == '...'

  • При совпадении текущей страницы с active_tab добавляется класс active

  • Позволяет стилизовать активный элемент меню

  • Помогает пользователю ориентироваться на сайте

Иконки

  • В каждой ссылке присутствует SVG-иконка из директории /static/img/

  • Иконки визуально обозначают функционал: генерация, сканирование, загрузка

  • Делают интерфейс современным и интуитивно понятным

Организация шаблонов

Для удобства организации кода создана следующая структура:

  • В папке templates создана подпапка pages

  • HTML-шаблоны размещены внутри папки pages

  • Такая организация предотвращает смешивание базового шаблона base.html с обычными шаблонами

Страницы приложения

pages/generate.html

Скрытый текст
{% extends "base.html" %}

{% block title %}Создать QR-код{% endblock %}

{% block extra_head %}
<link rel="stylesheet" href="/static/style/style_form.css">
{% endblock %}

{% block content %}
<h2>Создать кастомный QR-код</h2>

<form id="qrForm">
    <div class="form-group">
        <label for="qrInput">Текст или URL:</label>
        <input type="text" id="qrInput" name="text" placeholder="Введите текст или URL" required>
    </div>

    <div class="form-group">
        <label for="qrSize">Размер QR-кода:</label>
        <input type="range" id="qrSize" name="width" min="100" max="400" value="256" step="10">
        <span id="qrSizeValue">256px</span>
    </div>

    <div class="form-group">
        <label for="colorDark">Цвет QR-кода:</label>
        <input type="color" id="colorDark" name="colorDark" value="#000000">
    </div>

    <div class="form-group">
        <label for="colorLight">Цвет фона:</label>
        <input type="color" id="colorLight" name="colorLight" value="#ffffff">
    </div>

    <div class="form-group">
        <label for="correctLevel">Уровень коррекции ошибок:</label>
        <select id="correctLevel" name="correctLevel">
            <option value="L">Низкий (7%)</option>
            <option value="M">Средний (15%)</option>
            <option value="Q">Квартиль (25%)</option>
            <option value="H">Высокий (30%)</option>
        </select>
    </div>

    <div class="form-group">
        <label for="dotScale">Размер точек:</label>
        <input type="range" id="dotScale" name="dotScale" min="0.1" max="1" value="1" step="0.1">
        <span id="dotScaleValue">1</span>
    </div>

    <div class="form-group">
        <label for="quietZone">Отступ (тихая зона):</label>
        <input type="number" id="quietZone" name="quietZone" min="0" max="100" value="10">
    </div>

    <div class="form-group">
        <label for="backgroundImage">Фоновое изображение:</label>
        <input type="file" id="backgroundImage" name="backgroundImage" accept="image/*">
    </div>

    <div class="form-group">
        <label for="backgroundImageAlpha">Прозрачность фона:</label>
        <input type="range" id="backgroundImageAlpha" name="backgroundImageAlpha" min="0" max="1" value="0.1"
               step="0.1">
        <span id="backgroundImageAlphaValue">0.1</span>
    </div>

    <button type="submit" class="generate-btn">Создать QR-код</button>
</form>

<div id="qrcode" class="qr-result"></div>
<div id="loader" style="display: none;">Генерация QR-кода...</div>
{% endblock %}

{% block extra_scripts %}
<script src="/static/js/generate.js"></script>
{% endblock %}

Под страницу формы генерации я прописал отдельные стили и вынес их в отдельный файл. Далее, несмотря на то что кода получилось достаточно много, он простой.

Технически, тут мы видим простое описание формы, которую я дополнительно стилизовал в файле style_form.css.

Единственное что тут интересно – это:

<div id="qrcode" class="qr-result"></div>
<div id="loader" style="display: none;">Генерация QR-кода...</div>

В этом месте HTML-шаблона будет отображаться сгенерированный QR-код с кнопкой отправки его в телеграмм (метод для отправки мы написали ранее) и загрузчик, пока генерируется QR-код. Основная магия будет происходить тут в файле generate.js, который мы разберем чуть позже.

pages/scan.html

{% extends 'base.html' %}

{% block title %}Scan QR Code{% endblock %}

{% block content %}
<h2>Сканер QR-кодов</h2>
<div style="position: relative; overflow: hidden;" id="scanArea">
    <video style="width: 100%; height: 300px; object-fit: cover;" id="video"></video>
    <canvas style="display: none;" id="canvas"></canvas>
    <div style="display: none;" class="scanner-line" id="scannerLine"></div>
</div>
<button id="toggleScanBtn">Начать сканирование</button>
<div style="display: none;" id="resultArea">
    <p id="scanResult"></p>
    <button id="copyBtn">Копировать результат</button>
    <button id="sendToTelegramBtn">Отправить в Telegram</button>
</div>
{% endblock %}

{% block extra_scripts %}

{% endblock %}

Тут мы всецело положились на библиотеку: https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js и разве что немного адаптировали под себя отображение и добавили 2 своих кнопки: для копирования (добавления в буфер обмена) результатов сканирования и для отправки результатов в телеграмм (напоминаю, что API-метод для этой цели мы уже подготовили ранее).

pages/upload.html

{% extends 'base.html' %}

{% block title %}Загрузить QR-код{% endblock %}

{% block content %}
<h2>Загрузить QR-код</h2>
<div class="drop-area">
    <p>Нажмите здесь или перетащите файл для загрузки</p>
</div>
<input style="display: none;" accept="image/*" id="fileUpload" type="file">
<div style="display: none;" id="resultSection">
    <p id="scanResult"></p>
    <button id="copyBtn">Скопировать результат</button>
    <button id="sendToTelegramBtn">Отправить в Telegram</button>
</div>
{% endblock %}

{% block extra_scripts %}

{% endblock %}

Тут тоже добавил те же две кнопки (функции) для отправки результата в Telegram и для копирования (метод используется тот же, что и в случае со сканером по камере).

Теперь, подключим эти странички к FastApi и полюбуемся нашим результатом.

Подключение страниц к FastAPI

Теперь нас интересует папка app/pages, где создаём файл router.py, чтобы прописать эндпоинты для рендеринга наших страниц. Эти эндпоинты будут возвращать HTML-страницы с использованием шаблонов Jinja2.

Описание кода:

from fastapi import APIRouter
from fastapi.templating import Jinja2Templates
from fastapi.requests import Request
from fastapi.responses import HTMLResponse

router = APIRouter(prefix='', tags=['Фронтенд'])
templates = Jinja2Templates(directory='app/templates')
  • APIRouter — позволяет организовать маршруты для фронтенда

  • Jinja2Templates — указывает на папку с шаблонами, откуда будут рендериться страницы

  • Request — объект запроса, который требуется передавать в шаблоны для корректной работы

  • HTMLResponse — используется для возврата HTML-контента

Эндпоинты:

@router.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
    return templates.TemplateResponse(
        "pages/generate.html", 
        {"request": request, 'active_tab': 'generate'}
    )

@router.get("/scan", response_class=HTMLResponse)
async def scan_qr(request: Request):  # исправлено название функции
    return templates.TemplateResponse(
        "pages/scan.html", 
        {"request": request, 'active_tab': 'scan'}
    )

@router.get("/upload", response_class=HTMLResponse)
async def upload_qr(request: Request):
    return templates.TemplateResponse(
        "pages/upload.html", 
        {"request": request, 'active_tab': 'upload'}
    )

Описание эндпоинтов:

  1. Главная страница ("/")

    • Рендерит страницу генерации QR-кодов generate.html

    • Устанавливает активную вкладку generate

  2. Страница сканирования ("/scan")

    • Рендерит страницу сканирования QR-кодов scan.html

    • Устанавливает активную вкладку scan

  3. Страница загрузки ("/upload")

    • Рендерит страницу для загрузки изображений с QR-кодами upload.html

    • Устанавливает активную вкладку upload

Все эндпоинты используют шаблоны из папки app/templates, передавая в них объект запроса и активную вкладку для корректного отображения навигации.

Проверим в Telegram-боте.

Страница генерации кастомных QR-кодов
Страница генерации кастомных QR-кодов
Страница сканера QR-кодов камерой устройства в реальном времени
Страница сканера QR-кодов камерой устройства в реальном времени
Страница загрузки фото с QR-кодом для последующего считывания
Страница загрузки фото с QR-кодом для последующего считывания

Все страницы успешно рендерятся и выглядят неплохо. Остается только поместить в каждую страницу необходимую для нас функциональность через JavaScript, которая свяжет воедино все куски приложения, которые мы описали к настоящему моменту.

Написание JavaScript для MiniApp в Telegram

Если вы бэкенд разработчик как я, то словосочетание «Пишем JavaScript» может вызвать боль, но, к сожалению, других вариантов нет, так как MiniApp, это, в первую очередь, мир фронтенд разработки.

На этом этапе я не буду приводить полный JavaScript, просто потому что кода много, а я не позиционирую себя как продвинутый JavaScript разработчик, поэтому, могу где-то в объяснениях напортачить. Единственное что точно могу сказать, что код рабочий и выполняет заложенную мною в него логику. Если вы являетесь опытным JavaScript разработчиком, буду рад вашей конструктивной критике в комментариях.

Пишем JavaScript код для генератора QR-кодов (static/js/generate.js)

Логика в этом файле завязана на библиотеке easy.qrcode.js. Через данную библиотеку мы:

  • Генерируем QR-коды с различными настройками, такими как размер, цвет, уровень коррекции ошибок и фоновое изображение

  • Управляем внешним видом QR-кода, используя опции, которые задаются через HTML-форму (например, размер, цвет или прозрачность)

  • Рендерим сгенерированный QR-код прямо в контейнер на странице

  • Добавляем возможность отправки сгенерированного QR-кода в Telegram с помощью кнопки, которая появляется после успешного создания QR-кода

Я остановлюсь тут только на тех кусках, которые имеют непосредственное отношений к теме этой статьи.

После того как QR-код с необходимыми параметрами создан и он отображен на странице генератора QR-кодов, под ним появляется кнопка для отправки QR-кода пользователю в телеграм.

function showSuccessMessage(message) {
    if (tg?.showPopup) {
        tg.showPopup({title: 'Успех', message, buttons: [{type: 'close'}]});
        setTimeout(() =&gt; tg.close(), 2000);
    }
}

Начнем с простого. Есть тут функция, которая выполняет отправку так называемого popup. В данном месте я использовал встроенный в объект tg метод ShowPopup. Выше вы видите как он применяется.

После отправки уведомления, через 2 секунды, происходит закрытие окна MiniApp. Для этого используется второй метод – tg.close().

Есть и другая похожая функция, которая сообщает уже про ошибку.

function showErrorMessage(message) {
    if (tg?.showPopup) {
        tg.showPopup({title: 'Ошибка', message, buttons: [{type: 'close'}]});
    } else {
        alert(message);
    }
}

Тут отправляется сообщение с ошибкой.

После клика на «Отправить в Telegram» выполняется следующая функция:

async function sendToTelegram() {
    const img = document.querySelector('#qrcode img');
    if (!img) {
        return showErrorMessage('Пожалуйста, сначала создайте QR-код.');
    }

    if (tg) {
        try {
            await sendQRCodeToTelegram(img.src);
            showSuccessMessage('QR-код успешно отправлен в Telegram.');
        } catch (error) {
            showErrorMessage('Не удалось отправить QR-код. Попробуйте еще раз.');
        }
    } else {
        showErrorMessage('Эта функция доступна только в Telegram WebApp.');
    }
}

Самое интересное тут это то, как мы забрали сгенерированный QR-код.

const img = document.querySelector('#qrcode img');

Этой записью мы получаем изображение сгенерированного QR-кода, который уже отрисован в контейнере с ID qrcode. В HTML это представлено как элемент <img>, содержащий QR-код.

  • document.querySelector('#qrcode img') — мы ищем элемент изображения (<img>) внутри контейнера с ID qrcode. Если изображение QR-кода успешно сгенерировано, оно будет находиться в этом контейнере.

  • img.src — получаем атрибут src этого изображения, который содержит data URL (base64 данные изображения). Это строка, которая представляет изображение в виде данных, готовых для отправки в Telegram.

То есть мы забираем сгенерированный QR-код в формате base64 и передаём его дальше для отправки в Telegram, как если бы это был обычный файл изображения.

Этот data URL затем отправляется в функцию sendQRCodeToTelegram, которая уже занимается отправкой QR-кода через API.

async function sendQRCodeToTelegram(qrCodeUrl) {
    const userId = tg.initDataUnsafe.user.id;
    const response = await fetch(`/api/send-qr/`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({qr_code_url: qrCodeUrl, user_id: userId}),
    });

    if (!response.ok) {
        throw new Error('Ошибка при отправке QR-кода');
    }

    return response.json();
}

А вот тут уже объединяется воедино вся наша предварительная подготовка.

Благодаря тому что ранее мы инициировали объект tg, у нас получается возможность получить информацию о пользователе, а, точнее, его TelegramID.

const userId = tg.initDataUnsafe.user.id;

Далее, нам ничего не мешает использовать подготовленный ранее API-метод для отправки QR-кода в Telegram. Для этого нам достаточно выполнить простой fetch-запрос. Далее, если все пройдет хорошо, мы получим сообщение про успешную отправку, а после закроется окно MiniApp.

Этим простым примером я продемонстрировал, как можно отправлять байты с MiniApp обратно в Telegram пользователю, даже если вход был осуществлен не через текстовую кнопку.

Проверим.

Создаем через форму
Создаем через форму
Получаем в Telegram
Получаем в Telegram
Смотрим в чате
Смотрим в чате

Видим, что все прекрасно отработало. Идем дальше.

JavaScript код для сканера QR-кодов по камере (static/js/scan.js)

Тут большинство кода идет от библиотеки https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js и на нем я сейчас не буду заострять больше внимание. Если интересно, то смотрите в полном исходнике этого проекта. Сейчас я сосредоточусь только на блоке, который имеет прямое отношение к сегодняшней статье.

Копирование результата сканирования:

function copyResult() {
    if (scanResult) {
        navigator.clipboard.writeText(scanResult).then(() => {
            showPopup(`Вы добавили в буфер: ${scanResult}`);
        }, (err) => {
            console.error('Could not copy text: ', err);
            showPopup('Не удалось скопировать текст');
        });
    }
}

Тут тоже использовался уже знакомый метод showPopup объекта tg. Команда navigator.clipboard.writeText(scanResult) используется для копирования текста в буфер обмена.

async function sendToTelegram() {
    if (scanResult) {
        const userId = tg.initDataUnsafe.user.id;
        try {
            const response = await fetch('/api/send-scaner-info/', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    user_id: userId,
                    result_scan: scanResult
                }),
            });
            if (!response.ok) {
                throw new Error('Ошибка при отправке данных');
            }
            const result = await response.json();
            showPopup(result.message);
        } catch (error) {
            console.error('Ошибка:', error);
            showPopup('Не удалось отправить данные в Telegram');
        }
    }
}

А этой функцией мы использовали наш второй API-метод, уже для отправки пользователю результата сканирования в виде текста.

JS-скрипт блока загрузки фото и сканирования рассматривать не буду, так как там тоже используется библиотека JsQR и там тоже используется кнопка для копирования результата и кнопка для отправки результата в телеграмм этим же методом.

Для того, чтобы не отправлять вам просто скрины, я подготовил для вас небольшую видео-демонстрацию полученного функционала в боте.

Деплой проекта на Amvera Cloud

Наконец-то мы перешли к блоку про удаленный запуск бота на сервисе Amvera.

Для начала нам необходимо подготовить файл с инструкциями для разворачивания проекта на Amvera. Файл необходимо положить на один уровень с файлами .env и requirements.txt (в корень проекта).

Создаем файл amvera.yml и заполняем его следующим образом:

meta:
  environment: python
  toolchain:
    name: pip
    version: 3.12
build:
  requirementsPath: requirements.txt
run:
  persistenceMount: /data
  containerPort: 8000
  command: uvicorn app.main:app --host 0.0.0.0 --port 8000

Здесь мы указываем:

  • язык проекта — python,

  • установщик зависимостей — pip,

  • версию Python — 3.12,

  • передаем путь к файлу с зависимостями requirements.txt,

  • указываем порт и прописываем команду для запуска.

Настройки достаточно стандартные и интуитивно понятные. Этого достаточно, чтобы мы могли приступить к деплою.

Шаги по деплою:

  1. Регистрируемся на сайте Amvera Cloud (и получаем 111 рублей за регистрацию на основной баланс).

  2. Заходим в раздел проектов и нажимаем на «Создать проект».

  3. Даем имя проекту и выбираем тарифный план. Для текущего проекта вполне подойдет тариф «Начальный».

  4. Кликаем на «Далее».

  5. На открывшемся экране выбираем «Через интерфейс» и загружаем все файлы проекта перетягиванием (другой подход доставки — это команды GIT, здесь на ваше усмотрение).

  6. После кликаем на «Далее».

  7. Проверяем, корректно ли загружены настройки. Если всё верно, жмем на «Завершить».

Получение бесплатного домена и привязка к проекту

  1. Входим в созданный проект и переходим на вкладку «Настройки».

  2. Нас интересует кнопка «Добавить доменное имя».

Теперь копируем доменное имя и в локальном файле .env заменяем то доменное имя, которое вам было предоставлено сервисом NGINX, на то, которое вы получили от Amvera Cloud.

После этого возвращаемся в Amvera на вкладку «Репозиторий». На этом этапе вам необходимо будет перезаписать существующий файл .env, на измененный (с новым доменным именем).

Теперь заходим в BotFather и заменяем ссылку на MiniApp и на MenuButton на ту, что вы получили от Amvera.

И, чтоб все изменения вступили в силу и проект пересобрался уже с новым .env файлом и доменным именем жмем на кнопку «Пересобрать проект».

Если все шаги были выполнены корректно, то буквально через пару минут бот сообщит что он запущен.

Для того, чтоб поклацать готовый проект бота переходите по ссылке: Master QR-CODE.

Заключение

В этой статье я стремился продемонстрировать возможности современных Telegram-ботов, показав, насколько мощные и гибкие инструменты можно создавать, используя FastAPI, библиотеку Aiogram и фронтенд-технологии. Освоив эти основы, вы получаете почти безграничные возможности для разработки сложных, интерактивных приложений.

Вспомним, как шаг за шагом мы создавали бота, который:

  • Генерирует стильные и настраиваемые QR-коды с помощью библиотеки easy.qrcode.js.

  • Позволяет пользователям отправлять сгенерированные QR-коды напрямую в Telegram.

  • Сканирует QR-коды с изображений и обрабатывает их с помощью jsQR.

  • Интегрирует все эти функции в Telegram MiniApp, что позволяет управлять приложением прямо через Telegram WebApp.

Мы рассмотрели ключевые моменты разработки, такие как:

  • Обработка и загрузка фонового изображения для QR-кодов.

  • Взаимодействие с Telegram WebApp API для отправки данных пользователям или показа всплывающих уведомлений.

  • Простая интеграция фронтенда с FastAPI через шаблоны Jinja2, что позволяет рендерить динамические страницы.

  • Использование асинхронных запросов для плавной и удобной работы приложения.

  • Работа с буфером обмена, чтобы легко копировать результат сканирования QR-кодов через navigator.clipboard.

Итог:

Наш бот стал не просто инструментом для генерации и сканирования QR-кодов, а полноценным приложением, работающим на нескольких уровнях: от взаимодействия с пользователем до выполнения сложных серверных операций.

Освоив как фронтенд, так и бэкенд, вы откроете для себя мир разработки приложений любой сложности для Telegram MiniApp — от игр до банковских сервисов. Наш пример с ботом показал, как всего за несколько строк кода можно активировать камеру устройства, обработать результат и передать данные обратно в Telegram.

Надеюсь, эта статья вдохновила вас на создание собственных проектов и расширила представление о возможностях Telegram-ботов.

Если материал оказался полезным, буду рад вашим лайкам и комментариям — это мотивирует меня делиться новыми идеями и проектами.

Полный исходный код проекта и эксклюзивные материалы можно найти в моём Telegram-канале «Легкий путь в Python».

До скорых встреч!

Комментарии (4)


  1. Marsezi
    23.10.2024 12:12

    безумная огромная статья думал её никогда не дочитаю

    Я загуглил webapp и как понял это старая технология и они пытаются всех перекинуть в miniapp? Но вы постоянно в статье упоминаете webapp.


    1. yakvenalex Автор
      23.10.2024 12:12

      Вопрос привычки. Новое название технологии MiniApp. WebApp - это старое название технологии. Не обязательно читать за раз. На фоне других моих публикаций эта статья не огромная.