feat(bot): implement add contact wizard scene and enhance contact handling logic

This commit is contained in:
vchikalkin 2025-07-03 14:49:15 +03:00
parent 0bfebce1e3
commit 6eb421bfd4
3 changed files with 104 additions and 55 deletions

View File

@ -6,6 +6,7 @@ import {
KEYBOARD_REMOVE,
KEYBOARD_SHARE_BOT,
KEYBOARD_SHARE_PHONE,
MESSAGE_CANCEL,
MESSAGE_NOT_MASTER,
MESSAGE_SHARE_BOT,
MSG_ALREADY_MASTER,
@ -19,13 +20,91 @@ import {
MSG_WELCOME,
MSG_WELCOME_BACK,
} from './message';
import { isCustomerMaster } from './utils/customer';
import { normalizePhoneNumber } from './utils/phone';
import { CustomersService } from '@repo/graphql/api/customers';
import { Enum_Customer_Role } from '@repo/graphql/types';
import { Telegraf } from 'telegraf';
import { Scenes, session, Telegraf, type Context as TelegrafContext } from 'telegraf';
import { message } from 'telegraf/filters';
import {
type SceneContextScene,
type SceneSession,
type WizardContextWizard,
type WizardSessionData,
} from 'telegraf/typings/scenes';
const bot = new Telegraf(environment.BOT_TOKEN);
type BotContext = TelegrafContext & {
scene: SceneContextScene<BotContext, WizardSessionData>;
session: SceneSession<WizardSessionData>;
wizard: WizardContextWizard<BotContext>;
};
const bot = new Telegraf<BotContext>(environment.BOT_TOKEN);
const stage = new Scenes.Stage<BotContext>();
bot.use(session({ defaultSession: () => ({ __scenes: { cursor: 0, state: {} } }) }));
bot.use(stage.middleware());
const addContactScene = new Scenes.WizardScene<BotContext>(
'add-contact',
async (context) => {
await context.reply(MSG_SEND_CLIENT_CONTACT, { parse_mode: 'HTML' });
return context.wizard.next();
},
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)) {
await context.reply('Пожалуйста, отправьте контакт клиента через кнопку Telegram');
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' });
return context.scene.leave();
}
const { contact } = context.message;
const name = (contact.first_name || '') + ' ' + (contact.last_name || '').trim();
const phone = normalizePhoneNumber(contact.phone_number);
try {
const { customer: existingCustomer } = await customerService.getCustomer({ phone });
let documentId = existingCustomer?.documentId;
if (!documentId) {
const createCustomerResult = await customerService.createCustomer({ name, phone });
documentId = createCustomerResult?.createCustomer?.documentId;
if (!documentId) throw new Error('Customer not created');
}
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' });
} catch (error) {
await context.reply(MSG_ERROR(error), { parse_mode: 'HTML' });
} finally {
await context.reply(commandsList, { ...KEYBOARD_REMOVE, parse_mode: 'HTML' });
context.scene.leave();
}
},
);
stage.register(addContactScene);
bot.start(async (context) => {
const telegramId = context.from.id;
@ -49,19 +128,17 @@ bot.command('help', async (context) => {
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 (customer.role !== Enum_Customer_Role.Master) {
if (!isCustomerMaster(customer)) {
return context.reply(MESSAGE_NOT_MASTER, { parse_mode: 'HTML' });
}
return context.reply(MSG_SEND_CLIENT_CONTACT, { parse_mode: 'HTML' });
return context.scene.enter('add-contact');
});
bot.command('becomemaster', async (context) => {
@ -74,7 +151,7 @@ bot.command('becomemaster', async (context) => {
return context.reply(MSG_NEED_PHONE, { ...KEYBOARD_SHARE_PHONE, parse_mode: 'HTML' });
}
if (customer.role === Enum_Customer_Role.Master) {
if (isCustomerMaster(customer)) {
return context.reply(MSG_ALREADY_MASTER, { parse_mode: 'HTML' });
}
@ -100,60 +177,25 @@ bot.command('sharebot', async (context) => {
bot.on(message('contact'), async (context) => {
const telegramId = context.from.id;
const customerService = new CustomersService({ telegramId });
const { customer } = await customerService.getCustomer({ telegramId });
const isRegistration = !customer;
if (!customer) {
const { contact } = context.message;
const name = (contact.first_name || '') + ' ' + (contact.last_name || '').trim();
const phone = normalizePhoneNumber(contact.phone_number);
const { contact } = context.message;
const name = (contact.first_name || '') + ' ' + (contact.last_name || '').trim();
const phone = normalizePhoneNumber(contact.phone_number);
if (isRegistration) {
const response = await customerService
.createCustomer({
name,
phone,
telegramId: context.from.id,
})
.createCustomer({ name, phone, telegramId: context.from.id })
.catch((error) => {
context.reply(MSG_ERROR(error), { parse_mode: 'HTML' });
});
if (response) {
return context.reply(MSG_PHONE_SAVED + commandsList, {
...KEYBOARD_REMOVE,
parse_mode: 'HTML',
});
}
} else {
if (customer.role !== Enum_Customer_Role.Master) {
return context.reply(MESSAGE_NOT_MASTER, { parse_mode: 'HTML' });
}
try {
const createCustomerResult = await customerService.createCustomer({ name, phone });
const documentId = createCustomerResult?.createCustomer?.documentId;
if (!documentId) {
throw new Error('Customer not created');
}
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' });
} catch (error) {
context.reply(MSG_ERROR(error), { parse_mode: 'HTML' });
}
}
});

View File

@ -43,7 +43,7 @@ export const KEYBOARD_SHARE_BOT = {
};
export const MESSAGE_NOT_MASTER =
'⛔️ <b>Только мастер может добавлять контакты.</b>\nСтать мастером можно на странице профиля в приложении или с помощью команды <b>/becomemaster</b>';
'⛔️ <b>Только мастер может добавлять контакты</b>\nСтать мастером можно на странице профиля в приложении или с помощью команды <b>/becomemaster</b>';
export const MSG_WELCOME =
'👋 <b>Добро пожаловать!</b>\nПожалуйста, поделитесь своим номером телефона для регистрации';
@ -52,26 +52,28 @@ export const MSG_WELCOME_BACK = (name: string) =>
`👋 <b>С возвращением, ${name}!</b>\nЧтобы воспользоваться сервисом, откройте приложение.\n`;
export const MSG_NEED_PHONE =
'📱 <b>Чтобы добавить контакт, сначала поделитесь своим номером телефона.</b>';
'📱 <b>Чтобы добавить контакт, сначала поделитесь своим номером телефона</b>';
export const MSG_SEND_CLIENT_CONTACT =
'👤 <b>Отправьте контакт клиента, которого вы хотите добавить.</b>';
'👤 <b>Отправьте контакт клиента, которого вы хотите добавить. \n<em>Для отмены операции используйте команду /cancel</em></b>';
export const MSG_ALREADY_MASTER = '🎉 <b>Вы уже являетесь мастером!</b>';
export const MSG_BECOME_MASTER = '🥳 <b>Поздравляем! Теперь вы мастер.</b>';
export const MSG_BECOME_MASTER = '🥳 <b>Поздравляем! Теперь вы мастер</b>';
export const MSG_ERROR = (error?: unknown) =>
`❌ <b>Произошла ошибка.</b>\n${error ? String(error) : ''}`;
`❌ <b>Произошла ошибка</b>\n${error ? String(error) : ''}`;
export const MSG_PHONE_SAVED =
'✅ <b>Спасибо! Мы сохранили ваш номер телефона.</b>\nТеперь вы можете открыть приложение или воспользоваться командами бота';
'✅ <b>Спасибо! Мы сохранили ваш номер телефона</b>\nТеперь вы можете открыть приложение или воспользоваться командами бота';
export const MSG_CONTACT_ADDED = (name: string) =>
`✅ <b>Добавили контакт:</b> <b>${name}</b>\ригласите пользователя в приложение, чтобы вы могли добавлять записи с этим контактом`;
`✅ <b>Добавили контакт в список ваших клиентов</b>\n\nИмя: <b>${name}</b>\n\ригласите клиента в приложение, чтобы вы могли добавлять записи с этим контактом`;
export const MSG_CONTACT_FORWARD =
'<em>Перешлите следующее сообщение, чтобы пользователь мог начать пользоваться ботом ⬇️</em>';
'<em>Перешлите пользователю следующее сообщение, чтобы он мог начать пользоваться ботом ⬇️</em>';
export const MESSAGE_SHARE_BOT =
'📅 <b>Воспользуйтесь этим ботом для записи к вашему мастеру!</b>\nНажмите кнопку ниже, чтобы начать';
export const MESSAGE_CANCEL = '<b>❌ Отменена операции</b>';

View File

@ -0,0 +1,5 @@
import * as GQL from '@repo/graphql/types';
export function isCustomerMaster(customer: GQL.CustomerFieldsFragment) {
return customer?.role === GQL.Enum_Customer_Role.Master;
}