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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-22 00:28:14 +07:00
|
|
|
|
// @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,
|
2025-05-22 00:28:14 +07:00
|
|
|
|
deleteMessage,
|
|
|
|
|
editMessage, // <-- Добавляем новый метод
|
2025-05-21 22:13:09 +07:00
|
|
|
|
};
|