add tg_bot_sessions, padding to qrcode
Deploy app frontend / app frontend (push) Failing after 49s
Deploy db-migrations / db-migrations (push) Failing after 39s
Deploy service message-delivery-method-web-push / service message-delivery-method-web-push (push) Failing after 46s

This commit is contained in:
2024-12-12 13:30:28 +02:00
parent 2939b029b0
commit e07d748e87
16 changed files with 666 additions and 156 deletions
+4 -8
View File
@@ -1,5 +1,6 @@
import { MyContext } from "../../types/myContext";
import { getQRCodesByUser } from "../../api/qrCodes";
import { MyContext } from "@/types/myContext";
import { getQRCodesByUser } from "@/api/qrCodes";
import { buildManageKeyboard } from "@/bot/utils/keyboards";
/**
* Обработка команды /manage с выводом подменю
@@ -23,12 +24,7 @@ export async function manageCommand(ctx: MyContext) {
}
// Генерация клавиатуры с QR-кодами
const keyboard = qrCodes.map((qr) => [
{
text: qr.name,
callback_data: `manage:${qr.uri}`,
},
]);
const keyboard = buildManageKeyboard(qrCodes);
await ctx.reply("Выберите QR-код для управления:", {
reply_markup: {
@@ -68,14 +68,17 @@ export async function newQRCodeConversation(
}
// 3. Выбор действий, доступных при сканировании
await ctx.reply("Выберите доступные действия:", {
reply_markup: {
inline_keyboard: buildActionsKeyboard([
{ action: "sendMessage", label: "Отправить сообщение" },
{ action: "viewContactInfo", label: "Посмотреть контактные данные" },
]),
await ctx.reply(
"Выберите одно или несколько действий, которые будут доступны при сканировании этого QR-кода:",
{
reply_markup: {
inline_keyboard: buildActionsKeyboard([
{ action: "sendMessage", label: "Отправить сообщение" },
{ action: "viewContactInfo", label: "Посмотреть контактные данные" },
]),
},
},
});
);
const actionsMsg = await conversation.waitFor("callback_query:data");
const chosenAction = actionsMsg.update.callback_query?.data;
+9 -3
View File
@@ -1,8 +1,10 @@
import { Bot, session } from "grammy";
import { conversations, createConversation } from "@grammyjs/conversations";
import { MyContext } from "../types/myContext"; // Ваш расширенный контекст
import { env } from "../config/env";
import { MyContext } from "@/types/myContext";
import { env } from "@/config/env";
import { CouchDBStorageAdapter } from "./storageAdapters/CouchDBStorageAdapter";
import { loadUserMiddleware } from "./middleware/loadUser";
import { tgBotSessionsDb } from "@/dbs";
import { setupCommands } from "./commands/setupCommands";
import { startCommand } from "./commands/start";
import { helpCommand } from "./commands/help";
@@ -11,11 +13,15 @@ import { manageCommand } from "./commands/manage";
import { callbackQueryHandler } from "./callbackHandlers";
import { newQRCodeConversation } from "./conversations/newQRCodeConversation";
const storage = new CouchDBStorageAdapter(tgBotSessionsDb, {
softDelete: true,
});
// Инициализируем бота с нужным типом контекста
const bot = new Bot<MyContext>(env.TELEGRAM_BOT_TOKEN);
// Подключаем сессии. По необходимости определите initial state для сессии
bot.use(session({ initial: () => ({}) }));
bot.use(session({ storage, initial: () => ({}) }));
// Подключаем conversations для многошаговых сценариев
bot.use(conversations());
@@ -0,0 +1,131 @@
import type { StorageAdapter } from "grammy";
export interface CouchDBStorageOptions {
/** Если true, при удалении сессии будет ставиться _deleted: true для поддержки репликаций по условиям полей */
softDelete?: boolean;
type?: string;
}
export interface CouchDBSessionDocument<T> {
_id: string;
_rev?: string;
type: string;
session?: T;
created_at: string;
updated_at: string;
_deleted?: boolean;
}
export class CouchDBStorageAdapter<T> implements StorageAdapter<T> {
constructor(
private db: PouchDB.Database<CouchDBSessionDocument<T>>,
private options: CouchDBStorageOptions = {
softDelete: false,
type: "tg_bot_session",
},
) {}
/**
* Чтение сессии по ключу.
* Возвращает данные сессии или undefined, если сессия не найдена.
*/
async read(key: string) {
try {
const id = this.getId(key);
const doc = await this.db.get(id);
return doc.session;
} catch (error: any) {
if (error.status !== 404) {
throw error;
}
}
}
/**
* Запись или обновление сессии по ключу.
* При конфликте повторяет попытку записи.
*/
async write(key: string, value: T) {
const _id = this.getId(key);
await this.retryOnConflict(async () => {
let doc: CouchDBSessionDocument<T>;
const updated_at = new Date().toISOString();
try {
doc = await this.db.get(_id);
doc.session = value;
doc.updated_at = updated_at;
} catch (error: any) {
if (error.status === 404) {
// Документ не найден, создаём новый
doc = {
_id,
type: "tg_bot_session",
session: value,
created_at: updated_at,
updated_at,
};
} else {
throw error;
}
}
await this.db.put(doc);
});
}
/**
* Удаление сессии по ключу без предварительного получения документа.
* Если включен softDelete, пытаемся сразу поставить _deleted: true.
* Если нет softDelete, просто пытаемся удалить без rev, что приведёт к ошибке 409,
* тогда при конфликте получим актуальный _rev и удалим.
*/
async delete(key: string) {
const id = this.getId(key);
await this.retryOnConflict(async () => {
let doc: CouchDBSessionDocument<T>;
try {
doc = await this.db.get(id);
} catch (error: any) {
if (error.status === 404) {
return; // Документ уже отсутствует
} else {
throw error;
}
}
if (this.options.softDelete) {
doc._deleted = true; // Помечаем документ как удалённый
await this.db.put(doc);
} else {
await this.db.remove(id, doc._rev!);
}
});
}
/**
* Вспомогательная функция для повторения операции при конфликте.
* Если при put/remove случится конфликт, повторим попытку до retries раз.
* fn принимает параметр attempt (номер попытки, начиная с 1).
*/
private async retryOnConflict(
fn: (attempt: number) => Promise<void>,
retries = 3,
): Promise<void> {
for (let i = 0; i < retries; i++) {
try {
await fn(i + 1);
return;
} catch (error: any) {
if (error.status === 409 && i < retries - 1) {
// Попытка ещё раз при конфликте
} else {
throw error;
}
}
}
}
private getId(key: string) {
return this.options.type ? `${this.options.type}:${key}` : key;
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ export function buildManageKeyboard(
return qrCodes.map((qr) => [
{
text: qr.name,
callback_data: `manage:${qr.uri}:delete`, // Пример - сразу кнопка на удаление. Можно сделать отдельное меню.
callback_data: `manage:${qr.uri}`,
},
]);
}