чат
This commit is contained in:
506
api-chat.js
Normal file
506
api-chat.js
Normal file
@@ -0,0 +1,506 @@
|
||||
// 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 для чата задач подключено');
|
||||
};
|
||||
Reference in New Issue
Block a user