This commit is contained in:
Yash Goyal 2026-01-29 05:45:17 +00:00 committed by GitHub
commit 4b360f44ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 180 additions and 36 deletions

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import { hasControlCommand } from "../auto-reply/command-detection.js"; import { hasControlCommand } from "../auto-reply/command-detection.js";
import { import {
createInboundDebouncer, createInboundDebouncer,
@ -14,14 +13,53 @@ import { danger, logVerbose, warn } from "../globals.js";
import { resolveMedia } from "./bot/delivery.js"; import { resolveMedia } from "./bot/delivery.js";
import { withTelegramApiErrorLogging } from "./api-logging.js"; import { withTelegramApiErrorLogging } from "./api-logging.js";
import { resolveTelegramForumThreadId } from "./bot/helpers.js"; import { resolveTelegramForumThreadId } from "./bot/helpers.js";
import type { TelegramMessage } from "./bot/types.js"; import type { TelegramContext, TelegramMessage } from "./bot/types.js";
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js"; import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js";
import type { TelegramUpdateKeyContext } from "./bot-updates.js";
import { migrateTelegramGroupConfig } from "./group-migration.js"; import { migrateTelegramGroupConfig } from "./group-migration.js";
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js"; import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
import { readTelegramAllowFromStore } from "./pairing-store.js"; import { readTelegramAllowFromStore } from "./pairing-store.js";
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js"; import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js";
import { buildInlineKeyboard } from "./send.js"; import { buildInlineKeyboard } from "./send.js";
import type { Bot } from "grammy";
import type { MoltbotConfig } from "../config/types.clawdbot.js";
import type {
TelegramAccountConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../config/types.telegram.js";
import type { RuntimeEnv } from "../runtime.js";
import type { ChannelGroupPolicy } from "../config/group-policy.js";
import { Logger } from "tslog";
type TelegramHandlersParams = {
cfg: MoltbotConfig;
accountId: string | undefined;
bot: Bot;
opts: { token: string; proxyFetch?: typeof fetch };
runtime: RuntimeEnv;
mediaMaxBytes: number;
telegramCfg: TelegramAccountConfig;
groupAllowFrom?: Array<string | number>;
resolveGroupPolicy: (chatId: string | number) => ChannelGroupPolicy;
resolveTelegramGroupConfig: (
chatId: string | number,
messageThreadId?: number,
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean;
processMessage: (
primaryCtx: TelegramContext,
allMedia: Array<{
path: string;
contentType?: string;
stickerMetadata?: { emoji?: string; setName?: string; fileId?: string };
}>,
storeAllowFrom: string[],
options?: { forceWasMentioned?: boolean; messageIdOverride?: string },
) => Promise<void>;
logger: Logger<Record<string, unknown>>;
};
export const registerTelegramHandlers = ({ export const registerTelegramHandlers = ({
cfg, cfg,
@ -37,7 +75,7 @@ export const registerTelegramHandlers = ({
shouldSkipUpdate, shouldSkipUpdate,
processMessage, processMessage,
logger, logger,
}) => { }: TelegramHandlersParams) => {
const TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS = 4000; const TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS = 4000;
const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS = 1500; const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS = 1500;
const TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP = 1; const TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP = 1;
@ -49,7 +87,7 @@ export const registerTelegramHandlers = ({
type TextFragmentEntry = { type TextFragmentEntry = {
key: string; key: string;
messages: Array<{ msg: TelegramMessage; ctx: unknown; receivedAtMs: number }>; messages: Array<{ msg: TelegramMessage; ctx: TelegramContext; receivedAtMs: number }>;
timer: ReturnType<typeof setTimeout>; timer: ReturnType<typeof setTimeout>;
}; };
const textFragmentBuffer = new Map<string, TextFragmentEntry>(); const textFragmentBuffer = new Map<string, TextFragmentEntry>();
@ -57,7 +95,7 @@ export const registerTelegramHandlers = ({
const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" }); const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" });
type TelegramDebounceEntry = { type TelegramDebounceEntry = {
ctx: unknown; ctx: TelegramContext;
msg: TelegramMessage; msg: TelegramMessage;
allMedia: Array<{ path: string; contentType?: string }>; allMedia: Array<{ path: string; contentType?: string }>;
storeAllowFrom: string[]; storeAllowFrom: string[];
@ -86,9 +124,8 @@ export const registerTelegramHandlers = ({
.join("\n"); .join("\n");
if (!combinedText.trim()) return; if (!combinedText.trim()) return;
const first = entries[0]; const first = entries[0];
const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record<string, unknown>; const baseCtx = first.ctx;
const getFile = const getFile = baseCtx.getFile.bind(baseCtx) ?? (async () => ({}));
typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
const syntheticMessage: TelegramMessage = { const syntheticMessage: TelegramMessage = {
...first.msg, ...first.msg,
text: combinedText, text: combinedText,
@ -161,9 +198,8 @@ export const registerTelegramHandlers = ({
}; };
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record<string, unknown>; const baseCtx = first.ctx;
const getFile = const getFile = baseCtx.getFile.bind(baseCtx) ?? (async () => ({}));
typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
await processMessage( await processMessage(
{ message: syntheticMessage, me: baseCtx.me, getFile }, { message: syntheticMessage, me: baseCtx.me, getFile },
@ -377,7 +413,7 @@ export const registerTelegramHandlers = ({
caption_entities: undefined, caption_entities: undefined,
entities: undefined, entities: undefined,
}; };
const getFile = typeof ctx.getFile === "function" ? ctx.getFile.bind(ctx) : async () => ({}); const getFile = ctx.getFile.bind(ctx) ?? (async () => ({}));
await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, { await processMessage({ message: syntheticMessage, me: ctx.me, getFile }, [], storeAllowFrom, {
forceWasMentioned: true, forceWasMentioned: true,
messageIdOverride: callback.id, messageIdOverride: callback.id,

View File

@ -27,6 +27,7 @@ import { logVerbose, shouldLogVerbose } from "../globals.js";
import { recordChannelActivity } from "../infra/channel-activity.js"; import { recordChannelActivity } from "../infra/channel-activity.js";
import { resolveAgentRoute } from "../routing/resolve-route.js"; import { resolveAgentRoute } from "../routing/resolve-route.js";
import { resolveThreadSessionKeys } from "../routing/session-key.js"; import { resolveThreadSessionKeys } from "../routing/session-key.js";
import type { ResolvedAgentRoute } from "../routing/resolve-route.js";
import { shouldAckReaction as shouldAckReactionGate } from "../channels/ack-reactions.js"; import { shouldAckReaction as shouldAckReactionGate } from "../channels/ack-reactions.js";
import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js"; import { resolveMentionGatingWithBypass } from "../channels/mention-gating.js";
import { resolveControlCommandGate } from "../channels/command-gating.js"; import { resolveControlCommandGate } from "../channels/command-gating.js";
@ -54,6 +55,8 @@ import {
} from "./bot-access.js"; } from "./bot-access.js";
import { upsertTelegramPairingRequest } from "./pairing-store.js"; import { upsertTelegramPairingRequest } from "./pairing-store.js";
import type { TelegramContext } from "./bot/types.js"; import type { TelegramContext } from "./bot/types.js";
import type { FinalizedMsgContext } from "../auto-reply/templating.js";
import type { TelegramMessage } from "./bot/types.js";
type TelegramMediaRef = { type TelegramMediaRef = {
path: string; path: string;
@ -110,6 +113,33 @@ type BuildTelegramMessageContextParams = {
resolveTelegramGroupConfig: ResolveTelegramGroupConfig; resolveTelegramGroupConfig: ResolveTelegramGroupConfig;
}; };
export type TelegramMessageContext = {
ctxPayload: FinalizedMsgContext;
primaryCtx: TelegramContext;
msg: TelegramMessage;
chatId: number;
isGroup: boolean;
resolvedThreadId?: number;
isForum: boolean;
historyKey?: string;
historyLimit: number;
groupHistories: Map<string, HistoryEntry[]>;
route: ResolvedAgentRoute;
skillFilter?: string[];
sendTyping: () => Promise<void>;
sendRecordVoice: () => Promise<void>;
ackReactionPromise: Promise<boolean> | null;
reactionApi:
| ((
chatId: number | string,
messageId: number,
reactions: Array<{ type: "emoji"; emoji: string }>,
) => Promise<void>)
| null;
removeAckAfterReply: boolean;
accountId: string;
};
async function resolveStickerVisionSupport(params: { async function resolveStickerVisionSupport(params: {
cfg: MoltbotConfig; cfg: MoltbotConfig;
agentId?: string; agentId?: string;
@ -231,9 +261,7 @@ export const buildTelegramMessageContext = async ({
senderId: candidate, senderId: candidate,
senderUsername, senderUsername,
}); });
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${ const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${allowMatch.matchSource ?? "none"}`;
allowMatch.matchSource ?? "none"
}`;
const allowed = const allowed =
effectiveDmAllow.hasWildcard || (effectiveDmAllow.hasEntries && allowMatch.allowed); effectiveDmAllow.hasWildcard || (effectiveDmAllow.hasEntries && allowMatch.allowed);
if (!allowed) { if (!allowed) {

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js"; import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
import { import {
findModelInCatalog, findModelInCatalog,
@ -20,8 +19,15 @@ import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js";
import { createTelegramDraftStream } from "./draft-stream.js"; import { createTelegramDraftStream } from "./draft-stream.js";
import { cacheSticker, describeStickerImage } from "./sticker-cache.js"; import { cacheSticker, describeStickerImage } from "./sticker-cache.js";
import { resolveAgentDir } from "../agents/agent-scope.js"; import { resolveAgentDir } from "../agents/agent-scope.js";
import type { MoltbotConfig } from "../config/types.clawdbot.js";
import type { TelegramAccountConfig } from "../config/types.telegram.js";
import type { ReplyToMode } from "../config/types.base.js";
import type { Bot } from "grammy";
import type { RuntimeEnv } from "../runtime.js";
import type { TelegramContext, TelegramStreamMode } from "./bot/types.js";
import type { TelegramMessageContext } from "./bot-message-context.js";
async function resolveStickerVisionSupport(cfg, agentId) { async function resolveStickerVisionSupport(cfg: MoltbotConfig, agentId: string) {
try { try {
const catalog = await loadModelCatalog({ config: cfg }); const catalog = await loadModelCatalog({ config: cfg });
const defaultModel = resolveDefaultModelForAgent({ cfg, agentId }); const defaultModel = resolveDefaultModelForAgent({ cfg, agentId });
@ -44,6 +50,17 @@ export const dispatchTelegramMessage = async ({
telegramCfg, telegramCfg,
opts, opts,
resolveBotTopicsEnabled, resolveBotTopicsEnabled,
}: {
context: TelegramMessageContext;
bot: Bot;
cfg: MoltbotConfig;
runtime: RuntimeEnv;
replyToMode: ReplyToMode;
streamMode: TelegramStreamMode;
textLimit: number;
telegramCfg: TelegramAccountConfig;
opts: { token: string };
resolveBotTopicsEnabled: (ctx: TelegramContext) => Promise<boolean>;
}) => { }) => {
const { const {
ctxPayload, ctxPayload,
@ -185,6 +202,7 @@ export const dispatchTelegramMessage = async ({
} }
// Cache the description for future encounters // Cache the description for future encounters
if (sticker.fileId) {
cacheSticker({ cacheSticker({
fileId: sticker.fileId, fileId: sticker.fileId,
fileUniqueId: sticker.fileUniqueId, fileUniqueId: sticker.fileUniqueId,
@ -195,6 +213,9 @@ export const dispatchTelegramMessage = async ({
receivedFrom: ctxPayload.From, receivedFrom: ctxPayload.From,
}); });
logVerbose(`telegram: cached sticker description for ${sticker.fileUniqueId}`); logVerbose(`telegram: cached sticker description for ${sticker.fileUniqueId}`);
} else {
logVerbose(`telegram: skipped sticker cache (missing fileId) ${sticker.fileUniqueId}`);
}
} }
} }

View File

@ -1,8 +1,50 @@
// @ts-nocheck import type { Bot } from "grammy";
import { buildTelegramMessageContext } from "./bot-message-context.js"; import { buildTelegramMessageContext } from "./bot-message-context.js";
import { dispatchTelegramMessage } from "./bot-message-dispatch.js"; import { dispatchTelegramMessage } from "./bot-message-dispatch.js";
import type { MoltbotConfig } from "../config/types.clawdbot.js";
import type {
TelegramAccountConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../config/types.telegram.js";
import type { DmPolicy, ReplyToMode } from "../config/types.base.js";
import type { HistoryEntry } from "../auto-reply/reply/history.js";
import type { RuntimeEnv } from "../runtime.js";
import type { TelegramContext, TelegramStreamMode } from "./bot/types.js";
export const createTelegramMessageProcessor = (deps) => { export const createTelegramMessageProcessor = (deps: {
bot: Bot;
cfg: MoltbotConfig;
account: { accountId: string };
telegramCfg: TelegramAccountConfig;
historyLimit: number;
groupHistories: Map<string, HistoryEntry[]>;
dmPolicy: DmPolicy;
allowFrom?: (string | number)[];
groupAllowFrom?: (string | number)[];
ackReactionScope: "off" | "group-mentions" | "group-all" | "direct" | "all";
logger: { info: (obj: Record<string, unknown>, msg: string) => void };
resolveGroupActivation: (params: {
chatId: string | number;
agentId?: string;
messageThreadId?: number;
sessionKey?: string;
}) => boolean | undefined;
resolveGroupRequireMention: (chatId: string | number) => boolean;
resolveTelegramGroupConfig: (
chatId: string | number,
messageThreadId?: number,
) => {
groupConfig?: TelegramGroupConfig;
topicConfig?: TelegramTopicConfig;
};
runtime: RuntimeEnv;
replyToMode: ReplyToMode;
streamMode: TelegramStreamMode;
textLimit: number;
opts: { token: string };
resolveBotTopicsEnabled: (ctx?: TelegramContext) => Promise<boolean>;
}) => {
const { const {
bot, bot,
cfg, cfg,
@ -26,7 +68,16 @@ export const createTelegramMessageProcessor = (deps) => {
resolveBotTopicsEnabled, resolveBotTopicsEnabled,
} = deps; } = deps;
return async (primaryCtx, allMedia, storeAllowFrom, options) => { return async (
primaryCtx: TelegramContext,
allMedia: Array<{
path: string;
contentType?: string;
stickerMetadata?: { emoji?: string; setName?: string; fileId?: string };
}>,
storeAllowFrom: string[],
options?: { forceWasMentioned?: boolean; messageIdOverride?: string },
) => {
const context = await buildTelegramMessageContext({ const context = await buildTelegramMessageContext({
primaryCtx, primaryCtx,
allMedia, allMedia,

View File

@ -49,6 +49,7 @@ import {
} from "./bot/helpers.js"; } from "./bot/helpers.js";
import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js"; import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
import { readTelegramAllowFromStore } from "./pairing-store.js"; import { readTelegramAllowFromStore } from "./pairing-store.js";
import type { TelegramUpdateKeyContext } from "./bot-updates.js";
type TelegramNativeCommandContext = Context & { match?: string }; type TelegramNativeCommandContext = Context & { match?: string };
@ -83,7 +84,7 @@ type RegisterTelegramNativeCommandsParams = {
chatId: string | number, chatId: string | number,
messageThreadId?: number, messageThreadId?: number,
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig }; ) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
shouldSkipUpdate: (ctx: unknown) => boolean; shouldSkipUpdate: (ctx: TelegramUpdateKeyContext) => boolean;
opts: { token: string }; opts: { token: string };
}; };

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import { sequentialize } from "@grammyjs/runner"; import { sequentialize } from "@grammyjs/runner";
import { apiThrottler } from "@grammyjs/transformer-throttler"; import { apiThrottler } from "@grammyjs/transformer-throttler";
import type { ApiClientOptions } from "grammy"; import type { ApiClientOptions } from "grammy";
@ -46,6 +45,7 @@ import {
} from "./bot-updates.js"; } from "./bot-updates.js";
import { resolveTelegramFetch } from "./fetch.js"; import { resolveTelegramFetch } from "./fetch.js";
import { wasSentByBot } from "./sent-message-cache.js"; import { wasSentByBot } from "./sent-message-cache.js";
import type { ReactionTypeEmoji } from "grammy/types";
export type TelegramBotOptions = { export type TelegramBotOptions = {
token: string; token: string;
@ -385,11 +385,15 @@ export function createTelegramBot(opts: TelegramBotOptions) {
// Detect added reactions // Detect added reactions
const oldEmojis = new Set( const oldEmojis = new Set(
reaction.old_reaction reaction.old_reaction
.filter((r): r is { type: "emoji"; emoji: string } => r.type === "emoji") .filter(
(r): r is { type: "emoji"; emoji: ReactionTypeEmoji["emoji"] } => r.type === "emoji",
)
.map((r) => r.emoji), .map((r) => r.emoji),
); );
const addedReactions = reaction.new_reaction const addedReactions = reaction.new_reaction
.filter((r): r is { type: "emoji"; emoji: string } => r.type === "emoji") .filter(
(r): r is { type: "emoji"; emoji: ReactionTypeEmoji["emoji"] } => r.type === "emoji",
)
.filter((r) => !oldEmojis.has(r.emoji)); .filter((r) => !oldEmojis.has(r.emoji));
if (addedReactions.length === 0) return; if (addedReactions.length === 0) return;

View File

@ -1,11 +1,14 @@
// @ts-nocheck import type { Dispatcher } from "undici";
import { ProxyAgent } from "undici"; import { ProxyAgent } from "undici";
import { wrapFetchWithAbortSignal } from "../infra/fetch.js"; import { wrapFetchWithAbortSignal } from "../infra/fetch.js";
type FetchInit = RequestInit & { dispatcher?: Dispatcher };
export function makeProxyFetch(proxyUrl: string): typeof fetch { export function makeProxyFetch(proxyUrl: string): typeof fetch {
const agent = new ProxyAgent(proxyUrl); const agent = new ProxyAgent(proxyUrl);
return wrapFetchWithAbortSignal((input: RequestInfo | URL, init?: RequestInit) => { return wrapFetchWithAbortSignal((input: RequestInfo | URL, init?: RequestInit) => {
const base = init ? { ...init } : {}; const base = init ? { ...init } : {};
return fetch(input, { ...base, dispatcher: agent }); const nextInit: FetchInit = { ...base, dispatcher: agent };
return fetch(input, nextInit);
}); });
} }