fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
666
server/routes/bot-api.routes.ts
Normal file
666
server/routes/bot-api.routes.ts
Normal file
@@ -0,0 +1,666 @@
|
||||
import express from 'express';
|
||||
import crypto from 'crypto';
|
||||
import type { Response, NextFunction } from "express";
|
||||
import { storage } from "../storage";
|
||||
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
|
||||
import { tenantIsolation } from "../middleware/tenant.middleware";
|
||||
import { validateRequest } from "../middleware/validation.middleware";
|
||||
import { botMessageSchema, conversations, conversationMessages, conversationMembers, users, bots } from "@shared/schema";
|
||||
import { verifyAccessToken } from "../utils/jwt";
|
||||
import { sendWebhook } from "../utils/webhook";
|
||||
import { eventBus } from "./shared";
|
||||
import { pushTaskUpdated } from "../utils/pushTaskUpdated";
|
||||
import { decrypt } from "../crypto";
|
||||
import { withSuperAdmin, openTenantCtx, _tenantCtx, db } from "../db";
|
||||
import { eq, and, desc, sql, lt, inArray } from "drizzle-orm";
|
||||
import { enrichMessage, getMembersForSSE } from "./messenger.helpers";
|
||||
import { notificationService } from "../services/notification.service";
|
||||
|
||||
export function registerBotApiRoutes(app: import("express").Express): void {
|
||||
// =====================================================
|
||||
// BOT ACTIONS API - для ботов отправлять сообщения
|
||||
// =====================================================
|
||||
|
||||
// Middleware для аутентификации ботов
|
||||
// Поддерживает два типа токенов:
|
||||
// 1. Обычный токен бота (role: 'bot') - получается через /api/bot/auth/login
|
||||
// 2. Сервисный токен (type: 'bot_service') - передаётся в webhook при @mention
|
||||
const authenticateBot = async (req: AuthenticatedRequest & { bot?: Record<string, unknown> }, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Токен не предоставлен'
|
||||
});
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
// Попробуем сначала как обычный токен бота
|
||||
let botId: number;
|
||||
let organizationId: number;
|
||||
|
||||
try {
|
||||
const decoded = verifyAccessToken(token);
|
||||
if (decoded.appRole === 'bot') {
|
||||
// Обычный токен бота (userId содержит botId)
|
||||
botId = decoded.userId;
|
||||
organizationId = decoded.organizationId;
|
||||
} else {
|
||||
throw new Error('Not a bot token');
|
||||
}
|
||||
} catch {
|
||||
// Попробуем как сервисный токен
|
||||
try {
|
||||
const { verifyBotServiceToken } = await import('../utils/jwt');
|
||||
const serviceDecoded = verifyBotServiceToken(token);
|
||||
botId = serviceDecoded.botId;
|
||||
organizationId = serviceDecoded.organizationId;
|
||||
} catch {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Недействительный токен бота'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const bot = await withSuperAdmin(() => storage.getBot(botId, organizationId));
|
||||
if (!bot || !bot.isActive) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Бот не найден или деактивирован'
|
||||
});
|
||||
}
|
||||
|
||||
req.bot = bot;
|
||||
req.organizationId! = organizationId;
|
||||
|
||||
if (_tenantCtx.getStore()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
openTenantCtx(organizationId)
|
||||
.then((handle) => {
|
||||
handle.run(() => {
|
||||
const guard = setTimeout(() => {
|
||||
console.warn(`[POOL] Force-releasing bot tenant connection after 30s timeout (${req.method} ${req.path})`);
|
||||
handle.release();
|
||||
}, 30_000);
|
||||
res.once('finish', () => { clearTimeout(guard); handle.release(); });
|
||||
res.once('close', () => { clearTimeout(guard); handle.release(); });
|
||||
next();
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => next(err));
|
||||
} catch (error) {
|
||||
console.error('Bot auth error:', error);
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: 'Ошибка аутентификации бота'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Bot sends message with buttons
|
||||
app.post('/api/bot/messages',
|
||||
authenticateBot,
|
||||
validateRequest(botMessageSchema),
|
||||
async (req: AuthenticatedRequest & { bot?: Record<string, unknown> }, res) => {
|
||||
try {
|
||||
const { taskId, message, buttons, replyToMessageId } = req.body;
|
||||
|
||||
// Проверить что задача существует
|
||||
const task = await storage.getTask(taskId, req.organizationId!);
|
||||
if (!task) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Задача не найдена'
|
||||
});
|
||||
}
|
||||
|
||||
// Создать сообщение от бота
|
||||
const botMessage = await storage.createTaskMessage({
|
||||
taskId,
|
||||
formId: task.formId,
|
||||
authorId: null, // null для бота
|
||||
botId: req.bot!.id,
|
||||
replyToMessageId: replyToMessageId || null,
|
||||
message,
|
||||
messageType: 'bot',
|
||||
mentionedUserIds: null,
|
||||
botButtons: buttons || null
|
||||
}, req.organizationId!);
|
||||
|
||||
// Публикуем событие через SSE
|
||||
eventBus.publishEvent({
|
||||
type: 'message_created',
|
||||
organizationId: req.organizationId!,
|
||||
taskId,
|
||||
data: {
|
||||
taskId,
|
||||
message: {
|
||||
...botMessage,
|
||||
bot: {
|
||||
id: req.bot!.id,
|
||||
name: req.bot!.name,
|
||||
avatarUrl: req.bot!.avatarUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Сообщение отправлено',
|
||||
messageId: botMessage.id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Bot send message error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при отправке сообщения'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Bot callback handler - обработка нажатий на кнопки
|
||||
app.post('/api/bot/callback',
|
||||
authenticateToken,
|
||||
tenantIsolation,
|
||||
async (req: AuthenticatedRequest, res) => {
|
||||
try {
|
||||
const { messageId, callbackData } = req.body;
|
||||
|
||||
if (!messageId || !callbackData) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'messageId и callbackData обязательны'
|
||||
});
|
||||
}
|
||||
|
||||
// Получить сообщение
|
||||
const message = await storage.getTaskMessage(messageId, req.organizationId!);
|
||||
if (!message || !message.botId) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Сообщение бота не найдено'
|
||||
});
|
||||
}
|
||||
|
||||
// Получить бота
|
||||
const bot = await storage.getBot(message.botId, req.organizationId!);
|
||||
if (!bot || !bot.webhookUrl || !bot.webhookEnabled) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Бот не настроен для обработки callback'
|
||||
});
|
||||
}
|
||||
|
||||
// Получить задачу
|
||||
const task = await storage.getTask(message.taskId, req.organizationId!);
|
||||
|
||||
// Отправить callback в n8n webhook
|
||||
const callbackPayload = {
|
||||
event: 'button_callback',
|
||||
callbackData,
|
||||
messageId,
|
||||
taskId: message.taskId,
|
||||
task: task,
|
||||
user: {
|
||||
id: req.user!.id,
|
||||
firstName: req.user!.firstName,
|
||||
middleName: req.user!.middleName,
|
||||
lastName: req.user!.lastName,
|
||||
email: req.user!.email
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Создаём подпись HMAC если есть secret
|
||||
let signature = '';
|
||||
if (bot.webhookSecret) {
|
||||
const secret = decrypt(bot.webhookSecret);
|
||||
signature = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(JSON.stringify(callbackPayload))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
// Отправляем в n8n
|
||||
await sendWebhook(
|
||||
bot.webhookUrl,
|
||||
callbackPayload,
|
||||
{
|
||||
'X-Webhook-Signature': signature,
|
||||
'X-Bot-Id': bot.id.toString(),
|
||||
},
|
||||
{
|
||||
taskId: message.taskId,
|
||||
organizationId: req.organizationId!,
|
||||
botId: bot.id,
|
||||
eventType: 'button_callback',
|
||||
}
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Callback обработан'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Bot callback error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка обработки callback'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Bot get task info
|
||||
app.get('/api/bot/tasks/:id',
|
||||
authenticateBot,
|
||||
async (req: AuthenticatedRequest & { bot?: Record<string, unknown> }, res) => {
|
||||
try {
|
||||
const taskId = parseInt(req.params.id);
|
||||
if (isNaN(taskId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Некорректный ID задачи'
|
||||
});
|
||||
}
|
||||
|
||||
const task = await storage.getTask(taskId, req.organizationId!);
|
||||
if (!task) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Задача не найдена'
|
||||
});
|
||||
}
|
||||
|
||||
// Получить значения полей
|
||||
const fieldValues = await storage.getTaskFieldValues(taskId, req.organizationId!);
|
||||
|
||||
// Получить сообщения
|
||||
const messages = await storage.getTaskMessages(taskId, req.organizationId!);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
task: {
|
||||
...task,
|
||||
fieldValues,
|
||||
messages
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Bot get task error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при получении задачи'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Bot update task fields
|
||||
app.patch('/api/bot/tasks/:id/fields',
|
||||
authenticateBot,
|
||||
async (req: AuthenticatedRequest & { bot?: Record<string, unknown> }, res) => {
|
||||
try {
|
||||
const taskId = parseInt(req.params.id);
|
||||
if (isNaN(taskId)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Некорректный ID задачи'
|
||||
});
|
||||
}
|
||||
|
||||
const task = await storage.getTask(taskId, req.organizationId!);
|
||||
if (!task) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Задача не найдена'
|
||||
});
|
||||
}
|
||||
|
||||
const { fieldValues } = req.body;
|
||||
if (!fieldValues || typeof fieldValues !== 'object') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'fieldValues должен быть объектом'
|
||||
});
|
||||
}
|
||||
|
||||
// Получить поля формы
|
||||
const formFields = await storage.getFormFields(task.formId, req.organizationId!);
|
||||
const fieldMap = new Map(formFields.map(f => [f.code, f]));
|
||||
|
||||
// Получить старые значения для аудита
|
||||
const botOldValues = await storage.getTaskFieldValues(taskId, req.organizationId!);
|
||||
const botOldValueMap = new Map(botOldValues.map(v => [v.fieldId, v.value]));
|
||||
|
||||
// Обновить значения полей
|
||||
for (const [code, value] of Object.entries(fieldValues)) {
|
||||
const field = fieldMap.get(code);
|
||||
if (!field) continue;
|
||||
|
||||
try {
|
||||
await storage.updateTaskFieldValue(taskId, field.id, req.organizationId!, {
|
||||
value: value as string | number | boolean | null | Record<string, unknown>
|
||||
});
|
||||
} catch {
|
||||
// Если запись не существует, создаём
|
||||
await storage.createTaskFieldValue({
|
||||
taskId,
|
||||
fieldId: field.id,
|
||||
formId: task.formId,
|
||||
value: value as string | number | boolean | null | Record<string, unknown>
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Audit log: bot field changes
|
||||
for (const [code, value] of Object.entries(fieldValues)) {
|
||||
const field = fieldMap.get(code);
|
||||
if (!field) continue;
|
||||
const oldVal = botOldValueMap.get(field.id);
|
||||
const newVal = value;
|
||||
const oldStr = oldVal === null || oldVal === undefined ? '' : String(oldVal);
|
||||
const newStr = newVal === null || newVal === undefined ? '' : String(newVal);
|
||||
if (oldStr !== newStr) {
|
||||
storage.addTaskAuditLog({
|
||||
taskId,
|
||||
organizationId: req.organizationId!,
|
||||
action: 'field.changed',
|
||||
fieldId: field.id,
|
||||
fieldName: field.name,
|
||||
oldValue: oldVal ?? null,
|
||||
newValue: newVal as string | number | boolean | null,
|
||||
changedBy: null,
|
||||
changedByName: 'API / Бот',
|
||||
}).catch((auditErr: unknown) => { console.error('Audit log error:', auditErr); });
|
||||
}
|
||||
}
|
||||
|
||||
// Публикуем событие обновления задачи
|
||||
const updatedTask = await storage.getTask(taskId, req.organizationId!);
|
||||
eventBus.publishEvent({
|
||||
type: 'task_updated',
|
||||
organizationId: req.organizationId!,
|
||||
data: {
|
||||
taskId,
|
||||
formId: task.formId,
|
||||
task: updatedTask
|
||||
}
|
||||
});
|
||||
|
||||
// Web Push: notify all offline task participants
|
||||
pushTaskUpdated({
|
||||
taskId,
|
||||
organizationId: req.organizationId!,
|
||||
formId: task.formId,
|
||||
taskTitle: updatedTask?.title ?? task.title,
|
||||
actorId: 0, // bot update — no actor to exclude
|
||||
}).catch(() => {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Поля задачи обновлены'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Bot update task fields error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Ошибка при обновлении полей задачи'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Bot gets its messenger conversations (bot_direct only)
|
||||
app.get('/api/bot/conversations',
|
||||
authenticateBot,
|
||||
async (req: AuthenticatedRequest & { bot?: Record<string, unknown> }, res) => {
|
||||
try {
|
||||
const convs = await db
|
||||
.select()
|
||||
.from(conversations)
|
||||
.where(and(
|
||||
eq(conversations.type, 'bot_direct'),
|
||||
eq(conversations.botId, req.bot!.id as number),
|
||||
eq(conversations.organizationId, req.organizationId!),
|
||||
));
|
||||
|
||||
const result = await Promise.all(
|
||||
convs.map(async (conv) => {
|
||||
const members = await db
|
||||
.select({
|
||||
id: users.id,
|
||||
firstName: users.firstName,
|
||||
middleName: users.middleName,
|
||||
lastName: users.lastName,
|
||||
email: users.email,
|
||||
})
|
||||
.from(conversationMembers)
|
||||
.innerJoin(users, eq(conversationMembers.userId, users.id))
|
||||
.where(eq(conversationMembers.conversationId, conv.id));
|
||||
|
||||
return {
|
||||
id: conv.id,
|
||||
type: conv.type,
|
||||
createdAt: conv.createdAt,
|
||||
members,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json({ success: true, conversations: result });
|
||||
} catch (error) {
|
||||
console.error('Bot get conversations error:', error);
|
||||
res.status(500).json({ success: false, error: 'Ошибка при получении диалогов' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Bot gets messages from a messenger conversation (bot_direct only)
|
||||
app.get('/api/bot/conversations/:id/messages',
|
||||
authenticateBot,
|
||||
async (req: AuthenticatedRequest & { bot?: Record<string, unknown> }, res) => {
|
||||
try {
|
||||
const convId = parseInt(req.params.id);
|
||||
if (isNaN(convId)) {
|
||||
return res.status(400).json({ success: false, error: 'Некорректный ID чата' });
|
||||
}
|
||||
|
||||
const [conv] = await db
|
||||
.select()
|
||||
.from(conversations)
|
||||
.where(and(
|
||||
eq(conversations.id, convId),
|
||||
eq(conversations.organizationId, req.organizationId!),
|
||||
eq(conversations.type, 'bot_direct'),
|
||||
eq(conversations.botId, req.bot!.id as number),
|
||||
));
|
||||
|
||||
if (!conv) {
|
||||
return res.status(404).json({ success: false, error: 'Чат не найден или бот не является его участником' });
|
||||
}
|
||||
|
||||
const limit = Math.min(parseInt((req.query.limit as string) ?? '50'), 100);
|
||||
const beforeId = req.query.beforeId ? parseInt(req.query.beforeId as string) : null;
|
||||
|
||||
const conditions = [
|
||||
eq(conversationMessages.conversationId, convId),
|
||||
eq(conversationMessages.isDeleted, false),
|
||||
];
|
||||
if (beforeId !== null && !isNaN(beforeId)) {
|
||||
conditions.push(lt(conversationMessages.id, beforeId));
|
||||
}
|
||||
|
||||
const msgs = await db
|
||||
.select({
|
||||
id: conversationMessages.id,
|
||||
conversationId: conversationMessages.conversationId,
|
||||
message: conversationMessages.message,
|
||||
replyToId: conversationMessages.replyToId,
|
||||
mentionedUserIds: conversationMessages.mentionedUserIds,
|
||||
attachments: conversationMessages.attachments,
|
||||
createdAt: conversationMessages.createdAt,
|
||||
updatedAt: conversationMessages.updatedAt,
|
||||
isDeleted: conversationMessages.isDeleted,
|
||||
authorId: conversationMessages.authorId,
|
||||
botId: conversationMessages.botId,
|
||||
authorFirstName: users.firstName,
|
||||
authorLastName: users.lastName,
|
||||
})
|
||||
.from(conversationMessages)
|
||||
.leftJoin(users, eq(conversationMessages.authorId, users.id))
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(conversationMessages.id))
|
||||
.limit(limit);
|
||||
|
||||
// Load bot info for bot messages
|
||||
const botIds = [...new Set(msgs.filter(m => m.botId).map(m => m.botId!))];
|
||||
const botInfoMap = new Map<number, { id: number; name: string; avatarUrl: string | null }>();
|
||||
if (botIds.length > 0) {
|
||||
const botRows = await db
|
||||
.select({ id: bots.id, name: bots.name, avatarUrl: bots.avatarUrl })
|
||||
.from(bots)
|
||||
.where(inArray(bots.id, botIds));
|
||||
botRows.forEach(b => botInfoMap.set(b.id, b));
|
||||
}
|
||||
|
||||
const enrichedMsgs = await Promise.all(
|
||||
msgs.map(async m => {
|
||||
let replyTo: { id: number; message: string; author: { id: number; firstName: string; lastName: string } } | null = null;
|
||||
if (m.replyToId) {
|
||||
const [r] = await db
|
||||
.select({
|
||||
id: conversationMessages.id,
|
||||
message: conversationMessages.message,
|
||||
authorId: conversationMessages.authorId,
|
||||
authorFirstName: users.firstName,
|
||||
authorLastName: users.lastName,
|
||||
})
|
||||
.from(conversationMessages)
|
||||
.leftJoin(users, eq(conversationMessages.authorId, users.id))
|
||||
.where(and(
|
||||
eq(conversationMessages.id, m.replyToId),
|
||||
eq(conversationMessages.conversationId, convId),
|
||||
));
|
||||
if (r && r.authorId) {
|
||||
replyTo = {
|
||||
id: r.id,
|
||||
message: r.message,
|
||||
author: { id: r.authorId, firstName: r.authorFirstName ?? '', lastName: r.authorLastName ?? '' },
|
||||
};
|
||||
}
|
||||
}
|
||||
const botInfo = m.botId ? botInfoMap.get(m.botId) ?? null : null;
|
||||
return {
|
||||
...m,
|
||||
author: m.authorId
|
||||
? { id: m.authorId, firstName: m.authorFirstName ?? '', lastName: m.authorLastName ?? '' }
|
||||
: null,
|
||||
bot: botInfo,
|
||||
replyTo,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
res.json({ success: true, messages: enrichedMsgs.reverse() });
|
||||
} catch (error) {
|
||||
console.error('Bot get messenger messages error:', error);
|
||||
res.status(500).json({ success: false, error: 'Ошибка при получении сообщений' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Bot sends message to a messenger conversation (bot_direct only)
|
||||
app.post('/api/bot/conversations/:id/messages',
|
||||
authenticateBot,
|
||||
async (req: AuthenticatedRequest & { bot?: Record<string, unknown> }, res) => {
|
||||
try {
|
||||
const convId = parseInt(req.params.id);
|
||||
if (isNaN(convId)) {
|
||||
return res.status(400).json({ success: false, error: 'Некорректный ID чата' });
|
||||
}
|
||||
|
||||
const { message, attachments } = req.body;
|
||||
if (!message?.trim() && (!attachments || attachments.length === 0)) {
|
||||
return res.status(400).json({ success: false, error: 'Сообщение или вложения обязательны' });
|
||||
}
|
||||
|
||||
const [conv] = await db
|
||||
.select()
|
||||
.from(conversations)
|
||||
.where(and(
|
||||
eq(conversations.id, convId),
|
||||
eq(conversations.organizationId, req.organizationId!),
|
||||
eq(conversations.type, 'bot_direct'),
|
||||
eq(conversations.botId, req.bot!.id as number),
|
||||
));
|
||||
|
||||
if (!conv) {
|
||||
return res.status(404).json({ success: false, error: 'Чат не найден или бот не является его участником' });
|
||||
}
|
||||
|
||||
const [newMsg] = await db
|
||||
.insert(conversationMessages)
|
||||
.values({
|
||||
conversationId: convId,
|
||||
authorId: null,
|
||||
botId: req.bot!.id as number,
|
||||
message: message?.trim() || '',
|
||||
replyToId: null,
|
||||
mentionedUserIds: null,
|
||||
attachments: attachments?.length ? attachments : null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const enriched = await enrichMessage(newMsg.id);
|
||||
const members = await getMembersForSSE(convId, req.organizationId!);
|
||||
|
||||
for (const { userId: memberId, orgId: memberOrgId } of members) {
|
||||
eventBus.publishEvent({
|
||||
type: 'conv_message_created',
|
||||
data: { conversationId: convId, message: enriched },
|
||||
organizationId: memberOrgId,
|
||||
userId: memberId,
|
||||
});
|
||||
}
|
||||
|
||||
const botName = (req.bot!.name as string) || 'Бот';
|
||||
const memberMuteRows = await db
|
||||
.select({ userId: conversationMembers.userId, mutedAt: conversationMembers.mutedAt, externalOrgId: conversationMembers.externalOrgId })
|
||||
.from(conversationMembers)
|
||||
.where(eq(conversationMembers.conversationId, convId));
|
||||
|
||||
const memberOrgMap = new Map(
|
||||
memberMuteRows.map(r => [
|
||||
r.userId,
|
||||
{ orgId: r.externalOrgId ?? req.organizationId!, mutedAt: r.mutedAt ?? null },
|
||||
])
|
||||
);
|
||||
|
||||
notificationService.notifyMessengerMessage({
|
||||
conversationId: convId,
|
||||
authorId: -1,
|
||||
authorName: botName,
|
||||
chatName: botName,
|
||||
chatType: 'bot_direct',
|
||||
messageText: message?.trim() || '',
|
||||
mentionedUserIds: [],
|
||||
memberOrgMap,
|
||||
}).catch(err => console.error('[Bot Messenger Notify] Error:', err));
|
||||
|
||||
res.status(201).json({ success: true, message: enriched });
|
||||
} catch (error) {
|
||||
console.error('Bot send messenger message error:', error);
|
||||
res.status(500).json({ success: false, error: 'Ошибка при отправке сообщения' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user