diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index ee98a218fc..0fd1883eed 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -1,4 +1,4 @@ -import type { Message } from "@grammyjs/types"; +import type { Message, ReactionTypeEmoji } from "@grammyjs/types"; import type { TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js"; import type { TelegramMediaRef } from "./bot-message-context.js"; import type { TelegramContext } from "./bot/types.js"; @@ -18,6 +18,7 @@ import { loadConfig } from "../config/config.js"; import { writeConfigFile } from "../config/io.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { danger, logVerbose, warn } from "../globals.js"; +import { enqueueSystemEvent } from "../infra/system-events.js"; import { readChannelAllowFromStore } from "../pairing/pairing-store.js"; import { resolveAgentRoute } from "../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../routing/session-key.js"; @@ -52,6 +53,7 @@ import { } from "./model-buttons.js"; import { getSentPoll } from "./poll-vote-cache.js"; import { buildInlineKeyboard } from "./send.js"; +import { wasSentByBot } from "./sent-message-cache.js"; export const registerTelegramHandlers = ({ cfg, @@ -459,6 +461,98 @@ export const registerTelegramHandlers = ({ return false; }; + // Handle emoji reactions to messages. + bot.on("message_reaction", async (ctx) => { + try { + const reaction = ctx.messageReaction; + if (!reaction) { + return; + } + if (shouldSkipUpdate(ctx)) { + return; + } + + const chatId = reaction.chat.id; + const messageId = reaction.message_id; + const user = reaction.user; + + // Resolve reaction notification mode (default: "own"). + const reactionMode = telegramCfg.reactionNotifications ?? "own"; + if (reactionMode === "off") { + return; + } + if (user?.is_bot) { + return; + } + if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) { + return; + } + + // Detect added reactions. + const oldEmojis = new Set( + reaction.old_reaction + .filter((r): r is ReactionTypeEmoji => r.type === "emoji") + .map((r) => r.emoji), + ); + const addedReactions = reaction.new_reaction + .filter((r): r is ReactionTypeEmoji => r.type === "emoji") + .filter((r) => !oldEmojis.has(r.emoji)); + + if (addedReactions.length === 0) { + return; + } + + // Build sender label. + const senderName = user + ? [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username + : undefined; + const senderUsername = user?.username ? `@${user.username}` : undefined; + let senderLabel = senderName; + if (senderName && senderUsername) { + senderLabel = `${senderName} (${senderUsername})`; + } else if (!senderName && senderUsername) { + senderLabel = senderUsername; + } + if (!senderLabel && user?.id) { + senderLabel = `id:${user.id}`; + } + senderLabel = senderLabel || "unknown"; + + // Reactions target a specific message_id; the Telegram Bot API does not include + // message_thread_id on MessageReactionUpdated, so we route to the chat-level + // session (forum topic routing is not available for reactions). + const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup"; + const isForum = reaction.chat.is_forum === true; + const resolvedThreadId = isForum + ? resolveTelegramForumThreadId({ isForum, messageThreadId: undefined }) + : undefined; + const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId); + const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId }); + // Fresh config for bindings lookup; other routing inputs are payload-derived. + const route = resolveAgentRoute({ + cfg: loadConfig(), + channel: "telegram", + accountId, + peer: { kind: isGroup ? "group" : "direct", id: peerId }, + parentPeer, + }); + const sessionKey = route.sessionKey; + + // Enqueue system event for each added reaction. + for (const r of addedReactions) { + const emoji = r.emoji; + const text = `Telegram reaction added: ${emoji} by ${senderLabel} on msg ${messageId}`; + enqueueSystemEvent(text, { + sessionKey, + contextKey: `telegram:reaction:add:${chatId}:${messageId}:${user?.id ?? "anon"}:${emoji}`, + }); + logVerbose(`telegram: reaction event enqueued: ${text}`); + } + } catch (err) { + runtime.error?.(danger(`telegram reaction handler failed: ${String(err)}`)); + } + }); + bot.on("callback_query", async (ctx) => { const callback = ctx.callbackQuery; if (!callback) { diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index aaba247590..5d995eab8f 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -1,7 +1,7 @@ import type { ApiClientOptions } from "grammy"; import { sequentialize } from "@grammyjs/runner"; import { apiThrottler } from "@grammyjs/transformer-throttler"; -import { type Message, type UserFromGetMe, ReactionTypeEmoji } from "@grammyjs/types"; +import { type Message, type UserFromGetMe } from "@grammyjs/types"; import { Bot, webhookCallback } from "grammy"; import type { OpenClawConfig, ReplyToMode } from "../config/config.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -22,10 +22,8 @@ import { import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { danger, logVerbose, shouldLogVerbose } from "../globals.js"; import { formatUncaughtError } from "../infra/errors.js"; -import { enqueueSystemEvent } from "../infra/system-events.js"; import { getChildLogger } from "../logging.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; -import { resolveAgentRoute } from "../routing/resolve-route.js"; import { resolveTelegramAccount } from "./accounts.js"; import { registerTelegramHandlers } from "./bot-handlers.js"; import { createTelegramMessageProcessor } from "./bot-message.js"; @@ -38,12 +36,10 @@ import { } from "./bot-updates.js"; import { buildTelegramGroupPeerId, - buildTelegramParentPeer, resolveTelegramForumThreadId, resolveTelegramStreamMode, } from "./bot/helpers.js"; import { resolveTelegramFetch } from "./fetch.js"; -import { wasSentByBot } from "./sent-message-cache.js"; export type TelegramBotOptions = { token: string; @@ -354,98 +350,6 @@ export function createTelegramBot(opts: TelegramBotOptions) { opts, }); - // Handle emoji reactions to messages - bot.on("message_reaction", async (ctx) => { - try { - const reaction = ctx.messageReaction; - if (!reaction) { - return; - } - if (shouldSkipUpdate(ctx)) { - return; - } - - const chatId = reaction.chat.id; - const messageId = reaction.message_id; - const user = reaction.user; - - // Resolve reaction notification mode (default: "own") - const reactionMode = telegramCfg.reactionNotifications ?? "own"; - if (reactionMode === "off") { - return; - } - if (user?.is_bot) { - return; - } - if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) { - return; - } - - // Detect added reactions - const oldEmojis = new Set( - reaction.old_reaction - .filter((r): r is ReactionTypeEmoji => r.type === "emoji") - .map((r) => r.emoji), - ); - const addedReactions = reaction.new_reaction - .filter((r): r is ReactionTypeEmoji => r.type === "emoji") - .filter((r) => !oldEmojis.has(r.emoji)); - - if (addedReactions.length === 0) { - return; - } - - // Build sender label - const senderName = user - ? [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username - : undefined; - const senderUsername = user?.username ? `@${user.username}` : undefined; - let senderLabel = senderName; - if (senderName && senderUsername) { - senderLabel = `${senderName} (${senderUsername})`; - } else if (!senderName && senderUsername) { - senderLabel = senderUsername; - } - if (!senderLabel && user?.id) { - senderLabel = `id:${user.id}`; - } - senderLabel = senderLabel || "unknown"; - - // Reactions target a specific message_id; the Telegram Bot API does not include - // message_thread_id on MessageReactionUpdated, so we route to the chat-level - // session (forum topic routing is not available for reactions). - const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup"; - const isForum = reaction.chat.is_forum === true; - const resolvedThreadId = isForum - ? resolveTelegramForumThreadId({ isForum, messageThreadId: undefined }) - : undefined; - const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId); - const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId }); - // Fresh config for bindings lookup; other routing inputs are payload-derived. - const route = resolveAgentRoute({ - cfg: loadConfig(), - channel: "telegram", - accountId: account.accountId, - peer: { kind: isGroup ? "group" : "direct", id: peerId }, - parentPeer, - }); - const sessionKey = route.sessionKey; - - // Enqueue system event for each added reaction - for (const r of addedReactions) { - const emoji = r.emoji; - const text = `Telegram reaction added: ${emoji} by ${senderLabel} on msg ${messageId}`; - enqueueSystemEvent(text, { - sessionKey: sessionKey, - contextKey: `telegram:reaction:add:${chatId}:${messageId}:${user?.id ?? "anon"}:${emoji}`, - }); - logVerbose(`telegram: reaction event enqueued: ${text}`); - } - } catch (err) { - runtime.error?.(danger(`telegram reaction handler failed: ${String(err)}`)); - } - }); - registerTelegramHandlers({ cfg, accountId: account.accountId,