Добавлен выбор активного диалога кнопкой в Telegram

Этот коммит содержится в:
Виктор
2026-05-08 21:37:34 +09:00
родитель ccda416945
Коммит 5fb492ef7b
+82 -30
Просмотреть файл
@@ -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');