Generation, scanning of QR codes with the device camera and deployment in 5 minutes

  • Import JavaScript

  • Style connections

  • Main content placements

Navigation block

The application uses a simple navigation block based on <nav>-element:

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

Navigation Features:

Navigation links

  • QR code generation page (URL: /)

  • Scan Page (URL: /scan)

  • Image download page with QR code (URL: /upload)

Active element

  • The condition is used if active_tab == '...'

  • If the current page matches with active_tab class is added active

  • Allows you to style the active menu item

  • Helps the user navigate the site

Icons

  • Each link contains an SVG icon from the directory /static/img/

  • Icons visually indicate functionality: generation, scanning, downloading

  • Make the interface modern and intuitive

Organizing Templates

For the convenience of organizing the code, the following structure has been created:

  • In a folder templates subfolder created pages

  • HTML templates are placed inside a folder pages

  • This organization prevents the underlying template from being mixed up base.html with regular templates

Application pages

pages/generate.html

Hidden text
{% extends "base.html" %}

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

{% block extra_head %}
<link rel="stylesheet" href="https://habr.com/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 %}

I added separate styles to the generation form page and put them in a separate file. Further, despite the fact that there is quite a lot of code, it is simple.

Technically, here we see a simple description of the form, which I further styled in the style_form.css file.

The only interesting thing here is:

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

In this place of the HTML template, the generated QR code will be displayed with a button to send it to telegram (we wrote a method for sending it earlier) and a loader while the QR code is generated. The main magic will happen here in the generate.js file, which we will look at a little later.

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 %}

Here we relied entirely on the library: https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js and perhaps we slightly adapted the display for ourselves and added 2 of our own buttons: for copying (adding to the clipboard) scanning results and for sending results to telegram (I remind you that we have already prepared an API method for this purpose earlier).

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 %}

Here, too, I added the same two buttons (functions) for sending the result to Telegram and for copying (the method is the same as in the case of a camera scanner).

Now, let's connect these pages to FastApi and admire our result.

Connecting pages to FastAPI

Now we are interested in the folder app/pageswhere we create the file router.pyto register endpoints for rendering our pages. These endpoints will return HTML pages using Jinja2 templates.

Code description:

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 — allows you to organize routes for the frontend

  • Jinja2Templates — points to the folder with templates from which pages will be rendered

  • Request — a request object that needs to be passed to templates for correct operation

  • HTMLResponse – used to return HTML content

Endpoints:

@router.get("https://habr.com/", 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'}
    )

Description of endpoints:

  1. Home page (“https://habr.com/”)

  2. Scan page (“/scan”)

  3. Upload page (“/upload”)

    • Renders a page for loading images with QR codes upload.html

    • Sets the active tab upload

All endpoints use templates from the folder app/templatespassing in them the request object and the active tab to display the navigation correctly.

Let's check it in the Telegram bot.

Page for generating custom QR codes

Page for generating custom QR codes

Real-time device camera QR code scanner page

Real-time device camera QR code scanner page

Photo upload page with QR code for later reading

Photo upload page with QR code for later reading

All pages render successfully and look good. All that remains is to put into each page the functionality we need through JavaScript, which will tie together all the pieces of the application that we have described so far.

Writing JavaScript for MiniApp in Telegram

If you are a backend developer like me, then the phrase “Write JavaScript” can cause pain, but, unfortunately, there are no other options, since MiniApp is, first and foremost, the world of frontend development.

At this stage, I will not provide the full JavaScript, simply because there is a lot of code, and I do not position myself as an advanced JavaScript developer, therefore, I may mess up somewhere in the explanations. The only thing I can say for sure is that the code is working and executes the logic I put into it. If you are an experienced JavaScript developer, I would welcome your constructive criticism in the comments.

We write JavaScript code for the QR code generator (static/js/generate.js)

The logic in this file is based on the easy.qrcode.js library. Through this library we:

  • We generate QR codes with various settings, such as size, color, error correction level and background image

  • We control the appearance of the QR code using options that are set via the HTML form (for example, size, color or transparency)

  • We render the generated QR code directly into the container on the page

  • We add the ability to send the generated QR code to Telegram using the button that appears after the successful creation of the QR code

I will focus here only on those pieces that are directly related to the topic of this article.

After a QR code with the necessary parameters is created and it is displayed on the QR code generator page, a button appears below it to send the QR code to the user in Telegram.

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

Let's start with something simple. There is a function here that sends a so-called popup. Here I used the ShowPopup method built into the tg object. Above you see how it is used.

After sending the notification, after 2 seconds, the MiniApp window closes. For this, the second method is used – tg.close().

There is another similar function that reports an error.

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

An error message is sent here.

After clicking on “Send to Telegram” the following function is performed:

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.');
    }
}

The most interesting thing here is how we took the generated QR code.

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

With this entry we receive an image of the generated QR code, which is already drawn in the container with the ID qrcode. In HTML this is represented as an element <img>containing a QR code.

  • document.querySelector('#qrcode img') — we are looking for an image element (<img>) inside a container with ID qrcode. If the QR code image is successfully generated, it will be located in this container.

  • img.src — we get the src attribute of this image, which contains the data URL (base64 image data). This is a string that represents the image as data ready to be sent to Telegram.

That is, we take the generated QR code in base64 format and pass it on to be sent to Telegram, as if it were a regular image file.

This data URL is then sent to the sendQRCodeToTelegram function, which already handles sending the QR code via the 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();
}

And here all our preliminary preparation comes together.

Thanks to the fact that we previously initiated the tg object, we are able to obtain information about the user, or more precisely, his TelegramID.

const userId = tg.initDataUnsafe.user.id;

Further, nothing prevents us from using the previously prepared API method for sending a QR code to Telegram. To do this, we just need to execute a simple fetch request. Next, if everything goes well, we will receive a message about successful sending, and then the MiniApp window will close.

With this simple example, I demonstrated how you can send bytes from MiniApp back to Telegram to the user, even if the login was not made through a text button.

Let's check.

Create via form

Create via form

We receive it in Telegram

We receive it in Telegram

Let's watch in chat

Let's watch in chat

We see that everything worked perfectly. Let's move on.

JavaScript code for the camera QR code scanner (static/js/scan.js)

Most of the code here comes from the library https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js and I won’t focus any more attention on it now. If interested, then look at complete source code for this project. For now I will focus only on the block that is directly related to today's article.

Copying the scan result:

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

Here, too, the already familiar method was used showPopup object tg. Team navigator.clipboard.writeText(scanResult) used to copy text to the clipboard.

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');
        }
    }
}

And with this function we used our second API method, to send the scan result to the user in the form of text.

I will not consider the JS script for the photo loading and scanning block, since it also uses the JsQR library and also uses a button for copying the result and a button for sending the result to a telegram using the same method.

In order not to send you just screenshots, I have prepared for you a short video demonstration of the resulting functionality in the bot.

Project deployment on Amvera Cloud

Finally, we moved on to the block about remotely launching a bot on the Amvera service.

First, we need to prepare a file with instructions for deploying the project on Amvera. The file must be placed on the same level as the files .env And requirements.txt (at the root of the project).

Create a file amvera.yml and fill it in as follows:

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

Here we indicate:

  • project language – python,

  • dependency installer – pip,

  • Python version – 3.12,

  • pass the path to the file with dependencies requirements.txt,

  • we specify the port and write the command to run.

The settings are quite standard and intuitive. This is enough for us to start deploying.

Deployment steps:

  1. Register on the site Amvera Cloud (and we receive 111 rubles for registration on the main balance).

  2. Let's go to projects section and click on “Create project”.

  3. We give project name and select a tariff plan. For the current project, the “Starter” tariff is quite suitable.

  4. Click on “Next”.

  5. On the screen that opens, select “Through the interface” and upload all project files by drag-and-drop (another delivery approach is GIT commands, here at your discretion).

  6. Then click on “Next”.

  7. We check whether the settings are loaded correctly. If everything is correct, click on “Finish”.

Getting a free domain and linking to the project

  1. We enter the created project and go to the tab “Settings”.

  2. We are interested in the button “Add domain name”.

Now we copy the domain name and in the local .env file we replace the domain name that was provided to you by the NGINX service with the one that you received from Amvera Cloud.

After this, we return to Amvera to the “Repository” tab. At this point you will need to overwrite the existing .env file with the modified one (with the new domain name).

Now go to BotFather and replace the link to MiniApp and MenuButton with the one you received from Amvera.

And, so that all the changes take effect and the project is rebuilt with a new .env file and domain name, click on the “Rebuild project” button.

If all steps were completed correctly, then in just a couple of minutes the bot will report that it is running.

To click on the finished bot project, follow the link: Master QR-CODE.

Conclusion

In this article, I aimed to demonstrate the capabilities of modern Telegram bots, showing how powerful and flexible tools can be created using FastAPIlibrary AIogram and frontend technologies. Once you master these basics, you have almost limitless possibilities for developing complex, interactive applications.

Let's remember how, step by step, we created a bot that:

  • Generates stylish and customizable QR codes using the library easy.qrcode.js.

  • Allows users to send generated QR codes directly to Telegram.

  • Scans QR codes from images and processes them using jsQR.

  • Integrates all these features into Telegram MiniAppwhich allows you to manage the application directly through the Telegram WebApp.

We reviewed key development points such as:

  • Processing and loading background image for QR codes.

  • Interaction with Telegram WebApp API to send data to users or show pop-up notifications.

  • Easy frontend integration with FastAPI via Jinja2 templateswhich allows you to render dynamic pages.

  • Usage asynchronous requests for smooth and convenient operation of the application.

  • Working with clipboardto easily copy the result of scanning QR codes via navigator.clipboard.

Result:

Our bot has become not just a tool for generating and scanning QR codes, but a full-fledged application that works on several levels: from user interaction to complex server operations.

Having mastered both front-end and back-end, you will discover the world of developing applications of any complexity for Telegram MiniApp – from games to banking services. Our example with the bot showed how in just a few lines of code you can activate the device’s camera, process the result and transfer the data back to Telegram.

I hope this article has inspired you to create your own projects and expanded your understanding of the capabilities of Telegram bots.

If the material was useful, I will be glad to see your likes and comments – this motivates me to share new ideas and projects.

The full source code of the project and exclusive materials can be found in my Telegram channel The easy way into Python».

See you soon!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *