ваше сообщение коммита
This commit is contained in:
@@ -13,8 +13,10 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const { broadcastMessagesUpdate } = require('../wsHub');
|
||||
const botManager = require('../services/botManager');
|
||||
const universalGuestService = require('../services/UniversalGuestService');
|
||||
const { isUserBlocked } = require('../utils/userUtils');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { requirePermission } = require('../middleware/permissions');
|
||||
@@ -35,11 +37,166 @@ router.get('/public', requireAuth, async (req, res) => {
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const parseMetadata = (rawMetadata) => {
|
||||
if (!rawMetadata) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof rawMetadata === 'object') {
|
||||
return rawMetadata;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(rawMetadata);
|
||||
} catch (error) {
|
||||
logger.warn('[messages/public] Не удалось распарсить metadata гостевого сообщения:', error?.message);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// Публичные сообщения видны на главной странице пользователя
|
||||
const targetUserId = userId || currentUserId;
|
||||
|
||||
|
||||
const isGuestContact = typeof targetUserId === 'string' && targetUserId.startsWith('guest_');
|
||||
|
||||
if (isGuestContact) {
|
||||
const guestId = parseInt(targetUserId.replace('guest_', ''), 10);
|
||||
|
||||
if (Number.isNaN(guestId)) {
|
||||
return res.status(400).json({ error: 'Invalid guest ID format' });
|
||||
}
|
||||
|
||||
const guestIdentifierResult = await db.getQuery()(
|
||||
`WITH decrypted_guest AS (
|
||||
SELECT
|
||||
id,
|
||||
decrypt_text(identifier_encrypted, $2) AS guest_identifier,
|
||||
channel
|
||||
FROM unified_guest_messages
|
||||
WHERE user_id IS NULL
|
||||
)
|
||||
SELECT guest_identifier, channel
|
||||
FROM decrypted_guest
|
||||
GROUP BY guest_identifier, channel
|
||||
HAVING MIN(id) = $1
|
||||
LIMIT 1`,
|
||||
[guestId, encryptionKey]
|
||||
);
|
||||
|
||||
if (guestIdentifierResult.rows.length === 0) {
|
||||
return res.json({
|
||||
success: true,
|
||||
messages: [],
|
||||
total: 0,
|
||||
limit,
|
||||
offset,
|
||||
hasMore: false
|
||||
});
|
||||
}
|
||||
|
||||
const guestIdentifier = guestIdentifierResult.rows[0].guest_identifier;
|
||||
const guestChannel = guestIdentifierResult.rows[0].channel;
|
||||
|
||||
if (countOnly) {
|
||||
const countResult = await db.getQuery()(
|
||||
`SELECT COUNT(*)
|
||||
FROM unified_guest_messages
|
||||
WHERE decrypt_text(identifier_encrypted, $2) = $1`,
|
||||
[guestIdentifier, encryptionKey]
|
||||
);
|
||||
const totalCount = parseInt(countResult.rows[0].count, 10);
|
||||
return res.json({ success: true, count: totalCount, total: totalCount });
|
||||
}
|
||||
|
||||
const messagesResult = await db.getQuery()(
|
||||
`SELECT
|
||||
id,
|
||||
decrypt_text(content_encrypted, $3) AS content,
|
||||
is_ai,
|
||||
metadata,
|
||||
channel,
|
||||
created_at,
|
||||
decrypt_text(attachment_filename_encrypted, $3) AS attachment_filename,
|
||||
decrypt_text(attachment_mimetype_encrypted, $3) AS attachment_mimetype,
|
||||
attachment_size
|
||||
FROM unified_guest_messages
|
||||
WHERE decrypt_text(identifier_encrypted, $3) = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $4`,
|
||||
[guestIdentifier, limit, encryptionKey, offset]
|
||||
);
|
||||
|
||||
const countResult = await db.getQuery()(
|
||||
`SELECT COUNT(*)
|
||||
FROM unified_guest_messages
|
||||
WHERE decrypt_text(identifier_encrypted, $2) = $1`,
|
||||
[guestIdentifier, encryptionKey]
|
||||
);
|
||||
const totalCount = parseInt(countResult.rows[0].count, 10);
|
||||
|
||||
const mappedMessages = messagesResult.rows.map((row) => {
|
||||
const metadata = parseMetadata(row.metadata);
|
||||
const baseMessage = {
|
||||
id: row.id,
|
||||
user_id: targetUserId,
|
||||
sender_id: row.is_ai ? null : targetUserId,
|
||||
sender_type: row.is_ai ? 'assistant' : 'user',
|
||||
content: row.content,
|
||||
channel: row.channel || guestChannel,
|
||||
role: row.is_ai ? 'assistant' : 'user',
|
||||
direction: row.is_ai ? 'out' : 'in',
|
||||
created_at: row.created_at,
|
||||
message_type: 'public',
|
||||
is_ai: row.is_ai,
|
||||
metadata,
|
||||
last_read_at: null
|
||||
};
|
||||
|
||||
if (row.attachment_filename || row.attachment_mimetype || row.attachment_size) {
|
||||
baseMessage.attachments = [
|
||||
{
|
||||
filename: row.attachment_filename,
|
||||
mimetype: row.attachment_mimetype,
|
||||
size: row.attachment_size
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (metadata.consentRequired !== undefined) {
|
||||
baseMessage.consentRequired = metadata.consentRequired;
|
||||
}
|
||||
if (metadata.consentDocuments) {
|
||||
baseMessage.consentDocuments = metadata.consentDocuments;
|
||||
}
|
||||
if (metadata.autoConsentOnReply !== undefined) {
|
||||
baseMessage.autoConsentOnReply = metadata.autoConsentOnReply;
|
||||
}
|
||||
if (metadata.telegramBotUrl) {
|
||||
baseMessage.telegramBotUrl = metadata.telegramBotUrl;
|
||||
}
|
||||
if (metadata.supportEmail) {
|
||||
baseMessage.supportEmail = metadata.supportEmail;
|
||||
}
|
||||
|
||||
return baseMessage;
|
||||
});
|
||||
|
||||
const orderedMessages = mappedMessages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
messages: orderedMessages,
|
||||
total: totalCount,
|
||||
limit,
|
||||
offset,
|
||||
hasMore: offset + limit < totalCount,
|
||||
guest: {
|
||||
identifier: guestIdentifier,
|
||||
channel: guestChannel
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Если нужен только подсчет
|
||||
if (countOnly) {
|
||||
const countResult = await db.getQuery()(
|
||||
@@ -348,81 +505,177 @@ router.post('/send', requireAuth, async (req, res) => {
|
||||
if (!['public', 'private'].includes(messageType)) {
|
||||
return res.status(400).json({ error: 'messageType должен быть "public" или "private"' });
|
||||
}
|
||||
|
||||
// Определяем recipientId в зависимости от типа сообщения
|
||||
let recipientIdNum;
|
||||
if (messageType === 'private') {
|
||||
// Приватные сообщения всегда идут к редактору (ID = 1)
|
||||
recipientIdNum = 1;
|
||||
} else {
|
||||
// Конвертируем recipientId в число для публичных сообщений
|
||||
recipientIdNum = parseInt(recipientId);
|
||||
if (isNaN(recipientIdNum)) {
|
||||
return res.status(400).json({ error: 'recipientId должен быть числом' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const senderId = req.user.id;
|
||||
const senderRole = req.user.role || req.user.userAccessLevel?.level || 'user';
|
||||
const isGuestRecipient = typeof recipientId === 'string' && recipientId.startsWith('guest_');
|
||||
|
||||
try {
|
||||
const senderId = req.user.id;
|
||||
const senderRole = req.user.role || req.user.userAccessLevel?.level || 'user';
|
||||
|
||||
if (isGuestRecipient) {
|
||||
if (!hasPermission(senderRole, PERMISSIONS.SEND_TO_USERS)) {
|
||||
return res.status(403).json({ error: 'Недостаточно прав для отправки сообщений гостям' });
|
||||
}
|
||||
|
||||
if (messageType !== 'public') {
|
||||
return res.status(400).json({ error: 'Гостям можно отправлять только публичные сообщения' });
|
||||
}
|
||||
|
||||
const guestInternalId = parseInt(recipientId.replace('guest_', ''), 10);
|
||||
if (Number.isNaN(guestInternalId)) {
|
||||
return res.status(400).json({ error: 'Некорректный формат гостевого идентификатора' });
|
||||
}
|
||||
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const guestIdentifierResult = await db.getQuery()(
|
||||
`WITH decrypted_guest AS (
|
||||
SELECT
|
||||
id,
|
||||
decrypt_text(identifier_encrypted, $2) AS guest_identifier,
|
||||
channel
|
||||
FROM unified_guest_messages
|
||||
WHERE user_id IS NULL
|
||||
)
|
||||
SELECT guest_identifier, channel
|
||||
FROM decrypted_guest
|
||||
GROUP BY guest_identifier, channel
|
||||
HAVING MIN(id) = $1
|
||||
LIMIT 1`,
|
||||
[guestInternalId, encryptionKey]
|
||||
);
|
||||
|
||||
if (guestIdentifierResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Гостевой контакт не найден' });
|
||||
}
|
||||
|
||||
const guestIdentifier = guestIdentifierResult.rows[0].guest_identifier;
|
||||
const guestChannel = guestIdentifierResult.rows[0].channel;
|
||||
const deliveryMeta = {
|
||||
sentBy: 'admin_panel',
|
||||
senderId,
|
||||
senderRole,
|
||||
originalRecipientId: recipientId,
|
||||
messageType
|
||||
};
|
||||
|
||||
let deliveryStatus = { success: true };
|
||||
|
||||
try {
|
||||
if (guestChannel === 'telegram') {
|
||||
const telegramBot = botManager.getBot('telegram');
|
||||
if (telegramBot && telegramBot.isInitialized) {
|
||||
await telegramBot.getBot().telegram.sendMessage(guestIdentifier, content);
|
||||
} else {
|
||||
logger.warn('[messages/send] Telegram Bot не инициализирован, сообщение сохранено только в истории');
|
||||
deliveryStatus = { success: false, error: 'Telegram bot inactive' };
|
||||
}
|
||||
} else if (guestChannel === 'email') {
|
||||
const emailBot = botManager.getBot('email');
|
||||
if (emailBot && emailBot.isInitialized) {
|
||||
await emailBot.sendEmail(guestIdentifier, 'Ответ от администратора', content);
|
||||
} else {
|
||||
logger.warn('[messages/send] Email Bot не инициализирован, сообщение сохранено только в истории');
|
||||
deliveryStatus = { success: false, error: 'Email bot inactive' };
|
||||
}
|
||||
} else {
|
||||
logger.info(`[messages/send] Гость ${guestIdentifier} имеет канал ${guestChannel}, внешняя доставка не требуется`);
|
||||
}
|
||||
} catch (deliveryError) {
|
||||
logger.error('[messages/send] Ошибка отправки гостю через внешний канал:', deliveryError);
|
||||
deliveryStatus = { success: false, error: deliveryError.message };
|
||||
}
|
||||
|
||||
const saveResult = await universalGuestService.saveAiResponse({
|
||||
identifier: guestIdentifier,
|
||||
channel: guestChannel,
|
||||
content,
|
||||
metadata: deliveryMeta
|
||||
});
|
||||
|
||||
broadcastMessagesUpdate();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: {
|
||||
id: saveResult.messageId,
|
||||
user_id: recipientId,
|
||||
sender_id: null,
|
||||
sender_type: 'assistant',
|
||||
content,
|
||||
channel: guestChannel,
|
||||
role: 'assistant',
|
||||
direction: 'out',
|
||||
created_at: saveResult.created_at,
|
||||
message_type: 'public',
|
||||
metadata: deliveryMeta
|
||||
},
|
||||
delivery: deliveryStatus
|
||||
});
|
||||
}
|
||||
|
||||
// Работа с зарегистрированными пользователями
|
||||
let recipientIdNum;
|
||||
if (messageType === 'private') {
|
||||
recipientIdNum = 1;
|
||||
} else {
|
||||
recipientIdNum = parseInt(recipientId, 10);
|
||||
if (Number.isNaN(recipientIdNum)) {
|
||||
return res.status(400).json({ error: 'recipientId должен быть числом' });
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[DEBUG] /messages/send: senderId:', senderId, 'senderRole:', senderRole);
|
||||
|
||||
// Получаем информацию о получателе
|
||||
|
||||
const recipientResult = await db.getQuery()(
|
||||
'SELECT id, role FROM users WHERE id = $1',
|
||||
[recipientIdNum]
|
||||
);
|
||||
|
||||
|
||||
if (recipientResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Получатель не найден' });
|
||||
}
|
||||
|
||||
|
||||
const recipientRole = recipientResult.rows[0].role;
|
||||
console.log('[DEBUG] /messages/send: recipientId:', recipientIdNum, 'recipientRole:', recipientRole);
|
||||
|
||||
// Используем централизованную проверку прав
|
||||
|
||||
const { canSendMessage } = require('/app/shared/permissions');
|
||||
const permissionCheck = canSendMessage(senderRole, recipientRole, senderId, recipientIdNum);
|
||||
|
||||
|
||||
console.log('[DEBUG] /messages/send: canSend:', permissionCheck.canSend, 'senderRole:', senderRole, 'recipientRole:', recipientRole, 'error:', permissionCheck.errorMessage);
|
||||
|
||||
|
||||
if (!permissionCheck.canSend) {
|
||||
return res.status(403).json({
|
||||
error: permissionCheck.errorMessage || 'Недостаточно прав для отправки сообщения этому получателю'
|
||||
return res.status(403).json({
|
||||
error: permissionCheck.errorMessage || 'Недостаточно прав для отправки сообщения этому получателю'
|
||||
});
|
||||
}
|
||||
|
||||
// ✨ Используем unifiedMessageProcessor для унификации
|
||||
|
||||
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
|
||||
const identityService = require('../services/identity-service');
|
||||
|
||||
// Получаем wallet идентификатор отправителя
|
||||
|
||||
const walletIdentity = await identityService.findIdentity(senderId, 'wallet');
|
||||
if (!walletIdentity) {
|
||||
return res.status(403).json({
|
||||
error: 'Требуется подключение кошелька'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const identifier = `wallet:${walletIdentity.provider_id}`;
|
||||
|
||||
// Обрабатываем через unifiedMessageProcessor
|
||||
|
||||
const result = await unifiedMessageProcessor.processMessage({
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
identifier,
|
||||
content,
|
||||
channel: 'web',
|
||||
attachments: [],
|
||||
conversationId: null, // unifiedMessageProcessor сам найдет/создаст беседу
|
||||
conversationId: null,
|
||||
recipientId: recipientIdNum,
|
||||
userId: senderId,
|
||||
metadata: {
|
||||
messageType: messageType,
|
||||
markAsRead: markAsRead
|
||||
messageType,
|
||||
markAsRead
|
||||
}
|
||||
});
|
||||
|
||||
// Если нужно отметить как прочитанное
|
||||
|
||||
if (markAsRead) {
|
||||
try {
|
||||
const lastReadAt = new Date().toISOString();
|
||||
@@ -434,10 +687,9 @@ router.post('/send', requireAuth, async (req, res) => {
|
||||
);
|
||||
} catch (markError) {
|
||||
console.warn('[WARNING] /send mark-read error:', markError);
|
||||
// Не прерываем выполнение, если mark-read не удался
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
res.json({ success: true, message: result });
|
||||
} catch (e) {
|
||||
console.error('[ERROR] /send:', e);
|
||||
|
||||
@@ -622,24 +622,37 @@ router.put('/email-settings', requireAdmin, async (req, res, next) => {
|
||||
is_active
|
||||
} = req.body;
|
||||
|
||||
// Валидация обязательных полей
|
||||
if (!imap_host || !imap_port || !imap_user || !imap_password ||
|
||||
!smtp_host || !smtp_port || !smtp_user || !smtp_password || !from_email) {
|
||||
// Загрузка текущих настроек (нужна для сохранения старых паролей)
|
||||
const currentSettings = await botsSettings.getBotSettings('email');
|
||||
|
||||
// Валидация обязательных полей (пароли могут быть опущены, если уже сохранены)
|
||||
if (!imap_host || !imap_port || !imap_user ||
|
||||
!smtp_host || !smtp_port || !smtp_user || !from_email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Все поля обязательны для заполнения'
|
||||
});
|
||||
}
|
||||
|
||||
const finalImapPassword = imap_password || currentSettings?.imap_password;
|
||||
const finalSmtpPassword = smtp_password || currentSettings?.smtp_password;
|
||||
|
||||
if (!finalImapPassword || !finalSmtpPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Необходимо указать IMAP и SMTP пароли'
|
||||
});
|
||||
}
|
||||
|
||||
const settings = {
|
||||
imap_host,
|
||||
imap_port: parseInt(imap_port),
|
||||
imap_user,
|
||||
imap_password,
|
||||
imap_password: finalImapPassword,
|
||||
smtp_host,
|
||||
smtp_port: parseInt(smtp_port),
|
||||
smtp_user,
|
||||
smtp_password,
|
||||
smtp_password: finalSmtpPassword,
|
||||
from_email,
|
||||
is_active: is_active !== undefined ? is_active : true,
|
||||
updated_at: new Date()
|
||||
|
||||
@@ -239,20 +239,33 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
decrypt_text(identifier_encrypted, $1) as guest_identifier,
|
||||
channel,
|
||||
created_at,
|
||||
user_id
|
||||
user_id,
|
||||
metadata
|
||||
FROM unified_guest_messages
|
||||
WHERE user_id IS NULL
|
||||
),
|
||||
guest_groups AS (
|
||||
SELECT
|
||||
MIN(id) as guest_id,
|
||||
first_messages AS (
|
||||
SELECT DISTINCT ON (guest_identifier, channel)
|
||||
id as guest_id,
|
||||
guest_identifier,
|
||||
channel,
|
||||
MIN(created_at) as created_at,
|
||||
MAX(created_at) as last_message_at,
|
||||
COUNT(*) as message_count
|
||||
metadata,
|
||||
created_at
|
||||
FROM decrypted_guests
|
||||
GROUP BY guest_identifier, channel
|
||||
ORDER BY guest_identifier, channel, id ASC
|
||||
),
|
||||
guest_groups AS (
|
||||
SELECT
|
||||
fm.guest_id,
|
||||
fm.guest_identifier,
|
||||
fm.channel,
|
||||
fm.metadata,
|
||||
fm.created_at,
|
||||
MAX(dg.created_at) as last_message_at,
|
||||
COUNT(*) as message_count
|
||||
FROM first_messages fm
|
||||
JOIN decrypted_guests dg ON dg.guest_identifier = fm.guest_identifier AND dg.channel = fm.channel
|
||||
GROUP BY fm.guest_id, fm.guest_identifier, fm.channel, fm.metadata, fm.created_at
|
||||
)
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (ORDER BY guest_id ASC) as guest_number,
|
||||
@@ -261,7 +274,8 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
channel,
|
||||
created_at,
|
||||
last_message_at,
|
||||
message_count
|
||||
message_count,
|
||||
metadata
|
||||
FROM guest_groups
|
||||
ORDER BY guest_id ASC`,
|
||||
[encryptionKey]
|
||||
@@ -276,9 +290,21 @@ router.get('/', requireAuth, async (req, res, next) => {
|
||||
const icon = channelMap[g.channel] || '👤';
|
||||
const rawId = g.guest_identifier.replace(`${g.channel}:`, '');
|
||||
|
||||
// Формируем имя в зависимости от канала
|
||||
// Проверяем, есть ли кастомное имя в metadata
|
||||
let metadata = g.metadata || {};
|
||||
if (typeof metadata === 'string') {
|
||||
try {
|
||||
metadata = JSON.parse(metadata);
|
||||
} catch (e) {
|
||||
metadata = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Формируем имя: сначала проверяем кастомное имя, затем генерируем автоматически
|
||||
let displayName;
|
||||
if (g.channel === 'email') {
|
||||
if (metadata.custom_name) {
|
||||
displayName = metadata.custom_name;
|
||||
} else if (g.channel === 'email') {
|
||||
displayName = `${icon} ${rawId}`;
|
||||
} else if (g.channel === 'telegram') {
|
||||
displayName = `${icon} Telegram (${rawId})`;
|
||||
@@ -456,14 +482,121 @@ router.patch('/:id', requireAuth, requirePermission(PERMISSIONS.EDIT_CONTACTS),
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { first_name, last_name, name, preferred_language, language, is_blocked } = req.body;
|
||||
const fields = [];
|
||||
const values = [];
|
||||
let idx = 1;
|
||||
|
||||
// Получаем ключ шифрования один раз
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Обработка гостевых контактов (guest_123)
|
||||
if (userId.startsWith('guest_')) {
|
||||
const guestId = parseInt(userId.replace('guest_', ''));
|
||||
|
||||
if (isNaN(guestId)) {
|
||||
return res.status(400).json({ success: false, error: 'Invalid guest ID format' });
|
||||
}
|
||||
|
||||
// Проверяем, существует ли гость и получаем его идентификатор
|
||||
const guestResult = await db.getQuery()(
|
||||
`WITH decrypted_guest AS (
|
||||
SELECT
|
||||
id,
|
||||
decrypt_text(identifier_encrypted, $2) as guest_identifier,
|
||||
channel,
|
||||
metadata
|
||||
FROM unified_guest_messages
|
||||
WHERE user_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
id as first_message_id,
|
||||
guest_identifier,
|
||||
channel,
|
||||
metadata
|
||||
FROM decrypted_guest
|
||||
WHERE id = $1
|
||||
LIMIT 1`,
|
||||
[guestId, encryptionKey]
|
||||
);
|
||||
|
||||
if (guestResult.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, error: 'Guest contact not found' });
|
||||
}
|
||||
|
||||
const guest = guestResult.rows[0];
|
||||
const firstMessageId = guest.first_message_id;
|
||||
let metadata = guest.metadata || {};
|
||||
|
||||
// Если metadata - строка, парсим её
|
||||
if (typeof metadata === 'string') {
|
||||
try {
|
||||
metadata = JSON.parse(metadata);
|
||||
} catch (e) {
|
||||
metadata = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка имени гостя
|
||||
let hasUpdates = false;
|
||||
if (name !== undefined) {
|
||||
const nameParts = name.trim().split(' ');
|
||||
metadata.custom_name = name.trim();
|
||||
metadata.custom_first_name = nameParts[0] || '';
|
||||
metadata.custom_last_name = nameParts.slice(1).join(' ') || '';
|
||||
hasUpdates = true;
|
||||
} else {
|
||||
if (first_name !== undefined) {
|
||||
metadata.custom_first_name = first_name;
|
||||
// Обновляем полное имя, если есть
|
||||
if (metadata.custom_last_name) {
|
||||
metadata.custom_name = `${first_name} ${metadata.custom_last_name}`.trim();
|
||||
} else {
|
||||
metadata.custom_name = first_name;
|
||||
}
|
||||
hasUpdates = true;
|
||||
}
|
||||
if (last_name !== undefined) {
|
||||
metadata.custom_last_name = last_name;
|
||||
// Обновляем полное имя, если есть
|
||||
if (metadata.custom_first_name) {
|
||||
metadata.custom_name = `${metadata.custom_first_name} ${last_name}`.trim();
|
||||
} else {
|
||||
metadata.custom_name = last_name;
|
||||
}
|
||||
hasUpdates = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Если имя пустое, удаляем кастомное имя
|
||||
if (name === '' || (first_name === '' && last_name === '')) {
|
||||
delete metadata.custom_name;
|
||||
delete metadata.custom_first_name;
|
||||
delete metadata.custom_last_name;
|
||||
hasUpdates = true;
|
||||
}
|
||||
|
||||
if (!hasUpdates) {
|
||||
return res.status(400).json({ success: false, error: 'Нет данных для обновления' });
|
||||
}
|
||||
|
||||
// Обновляем metadata первого сообщения гостя
|
||||
await db.getQuery()(
|
||||
`UPDATE unified_guest_messages
|
||||
SET metadata = $1
|
||||
WHERE id = $2`,
|
||||
[JSON.stringify(metadata), firstMessageId]
|
||||
);
|
||||
|
||||
broadcastContactsUpdate();
|
||||
return res.json({
|
||||
success: true,
|
||||
message: 'Имя гостя обновлено'
|
||||
});
|
||||
}
|
||||
|
||||
// Обработка обычных пользователей
|
||||
const fields = [];
|
||||
const values = [];
|
||||
let idx = 1;
|
||||
|
||||
// Обработка поля name - разбиваем на first_name и last_name
|
||||
if (name !== undefined) {
|
||||
const nameParts = name.trim().split(' ');
|
||||
@@ -504,8 +637,15 @@ router.patch('/:id', requireAuth, requirePermission(PERMISSIONS.EDIT_CONTACTS),
|
||||
}
|
||||
}
|
||||
if (!fields.length) return res.status(400).json({ success: false, error: 'Нет данных для обновления' });
|
||||
|
||||
// Проверяем, что userId - это число
|
||||
const userIdNum = Number(userId);
|
||||
if (isNaN(userIdNum)) {
|
||||
return res.status(400).json({ success: false, error: 'Invalid user ID format' });
|
||||
}
|
||||
|
||||
const sql = `UPDATE users SET ${fields.join(', ')} WHERE id = $${idx}`;
|
||||
values.push(userId);
|
||||
values.push(userIdNum);
|
||||
await db.query(sql, values);
|
||||
broadcastContactsUpdate();
|
||||
res.json({ success: true, message: 'Пользователь обновлен' });
|
||||
@@ -652,20 +792,22 @@ router.get('/:id', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), as
|
||||
decrypt_text(identifier_encrypted, $2) as guest_identifier,
|
||||
channel,
|
||||
created_at,
|
||||
user_id
|
||||
user_id,
|
||||
metadata
|
||||
FROM unified_guest_messages
|
||||
WHERE user_id IS NULL
|
||||
)
|
||||
SELECT
|
||||
MIN(id) as guest_id,
|
||||
id as guest_id,
|
||||
guest_identifier,
|
||||
channel,
|
||||
MIN(created_at) as created_at,
|
||||
MAX(created_at) as last_message_at,
|
||||
COUNT(*) as message_count
|
||||
created_at,
|
||||
(SELECT MAX(created_at) FROM decrypted_guest dg2 WHERE dg2.guest_identifier = decrypted_guest.guest_identifier AND dg2.channel = decrypted_guest.channel) as last_message_at,
|
||||
(SELECT COUNT(*) FROM decrypted_guest dg2 WHERE dg2.guest_identifier = decrypted_guest.guest_identifier AND dg2.channel = decrypted_guest.channel) as message_count,
|
||||
metadata
|
||||
FROM decrypted_guest
|
||||
GROUP BY guest_identifier, channel
|
||||
HAVING MIN(id) = $1`,
|
||||
WHERE id = $1
|
||||
LIMIT 1`,
|
||||
[guestId, encryptionKey]
|
||||
);
|
||||
|
||||
@@ -682,9 +824,21 @@ router.get('/:id', requireAuth, requirePermission(PERMISSIONS.VIEW_CONTACTS), as
|
||||
};
|
||||
const icon = channelMap[guest.channel] || '👤';
|
||||
|
||||
// Формируем имя в зависимости от канала
|
||||
// Проверяем, есть ли кастомное имя в metadata
|
||||
let metadata = guest.metadata || {};
|
||||
if (typeof metadata === 'string') {
|
||||
try {
|
||||
metadata = JSON.parse(metadata);
|
||||
} catch (e) {
|
||||
metadata = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Формируем имя: сначала проверяем кастомное имя, затем генерируем автоматически
|
||||
let displayName;
|
||||
if (guest.channel === 'email') {
|
||||
if (metadata.custom_name) {
|
||||
displayName = metadata.custom_name;
|
||||
} else if (guest.channel === 'email') {
|
||||
displayName = `${icon} ${rawId}`;
|
||||
} else if (guest.channel === 'telegram') {
|
||||
displayName = `${icon} Telegram (${rawId})`;
|
||||
|
||||
Reference in New Issue
Block a user