diff --git a/apps/bot/src/bot/conversations/add-contact.ts b/apps/bot/src/bot/conversations/add-contact.ts new file mode 100644 index 0000000..aa37d6f --- /dev/null +++ b/apps/bot/src/bot/conversations/add-contact.ts @@ -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, 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); +} diff --git a/apps/bot/src/bot/conversations/index.ts b/apps/bot/src/bot/conversations/index.ts new file mode 100644 index 0000000..cb0d2ef --- /dev/null +++ b/apps/bot/src/bot/conversations/index.ts @@ -0,0 +1 @@ +export * from './add-contact'; diff --git a/apps/bot/src/bot/features/add-contact.ts b/apps/bot/src/bot/features/add-contact.ts index 1c1111b..ac4bf52 100644 --- a/apps/bot/src/bot/features/add-contact.ts +++ b/apps/bot/src/bot/features/add-contact.ts @@ -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(); const feature = composer.chatType('private'); -async function addContact(conversation: Conversation, 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'); }); diff --git a/apps/bot/src/bot/index.ts b/apps/bot/src/bot/index.ts index 06f1b7e..f3448e6 100644 --- a/apps/bot/src/bot/index.ts +++ b/apps/bot/src/bot/index.ts @@ -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); diff --git a/apps/bot/tsup.config.js b/apps/bot/tsup.config.js index 0d0e54f..12b409c 100644 --- a/apps/bot/tsup.config.js +++ b/apps/bot/tsup.config.js @@ -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, diff --git a/packages/graphql/api/base.ts b/packages/graphql/api/base.ts index fced362..fd65008 100644 --- a/packages/graphql/api/base.ts +++ b/packages/graphql/api/base.ts @@ -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); } diff --git a/packages/graphql/api/registration.ts b/packages/graphql/api/registration.ts index d65946c..120e95a 100644 --- a/packages/graphql/api/registration.ts +++ b/packages/graphql/api/registration.ts @@ -20,10 +20,6 @@ export class RegistrationService { } async getCustomer(variables: VariablesOf) { - 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 }; - } }