Reflex/backend/controllers/conversationController.js
Professional c14811bdac фикс
2025-05-24 02:30:51 +07:00

391 lines
17 KiB
JavaScript
Raw Permalink 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 отправителю и добавления status
const processedMessages = messages.map(msg => {
const msgObj = msg.toObject(); // Преобразуем в обычный объект для модификации
// Добавляем mainPhotoUrl для отправителя сообщения
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;
}
// Добавляем информацию о статусе сообщения
// Если статус не установлен, определяем его на основе readBy
if (!msgObj.status) {
if (msgObj.readBy && msgObj.readBy.length > 0) {
msgObj.status = 'read';
} else {
msgObj.status = 'delivered';
}
}
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);
}
};
// @desc Редактировать сообщение
// @route PUT /api/conversations/:conversationId/messages/:messageId
// @access Private
const editMessage = async (req, res, next) => {
const { conversationId, messageId } = req.params;
const { text } = req.body;
const currentUserId = req.user._id;
if (!text || text.trim() === '') {
const error = new Error('Текст сообщения не может быть пустым.');
error.statusCode = 400;
return next(error);
}
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);
}
// Ограничение по времени на редактирование (например, 24 часа)
// const twentyFourHours = 24 * 60 * 60 * 1000;
// if (new Date() - new Date(message.createdAt) > twentyFourHours) {
// const error = new Error('Сообщение не может быть отредактировано по истечении 24 часов.');
// error.statusCode = 403;
// return next(error);
// }
message.text = text.trim();
message.isEdited = true;
message.editedAt = new Date();
const updatedMessage = await message.save();
// Загружаем данные отправителя для ответа клиенту
await updatedMessage.populate('sender', 'name photos _id');
// Эмитим событие редактирования сообщения в комнату диалога
const io = req.app.get('io');
if (io) {
io.to(conversationId).emit('messageEdited', {
message: updatedMessage.toObject(), // Отправляем обновленное сообщение целиком
conversationId,
});
console.log(`[CONV_CTRL] Emitted messageEdited to room ${conversationId} for message ${messageId}`);
} else {
console.warn('[CONV_CTRL] Socket.io instance (io) not found on req.app. Could not emit messageEdited.');
}
res.status(200).json(updatedMessage);
} catch (error) {
console.error(`[CONV_CTRL] Ошибка при редактировании сообщения ${messageId} из диалога ${conversationId}:`, error.message);
next(error);
}
};
module.exports = {
createOrGetConversation,
getUserConversations,
getMessagesForConversation,
deleteMessage,
editMessage, // <-- Добавляем новый метод
};