'use strict'; const db = require('../../db/db'); const path = require('path'); const fs = require('fs'); const { emitToUser } = require('../../ws-server'); const { emitToSession, hasAccess } = require('./_shared'); const CHAT_UPLOADS_DIR = path.join(__dirname, '../../../uploads/chat'); if (!fs.existsSync(CHAT_UPLOADS_DIR)) fs.mkdirSync(CHAT_UPLOADS_DIR, { recursive: true }); const ALLOWED_REACTIONS = ['like', 'heart', 'question', 'idea', 'wow']; function sendChat(req, res) { const sessionId = Number(req.params.id); const { message = '', attachment_url, attachment_type } = req.body; const text = message.trim().slice(0, 2000); const safeUrl = attachment_url && typeof attachment_url === 'string' && attachment_url.startsWith('/uploads/') ? attachment_url : null; if (!text && !safeUrl) return res.status(400).json({ error: 'Пустое сообщение' }); const session = db.prepare(`SELECT * FROM classroom_sessions WHERE id=? AND status='active'`).get(sessionId); if (!session) return res.status(404).json({ error: 'Сессия не активна' }); if (!hasAccess(session, req.user.id, req.user.role)) return res.status(403).json({ error: 'Нет доступа' }); const { lastInsertRowid } = db.prepare( 'INSERT INTO classroom_chat (session_id, user_id, message, attachment_url, attachment_type) VALUES (?,?,?,?,?)' ).run(sessionId, req.user.id, text, safeUrl, safeUrl ? (attachment_type || null) : null); const row = db.prepare('SELECT * FROM classroom_chat WHERE id=?').get(lastInsertRowid); emitToSession(sessionId, { type: 'classroom_chat', sessionId, id: row.id, userId: req.user.id, userName: req.user.name, message: text, createdAt: row.created_at, attachmentUrl: row.attachment_url || null, attachmentType: row.attachment_type || null, }); res.json(row); } function getChat(req, res) { const sessionId = Number(req.params.id); const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId); if (!session) return res.status(404).json({ error: 'Не найдено' }); if (!hasAccess(session, req.user.id, req.user.role)) return res.status(403).json({ error: 'Нет доступа' }); const sinceId = Number(req.query.since_id) || 0; const messages = sinceId ? db.prepare(`SELECT c.*, u.name AS user_name FROM classroom_chat c JOIN users u ON u.id = c.user_id WHERE c.session_id=? AND c.id > ? ORDER BY c.id ASC LIMIT 100`).all(sessionId, sinceId) : db.prepare(`SELECT c.*, u.name AS user_name FROM classroom_chat c JOIN users u ON u.id = c.user_id WHERE c.session_id=? ORDER BY c.id DESC LIMIT 200`).all(sessionId).reverse(); if (messages.length > 0) { const ids = messages.map(m => m.id); const reactions = db.prepare( `SELECT chat_id, reaction, COUNT(*) AS cnt, GROUP_CONCAT(user_id) AS uids FROM classroom_chat_reactions WHERE chat_id IN (${ids.map(() => '?').join(',')}) GROUP BY chat_id, reaction` ).all(...ids); const rmap = {}; reactions.forEach(r => { if (!rmap[r.chat_id]) rmap[r.chat_id] = {}; rmap[r.chat_id][r.reaction] = { count: r.cnt, mine: (r.uids || '').split(',').includes(String(req.user.id)) }; }); messages.forEach(m => { m.reactions = rmap[m.id] || {}; }); } res.json({ messages }); } function pinMessage(req, res) { const sessionId = Number(req.params.id); const msgId = Number(req.params.msgId); const session = db.prepare(`SELECT * FROM classroom_sessions WHERE id=? AND status='active'`).get(sessionId); if (!session) return res.status(404).json({ error: 'Сессия не активна' }); if (session.teacher_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Нет доступа' }); const msg = db.prepare('SELECT * FROM classroom_chat WHERE id=? AND session_id=?').get(msgId, sessionId); if (!msg) return res.status(404).json({ error: 'Сообщение не найдено' }); const newPinned = msg.pinned ? 0 : 1; db.prepare('UPDATE classroom_chat SET pinned=? WHERE id=?').run(newPinned, msgId); emitToSession(sessionId, { type: 'classroom_message_pinned', sessionId, msgId, pinned: !!newPinned, message: msg.message }); res.json({ ok: true, pinned: !!newPinned }); } function reactToMessage(req, res) { const chatId = Number(req.params.msgId); const { reaction } = req.body; if (!ALLOWED_REACTIONS.includes(reaction)) return res.status(400).json({ error: 'Неизвестная реакция' }); const msg = db.prepare('SELECT * FROM classroom_chat WHERE id=?').get(chatId); if (!msg) return res.status(404).json({ error: 'Сообщение не найдено' }); const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(msg.session_id); if (!session || !hasAccess(session, req.user.id, req.user.role)) return res.status(403).json({ error: 'Нет доступа' }); const existing = db.prepare('SELECT id FROM classroom_chat_reactions WHERE chat_id=? AND user_id=? AND reaction=?').get(chatId, req.user.id, reaction); let added; if (existing) { db.prepare('DELETE FROM classroom_chat_reactions WHERE id=?').run(existing.id); added = false; } else { db.prepare('INSERT INTO classroom_chat_reactions (chat_id, user_id, reaction) VALUES (?,?,?)').run(chatId, req.user.id, reaction); added = true; } const counts = db.prepare(`SELECT reaction, COUNT(*) AS cnt, GROUP_CONCAT(user_id) AS uids FROM classroom_chat_reactions WHERE chat_id=? GROUP BY reaction`).all(chatId); const reactionsMap = {}; counts.forEach(r => { reactionsMap[r.reaction] = { count: r.cnt, uids: r.uids }; }); emitToSession(msg.session_id, { type: 'classroom_reaction', sessionId: msg.session_id, chatId, reaction, userId: req.user.id, added, reactions: reactionsMap }); res.json({ ok: true, added, reactions: reactionsMap }); } function uploadChatAttachment(req, res) { if (!req.file) return res.status(400).json({ error: 'Файл не получен' }); const url = `/uploads/chat/${req.file.filename}`; const type = req.file.mimetype.startsWith('image/') ? 'image' : 'file'; res.json({ url, type, name: req.file.originalname }); } function exportChat(req, res) { const sessionId = Number(req.params.id); const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId); if (!session) return res.status(404).json({ error: 'Не найдено' }); if (session.teacher_id !== req.user.id && req.user.role !== 'admin') return res.status(403).json({ error: 'Нет доступа' }); const messages = db.prepare(` SELECT c.created_at, u.name AS user_name, c.message, c.attachment_url FROM classroom_chat c JOIN users u ON u.id = c.user_id WHERE c.session_id=? ORDER BY c.id ASC `).all(sessionId); const title = session.title || `Урок #${sessionId}`; const date = session.created_at ? new Date(session.created_at).toLocaleDateString('ru-RU') : ''; let text = `Чат урока: ${title}\nДата: ${date}\n${'─'.repeat(50)}\n\n`; messages.forEach(m => { const ts = m.created_at ? new Date(m.created_at).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' }) : ''; const safeUser = (m.user_name || '').replace(/[\r\n]/g, ' '); const safeMsg = (m.message || '').replace(/[\r\n]/g, ' '); text += `[${ts}] ${safeUser}: ${safeMsg}`; if (m.attachment_url) text += ` [вложение]`; text += '\n'; }); const filename = `chat_${sessionId}_${date.replace(/\./g, '-')}.txt`; res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.send(text); } module.exports = { CHAT_UPLOADS_DIR, ALLOWED_REACTIONS, sendChat, getChat, pinMessage, reactToMessage, uploadChatAttachment, exportChat, };