795 lines
36 KiB
JavaScript
795 lines
36 KiB
JavaScript
/**
|
||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||
* All rights reserved.
|
||
*
|
||
* This software is proprietary and confidential.
|
||
* Unauthorized copying, modification, or distribution is prohibited.
|
||
*
|
||
* For licensing inquiries: info@hb3-accelerator.com
|
||
* Website: https://hb3-accelerator.com
|
||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||
*/
|
||
|
||
const express = require('express');
|
||
const router = express.Router();
|
||
const db = require('../db');
|
||
const { broadcastMessagesUpdate } = require('../wsHub');
|
||
const telegramBot = require('../services/telegramBot');
|
||
const emailBot = new (require('../services/emailBot'))();
|
||
const { isUserBlocked } = require('../utils/userUtils');
|
||
|
||
// GET /api/messages?userId=123
|
||
router.get('/', async (req, res) => {
|
||
const userId = req.query.userId;
|
||
const conversationId = req.query.conversationId;
|
||
|
||
// Получаем ключ шифрования
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
let encryptionKey = 'default-key';
|
||
|
||
try {
|
||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||
if (fs.existsSync(keyPath)) {
|
||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||
}
|
||
} catch (keyError) {
|
||
console.error('Error reading encryption key:', keyError);
|
||
}
|
||
|
||
try {
|
||
let result;
|
||
if (conversationId) {
|
||
result = await db.getQuery()(
|
||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||
FROM messages
|
||
WHERE conversation_id = $1 AND message_type = 'user_chat'
|
||
ORDER BY created_at ASC`,
|
||
[conversationId, encryptionKey]
|
||
);
|
||
} else if (userId) {
|
||
result = await db.getQuery()(
|
||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||
FROM messages
|
||
WHERE user_id = $1 AND message_type = 'user_chat'
|
||
ORDER BY created_at ASC`,
|
||
[userId, encryptionKey]
|
||
);
|
||
} else {
|
||
result = await db.getQuery()(
|
||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||
FROM messages
|
||
WHERE message_type = 'user_chat'
|
||
ORDER BY created_at ASC`,
|
||
[encryptionKey]
|
||
);
|
||
}
|
||
res.json(result.rows);
|
||
} catch (e) {
|
||
res.status(500).json({ error: 'DB error', details: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /api/messages
|
||
router.post('/', async (req, res) => {
|
||
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data } = req.body;
|
||
|
||
// Определяем тип сообщения
|
||
const senderId = req.user && req.user.id;
|
||
let messageType = 'user_chat'; // по умолчанию для публичных сообщений
|
||
|
||
if (senderId) {
|
||
// Проверяем, является ли отправитель админом
|
||
const senderCheck = await db.getQuery()(
|
||
'SELECT role FROM users WHERE id = $1',
|
||
[senderId]
|
||
);
|
||
|
||
if (senderCheck.rows.length > 0 && (senderCheck.rows[0].role === 'editor' || senderCheck.rows[0].role === 'readonly')) {
|
||
// Если отправитель админ, проверяем получателя
|
||
const recipientCheck = await db.getQuery()(
|
||
'SELECT role FROM users WHERE id = $1',
|
||
[user_id]
|
||
);
|
||
|
||
// Если получатель тоже админ, то это приватное сообщение
|
||
if (recipientCheck.rows.length > 0 && (recipientCheck.rows[0].role === 'editor' || recipientCheck.rows[0].role === 'readonly')) {
|
||
messageType = 'admin_chat';
|
||
} else {
|
||
// Если получатель обычный пользователь, то это публичное сообщение
|
||
messageType = 'user_chat';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Получаем ключ шифрования
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
let encryptionKey = 'default-key';
|
||
|
||
try {
|
||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||
if (fs.existsSync(keyPath)) {
|
||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||
}
|
||
} catch (keyError) {
|
||
console.error('Error reading encryption key:', keyError);
|
||
}
|
||
|
||
try {
|
||
// Проверка блокировки пользователя
|
||
if (await isUserBlocked(user_id)) {
|
||
return res.status(403).json({ error: 'Пользователь заблокирован. Сообщение не принимается.' });
|
||
}
|
||
// Проверка наличия идентификатора для выбранного канала
|
||
if (channel === 'email') {
|
||
const emailIdentity = await db.getQuery()(
|
||
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
|
||
[user_id, 'email', encryptionKey]
|
||
);
|
||
if (emailIdentity.rows.length === 0) {
|
||
return res.status(400).json({ error: 'У пользователя не указан email. Сообщение не отправлено.' });
|
||
}
|
||
}
|
||
if (channel === 'telegram') {
|
||
const tgIdentity = await db.getQuery()(
|
||
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
|
||
[user_id, 'telegram', encryptionKey]
|
||
);
|
||
if (tgIdentity.rows.length === 0) {
|
||
return res.status(400).json({ error: 'У пользователя не привязан Telegram. Сообщение не отправлено.' });
|
||
}
|
||
}
|
||
if (channel === 'wallet' || channel === 'web3' || channel === 'web') {
|
||
const walletIdentity = await db.getQuery()(
|
||
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
|
||
[user_id, 'wallet', encryptionKey]
|
||
);
|
||
if (walletIdentity.rows.length === 0) {
|
||
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' });
|
||
}
|
||
}
|
||
let conversation;
|
||
|
||
if (messageType === 'admin_chat') {
|
||
// Для админских сообщений ищем приватную беседу через conversation_participants
|
||
let conversationResult = await db.getQuery()(`
|
||
SELECT c.id
|
||
FROM conversations c
|
||
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||
WHERE c.conversation_type = 'admin_chat'
|
||
LIMIT 1
|
||
`, [senderId, user_id]);
|
||
|
||
if (conversationResult.rows.length === 0) {
|
||
// Создаем новую приватную беседу между админами
|
||
const title = `Приватная беседа ${senderId} - ${user_id}`;
|
||
const newConv = await db.getQuery()(
|
||
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING *',
|
||
[user_id, title, encryptionKey, 'admin_chat']
|
||
);
|
||
conversation = newConv.rows[0];
|
||
|
||
// Добавляем участников в беседу
|
||
await db.getQuery()(
|
||
'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2), ($1, $3)',
|
||
[conversation.id, senderId, user_id]
|
||
);
|
||
} else {
|
||
conversation = { id: conversationResult.rows[0].id };
|
||
}
|
||
} else {
|
||
// Для обычных пользовательских сообщений используем старую логику с user_id
|
||
let conversationResult = await db.getQuery()(
|
||
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||
[user_id, encryptionKey]
|
||
);
|
||
|
||
if (conversationResult.rows.length === 0) {
|
||
// Создаем новую беседу
|
||
const title = `Чат с пользователем ${user_id}`;
|
||
const newConv = await db.getQuery()(
|
||
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING *',
|
||
[user_id, title, encryptionKey, 'user_chat']
|
||
);
|
||
conversation = newConv.rows[0];
|
||
} else {
|
||
conversation = conversationResult.rows[0];
|
||
}
|
||
}
|
||
// 3. Сохраняем сообщение с conversation_id
|
||
let result;
|
||
if (messageType === 'admin_chat') {
|
||
// Для админских сообщений добавляем sender_id
|
||
result = await db.getQuery()(
|
||
`INSERT INTO messages (conversation_id, user_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
|
||
VALUES ($1,$2,$3,encrypt_text($4,$13),encrypt_text($5,$13),encrypt_text($6,$13),encrypt_text($7,$13),encrypt_text($8,$13),$9,NOW(),encrypt_text($10,$13),encrypt_text($11,$13),$12,$14) RETURNING *`,
|
||
[conversation.id, user_id, senderId, sender_type, content, channel, role, direction, messageType, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
|
||
);
|
||
} else {
|
||
// Для обычных сообщений без sender_id
|
||
result = await db.getQuery()(
|
||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
|
||
VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),$13,NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`,
|
||
[user_id, conversation.id, sender_type, content, channel, role, direction, messageType, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
|
||
);
|
||
}
|
||
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
|
||
if (channel === 'telegram' && direction === 'out') {
|
||
try {
|
||
// console.log(`[messages.js] Попытка отправки сообщения в Telegram для user_id=${user_id}`);
|
||
// Получаем Telegram ID пользователя
|
||
const tgIdentity = await db.getQuery()(
|
||
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
|
||
[user_id, 'telegram', encryptionKey]
|
||
);
|
||
// console.log(`[messages.js] Результат поиска Telegram ID:`, tgIdentity.rows);
|
||
if (tgIdentity.rows.length > 0) {
|
||
const telegramId = tgIdentity.rows[0].provider_id;
|
||
// console.log(`[messages.js] Отправка сообщения в Telegram ID: ${telegramId}, текст: ${content}`);
|
||
const bot = await telegramBot.getBot();
|
||
try {
|
||
const sendResult = await bot.telegram.sendMessage(telegramId, content);
|
||
// console.log(`[messages.js] Результат отправки в Telegram:`, sendResult);
|
||
} catch (sendErr) {
|
||
// console.error(`[messages.js] Ошибка при отправке в Telegram:`, sendErr);
|
||
}
|
||
} else {
|
||
// console.warn(`[messages.js] Не найден Telegram ID для user_id=${user_id}`);
|
||
}
|
||
} catch (err) {
|
||
// console.error('[messages.js] Ошибка отправки сообщения в Telegram:', err);
|
||
}
|
||
}
|
||
// 5. Если это исходящее сообщение для Email — отправляем email
|
||
if (channel === 'email' && direction === 'out') {
|
||
try {
|
||
// Получаем email пользователя
|
||
const emailIdentity = await db.getQuery()(
|
||
'SELECT decrypt_text(provider_id_encrypted, $3) as provider_id FROM user_identities WHERE user_id = $1 AND provider_encrypted = encrypt_text($2, $3) LIMIT 1',
|
||
[user_id, 'email', encryptionKey]
|
||
);
|
||
if (emailIdentity.rows.length > 0) {
|
||
const email = emailIdentity.rows[0].provider_id;
|
||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||
}
|
||
} catch (err) {
|
||
// console.error('[messages.js] Ошибка отправки email:', err);
|
||
}
|
||
}
|
||
|
||
// Отправляем WebSocket уведомления
|
||
broadcastMessagesUpdate();
|
||
|
||
res.json({ success: true, message: result.rows[0] });
|
||
} catch (e) {
|
||
res.status(500).json({ error: 'DB error', details: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /api/messages/mark-read
|
||
router.post('/mark-read', async (req, res) => {
|
||
try {
|
||
// console.log('[DEBUG] /mark-read req.user:', req.user);
|
||
// console.log('[DEBUG] /mark-read req.body:', req.body);
|
||
const adminId = req.user && req.user.id;
|
||
const { userId, lastReadAt, messageType = 'user_chat' } = req.body;
|
||
|
||
if (!adminId) {
|
||
// console.error('[ERROR] /mark-read: adminId (req.user.id) is missing');
|
||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||
}
|
||
if (!userId || !lastReadAt) {
|
||
// console.error('[ERROR] /mark-read: userId or lastReadAt missing');
|
||
return res.status(400).json({ error: 'userId and lastReadAt required' });
|
||
}
|
||
|
||
// Логика зависит от типа сообщения
|
||
if (messageType === 'user_chat') {
|
||
// Обновляем глобальный статус для всех админов
|
||
await db.query(`
|
||
INSERT INTO global_read_status (user_id, last_read_at, updated_by_admin_id)
|
||
VALUES ($1, $2, $3)
|
||
ON CONFLICT (user_id) DO UPDATE SET
|
||
last_read_at = EXCLUDED.last_read_at,
|
||
updated_by_admin_id = EXCLUDED.updated_by_admin_id,
|
||
updated_at = NOW()
|
||
`, [userId, lastReadAt, adminId]);
|
||
} else if (messageType === 'admin_chat') {
|
||
// Обновляем персональный статус для админских сообщений
|
||
await db.query(`
|
||
INSERT INTO admin_read_messages (admin_id, user_id, last_read_at)
|
||
VALUES ($1, $2, $3)
|
||
ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at
|
||
`, [adminId, userId, lastReadAt]);
|
||
} else {
|
||
return res.status(400).json({ error: 'Invalid messageType. Must be "user_chat" or "admin_chat"' });
|
||
}
|
||
|
||
res.json({ success: true, messageType });
|
||
} catch (e) {
|
||
// console.error('[ERROR] /mark-read:', e);
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// GET /api/messages/read-status
|
||
router.get('/read-status', async (req, res) => {
|
||
try {
|
||
// console.log('[DEBUG] /read-status req.user:', req.user);
|
||
// console.log('[DEBUG] /read-status req.session:', req.session);
|
||
// console.log('[DEBUG] /read-status req.session.userId:', req.session && req.session.userId);
|
||
const adminId = req.user && req.user.id;
|
||
const { messageType = 'user_chat' } = req.query;
|
||
|
||
if (!adminId) {
|
||
// console.error('[ERROR] /read-status: adminId (req.user.id) is missing');
|
||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||
}
|
||
|
||
let result;
|
||
if (messageType === 'user_chat') {
|
||
// Возвращаем глобальный статус для сообщений с пользователями
|
||
result = await db.query('SELECT user_id, last_read_at FROM global_read_status');
|
||
} else if (messageType === 'admin_chat') {
|
||
// Возвращаем персональный статус для админских сообщений
|
||
result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]);
|
||
} else {
|
||
return res.status(400).json({ error: 'Invalid messageType. Must be "user_chat" or "admin_chat"' });
|
||
}
|
||
|
||
// console.log('[DEBUG] /read-status SQL result:', result.rows);
|
||
const map = {};
|
||
for (const row of result.rows) {
|
||
map[row.user_id] = row.last_read_at;
|
||
}
|
||
res.json(map);
|
||
} catch (e) {
|
||
// console.error('[ERROR] /read-status:', e);
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// GET /api/conversations?userId=123
|
||
router.get('/conversations', async (req, res) => {
|
||
const userId = req.query.userId;
|
||
if (!userId) return res.status(400).json({ error: 'userId required' });
|
||
try {
|
||
const result = await db.getQuery()(
|
||
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||
[userId]
|
||
);
|
||
if (result.rows.length === 0) {
|
||
return res.status(404).json({ error: 'Conversation not found' });
|
||
}
|
||
res.json(result.rows[0]);
|
||
} catch (e) {
|
||
res.status(500).json({ error: 'DB error', details: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /api/conversations - создать беседу для пользователя
|
||
router.post('/conversations', async (req, res) => {
|
||
const { userId, title } = req.body;
|
||
if (!userId) return res.status(400).json({ error: 'userId required' });
|
||
|
||
// Получаем ключ шифрования
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
let encryptionKey = 'default-key';
|
||
|
||
try {
|
||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||
if (fs.existsSync(keyPath)) {
|
||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||
}
|
||
} catch (keyError) {
|
||
console.error('Error reading encryption key:', keyError);
|
||
}
|
||
|
||
try {
|
||
const conversationTitle = title || `Чат с пользователем ${userId}`;
|
||
const result = await db.getQuery()(
|
||
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
|
||
[userId, conversationTitle, encryptionKey]
|
||
);
|
||
res.json(result.rows[0]);
|
||
} catch (e) {
|
||
res.status(500).json({ error: 'DB error', details: e.message });
|
||
}
|
||
});
|
||
|
||
// Массовая рассылка сообщения во все каналы пользователя
|
||
router.post('/broadcast', async (req, res) => {
|
||
const { user_id, content } = req.body;
|
||
if (!user_id || !content) {
|
||
return res.status(400).json({ error: 'user_id и content обязательны' });
|
||
}
|
||
|
||
// Получаем ключ шифрования
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
let encryptionKey = 'default-key';
|
||
|
||
try {
|
||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||
if (fs.existsSync(keyPath)) {
|
||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||
}
|
||
} catch (keyError) {
|
||
console.error('Error reading encryption key:', keyError);
|
||
}
|
||
|
||
try {
|
||
// Получаем все идентификаторы пользователя
|
||
const identitiesRes = await db.getQuery()(
|
||
'SELECT decrypt_text(provider_encrypted, $2) as provider, decrypt_text(provider_id_encrypted, $2) as provider_id FROM user_identities WHERE user_id = $1',
|
||
[user_id, encryptionKey]
|
||
);
|
||
const identities = identitiesRes.rows;
|
||
// --- Найти или создать беседу (conversation) ---
|
||
let conversationResult = await db.getQuery()(
|
||
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||
[user_id, encryptionKey]
|
||
);
|
||
let conversation;
|
||
if (conversationResult.rows.length === 0) {
|
||
const title = `Чат с пользователем ${user_id}`;
|
||
const newConv = await db.getQuery()(
|
||
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
|
||
[user_id, title, encryptionKey]
|
||
);
|
||
conversation = newConv.rows[0];
|
||
} else {
|
||
conversation = conversationResult.rows[0];
|
||
}
|
||
const results = [];
|
||
let sent = false;
|
||
// Email
|
||
const email = identities.find(i => i.provider === 'email')?.provider_id;
|
||
if (email) {
|
||
try {
|
||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||
// Сохраняем в messages с conversation_id
|
||
await db.getQuery()(
|
||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', 'user_chat', encryptionKey]
|
||
);
|
||
results.push({ channel: 'email', status: 'sent' });
|
||
sent = true;
|
||
} catch (err) {
|
||
results.push({ channel: 'email', status: 'error', error: err.message });
|
||
}
|
||
}
|
||
// Telegram
|
||
const telegram = identities.find(i => i.provider === 'telegram')?.provider_id;
|
||
if (telegram) {
|
||
try {
|
||
const bot = await telegramBot.getBot();
|
||
await bot.telegram.sendMessage(telegram, content);
|
||
await db.getQuery()(
|
||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', 'user_chat', encryptionKey]
|
||
);
|
||
results.push({ channel: 'telegram', status: 'sent' });
|
||
sent = true;
|
||
} catch (err) {
|
||
results.push({ channel: 'telegram', status: 'error', error: err.message });
|
||
}
|
||
}
|
||
// Wallet/web3
|
||
const wallet = identities.find(i => i.provider === 'wallet')?.provider_id;
|
||
if (wallet) {
|
||
// Здесь можно реализовать отправку через web3, если нужно
|
||
await db.getQuery()(
|
||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
|
||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
|
||
[user_id, conversation.id, 'admin', content, 'wallet', 'user', 'out', encryptionKey]
|
||
);
|
||
results.push({ channel: 'wallet', status: 'saved' });
|
||
sent = true;
|
||
}
|
||
if (!sent) {
|
||
return res.status(400).json({ error: 'У пользователя нет ни одного канала для рассылки.' });
|
||
}
|
||
res.json({ success: true, results });
|
||
} catch (e) {
|
||
res.status(500).json({ error: 'Broadcast error', details: e.message });
|
||
}
|
||
});
|
||
|
||
// DELETE /api/messages/history/:userId - удалить историю сообщений пользователя
|
||
router.delete('/history/:userId', async (req, res) => {
|
||
const userId = req.params.userId;
|
||
if (!userId) {
|
||
return res.status(400).json({ error: 'userId required' });
|
||
}
|
||
|
||
try {
|
||
// Проверяем права администратора
|
||
if (!req.user || !req.user.isAdmin) {
|
||
return res.status(403).json({ error: 'Only administrators can delete message history' });
|
||
}
|
||
|
||
// Удаляем все сообщения пользователя
|
||
const result = await db.getQuery()(
|
||
'DELETE FROM messages WHERE user_id = $1 RETURNING id',
|
||
[userId]
|
||
);
|
||
|
||
// Удаляем беседы пользователя (если есть)
|
||
const conversationResult = await db.getQuery()(
|
||
'DELETE FROM conversations WHERE user_id = $1 RETURNING id',
|
||
[userId]
|
||
);
|
||
|
||
console.log(`[messages.js] Deleted ${result.rowCount} messages and ${conversationResult.rowCount} conversations for user ${userId}`);
|
||
|
||
// Отправляем обновление через WebSocket
|
||
broadcastMessagesUpdate();
|
||
|
||
res.json({
|
||
success: true,
|
||
deletedMessages: result.rowCount,
|
||
deletedConversations: conversationResult.rowCount
|
||
});
|
||
} catch (e) {
|
||
console.error('[ERROR] /history/:userId:', e);
|
||
res.status(500).json({ error: 'DB error', details: e.message });
|
||
}
|
||
});
|
||
|
||
// POST /api/messages/admin/send - отправка сообщения админу
|
||
router.post('/admin/send', async (req, res) => {
|
||
try {
|
||
const adminId = req.user && req.user.id;
|
||
const { recipientAdminId, content } = req.body;
|
||
|
||
if (!adminId) {
|
||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||
}
|
||
if (!recipientAdminId || !content) {
|
||
return res.status(400).json({ error: 'recipientAdminId and content required' });
|
||
}
|
||
|
||
// Получаем ключ шифрования
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
let encryptionKey = 'default-key';
|
||
|
||
try {
|
||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||
if (fs.existsSync(keyPath)) {
|
||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||
}
|
||
} catch (keyError) {
|
||
console.error('Error reading encryption key:', keyError);
|
||
}
|
||
|
||
// Ищем существующую приватную беседу между двумя админами через conversation_participants
|
||
let conversationResult = await db.getQuery()(`
|
||
SELECT c.id
|
||
FROM conversations c
|
||
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||
WHERE c.conversation_type = 'admin_chat'
|
||
LIMIT 1
|
||
`, [adminId, recipientAdminId]);
|
||
|
||
let conversationId;
|
||
if (conversationResult.rows.length === 0) {
|
||
// Создаем новую приватную беседу между админами
|
||
const title = `Приватная беседа ${adminId} - ${recipientAdminId}`;
|
||
const newConv = await db.getQuery()(
|
||
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING id',
|
||
[recipientAdminId, title, encryptionKey, 'admin_chat']
|
||
);
|
||
conversationId = newConv.rows[0].id;
|
||
|
||
// Добавляем участников в беседу
|
||
await db.getQuery()(
|
||
'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2), ($1, $3)',
|
||
[conversationId, adminId, recipientAdminId]
|
||
);
|
||
|
||
console.log(`[admin/send] Создана новая беседа ${conversationId} между ${adminId} и ${recipientAdminId}`);
|
||
} else {
|
||
conversationId = conversationResult.rows[0].id;
|
||
console.log(`[admin/send] Найдена существующая беседа ${conversationId} между ${adminId} и ${recipientAdminId}`);
|
||
}
|
||
|
||
// Сохраняем сообщение с типом 'admin_chat'
|
||
const result = await db.getQuery()(
|
||
`INSERT INTO messages (conversation_id, user_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||
VALUES ($1, $2, $3, encrypt_text($4, $9), encrypt_text($5, $9), encrypt_text($6, $9), encrypt_text($7, $9), encrypt_text($8, $9), $10, NOW()) RETURNING id`,
|
||
[conversationId, recipientAdminId, adminId, 'admin', content, 'web', 'admin', 'out', encryptionKey, 'admin_chat']
|
||
);
|
||
|
||
// Отправляем WebSocket уведомления
|
||
broadcastMessagesUpdate();
|
||
|
||
res.json({
|
||
success: true,
|
||
messageId: result.rows[0].id,
|
||
conversationId,
|
||
messageType: 'admin_chat'
|
||
});
|
||
} catch (e) {
|
||
console.error('[ERROR] /admin/send:', e);
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// GET /api/messages/admin/conversations - получить личные чаты админа
|
||
router.get('/admin/conversations', async (req, res) => {
|
||
try {
|
||
const adminId = req.user && req.user.id;
|
||
|
||
if (!adminId) {
|
||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||
}
|
||
|
||
// Получаем список админов, с которыми есть переписка
|
||
const conversations = await db.query(`
|
||
SELECT DISTINCT
|
||
CASE
|
||
WHEN sender_type = 'admin' AND user_id != $1 THEN user_id
|
||
ELSE sender_id
|
||
END as admin_id,
|
||
MAX(created_at) as last_message_at
|
||
FROM messages
|
||
WHERE message_type = 'admin_chat'
|
||
AND (user_id = $1 OR sender_id = $1)
|
||
GROUP BY admin_id
|
||
ORDER BY last_message_at DESC
|
||
`, [adminId]);
|
||
|
||
res.json({
|
||
success: true,
|
||
conversations: conversations.rows
|
||
});
|
||
} catch (e) {
|
||
console.error('[ERROR] /admin/conversations:', e);
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// GET /api/messages/admin/contacts - получить админов для приватного чата
|
||
router.get('/admin/contacts', async (req, res) => {
|
||
try {
|
||
const adminId = req.user && req.user.id;
|
||
|
||
if (!adminId) {
|
||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||
}
|
||
|
||
// Получаем ключ шифрования
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
let encryptionKey = 'default-key';
|
||
|
||
try {
|
||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||
if (fs.existsSync(keyPath)) {
|
||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||
}
|
||
} catch (keyError) {
|
||
console.error('Error reading encryption key:', keyError);
|
||
}
|
||
|
||
// Получаем всех пользователей, с которыми есть приватные беседы через conversation_participants
|
||
const adminContacts = await db.getQuery()(`
|
||
SELECT DISTINCT
|
||
other_user.id,
|
||
COALESCE(
|
||
decrypt_text(other_user.first_name_encrypted, $2),
|
||
decrypt_text(other_user.username_encrypted, $2),
|
||
'Пользователь ' || other_user.id
|
||
) as name,
|
||
'admin@system' as email,
|
||
CASE
|
||
WHEN other_user.role = 'editor' THEN 'admin'
|
||
WHEN other_user.role = 'readonly' THEN 'admin'
|
||
ELSE 'user'
|
||
END as contact_type,
|
||
MAX(m.created_at) as last_message_at,
|
||
COUNT(m.id) as message_count
|
||
FROM conversations c
|
||
INNER JOIN conversation_participants cp_current ON cp_current.conversation_id = c.id AND cp_current.user_id = $1
|
||
INNER JOIN conversation_participants cp_other ON cp_other.conversation_id = c.id AND cp_other.user_id != $1
|
||
INNER JOIN users other_user ON other_user.id = cp_other.user_id
|
||
LEFT JOIN messages m ON m.conversation_id = c.id AND m.message_type = 'admin_chat'
|
||
WHERE c.conversation_type = 'admin_chat'
|
||
GROUP BY
|
||
other_user.id,
|
||
other_user.first_name_encrypted,
|
||
other_user.username_encrypted,
|
||
other_user.role
|
||
ORDER BY MAX(m.created_at) DESC
|
||
`, [adminId, encryptionKey]);
|
||
|
||
res.json({
|
||
success: true,
|
||
contacts: adminContacts.rows.map(contact => ({
|
||
...contact,
|
||
created_at: contact.last_message_at, // Используем время последнего сообщения как время создания для сортировки
|
||
telegram: null,
|
||
wallet: null
|
||
}))
|
||
});
|
||
} catch (e) {
|
||
console.error('[ERROR] /admin/contacts:', e);
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
// GET /api/messages/admin/:adminId - получить сообщения с конкретным админом
|
||
router.get('/admin/:adminId', async (req, res) => {
|
||
try {
|
||
const currentAdminId = req.user && req.user.id;
|
||
const { adminId } = req.params;
|
||
|
||
if (!currentAdminId) {
|
||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||
}
|
||
|
||
// Получаем ключ шифрования
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
let encryptionKey = 'default-key';
|
||
|
||
try {
|
||
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||
if (fs.existsSync(keyPath)) {
|
||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||
}
|
||
} catch (keyError) {
|
||
console.error('Error reading encryption key:', keyError);
|
||
}
|
||
|
||
// Получаем сообщения из приватной беседы между админами через conversation_participants
|
||
const result = await db.getQuery()(
|
||
`SELECT m.id, m.user_id, m.sender_id,
|
||
decrypt_text(m.sender_type_encrypted, $3) as sender_type,
|
||
decrypt_text(m.content_encrypted, $3) as content,
|
||
decrypt_text(m.channel_encrypted, $3) as channel,
|
||
decrypt_text(m.role_encrypted, $3) as role,
|
||
decrypt_text(m.direction_encrypted, $3) as direction,
|
||
m.created_at, m.message_type,
|
||
-- Получаем wallet адреса отправителей (расшифровываем provider_id_encrypted)
|
||
CASE
|
||
WHEN sender_ui.provider_encrypted = encrypt_text('wallet', $3)
|
||
THEN decrypt_text(sender_ui.provider_id_encrypted, $3)
|
||
ELSE 'Админ'
|
||
END as sender_wallet,
|
||
CASE
|
||
WHEN recipient_ui.provider_encrypted = encrypt_text('wallet', $3)
|
||
THEN decrypt_text(recipient_ui.provider_id_encrypted, $3)
|
||
ELSE 'Админ'
|
||
END as recipient_wallet
|
||
FROM messages m
|
||
INNER JOIN conversations c ON c.id = m.conversation_id
|
||
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||
LEFT JOIN user_identities sender_ui ON sender_ui.user_id = m.sender_id
|
||
LEFT JOIN user_identities recipient_ui ON recipient_ui.user_id = m.user_id
|
||
WHERE m.message_type = 'admin_chat' AND c.conversation_type = 'admin_chat'
|
||
ORDER BY m.created_at ASC`,
|
||
[currentAdminId, adminId, encryptionKey]
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
messages: result.rows,
|
||
messageType: 'admin_chat'
|
||
});
|
||
} catch (e) {
|
||
console.error('[ERROR] /admin/:adminId:', e);
|
||
res.status(500).json({ error: e.message });
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|