2025-05-21 22:13:09 +07:00
|
|
|
|
const dotenv = require('dotenv');
|
|
|
|
|
// Загружаем переменные окружения как можно раньше
|
|
|
|
|
dotenv.config();
|
|
|
|
|
|
|
|
|
|
const express = require('express');
|
|
|
|
|
const http = require('http'); // <--- Импорт http
|
|
|
|
|
const { Server } = require("socket.io"); // <--- Импорт Server из socket.io
|
|
|
|
|
const conversationRoutes = require('./routes/conversationRoutes');
|
|
|
|
|
// const dotenv = require('dotenv'); // Уже импортировано выше
|
|
|
|
|
const cors = require('cors');
|
|
|
|
|
// Импортируем модуль для подключения к БД
|
|
|
|
|
const connectDBModule = require('./config/db');
|
|
|
|
|
const authRoutes = require('./routes/authRoutes'); // <--- 1. РАСКОММЕНТИРУЙ ИЛИ ДОБАВЬ ЭТОТ ИМПОРТ
|
|
|
|
|
const userRoutes = require('./routes/userRoutes'); // 1. Импортируем userRoutes
|
|
|
|
|
const actionRoutes = require('./routes/actionRoutes'); // 1. Импортируем actionRoutes
|
|
|
|
|
const Message = require('./models/Message'); // Импорт модели Message
|
|
|
|
|
const Conversation = require('./models/Conversation'); // Импорт модели Conversation
|
|
|
|
|
|
|
|
|
|
// Загружаем переменные окружения до их использования
|
|
|
|
|
// dotenv.config(); // Перемещено в начало файла
|
|
|
|
|
|
|
|
|
|
// Проверяем, что JWT_SECRET правильно загружен
|
|
|
|
|
console.log('JWT_SECRET loaded:', process.env.JWT_SECRET ? 'Yes' : 'No');
|
|
|
|
|
|
|
|
|
|
// Добавляем отладочное логирование
|
|
|
|
|
console.log('Тип импортированного модуля connectDB:', typeof connectDBModule);
|
|
|
|
|
|
|
|
|
|
const app = express();
|
|
|
|
|
const server = http.createServer(app); // <--- Создание HTTP сервера
|
|
|
|
|
const io = new Server(server, { // <--- Инициализация socket.io
|
|
|
|
|
cors: {
|
|
|
|
|
origin: "*", // <--- Разрешаем все источники
|
|
|
|
|
methods: ["GET", "POST"]
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-22 00:37:45 +07:00
|
|
|
|
// Сделать экземпляр io доступным в запросах
|
|
|
|
|
app.set('io', io);
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
// Подключение к базе данных - проверяем, что импортировали функцию
|
|
|
|
|
if (typeof connectDBModule === 'function') {
|
|
|
|
|
try {
|
|
|
|
|
connectDBModule(); // Вызов функции подключения
|
|
|
|
|
console.log('Попытка подключения к базе данных была инициирована...'); // Изменил лог для ясности
|
|
|
|
|
} catch (err) {
|
|
|
|
|
// Этот catch здесь, скорее всего, не поймает асинхронные ошибки из connectDBModule,
|
|
|
|
|
// так как connectDBModule асинхронная и не возвращает Promise, который здесь ожидался бы с await.
|
|
|
|
|
// Ошибки из connectDBModule (если они есть) будут выведены внутри самой функции connectDBModule.
|
|
|
|
|
console.error('Синхронная ошибка при вызове connectDBModule (маловероятно):', err);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.error('Ошибка: connectDB не является функцией! Тип:', typeof connectDBModule);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Настройка CORS для Express
|
|
|
|
|
app.use(cors({
|
|
|
|
|
origin: "*", // <--- Разрешаем все источники
|
|
|
|
|
credentials: true // Если нужны куки или заголовки авторизации
|
|
|
|
|
}));
|
|
|
|
|
app.use(express.json());
|
|
|
|
|
app.use(express.urlencoded({ extended: false }));
|
|
|
|
|
|
|
|
|
|
app.get('/', (req, res) => {
|
|
|
|
|
res.send('API для Dating App работает!');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 2. РАСКОММЕНТИРУЙ ИЛИ ДОБАВЬ ЭТИ СТРОКИ для подключения маршрутов аутентификации
|
|
|
|
|
app.use('/api/auth', authRoutes);
|
|
|
|
|
app.use('/api/users', userRoutes); // 2. Подключаем userRoutes с префиксом /api/users
|
|
|
|
|
app.use('/api/actions', actionRoutes);
|
|
|
|
|
app.use('/api/conversations', conversationRoutes);
|
|
|
|
|
|
|
|
|
|
// Socket.IO логика
|
|
|
|
|
let activeUsers = [];
|
|
|
|
|
|
|
|
|
|
io.on("connection", (socket) => {
|
|
|
|
|
console.log("A user connected:", socket.id);
|
|
|
|
|
|
|
|
|
|
// Добавление нового пользователя
|
|
|
|
|
socket.on("addUser", (userId) => {
|
|
|
|
|
// Проверяем, не добавлен ли уже пользователь с таким userId, но другим socket.id
|
|
|
|
|
// Это может произойти, если пользователь открыл несколько вкладок или переподключился
|
|
|
|
|
const existingUser = activeUsers.find(user => user.userId === userId);
|
|
|
|
|
if (existingUser) {
|
|
|
|
|
// Обновляем socket.id для существующего userId
|
|
|
|
|
existingUser.socketId = socket.id;
|
|
|
|
|
} else {
|
|
|
|
|
// Добавляем нового пользователя
|
|
|
|
|
activeUsers.push({ userId, socketId: socket.id });
|
|
|
|
|
}
|
|
|
|
|
io.emit("getUsers", activeUsers); // Отправляем обновленный список активных пользователей всем клиентам
|
|
|
|
|
console.log("User added to active users:", userId, socket.id);
|
|
|
|
|
console.log("Active users:", activeUsers);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Вспомогательная функция для получения сокета пользователя
|
|
|
|
|
const getUserSocket = (userId) => {
|
|
|
|
|
return activeUsers.find(u => u.userId === userId);
|
|
|
|
|
};
|
|
|
|
|
|
2025-05-22 00:37:45 +07:00
|
|
|
|
socket.on("joinConversationRoom", (roomId) => {
|
|
|
|
|
socket.join(roomId);
|
|
|
|
|
console.log(`[Socket.IO] User ${socket.id} joined room ${roomId}`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
socket.on("leaveConversationRoom", (roomId) => {
|
|
|
|
|
socket.leave(roomId);
|
|
|
|
|
console.log(`[Socket.IO] User ${socket.id} left room ${roomId}`);
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
// Отправка сообщения
|
|
|
|
|
socket.on("sendMessage", async ({ senderId, receiverId, text, clientConversationId }) => { // Добавлен clientConversationId, async
|
|
|
|
|
console.log(`"[Socket.IO] Получено сообщение от ${senderId} для ${receiverId}: "${text}" (попытка для диалога: ${clientConversationId})"`);
|
|
|
|
|
|
|
|
|
|
const getUserSocket = (userId) => {
|
|
|
|
|
const user = activeUsers.find(u => u.userId === userId);
|
|
|
|
|
return user ? user : null; // Возвращаем объект пользователя или null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 1. Найти или создать диалог
|
|
|
|
|
let conversation = await Conversation.findOne({
|
|
|
|
|
participants: { $all: [senderId, receiverId] }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!conversation) {
|
|
|
|
|
console.log(`[Socket.IO] Диалог не найден, создаем новый между ${senderId} и ${receiverId}`);
|
|
|
|
|
conversation = new Conversation({
|
|
|
|
|
participants: [senderId, receiverId]
|
|
|
|
|
// lastMessage будет установлено позже
|
|
|
|
|
});
|
|
|
|
|
// Не сохраняем conversation здесь сразу, сохраним вместе с lastMessage
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[Socket.IO] Найден существующий диалог: ${conversation._id}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Создать и сохранить новое сообщение
|
|
|
|
|
const newMessage = new Message({
|
|
|
|
|
conversationId: conversation._id, // Используем ID найденного или нового (но еще не сохраненного) диалога
|
|
|
|
|
sender: senderId,
|
|
|
|
|
text: text,
|
2025-05-24 02:30:51 +07:00
|
|
|
|
status: 'sending' // Начальный статус - отправка
|
2025-05-21 22:13:09 +07:00
|
|
|
|
});
|
|
|
|
|
await newMessage.save();
|
|
|
|
|
console.log(`[Socket.IO] Сообщение сохранено в БД: ${newMessage._id}`);
|
|
|
|
|
|
|
|
|
|
// 3. Обновить lastMessage и updatedAt в диалоге
|
|
|
|
|
conversation.lastMessage = newMessage._id;
|
|
|
|
|
// conversation.lastMessageTimestamp = newMessage.createdAt; // Если используем это поле
|
|
|
|
|
await conversation.save(); // Теперь сохраняем диалог (или обновляем существующий)
|
|
|
|
|
console.log(`[Socket.IO] Диалог ${conversation._id} обновлен с lastMessage.`);
|
|
|
|
|
|
|
|
|
|
// 4. Подготовить данные сообщения для отправки клиентам (с populated sender)
|
|
|
|
|
const populatedMessage = await Message.findById(newMessage._id)
|
|
|
|
|
.populate('sender', 'name photos'); // Загружаем данные отправителя
|
|
|
|
|
|
|
|
|
|
// 5. Отправить сообщение обоим участникам диалога (отправителю для подтверждения и получателю)
|
|
|
|
|
const senderSocketInfo = getUserSocket(senderId);
|
|
|
|
|
const receiverSocketInfo = getUserSocket(receiverId);
|
|
|
|
|
|
|
|
|
|
const messageToSend = {
|
|
|
|
|
_id: populatedMessage._id,
|
|
|
|
|
conversationId: populatedMessage.conversationId,
|
|
|
|
|
sender: populatedMessage.sender, // Объект с name, photos
|
|
|
|
|
text: populatedMessage.text,
|
|
|
|
|
createdAt: populatedMessage.createdAt,
|
2025-05-24 02:30:51 +07:00
|
|
|
|
status: populatedMessage.status
|
2025-05-21 22:13:09 +07:00
|
|
|
|
};
|
|
|
|
|
|
2025-05-24 02:30:51 +07:00
|
|
|
|
// После успешного сохранения в БД меняем статус на "delivered"
|
|
|
|
|
await Message.findByIdAndUpdate(newMessage._id, { status: 'delivered' });
|
|
|
|
|
messageToSend.status = 'delivered';
|
|
|
|
|
|
2025-05-21 22:13:09 +07:00
|
|
|
|
if (senderSocketInfo && senderSocketInfo.socketId) {
|
|
|
|
|
io.to(senderSocketInfo.socketId).emit("getMessage", messageToSend);
|
|
|
|
|
console.log(`[Socket.IO] Сообщение (подтверждение) отправлено отправителю ${senderId}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (receiverSocketInfo && receiverSocketInfo.socketId) {
|
|
|
|
|
io.to(receiverSocketInfo.socketId).emit("getMessage", messageToSend);
|
|
|
|
|
console.log(`[Socket.IO] Сообщение отправлено получателю ${receiverId}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[Socket.IO] Получатель ${receiverId} не онлайн / сокет не найден.`);
|
|
|
|
|
// TODO: Push-уведомление
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[Socket.IO] Ошибка при обработке sendMessage:', error.message, error.stack);
|
|
|
|
|
const senderSocketInfo = getUserSocket(senderId);
|
|
|
|
|
if (senderSocketInfo && senderSocketInfo.socketId) {
|
|
|
|
|
io.to(senderSocketInfo.socketId).emit("messageError", {
|
|
|
|
|
message: "Не удалось отправить сообщение.",
|
|
|
|
|
originalText: text
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Событие: Пользователь прочитал сообщения
|
|
|
|
|
socket.on("markMessagesAsRead", async ({ conversationId, userId }) => {
|
|
|
|
|
console.log(`[Socket.IO] User ${userId} marked messages as read in conversation ${conversationId}`);
|
|
|
|
|
try {
|
|
|
|
|
// 1. Найти все сообщения в диалоге, отправленные НЕ userId, и где userId еще нет в readBy
|
|
|
|
|
const result = await Message.updateMany(
|
|
|
|
|
{
|
|
|
|
|
conversationId: conversationId,
|
|
|
|
|
sender: { $ne: userId }, // Сообщения от другого пользователя
|
|
|
|
|
readBy: { $ne: userId } // Которые текущий пользователь еще не читал (его ID нет в readBy)
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-05-24 02:30:51 +07:00
|
|
|
|
$addToSet: { readBy: userId }, // Добавить ID текущего пользователя в массив readBy
|
|
|
|
|
$set: { status: 'read' } // Обновляем статус сообщения на "прочитано"
|
2025-05-21 22:13:09 +07:00
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log(`[Socket.IO] Messages updated for read status: ${result.modifiedCount} in conversation ${conversationId} by user ${userId}`);
|
|
|
|
|
|
|
|
|
|
if (result.modifiedCount > 0) {
|
|
|
|
|
// 2. Найти другого участника диалога, чтобы уведомить его
|
|
|
|
|
const conversation = await Conversation.findById(conversationId);
|
|
|
|
|
if (!conversation) {
|
|
|
|
|
console.log(`[Socket.IO] Conversation ${conversationId} not found for sending read notification.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const otherParticipantId = conversation.participants.find(pId => pId.toString() !== userId);
|
|
|
|
|
|
|
|
|
|
if (otherParticipantId) {
|
|
|
|
|
const otherUserSocket = getUserSocket(otherParticipantId.toString());
|
|
|
|
|
if (otherUserSocket && otherUserSocket.socketId) {
|
|
|
|
|
io.to(otherUserSocket.socketId).emit("messagesRead", {
|
|
|
|
|
conversationId: conversationId,
|
2025-05-24 02:30:51 +07:00
|
|
|
|
readerId: userId, // ID пользователя, который прочитал сообщения
|
|
|
|
|
status: 'read' // Добавляем статус "прочитано"
|
2025-05-21 22:13:09 +07:00
|
|
|
|
});
|
|
|
|
|
console.log(`[Socket.IO] Sent 'messagesRead' event to ${otherParticipantId} for conversation ${conversationId}`);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(`[Socket.IO] Other participant ${otherParticipantId} for messagesRead notification is not online or socket not found.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[Socket.IO] Error in markMessagesAsRead:', error.message, error.stack);
|
|
|
|
|
// Можно отправить ошибку обратно клиенту, если это необходимо
|
|
|
|
|
// socket.emit('markMessagesAsReadError', { conversationId, message: 'Failed to mark messages as read' });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Пользователь печатает
|
|
|
|
|
socket.on("typing", ({ conversationId, receiverId, senderId }) => { // Добавлен senderId, conversationId
|
|
|
|
|
const receiver = activeUsers.find(user => user.userId === receiverId);
|
|
|
|
|
if (receiver) {
|
|
|
|
|
io.to(receiver.socketId).emit("userTyping", { conversationId, senderId }); // Добавлен conversationId
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Пользователь перестал печатать
|
|
|
|
|
socket.on("stopTyping", ({ conversationId, receiverId, senderId }) => { // Добавлен senderId, conversationId
|
|
|
|
|
const receiver = activeUsers.find(user => user.userId === receiverId);
|
|
|
|
|
if (receiver) {
|
|
|
|
|
io.to(receiver.socketId).emit("userStopTyping", { conversationId, senderId }); // Добавлен conversationId
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Отключение пользователя
|
|
|
|
|
socket.on("disconnect", () => {
|
|
|
|
|
activeUsers = activeUsers.filter(user => user.socketId !== socket.id);
|
|
|
|
|
io.emit("getUsers", activeUsers); // Обновляем список у всех клиентов
|
|
|
|
|
console.log("User disconnected:", socket.id);
|
|
|
|
|
console.log("Active users:", activeUsers);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const errorHandler = (err, req, res, next) => {
|
|
|
|
|
console.error('--- ERROR HANDLER ---');
|
|
|
|
|
console.error('Name:', err.name);
|
|
|
|
|
console.error('Message:', err.message);
|
|
|
|
|
console.error('StatusCode on err object:', err.statusCode);
|
|
|
|
|
console.error('Current res.statusCode:', res.statusCode);
|
|
|
|
|
// console.error('Stack:', err.stack);
|
|
|
|
|
|
|
|
|
|
let finalStatusCode;
|
|
|
|
|
let finalMessage = err.message || 'Произошла ошибка на сервере';
|
|
|
|
|
|
|
|
|
|
// Приоритетная обработка специфических ошибок Mongoose
|
|
|
|
|
if (err.name === 'CastError') {
|
|
|
|
|
finalStatusCode = 400; // Или 404, если считаешь, что неверный ID = "не найдено"
|
|
|
|
|
finalMessage = `Неверный формат ID ресурса: ${err.path} со значением '${err.value}' не является валидным ObjectId.`;
|
|
|
|
|
console.log(`Mongoose CastError detected. Setting status to ${finalStatusCode}`);
|
|
|
|
|
} else if (err.name === 'ValidationError') {
|
|
|
|
|
finalStatusCode = 400;
|
|
|
|
|
console.log(`Mongoose ValidationError detected. Setting status to ${finalStatusCode}`);
|
|
|
|
|
} else if (err.statusCode) {
|
|
|
|
|
finalStatusCode = err.statusCode;
|
|
|
|
|
console.log(`Error has statusCode property. Setting status to ${finalStatusCode}`);
|
|
|
|
|
} else {
|
|
|
|
|
finalStatusCode = 500; // Default to 500
|
|
|
|
|
console.log(`Defaulting status to ${finalStatusCode}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`[ERROR_HANDLER] Final StatusCode to be sent: ${finalStatusCode}`);
|
|
|
|
|
|
|
|
|
|
if (res.headersSent) {
|
|
|
|
|
console.error('[ERROR_HANDLER] Headers already sent.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.status(finalStatusCode).json({
|
|
|
|
|
message: finalMessage,
|
|
|
|
|
stack: process.env.NODE_ENV === 'production' ? null : err.stack,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
app.use(errorHandler);
|
|
|
|
|
|
|
|
|
|
const PORT = process.env.PORT || 5000;
|
|
|
|
|
|
|
|
|
|
// Запуск HTTP сервера вместо app.listen
|
|
|
|
|
server.listen(PORT, () => {
|
|
|
|
|
console.log(`Сервер запущен на порту ${PORT} и слушает WebSocket соединения`);
|
|
|
|
|
});
|