Files
openclaw/src/telegram/bot-message-context.ts

474 lines
15 KiB
TypeScript
Raw Normal View History

import { ensureConfiguredAcpRouteReady } from "../acp/persistent-bindings.route.js";
2026-01-14 09:11:32 +00:00
import { resolveAckReaction } from "../agents/identity.js";
import { shouldAckReaction as shouldAckReactionGate } from "../channels/ack-reactions.js";
import { logInboundDrop } from "../channels/logging.js";
import {
createStatusReactionController,
type StatusReactionController,
} from "../channels/status-reactions.js";
import { loadConfig } from "../config/config.js";
import type { TelegramDirectConfig, TelegramGroupConfig } from "../config/types.js";
import { logVerbose } from "../globals.js";
2026-01-14 09:11:32 +00:00
import { recordChannelActivity } from "../infra/channel-activity.js";
import { buildAgentSessionKey, deriveLastRoutePolicy } from "../routing/resolve-route.js";
import { DEFAULT_ACCOUNT_ID, resolveThreadSessionKeys } from "../routing/session-key.js";
2026-01-26 20:25:06 -06:00
import { withTelegramApiErrorLogging } from "./api-logging.js";
import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from "./bot-access.js";
import { resolveTelegramInboundBody } from "./bot-message-context.body.js";
import { buildTelegramInboundContextPayload } from "./bot-message-context.session.js";
import type { BuildTelegramMessageContextParams } from "./bot-message-context.types.js";
import {
buildTypingThreadParams,
resolveTelegramDirectPeerId,
2026-02-02 08:53:42 +05:30
resolveTelegramThreadSpec,
2026-01-14 09:11:32 +00:00
} from "./bot/helpers.js";
import { resolveTelegramConversationRoute } from "./conversation-route.js";
import { enforceTelegramDmAccess } from "./dm-access.js";
import { evaluateTelegramGroupBaseAccess } from "./group-access.js";
import {
buildTelegramStatusReactionVariants,
resolveTelegramAllowedEmojiReactions,
resolveTelegramReactionVariant,
resolveTelegramStatusReactionEmojis,
} from "./status-reaction-variants.js";
export type {
BuildTelegramMessageContextParams,
TelegramMediaRef,
} from "./bot-message-context.types.js";
2026-01-14 09:11:32 +00:00
export const buildTelegramMessageContext = async ({
primaryCtx,
allMedia,
replyMedia = [],
2026-01-14 09:11:32 +00:00
storeAllowFrom,
options,
bot,
cfg,
account,
historyLimit,
groupHistories,
dmPolicy,
allowFrom,
groupAllowFrom,
ackReactionScope,
logger,
resolveGroupActivation,
resolveGroupRequireMention,
resolveTelegramGroupConfig,
sendChatActionHandler,
}: BuildTelegramMessageContextParams) => {
2026-01-14 09:11:32 +00:00
const msg = primaryCtx.message;
const chatId = msg.chat.id;
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
const senderId = msg.from?.id ? String(msg.from.id) : "";
const messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
2026-01-14 09:11:32 +00:00
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
2026-02-02 08:53:42 +05:30
const threadSpec = resolveTelegramThreadSpec({
isGroup,
2026-01-14 09:11:32 +00:00
isForum,
messageThreadId,
});
2026-02-02 08:53:42 +05:30
const resolvedThreadId = threadSpec.scope === "forum" ? threadSpec.id : undefined;
const replyThreadId = threadSpec.id;
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
const threadIdForConfig = resolvedThreadId ?? dmThreadId;
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, threadIdForConfig);
// Use direct config dmPolicy override if available for DMs
const effectiveDmPolicy =
!isGroup && groupConfig && "dmPolicy" in groupConfig
? (groupConfig.dmPolicy ?? dmPolicy)
: dmPolicy;
// Fresh config for bindings lookup; other routing inputs are payload-derived.
const freshCfg = loadConfig();
let { route, configuredBinding, configuredBindingSessionKey } = resolveTelegramConversationRoute({
cfg: freshCfg,
2026-01-14 09:11:32 +00:00
accountId: account.accountId,
chatId,
isGroup,
resolvedThreadId,
replyThreadId,
senderId,
topicAgentId: topicConfig?.agentId,
ACP: add persistent Discord channel and Telegram topic bindings (#34873) * docs: add ACP persistent binding experiment plan * docs: align ACP persistent binding spec to channel-local config * docs: scope Telegram ACP bindings to forum topics only * docs: lock bound /new and /reset behavior to in-place ACP reset * ACP: add persistent discord/telegram conversation bindings * ACP: fix persistent binding reuse and discord thread parent context * docs: document channel-specific persistent ACP bindings * ACP: split persistent bindings and share conversation id helpers * ACP: defer configured binding init until preflight passes * ACP: fix discord thread parent fallback and explicit disable inheritance * ACP: keep bound /new and /reset in-place * ACP: honor configured bindings in native command flows * ACP: avoid configured fallback after runtime bind failure * docs: refine ACP bindings experiment config examples * acp: cut over to typed top-level persistent bindings * ACP bindings: harden reset recovery and native command auth * Docs: add ACP bound command auth proposal * Tests: normalize i18n registry zh-CN assertion encoding * ACP bindings: address review findings for reset and fallback routing * ACP reset: gate hooks on success and preserve /new arguments * ACP bindings: fix auth and binding-priority review findings * Telegram ACP: gate ensure on auth and accepted messages * ACP bindings: fix session-key precedence and unavailable handling * ACP reset/native commands: honor fallback targets and abort on bootstrap failure * Config schema: validate ACP binding channel and Telegram topic IDs * Discord ACP: apply configured DM bindings to native commands * ACP reset tails: dispatch through ACP after command handling * ACP tails/native reset auth: fix target dispatch and restore full auth * ACP reset detection: fallback to active ACP keys for DM contexts * Tests: type runTurn mock input in ACP dispatch test * ACP: dedup binding route bootstrap and reset target resolution * reply: align ACP reset hooks with bound session key * docs: replace personal discord ids with placeholders * fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-05 09:38:12 +01:00
});
const requiresExplicitAccountBinding = (
candidate: ReturnType<typeof resolveTelegramConversationRoute>["route"],
): boolean => candidate.accountId !== DEFAULT_ACCOUNT_ID && candidate.matchedBy === "default";
const isNamedAccountFallback = requiresExplicitAccountBinding(route);
// Named-account groups still require an explicit binding; DMs get a
// per-account fallback session key below to preserve isolation.
if (isNamedAccountFallback && isGroup) {
logInboundDrop({
log: logVerbose,
channel: "telegram",
reason: "non-default account requires explicit binding",
target: route.accountId,
});
return null;
}
// Calculate groupAllowOverride first - it's needed for both DM and group allowlist checks
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
// For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom
const dmAllowFrom = groupAllowOverride ?? allowFrom;
const effectiveDmAllow = normalizeDmAllowFromWithStore({
allowFrom: dmAllowFrom,
storeAllowFrom,
dmPolicy: effectiveDmPolicy,
});
// Group sender checks are explicit and must not inherit DM pairing-store entries.
const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom);
2026-01-14 09:11:32 +00:00
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
const senderUsername = msg.from?.username ?? "";
const baseAccess = evaluateTelegramGroupBaseAccess({
isGroup,
groupConfig,
topicConfig,
hasGroupAllowOverride,
effectiveGroupAllow,
senderId,
senderUsername,
enforceAllowOverride: true,
requireSenderForAllowOverride: false,
});
if (!baseAccess.allowed) {
if (baseAccess.reason === "group-disabled") {
logVerbose(`Blocked telegram group ${chatId} (group disabled)`);
return null;
}
if (baseAccess.reason === "topic-disabled") {
logVerbose(
`Blocked telegram topic ${chatId} (${resolvedThreadId ?? "unknown"}) (topic disabled)`,
);
return null;
}
logVerbose(
isGroup
? `Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`
: `Blocked telegram DM sender ${senderId || "unknown"} (DM allowFrom override)`,
);
2026-01-14 09:11:32 +00:00
return null;
}
const requireTopic = (groupConfig as TelegramDirectConfig | undefined)?.requireTopic;
const topicRequiredButMissing = !isGroup && requireTopic === true && dmThreadId == null;
if (topicRequiredButMissing) {
logVerbose(`Blocked telegram DM ${chatId}: requireTopic=true but no topic present`);
return null;
}
2026-01-14 09:11:32 +00:00
const sendTyping = async () => {
2026-01-26 20:25:06 -06:00
await withTelegramApiErrorLogging({
operation: "sendChatAction",
fn: () =>
sendChatActionHandler.sendChatAction(
chatId,
"typing",
buildTypingThreadParams(replyThreadId),
),
2026-01-26 20:25:06 -06:00
});
2026-01-14 09:11:32 +00:00
};
2026-01-16 03:15:07 +00:00
const sendRecordVoice = async () => {
try {
2026-01-26 20:25:06 -06:00
await withTelegramApiErrorLogging({
operation: "sendChatAction",
fn: () =>
sendChatActionHandler.sendChatAction(
chatId,
"record_voice",
buildTypingThreadParams(replyThreadId),
),
2026-01-26 20:25:06 -06:00
});
2026-01-16 03:15:07 +00:00
} catch (err) {
logVerbose(`telegram record_voice cue failed for chat ${chatId}: ${String(err)}`);
}
};
if (
!(await enforceTelegramDmAccess({
isGroup,
dmPolicy: effectiveDmPolicy,
msg,
chatId,
effectiveDmAllow,
accountId: account.accountId,
bot,
logger,
}))
) {
return null;
2026-01-14 09:11:32 +00:00
}
ACP: add persistent Discord channel and Telegram topic bindings (#34873) * docs: add ACP persistent binding experiment plan * docs: align ACP persistent binding spec to channel-local config * docs: scope Telegram ACP bindings to forum topics only * docs: lock bound /new and /reset behavior to in-place ACP reset * ACP: add persistent discord/telegram conversation bindings * ACP: fix persistent binding reuse and discord thread parent context * docs: document channel-specific persistent ACP bindings * ACP: split persistent bindings and share conversation id helpers * ACP: defer configured binding init until preflight passes * ACP: fix discord thread parent fallback and explicit disable inheritance * ACP: keep bound /new and /reset in-place * ACP: honor configured bindings in native command flows * ACP: avoid configured fallback after runtime bind failure * docs: refine ACP bindings experiment config examples * acp: cut over to typed top-level persistent bindings * ACP bindings: harden reset recovery and native command auth * Docs: add ACP bound command auth proposal * Tests: normalize i18n registry zh-CN assertion encoding * ACP bindings: address review findings for reset and fallback routing * ACP reset: gate hooks on success and preserve /new arguments * ACP bindings: fix auth and binding-priority review findings * Telegram ACP: gate ensure on auth and accepted messages * ACP bindings: fix session-key precedence and unavailable handling * ACP reset/native commands: honor fallback targets and abort on bootstrap failure * Config schema: validate ACP binding channel and Telegram topic IDs * Discord ACP: apply configured DM bindings to native commands * ACP reset tails: dispatch through ACP after command handling * ACP tails/native reset auth: fix target dispatch and restore full auth * ACP reset detection: fallback to active ACP keys for DM contexts * Tests: type runTurn mock input in ACP dispatch test * ACP: dedup binding route bootstrap and reset target resolution * reply: align ACP reset hooks with bound session key * docs: replace personal discord ids with placeholders * fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-05 09:38:12 +01:00
const ensureConfiguredBindingReady = async (): Promise<boolean> => {
if (!configuredBinding) {
return true;
}
const ensured = await ensureConfiguredAcpRouteReady({
cfg: freshCfg,
configuredBinding,
});
if (ensured.ok) {
logVerbose(
`telegram: using configured ACP binding for ${configuredBinding.spec.conversationId} -> ${configuredBindingSessionKey}`,
);
return true;
}
logVerbose(
`telegram: configured ACP binding unavailable for ${configuredBinding.spec.conversationId}: ${ensured.error}`,
);
logInboundDrop({
log: logVerbose,
channel: "telegram",
reason: "configured ACP binding unavailable",
target: configuredBinding.spec.conversationId,
});
return false;
};
const baseSessionKey = isNamedAccountFallback
? buildAgentSessionKey({
agentId: route.agentId,
channel: "telegram",
accountId: route.accountId,
peer: {
kind: "direct",
id: resolveTelegramDirectPeerId({
chatId,
senderId,
}),
},
dmScope: "per-account-channel-peer",
identityLinks: freshCfg.session?.identityLinks,
}).toLowerCase()
: route.sessionKey;
ACP: add persistent Discord channel and Telegram topic bindings (#34873) * docs: add ACP persistent binding experiment plan * docs: align ACP persistent binding spec to channel-local config * docs: scope Telegram ACP bindings to forum topics only * docs: lock bound /new and /reset behavior to in-place ACP reset * ACP: add persistent discord/telegram conversation bindings * ACP: fix persistent binding reuse and discord thread parent context * docs: document channel-specific persistent ACP bindings * ACP: split persistent bindings and share conversation id helpers * ACP: defer configured binding init until preflight passes * ACP: fix discord thread parent fallback and explicit disable inheritance * ACP: keep bound /new and /reset in-place * ACP: honor configured bindings in native command flows * ACP: avoid configured fallback after runtime bind failure * docs: refine ACP bindings experiment config examples * acp: cut over to typed top-level persistent bindings * ACP bindings: harden reset recovery and native command auth * Docs: add ACP bound command auth proposal * Tests: normalize i18n registry zh-CN assertion encoding * ACP bindings: address review findings for reset and fallback routing * ACP reset: gate hooks on success and preserve /new arguments * ACP bindings: fix auth and binding-priority review findings * Telegram ACP: gate ensure on auth and accepted messages * ACP bindings: fix session-key precedence and unavailable handling * ACP reset/native commands: honor fallback targets and abort on bootstrap failure * Config schema: validate ACP binding channel and Telegram topic IDs * Discord ACP: apply configured DM bindings to native commands * ACP reset tails: dispatch through ACP after command handling * ACP tails/native reset auth: fix target dispatch and restore full auth * ACP reset detection: fallback to active ACP keys for DM contexts * Tests: type runTurn mock input in ACP dispatch test * ACP: dedup binding route bootstrap and reset target resolution * reply: align ACP reset hooks with bound session key * docs: replace personal discord ids with placeholders * fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-05 09:38:12 +01:00
// DMs: use thread suffix for session isolation (works regardless of dmScope)
const threadKeys =
dmThreadId != null
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${chatId}:${dmThreadId}` })
: null;
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
route = {
...route,
sessionKey,
lastRoutePolicy: deriveLastRoutePolicy({
sessionKey,
mainSessionKey: route.mainSessionKey,
}),
};
ACP: add persistent Discord channel and Telegram topic bindings (#34873) * docs: add ACP persistent binding experiment plan * docs: align ACP persistent binding spec to channel-local config * docs: scope Telegram ACP bindings to forum topics only * docs: lock bound /new and /reset behavior to in-place ACP reset * ACP: add persistent discord/telegram conversation bindings * ACP: fix persistent binding reuse and discord thread parent context * docs: document channel-specific persistent ACP bindings * ACP: split persistent bindings and share conversation id helpers * ACP: defer configured binding init until preflight passes * ACP: fix discord thread parent fallback and explicit disable inheritance * ACP: keep bound /new and /reset in-place * ACP: honor configured bindings in native command flows * ACP: avoid configured fallback after runtime bind failure * docs: refine ACP bindings experiment config examples * acp: cut over to typed top-level persistent bindings * ACP bindings: harden reset recovery and native command auth * Docs: add ACP bound command auth proposal * Tests: normalize i18n registry zh-CN assertion encoding * ACP bindings: address review findings for reset and fallback routing * ACP reset: gate hooks on success and preserve /new arguments * ACP bindings: fix auth and binding-priority review findings * Telegram ACP: gate ensure on auth and accepted messages * ACP bindings: fix session-key precedence and unavailable handling * ACP reset/native commands: honor fallback targets and abort on bootstrap failure * Config schema: validate ACP binding channel and Telegram topic IDs * Discord ACP: apply configured DM bindings to native commands * ACP reset tails: dispatch through ACP after command handling * ACP tails/native reset auth: fix target dispatch and restore full auth * ACP reset detection: fallback to active ACP keys for DM contexts * Tests: type runTurn mock input in ACP dispatch test * ACP: dedup binding route bootstrap and reset target resolution * reply: align ACP reset hooks with bound session key * docs: replace personal discord ids with placeholders * fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-05 09:38:12 +01:00
// Compute requireMention after access checks and final route selection.
const activationOverride = resolveGroupActivation({
chatId,
messageThreadId: resolvedThreadId,
sessionKey: sessionKey,
agentId: route.agentId,
});
const baseRequireMention = resolveGroupRequireMention(chatId);
const requireMention = firstDefined(
activationOverride,
topicConfig?.requireMention,
(groupConfig as TelegramGroupConfig | undefined)?.requireMention,
baseRequireMention,
);
2026-01-14 09:11:32 +00:00
recordChannelActivity({
channel: "telegram",
accountId: account.accountId,
direction: "inbound",
});
const bodyResult = await resolveTelegramInboundBody({
cfg,
primaryCtx,
msg,
allMedia,
isGroup,
chatId,
2026-01-14 09:11:32 +00:00
senderId,
senderUsername,
resolvedThreadId,
routeAgentId: route.agentId,
effectiveGroupAllow,
effectiveDmAllow,
groupConfig,
topicConfig,
requireMention,
options,
groupHistories,
historyLimit,
logger,
2026-01-14 09:11:32 +00:00
});
if (!bodyResult) {
return null;
}
2026-01-14 09:11:32 +00:00
ACP: add persistent Discord channel and Telegram topic bindings (#34873) * docs: add ACP persistent binding experiment plan * docs: align ACP persistent binding spec to channel-local config * docs: scope Telegram ACP bindings to forum topics only * docs: lock bound /new and /reset behavior to in-place ACP reset * ACP: add persistent discord/telegram conversation bindings * ACP: fix persistent binding reuse and discord thread parent context * docs: document channel-specific persistent ACP bindings * ACP: split persistent bindings and share conversation id helpers * ACP: defer configured binding init until preflight passes * ACP: fix discord thread parent fallback and explicit disable inheritance * ACP: keep bound /new and /reset in-place * ACP: honor configured bindings in native command flows * ACP: avoid configured fallback after runtime bind failure * docs: refine ACP bindings experiment config examples * acp: cut over to typed top-level persistent bindings * ACP bindings: harden reset recovery and native command auth * Docs: add ACP bound command auth proposal * Tests: normalize i18n registry zh-CN assertion encoding * ACP bindings: address review findings for reset and fallback routing * ACP reset: gate hooks on success and preserve /new arguments * ACP bindings: fix auth and binding-priority review findings * Telegram ACP: gate ensure on auth and accepted messages * ACP bindings: fix session-key precedence and unavailable handling * ACP reset/native commands: honor fallback targets and abort on bootstrap failure * Config schema: validate ACP binding channel and Telegram topic IDs * Discord ACP: apply configured DM bindings to native commands * ACP reset tails: dispatch through ACP after command handling * ACP tails/native reset auth: fix target dispatch and restore full auth * ACP reset detection: fallback to active ACP keys for DM contexts * Tests: type runTurn mock input in ACP dispatch test * ACP: dedup binding route bootstrap and reset target resolution * reply: align ACP reset hooks with bound session key * docs: replace personal discord ids with placeholders * fix: add changelog entry for ACP persistent bindings (#34873) (thanks @dutifulbob) --------- Co-authored-by: Onur <2453968+osolmaz@users.noreply.github.com>
2026-03-05 09:38:12 +01:00
if (!(await ensureConfiguredBindingReady())) {
return null;
}
2026-01-14 09:11:32 +00:00
// ACK reactions
const ackReaction = resolveAckReaction(cfg, route.agentId, {
channel: "telegram",
accountId: account.accountId,
});
2026-01-14 09:11:32 +00:00
const removeAckAfterReply = cfg.messages?.removeAckAfterReply ?? false;
const shouldAckReaction = () =>
Boolean(
ackReaction &&
shouldAckReactionGate({
scope: ackReactionScope,
isDirect: !isGroup,
isGroup,
isMentionableGroup: isGroup,
requireMention: Boolean(requireMention),
canDetectMention: bodyResult.canDetectMention,
effectiveWasMentioned: bodyResult.effectiveWasMentioned,
shouldBypassMention: bodyResult.shouldBypassMention,
}),
);
2026-01-14 09:11:32 +00:00
const api = bot.api as unknown as {
setMessageReaction?: (
chatId: number | string,
messageId: number,
reactions: Array<{ type: "emoji"; emoji: string }>,
) => Promise<void>;
getChat?: (chatId: number | string) => Promise<unknown>;
2026-01-14 09:11:32 +00:00
};
const reactionApi =
typeof api.setMessageReaction === "function" ? api.setMessageReaction.bind(api) : null;
const getChatApi = typeof api.getChat === "function" ? api.getChat.bind(api) : null;
// Status Reactions controller (lifecycle reactions)
const statusReactionsConfig = cfg.messages?.statusReactions;
const statusReactionsEnabled =
statusReactionsConfig?.enabled === true && Boolean(reactionApi) && shouldAckReaction();
const resolvedStatusReactionEmojis = resolveTelegramStatusReactionEmojis({
initialEmoji: ackReaction,
overrides: statusReactionsConfig?.emojis,
});
const statusReactionVariantsByEmoji = buildTelegramStatusReactionVariants(
resolvedStatusReactionEmojis,
);
let allowedStatusReactionEmojisPromise: Promise<Set<string> | null> | null = null;
const statusReactionController: StatusReactionController | null =
statusReactionsEnabled && msg.message_id
? createStatusReactionController({
enabled: true,
adapter: {
setReaction: async (emoji: string) => {
if (reactionApi) {
if (!allowedStatusReactionEmojisPromise) {
allowedStatusReactionEmojisPromise = resolveTelegramAllowedEmojiReactions({
chat: msg.chat,
chatId,
getChat: getChatApi ?? undefined,
}).catch((err) => {
logVerbose(
`telegram status-reaction available_reactions lookup failed for chat ${chatId}: ${String(err)}`,
);
return null;
});
}
const allowedStatusReactionEmojis = await allowedStatusReactionEmojisPromise;
const resolvedEmoji = resolveTelegramReactionVariant({
requestedEmoji: emoji,
variantsByRequestedEmoji: statusReactionVariantsByEmoji,
allowedEmojiReactions: allowedStatusReactionEmojis,
});
if (!resolvedEmoji) {
return;
}
await reactionApi(chatId, msg.message_id, [
{ type: "emoji", emoji: resolvedEmoji },
]);
}
},
// Telegram replaces atomically — no removeReaction needed
},
initialEmoji: ackReaction,
emojis: resolvedStatusReactionEmojis,
timing: statusReactionsConfig?.timing,
onError: (err) => {
logVerbose(`telegram status-reaction error for chat ${chatId}: ${String(err)}`);
},
})
: null;
// When status reactions are enabled, setQueued() replaces the simple ack reaction
const ackReactionPromise = statusReactionController
? shouldAckReaction()
? Promise.resolve(statusReactionController.setQueued()).then(
() => true,
() => false,
)
: null
: shouldAckReaction() && msg.message_id && reactionApi
2026-01-26 20:25:06 -06:00
? withTelegramApiErrorLogging({
operation: "setMessageReaction",
fn: () => reactionApi(chatId, msg.message_id, [{ type: "emoji", emoji: ackReaction }]),
}).then(
2026-01-14 09:11:32 +00:00
() => true,
(err) => {
logVerbose(`telegram react failed for chat ${chatId}: ${String(err)}`);
2026-01-14 09:11:32 +00:00
return false;
},
)
: null;
const { ctxPayload, skillFilter } = await buildTelegramInboundContextPayload({
cfg,
primaryCtx,
msg,
allMedia,
replyMedia,
isGroup,
isForum,
chatId,
senderId,
senderUsername,
resolvedThreadId,
dmThreadId,
threadSpec,
route,
rawBody: bodyResult.rawBody,
bodyText: bodyResult.bodyText,
historyKey: bodyResult.historyKey,
historyLimit,
groupHistories,
groupConfig,
topicConfig,
stickerCacheHit: bodyResult.stickerCacheHit,
effectiveWasMentioned: bodyResult.effectiveWasMentioned,
locationData: bodyResult.locationData,
options,
dmAllowFrom,
commandAuthorized: bodyResult.commandAuthorized,
});
2026-01-14 09:11:32 +00:00
return {
ctxPayload,
primaryCtx,
msg,
chatId,
isGroup,
resolvedThreadId,
2026-02-02 08:53:42 +05:30
threadSpec,
replyThreadId,
2026-01-14 09:11:32 +00:00
isForum,
historyKey: bodyResult.historyKey,
2026-01-14 09:11:32 +00:00
historyLimit,
groupHistories,
route,
skillFilter,
sendTyping,
2026-01-16 03:15:07 +00:00
sendRecordVoice,
2026-01-14 09:11:32 +00:00
ackReactionPromise,
reactionApi,
removeAckAfterReply,
statusReactionController,
2026-01-14 09:11:32 +00:00
accountId: account.accountId,
};
};
feat: per-channel responsePrefix override (#9001) * feat: per-channel responsePrefix override Add responsePrefix field to all channel config types and Zod schemas, enabling per-channel and per-account outbound response prefix overrides. Resolution cascade (most specific wins): L1: channels.<ch>.accounts.<id>.responsePrefix L2: channels.<ch>.responsePrefix L3: (reserved for channels.defaults) L4: messages.responsePrefix (existing global) Semantics: - undefined -> inherit from parent level - empty string -> explicitly no prefix (stops cascade) - "auto" -> derive [identity.name] from routed agent Changes: - Core logic: resolveResponsePrefix() in identity.ts accepts optional channel/accountId and walks the cascade - resolveEffectiveMessagesConfig() passes channel context through - Types: responsePrefix added to WhatsApp, Telegram, Discord, Slack, Signal, iMessage, Google Chat, MS Teams, Feishu, BlueBubbles configs - Zod schemas: responsePrefix added for config validation - All channel handlers wired: telegram, discord, slack, signal, imessage, line, heartbeat runner, route-reply, native commands - 23 new tests covering backward compat, channel/account levels, full cascade, auto keyword, empty string stops, unknown fallthrough Fully backward compatible - no existing config is affected. Fixes #8857 * fix: address CI lint + review feedback - Replace Record<string, any> with proper typed helpers (no-explicit-any) - Add curly braces to single-line if returns (eslint curly) - Fix JSDoc: 'Per-channel' → 'channel/account' on shared config types - Extract getChannelConfig() helper for type-safe dynamic key access * fix: finish responsePrefix overrides (#9001) (thanks @mudrii) * fix: normalize prefix wiring and types (#9001) (thanks @mudrii) --------- Co-authored-by: Gustavo Madeira Santana <gumadeiras@gmail.com>
2026-02-05 05:16:34 +08:00
export type TelegramMessageContext = NonNullable<
Awaited<ReturnType<typeof buildTelegramMessageContext>>
>;