add tg_bot_sessions, padding to qrcode
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user