Разработка TELEGRAM-бота на JAVA для генерации и считывания QR-кодов
РАЗРАБОТКА TELEGRAM-БОТА ДЛЯ РАБОТЫ С QR-КОДАМИ
Разработка TELEGRAM-бота на JAVA для генерации и считывания QR-кодов
Введение
С недавнего времени QR-коды всерьез, и похоже, надолго вошли в нашу жизнь. QR-код — это простой по своей сути, но при этом чрезвычайно полезный в прикладном плане механизм графического кодирования информации. Мать-прародительница (компания DENSO) внедрила использование QR-кодов с целью оптимизации временных издержек в производственных процессах. К сожалению, на сегодняшний день с введением ограничений со стороны властей на посещение общественных заведений, понятие «QR-код» приобрело негативный окрас, заставляя многих вздрагивать при его упоминании, от чего даже становится немного грустно, ведь не для ограничений были созданы эти черно-белые квадраты.
О QR-кодах я не так давно писал в своей первой публикации Почему введение проверки QR-кодов не имеет смысла в общественном транспорте и торговых центрах?, не смотря на сравнительно небольшой объем технических подробностей, данный материал получил множество положительных откликов, что на время позволило мне войти в первую сотню авторов. Не скрою, это было неожиданным и приятным сюрпризом, большое спасибо всем тем, кто плюсовал в карму, конструктивно критиковал и дискутировал в комментариях.
Во время подготовки первой статьи я отметил для себя, что инструментария работы с QR-кодами может не быть под рукой, либо его функционала будет недостаточно для покрытия текущих потребностей. Например, приложение для считывания QR-кодов не установлено в телефоне, либо возникает необходимость отсканировать QR-код с электронного изображения. Ну и самое интересное, если есть возможность считать код, то должен быть инструмент для того, чтобы его сгенерировать. Так возникла идея разработки TELEGRAM-бота, функционал которого позволяет сканировать и генерировать QR-коды. Плюсы использования TELEGRAM-бота в сравнении с традиционными приложениями-сканерами это: отсутствие необходимости ставить дополнительный софт (при наличии телеги, естественно), возможность чтения цифровых изображений без использования камеры (например, из галереи или с web-сайта) и кроссплатформенность. Логика работы бота проста — отправляешь боту QR-код, в ответе получаешь расшифрованную информацию, отправляешь текст — в ответе получаешь QR-код. Просто? Да! Удобно? Несомненно!
Целевая аудитория
Материал статьи сравнительно несложен, не думаю, что он подойдет для людей, делающих свои первые шаги в программировании, но если опыт разработки на объектно-ориентированных языках имеется, то проблем, препятствующих восприятию, возникнуть не должно.
Если вы еще не разработали своего первого TELEGRAM-бота, либо вам хочется понять, как работают механизмы отправки/приема файлов или же познакомиться с работой одной из самых популярных библиотек для работы с QR-кодами в JAVA – ZXING, то этот пост для вас.
Получение имени и токена бота
Не думаю, что стоит подробно описывать процесс регистрации бота и получения токена, ибо на эту тему было написано множество статей, но если есть вопросы о том, кто такой @BotFather и что с ним делать, то можно почитать материал Всё, о чём должен знать разработчик TELEGRAM-ботов или оставить вопрос в комментариях.
Итак, у нас имеется имя бота (в моем случае — @QRVisorBot) и токен, по понятным причинам значение токена не выкладываю, т.к. это конфиденциальная информация, важно понимать, что лица, имеющие доступ к токену, имеют максимальные полномочия на управление ботом, так что держите токен при себе, если не хотите лишиться своего бота.
Создание проекта
Создаем JAVA-проект, лично я работаю в IDE JETBRAINS IDEA, для сборки использую сборщик проектов MAVEN, поэтому буду указывать список зависимостей для данного сборщика.
Настройки бота
Дабы не хардкодить настроечную информацию, выносим ее в файл настроек, для этого в папке ресурсов (resources) создаем текстовый файл с именем «config.properties», в котором прописываем полученные имя и токен чат-бота:
token = 0000000000: XXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
username = QRVisor
, где:
token — значение токена;
username — имя бота.
Чтобы настройки были доступны в рантайме, я создал класс BotSettings (код ниже), который считывает значения настроек из файла config.properties. Здесь и далее я использую плагин LOMBOK, он служит для уменьшения количества типового кода, не думаю, что программируя на JAVA вы могли пройти мимо него, но если так, то настоятельно рекомендую ознакомиться с его функционалом. Также стоит обозначить, что в классе BotSettings используется порождающий шаблон проектирования СИНГЛТОН, служит он для того, чтобы не было возможности создать несколько экземпляров класса в одном потоке.
BotSettings
package ru.dsci.qrvisor.bot;
import lombok.Data;
import org.telegram.telegrambots.meta.TelegramBotsApi;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
@Data
public class BotSettings {
public static final String FILE_NAME = "config.properties";
private static BotSettings instance;
private Properties properties;
private String token;
private String userName;
private TelegramBotsApi telegramBotsApi;
public static BotSettings getInstance() {
if (instance == null)
instance = new BotSettings();
return instance;
}
{
try {
properties = new Properties();
try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(FILE_NAME)) {
properties.load(inputStream);
} catch (IOException e) {
throw new IOException(String.format("Error loading properties file '%s'", FILE_NAME));
}
token = properties.getProperty("token");
if (token == null) {
throw new RuntimeException("Token value is null");
}
userName = properties.getProperty("username");
if (userName == null) {
throw new RuntimeException("UserName value is null");
}
} catch (RuntimeException | IOException e) {
throw new RuntimeException("Bot initialization error: " + e.getMessage());
}
}
}
Основная логика чат-бота
Основная логика бота размещена в классе BotProcessor (код ниже). Несмотря на то, что бот имеет достаточно простой алгоритм работы, пара команд ему все-таки потребуется. Для возможности работы с командами необходимо основной класс бота унаследовать от TelegramLongPollingCommandBot. TelegramLongPollingCommandBot — содержит методы обработки команд.
Переопределяемые методы базового класса (TelegramLongPollingCommandBot):
getBotUsername — возвращает имя пользователя, на которого зарегистрирован бот;
getBotToken — возвращает токен;
onRegister — действие после регистрации бота (в нашем случае просто вызываем метод класса-родителя);
processNonCommandUpdate — обрабатывает сообщение, которое не является зарегистрированной командой;
processInvalidCommandUpdate — действие при отправке боту некорректной команды.
Методы обработки сообщений:
getMessageType — определяет тип сообщения (типы сообщений бота перечислены в MessageType).
sendMessage — отправляет сообщение в заданный чат.
sendImage — отправляет изображение в заданный чат.
sendQRImage — отправляет QR-код в заданный чат (отличается от sendImage тем, что отправленное изображение QR-кода необходимо удалить).
processImage — обрабатывает полученное от пользователя изображение. TELEGRAM API хранит несколько размеров изображений, получить которые можно с помощью метода getPhoto, каждый файл имеет собственный идентификатор, список сохраняем в коллекцию photoSizes, наибольший размер изображения соответствует максимальному индексу коллекции (нам нужен именно он). Ссылку для скачивания файла получаем с помощью метода getFileUrl (описан ниже).
Системные методы:
setRegisteredCommands — регистрирует команды бота. Для того, чтобы чат-бот мог распознавать команды, их необходимо зарегистрировать. Но как? О том, как создавать команды описано в секции «Команды».
getFileRequest – метод запрашивает информацию о файле хранилища, возвращает информацию о файле в формате JSON, ссылка на файл хранилища имеет вид: https://api.telegram.org/bot
getFileUrl — возвращает ссылку на файл хранилища, для этого сначала запрашиваем информацию о файле (метод getFileRequest), откуда получаем путь к файлу в хранилище (поле «file_path»). Ссылка на файл имеет вид: https://api.telegram.org/file/bot
registerBot — подключает бот к Telegram API.
Зависимости:
org.projectlombok
lombok
1.18.22
provided
org.json
json
20210307
org.telegram
telegrambots
5.4.0
org.telegram
telegrambotsextensions
5.4.0
BotProcessor
package ru.dsci.qrvisor.bot;
import com.google.zxing.WriterException;
import org.json.JSONObject;
import org.telegram.telegrambots.meta.api.methods.send.SendPhoto;
import org.telegram.telegrambots.meta.api.objects.InputFile;
import ru.dsci.qrvisor.bot.commands.CommandHelp;
import ru.dsci.qrvisor.bot.commands.CommandStart;
import ru.dsci.qrvisor.core.exceptions.UserException;
import lombok.extern.slf4j.Slf4j;
import org.telegram.telegrambots.extensions.bots.commandbot.TelegramLongPollingCommandBot;
import org.telegram.telegrambots.extensions.bots.commandbot.commands.IBotCommand;
import org.telegram.telegrambots.meta.TelegramBotsApi;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.PhotoSize;
import org.telegram.telegrambots.meta.api.objects.Update;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import org.telegram.telegrambots.updatesreceivers.DefaultBotSession;
import ru.dsci.qrvisor.qr.QRTools;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class BotProcessor extends TelegramLongPollingCommandBot {
private final static int TEXT_LIMIT = 512;
private final static BotSettings botSettings = BotSettings.getInstance();
private static BotProcessor instance;
private final TelegramBotsApi telegramBotsApi;
private List registeredCommands = new ArrayList<>();
private MessageType getMessageType(Update update) throws UserException {
MessageType messageType = null;
try {
if (update.getMessage().getPhoto() != null)
messageType = MessageType.IMAGE;
else if (update.getMessage().getText() != null)
messageType = (update.getMessage().getText().matches("^/[\\w]*$")) ?
MessageType.COMMAND :
MessageType.TEXT;
if (messageType == null)
throw new IllegalArgumentException(update.toString());
return messageType;
} catch (RuntimeException e) {
log.error(String.format("Invalid message type: %s", e.getMessage()));
throw new UserException("Неподдерживаемый тип сообщения");
}
}
public void sendMessage(Long chatId, String message) {
try {
SendMessage sendMessage = SendMessage
.builder()
.chatId(chatId.toString())
.text(message)
.build();
execute(sendMessage);
} catch (TelegramApiException e) {
log.error(String.format("Sending message error: %s", e.getMessage()));
}
}
public void sendImage(Long chatId, String path) throws UserException {
try {
SendPhoto photo = new SendPhoto();
photo.setPhoto(new InputFile(new File(path)));
photo.setChatId(chatId.toString());
execute(photo);
} catch (TelegramApiException e) {
log.error(String.format("Sending image error: %s", e.getMessage()));
throw new UserException("Ошибка отправки изображения");
}
}
public void sendQRImage(Long chatId, String path) throws UserException {
sendImage(chatId, path);
File file = new File(path);
if (!file.delete()) {
log.error(String.format("File '%s' removing error", path));
}
}
private void processText(Update update) throws TelegramApiException, IOException, WriterException, UserException {
String text = update.getMessage().getText();
logMessage(
update.getMessage().getChatId(),
update.getMessage().getFrom().getId(),
true,
text);
if (text.length() > TEXT_LIMIT) {
log.error(String.format("Message exceeds maximum length of %d", TEXT_LIMIT));
throw new UserException(String.format("Сообщение превышает максимальную длину %d символов", TEXT_LIMIT));
}
String imageUrl = QRTools.encodeText(text);
logMessage(update.getMessage().getChatId(), update.getMessage().getFrom().getId(), false, "$image");
sendQRImage(update.getMessage().getChatId(), imageUrl);
}
private void processImage(Update update) throws TelegramApiException, IOException, UserException {
logMessage(update.getMessage().getChatId(), update.getMessage().getFrom().getId(), true, "$image");
List photoSizes = update.getMessage().getPhoto();
String fileUrl = getFileUrl(update.getMessage().getPhoto().get(photoSizes.size() - 1).getFileId());
String text = QRTools.getTextFromQR(fileUrl);
logMessage(update.getMessage().getChatId(), update.getMessage().getFrom().getId(), false, text);
sendMessage(update.getMessage().getChatId(), text);
}
private JSONObject getFileRequest(String fileId) throws IOException {
String fileUrl = String.format("https://api.telegram.org/bot%s/getFile?file_id=%s",
botSettings.getToken(),
fileId);
return IOTools.readJsonFromUrl(fileUrl);
}
private String getFileUrl(String fileId) throws IOException {
JSONObject jsonObject = getFileRequest(fileId);
return String.format("https://api.telegram.org/file/bot%s/%s",
botSettings.getToken(),
jsonObject.get("file_path"));
}
@Override
public String getBotUsername() {
return botSettings.getUserName();
}
@Override
protected void processInvalidCommandUpdate(Update update) {
String command = update.getMessage().getText().substring(1);
sendMessage(
update.getMessage().getChatId()
, String.format("Некорректная команда [%s], доступные команды: %s"
, command
, registeredCommands.toString()));
}
@Override
public void processNonCommandUpdate(Update update) {
if (update.hasMessage()) {
try {
MessageType messageType = getMessageType(update);
switch (messageType) {
case COMMAND:
processInvalidCommandUpdate(update);
break;
case IMAGE:
processImage(update);
break;
case TEXT:
processText(update);
break;
}
} catch (UserException e) {
sendMessage(update.getMessage().getChatId(), e.getMessage());
} catch (TelegramApiException | RuntimeException | IOException | WriterException e) {
log.error(String.format("Received message processing error: %s", e.getMessage()));
sendMessage(update.getMessage().getChatId(), "Ошибка обработки сообщения");
}
}
}
@Override
public String getBotToken() {
return botSettings.getToken();
}
@Override
public void onRegister() {
super.onRegister();
}
private void logMessage(Long chatId, Long userId, boolean input, String text) {
if (text.length() > TEXT_LIMIT)
text = text.substring(0, TEXT_LIMIT);
log.info(String.format("CHAT [%d] MESSAGE %s %d: %s", chatId, input ? "FROM" : "TO", userId, text));
}
private void setRegisteredCommands() {
registeredCommands = getRegisteredCommands()
.stream()
.map(IBotCommand::getCommandIdentifier)
.collect(Collectors.toList());
}
private void registerCommands() {
registerCommands();
register(new CommandStart());
register(new CommandHelp());
setRegisteredCommands();
}
public void registerBot() {
try {
telegramBotsApi.registerBot(this);
} catch (TelegramApiException e) {
throw new RuntimeException("Telegram API initialization error: " + e.getMessage());
}
}
{
try {
telegramBotsApi = new TelegramBotsApi(DefaultBotSession.class);
registerBot();
registerCommands();
} catch (TelegramApiException e) {
throw new RuntimeException("Telegram Bot initialization error: " + e.getMessage());
}
}
public static BotProcessor getInstance() {
if (instance == null)
instance = new BotProcessor();
return instance;
}
public BotProcessor() {
super();
}
}
Команды
Команды должны имплементировать интерфейс IBotCommand, в моей реализации интерфейс имплементируется абстрактным классом Command, от которого наследуются классы, содержащие реализацию команд (классы CommandStart и CommandHelp).
В боте имеются команды /START и /HELP, по наименованию команд несложно догадаться, что они выполняются при запуске бота и при запросе справки, соответственно. Логика команд содержится в классах CommandStart и CommandHelp (код ниже).
Command
package ru.dsci.qrvisor.bot.commands;
import lombok.extern.slf4j.Slf4j;
import org.telegram.telegrambots.extensions.bots.commandbot.commands.IBotCommand;
import org.telegram.telegrambots.meta.api.methods.send.SendMessage;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.bots.AbsSender;
import org.telegram.telegrambots.meta.exceptions.TelegramApiException;
import java.util.Arrays;
@Slf4j
public abstract class Command implements IBotCommand {
private final String commandIdentifier;
private final String description;
public Command(String commandIdentifier, String description) {
this.commandIdentifier = commandIdentifier;
this.description = description;
}
@Override
public String getCommandIdentifier() {
return commandIdentifier;
}
@Override
public String getDescription() {
return description;
}
@Override
public void processMessage(AbsSender absSender, Message message, String[] strings) {
log.debug(String.format(String.format("COMMAND: %s(%s)", message.getText(), Arrays.toString(strings))));
try {
SendMessage sendMessage = SendMessage
.builder()
.chatId(message.getChatId().toString())
.text(message.getText())
.build();
absSender.execute(sendMessage);
} catch (TelegramApiException e) {
log.error(String.format("Command message processing error: %s", e.getMessage(), e));
}
}
}
CommandStart
package ru.dsci.qrvisor.bot.commands;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.bots.AbsSender;
public class CommandStart extends Command {
@Override
public void processMessage(AbsSender absSender, Message message, String[] strings) {
message.setText("Добро пожаловать! \n"
+ "Вас приветствует бот @QRVisorBot, у меня простые функции: чтение и генерация QR-кодов. \n"
+ "Начнём?"
);
super.processMessage(absSender, message, null);
}
public CommandStart() {
super("start", "Запуск бота");
}
CommandHelp
package ru.dsci.qrvisor.bot.commands;
import org.telegram.telegrambots.meta.api.objects.Message;
import org.telegram.telegrambots.meta.bots.AbsSender;
public class CommandHelp extends Command {
@Override
public void processMessage(AbsSender absSender, Message message, String[] strings) {
message.setText("Функции чат-бота: \n" +
"- считывание QR-кода: для считывания QR-кода сфотографируйте код и отправьте изображение в чат \n" +
"- генерация QR-кода: для генерации QR-кода отправьте текст или ссылку в чат");
super.processMessage(absSender, message, strings);
}
public CommandHelp() {
super("help", "Справка \\help \n");
}
}
С реализацией команд все понятно, но как быть в случае, если пользователь отправит боту незарегистрированную команду? Для обработки подобных ситуаций необходимо переопределить метод processInvalidCommandUpdate.
Запуск приложения
Основную логику приложения разработали, но как его запустить? Для запуска используем класс Main (код ниже), в основном методе main создаем инстанс разработанного нами TELEGRAM-бота. Запускаем… Работает!
Main
package ru.dsci.qrvisor;
import ru.dsci.qrvisor.bot.BotProcessor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class Main {
public static void main(String[] args) {
try {
BotProcessor botProcessor = BotProcessor.getInstance();
log.info("Telegram bot started");
} catch (Throwable e) {
log.error(e.getMessage(), e);
}
}
}
Работа с QR-кодами (ZXing)
Для работы с QR-кодами я использовал open-source библиотеку ZXing (zebra crossing), назначение которой — работа с штирих- и QR-кодами.
Для обработки QR-кодов я разработал класс QRTools (код ниже).
Методы:
getBitmapFromUrl – возвращает изображение по url-адресу, как мы помним, изображения хранятся в хранилище telegram, данный метод возвращает объект BinaryBitmap библиотеки Zxing
decodeBitmap – декодирует изображение в текст
encodeText – кодирует текст в QR-код, полученному изображение сохраняется в файл с уникальным именем, метод возвращает путь к файлу.
getTextFromQR — возвращает текст, который содержит QR-код.
Зависимости:
com.google.zxing
core
3.4.1
com.google.zxing
javase
3.4.1
QRTools
package ru.dsci.qrvisor.qr;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeWriter;
import lombok.extern.slf4j.Slf4j;
import javax.imageio.ImageIO;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.*;
@Slf4j
public class QRTools {
private static final String FILE_FORMAT = "png";
public static final String CHARSET = "UTF-8";
private static final QRSIze DEFAULT_VERSION = QRSIze.MEDIUM;
private static final HashMap qrSizes;
private static int getQRSize(QRSIze QRSIze) {
return qrSizes.get(QRSIze);
}
private static BinaryBitmap getBitmapFromUrl(String url) throws IOException {
BinaryBitmap binaryBitmap;
try {
binaryBitmap = new BinaryBitmap(new HybridBinarizer(
new BufferedImageLuminanceSource(ImageIO.read(new URL(url)))));
} catch (IOException e) {
log.error(String.format("{QRTools.getBitmapFromUrl}: %s", e.getMessage()));
throw new IOException(String.format("Unable to decrypt QR-code: %s", e.getMessage()));
}
return binaryBitmap;
}
private static Result decodeBitmap(BinaryBitmap binaryBitmap) {
Result result;
try {
result = new MultiFormatReader().decode(binaryBitmap);
} catch (NotFoundException e) {
throw new IllegalArgumentException(String.format("Image does not contain QR-code: %s", e.getMessage()));
}
return result;
}
public static String encodeText(String text, int width, int height)
throws WriterException, IOException {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
Hashtable hashtable = new Hashtable();
hashtable.put(EncodeHintType.CHARACTER_SET, CHARSET);
BitMatrix bitMatrix = qrCodeWriter.encode(text, BarcodeFormat.QR_CODE, width, height, hashtable);
Path path = FileSystems.getDefault().getPath(String.format("./images/%s.%s", UUID.randomUUID(), FILE_FORMAT));
MatrixToImageWriter.writeToPath(bitMatrix, "PNG", path);
return path.toAbsolutePath().toString();
}
public static String encodeText(String text, QRSIze QRSIze)
throws WriterException, IOException {
int size = getQRSize(QRSIze);
return encodeText(text, size, size);
}
public static String encodeText(String text)
throws WriterException, IOException {
return encodeText(text, DEFAULT_VERSION);
}
public static String getTextFromQR(String url) throws IOException {
Result result;
try {
result = decodeBitmap(getBitmapFromUrl(url));
} catch (RuntimeException | MalformedURLException e) {
log.debug(String.format("decodeQR: %s", e.getMessage()));
throw new IOException(String.format("Unable to decrypt QR-code: %s", e.getMessage()));
}
return result.getText();
}
static {
qrSizes = new HashMap<>(Map.ofEntries(
new AbstractMap.SimpleEntry<>(QRSIze.SMALL, 256),
new AbstractMap.SimpleEntry<>(QRSIze.MEDIUM, 512),
new AbstractMap.SimpleEntry<>(QRSIze.LARGE, 1024))
);
}
}
Обработка ошибок
Не могу не написать про необходимость обработки исключений. Вы можете со мной поспорить, но на мой взгляд, стабильность работы приложения и информативность сообщений о возникающих в ходе выполнения ошибок, даже важнее оптимальности и скорости работы алгоритмов, заложенных в программу. Для проброса ошибок, которые адресованы пользователю, создан класс UserException, данное исключение служит для того, чтобы пользователь получал лишь информацию об успешности выполнения его запросов, более подробная (системная информация) должна попадать в лог.
UserException
package ru.dsci.qrvisor.core.exceptions;
public class UserException extends Exception {
public UserException(String message) {
super(message);
}
}
Логгирование
Для логгирования я воспользовался библиотекой SLF4J. Бот не собирает информацию о личных данных, отправляемых пользователем, механизм протоколирования используется лишь для отладки.
Зависимости:
org.apache.logging.log4j
log4j-api
2.7
org.apache.logging.log4j
log4j-core
2.7
org.apache.logging.log4j
log4j-slf4j-impl
2.7
Ресурсы
Скачать проект можно по ссылке: https://github.com/SkyZion-public/qrvisor
Найти бот в TELEGRAM можно по имени: @QRVisorBot
Заключение
Друзья, я рассмотрел создание сравнительно простого, но имеющего практическое применение бота, буду чрезвычайно рад если и бот, и данный материал будут вам полезны. Если я что-то упустил, или вы обнаружите неточность, пишите мне, постараюсь ответить на все вопросы.
Есть идея для написания следующей статьи, хотел бы поделиться своими изысканиями на тему работы с API TINKOFF-инвестиции, в планах написать пример торгового робота. Но это если вам, что называется, зайдет данный материал.
Желаю читателям здоровья и терпения, надеюсь, что в скором времени с нас снимут ограничения на посещение общественных мест по QR-кодам.