317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
import { normalizeCommandBody } from "../auto-reply/commands-registry.js";
|
|
import { formatInboundEnvelope, resolveEnvelopeFormatOptions } from "../auto-reply/envelope.js";
|
|
import {
|
|
buildPendingHistoryContextFromMap,
|
|
type HistoryEntry,
|
|
} from "../auto-reply/reply/history.js";
|
|
import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
|
|
import { toLocationContext } from "../channels/location.js";
|
|
import { recordInboundSession } from "../channels/session.js";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js";
|
|
import type {
|
|
TelegramDirectConfig,
|
|
TelegramGroupConfig,
|
|
TelegramTopicConfig,
|
|
} from "../config/types.js";
|
|
import { logVerbose, shouldLogVerbose } from "../globals.js";
|
|
import type { ResolvedAgentRoute } from "../routing/resolve-route.js";
|
|
import { resolveInboundLastRouteSessionKey } from "../routing/resolve-route.js";
|
|
import { resolvePinnedMainDmOwnerFromAllowlist } from "../security/dm-policy-shared.js";
|
|
import { normalizeAllowFrom } from "./bot-access.js";
|
|
import type {
|
|
TelegramMediaRef,
|
|
TelegramMessageContextOptions,
|
|
} from "./bot-message-context.types.js";
|
|
import {
|
|
buildGroupLabel,
|
|
buildSenderLabel,
|
|
buildSenderName,
|
|
buildTelegramGroupFrom,
|
|
describeReplyTarget,
|
|
normalizeForwardedContext,
|
|
type TelegramThreadSpec,
|
|
} from "./bot/helpers.js";
|
|
import type { TelegramContext } from "./bot/types.js";
|
|
import { resolveTelegramGroupPromptSettings } from "./group-config-helpers.js";
|
|
|
|
export async function buildTelegramInboundContextPayload(params: {
|
|
cfg: OpenClawConfig;
|
|
primaryCtx: TelegramContext;
|
|
msg: TelegramContext["message"];
|
|
allMedia: TelegramMediaRef[];
|
|
replyMedia: TelegramMediaRef[];
|
|
isGroup: boolean;
|
|
isForum: boolean;
|
|
chatId: number | string;
|
|
senderId: string;
|
|
senderUsername: string;
|
|
resolvedThreadId?: number;
|
|
dmThreadId?: number;
|
|
threadSpec: TelegramThreadSpec;
|
|
route: ResolvedAgentRoute;
|
|
rawBody: string;
|
|
bodyText: string;
|
|
historyKey?: string;
|
|
historyLimit: number;
|
|
groupHistories: Map<string, HistoryEntry[]>;
|
|
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
|
|
topicConfig?: TelegramTopicConfig;
|
|
stickerCacheHit: boolean;
|
|
effectiveWasMentioned: boolean;
|
|
commandAuthorized: boolean;
|
|
locationData?: import("../channels/location.js").NormalizedLocation;
|
|
options?: TelegramMessageContextOptions;
|
|
dmAllowFrom?: Array<string | number>;
|
|
}): Promise<{
|
|
ctxPayload: ReturnType<typeof finalizeInboundContext>;
|
|
skillFilter: string[] | undefined;
|
|
}> {
|
|
const {
|
|
cfg,
|
|
primaryCtx,
|
|
msg,
|
|
allMedia,
|
|
replyMedia,
|
|
isGroup,
|
|
isForum,
|
|
chatId,
|
|
senderId,
|
|
senderUsername,
|
|
resolvedThreadId,
|
|
dmThreadId,
|
|
threadSpec,
|
|
route,
|
|
rawBody,
|
|
bodyText,
|
|
historyKey,
|
|
historyLimit,
|
|
groupHistories,
|
|
groupConfig,
|
|
topicConfig,
|
|
stickerCacheHit,
|
|
effectiveWasMentioned,
|
|
commandAuthorized,
|
|
locationData,
|
|
options,
|
|
dmAllowFrom,
|
|
} = params;
|
|
const replyTarget = describeReplyTarget(msg);
|
|
const forwardOrigin = normalizeForwardedContext(msg);
|
|
const replyForwardAnnotation = replyTarget?.forwardedFrom
|
|
? `[Forwarded from ${replyTarget.forwardedFrom.from}${
|
|
replyTarget.forwardedFrom.date
|
|
? ` at ${new Date(replyTarget.forwardedFrom.date * 1000).toISOString()}`
|
|
: ""
|
|
}]\n`
|
|
: "";
|
|
const replySuffix = replyTarget
|
|
? replyTarget.kind === "quote"
|
|
? `\n\n[Quoting ${replyTarget.sender}${
|
|
replyTarget.id ? ` id:${replyTarget.id}` : ""
|
|
}]\n${replyForwardAnnotation}"${replyTarget.body}"\n[/Quoting]`
|
|
: `\n\n[Replying to ${replyTarget.sender}${
|
|
replyTarget.id ? ` id:${replyTarget.id}` : ""
|
|
}]\n${replyForwardAnnotation}${replyTarget.body}\n[/Replying]`
|
|
: "";
|
|
const forwardPrefix = forwardOrigin
|
|
? `[Forwarded from ${forwardOrigin.from}${
|
|
forwardOrigin.date ? ` at ${new Date(forwardOrigin.date * 1000).toISOString()}` : ""
|
|
}]\n`
|
|
: "";
|
|
const groupLabel = isGroup ? buildGroupLabel(msg, chatId, resolvedThreadId) : undefined;
|
|
const senderName = buildSenderName(msg);
|
|
const conversationLabel = isGroup
|
|
? (groupLabel ?? `group:${chatId}`)
|
|
: buildSenderLabel(msg, senderId || chatId);
|
|
const storePath = resolveStorePath(cfg.session?.store, {
|
|
agentId: route.agentId,
|
|
});
|
|
const envelopeOptions = resolveEnvelopeFormatOptions(cfg);
|
|
const previousTimestamp = readSessionUpdatedAt({
|
|
storePath,
|
|
sessionKey: route.sessionKey,
|
|
});
|
|
const body = formatInboundEnvelope({
|
|
channel: "Telegram",
|
|
from: conversationLabel,
|
|
timestamp: msg.date ? msg.date * 1000 : undefined,
|
|
body: `${forwardPrefix}${bodyText}${replySuffix}`,
|
|
chatType: isGroup ? "group" : "direct",
|
|
sender: {
|
|
name: senderName,
|
|
username: senderUsername || undefined,
|
|
id: senderId || undefined,
|
|
},
|
|
previousTimestamp,
|
|
envelope: envelopeOptions,
|
|
});
|
|
let combinedBody = body;
|
|
if (isGroup && historyKey && historyLimit > 0) {
|
|
combinedBody = buildPendingHistoryContextFromMap({
|
|
historyMap: groupHistories,
|
|
historyKey,
|
|
limit: historyLimit,
|
|
currentMessage: combinedBody,
|
|
formatEntry: (entry) =>
|
|
formatInboundEnvelope({
|
|
channel: "Telegram",
|
|
from: groupLabel ?? `group:${chatId}`,
|
|
timestamp: entry.timestamp,
|
|
body: `${entry.body} [id:${entry.messageId ?? "unknown"} chat:${chatId}]`,
|
|
chatType: "group",
|
|
senderLabel: entry.sender,
|
|
envelope: envelopeOptions,
|
|
}),
|
|
});
|
|
}
|
|
|
|
const { skillFilter, groupSystemPrompt } = resolveTelegramGroupPromptSettings({
|
|
groupConfig,
|
|
topicConfig,
|
|
});
|
|
const commandBody = normalizeCommandBody(rawBody, {
|
|
botUsername: primaryCtx.me?.username?.toLowerCase(),
|
|
});
|
|
const inboundHistory =
|
|
isGroup && historyKey && historyLimit > 0
|
|
? (groupHistories.get(historyKey) ?? []).map((entry) => ({
|
|
sender: entry.sender,
|
|
body: entry.body,
|
|
timestamp: entry.timestamp,
|
|
}))
|
|
: undefined;
|
|
const currentMediaForContext = stickerCacheHit ? [] : allMedia;
|
|
const contextMedia = [...currentMediaForContext, ...replyMedia];
|
|
const ctxPayload = finalizeInboundContext({
|
|
Body: combinedBody,
|
|
BodyForAgent: bodyText,
|
|
InboundHistory: inboundHistory,
|
|
RawBody: rawBody,
|
|
CommandBody: commandBody,
|
|
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
|
|
To: `telegram:${chatId}`,
|
|
SessionKey: route.sessionKey,
|
|
AccountId: route.accountId,
|
|
ChatType: isGroup ? "group" : "direct",
|
|
ConversationLabel: conversationLabel,
|
|
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
|
|
GroupSystemPrompt: isGroup || (!isGroup && groupConfig) ? groupSystemPrompt : undefined,
|
|
SenderName: senderName,
|
|
SenderId: senderId || undefined,
|
|
SenderUsername: senderUsername || undefined,
|
|
Provider: "telegram",
|
|
Surface: "telegram",
|
|
MessageSid: options?.messageIdOverride ?? String(msg.message_id),
|
|
ReplyToId: replyTarget?.id,
|
|
ReplyToBody: replyTarget?.body,
|
|
ReplyToSender: replyTarget?.sender,
|
|
ReplyToIsQuote: replyTarget?.kind === "quote" ? true : undefined,
|
|
ReplyToForwardedFrom: replyTarget?.forwardedFrom?.from,
|
|
ReplyToForwardedFromType: replyTarget?.forwardedFrom?.fromType,
|
|
ReplyToForwardedFromId: replyTarget?.forwardedFrom?.fromId,
|
|
ReplyToForwardedFromUsername: replyTarget?.forwardedFrom?.fromUsername,
|
|
ReplyToForwardedFromTitle: replyTarget?.forwardedFrom?.fromTitle,
|
|
ReplyToForwardedDate: replyTarget?.forwardedFrom?.date
|
|
? replyTarget.forwardedFrom.date * 1000
|
|
: undefined,
|
|
ForwardedFrom: forwardOrigin?.from,
|
|
ForwardedFromType: forwardOrigin?.fromType,
|
|
ForwardedFromId: forwardOrigin?.fromId,
|
|
ForwardedFromUsername: forwardOrigin?.fromUsername,
|
|
ForwardedFromTitle: forwardOrigin?.fromTitle,
|
|
ForwardedFromSignature: forwardOrigin?.fromSignature,
|
|
ForwardedFromChatType: forwardOrigin?.fromChatType,
|
|
ForwardedFromMessageId: forwardOrigin?.fromMessageId,
|
|
ForwardedDate: forwardOrigin?.date ? forwardOrigin.date * 1000 : undefined,
|
|
Timestamp: msg.date ? msg.date * 1000 : undefined,
|
|
WasMentioned: isGroup ? effectiveWasMentioned : undefined,
|
|
MediaPath: contextMedia.length > 0 ? contextMedia[0]?.path : undefined,
|
|
MediaType: contextMedia.length > 0 ? contextMedia[0]?.contentType : undefined,
|
|
MediaUrl: contextMedia.length > 0 ? contextMedia[0]?.path : undefined,
|
|
MediaPaths: contextMedia.length > 0 ? contextMedia.map((m) => m.path) : undefined,
|
|
MediaUrls: contextMedia.length > 0 ? contextMedia.map((m) => m.path) : undefined,
|
|
MediaTypes:
|
|
contextMedia.length > 0
|
|
? (contextMedia.map((m) => m.contentType).filter(Boolean) as string[])
|
|
: undefined,
|
|
Sticker: allMedia[0]?.stickerMetadata,
|
|
StickerMediaIncluded: allMedia[0]?.stickerMetadata ? !stickerCacheHit : undefined,
|
|
...(locationData ? toLocationContext(locationData) : undefined),
|
|
CommandAuthorized: commandAuthorized,
|
|
MessageThreadId: threadSpec.id,
|
|
IsForum: isForum,
|
|
OriginatingChannel: "telegram" as const,
|
|
OriginatingTo: `telegram:${chatId}`,
|
|
});
|
|
|
|
const pinnedMainDmOwner = !isGroup
|
|
? resolvePinnedMainDmOwnerFromAllowlist({
|
|
dmScope: cfg.session?.dmScope,
|
|
allowFrom: dmAllowFrom,
|
|
normalizeEntry: (entry) => normalizeAllowFrom([entry]).entries[0],
|
|
})
|
|
: null;
|
|
const updateLastRouteSessionKey = resolveInboundLastRouteSessionKey({
|
|
route,
|
|
sessionKey: route.sessionKey,
|
|
});
|
|
|
|
await recordInboundSession({
|
|
storePath,
|
|
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
ctx: ctxPayload,
|
|
updateLastRoute: !isGroup
|
|
? {
|
|
sessionKey: updateLastRouteSessionKey,
|
|
channel: "telegram",
|
|
to: `telegram:${chatId}`,
|
|
accountId: route.accountId,
|
|
threadId: dmThreadId != null ? String(dmThreadId) : undefined,
|
|
mainDmOwnerPin:
|
|
updateLastRouteSessionKey === route.mainSessionKey && pinnedMainDmOwner && senderId
|
|
? {
|
|
ownerRecipient: pinnedMainDmOwner,
|
|
senderRecipient: senderId,
|
|
onSkip: ({ ownerRecipient, senderRecipient }) => {
|
|
logVerbose(
|
|
`telegram: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
|
);
|
|
},
|
|
}
|
|
: undefined,
|
|
}
|
|
: undefined,
|
|
onRecordError: (err) => {
|
|
logVerbose(`telegram: failed updating session meta: ${String(err)}`);
|
|
},
|
|
});
|
|
|
|
if (replyTarget && shouldLogVerbose()) {
|
|
const preview = replyTarget.body.replace(/\s+/g, " ").slice(0, 120);
|
|
logVerbose(
|
|
`telegram reply-context: replyToId=${replyTarget.id} replyToSender=${replyTarget.sender} replyToBody="${preview}"`,
|
|
);
|
|
}
|
|
|
|
if (forwardOrigin && shouldLogVerbose()) {
|
|
logVerbose(
|
|
`telegram forward-context: forwardedFrom="${forwardOrigin.from}" type=${forwardOrigin.fromType}`,
|
|
);
|
|
}
|
|
|
|
if (shouldLogVerbose()) {
|
|
const preview = body.slice(0, 200).replace(/\n/g, "\\n");
|
|
const mediaInfo = allMedia.length > 1 ? ` mediaCount=${allMedia.length}` : "";
|
|
const topicInfo = resolvedThreadId != null ? ` topic=${resolvedThreadId}` : "";
|
|
logVerbose(
|
|
`telegram inbound: chatId=${chatId} from=${ctxPayload.From} len=${body.length}${mediaInfo}${topicInfo} preview="${preview}"`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
ctxPayload,
|
|
skillFilter,
|
|
};
|
|
}
|