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 {
createInboundDebouncer,
@ -14,14 +13,53 @@ import { danger, logVerbose, warn } from "../globals.js";
import { resolveMedia } from "./bot/delivery.js";
import { withTelegramApiErrorLogging } from "./api-logging.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 { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js";
import type { TelegramUpdateKeyContext } from "./bot-updates.js";
import { migrateTelegramGroupConfig } from "./group-migration.js";
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
import { readTelegramAllowFromStore } from "./pairing-store.js";
import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.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 = ({
cfg,
@ -37,7 +75,7 @@ export const registerTelegramHandlers = ({
shouldSkipUpdate,
processMessage,
logger,
}) => {
}: TelegramHandlersParams) => {
const TELEGRAM_TEXT_FRAGMENT_START_THRESHOLD_CHARS = 4000;
const TELEGRAM_TEXT_FRAGMENT_MAX_GAP_MS = 1500;
const TELEGRAM_TEXT_FRAGMENT_MAX_ID_GAP = 1;
@ -49,7 +87,7 @@ export const registerTelegramHandlers = ({
type TextFragmentEntry = {
key: string;
messages: Array<{ msg: TelegramMessage; ctx: unknown; receivedAtMs: number }>;
messages: Array<{ msg: TelegramMessage; ctx: TelegramContext; receivedAtMs: number }>;
timer: ReturnType<typeof setTimeout>;
};
const textFragmentBuffer = new Map<string, TextFragmentEntry>();
@ -57,7 +95,7 @@ export const registerTelegramHandlers = ({
const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" });
type TelegramDebounceEntry = {
ctx: unknown;
ctx: TelegramContext;
msg: TelegramMessage;
allMedia: Array<{ path: string; contentType?: string }>;
storeAllowFrom: string[];
@ -86,9 +124,8 @@ export const registerTelegramHandlers = ({
.join("\n");
if (!combinedText.trim()) return;
const first = entries[0];
const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record<string, unknown>;
const getFile =
typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
const baseCtx = first.ctx;
const getFile = baseCtx.getFile.bind(baseCtx) ?? (async () => ({}));
const syntheticMessage: TelegramMessage = {
...first.msg,
text: combinedText,
@ -161,9 +198,8 @@ export const registerTelegramHandlers = ({
};
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
const baseCtx = first.ctx as { me?: unknown; getFile?: unknown } & Record<string, unknown>;
const getFile =
typeof baseCtx.getFile === "function" ? baseCtx.getFile.bind(baseCtx) : async () => ({});
const baseCtx = first.ctx;
const getFile = baseCtx.getFile.bind(baseCtx) ?? (async () => ({}));
await processMessage(
{ message: syntheticMessage, me: baseCtx.me, getFile },
@ -377,7 +413,7 @@ export const registerTelegramHandlers = ({
caption_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, {
forceWasMentioned: true,
messageIdOverride: callback.id,

View File

@ -27,6 +27,7 @@ import { logVerbose, shouldLogVerbose } from "../globals.js";
import { recordChannelActivity } from "../infra/channel-activity.js";
import { resolveAgentRoute } from "../routing/resolve-route.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 { resolveMentionGatingWithBypass } from "../channels/mention-gating.js";
import { resolveControlCommandGate } from "../channels/command-gating.js";
@ -54,6 +55,8 @@ import {
} from "./bot-access.js";
import { upsertTelegramPairingRequest } from "./pairing-store.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 = {
path: string;
@ -110,6 +113,33 @@ type BuildTelegramMessageContextParams = {
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: {
cfg: MoltbotConfig;
agentId?: string;
@ -231,9 +261,7 @@ export const buildTelegramMessageContext = async ({
senderId: candidate,
senderUsername,
});
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${
allowMatch.matchSource ?? "none"
}`;
const allowMatchMeta = `matchKey=${allowMatch.matchKey ?? "none"} matchSource=${allowMatch.matchSource ?? "none"}`;
const allowed =
effectiveDmAllow.hasWildcard || (effectiveDmAllow.hasEntries && allowMatch.allowed);
if (!allowed) {

View File

@ -1,4 +1,3 @@
// @ts-nocheck
import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
import {
findModelInCatalog,
@ -20,8 +19,15 @@ import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js";
import { createTelegramDraftStream } from "./draft-stream.js";
import { cacheSticker, describeStickerImage } from "./sticker-cache.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 {
const catalog = await loadModelCatalog({ config: cfg });
const defaultModel = resolveDefaultModelForAgent({ cfg, agentId });
@ -44,6 +50,17 @@ export const dispatchTelegramMessage = async ({
telegramCfg,
opts,
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 {
ctxPayload,
@ -185,16 +202,20 @@ export const dispatchTelegramMessage = async ({
}
// Cache the description for future encounters
cacheSticker({
fileId: sticker.fileId,
fileUniqueId: sticker.fileUniqueId,
emoji: sticker.emoji,
setName: sticker.setName,
description,
cachedAt: new Date().toISOString(),
receivedFrom: ctxPayload.From,
});
logVerbose(`telegram: cached sticker description for ${sticker.fileUniqueId}`);
if (sticker.fileId) {
cacheSticker({
fileId: sticker.fileId,
fileUniqueId: sticker.fileUniqueId,
emoji: sticker.emoji,
setName: sticker.setName,
description,
cachedAt: new Date().toISOString(),
receivedFrom: ctxPayload.From,
});
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 { 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 {
bot,
cfg,
@ -26,7 +68,16 @@ export const createTelegramMessageProcessor = (deps) => {
resolveBotTopicsEnabled,
} = 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({
primaryCtx,
allMedia,

View File

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

View File

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

View File

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