From e4816aae8043432c7f1476f2b37424a6966bc781 Mon Sep 17 00:00:00 2001 From: Sam Padilla Date: Sun, 15 Feb 2026 22:53:25 -0600 Subject: [PATCH] feat(telegram): add channel_post support for bot-to-bot communication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Telegram bots cannot see messages from other bots in groups — this is a hard platform limitation. However, bots CAN see other bot messages in Telegram channels (broadcast channels). This patch: - Adds 'channel_post' to the allowed Telegram update types - Registers a channel_post handler that normalizes channel posts into the existing message pipeline via synthetic message/context objects - Enables bot-to-bot wake signals via private Telegram channels Use case: external services (APIs, CRMs) can send wake/trigger signals to an OpenClaw bot through a dedicated Telegram channel using a second bot, without exposing the gateway. --- src/telegram/allowed-updates.ts | 3 ++ src/telegram/bot-handlers.ts | 71 +++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/src/telegram/allowed-updates.ts b/src/telegram/allowed-updates.ts index e32fefd096f..a081373e810 100644 --- a/src/telegram/allowed-updates.ts +++ b/src/telegram/allowed-updates.ts @@ -7,5 +7,8 @@ export function resolveTelegramAllowedUpdates(): ReadonlyArray { + try { + const post = ctx.channelPost; + if (!post) { + return; + } + + const chatId = post.chat.id; + + // Check group allowlist (channels use the same groups config) + const groupAllowlist = resolveGroupPolicy(chatId); + if (groupAllowlist.allowlistEnabled && !groupAllowlist.allowed) { + return; + } + + const { groupConfig } = resolveTelegramGroupConfig(chatId, undefined); + if (!groupConfig || groupConfig.enabled === false) { + return; + } + + // Build a synthetic `from` field since channel posts may not have one. + // Use sender_chat (the bot/user that posted) if available. + const syntheticFrom = post.sender_chat + ? { + id: post.sender_chat.id, + is_bot: true as const, + first_name: post.sender_chat.title || "Channel", + username: post.sender_chat.username, + } + : { + id: chatId, + is_bot: true as const, + first_name: post.chat.title || "Channel", + username: post.chat.username, + }; + + const syntheticMsg: Message = { + ...post, + from: post.from ?? syntheticFrom, + chat: { + ...post.chat, + type: "supergroup" as const, + }, + } as Message; + + const syntheticCtx = Object.create(ctx, { + message: { value: syntheticMsg, writable: true, enumerable: true }, + }); + + const storeAllowFrom = await readChannelAllowFromStore( + "telegram", + process.env, + accountId, + ).catch(() => []); + + await inboundDebouncer.enqueue({ + ctx: syntheticCtx, + msg: syntheticMsg, + allMedia: [], + storeAllowFrom, + debounceKey: null, + botUsername: ctx.me?.username, + }); + } catch (err) { + runtime.error?.(danger(`channel_post handler failed: ${String(err)}`)); + } + }); };