Files
tg_tinder_bot/src/handlers/notificationHandlers.ts
2025-09-18 13:46:35 +09:00

645 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import TelegramBot from 'node-telegram-bot-api';
import { v4 as uuidv4 } from 'uuid';
import { query } from '../database/connection';
import { NotificationService } from '../services/notificationService';
interface NotificationSettings {
newMatches: boolean;
newMessages: boolean;
newLikes: boolean;
reminders: boolean;
dailySummary: boolean;
timePreference: 'morning' | 'afternoon' | 'evening' | 'night';
doNotDisturb: boolean;
doNotDisturbStart?: string;
doNotDisturbEnd?: string;
}
export class NotificationHandlers {
private bot: TelegramBot;
private notificationService: NotificationService;
constructor(bot: TelegramBot) {
this.bot = bot;
this.notificationService = new NotificationService(bot);
}
// Метод для получения экземпляра сервиса уведомлений
getNotificationService(): NotificationService {
return this.notificationService;
}
// Обработка команды /notifications
async handleNotificationsCommand(msg: TelegramBot.Message): Promise<void> {
const telegramId = msg.from?.id.toString();
if (!telegramId) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.sendMessage(msg.chat.id, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.');
return;
}
const settings = await this.notificationService.getNotificationSettings(userId);
await this.sendNotificationSettings(msg.chat.id, settings as NotificationSettings);
} catch (error) {
console.error('Error handling notifications command:', error);
await this.bot.sendMessage(msg.chat.id, '❌ Произошла ошибка при загрузке настроек уведомлений.');
}
}
// Отправка меню настроек уведомлений
async sendNotificationSettings(chatId: number, settings: NotificationSettings): Promise<void> {
const message = `
🔔 *Настройки уведомлений*
Выберите, какие уведомления вы хотите получать:
${settings.newMatches ? '✅' : '❌'} Новые матчи
${settings.newMessages ? '✅' : '❌'} Новые сообщения
${settings.newLikes ? '✅' : '❌'} Новые лайки
${settings.reminders ? '✅' : '❌'} Напоминания
${settings.dailySummary ? '✅' : '❌'} Ежедневные сводки
⏰ Предпочтительное время: ${this.getTimePreferenceText(settings.timePreference)}
${settings.doNotDisturb ? '🔕' : '🔔'} Режим "Не беспокоить": ${settings.doNotDisturb ? 'Включен' : 'Выключен'}
${settings.doNotDisturb && settings.doNotDisturbStart && settings.doNotDisturbEnd ?
`с ${settings.doNotDisturbStart} до ${settings.doNotDisturbEnd}` : ''}
Нажмите на кнопку, чтобы изменить настройку:
`;
await this.bot.sendMessage(chatId, message, {
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: `${settings.newMatches ? '✅' : '❌'} Новые матчи`, callback_data: 'notif_toggle:newMatches' },
{ text: `${settings.newMessages ? '✅' : '❌'} Новые сообщения`, callback_data: 'notif_toggle:newMessages' }
],
[
{ text: `${settings.newLikes ? '✅' : '❌'} Новые лайки`, callback_data: 'notif_toggle:newLikes' },
{ text: `${settings.reminders ? '✅' : '❌'} Напоминания`, callback_data: 'notif_toggle:reminders' }
],
[
{ text: `${settings.dailySummary ? '✅' : '❌'} Ежедневные сводки`, callback_data: 'notif_toggle:dailySummary' }
],
[
{ text: `⏰ Время: ${this.getTimePreferenceText(settings.timePreference)}`, callback_data: 'notif_time' }
],
[
{ text: `${settings.doNotDisturb ? '🔕' : '🔔'} Режим "Не беспокоить"`, callback_data: 'notif_dnd' }
],
[
{ text: '↩️ Назад', callback_data: 'settings' }
]
]
}
});
}
// Обработка переключения настройки уведомления
async handleNotificationToggle(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
// notif_toggle:settingName
const settingName = callbackQuery.data?.split(':')[1];
if (!settingName) return;
const settings = await this.notificationService.getNotificationSettings(userId);
let updatedSettings: Partial<NotificationSettings> = { ...settings };
// Инвертируем значение настройки
if (settingName in updatedSettings) {
switch(settingName) {
case 'newMatches':
updatedSettings.newMatches = !updatedSettings.newMatches;
break;
case 'newMessages':
updatedSettings.newMessages = !updatedSettings.newMessages;
break;
case 'newLikes':
updatedSettings.newLikes = !updatedSettings.newLikes;
break;
case 'reminders':
updatedSettings.reminders = !updatedSettings.reminders;
break;
case 'dailySummary':
updatedSettings.dailySummary = !updatedSettings.dailySummary;
break;
}
}
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
// Отправляем обновленные настройки
await this.bot.answerCallbackQuery(callbackQuery.id, {
text: `✅ Настройка "${this.getSettingName(settingName)}" ${updatedSettings[settingName as keyof NotificationSettings] ? 'включена' : 'отключена'}`
});
await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings as NotificationSettings);
} catch (error) {
console.error('Error handling notification toggle:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении настроек.' });
}
}
// Обработка выбора времени для уведомлений
async handleTimePreference(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
if (!callbackQuery.message) return;
await this.bot.editMessageText('⏰ *Выберите предпочтительное время для уведомлений:*', {
chat_id: callbackQuery.message.chat.id,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '🌅 Утро (9:00)', callback_data: 'notif_time_set:morning' },
{ text: '☀️ День (13:00)', callback_data: 'notif_time_set:afternoon' }
],
[
{ text: '🌆 Вечер (19:00)', callback_data: 'notif_time_set:evening' },
{ text: '🌙 Ночь (22:00)', callback_data: 'notif_time_set:night' }
],
[
{ text: '↩️ Назад', callback_data: 'notifications' }
]
]
}
});
await this.bot.answerCallbackQuery(callbackQuery.id);
}
// Обработка установки времени для уведомлений
async handleTimePreferenceSet(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
// notif_time_set:timePreference
const timePreference = callbackQuery.data?.split(':')[1] as 'morning' | 'afternoon' | 'evening' | 'night';
if (!timePreference) return;
const settings = await this.notificationService.getNotificationSettings(userId);
// Копируем существующие настройки и обновляем нужные поля
const existingSettings = settings as NotificationSettings;
const updatedSettings: NotificationSettings = {
...existingSettings,
timePreference
};
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
// Отправляем обновленные настройки
await this.bot.answerCallbackQuery(callbackQuery.id, {
text: `✅ Время уведомлений установлено на ${this.getTimePreferenceText(timePreference)}`
});
await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings);
} catch (error) {
console.error('Error handling time preference set:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении времени уведомлений.' });
}
}
// Обработка режима "Не беспокоить"
async handleDndMode(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
if (!callbackQuery.message) return;
await this.bot.editMessageText('🔕 *Режим "Не беспокоить"*\n\nВыберите действие:', {
chat_id: callbackQuery.message.chat.id,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '✅ Включить', callback_data: 'notif_dnd_set:on' },
{ text: '❌ Выключить', callback_data: 'notif_dnd_set:off' }
],
[
{ text: '⏰ Настроить время', callback_data: 'notif_dnd_time' }
],
[
{ text: '↩️ Назад', callback_data: 'notifications' }
]
]
}
});
await this.bot.answerCallbackQuery(callbackQuery.id);
}
// Обработка установки режима "Не беспокоить"
async handleDndModeSet(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
// notif_dnd_set:on/off
const mode = callbackQuery.data?.split(':')[1];
if (!mode) return;
const settings = await this.notificationService.getNotificationSettings(userId);
// Копируем существующие настройки и обновляем нужное поле
const existingSettings = settings as NotificationSettings;
let updatedSettings: NotificationSettings = {
...existingSettings,
doNotDisturb: mode === 'on'
};
// Если включаем режим "Не беспокоить", но не задано время, ставим дефолтные значения
if (mode === 'on' && (!updatedSettings.doNotDisturbStart || !updatedSettings.doNotDisturbEnd)) {
updatedSettings.doNotDisturbStart = '23:00';
updatedSettings.doNotDisturbEnd = '08:00';
}
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
// Отправляем обновленные настройки
await this.bot.answerCallbackQuery(callbackQuery.id, {
text: `✅ Режим "Не беспокоить" ${mode === 'on' ? 'включен' : 'выключен'}`
});
await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings);
} catch (error) {
console.error('Error handling DND mode set:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении режима "Не беспокоить".' });
}
}
// Настройка времени для режима "Не беспокоить"
async handleDndTimeSetup(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
if (!callbackQuery.message) return;
await this.bot.editMessageText('⏰ *Настройка времени для режима "Не беспокоить"*\n\nВыберите один из предустановленных вариантов или введите свой:', {
chat_id: callbackQuery.message.chat.id,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '🌙 23:00 - 08:00', callback_data: 'notif_dnd_time_set:23:00:08:00' }
],
[
{ text: '🌙 22:00 - 07:00', callback_data: 'notif_dnd_time_set:22:00:07:00' }
],
[
{ text: '🌙 00:00 - 09:00', callback_data: 'notif_dnd_time_set:00:00:09:00' }
],
[
{ text: '✏️ Ввести свой вариант', callback_data: 'notif_dnd_time_custom' }
],
[
{ text: '↩️ Назад', callback_data: 'notif_dnd' }
]
]
}
});
await this.bot.answerCallbackQuery(callbackQuery.id);
}
// Установка предустановленного времени для режима "Не беспокоить"
async handleDndTimeSet(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
// notif_dnd_time_set:startTime:endTime
const parts = callbackQuery.data?.split(':');
if (parts && parts.length >= 4) {
const startTime = `${parts[2]}:${parts[3]}`;
const endTime = `${parts[4]}:${parts[5]}`;
const settings = await this.notificationService.getNotificationSettings(userId);
// Копируем существующие настройки и обновляем нужные поля
const existingSettings = settings as NotificationSettings;
const updatedSettings: NotificationSettings = {
...existingSettings,
doNotDisturb: true,
doNotDisturbStart: startTime,
doNotDisturbEnd: endTime
};
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
// Отправляем обновленные настройки
await this.bot.answerCallbackQuery(callbackQuery.id, {
text: `✅ Время "Не беспокоить" установлено с ${startTime} до ${endTime}`
});
await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings);
}
} catch (error) {
console.error('Error handling DND time set:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при настройке времени "Не беспокоить".' });
}
}
// Запрос пользовательского времени для режима "Не беспокоить"
async handleDndTimeCustom(callbackQuery: TelegramBot.CallbackQuery): Promise<void> {
if (!callbackQuery.message) return;
// Устанавливаем ожидание пользовательского ввода
const userId = callbackQuery.from?.id.toString();
if (userId) {
await this.setUserState(userId, 'waiting_dnd_time');
}
await this.bot.editMessageText('⏰ *Введите время для режима "Не беспокоить"*\n\nУкажите время в формате:\n`с [ЧЧ:ММ] до [ЧЧ:ММ]`\n\nНапример: `с 23:30 до 07:00`', {
chat_id: callbackQuery.message.chat.id,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: {
inline_keyboard: [
[
{ text: '↩️ Отмена', callback_data: 'notif_dnd_time' }
]
]
}
});
await this.bot.answerCallbackQuery(callbackQuery.id);
}
// Обработка пользовательского ввода времени для режима "Не беспокоить"
async handleDndTimeInput(msg: TelegramBot.Message): Promise<void> {
const telegramId = msg.from?.id.toString();
if (!telegramId) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.sendMessage(msg.chat.id, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.');
return;
}
// Очищаем состояние ожидания
await this.clearUserState(telegramId);
// Парсим введенное время
const timeRegex = /с\s+(\d{1,2}[:\.]\d{2})\s+до\s+(\d{1,2}[:\.]\d{2})/i;
const match = msg.text?.match(timeRegex);
if (match && match.length >= 3) {
let startTime = match[1].replace('.', ':');
let endTime = match[2].replace('.', ':');
// Проверяем и форматируем время
if (this.isValidTime(startTime) && this.isValidTime(endTime)) {
startTime = this.formatTime(startTime);
endTime = this.formatTime(endTime);
const settings = await this.notificationService.getNotificationSettings(userId);
// Копируем существующие настройки и обновляем нужные поля
const existingSettings = settings as NotificationSettings;
const updatedSettings: NotificationSettings = {
...existingSettings,
doNotDisturb: true,
doNotDisturbStart: startTime,
doNotDisturbEnd: endTime
};
// Обновляем настройки
await this.notificationService.updateNotificationSettings(userId, updatedSettings);
await this.bot.sendMessage(msg.chat.id, `✅ Время "Не беспокоить" установлено с ${startTime} до ${endTime}`);
await this.sendNotificationSettings(msg.chat.id, updatedSettings);
} else {
await this.bot.sendMessage(msg.chat.id, '❌ Неверный формат времени. Пожалуйста, используйте формат ЧЧ:ММ (например, 23:30).');
}
} else {
await this.bot.sendMessage(msg.chat.id, '❌ Неверный формат ввода. Пожалуйста, введите время в формате "с [ЧЧ:ММ] до [ЧЧ:ММ]" (например, "с 23:30 до 07:00").');
}
} catch (error) {
console.error('Error handling DND time input:', error);
await this.bot.sendMessage(msg.chat.id, '❌ Произошла ошибка при настройке времени "Не беспокоить".');
}
}
// Проверка валидности времени
private isValidTime(time: string): boolean {
const regex = /^(\d{1,2}):(\d{2})$/;
const match = time.match(regex);
if (match) {
const hours = parseInt(match[1]);
const minutes = parseInt(match[2]);
return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59;
}
return false;
}
// Форматирование времени в формат ЧЧ:ММ
private formatTime(time: string): string {
const [hours, minutes] = time.split(':').map(Number);
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
}
// Получение текстового представления времени
private getTimePreferenceText(preference: string): string {
switch (preference) {
case 'morning': return 'Утро (9:00)';
case 'afternoon': return 'День (13:00)';
case 'evening': return 'Вечер (19:00)';
case 'night': return 'Ночь (22:00)';
default: return 'Вечер (19:00)';
}
}
// Получение названия настройки
private getSettingName(setting: string): string {
switch (setting) {
case 'newMatches': return 'Новые матчи';
case 'newMessages': return 'Новые сообщения';
case 'newLikes': return 'Новые лайки';
case 'reminders': return 'Напоминания';
case 'dailySummary': return 'Ежедневные сводки';
default: return setting;
}
}
// Получение ID пользователя по Telegram ID
private async getUserIdByTelegramId(telegramId: string): Promise<string | null> {
try {
const result = await query(
'SELECT id FROM users WHERE telegram_id = $1',
[parseInt(telegramId)]
);
return result.rows.length > 0 ? result.rows[0].id : null;
} catch (error) {
console.error('Error getting user by telegram ID:', error);
return null;
}
}
// Установка состояния ожидания пользователя
private async setUserState(telegramId: string, state: string): Promise<void> {
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) return;
// Сначала проверяем, существуют ли столбцы state и state_data
const checkColumnResult = await query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'state'
`);
if (checkColumnResult.rows.length === 0) {
console.log('Adding state and state_data columns to users table...');
// Добавляем столбцы, если их нет
await query(`
ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb;
`);
}
// Теперь устанавливаем состояние
await query(
`UPDATE users
SET state = $1,
state_data = jsonb_set(COALESCE(state_data, '{}'::jsonb), '{timestamp}', to_jsonb(NOW()))
WHERE telegram_id = $2`,
[state, parseInt(telegramId)]
);
} catch (error) {
console.error('Error setting user state:', error);
}
}
// Очистка состояния ожидания пользователя
private async clearUserState(telegramId: string): Promise<void> {
try {
await query(
'UPDATE users SET state = NULL WHERE telegram_id = $1',
[parseInt(telegramId)]
);
} catch (error) {
console.error('Error clearing user state:', error);
}
}
// Регистрация обработчиков уведомлений
register(): void {
// Команда настройки уведомлений
this.bot.onText(/\/notifications/, this.handleNotificationsCommand.bind(this));
// Обработчик для кнопки настроек уведомлений в меню настроек
this.bot.on('callback_query', async (callbackQuery) => {
if (callbackQuery.data === 'notifications') {
const telegramId = callbackQuery.from?.id.toString();
if (!telegramId || !callbackQuery.message) return;
try {
const userId = await this.getUserIdByTelegramId(telegramId);
if (!userId) {
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' });
return;
}
const settings = await this.notificationService.getNotificationSettings(userId);
await this.sendNotificationSettings(callbackQuery.message.chat.id, settings as NotificationSettings);
await this.bot.answerCallbackQuery(callbackQuery.id);
} catch (error) {
console.error('Error handling notifications callback:', error);
await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при загрузке настроек уведомлений.' });
}
}
else if (callbackQuery.data?.startsWith('notif_toggle:')) {
await this.handleNotificationToggle(callbackQuery);
}
else if (callbackQuery.data === 'notif_time') {
await this.handleTimePreference(callbackQuery);
}
else if (callbackQuery.data?.startsWith('notif_time_set:')) {
await this.handleTimePreferenceSet(callbackQuery);
}
else if (callbackQuery.data === 'notif_dnd') {
await this.handleDndMode(callbackQuery);
}
else if (callbackQuery.data?.startsWith('notif_dnd_set:')) {
await this.handleDndModeSet(callbackQuery);
}
else if (callbackQuery.data === 'notif_dnd_time') {
await this.handleDndTimeSetup(callbackQuery);
}
else if (callbackQuery.data?.startsWith('notif_dnd_time_set:')) {
await this.handleDndTimeSet(callbackQuery);
}
else if (callbackQuery.data === 'notif_dnd_time_custom') {
await this.handleDndTimeCustom(callbackQuery);
}
});
// Обработчик пользовательского ввода для времени "Не беспокоить"
this.bot.on('message', async (msg) => {
if (!msg.text) return;
const telegramId = msg.from?.id.toString();
if (!telegramId) return;
try {
// Сначала проверяем, существует ли столбец state
const checkColumnResult = await query(`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'state'
`);
if (checkColumnResult.rows.length === 0) {
console.log('State column does not exist in users table. Skipping state check.');
return;
}
// Теперь проверяем состояние пользователя
const result = await query(
'SELECT state FROM users WHERE telegram_id = $1',
[parseInt(telegramId)]
);
if (result.rows.length > 0 && result.rows[0].state === 'waiting_dnd_time') {
await this.handleDndTimeInput(msg);
}
} catch (error) {
console.error('Error checking user state:', error);
}
});
}
}