create qr-codes by tg bot
This commit is contained in:
@@ -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: "Неизвестное действие." });
|
||||
}
|
||||
}
|
||||
@@ -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-кодов. Попробуйте позже.",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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("Команды успешно установлены.");
|
||||
}
|
||||
@@ -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} посетители смогут взаимодействовать с вами.`,
|
||||
);
|
||||
|
||||
// Завершаем разговор
|
||||
}
|
||||
@@ -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`, // Пример - сразу кнопка на удаление. Можно сделать отдельное меню.
|
||||
},
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user