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