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 adminRoutes = require('./routes/adminRoutes'); // Импортируем маршруты админа const reportRoutes = require('./routes/reportRoutes'); // Импортируем маршруты для жалоб const Message = require('./models/Message'); // Импорт модели Message const Conversation = require('./models/Conversation'); // Импорт модели Conversation const initAdminAccount = require('./utils/initAdmin'); // Импорт инициализации админ-аккаунта // Загружаем переменные окружения до их использования // 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"] } }); // Сделать экземпляр io доступным в запросах app.set('io', io); // Подключение к базе данных - проверяем, что импортировали функцию if (typeof connectDBModule === 'function') { try { connectDBModule() // Вызов функции подключения .then(async () => { console.log('Успешное подключение к базе данных'); console.log('Инициализация административного аккаунта...'); // Инициализируем админ-аккаунт после подключения к БД try { await initAdminAccount(); console.log('Инициализация администратора завершена'); } catch (adminError) { console.error('Ошибка при инициализации административного аккаунта:', adminError); } }) .catch(err => { console.error('Ошибка при подключении к базе данных:', err); }); 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 работает!'); }); // Подключаем маршруты app.use('/api/auth', authRoutes); app.use('/api/users', userRoutes); // 2. Подключаем userRoutes с префиксом /api/users app.use('/api/actions', actionRoutes); app.use('/api/conversations', conversationRoutes); app.use('/api/reports', reportRoutes); // Подключаем маршруты для жалоб app.use('/api/admin', adminRoutes); // Подключаем маршруты для админ-панели // 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); }; 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}`); }); // Отправка сообщения 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, status: 'sending' // Начальный статус - отправка }); 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, status: populatedMessage.status }; // После успешного сохранения в БД меняем статус на "delivered" await Message.findByIdAndUpdate(newMessage._id, { status: 'delivered' }); messageToSend.status = 'delivered'; // Убедимся, что статус установлен явно 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) }, { $addToSet: { readBy: userId }, // Добавить ID текущего пользователя в массив readBy $set: { status: 'read' } // Обновляем статус сообщения на "прочитано" } ); 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, readerId: userId, // ID пользователя, который прочитал сообщения status: 'read' // Добавляем статус "прочитано" }); 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 соединения`); });