300 lines
15 KiB
JavaScript
300 lines
15 KiB
JavaScript
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"]
|
||
}
|
||
});
|
||
|
||
// Подключение к базе данных - проверяем, что импортировали функцию
|
||
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);
|
||
};
|
||
|
||
// Отправка сообщения
|
||
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,
|
||
});
|
||
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,
|
||
};
|
||
|
||
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
|
||
}
|
||
);
|
||
|
||
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 пользователя, который прочитал сообщения
|
||
});
|
||
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 соединения`);
|
||
}); |