Hotfix/bot registraion add contact (#99)

* refactor(registration): remove checkBanStatus method and related logic

* fix(BaseService): streamline customer permission checks by consolidating conditions

* bot: remove logger from context

* 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.

* feat(bot): add cancel command to conversation middleware

- Implemented a '/cancel' command in the bot's conversation middleware to allow users to exit all conversations gracefully.
- Removed the manual cancellation check from the addContact conversation, streamlining the code and improving user experience.

* fix(bot): conversations register - disable minification in build configuration
This commit is contained in:
Vlad Chikalkin 2025-09-07 16:36:31 +03:00 committed by GitHub
parent 0934417aaf
commit 3d0f74ef62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 93 additions and 123 deletions

View File

@ -0,0 +1,82 @@
/* 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?.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';
@ -7,10 +8,9 @@ import * as middlewares from './middlewares';
import { setCommands } from './settings/commands';
import { setInfo } from './settings/info';
import { env } from '@/config/env';
import { logger } from '@/utils/logger';
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';
@ -38,14 +38,14 @@ export function createBot({ token }: Parameters_) {
}),
);
bot.use(async (context, next) => {
context.logger = logger.child({
update_id: context.update.update_id,
});
await next();
bot.use(grammyConversations()).command('cancel', async (ctx) => {
await ctx.conversation.exitAll();
await ctx.reply(ctx.t('msg-cancel'));
});
bot.use(conversations());
for (const conversation of Object.values(conversations)) {
bot.use(createConversation(conversation));
}
setInfo(bot);
setCommands(bot);

View File

@ -7,7 +7,7 @@ export default defineConfig({
external: ['telegraf', 'zod'],
format: 'cjs',
loader: { '.json': 'copy' },
minify: true,
minify: false,
noExternal: ['@repo'],
outDir: './dist',
sourcemap: false,

View File

@ -53,11 +53,7 @@ export class BaseService {
const customer = result.data.customers.at(0);
if (!customer) {
throw new Error(BASE_ERRORS.NOT_FOUND_CUSTOMER);
}
if (isCustomerBanned(customer)) {
if (customer && isCustomerBanned(customer)) {
throw new Error(ERRORS.NO_PERMISSION);
}

View File

@ -20,10 +20,6 @@ export class RegistrationService {
}
async getCustomer(variables: VariablesOf<typeof GQL.GetCustomerDocument>) {
if (variables.telegramId) {
await this.checkBanStatus(variables.telegramId);
}
const { query } = await getClientWithToken();
const result = await query({
@ -66,21 +62,4 @@ export class RegistrationService {
return mutationResult.data;
}
private async checkBanStatus(telegramId: number) {
const { query } = await getClientWithToken();
const result = await query({
query: GQL.GetCustomerDocument,
variables: { telegramId },
});
const customer = result.data.customers.at(0);
if (customer && isCustomerBanned(customer)) {
throw new Error(ERRORS.NO_PERMISSION);
}
return { customer };
}
}