// backend/controllers/conversationController.js const Conversation = require('../models/Conversation'); const Message = require('../models/Message'); const User = require('../models/User'); // Может понадобиться для populate // @desc Создать новый диалог (или найти существующий) // @route POST /api/conversations // @access Private // Этот эндпоинт может быть вызван, например, когда пользователи мэтчатся, // или когда один пользователь хочет начать чат с другим из списка мэтчей. // Мы будем предполагать, что в теле запроса приходит ID другого участника. const createOrGetConversation = async (req, res, next) => { const currentUserId = req.user._id; const { otherUserId } = req.body; // ID другого участника if (!otherUserId) { const error = new Error('Не указан ID другого участника (otherUserId).'); error.statusCode = 400; return next(error); } if (currentUserId.equals(otherUserId)) { const error = new Error('Нельзя создать диалог с самим собой.'); error.statusCode = 400; return next(error); } try { // Ищем существующий диалог между этими двумя пользователями // Используем $all, чтобы порядок участников в массиве не имел значения let conversation = await Conversation.findOne({ participants: { $all: [currentUserId, otherUserId] } }) .populate('participants', 'name photos') // Загружаем немного данных об участниках .populate({ // Загружаем последнее сообщение и данные его отправителя path: 'lastMessage', populate: { path: 'sender', select: 'name photos' } }); if (conversation) { console.log(`[CONV_CTRL] Найден существующий диалог: ${conversation._id}`); return res.status(200).json(conversation); } // Если диалога нет, создаем новый console.log(`[CONV_CTRL] Создание нового диалога между ${currentUserId} и ${otherUserId}`); conversation = new Conversation({ participants: [currentUserId, otherUserId] }); await conversation.save(); // Загружаем данные после сохранения, чтобы получить _id и populate участников const newConversation = await Conversation.findById(conversation._id) .populate('participants', 'name photos') // lastMessage пока будет null res.status(201).json(newConversation); } catch (error) { console.error('[CONV_CTRL] Ошибка при создании/получении диалога:', error.message); next(error); } }; // @desc Получить все диалоги текущего пользователя // @route GET /api/conversations // @access Private const getUserConversations = async (req, res, next) => { try { let conversations = await Conversation.find({ participants: req.user._id }) .populate('participants', 'name photos dateOfBirth gender') // Загружаем нужные поля .populate({ path: 'lastMessage', populate: { path: 'sender', select: 'name photos _id' } // Добавил _id отправителя и photos }) .sort({ updatedAt: -1 }); // Сортируем по последнему обновлению (новое сообщение обновит updatedAt) // Добавляем mainPhotoUrl и возраст для участников conversations = conversations.map(conv => { const convObj = conv.toObject(); // Преобразуем в обычный объект для модификации convObj.participants = convObj.participants.map(p => { let mainPhotoUrl = null; if (p.photos && p.photos.length > 0) { const profilePic = p.photos.find(photo => photo.isProfilePhoto === true); mainPhotoUrl = profilePic ? profilePic.url : p.photos[0].url; } let age = null; if (p.dateOfBirth) { const birthDate = new Date(p.dateOfBirth); const today = new Date(); age = today.getFullYear() - birthDate.getFullYear(); const m = today.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { age--; } } // Удаляем исходные поля photos и dateOfBirth из участника, если они не нужны напрямую клиенту в этом контексте const { photos, dateOfBirth, ...participantRest } = p; return { ...participantRest, mainPhotoUrl, age }; // Возвращаем участника с доп. полями }); // Обработка фото для отправителя последнего сообщения, если оно есть if (convObj.lastMessage && convObj.lastMessage.sender && convObj.lastMessage.sender.photos && convObj.lastMessage.sender.photos.length > 0) { const sender = convObj.lastMessage.sender; let senderMainPhotoUrl = null; const profilePic = sender.photos.find(photo => photo.isProfilePhoto === true); senderMainPhotoUrl = profilePic ? profilePic.url : sender.photos[0].url; const { photos, ...senderRest } = sender; convObj.lastMessage.sender = { ...senderRest, mainPhotoUrl: senderMainPhotoUrl }; } else if (convObj.lastMessage && convObj.lastMessage.sender) { // Если фото нет, убедимся, что mainPhotoUrl null и photos не передается const { photos, ...senderRest } = convObj.lastMessage.sender; convObj.lastMessage.sender = { ...senderRest, mainPhotoUrl: null }; } return convObj; }); res.status(200).json(conversations); } catch (error) { console.error('[CONV_CTRL] Ошибка при получении диалогов:', error.message); next(error); } }; // @desc Получить сообщения для конкретного диалога (с пагинацией) // @route GET /api/conversations/:conversationId/messages // @access Private const getMessagesForConversation = async (req, res, next) => { const { conversationId } = req.params; const currentUserId = req.user._id; // Пагинация (пример) const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 20; // Например, 20 сообщений на страницу const skip = (page - 1) * limit; try { // 1. Проверить, является ли текущий пользователь участником этого диалога // и сразу загрузить участников const conversation = await Conversation.findById(conversationId) .populate('participants', 'name photos dateOfBirth gender'); // Загружаем участников здесь if (!conversation || !conversation.participants.some(p => p._id.equals(currentUserId))) { const error = new Error('Диалог не найден или у вас нет к нему доступа.'); error.statusCode = 404; // Или 403 Forbidden return next(error); } // 2. Получить сообщения с пагинацией, отсортированные по дате создания (старые -> новые) // Чтобы отображать в чате снизу вверх, или новые -> старые для отображения сверху вниз const messages = await Message.find({ conversationId: conversationId }) .populate('sender', 'name photos _id') // Загружаем данные отправителя, включая _id .sort({ createdAt: -1 }) // Сначала новые, если нужно отображать с конца .skip(skip) .limit(limit); // Обработка сообщений для добавления mainPhotoUrl отправителю const processedMessages = messages.map(msg => { const msgObj = msg.toObject(); // Преобразуем в обычный объект для модификации if (msgObj.sender && msgObj.sender.photos && msgObj.sender.photos.length > 0) { let senderMainPhotoUrl = null; const profilePic = msgObj.sender.photos.find(photo => photo.isProfilePhoto === true); senderMainPhotoUrl = profilePic ? profilePic.url : msgObj.sender.photos[0].url; msgObj.sender.mainPhotoUrl = senderMainPhotoUrl; } else if (msgObj.sender) { // Если у отправителя нет фото, явно устанавливаем mainPhotoUrl в null msgObj.sender.mainPhotoUrl = null; } return msgObj; }); const totalMessages = await Message.countDocuments({ conversationId: conversationId }); // Обработка данных диалога для добавления mainPhotoUrl и возраста участникам const convObject = conversation.toObject(); convObject.participants = convObject.participants.map(p => { let mainPhotoUrl = null; if (p.photos && p.photos.length > 0) { const profilePic = p.photos.find(photo => photo.isProfilePhoto === true); mainPhotoUrl = profilePic ? profilePic.url : p.photos[0].url; } let age = null; if (p.dateOfBirth) { const birthDate = new Date(p.dateOfBirth); const today = new Date(); age = today.getFullYear() - birthDate.getFullYear(); const m = today.getMonth() - birthDate.getMonth(); if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { age--; } } const { photos, dateOfBirth, ...participantRest } = p; // p здесь это Mongoose документ, нужно p.toObject() или работать с полями напрямую return { _id: p._id, name: p.name, gender: p.gender, mainPhotoUrl, age }; }); res.status(200).json({ conversation: convObject, // <--- ВОТ ЗДЕСЬ ДОБАВЛЯЕМ ДАННЫЕ ДИАЛОГА messages: processedMessages.reverse(), // reverse(), чтобы старые были в начале массива (для типичного отображения чата) currentPage: page, totalPages: Math.ceil(totalMessages / limit), totalMessages }); } catch (error) { console.error(`[CONV_CTRL] Ошибка при получении сообщений для диалога ${conversationId}:`, error.message); next(error); } }; // @desc Удалить сообщение // @route DELETE /api/conversations/:conversationId/messages/:messageId // @access Private const deleteMessage = async (req, res, next) => { const { conversationId, messageId } = req.params; const currentUserId = req.user._id; try { const conversation = await Conversation.findById(conversationId); if (!conversation) { const error = new Error('Диалог не найден.'); error.statusCode = 404; return next(error); } if (!conversation.participants.includes(currentUserId)) { const error = new Error('Вы не являетесь участником этого диалога.'); error.statusCode = 403; return next(error); } const message = await Message.findById(messageId); if (!message) { const error = new Error('Сообщение не найдено.'); error.statusCode = 404; return next(error); } if (!message.sender.equals(currentUserId)) { const error = new Error('Вы можете удалять только свои сообщения.'); error.statusCode = 403; return next(error); } // Проверяем, является ли удаляемое сообщение последним в диалоге const isLastMessage = conversation.lastMessage && conversation.lastMessage.equals(messageId); await Message.findByIdAndDelete(messageId); // Если удаленное сообщение было последним, обновить lastMessage в диалоге if (isLastMessage) { const newLastMessage = await Message.findOne({ conversationId }) .sort({ createdAt: -1 }); // Находим самое новое сообщение conversation.lastMessage = newLastMessage ? newLastMessage._id : null; await conversation.save(); } // Эмитим событие удаления сообщения в комнату диалога // Предполагается, что `io` доступен через `req.app.get('io')` const io = req.app.get('io'); if (io) { io.to(conversationId).emit('messageDeleted', { messageId, conversationId, deleterId: currentUserId }); console.log(`[CONV_CTRL] Emitted messageDeleted to room ${conversationId} for message ${messageId}`); } else { console.warn('[CONV_CTRL] Socket.io instance (io) not found on req.app. Could not emit messageDeleted.'); } res.status(200).json({ message: 'Сообщение успешно удалено.' }); } catch (error) { console.error(`[CONV_CTRL] Ошибка при удалении сообщения ${messageId} из диалога ${conversationId}:`, error.message); next(error); } }; module.exports = { createOrGetConversation, getUserConversations, getMessagesForConversation, deleteMessage, // <-- Добавляем новый метод };