refactor(bot): update conversation handling and middleware integration

- Replaced the previous conversations middleware with the Grammy framework's implementation.
- Introduced a loop to register custom conversations, enhancing the bot's conversation management capabilities.
- Removed the addContact feature implementation from the add-contact file, streamlining the codebase.
This commit is contained in:
vchikalkin 2025-09-07 15:50:34 +03:00
parent a2bbcbb107
commit 5671ad5b02
4 changed files with 95 additions and 90 deletions

View File

@ -0,0 +1,87 @@
/* eslint-disable id-length */
import { type Context } from '@/bot/context';
import { KEYBOARD_REMOVE, KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards';
import { isCustomerMaster } from '@/utils/customer';
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
import { type Conversation } from '@grammyjs/conversations';
import { CustomersService } from '@repo/graphql/api/customers';
import { RegistrationService } from '@repo/graphql/api/registration';
export async function addContact(conversation: Conversation<Context, Context>, ctx: Context) {
// Проверяем, что пользователь является мастером
const telegramId = ctx.from?.id;
if (!telegramId) {
return ctx.reply(await conversation.external(({ t }) => t('err-generic')));
}
const customerService = new CustomersService({ telegramId });
const { customer } = await customerService.getCustomer({ telegramId });
if (!customer) {
return ctx.reply(
await conversation.external(({ t }) => t('msg-need-phone')),
KEYBOARD_SHARE_PHONE,
);
}
if (!isCustomerMaster(customer)) {
return ctx.reply(await conversation.external(({ t }) => t('msg-not-master')));
}
// Просим отправить контакт клиента
await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact')));
// Ждем любое сообщение от пользователя
const waitCtx = await conversation.wait();
// Проверяем команду отмены
if (waitCtx.message?.text === '/cancel') {
return ctx.reply(await conversation.external(({ t }) => t('msg-cancel')));
}
// Проверяем, что отправлен контакт
if (!waitCtx.message?.contact) {
return ctx.reply(await conversation.external(({ t }) => t('msg-send-contact')));
}
const { contact } = waitCtx.message;
const name = `${contact.first_name || ''} ${contact.last_name || ''}`.trim();
const phone = normalizePhoneNumber(contact.phone_number);
// Проверяем валидность номера телефона
if (!isValidPhoneNumber(phone)) {
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
}
try {
// Проверяем, есть ли клиент с таким номером
const { customer: existingCustomer } = await customerService.getCustomer({ phone });
let documentId = existingCustomer?.documentId;
// Если клиента нет, создаём нового
if (!documentId) {
const registrationService = new RegistrationService();
const createCustomerResult = await registrationService.createCustomer({ name, phone });
documentId = createCustomerResult?.createCustomer?.documentId;
if (!documentId) throw new Error('Клиент не создан');
}
// Добавляем текущего мастера к клиенту
const masters = [customer.documentId];
await customerService.addMasters({ data: { masters }, documentId });
// Отправляем подтверждения и инструкции
await ctx.reply(await conversation.external(({ t }) => t('msg-contact-added', { name })));
await ctx.reply(await conversation.external(({ t }) => t('msg-contact-forward')));
await ctx.reply(await conversation.external(({ t }) => t('msg-share-bot')), KEYBOARD_SHARE_BOT);
} catch (error) {
await ctx.reply(
await conversation.external(({ t }) => t('err-with-details', { error: String(error) })),
);
} finally {
await ctx.reply(await conversation.external(({ t }) => t('commands-list')), KEYBOARD_REMOVE);
}
return ctx.reply(await conversation.external(({ t }) => t('err-generic')), KEYBOARD_REMOVE);
}

View File

@ -0,0 +1 @@
export * from './add-contact';

View File

@ -1,99 +1,11 @@
/* eslint-disable id-length */
import { type Context } from '@/bot/context';
import { logHandle } from '@/bot/helpers/logging';
import { KEYBOARD_REMOVE, KEYBOARD_SHARE_BOT, KEYBOARD_SHARE_PHONE } from '@/config/keyboards';
import { isCustomerMaster } from '@/utils/customer';
import { isValidPhoneNumber, normalizePhoneNumber } from '@/utils/phone';
import { type Conversation, createConversation } from '@grammyjs/conversations';
import { CustomersService } from '@repo/graphql/api/customers';
import { RegistrationService } from '@repo/graphql/api/registration';
import { Composer } from 'grammy';
const composer = new Composer<Context>();
const feature = composer.chatType('private');
async function addContact(conversation: Conversation<Context, Context>, ctx: Context) {
// Проверяем, что пользователь является мастером
const telegramId = ctx.from?.id;
if (!telegramId) {
return ctx.reply(await conversation.external(({ t }) => t('err-generic')));
}
const customerService = new CustomersService({ telegramId });
const { customer } = await customerService.getCustomer({ telegramId });
if (!customer) {
return ctx.reply(
await conversation.external(({ t }) => t('msg-need-phone')),
KEYBOARD_SHARE_PHONE,
);
}
if (!isCustomerMaster(customer)) {
return ctx.reply(await conversation.external(({ t }) => t('msg-not-master')));
}
// Просим отправить контакт клиента
await ctx.reply(await conversation.external(({ t }) => t('msg-send-client-contact')));
// Ждем любое сообщение от пользователя
const waitCtx = await conversation.wait();
// Проверяем команду отмены
if (waitCtx.message?.text === '/cancel') {
return ctx.reply(await conversation.external(({ t }) => t('msg-cancel')));
}
// Проверяем, что отправлен контакт
if (!waitCtx.message?.contact) {
return ctx.reply(await conversation.external(({ t }) => t('msg-send-contact')));
}
const { contact } = waitCtx.message;
const name = `${contact.first_name || ''} ${contact.last_name || ''}`.trim();
const phone = normalizePhoneNumber(contact.phone_number);
// Проверяем валидность номера телефона
if (!isValidPhoneNumber(phone)) {
return ctx.reply(await conversation.external(({ t }) => t('msg-invalid-phone')));
}
try {
// Проверяем, есть ли клиент с таким номером
const { customer: existingCustomer } = await customerService.getCustomer({ phone });
let documentId = existingCustomer?.documentId;
// Если клиента нет, создаём нового
if (!documentId) {
const registrationService = new RegistrationService();
const createCustomerResult = await registrationService.createCustomer({ name, phone });
documentId = createCustomerResult?.createCustomer?.documentId;
if (!documentId) throw new Error('Клиент не создан');
}
// Добавляем текущего мастера к клиенту
const masters = [customer.documentId];
await customerService.addMasters({ data: { masters }, documentId });
// Отправляем подтверждения и инструкции
await ctx.reply(await conversation.external(({ t }) => t('msg-contact-added', { name })));
await ctx.reply(await conversation.external(({ t }) => t('msg-contact-forward')));
await ctx.reply(await conversation.external(({ t }) => t('msg-share-bot')), KEYBOARD_SHARE_BOT);
} catch (error) {
await ctx.reply(
await conversation.external(({ t }) => t('err-with-details', { error: String(error) })),
);
} finally {
await ctx.reply(await conversation.external(({ t }) => t('commands-list')), KEYBOARD_REMOVE);
}
return ctx.reply(await conversation.external(({ t }) => t('err-generic')), KEYBOARD_REMOVE);
}
feature.use(createConversation(addContact));
feature.command('addcontact', logHandle('command-add-contact'), async (ctx) => {
await ctx.conversation.enter('addContact');
});

View File

@ -1,4 +1,5 @@
import { type Context } from './context';
import * as conversations from './conversations';
import * as features from './features';
import { unhandledFeature } from './features/unhandled';
import { errorHandler } from './handlers/errors';
@ -9,7 +10,7 @@ import { setInfo } from './settings/info';
import { env } from '@/config/env';
import { getRedisInstance } from '@/utils/redis';
import { autoChatAction } from '@grammyjs/auto-chat-action';
import { conversations } from '@grammyjs/conversations';
import { createConversation, conversations as grammyConversations } from '@grammyjs/conversations';
import { hydrate } from '@grammyjs/hydrate';
import { limit } from '@grammyjs/ratelimiter';
import { Bot } from 'grammy';
@ -37,7 +38,11 @@ export function createBot({ token }: Parameters_) {
}),
);
bot.use(conversations());
bot.use(grammyConversations());
for (const conversation of Object.values(conversations)) {
bot.use(createConversation(conversation));
}
setInfo(bot);
setCommands(bot);