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