import { welcomeText, WELCOME_PARSE_MODE } from '../bot/welcome.js'; import { detectMediaFromBusiness } from '../core/media.js'; import { logger } from '../core/logger.js'; import { runHook } from '../core/registry.js'; import { rememberConnection, resolveOwner } from './connections.js'; /** * Подписывает бота на бизнес-апдейты Telegram и раздаёт их модулям через хуки. * Работает через официальное бизнес-подключение (Настройки → Telegram для * бизнеса → Чат-боты) — никакой сессии, только апдейты от подключённых аккаунтов. * * Мультитенант: бота подключают разные владельцы. У бизнес-сообщений НЕТ флага * «исходящее», поэтому по business_connection_id восстанавливаем владельца и * сравниваем with from.id — так отсекаем собственные сообщения владельца. Владелец * нужен и для адресации: уведомления/приветствие шлём в его личку (owner_chat_id). * * Событие удаления (deleted_business_messages) несёт только id сообщений и чат — * без содержимого. Поэтому модуль cache складывает каждое входящее заранее, * а antidelete достаёт его из кэша по (connId, chatId, msgId). */ export function wireBusiness(bot, registry, ctx) { bot.on('business_message', async (gctx) => { const msg = await normalizeBusinessMessage(gctx); return runHook(registry, 'onMessage', msg, ctx); }); bot.on('edited_business_message', async (gctx) => { const msg = await normalizeBusinessMessage(gctx); return runHook(registry, 'onEdited', msg, ctx); }); bot.on('deleted_business_messages', async (gctx) => { const d = gctx.deletedBusinessMessages; const connId = d.business_connection_id != null ? String(d.business_connection_id) : ''; const owner = await resolveOwner(connId, gctx.api); return runHook( registry, 'onDeleted', { connId, ownerId: owner?.ownerId ?? '', ownerChatId: owner?.ownerChatId ?? '', chatId: d.chat?.id, msgIds: d.message_ids ?? [], }, ctx, ); }); // Подключение/отключение бота владельцем. Здесь же — красочное приветствие. bot.on('business_connection', async (gctx) => { const c = gctx.businessConnection; if (!c) return; const record = rememberConnection(c); if (c.is_enabled) { logger.info(`Бизнес-подключение активно (владелец ${record.ownerId || '?'}, id ${c.id})`); await greetOwner(gctx.api, record); } else { logger.warn(`Бизнес-подключение отключено (id ${c.id}). Перехват для владельца приостановлен.`); } }); logger.debug('бизнес-апдейты подписаны: message / edited / deleted / connection'); } /** Шлёт приветствие владельцу в его личку с ботом сразу после подключения. */ async function greetOwner(api, record) { if (!record.ownerChatId) return; try { await api.sendMessage(record.ownerChatId, welcomeText({ name: record.ownerName, connected: true }), { parse_mode: WELCOME_PARSE_MODE, }); } catch (err) { logger.warn('business: не удалось отправить приветствие владельцу', err.message); } } /** * Приводит grammY-контекст бизнес-сообщения к плоскому виду, понятному модулям, * и подмешивает владельца подключения (connId/ownerId/ownerChatId). * isPrivateIncoming — входящее в личном 1-на-1 чате, НЕ от самого владельца. * Если владельца распознать не удалось (ownerId пуст) — считаем сообщение * неопределённым и НЕ кэшируем: лучше пропустить, чем сохранить своё исходящее. */ export async function normalizeBusinessMessage(gctx) { const msg = gctx.businessMessage ?? gctx.editedBusinessMessage ?? gctx.msg; const from = msg?.from; const chat = msg?.chat; const connId = msg?.business_connection_id != null ? String(msg.business_connection_id) : ''; const owner = await resolveOwner(connId, gctx.api); const ownerId = owner?.ownerId ?? ''; return { connId, ownerId, ownerChatId: owner?.ownerChatId ?? '', chatId: chat?.id, msgId: msg?.message_id, senderId: from?.id, sender: senderLabel(from), text: msg?.text ?? msg?.caption ?? '', media: detectMediaFromBusiness(msg), isPrivateIncoming: chat?.type === 'private' && ownerId !== '' && String(from?.id) !== ownerId, }; } function senderLabel(from) { if (!from) return ''; const name = [from.first_name, from.last_name].filter(Boolean).join(' '); return from.username ? `${name} (@${from.username})`.trim() : name; }