// @ts-nocheck import { danger, logVerbose } from "../globals.js"; import { resolveMedia } from "./bot/delivery.js"; import { resolveTelegramForumThreadId } from "./bot/helpers.js"; import type { TelegramMessage } from "./bot/types.js"; import { firstDefined, isSenderAllowed, normalizeAllowFrom, } from "./bot-access.js"; import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js"; import { readTelegramAllowFromStore } from "./pairing-store.js"; export const registerTelegramHandlers = ({ bot, opts, runtime, mediaMaxBytes, telegramCfg, groupAllowFrom, resolveGroupPolicy, resolveTelegramGroupConfig, shouldSkipUpdate, processMessage, logger, }) => { const mediaGroupBuffer = new Map(); let mediaGroupProcessing: Promise = Promise.resolve(); const processMediaGroup = async (entry: MediaGroupEntry) => { try { entry.messages.sort((a, b) => a.msg.message_id - b.msg.message_id); const captionMsg = entry.messages.find( (m) => m.msg.caption || m.msg.text, ); const primaryEntry = captionMsg ?? entry.messages[0]; const allMedia: Array<{ path: string; contentType?: string }> = []; for (const { ctx } of entry.messages) { const media = await resolveMedia( ctx, mediaMaxBytes, opts.token, opts.proxyFetch, ); if (media) { allMedia.push({ path: media.path, contentType: media.contentType }); } } const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); await processMessage(primaryEntry.ctx, allMedia, storeAllowFrom); } catch (err) { runtime.error?.(danger(`media group handler failed: ${String(err)}`)); } }; bot.on("callback_query", async (ctx) => { const callback = ctx.callbackQuery; if (!callback) return; if (shouldSkipUpdate(ctx)) return; try { const data = (callback.data ?? "").trim(); const callbackMessage = callback.message; if (!data || !callbackMessage) return; const syntheticMessage: TelegramMessage = { ...callbackMessage, from: callback.from, text: data, caption: undefined, caption_entities: undefined, entities: undefined, }; const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({}); await processMessage( { message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, { forceWasMentioned: true, messageIdOverride: callback.id }, ); } catch (err) { runtime.error?.(danger(`callback handler failed: ${String(err)}`)); } finally { await bot.api.answerCallbackQuery(callback.id).catch(() => {}); } }); bot.on("message", async (ctx) => { try { const msg = ctx.message; if (!msg) return; if (shouldSkipUpdate(ctx)) return; const chatId = msg.chat.id; const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup"; const messageThreadId = (msg as { message_thread_id?: number }) .message_thread_id; const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true; const resolvedThreadId = resolveTelegramForumThreadId({ isForum, messageThreadId, }); const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const { groupConfig, topicConfig } = resolveTelegramGroupConfig( chatId, resolvedThreadId, ); const groupAllowOverride = firstDefined( topicConfig?.allowFrom, groupConfig?.allowFrom, ); const effectiveGroupAllow = normalizeAllowFrom([ ...(groupAllowOverride ?? groupAllowFrom ?? []), ...storeAllowFrom, ]); const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; if (isGroup) { if (groupConfig?.enabled === false) { logVerbose(`Blocked telegram group ${chatId} (group disabled)`); return; } if (topicConfig?.enabled === false) { logVerbose( `Blocked telegram topic ${chatId} (${resolvedThreadId ?? "unknown"}) (topic disabled)`, ); return; } if (hasGroupAllowOverride) { const senderId = msg.from?.id; const senderUsername = msg.from?.username ?? ""; const allowed = senderId != null && isSenderAllowed({ allow: effectiveGroupAllow, senderId: String(senderId), senderUsername, }); if (!allowed) { logVerbose( `Blocked telegram group sender ${senderId ?? "unknown"} (group allowFrom override)`, ); return; } } // Group policy filtering: controls how group messages are handled // - "open": groups bypass allowFrom, only mention-gating applies // - "disabled": block all group messages entirely // - "allowlist": only allow group messages from senders in groupAllowFrom/allowFrom const groupPolicy = telegramCfg.groupPolicy ?? "open"; if (groupPolicy === "disabled") { logVerbose(`Blocked telegram group message (groupPolicy: disabled)`); return; } if (groupPolicy === "allowlist") { // For allowlist mode, the sender (msg.from.id) must be in allowFrom const senderId = msg.from?.id; if (senderId == null) { logVerbose( `Blocked telegram group message (no sender ID, groupPolicy: allowlist)`, ); return; } if (!effectiveGroupAllow.hasEntries) { logVerbose( "Blocked telegram group message (groupPolicy: allowlist, no group allowlist entries)", ); return; } const senderUsername = msg.from?.username ?? ""; if ( !isSenderAllowed({ allow: effectiveGroupAllow, senderId: String(senderId), senderUsername, }) ) { logVerbose( `Blocked telegram group message from ${senderId} (groupPolicy: allowlist)`, ); return; } } // Group allowlist based on configured group IDs. const groupAllowlist = resolveGroupPolicy(chatId); if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) { logger.info( { chatId, title: msg.chat.title, reason: "not-allowed" }, "skipping group message", ); return; } } // Media group handling - buffer multi-image messages const mediaGroupId = (msg as { media_group_id?: string }).media_group_id; if (mediaGroupId) { const existing = mediaGroupBuffer.get(mediaGroupId); if (existing) { clearTimeout(existing.timer); existing.messages.push({ msg, ctx }); existing.timer = setTimeout(async () => { mediaGroupBuffer.delete(mediaGroupId); mediaGroupProcessing = mediaGroupProcessing .then(async () => { await processMediaGroup(existing); }) .catch(() => undefined); await mediaGroupProcessing; }, MEDIA_GROUP_TIMEOUT_MS); } else { const entry: MediaGroupEntry = { messages: [{ msg, ctx }], timer: setTimeout(async () => { mediaGroupBuffer.delete(mediaGroupId); mediaGroupProcessing = mediaGroupProcessing .then(async () => { await processMediaGroup(entry); }) .catch(() => undefined); await mediaGroupProcessing; }, MEDIA_GROUP_TIMEOUT_MS), }; mediaGroupBuffer.set(mediaGroupId, entry); } return; } let media: Awaited> = null; try { media = await resolveMedia( ctx, mediaMaxBytes, opts.token, opts.proxyFetch, ); } catch (mediaErr) { const errMsg = String(mediaErr); if (errMsg.includes("exceeds") && errMsg.includes("MB limit")) { const limitMb = Math.round(mediaMaxBytes / (1024 * 1024)); await bot.api .sendMessage( chatId, `⚠️ File too large. Maximum size is ${limitMb}MB.`, { reply_to_message_id: msg.message_id }, ) .catch(() => {}); logger.warn({ chatId, error: errMsg }, "media exceeds size limit"); return; } throw mediaErr; } const allMedia = media ? [{ path: media.path, contentType: media.contentType }] : []; await processMessage(ctx, allMedia, storeAllowFrom); } catch (err) { runtime.error?.(danger(`handler failed: ${String(err)}`)); } }); };