From 5fb492ef7b6aa94374929d2a90caf71a4881c59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Fri, 8 May 2026 21:37:34 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=20=D0=B0=D0=BA=D1=82?= =?UTF-8?q?=D0=B8=D0=B2=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B4=D0=B8=D0=B0=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B0=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=B2=20Telegram?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/telegram.js | 112 +++++++++++++++++++++++++++++----------- 1 file changed, 82 insertions(+), 30 deletions(-) diff --git a/backend/src/telegram.js b/backend/src/telegram.js index 92ff3e4..06dba82 100644 --- a/backend/src/telegram.js +++ b/backend/src/telegram.js @@ -1,4 +1,4 @@ -import { Telegraf } from 'telegraf'; +import { Telegraf, Markup } from 'telegraf'; import { SocksProxyAgent } from 'socks-proxy-agent'; import { createOperatorMessage, @@ -11,6 +11,7 @@ import { let bot = null; let operatorMessageNotifier = null; +const activeOperatorConversations = new Map(); let botStatus = { enabled: false, running: false, @@ -48,6 +49,37 @@ function buildTelegramOptions(settings) { }; } +function getConversationIdFromReply(ctx) { + const replyTo = ctx.message?.reply_to_message; + const sourceText = replyTo?.text || replyTo?.caption || ''; + const match = sourceText.match(/Conversation:\s*(conv_[a-zA-Z0-9_-]+)/); + return match ? match[1] : ''; +} + +async function saveOperatorAnswer({ ctx, operator, conversationId, text }) { + const conversation = getConversationWithVisitor(conversationId); + if (!conversation) { + await ctx.reply('Conversation not found.'); + return false; + } + + const message = createOperatorMessage({ + conversationId, + siteId: conversation.site_id, + operatorId: operator.id, + body: text, + telegramMessageId: String(ctx.message.message_id) + }); + + if (operatorMessageNotifier) { + operatorMessageNotifier({ conversation, message, operator }); + } + + activeOperatorConversations.set(String(ctx.from.id), conversationId); + await ctx.reply(`Answer sent to ${conversation.site_domain || conversation.site_id} / ${conversation.visitor_key}.`); + return true; +} + export function setOperatorMessageNotifier(callback) { operatorMessageNotifier = callback; } @@ -55,7 +87,8 @@ export function setOperatorMessageNotifier(callback) { export function getTelegramBridgeStatus() { return { ...botStatus, - hasBot: Boolean(bot) + hasBot: Boolean(bot), + activeTelegramDialogs: activeOperatorConversations.size }; } @@ -69,6 +102,7 @@ export async function stopTelegramBridge(reason = 'restart') { } bot = null; + activeOperatorConversations.clear(); botStatus = { ...botStatus, running: false, @@ -117,14 +151,46 @@ export async function startTelegramBridge({ logger } = {}) { nextBot.command('status', async (ctx) => { const status = getTelegramBridgeStatus(); + const activeConversationId = activeOperatorConversations.get(String(ctx.from.id)); await ctx.reply( `WSChat bridge\n` + `running: ${status.running ? 'yes' : 'no'}\n` + `proxy: ${status.proxyEnabled ? 'enabled' : 'disabled'}\n` + - `bot: ${status.username || 'unknown'}` + `bot: ${status.username || 'unknown'}\n` + + `active dialog: ${activeConversationId || 'not selected'}` ); }); + nextBot.action(/^answer:(conv_[a-zA-Z0-9_-]+)$/, async (ctx) => { + const operator = getOperatorByTelegramUserId(ctx.from.id); + if (!operator) { + await ctx.answerCbQuery('Send /start first'); + return; + } + + const conversationId = ctx.match[1]; + const conversation = getConversationWithVisitor(conversationId); + if (!conversation) { + await ctx.answerCbQuery('Conversation not found'); + return; + } + + activeOperatorConversations.set(String(ctx.from.id), conversationId); + await ctx.answerCbQuery('Dialog selected'); + await ctx.reply( + `Active dialog selected:\n` + + `${conversation.site_domain || conversation.site_id}\n` + + `Visitor: ${conversation.visitor_key}\n\n` + + `Now send a normal Telegram message here. It will go to this visitor.` + ); + }); + + nextBot.action(/^close_active$/, async (ctx) => { + activeOperatorConversations.delete(String(ctx.from.id)); + await ctx.answerCbQuery('Active dialog cleared'); + await ctx.reply('Active dialog cleared. Choose another dialog with the Answer button or reply to a notification.'); + }); + nextBot.on('text', async (ctx) => { const text = ctx.message?.text || ''; if (!text || text.startsWith('/')) return; @@ -135,35 +201,16 @@ export async function startTelegramBridge({ logger } = {}) { return; } - const replyTo = ctx.message?.reply_to_message; - const sourceText = replyTo?.text || replyTo?.caption || ''; - const match = sourceText.match(/Conversation:\s*(conv_[a-zA-Z0-9_-]+)/); + const replyConversationId = getConversationIdFromReply(ctx); + const activeConversationId = activeOperatorConversations.get(String(ctx.from.id)); + const conversationId = replyConversationId || activeConversationId; - if (!match) { - await ctx.reply('Reply to a WSChat notification message to answer a visitor.'); + if (!conversationId) { + await ctx.reply('Choose a dialog with the Answer button or reply to a WSChat notification message.'); return; } - const conversationId = match[1]; - const conversation = getConversationWithVisitor(conversationId); - if (!conversation) { - await ctx.reply('Conversation not found.'); - return; - } - - const message = createOperatorMessage({ - conversationId, - siteId: conversation.site_id, - operatorId: operator.id, - body: text, - telegramMessageId: String(ctx.message.message_id) - }); - - if (operatorMessageNotifier) { - operatorMessageNotifier({ conversation, message, operator }); - } - - await ctx.reply(`Answer saved for conversation ${conversationId}.`); + await saveOperatorAnswer({ ctx, operator, conversationId, text }); }); const me = await nextBot.telegram.getMe(); @@ -221,13 +268,18 @@ export async function notifyOperatorsAboutVisitorMessage({ site, visitor, conver '', escapeHtml(message.body), '', - 'Reply to this message to save an operator answer.' + 'Press Answer to select this dialog, or reply to this message directly.' ].join('\n'); + const keyboard = Markup.inlineKeyboard([ + [Markup.button.callback('Ответить в этот чат', `answer:${conversation.id}`)], + [Markup.button.callback('Сбросить активный диалог', 'close_active')] + ]); + let sent = 0; for (const operator of operators) { try { - await bot.telegram.sendMessage(operator.telegram_user_id, text); + await bot.telegram.sendMessage(operator.telegram_user_id, text, keyboard); sent += 1; } catch (error) { logger?.warn?.({ operatorId: operator.id, error: error.message }, 'Failed to notify Telegram operator');