create qr-codes by tg bot
Deploy app frontend / app frontend (push) Failing after 48s
Deploy db-migrations / db-migrations (push) Failing after 35s
Deploy service message-delivery-method-web-push / service message-delivery-method-web-push (push) Failing after 53s

This commit is contained in:
2024-12-10 13:21:38 +02:00
parent e9b3df38fe
commit 1810c550ba
91 changed files with 3317 additions and 1909 deletions
@@ -0,0 +1,48 @@
import { MyContext } from "../../types/myContext";
import { getQRCodeByUri } from "../../api/qrCodes";
export async function manageMenuActions(ctx: MyContext) {
const data = ctx.callbackQuery?.data;
if (!data || !data.startsWith("manage:")) {
return; // Игнорируем нерелевантные данные
}
const qrCodeUri = data.split(":")[1];
try {
const qrCode = await getQRCodeByUri(qrCodeUri);
if (!qrCode) {
await ctx.answerCallbackQuery({ text: "QR-код не найден." });
return;
}
// Отображаем подменю для управления выбранным QR-кодом
await ctx.editMessageText(
`Вы выбрали QR-код: "${qrCode.name}". Что вы хотите сделать?`,
{
reply_markup: {
inline_keyboard: [
[
{ text: "Информация", callback_data: `manage:info:${qrCodeUri}` },
{
text: "Показать QR-код",
callback_data: `manage:show:${qrCodeUri}`,
},
],
[
{
text: "❌ Удалить",
callback_data: `manage:delete:${qrCodeUri}`,
},
],
],
},
},
);
} catch (error) {
console.error("Ошибка при обработке QR-кода:", qrCodeUri, error);
await ctx.answerCallbackQuery({ text: "Произошла ошибка." });
}
}
@@ -0,0 +1,78 @@
import { deleteQRCode, getQRCodeByUri } from "../../api/qrCodes";
import { MyContext } from "../../types/myContext";
import { generateQRCodeImage } from "../../services/qrGenerator";
import { InputFile } from "grammy";
export async function qrCodeActions(ctx: MyContext) {
const data = ctx.callbackQuery?.data;
if (!data) {
await ctx.answerCallbackQuery({ text: "Неверные данные действия." });
return;
}
const parts = data.split(":");
if (parts.length < 3) {
await ctx.answerCallbackQuery({ text: "Неверный формат данных действия." });
return;
}
const action = parts[1];
const qrCodeUri = parts[2];
// Получаем документ QR-кода
let qrCode;
try {
qrCode = await getQRCodeByUri(qrCodeUri);
if (!qrCode) {
await ctx.answerCallbackQuery({ text: "QR-код не найден." });
return;
}
} catch (error) {
console.error("Ошибка при получении QR-кода:", qrCodeUri, error);
await ctx.answerCallbackQuery({ text: "Ошибка при обработке QR-кода." });
return;
}
// Обработка действия
switch (action) {
case "info":
await ctx.reply(
`Информация о QR-коде:\n\nНазвание: ${qrCode.name}\nURL: ${qrCode.url}`,
);
break;
case "delete":
try {
await deleteQRCode(qrCode.uri);
await ctx.editMessageText(`QR-код "${qrCode.name}" успешно удалён.`);
} catch (error) {
console.error("Ошибка при удалении QR-кода:", error);
await ctx.answerCallbackQuery({ text: "Ошибка при удалении QR-кода." });
}
break;
case "show":
try {
// Генерация QR-кода
const pngBuffer = await generateQRCodeImage(qrCode.url, {
format: "png",
});
// Отправка QR-кода
await ctx.replyWithPhoto(
new InputFile(new Uint8Array(pngBuffer.buffer), "qr.png"),
);
} catch (error) {
console.error("Ошибка при генерации QR-кода:", error);
await ctx.answerCallbackQuery({
text: "Ошибка при отображении QR-кода.",
});
}
break;
default:
await ctx.answerCallbackQuery({ text: "Неизвестное действие." });
}
}
@@ -0,0 +1,27 @@
import { MyContext } from "../types/myContext";
import { manageMenuActions } from "./actions/manageMenuActions";
import { qrCodeActions } from "./actions/qrCodeActions";
/**
* Обработка callback_query для управления QR-кодами
*/
export async function callbackQueryHandler(ctx: MyContext) {
const data = ctx.callbackQuery?.data;
if (!data) return; // Игнорируем пустые callback queries
if (
data.startsWith("manage:info:") ||
data.startsWith("manage:show:") ||
data.startsWith("manage:delete:")
) {
// Обработка действий с QR-кодами (информация, показать, удалить)
await qrCodeActions(ctx);
} else if (data.startsWith("manage:")) {
// Обработка выбора QR-кода
await manageMenuActions(ctx);
} else {
// Ответ на неизвестные callback queries
await ctx.answerCallbackQuery({ text: "Неизвестное действие." });
}
}
+11
View File
@@ -0,0 +1,11 @@
import { MyContext } from "../../types/myContext";
export async function helpCommand(ctx: MyContext) {
await ctx.reply(
"Список доступных команд:\n" +
"/start - начать работу с ботом\n" +
"/help - показать помощь\n" +
"/new - создать новый QR-код\n" +
"/manage - управлять вашими QR-кодами\n",
);
}
@@ -0,0 +1,44 @@
import { MyContext } from "../../types/myContext";
import { getQRCodesByUser } from "../../api/qrCodes";
/**
* Обработка команды /manage с выводом подменю
*/
export async function manageCommand(ctx: MyContext) {
if (!ctx.userDoc) {
await ctx.reply(
"Ваш профиль не найден. Попробуйте снова или обратитесь в поддержку.",
);
return;
}
try {
const qrCodes = await getQRCodesByUser(ctx.userDoc.user_uuid);
if (qrCodes.length === 0) {
await ctx.reply(
"У вас ещё нет QR-кодов. Используйте команду /new для создания.",
);
return;
}
// Генерация клавиатуры с QR-кодами
const keyboard = qrCodes.map((qr) => [
{
text: qr.name,
callback_data: `manage:${qr.uri}`,
},
]);
await ctx.reply("Выберите QR-код для управления:", {
reply_markup: {
inline_keyboard: keyboard,
},
});
} catch (error) {
console.error("Ошибка при получении QR-кодов:", error);
await ctx.reply(
"Произошла ошибка при загрузке ваших QR-кодов. Попробуйте позже.",
);
}
}
+7
View File
@@ -0,0 +1,7 @@
import { MyContext } from "../../types/myContext";
export async function newCommand(ctx: MyContext) {
// Запускаем многошаговый сценарий создания нового QR-кода
// conversation: "newQRCodeConversation"
await ctx.conversation.enter("newQRCodeConversation");
}
@@ -0,0 +1,37 @@
import { Bot } from "grammy";
import { MyContext } from "../../types/myContext";
import { bot } from "old_bot"; // Ваш кастомный контекст
/**
* Устанавливает команды для бота.
* @param bot - Экземпляр бота.
*/
export async function setupCommands(bot: Bot<MyContext>): Promise<void> {
// Настройка команд для английского языка (по умолчанию)
await bot.api.setMyCommands([]);
// Настройка команд для русского языка
await bot.api.setMyCommands(
[
{
command: "start",
description: "Запустить бота",
},
{
command: "help",
description: "Получить помощь",
},
{
command: "new",
description: "Создать новый QR-код",
},
{
command: "manage",
description: "Управлять QR-кодами",
},
],
{ language_code: "ru" },
);
console.log("Команды успешно установлены.");
}
+23
View File
@@ -0,0 +1,23 @@
import { MyContext } from "../../types/myContext";
import { ensureUserExists } from "../../api/users";
export async function startCommand(ctx: MyContext) {
const userId = ctx.from?.id;
if (userId === undefined) {
await ctx.reply(
"Не удалось определить ваш идентификатор пользователя. Попробуйте позже или обратитесь в поддержку.",
);
return;
}
// Проверяем или создаем пользователя в БД
await ensureUserExists(userId);
await ctx.reply(
"Привет! Я бот сервиса «НаСвязи». Я помогу вам создавать и настраивать QR-коды.",
);
await ctx.reply(
"Используйте команду /new чтобы создать новый QR-код или /manage для управления уже созданными кодами.",
);
}
@@ -0,0 +1,116 @@
import { type QRCodeDocument } from "@hereconnect/types";
import { Conversation } from "@grammyjs/conversations";
import { InputFile } from "grammy";
import type { MyContext } from "../../types/myContext";
import { qrCodePlacements } from "../../models/types";
import {
buildPlacementKeyboard,
buildActionsKeyboard,
} from "../utils/keyboards";
import { createQRCode } from "../../api/qrCodes";
import { generateQRCodeImage } from "../../services/qrGenerator";
// Тип разговора (конверсии)
type MyConversation = Conversation<MyContext>;
export async function newQRCodeConversation(
conversation: MyConversation,
ctx: MyContext,
) {
if (!ctx.userDoc) {
await ctx.reply(
"Не удалось определить вашего пользователя. Попробуйте позже.",
);
return;
}
const userUuid = ctx.userDoc.user_uuid;
if (!userUuid) {
await ctx.reply(
"У вашего профиля отсутствует UUID. Обратитесь в поддержку.",
);
return;
}
// 1. Выбор типа объекта (placement)
await ctx.reply("Выберите объект, для которого будет создан QR-код:", {
reply_markup: {
inline_keyboard: buildPlacementKeyboard(qrCodePlacements),
},
});
const placementChoice = await conversation.waitFor("callback_query:data");
const placement = placementChoice.update.callback_query?.data;
// Проверяем, что выбранное значение входит в допустимые типы
if (!qrCodePlacements.some((p) => p.type === placement)) {
await ctx.reply("Неверный выбор. Попробуйте снова /new");
return;
}
// Преобразуем `placement` в строго типизированное значение
const typedPlacement = placement as QRCodeDocument["placement"];
// Подтверждаем выбор
await placementChoice.answerCallbackQuery({ text: "Объект выбран!" });
// 2. Ввод названия QR-кода
await ctx.reply(
"Введите название для вашего QR-кода (например: 'Моя машина'):",
);
const nameMsg = await conversation.waitFor("message:text");
const name = nameMsg.message.text.trim();
if (name.length < 2) {
await ctx.reply(
"Название должно содержать минимум 2 символа. Попробуйте снова /new.",
);
return;
}
// 3. Выбор действий, доступных при сканировании
await ctx.reply("Выберите доступные действия:", {
reply_markup: {
inline_keyboard: buildActionsKeyboard([
{ action: "send_message", label: "Отправить сообщение" },
{ action: "show_phone", label: "Показать номер телефона" },
]),
},
});
const actionsMsg = await conversation.waitFor("callback_query:data");
const chosenAction = actionsMsg.update.callback_query?.data;
if (!chosenAction) {
await ctx.reply("Вы не выбрали действие. Попробуйте снова /new.");
return;
}
// Подтверждаем выбор
await actionsMsg.answerCallbackQuery({ text: "Действие выбрано!" });
// Пока для MVP можно выбрать только одно действие, в будущем можно расширить
const chosenActions = [chosenAction];
// 4. Создание документа в БД
const { url, uri } = await createQRCode({
userUuid,
name,
placement: typedPlacement,
actions: chosenActions,
});
// 5. Генерация QR-кода
const pngBuffer = await generateQRCodeImage(url, { format: "png" });
// 6. Отправляем пользователю QR-код
await ctx.reply("Ваш QR-код готов! Вот он:");
await ctx.replyWithPhoto(
new InputFile(new Uint8Array(pngBuffer.buffer), "qr.png"),
);
await ctx.reply(
`Разместите этот код на вашем объекте. При сканировании по адресу ${url} посетители смогут взаимодействовать с вами.`,
);
// Завершаем разговор
}
+45
View File
@@ -0,0 +1,45 @@
import { Bot, session } from "grammy";
import { conversations, createConversation } from "@grammyjs/conversations";
import { MyContext } from "../types/myContext"; // Ваш расширенный контекст
import { env } from "../config/env";
import { loadUserMiddleware } from "./middleware/loadUser";
import { setupCommands } from "./commands/setupCommands";
import { startCommand } from "./commands/start";
import { helpCommand } from "./commands/help";
import { newCommand } from "./commands/new";
import { manageCommand } from "./commands/manage";
import { callbackQueryHandler } from "./callbackHandlers";
import { newQRCodeConversation } from "./conversations/newQRCodeConversation";
// Инициализируем бота с нужным типом контекста
const bot = new Bot<MyContext>(env.TELEGRAM_BOT_TOKEN);
// Подключаем сессии. По необходимости определите initial state для сессии
bot.use(session({ initial: () => ({}) }));
// Подключаем conversations для многошаговых сценариев
bot.use(conversations());
bot.use(createConversation(newQRCodeConversation, "newQRCodeConversation"));
// Middleware для загрузки или создания пользователя
bot.use(loadUserMiddleware);
// Регистрация команд
bot.command("start", startCommand);
bot.command("help", helpCommand);
bot.command("new", newCommand);
bot.command("manage", manageCommand);
// Обработка callback_query
bot.on("callback_query:data", callbackQueryHandler);
(async () => {
// Установка команд
await setupCommands(bot);
// Запуск бота
bot.start();
console.log("Бот запущен!");
})();
export { bot };
@@ -0,0 +1,36 @@
import { MiddlewareFn } from "grammy";
import { MyContext } from "../../types/myContext";
import { getOrCreateUserByTelegramId } from "../../api/users";
/**
* Данный middleware пытается загрузить пользователя из базы данных
* по его Telegram ID, а если пользователя ещё нет - создаёт.
* Результат сохраняется в ctx.userDoc, чтобы использовать далее в логике бота.
*/
export const loadUserMiddleware: MiddlewareFn<MyContext> = async (
ctx,
next,
) => {
// Если апдейт не содержит from или это системное сообщение от Telegram
if (!ctx.from) {
return next();
}
const telegramId = ctx.from.id;
try {
// Пытаемся получить или создать пользователя по Telegram ID
const userDoc = await getOrCreateUserByTelegramId(telegramId);
ctx.userDoc = userDoc; // сохраняем пользователя в контекст
} catch (error) {
console.error("Ошибка при загрузке пользователя:", error);
// Можно отправить пользователю сообщение об ошибке или пропустить
await ctx.reply(
"Произошла ошибка при загрузке ваших данных. Попробуйте позже.",
);
return; // Прекращаем обработку, если критическая ошибка
}
// Переходим к следующему middleware или обработчику
return next();
};
@@ -0,0 +1,35 @@
import { InlineKeyboardButton } from "grammy/types";
export function buildPlacementKeyboard(
placements: { type: string; label: string }[],
): InlineKeyboardButton[][] {
return placements.map((p) => [
{
text: p.label,
callback_data: p.type,
},
]);
}
export function buildActionsKeyboard(
actions: { action: string; label: string }[],
): InlineKeyboardButton[][] {
// Для простоты пусть выбирать только один вариант. Можно усложнить по необходимости.
return actions.map((a) => [
{
text: a.label,
callback_data: a.action,
},
]);
}
export function buildManageKeyboard(
qrCodes: { uri: string; name: string }[],
): InlineKeyboardButton[][] {
return qrCodes.map((qr) => [
{
text: qr.name,
callback_data: `manage:${qr.uri}:delete`, // Пример - сразу кнопка на удаление. Можно сделать отдельное меню.
},
]);
}