import { Router } from "express"; import { z } from 'zod'; import { storage } from "../storage"; import { authenticateToken, authenticateTokenOrApiKey, tryBotServiceToken, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { tenantIsolation } from "../middleware/tenant.middleware"; import { validateRequest } from "../middleware/validation.middleware"; import { createTaskMessageSchema } from "@shared/schema"; import { eventBus } from "./shared"; import { sendTaskMessage, SendTaskMessageError } from "../services/task-message.service"; import { isFormAllowedByScopes } from "../utils/api-key"; export function registerChatMessageRoutes(router: Router): void { // Get task messages router.get('/api/tasks/:id/messages', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const taskId = parseInt(req.params.id); if (isNaN(taskId)) { return res.status(400).json({ success: false, error: 'Некорректный ID задачи' }); } const taskRolesForRead = await storage.getTaskRoles(taskId, req.organizationId!); if (taskRolesForRead.length > 0) { const hasAccess = await storage.canUserAccessTask( taskId, req.user!.id, req.organizationId!, req.user!.appRole ); if (!hasAccess) { return res.status(403).json({ success: false, error: 'Нет доступа к этой задаче' }); } } const afterId = req.query.afterId ? parseInt(req.query.afterId as string) : undefined; const messages = await storage.getTaskMessages(taskId, req.organizationId!, afterId); res.json({ success: true, messages }); } catch (error) { console.error('Get task messages error:', error); res.status(500).json({ success: false, error: 'Ошибка при загрузке сообщений' }); } } ); // Create task message (JWT пользователя или API-ключ организации) router.post('/api/tasks/:id/messages', tryBotServiceToken, authenticateTokenOrApiKey, tenantIsolation, validateRequest(createTaskMessageSchema.omit({ taskId: true })), async (req: AuthenticatedRequest, res) => { try { const taskId = parseInt(req.params.id); if (isNaN(taskId)) { return res.status(400).json({ success: false, error: 'Некорректный ID задачи' }); } const { message: msgBody, attachments: msgAttachments } = req.body; if (!msgBody?.trim() && (!msgAttachments || msgAttachments.length === 0)) { return res.status(400).json({ success: false, error: 'Сообщение или вложения обязательны' }); } const task = await storage.getTask(taskId, req.organizationId!); if (!task) { return res.status(404).json({ success: false, error: 'Задача не найдена' }); } { const taskRolesForGuard = await storage.getTaskRoles(taskId, req.organizationId!); if (req.user && taskRolesForGuard.length > 0) { const hasAccess = await storage.canUserAccessTask( taskId, req.user.id, req.organizationId!, req.user.appRole ); if (!hasAccess) { return res.status(403).json({ success: false, error: 'Нет доступа к этой задаче' }); } } } // Проверки для авторизации по API-ключу (JWT-путь не меняется) if (req.apiKey) { if (req.apiKey.scopes.mode === 'read') { return res.status(403).json({ success: false, error: 'API-ключ в режиме read не может отправлять сообщения' }); } if (!isFormAllowedByScopes(req.apiKey.scopes, task.formId)) { return res.status(403).json({ success: false, error: 'Доступ к форме этой задачи запрещён правами API-ключа' }); } } // Автор сообщения: пользователь (JWT); по ключу бота — сам бот; // по ключу без бота — владелец ключа let messageUser = req.user ?? null; let messageBotId: number | null = null; if (!messageUser && req.apiKey) { if (req.apiKey.botId) { messageBotId = req.apiKey.botId; } else { messageUser = await storage.getUser(req.apiKey.createdBy); } } if (!messageUser && !messageBotId) { return res.status(401).json({ success: false, error: 'Не удалось определить автора сообщения' }); } // Основная логика (сообщение + side-эффекты) вынесена в сервис — // переиспользуется также MCP-сервером (инструмент send_task_message). try { const createdMessage = await sendTaskMessage({ task, user: messageUser ?? undefined, botId: messageBotId, organizationId: req.organizationId!, message: req.body.message || '', messageType: req.body.messageType, replyToMessageId: req.body.replyToMessageId, mentionedUserIds: req.body.mentionedUserIds, mentionedBotIds: req.body.mentionedBotIds, attachments: req.body.attachments, bodyAuthorId: req.body.authorId, isBotToken: req.isBotToken === true, }); res.status(201).json({ success: true, message: 'Сообщение добавлено', taskMessage: createdMessage }); } catch (err) { if (err instanceof SendTaskMessageError) { return res.status(err.status).json({ success: false, error: err.message }); } throw err; } } catch (error) { console.error('Create task message error:', error); res.status(500).json({ success: false, error: 'Ошибка при создании сообщения' }); } } ); // Update task message router.put('/api/messages/:id', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const messageId = parseInt(req.params.id); if (isNaN(messageId)) { return res.status(400).json({ success: false, error: 'Некорректный ID сообщения' }); } const { message } = req.body; if (!message || typeof message !== 'string' || message.trim().length === 0) { return res.status(400).json({ success: false, error: 'Текст сообщения обязателен' }); } const existingMessage = await storage.getTaskMessage(messageId, req.organizationId!); if (!existingMessage) { return res.status(404).json({ success: false, error: 'Сообщение не найдено' }); } if (existingMessage.authorId !== req.user!.id && req.user!.appRole !== 'admin') { return res.status(403).json({ success: false, error: 'Нет прав для редактирования этого сообщения' }); } const updatedMessage = await storage.updateTaskMessage( messageId, req.organizationId!, { message: message.trim() } ); res.json({ success: true, message: 'Сообщение обновлено', taskMessage: updatedMessage }); } catch (error) { console.error('Update task message error:', error); res.status(500).json({ success: false, error: 'Ошибка при обновлении сообщения' }); } } ); // Delete task message router.delete('/api/messages/:id', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const messageId = parseInt(req.params.id); if (isNaN(messageId)) { return res.status(400).json({ success: false, error: 'Некорректный ID сообщения' }); } const existingMessage = await storage.getTaskMessage(messageId, req.organizationId!); if (!existingMessage) { return res.status(404).json({ success: false, error: 'Сообщение не найдено' }); } if (existingMessage.authorId !== req.user!.id && req.user!.appRole !== 'admin') { return res.status(403).json({ success: false, error: 'Нет прав для удаления этого сообщения' }); } await storage.deleteTaskMessage(messageId, req.organizationId!); res.json({ success: true, message: 'Сообщение удалено' }); } catch (error) { console.error('Delete task message error:', error); res.status(500).json({ success: false, error: 'Ошибка при удалении сообщения' }); } } ); // Mark message as read router.post('/api/messages/:id/read', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const messageId = parseInt(req.params.id); if (isNaN(messageId)) { return res.status(400).json({ success: false, error: 'Некорректный ID сообщения' }); } const targetMessage = await storage.getTaskMessage(messageId, req.organizationId!); if (!targetMessage) { return res.status(404).json({ success: false, error: 'Сообщение не найдено' }); } const messageRead = await storage.markMessageAsRead( messageId, req.user!.id, req.organizationId! ); if (targetMessage && targetMessage.authorId !== req.user!.id) { eventBus.publishEvent({ type: 'message_read', data: { messageId: messageId, taskId: targetMessage.taskId, readerId: req.user!.id, readerName: `${req.user!.firstName} ${req.user!.middleName || ''} ${req.user!.lastName}`.trim(), authorId: targetMessage.authorId }, organizationId: req.organizationId!, userId: targetMessage.authorId ?? undefined }); } res.json({ success: true, message: 'Сообщение отмечено как прочитанное', messageRead }); } catch (error) { console.error('Mark message as read error:', error); res.status(500).json({ success: false, error: 'Ошибка при отметке сообщения как прочитанного' }); } } ); // Get message read receipts router.get('/api/messages/:id/reads', authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const messageId = parseInt(req.params.id); if (isNaN(messageId)) { return res.status(400).json({ success: false, error: 'Некорректный ID сообщения' }); } const message = await storage.getTaskMessage(messageId, req.organizationId!); if (!message) { return res.status(404).json({ success: false, error: 'Сообщение не найдено' }); } const reads = await storage.getMessageReads(messageId, req.organizationId!); res.json({ success: true, reads }); } catch (error) { console.error('Get message reads error:', error); res.status(500).json({ success: false, error: 'Ошибка при получении статуса прочтения' }); } } ); }