Reflex/backend/controllers/conversationController.js

391 lines
17 KiB
JavaScript
Raw Permalink Normal View History

2025-05-21 22:13:09 +07:00
// 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);
2025-05-24 02:30:51 +07:00
// Обработка сообщений для добавления mainPhotoUrl отправителю и добавления status
2025-05-21 22:13:09 +07:00
const processedMessages = messages.map(msg => {
const msgObj = msg.toObject(); // Преобразуем в обычный объект для модификации
2025-05-24 02:30:51 +07:00
// Добавляем mainPhotoUrl для отправителя сообщения
2025-05-21 22:13:09 +07:00
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;
}
2025-05-24 02:30:51 +07:00
// Добавляем информацию о статусе сообщения
// Если статус не установлен, определяем его на основе readBy
if (!msgObj.status) {
if (msgObj.readBy && msgObj.readBy.length > 0) {
msgObj.status = 'read';
} else {
msgObj.status = 'delivered';
}
}
2025-05-21 22:13:09 +07:00
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);
}
};
2025-05-21 22:13:09 +07:00
module.exports = {
createOrGetConversation,
getUserConversations,
getMessagesForConversation,
deleteMessage,
editMessage, // <-- Добавляем новый метод
2025-05-21 22:13:09 +07:00
};