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