Reflex/backend/controllers/conversationController.js
2025-05-21 22:13:09 +07:00

298 lines
14 KiB
JavaScript
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.

// 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, // <-- Добавляем новый метод
};