From 1c295a4b41bce3311a2070e3871d12582e129057 Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Thu, 3 Jul 2025 21:36:44 +0300 Subject: [PATCH] refactor(bot): add comments --- apps/bot/src/index.ts | 74 ++++++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/apps/bot/src/index.ts b/apps/bot/src/index.ts index a264fd0..65c8079 100644 --- a/apps/bot/src/index.ts +++ b/apps/bot/src/index.ts @@ -35,42 +35,54 @@ import { type WizardSessionData, } from 'telegraf/typings/scenes'; +// Расширяем контекст бота для работы с сценами и сессиями type BotContext = TelegrafContext & { scene: SceneContextScene; session: SceneSession; wizard: WizardContextWizard; }; +// Создаём экземпляр бота с токеном const bot = new Telegraf(environment.BOT_TOKEN); +// Создаём менеджер сцен и подключаем сессию const stage = new Scenes.Stage(); bot.use(session({ defaultSession: () => ({ __scenes: { cursor: 0, state: {} } }) })); bot.use(stage.middleware()); +// Сцена добавления контакта клиента мастером const addContactScene = new Scenes.WizardScene( 'add-contact', + + // Шаг 1: Просим отправить контакт клиента async (context) => { await context.reply(MSG_SEND_CLIENT_CONTACT, { parse_mode: 'HTML' }); return context.wizard.next(); }, + + // Шаг 2: Обрабатываем полученный контакт async (context) => { if (!context.from) { await context.reply('Ошибка: не удалось определить пользователя'); return context.scene.leave(); } + // Команда отмены — выход из сцены if (context.message && 'text' in context.message && context.message.text === '/cancel') { await context.reply(MESSAGE_CANCEL + commandsList, { parse_mode: 'HTML' }); return context.scene.leave(); } - if (!('message' in context && context.message && 'contact' in context.message)) { + // Проверяем, что отправлен контакт (через кнопку Telegram) + if (!context.message || !('contact' in context.message)) { await context.reply('Пожалуйста, отправьте контакт клиента через кнопку Telegram'); - return; + return; // остаёмся в сцене, ждем корректный контакт } const telegramId = context.from.id; const customerService = new CustomersService({ telegramId }); + + // Проверяем, что текущий пользователь — мастер const { customer } = await customerService.getCustomer({ telegramId }); if (!customer || !isCustomerMaster(customer)) { await context.reply(MESSAGE_NOT_MASTER, { parse_mode: 'HTML' }); @@ -78,23 +90,34 @@ const addContactScene = new Scenes.WizardScene( } const { contact } = context.message; - const name = (contact.first_name || '') + ' ' + (contact.last_name || '').trim(); + const name = `${contact.first_name || ''} ${contact.last_name || ''}`.trim(); const phone = normalizePhoneNumber(contact.phone_number); - try { - const { customer: existingCustomer } = await customerService.getCustomer({ phone }); + // Проверяем валидность номера телефона + if (!isValidPhoneNumber(phone)) { + await context.reply(MESSAGE_INVALID_PHONE, { parse_mode: 'HTML' }); + return; // остаёмся в сцене, ждем правильный номер + } + 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('Customer not created'); + if (!documentId) throw new Error('Клиент не создан'); } + // Добавляем текущего мастера к клиенту const masters = [customer.documentId]; await customerService.addMasters({ data: { masters }, documentId }); + + // Отправляем подтверждения и инструкции await context.reply(MSG_CONTACT_ADDED(name), { parse_mode: 'HTML' }); await context.reply(MSG_CONTACT_FORWARD, { parse_mode: 'HTML' }); await context.reply(MESSAGE_SHARE_BOT, { ...KEYBOARD_SHARE_BOT, parse_mode: 'HTML' }); @@ -107,8 +130,10 @@ const addContactScene = new Scenes.WizardScene( }, ); +// Регистрируем сцену stage.register(addContactScene); +// Команда /start — приветствие и запрос номера, если пользователь новый bot.start(async (context) => { const telegramId = context.from.id; @@ -116,37 +141,45 @@ bot.start(async (context) => { const { customer } = await customerService.getCustomer({ telegramId }); if (customer) { + // Пользователь уже зарегистрирован — приветствуем return context.reply(MSG_WELCOME_BACK(customer.name) + commandsList, { ...KEYBOARD_REMOVE, parse_mode: 'HTML', }); } + // Новый пользователь — просим поделиться номером return context.reply(MSG_WELCOME, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' }); }); +// Команда /help — список команд bot.command('help', async (context) => { return context.reply(commandsList, { ...KEYBOARD_REMOVE, parse_mode: 'HTML' }); }); +// Команда /addcontact — начать сцену добавления контакта bot.command('addcontact', async (context) => { const telegramId = context.from.id; const customerService = new CustomersService({ telegramId }); const { customer } = await customerService.getCustomer({ telegramId }); + if (!customer) { + // Нет номера — просим поделиться return context.reply(MSG_NEED_PHONE, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' }); } if (!isCustomerMaster(customer)) { + // Нет прав мастера return context.reply(MESSAGE_NOT_MASTER, { parse_mode: 'HTML' }); } + // Входим в сцену return context.scene.enter('add-contact'); }); +// Команда /becomemaster — запрос статуса мастера bot.command('becomemaster', async (context) => { const telegramId = context.from.id; - const customerService = new CustomersService({ telegramId }); const { customer } = await customerService.getCustomer({ telegramId }); @@ -158,11 +191,10 @@ bot.command('becomemaster', async (context) => { return context.reply(MSG_ALREADY_MASTER, { parse_mode: 'HTML' }); } + // Обновляем роль клиента на мастер const response = await customerService .updateCustomer({ - data: { - role: Enum_Customer_Role.Master, - }, + data: { role: Enum_Customer_Role.Master }, }) .catch((error) => { context.reply(MSG_ERROR(error), { parse_mode: 'HTML' }); @@ -173,24 +205,25 @@ bot.command('becomemaster', async (context) => { } }); +// Команда /sharebot — прислать ссылку на бота bot.command('sharebot', async (context) => { await context.reply(MSG_CONTACT_FORWARD, { parse_mode: 'HTML' }); await context.reply(MESSAGE_SHARE_BOT, { ...KEYBOARD_SHARE_BOT, parse_mode: 'HTML' }); }); +// Обработка получения контакта от пользователя (регистрация или обновление) bot.on(message('contact'), async (context) => { const telegramId = context.from.id; const { contact } = context.message; - const name = (contact.first_name || '') + ' ' + (contact.last_name || '').trim(); + const name = `${contact.first_name || ''} ${contact.last_name || ''}`.trim(); - // Проверка наличия номера + // Проверка наличия номера телефона if (!contact.phone_number) { return context.reply(MESSAGE_INVALID_PHONE, { parse_mode: 'HTML' }); } + // Нормализация и валидация номера const phone = normalizePhoneNumber(contact.phone_number); - - // Валидация номера if (!isValidPhoneNumber(phone)) { return context.reply(MESSAGE_INVALID_PHONE, { parse_mode: 'HTML' }); } @@ -201,13 +234,9 @@ bot.on(message('contact'), async (context) => { const { customer } = await registrationService.getCustomer({ phone }); if (customer && !customer.telegramId) { - // Пользователь был добавлен ранее мастером — обновляем + // Пользователь добавлен ранее мастером — обновляем данные await registrationService.updateCustomer({ - data: { - active: true, - name, - telegramId, - }, + data: { active: true, name, telegramId }, documentId: customer.documentId, }); @@ -217,7 +246,7 @@ bot.on(message('contact'), async (context) => { }); } - // Иначе — новый пользователь, создаём и активируем + // Новый пользователь — создаём и активируем const response = await registrationService.createCustomer({ name, phone, telegramId }); const documentId = response?.createCustomer?.documentId; @@ -239,8 +268,9 @@ bot.on(message('contact'), async (context) => { } }); +// Запуск бота bot.launch(); -// Enable graceful stop +// Корректное завершение работы process.once('SIGINT', () => bot.stop('SIGINT')); process.once('SIGTERM', () => bot.stop('SIGTERM'));