Fix JSON format issues with photos and add multi-photo gallery support
This commit is contained in:
@@ -9,12 +9,12 @@ services:
|
||||
- db
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DB_HOST=db
|
||||
- DB_PORT=5432
|
||||
- DB_NAME=telegram_tinder_bot
|
||||
- DB_USERNAME=postgres
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
|
||||
- DB_HOST={DB_HOST}
|
||||
- DB_PORT={DB_PORT}
|
||||
- DB_NAME={DB_NAME}
|
||||
- DB_USERNAME={DB_USERNAME}
|
||||
- DB_PASSWORD={DB_PASSWORD}
|
||||
- TELEGRAM_BOT_TOKEN={TELEGRAM_BOT_TOKEN}
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
networks:
|
||||
|
||||
174
init-notifications-db.js
Normal file
174
init-notifications-db.js
Normal file
@@ -0,0 +1,174 @@
|
||||
require('dotenv').config();
|
||||
const { Pool } = require('pg');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
// Функция для запуска скрипта
|
||||
async function initializeDatabase() {
|
||||
console.log('Starting database initialization script...');
|
||||
|
||||
const pool = new Pool({
|
||||
host: 'localhost', // Используем localhost
|
||||
port: 5432, // Используем стандартный порт 5432
|
||||
database: 'telegram_tinder_bot',
|
||||
user: 'postgres',
|
||||
password: '',
|
||||
max: 5,
|
||||
connectionTimeoutMillis: 5000
|
||||
});
|
||||
|
||||
console.log('DB Connection Details:');
|
||||
console.log('- Host: localhost');
|
||||
console.log('- Port: 5432');
|
||||
console.log('- Database: telegram_tinder_bot');
|
||||
console.log('- User: postgres');
|
||||
|
||||
try {
|
||||
// Проверяем подключение
|
||||
console.log('Testing connection...');
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
console.log('✅ Connected to database successfully!');
|
||||
|
||||
// 1. Создаем расширение для генерации UUID если его нет
|
||||
console.log('Creating UUID extension...');
|
||||
await client.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
||||
|
||||
// 2. Создаем таблицу для шаблонов уведомлений
|
||||
console.log('Creating notification_templates table...');
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS notification_templates (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
type VARCHAR(50) NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
message_template TEXT NOT NULL,
|
||||
button_template JSONB NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
// 3. Вставляем базовые шаблоны
|
||||
console.log('Inserting notification templates...');
|
||||
|
||||
const templates = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_like',
|
||||
title: 'Новый лайк!',
|
||||
message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||
[
|
||||
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||
],
|
||||
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'super_like',
|
||||
title: 'Супер-лайк!',
|
||||
message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||
[
|
||||
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||
],
|
||||
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_match',
|
||||
title: 'Новый матч!',
|
||||
message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||
[
|
||||
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
|
||||
{ text: '📋 Все матчи', callback_data: 'native_chats' }
|
||||
]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_message',
|
||||
title: 'Новое сообщение!',
|
||||
message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||
[
|
||||
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
|
||||
{ text: '📋 Все чаты', callback_data: 'native_chats' }
|
||||
]
|
||||
]
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
// Вставляем шаблоны с проверкой на конфликты
|
||||
for (const template of templates) {
|
||||
await client.query(`
|
||||
INSERT INTO notification_templates
|
||||
(id, type, title, message_template, button_template, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (type) DO UPDATE
|
||||
SET title = $3,
|
||||
message_template = $4,
|
||||
button_template = $5
|
||||
`, [template.id, template.type, template.title, template.message_template, template.button_template]);
|
||||
}
|
||||
|
||||
console.log('✅ Notification templates created/updated successfully');
|
||||
|
||||
// 4. Создаем таблицу для хранения логов уведомлений если её нет
|
||||
console.log('Creating notifications table...');
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB NOT NULL,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('✅ Notifications table created successfully');
|
||||
|
||||
// 5. Проверяем, что таблицы созданы
|
||||
const tablesResult = await client.query(`
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name IN ('notification_templates', 'notifications')
|
||||
`);
|
||||
|
||||
console.log('Created tables:');
|
||||
tablesResult.rows.forEach(row => {
|
||||
console.log(`- ${row.table_name}`);
|
||||
});
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
console.log('✅ Database initialization completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Запускаем скрипт
|
||||
initializeDatabase();
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
exports.shorthands = undefined;
|
||||
|
||||
exports.up = pgm => {
|
||||
// Проверяем существование таблицы scheduled_notifications
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.tables
|
||||
WHERE table_name = 'scheduled_notifications'
|
||||
) THEN
|
||||
-- Проверяем, нет ли уже столбца processed
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'scheduled_notifications' AND column_name = 'processed'
|
||||
) THEN
|
||||
-- Добавляем столбец processed
|
||||
ALTER TABLE scheduled_notifications ADD COLUMN processed BOOLEAN DEFAULT FALSE;
|
||||
END IF;
|
||||
ELSE
|
||||
-- Создаем таблицу, если она не существует
|
||||
CREATE TABLE scheduled_notifications (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB,
|
||||
scheduled_at TIMESTAMP NOT NULL,
|
||||
processed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
};
|
||||
|
||||
exports.down = pgm => {
|
||||
pgm.sql(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'scheduled_notifications' AND column_name = 'processed'
|
||||
) THEN
|
||||
ALTER TABLE scheduled_notifications DROP COLUMN processed;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
`);
|
||||
};
|
||||
@@ -22,6 +22,7 @@
|
||||
"premium:direct": "ts-node src/scripts/setPremiumDirectConnect.ts",
|
||||
"db:info": "ts-node src/scripts/getDatabaseInfo.ts",
|
||||
"db:test-data": "ts-node src/scripts/createTestData.ts",
|
||||
"enhance-notifications": "ts-node src/scripts/enhanceNotifications.ts",
|
||||
"update": "bash ./bin/update.sh",
|
||||
"update:win": ".\\bin\\update.bat",
|
||||
"start:sh": "bash ./bin/start_bot.sh"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { VipController } from '../controllers/vipController';
|
||||
import { VipService } from '../services/vipService';
|
||||
import { TranslationController } from '../controllers/translationController';
|
||||
import { t } from '../services/localizationService';
|
||||
import { LikeBackHandler } from './likeBackHandler';
|
||||
|
||||
export class CallbackHandlers {
|
||||
private bot: TelegramBot;
|
||||
@@ -22,6 +23,7 @@ export class CallbackHandlers {
|
||||
private vipController: VipController;
|
||||
private vipService: VipService;
|
||||
private translationController: TranslationController;
|
||||
private likeBackHandler: LikeBackHandler;
|
||||
|
||||
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
||||
this.bot = bot;
|
||||
@@ -34,6 +36,7 @@ export class CallbackHandlers {
|
||||
this.vipController = new VipController(bot);
|
||||
this.vipService = new VipService();
|
||||
this.translationController = new TranslationController();
|
||||
this.likeBackHandler = new LikeBackHandler(bot);
|
||||
}
|
||||
|
||||
register(): void {
|
||||
@@ -167,6 +170,12 @@ export class CallbackHandlers {
|
||||
await this.handleMorePhotos(chatId, telegramId, targetUserId);
|
||||
}
|
||||
|
||||
// Обработка лайков и ответных лайков из уведомлений
|
||||
else if (data.startsWith('like_back:')) {
|
||||
const targetUserId = data.replace('like_back:', '');
|
||||
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
|
||||
}
|
||||
|
||||
// Матчи и чаты
|
||||
else if (data === 'view_matches') {
|
||||
await this.handleViewMatches(chatId, telegramId);
|
||||
@@ -385,9 +394,15 @@ export class CallbackHandlers {
|
||||
await this.bot.sendMessage(chatId, '👍 Лайк отправлен!');
|
||||
await this.showNextCandidate(chatId, telegramId);
|
||||
}
|
||||
} catch (error) {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка');
|
||||
console.error('Like error:', error);
|
||||
} catch (error: any) {
|
||||
// Проверяем, связана ли ошибка с уже существующим свайпом
|
||||
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
|
||||
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
|
||||
await this.showNextCandidate(chatId, telegramId);
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка');
|
||||
console.error('Like error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,9 +417,15 @@ export class CallbackHandlers {
|
||||
|
||||
await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass');
|
||||
await this.showNextCandidate(chatId, telegramId);
|
||||
} catch (error) {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка');
|
||||
console.error('Dislike error:', error);
|
||||
} catch (error: any) {
|
||||
// Проверяем, связана ли ошибка с уже существующим свайпом
|
||||
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
|
||||
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
|
||||
await this.showNextCandidate(chatId, telegramId);
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка');
|
||||
console.error('Dislike error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,9 +464,73 @@ export class CallbackHandlers {
|
||||
await this.bot.sendMessage(chatId, '💖 Супер лайк отправлен!');
|
||||
await this.showNextCandidate(chatId, telegramId);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Проверяем, связана ли ошибка с уже существующим свайпом
|
||||
if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') {
|
||||
await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее');
|
||||
await this.showNextCandidate(chatId, telegramId);
|
||||
} else {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка');
|
||||
console.error('Superlike error:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка обратного лайка из уведомления
|
||||
async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
||||
try {
|
||||
// Получаем информацию о пользователях
|
||||
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
|
||||
|
||||
if (!targetProfile) {
|
||||
await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль');
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем telegram ID целевого пользователя для свайпа
|
||||
const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetUserId);
|
||||
if (!targetTelegramId) {
|
||||
await this.bot.sendMessage(chatId, '❌ Не удалось найти пользователя');
|
||||
return;
|
||||
}
|
||||
|
||||
// Выполняем свайп
|
||||
const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'like');
|
||||
|
||||
if (result.isMatch) {
|
||||
// Это матч!
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'🎉 *Поздравляем! Это взаимно!*\n\n' +
|
||||
`Вы и *${targetProfile.name}* понравились друг другу!\n` +
|
||||
'Теперь вы можете начать общение.',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }],
|
||||
[{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' +
|
||||
'Если вы также понравитесь этому пользователю, будет создан матч.',
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка');
|
||||
console.error('Superlike error:', error);
|
||||
console.error('Error in handleLikeBack:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,9 +561,28 @@ export class CallbackHandlers {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 1; i < targetProfile.photos.length; i++) {
|
||||
const photoFileId = targetProfile.photos[i];
|
||||
await this.bot.sendPhoto(chatId, photoFileId);
|
||||
// Отправляем фотографии в виде медиа-группы (коллажа)
|
||||
// Создаем массив объектов медиа для группового отправления
|
||||
const mediaGroup = targetProfile.photos.slice(1).map((photoFileId, index) => ({
|
||||
type: 'photo' as const,
|
||||
media: photoFileId,
|
||||
caption: index === 0 ? `📸 Дополнительные фото ${targetProfile.name}` : undefined
|
||||
}));
|
||||
|
||||
try {
|
||||
// Отправляем все фото одним сообщением (медиа-группой)
|
||||
await this.bot.sendMediaGroup(chatId, mediaGroup);
|
||||
} catch (error) {
|
||||
console.error('Error sending media group:', error);
|
||||
|
||||
// Если не получилось отправить медиа-группой, отправляем по одной
|
||||
for (let i = 1; i < targetProfile.photos.length; i++) {
|
||||
try {
|
||||
await this.bot.sendPhoto(chatId, targetProfile.photos[i]);
|
||||
} catch (photoError) {
|
||||
console.error(`Error sending photo ${i}:`, photoError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const keyboard: InlineKeyboardMarkup = {
|
||||
@@ -807,6 +911,7 @@ export class CallbackHandlers {
|
||||
|
||||
// Вспомогательные методы
|
||||
async showProfile(chatId: number, profile: Profile, isOwner: boolean = false, viewerId?: string): Promise<void> {
|
||||
const hasMultiplePhotos = profile.photos.length > 1;
|
||||
const mainPhotoFileId = profile.photos[0]; // Первое фото - главное
|
||||
|
||||
let profileText = '👤 ' + profile.name + ', ' + profile.age + '\n';
|
||||
@@ -876,20 +981,43 @@ export class CallbackHandlers {
|
||||
|
||||
if (hasValidPhoto) {
|
||||
try {
|
||||
await this.bot.sendPhoto(chatId, mainPhotoFileId, {
|
||||
caption: profileText,
|
||||
reply_markup: keyboard
|
||||
});
|
||||
if (hasMultiplePhotos) {
|
||||
// Если есть несколько фото, отправляем их как медиа-группу (коллаж)
|
||||
const mediaGroup = profile.photos.map((photoFileId, index) => ({
|
||||
type: 'photo' as const,
|
||||
media: photoFileId,
|
||||
caption: index === 0 ? profileText : undefined,
|
||||
parse_mode: index === 0 ? 'Markdown' as const : undefined
|
||||
}));
|
||||
|
||||
// Сначала отправляем медиа-группу
|
||||
await this.bot.sendMediaGroup(chatId, mediaGroup);
|
||||
|
||||
// Затем отправляем отдельное сообщение с кнопками
|
||||
await this.bot.sendMessage(chatId, '📸 Выберите действие:', {
|
||||
reply_markup: keyboard
|
||||
});
|
||||
} else {
|
||||
// Если только одно фото, отправляем его с текстом
|
||||
await this.bot.sendPhoto(chatId, mainPhotoFileId, {
|
||||
caption: profileText,
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending profile photos:', error);
|
||||
// Если не удалось отправить фото, отправляем текст
|
||||
await this.bot.sendMessage(chatId, '🖼 Фото недоступно\n\n' + profileText, {
|
||||
reply_markup: keyboard
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Отправляем как текстовое сообщение
|
||||
await this.bot.sendMessage(chatId, profileText, {
|
||||
reply_markup: keyboard
|
||||
reply_markup: keyboard,
|
||||
parse_mode: 'Markdown'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,25 +168,24 @@ export class EnhancedChatHandlers {
|
||||
|
||||
// ===== СИСТЕМА УВЕДОМЛЕНИЙ =====
|
||||
|
||||
// Отправить уведомление о новом сообщении
|
||||
// Отправить уведомление о новом сообщении - теперь используем NotificationService
|
||||
async sendMessageNotification(receiverTelegramId: string, senderName: string, messagePreview: string, matchId: string): Promise<void> {
|
||||
try {
|
||||
const receiverChatId = parseInt(receiverTelegramId);
|
||||
// Получаем идентификаторы пользователей для использования в NotificationService
|
||||
const receiverUserId = await this.profileService.getUserIdByTelegramId(receiverTelegramId);
|
||||
const sender = await this.chatService.getMatchInfo(matchId, receiverTelegramId);
|
||||
|
||||
await this.bot.sendMessage(
|
||||
receiverChatId,
|
||||
`💌 *Новое сообщение от ${senderName}*\n\n` +
|
||||
`"${this.escapeMarkdown(messagePreview)}"\n\n` +
|
||||
'👆 Нажмите "Открыть чат" для ответа',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[{ text: '💬 Открыть чат', callback_data: `open_native_chat_${matchId}` }],
|
||||
[{ text: '📱 Все чаты', callback_data: 'native_chats' }]
|
||||
]
|
||||
}
|
||||
}
|
||||
if (!receiverUserId || !sender?.otherUserId) {
|
||||
console.error('Failed to get user IDs for notification');
|
||||
return;
|
||||
}
|
||||
|
||||
// Используем сервис уведомлений для отправки более красивого уведомления
|
||||
await this.notificationService.sendMessageNotification(
|
||||
receiverUserId,
|
||||
sender.otherUserId,
|
||||
messagePreview,
|
||||
matchId
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error sending message notification:', error);
|
||||
|
||||
76
src/handlers/likeBackHandler.ts
Normal file
76
src/handlers/likeBackHandler.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import { ProfileService } from '../services/profileService';
|
||||
import { MatchingService } from '../services/matchingService';
|
||||
|
||||
export class LikeBackHandler {
|
||||
private bot: TelegramBot;
|
||||
private profileService: ProfileService;
|
||||
private matchingService: MatchingService;
|
||||
|
||||
constructor(bot: TelegramBot) {
|
||||
this.bot = bot;
|
||||
this.profileService = new ProfileService();
|
||||
this.matchingService = new MatchingService();
|
||||
}
|
||||
|
||||
// Функция для обработки обратного лайка из уведомления
|
||||
async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
||||
try {
|
||||
// Получаем информацию о пользователях
|
||||
const [userId, targetProfile] = await Promise.all([
|
||||
this.profileService.getUserIdByTelegramId(telegramId),
|
||||
this.profileService.getProfileByUserId(targetUserId)
|
||||
]);
|
||||
|
||||
if (!userId || !targetProfile) {
|
||||
await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, есть ли уже свайп
|
||||
const existingSwipe = await this.matchingService.getSwipeBetweenUsers(userId, targetUserId);
|
||||
if (existingSwipe) {
|
||||
await this.bot.sendMessage(chatId, '❓ Вы уже оценили этот профиль ранее.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем свайп (лайк)
|
||||
const result = await this.matchingService.createSwipe(userId, targetUserId, 'like');
|
||||
|
||||
if (result.isMatch) {
|
||||
// Это матч!
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'🎉 *Поздравляем! Это взаимно!*\n\n' +
|
||||
`Вы и *${targetProfile.name}* понравились друг другу!\n` +
|
||||
'Теперь вы можете начать общение.',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }],
|
||||
[{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' +
|
||||
'Если вы также понравитесь этому пользователю, будет создан матч.',
|
||||
{
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in handleLikeBack:', error);
|
||||
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка');
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/scripts/enhanceNotifications.ts
Normal file
133
src/scripts/enhanceNotifications.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { query } from '../database/connection';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
// Загружаем переменные окружения
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Скрипт для обновления механизма уведомлений
|
||||
*/
|
||||
export async function enhanceNotifications() {
|
||||
try {
|
||||
console.log('Enhancing notifications system...');
|
||||
console.log('DB Connection Details:');
|
||||
console.log(`- Host: ${process.env.DB_HOST}`);
|
||||
console.log(`- Port: ${process.env.DB_PORT}`);
|
||||
console.log(`- Database: ${process.env.DB_NAME}`);
|
||||
|
||||
// 1. Создаем расширение для генерации UUID, если его нет
|
||||
await query(`
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"
|
||||
`);
|
||||
|
||||
// 2. Создаем таблицу для хранения типов уведомлений и шаблонов сообщений, если её нет
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS notification_templates (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
type VARCHAR(50) NOT NULL UNIQUE,
|
||||
title TEXT NOT NULL,
|
||||
message_template TEXT NOT NULL,
|
||||
button_template JSONB NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
// 2. Вставляем базовые шаблоны для различных типов уведомлений
|
||||
const templates = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_like',
|
||||
title: 'Новый лайк!',
|
||||
message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||
[
|
||||
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||
],
|
||||
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'super_like',
|
||||
title: 'Супер-лайк!',
|
||||
message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||
[
|
||||
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||
],
|
||||
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_match',
|
||||
title: 'Новый матч!',
|
||||
message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||
[
|
||||
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
|
||||
{ text: '📋 Все матчи', callback_data: 'native_chats' }
|
||||
]
|
||||
]
|
||||
})
|
||||
},
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'new_message',
|
||||
title: 'Новое сообщение!',
|
||||
message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
|
||||
button_template: JSON.stringify({
|
||||
inline_keyboard: [
|
||||
[{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||
[
|
||||
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
|
||||
{ text: '📋 Все чаты', callback_data: 'native_chats' }
|
||||
]
|
||||
]
|
||||
})
|
||||
}
|
||||
];
|
||||
|
||||
// Вставляем шаблоны с проверкой на конфликты
|
||||
for (const template of templates) {
|
||||
await query(`
|
||||
INSERT INTO notification_templates
|
||||
(id, type, title, message_template, button_template, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, NOW())
|
||||
ON CONFLICT (type) DO UPDATE
|
||||
SET title = $3,
|
||||
message_template = $4,
|
||||
button_template = $5
|
||||
`, [template.id, template.type, template.title, template.message_template, template.button_template]);
|
||||
}
|
||||
|
||||
console.log('✅ Notification templates updated successfully');
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Error enhancing notifications:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если запускаем файл напрямую
|
||||
if (require.main === module) {
|
||||
enhanceNotifications().then(() => {
|
||||
console.log('Notification system enhancement completed');
|
||||
process.exit(0);
|
||||
}).catch(error => {
|
||||
console.error('Error:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -152,6 +152,89 @@ export class MatchingService {
|
||||
|
||||
return this.mapEntityToSwipe(result.rows[0]);
|
||||
}
|
||||
|
||||
// Получить свайп между двумя пользователями (псевдоним для getSwipe)
|
||||
async getSwipeBetweenUsers(userId: string, targetUserId: string): Promise<Swipe | null> {
|
||||
return this.getSwipe(userId, targetUserId);
|
||||
}
|
||||
|
||||
// Создать свайп (лайк, дислайк или суперлайк)
|
||||
async createSwipe(userId: string, targetUserId: string, swipeType: SwipeType): Promise<{
|
||||
swipe: Swipe;
|
||||
isMatch: boolean;
|
||||
match?: Match;
|
||||
}> {
|
||||
const swipeId = uuidv4();
|
||||
let isMatch = false;
|
||||
let match: Match | undefined;
|
||||
|
||||
await transaction(async (client) => {
|
||||
// Создаем свайп
|
||||
await client.query(`
|
||||
INSERT INTO swipes (id, user_id, target_user_id, type, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
`, [swipeId, userId, targetUserId, swipeType, new Date()]);
|
||||
|
||||
// Если это лайк или суперлайк, проверяем взаимность
|
||||
if (swipeType === 'like' || swipeType === 'superlike') {
|
||||
const reciprocalSwipe = await client.query(`
|
||||
SELECT * FROM swipes
|
||||
WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike')
|
||||
`, [targetUserId, userId]);
|
||||
|
||||
if (reciprocalSwipe.rows.length > 0) {
|
||||
// Проверяем, что матч еще не существует
|
||||
const existingMatch = await client.query(`
|
||||
SELECT * FROM matches
|
||||
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||||
`, [userId, targetUserId]);
|
||||
|
||||
if (existingMatch.rows.length === 0) {
|
||||
isMatch = true;
|
||||
const matchId = uuidv4();
|
||||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike';
|
||||
|
||||
// Упорядочиваем пользователей для консистентности
|
||||
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||||
|
||||
// Создаем матч
|
||||
await client.query(`
|
||||
INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active, is_super_match)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, [matchId, user1Id, user2Id, new Date(), true, isSuperMatch]);
|
||||
|
||||
match = new Match({
|
||||
id: matchId,
|
||||
userId1: user1Id,
|
||||
userId2: user2Id,
|
||||
createdAt: new Date(),
|
||||
isActive: true,
|
||||
isSuperMatch: isSuperMatch,
|
||||
unreadCount1: 0,
|
||||
unreadCount2: 0
|
||||
});
|
||||
|
||||
// Обновляем свайпы, отмечая что они образуют матч
|
||||
await client.query(`
|
||||
UPDATE swipes SET is_match = true
|
||||
WHERE (user_id = $1 AND target_user_id = $2) OR (user_id = $2 AND target_user_id = $1)
|
||||
`, [userId, targetUserId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const swipe = new Swipe({
|
||||
id: swipeId,
|
||||
userId,
|
||||
targetUserId,
|
||||
type: swipeType,
|
||||
timestamp: new Date(),
|
||||
isMatch
|
||||
});
|
||||
|
||||
return { swipe, isMatch, match };
|
||||
}
|
||||
|
||||
// Получить все матчи пользователя по telegram ID
|
||||
async getUserMatches(telegramId: string, limit: number = 50): Promise<Match[]> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import { query } from '../database/connection';
|
||||
import { ProfileService } from './profileService';
|
||||
import config from '../../config/default.json';
|
||||
|
||||
export interface NotificationData {
|
||||
userId: string;
|
||||
@@ -19,6 +18,126 @@ export class NotificationService {
|
||||
this.profileService = new ProfileService();
|
||||
}
|
||||
|
||||
// Получить шаблон уведомления из базы данных или использовать встроенный
|
||||
private async getNotificationTemplate(type: string): Promise<{
|
||||
title: string;
|
||||
messageTemplate: string;
|
||||
buttonTemplate: any;
|
||||
}> {
|
||||
try {
|
||||
// Попытка получить шаблон из базы данных
|
||||
const result = await query(`
|
||||
SELECT title, message_template, button_template
|
||||
FROM notification_templates
|
||||
WHERE type = $1
|
||||
`, [type]);
|
||||
|
||||
if (result.rows.length > 0) {
|
||||
return {
|
||||
title: result.rows[0].title,
|
||||
messageTemplate: result.rows[0].message_template,
|
||||
buttonTemplate: result.rows[0].button_template
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log('Using default template as database is not available:', error.message);
|
||||
}
|
||||
|
||||
// Если не удалось получить из базы или произошла ошибка, используем встроенные шаблоны
|
||||
const defaultTemplates: Record<string, any> = {
|
||||
'new_like': {
|
||||
title: 'Новый лайк!',
|
||||
messageTemplate: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.',
|
||||
buttonTemplate: {
|
||||
inline_keyboard: [
|
||||
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||
[
|
||||
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||
],
|
||||
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
'super_like': {
|
||||
title: 'Супер-лайк!',
|
||||
messageTemplate: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.',
|
||||
buttonTemplate: {
|
||||
inline_keyboard: [
|
||||
[{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }],
|
||||
[
|
||||
{ text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' },
|
||||
{ text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' }
|
||||
],
|
||||
[{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }]
|
||||
]
|
||||
}
|
||||
},
|
||||
'new_match': {
|
||||
title: 'Новый матч!',
|
||||
messageTemplate: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!',
|
||||
buttonTemplate: {
|
||||
inline_keyboard: [
|
||||
[{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||
[
|
||||
{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' },
|
||||
{ text: '📋 Все матчи', callback_data: 'native_chats' }
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
'new_message': {
|
||||
title: 'Новое сообщение!',
|
||||
messageTemplate: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!',
|
||||
buttonTemplate: {
|
||||
inline_keyboard: [
|
||||
[{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }],
|
||||
[
|
||||
{ text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' },
|
||||
{ text: '📋 Все чаты', callback_data: 'native_chats' }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return defaultTemplates[type] || {
|
||||
title: 'Уведомление',
|
||||
messageTemplate: 'Новое уведомление',
|
||||
buttonTemplate: { inline_keyboard: [] }
|
||||
};
|
||||
}
|
||||
|
||||
// Применить данные к шаблону
|
||||
private applyTemplateData(template: string, data: Record<string, any>): string {
|
||||
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
||||
return data[key] !== undefined ? data[key] : '';
|
||||
});
|
||||
}
|
||||
|
||||
// Применить данные к шаблону кнопок
|
||||
private applyTemplateDataToButtons(buttonTemplate: any, data: Record<string, any>): any {
|
||||
const result = JSON.parse(JSON.stringify(buttonTemplate)); // глубокая копия
|
||||
|
||||
// Рекурсивная функция для замены в любой вложенности
|
||||
const replaceInObject = (obj: any): any => {
|
||||
if (typeof obj === 'string') {
|
||||
return this.applyTemplateData(obj, data);
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(item => replaceInObject(item));
|
||||
} else if (obj !== null && typeof obj === 'object') {
|
||||
const newObj: Record<string, any> = {};
|
||||
for (const key in obj) {
|
||||
newObj[key] = replaceInObject(obj[key]);
|
||||
}
|
||||
return newObj;
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
return replaceInObject(result);
|
||||
}
|
||||
|
||||
// Отправить уведомление о новом лайке
|
||||
async sendLikeNotification(targetTelegramId: string, likerTelegramId: string, isSuperLike: boolean = false): Promise<void> {
|
||||
try {
|
||||
@@ -30,25 +149,41 @@ export class NotificationService {
|
||||
if (!targetUser || !likerProfile || !this.bot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = isSuperLike
|
||||
? `⭐ ${likerProfile.name} отправил вам суперлайк!`
|
||||
: `💖 ${likerProfile.name} поставил вам лайк!`;
|
||||
|
||||
|
||||
// Получаем шаблон уведомления
|
||||
const templateType = isSuperLike ? 'super_like' : 'new_like';
|
||||
const template = await this.getNotificationTemplate(templateType);
|
||||
|
||||
// Подготовка данных для шаблона
|
||||
const templateData = {
|
||||
name: likerProfile.name,
|
||||
age: likerProfile.age.toString(),
|
||||
city: likerProfile.city || '',
|
||||
userId: likerProfile.userId
|
||||
};
|
||||
|
||||
// Применяем данные к шаблону сообщения
|
||||
const message = this.applyTemplateData(template.messageTemplate, templateData);
|
||||
|
||||
// Применяем данные к шаблону кнопок
|
||||
const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData);
|
||||
|
||||
// Отправляем уведомление
|
||||
await this.bot.sendMessage(targetUser.telegram_id, message, {
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${likerProfile.userId}` },
|
||||
{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }
|
||||
]]
|
||||
}
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
|
||||
// Логируем уведомление
|
||||
await this.logNotification({
|
||||
userId: targetUser.id,
|
||||
type: isSuperLike ? 'super_like' : 'new_like',
|
||||
data: { likerUserId: likerProfile.userId, likerName: likerProfile.name }
|
||||
type: templateType,
|
||||
data: {
|
||||
likerUserId: likerProfile.userId,
|
||||
likerName: likerProfile.name,
|
||||
age: likerProfile.age,
|
||||
city: likerProfile.city
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending like notification:', error);
|
||||
@@ -67,22 +202,50 @@ export class NotificationService {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = `🎉 У вас новый матч с ${matchedProfile.name}!\n\nТеперь вы можете начать общение.`;
|
||||
// Получаем матч-ID для перехода в чат
|
||||
const matchResult = await query(`
|
||||
SELECT id FROM matches
|
||||
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||||
AND is_active = true
|
||||
`, [userId, matchedUserId]);
|
||||
|
||||
const matchId = matchResult.rows[0]?.id;
|
||||
|
||||
// Получаем шаблон уведомления
|
||||
const template = await this.getNotificationTemplate('new_match');
|
||||
|
||||
// Подготовка данных для шаблона
|
||||
const templateData = {
|
||||
name: matchedProfile.name,
|
||||
age: matchedProfile.age.toString(),
|
||||
city: matchedProfile.city || '',
|
||||
userId: matchedProfile.userId,
|
||||
matchId: matchId || ''
|
||||
};
|
||||
|
||||
// Применяем данные к шаблону сообщения
|
||||
const message = this.applyTemplateData(template.messageTemplate, templateData);
|
||||
|
||||
// Применяем данные к шаблону кнопок
|
||||
const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData);
|
||||
|
||||
// Отправляем уведомление
|
||||
await this.bot.sendMessage(user.telegram_id, message, {
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '💬 Написать сообщение', callback_data: `start_chat:${matchedUserId}` },
|
||||
{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${matchedUserId}` }
|
||||
]]
|
||||
}
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
|
||||
// Логируем уведомление
|
||||
await this.logNotification({
|
||||
userId,
|
||||
type: 'new_match',
|
||||
data: { matchedUserId, matchedName: matchedProfile.name }
|
||||
data: {
|
||||
matchedUserId,
|
||||
matchedName: matchedProfile.name,
|
||||
age: matchedProfile.age,
|
||||
city: matchedProfile.city,
|
||||
matchId
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending match notification:', error);
|
||||
@@ -90,7 +253,7 @@ export class NotificationService {
|
||||
}
|
||||
|
||||
// Отправить уведомление о новом сообщении
|
||||
async sendMessageNotification(receiverId: string, senderId: string, messageContent: string): Promise<void> {
|
||||
async sendMessageNotification(receiverId: string, senderId: string, messageContent: string, matchId?: string): Promise<void> {
|
||||
try {
|
||||
const [receiver, senderProfile] = await Promise.all([
|
||||
this.getUserByUserId(receiverId),
|
||||
@@ -107,25 +270,55 @@ export class NotificationService {
|
||||
return; // Не отправляем уведомление, если пользователь активен в чате
|
||||
}
|
||||
|
||||
// Если matchId не передан, пытаемся его получить
|
||||
let actualMatchId = matchId;
|
||||
if (!actualMatchId) {
|
||||
const matchResult = await query(`
|
||||
SELECT id FROM matches
|
||||
WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1)
|
||||
AND is_active = true
|
||||
`, [receiverId, senderId]);
|
||||
|
||||
actualMatchId = matchResult.rows[0]?.id;
|
||||
}
|
||||
|
||||
const truncatedMessage = messageContent.length > 50
|
||||
? messageContent.substring(0, 50) + '...'
|
||||
: messageContent;
|
||||
|
||||
const message = `💬 Новое сообщение от ${senderProfile.name}:\n\n${truncatedMessage}`;
|
||||
|
||||
|
||||
// Получаем шаблон уведомления
|
||||
const template = await this.getNotificationTemplate('new_message');
|
||||
|
||||
// Подготовка данных для шаблона
|
||||
const templateData = {
|
||||
name: senderProfile.name,
|
||||
message: truncatedMessage,
|
||||
userId: senderProfile.userId,
|
||||
matchId: actualMatchId || ''
|
||||
};
|
||||
|
||||
// Применяем данные к шаблону сообщения
|
||||
const message = this.applyTemplateData(template.messageTemplate, templateData);
|
||||
|
||||
// Применяем данные к шаблону кнопок
|
||||
const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData);
|
||||
|
||||
// Отправляем уведомление
|
||||
await this.bot.sendMessage(receiver.telegram_id, message, {
|
||||
reply_markup: {
|
||||
inline_keyboard: [[
|
||||
{ text: '💬 Ответить', callback_data: `open_chat:${senderId}` }
|
||||
]]
|
||||
}
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: keyboard
|
||||
});
|
||||
|
||||
// Логируем уведомление
|
||||
await this.logNotification({
|
||||
userId: receiverId,
|
||||
type: 'new_message',
|
||||
data: { senderId, senderName: senderProfile.name, messageContent: truncatedMessage }
|
||||
data: {
|
||||
senderId,
|
||||
senderName: senderProfile.name,
|
||||
messageContent: truncatedMessage,
|
||||
matchId: actualMatchId
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error sending message notification:', error);
|
||||
@@ -364,7 +557,7 @@ export class NotificationService {
|
||||
type VARCHAR(50) NOT NULL,
|
||||
data JSONB,
|
||||
scheduled_at TIMESTAMP NOT NULL,
|
||||
is_processed BOOLEAN DEFAULT FALSE,
|
||||
processed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
@@ -373,7 +566,7 @@ export class NotificationService {
|
||||
// Получаем запланированные уведомления
|
||||
const result = await query(`
|
||||
SELECT * FROM scheduled_notifications
|
||||
WHERE scheduled_at <= $1 AND is_processed = false
|
||||
WHERE scheduled_at <= $1 AND processed = false
|
||||
ORDER BY scheduled_at ASC
|
||||
LIMIT 100
|
||||
`, [new Date()]);
|
||||
@@ -393,7 +586,7 @@ export class NotificationService {
|
||||
|
||||
// Отмечаем как обработанное
|
||||
await query(
|
||||
'UPDATE scheduled_notifications SET is_processed = true WHERE id = $1',
|
||||
'UPDATE scheduled_notifications SET processed = true WHERE id = $1',
|
||||
[notification.id]
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -404,4 +597,4 @@ export class NotificationService {
|
||||
console.error('Error processing scheduled notifications:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,8 +169,8 @@ export class ProfileService {
|
||||
case 'photos':
|
||||
case 'interests':
|
||||
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
||||
// Для PostgreSQL массивы передаем как есть, не как JSON строки
|
||||
updateValues.push(value);
|
||||
// Для PostgreSQL массивы должны быть преобразованы в JSON-строку
|
||||
updateValues.push(JSON.stringify(value));
|
||||
break;
|
||||
case 'location':
|
||||
// Пропускаем обработку местоположения, так как колонки location нет
|
||||
|
||||
37
test-connection.js
Normal file
37
test-connection.js
Normal file
@@ -0,0 +1,37 @@
|
||||
require('dotenv').config();
|
||||
const { Pool } = require('pg');
|
||||
|
||||
// Используем параметры напрямую из .env
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 5,
|
||||
connectionTimeoutMillis: 5000
|
||||
});
|
||||
|
||||
console.log('DB Connection Details:');
|
||||
console.log(`- Host: ${process.env.DB_HOST}`);
|
||||
console.log(`- Port: ${process.env.DB_PORT}`);
|
||||
console.log(`- Database: ${process.env.DB_NAME}`);
|
||||
console.log(`- User: ${process.env.DB_USERNAME}`);
|
||||
|
||||
async function testConnection() {
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query('SELECT NOW() as current_time');
|
||||
console.log('✅ Connected to database successfully!');
|
||||
console.log(`Current database time: ${result.rows[0].current_time}`);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
await pool.end();
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to connect to database:', error);
|
||||
}
|
||||
}
|
||||
|
||||
testConnection();
|
||||
0
test-db.js
Normal file
0
test-db.js
Normal file
Reference in New Issue
Block a user