// api-chat.js - API для чата задач const express = require('express'); const router = express.Router(); const path = require('path'); const fs = require('fs'); module.exports = function(app, db, upload) { // Получаем функции из database.js let logActivity, checkTaskAccess; // Пытаемся импортировать функции try { const dbModule = require('./database'); logActivity = dbModule.logActivity; checkTaskAccess = dbModule.checkTaskAccess; console.log('✅ Функции database.js загружены в api-chat'); } catch (error) { console.error('❌ Ошибка загрузки функций из database.js:', error); // Создаем заглушки logActivity = (taskId, userId, action, details) => { console.log(`[LOG] Task ${taskId}, User ${userId}: ${action} - ${details}`); }; checkTaskAccess = (userId, taskId, callback) => { // Заглушка - даем доступ всем callback(null, true); }; } // Middleware для аутентификации const requireAuth = (req, res, next) => { if (!req.session || !req.session.user) { return res.status(401).json({ error: 'Требуется аутентификация' }); } next(); }; // GET /api/chat/tasks/:taskId/messages - Получить сообщения чата задачи router.get('/api/chat/tasks/:taskId/messages', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; const { before, limit = 50 } = req.query; console.log(`📨 Запрос сообщений для задачи ${taskId} от пользователя ${userId}`); // Проверяем доступ к задаче checkTaskAccess(userId, taskId, (err, hasAccess) => { if (err) { console.error('❌ Ошибка проверки доступа:', err); return res.status(500).json({ error: 'Ошибка проверки доступа' }); } if (!hasAccess) { return res.status(403).json({ error: 'Нет доступа к задаче' }); } // Сначала проверим существование таблицы db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='task_chat_messages'", [], (tableErr, tableExists) => { if (tableErr || !tableExists) { console.error('❌ Таблица task_chat_messages не существует'); return res.status(500).json({ error: 'Таблица чата не создана', messages: [], hasMore: false }); } let query = ` SELECT m.*, u.name as user_name, u.login as user_login, rm.message as reply_to_message, ru.name as reply_to_user_name FROM task_chat_messages m LEFT JOIN users u ON m.user_id = u.id LEFT JOIN task_chat_messages rm ON m.reply_to_id = rm.id LEFT JOIN users ru ON rm.user_id = ru.id WHERE m.task_id = ? AND m.is_deleted = 0 `; const params = [taskId]; if (before) { query += ` AND m.created_at < ?`; params.push(before); } query += ` ORDER BY m.created_at DESC LIMIT ?`; params.push(parseInt(limit)); db.all(query, params, (err, messages) => { if (err) { console.error('❌ Ошибка получения сообщений:', err); return res.status(500).json({ error: 'Ошибка получения сообщений', details: err.message }); } // Получаем файлы для каждого сообщения const messageIds = messages.map(m => m.id); if (messageIds.length === 0) { return res.json({ messages: [], hasMore: false }); } const placeholders = messageIds.map(() => '?').join(','); db.all(` SELECT * FROM task_chat_files WHERE message_id IN (${placeholders}) `, messageIds, (fileErr, files) => { if (fileErr) { console.error('❌ Ошибка получения файлов:', fileErr); } // Группируем файлы по сообщениям const filesByMessage = {}; if (files) { files.forEach(file => { if (!filesByMessage[file.message_id]) { filesByMessage[file.message_id] = []; } filesByMessage[file.message_id].push(file); }); } // Добавляем файлы к сообщениям const messagesWithFiles = messages.map(msg => ({ ...msg, files: filesByMessage[msg.id] || [] })); // Помечаем сообщения как прочитанные if (messagesWithFiles.length > 0) { const unreadMessageIds = messagesWithFiles .filter(m => m.user_id != userId) .map(m => m.id); if (unreadMessageIds.length > 0) { const unreadPlaceholders = unreadMessageIds.map(() => '?').join(','); db.run(` INSERT OR IGNORE INTO task_chat_reads (message_id, user_id) SELECT id, ? FROM task_chat_messages WHERE id IN (${unreadPlaceholders}) `, [userId, ...unreadMessageIds], (readErr) => { if (readErr) console.error('❌ Ошибка отметки прочитанных:', readErr); }); } } res.json({ messages: messagesWithFiles, hasMore: messages.length === parseInt(limit) }); }); }); }); }); }); // POST /api/chat/tasks/:taskId/messages - Отправить сообщение router.post('/api/chat/tasks/:taskId/messages', requireAuth, upload.array('files', 5), (req, res) => { const { taskId } = req.params; const { message, reply_to_id } = req.body; const userId = req.session.user.id; console.log(`📝 Отправка сообщения в задачу ${taskId} от пользователя ${userId}`); if (!message || message.trim() === '') { return res.status(400).json({ error: 'Сообщение не может быть пустым' }); } // Проверяем доступ к задаче checkTaskAccess(userId, taskId, (err, hasAccess) => { if (err || !hasAccess) { return res.status(403).json({ error: 'Нет доступа к задаче' }); } // Проверяем существование таблицы db.get("SELECT name FROM sqlite_master WHERE type='table' AND name='task_chat_messages'", [], (tableErr, tableExists) => { if (tableErr || !tableExists) { console.error('❌ Таблица task_chat_messages не существует'); return res.status(500).json({ error: 'Система чата не инициализирована' }); } // Вставляем сообщение db.run( `INSERT INTO task_chat_messages (task_id, user_id, message, reply_to_id) VALUES (?, ?, ?, ?)`, [taskId, userId, message.trim(), reply_to_id || null], function(err) { if (err) { console.error('❌ Ошибка создания сообщения:', err); return res.status(500).json({ error: 'Ошибка создания сообщения' }); } const messageId = this.lastID; const uploadedFiles = []; // Если есть файлы, сохраняем их if (req.files && req.files.length > 0) { const chatDir = path.join(__dirname, 'data', 'uploads', 'chat', taskId.toString()); if (!fs.existsSync(chatDir)) { fs.mkdirSync(chatDir, { recursive: true }); } let filesProcessed = 0; req.files.forEach((file, index) => { const fileExt = path.extname(file.originalname); const fileName = `${messageId}_${Date.now()}_${index}${fileExt}`; const filePath = path.join(chatDir, fileName); try { fs.renameSync(file.path, filePath); } catch (renameErr) { console.error('❌ Ошибка перемещения файла:', renameErr); } db.run( `INSERT INTO task_chat_files (message_id, file_path, original_name, file_size, file_type) VALUES (?, ?, ?, ?, ?)`, [messageId, filePath, file.originalname, file.size, file.mimetype], function(fileErr) { if (!fileErr) { uploadedFiles.push({ id: this.lastID, original_name: file.originalname, file_size: file.size, file_type: file.mimetype }); } filesProcessed++; if (filesProcessed === req.files.length) { finishTransaction(); } } ); }); } else { finishTransaction(); } function finishTransaction() { // Логируем действие if (logActivity) { logActivity(parseInt(taskId), userId, 'CHAT_MESSAGE', 'Отправлено сообщение в чат'); } // Получаем полную информацию о сообщении db.get(` SELECT m.*, u.name as user_name, u.login as user_login, rm.message as reply_to_message, ru.name as reply_to_user_name FROM task_chat_messages m LEFT JOIN users u ON m.user_id = u.id LEFT JOIN task_chat_messages rm ON m.reply_to_id = rm.id LEFT JOIN users ru ON rm.user_id = ru.id WHERE m.id = ? `, [messageId], (err, newMessage) => { if (err) { console.error('❌ Ошибка получения сообщения:', err); return res.json({ success: true, messageId, files: uploadedFiles }); } newMessage.files = uploadedFiles; // Отправляем уведомления участникам задачи notifyTaskParticipants(taskId, userId, newMessage); res.json({ success: true, message: newMessage }); }); } } ); }); }); }); // PUT /api/chat/messages/:messageId - Редактировать сообщение router.put('/api/chat/messages/:messageId', requireAuth, (req, res) => { const { messageId } = req.params; const { message } = req.body; const userId = req.session.user.id; if (!message || message.trim() === '') { return res.status(400).json({ error: 'Сообщение не может быть пустым' }); } db.get( 'SELECT task_id, user_id FROM task_chat_messages WHERE id = ? AND is_deleted = 0', [messageId], (err, msg) => { if (err || !msg) { return res.status(404).json({ error: 'Сообщение не найдено' }); } // Проверяем, что пользователь - автор сообщения или админ if (parseInt(msg.user_id) !== parseInt(userId) && req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Нет прав для редактирования этого сообщения' }); } db.run( `UPDATE task_chat_messages SET message = ?, is_edited = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, [message.trim(), messageId], function(err) { if (err) { console.error('❌ Ошибка редактирования сообщения:', err); return res.status(500).json({ error: 'Ошибка редактирования сообщения' }); } if (logActivity) { logActivity(msg.task_id, userId, 'CHAT_MESSAGE_EDITED', 'Сообщение отредактировано'); } res.json({ success: true, message: 'Сообщение отредактировано' }); } ); } ); }); // DELETE /api/chat/messages/:messageId - Удалить сообщение (soft delete) router.delete('/api/chat/messages/:messageId', requireAuth, (req, res) => { const { messageId } = req.params; const userId = req.session.user.id; db.get( 'SELECT task_id, user_id FROM task_chat_messages WHERE id = ? AND is_deleted = 0', [messageId], (err, msg) => { if (err || !msg) { return res.status(404).json({ error: 'Сообщение не найдено' }); } // Проверяем, что пользователь - автор сообщения или админ if (parseInt(msg.user_id) !== parseInt(userId) && req.session.user.role !== 'admin') { return res.status(403).json({ error: 'Нет прав для удаления этого сообщения' }); } db.run( 'UPDATE task_chat_messages SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [messageId], function(err) { if (err) { console.error('❌ Ошибка удаления сообщения:', err); return res.status(500).json({ error: 'Ошибка удаления сообщения' }); } if (logActivity) { logActivity(msg.task_id, userId, 'CHAT_MESSAGE_DELETED', 'Сообщение удалено'); } res.json({ success: true, message: 'Сообщение удалено' }); } ); } ); }); // GET /api/chat/tasks/:taskId/unread-count - Получить количество непрочитанных сообщений router.get('/api/chat/tasks/:taskId/unread-count', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; checkTaskAccess(userId, taskId, (err, hasAccess) => { if (err || !hasAccess) { return res.status(403).json({ error: 'Нет доступа к задаче' }); } db.get(` SELECT COUNT(*) as unread_count FROM task_chat_messages m LEFT JOIN task_chat_reads r ON m.id = r.message_id AND r.user_id = ? WHERE m.task_id = ? AND m.user_id != ? AND m.is_deleted = 0 AND r.id IS NULL `, [userId, taskId, userId], (err, result) => { if (err) { console.error('❌ Ошибка подсчета непрочитанных:', err); return res.status(500).json({ error: 'Ошибка подсчета' }); } res.json({ unread_count: result?.unread_count || 0 }); }); }); }); // POST /api/chat/tasks/:taskId/mark-read - Отметить все сообщения как прочитанные router.post('/api/chat/tasks/:taskId/mark-read', requireAuth, (req, res) => { const { taskId } = req.params; const userId = req.session.user.id; checkTaskAccess(userId, taskId, (err, hasAccess) => { if (err || !hasAccess) { return res.status(403).json({ error: 'Нет доступа к задаче' }); } db.run(` INSERT OR IGNORE INTO task_chat_reads (message_id, user_id) SELECT id, ? FROM task_chat_messages WHERE task_id = ? AND user_id != ? `, [userId, taskId, userId], function(err) { if (err) { console.error('❌ Ошибка отметки прочитанных:', err); return res.status(500).json({ error: 'Ошибка отметки' }); } res.json({ success: true, marked_count: this.changes }); }); }); }); // GET /api/chat/files/:fileId/download - Скачать файл из чата router.get('/api/chat/files/:fileId/download', requireAuth, (req, res) => { const { fileId } = req.params; const userId = req.session.user.id; db.get(` SELECT f.*, m.task_id FROM task_chat_files f JOIN task_chat_messages m ON f.message_id = m.id WHERE f.id = ? `, [fileId], (err, file) => { if (err || !file) { return res.status(404).json({ error: 'Файл не найден' }); } checkTaskAccess(userId, file.task_id, (err, hasAccess) => { if (err || !hasAccess) { return res.status(403).json({ error: 'Нет доступа к файлу' }); } if (!fs.existsSync(file.file_path)) { return res.status(404).json({ error: 'Файл не найден на сервере' }); } const encodedFileName = encodeURIComponent(file.original_name); res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`); res.setHeader('Content-Type', file.file_type || 'application/octet-stream'); res.sendFile(file.file_path); }); }); }); // Вспомогательная функция для уведомлений участников function notifyTaskParticipants(taskId, senderId, message) { db.all(` SELECT DISTINCT user_id FROM task_assignments WHERE task_id = ? AND user_id != ? UNION SELECT created_by as user_id FROM tasks WHERE id = ? AND created_by != ? `, [taskId, senderId, taskId, senderId], (err, participants) => { if (err || !participants || participants.length === 0) return; // Пытаемся импортировать функцию уведомлений try { const { sendTaskNotifications } = require('./notifications'); participants.forEach(p => { try { sendTaskNotifications( 'chat_message', taskId, 'Новое сообщение в чате', message.message.substring(0, 100), senderId, '', 'chat', message.user_name, p.user_id ); } catch (notifyErr) { console.error('Ошибка отправки уведомления:', notifyErr); } }); } catch (importErr) { console.log('Модуль уведомлений не загружен'); } }); } // Подключаем роутер app.use(router); console.log('✅ API для чата задач подключено'); };