fix(telegram): scope DM topic thread keys by chat id (#31064)
* fix(telegram): scope DM topic thread keys by chat id * test(telegram): update dm topic session-key expectation * fix(telegram): parse scoped dm thread ids in outbound recovery * chore(telegram): format accounts config merge block * test(nodes): simplify mocked exports for ts tuple spreads
This commit is contained in:
@@ -25,20 +25,20 @@ const screenMocks = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
vi.mock("./gateway.js", () => ({
|
||||
callGatewayTool: (...args: unknown[]) => gatewayMocks.callGatewayTool(...args),
|
||||
readGatewayCallOptions: (...args: unknown[]) => gatewayMocks.readGatewayCallOptions(...args),
|
||||
callGatewayTool: gatewayMocks.callGatewayTool,
|
||||
readGatewayCallOptions: gatewayMocks.readGatewayCallOptions,
|
||||
}));
|
||||
|
||||
vi.mock("./nodes-utils.js", () => ({
|
||||
resolveNodeId: (...args: unknown[]) => nodeUtilsMocks.resolveNodeId(...args),
|
||||
listNodes: (...args: unknown[]) => nodeUtilsMocks.listNodes(...args),
|
||||
resolveNodeIdFromList: (...args: unknown[]) => nodeUtilsMocks.resolveNodeIdFromList(...args),
|
||||
resolveNodeId: nodeUtilsMocks.resolveNodeId,
|
||||
listNodes: nodeUtilsMocks.listNodes,
|
||||
resolveNodeIdFromList: nodeUtilsMocks.resolveNodeIdFromList,
|
||||
}));
|
||||
|
||||
vi.mock("../../cli/nodes-screen.js", () => ({
|
||||
parseScreenRecordPayload: (...args: unknown[]) => screenMocks.parseScreenRecordPayload(...args),
|
||||
screenRecordTempPath: (...args: unknown[]) => screenMocks.screenRecordTempPath(...args),
|
||||
writeScreenRecordToFile: (...args: unknown[]) => screenMocks.writeScreenRecordToFile(...args),
|
||||
parseScreenRecordPayload: screenMocks.parseScreenRecordPayload,
|
||||
screenRecordTempPath: screenMocks.screenRecordTempPath,
|
||||
writeScreenRecordToFile: screenMocks.writeScreenRecordToFile,
|
||||
}));
|
||||
|
||||
import { createNodesTool } from "./nodes-tool.js";
|
||||
|
||||
@@ -32,6 +32,32 @@ describe("telegramOutbound", () => {
|
||||
expect(result).toEqual({ channel: "telegram", messageId: "tg-text-1", chatId: "123" });
|
||||
});
|
||||
|
||||
it("parses scoped DM thread ids for sendText", async () => {
|
||||
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "tg-text-2", chatId: "12345" });
|
||||
const sendText = telegramOutbound.sendText;
|
||||
expect(sendText).toBeDefined();
|
||||
|
||||
await sendText!({
|
||||
cfg: {},
|
||||
to: "12345",
|
||||
text: "<b>hello</b>",
|
||||
accountId: "work",
|
||||
threadId: "12345:99",
|
||||
deps: { sendTelegram },
|
||||
});
|
||||
|
||||
expect(sendTelegram).toHaveBeenCalledWith(
|
||||
"12345",
|
||||
"<b>hello</b>",
|
||||
expect.objectContaining({
|
||||
textMode: "html",
|
||||
verbose: false,
|
||||
accountId: "work",
|
||||
messageThreadId: 99,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes media options for sendMedia", async () => {
|
||||
const sendTelegram = vi.fn().mockResolvedValue({ messageId: "tg-media-1", chatId: "123" });
|
||||
const sendMedia = telegramOutbound.sendMedia;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { normalizeAllowListLower } from "../../slack/monitor/allow-list.js";
|
||||
import { parseSlackTarget } from "../../slack/targets.js";
|
||||
import { buildTelegramGroupPeerId } from "../../telegram/bot/helpers.js";
|
||||
import { resolveTelegramTargetChatType } from "../../telegram/inline-buttons.js";
|
||||
import { parseTelegramThreadId } from "../../telegram/outbound-params.js";
|
||||
import { parseTelegramTarget } from "../../telegram/targets.js";
|
||||
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
|
||||
import type { ResolvedMessagingTarget } from "./target-resolver.js";
|
||||
@@ -283,8 +284,7 @@ function resolveTelegramSession(
|
||||
}
|
||||
const parsedThreadId = parsed.messageThreadId;
|
||||
const fallbackThreadId = normalizeThreadId(params.threadId);
|
||||
const resolvedThreadId =
|
||||
parsedThreadId ?? (fallbackThreadId ? Number.parseInt(fallbackThreadId, 10) : undefined);
|
||||
const resolvedThreadId = parsedThreadId ?? parseTelegramThreadId(fallbackThreadId);
|
||||
// Telegram topics are encoded in the peer id (chatId:topic:<id>).
|
||||
const chatType = resolveTelegramTargetChatType(params.target);
|
||||
// If the target is a username and we lack a resolvedTarget, default to DM to avoid group keys.
|
||||
|
||||
@@ -891,6 +891,7 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
channel: string;
|
||||
target: string;
|
||||
replyToId?: string;
|
||||
threadId?: string;
|
||||
expected: {
|
||||
sessionKey: string;
|
||||
from?: string;
|
||||
@@ -934,6 +935,20 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
chatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Telegram DM scoped threadId fallback",
|
||||
cfg: perChannelPeerCfg,
|
||||
channel: "telegram",
|
||||
target: "12345",
|
||||
threadId: "12345:99",
|
||||
expected: {
|
||||
sessionKey: "agent:main:telegram:direct:12345",
|
||||
from: "telegram:12345",
|
||||
to: "telegram:12345",
|
||||
threadId: 99,
|
||||
chatType: "direct",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "identity-links per-peer",
|
||||
cfg: identityLinksCfg,
|
||||
@@ -1018,6 +1033,7 @@ describe("resolveOutboundSessionRoute", () => {
|
||||
agentId: "main",
|
||||
target: testCase.target,
|
||||
replyToId: testCase.replyToId,
|
||||
threadId: testCase.threadId,
|
||||
});
|
||||
expect(route?.sessionKey, testCase.name).toBe(testCase.expected.sessionKey);
|
||||
if (testCase.expected.from !== undefined) {
|
||||
|
||||
@@ -84,8 +84,11 @@ function resolveAccountConfig(
|
||||
}
|
||||
|
||||
function mergeTelegramAccountConfig(cfg: OpenClawConfig, accountId: string): TelegramAccountConfig {
|
||||
const { accounts: _ignored, groups: channelGroups, ...base } = (cfg.channels?.telegram ??
|
||||
{}) as TelegramAccountConfig & { accounts?: unknown };
|
||||
const {
|
||||
accounts: _ignored,
|
||||
groups: channelGroups,
|
||||
...base
|
||||
} = (cfg.channels?.telegram ?? {}) as TelegramAccountConfig & { accounts?: unknown };
|
||||
const account = resolveAccountConfig(cfg, accountId) ?? {};
|
||||
|
||||
// In multi-account setups, channel-level `groups` must NOT be inherited by
|
||||
|
||||
@@ -290,7 +290,7 @@ export const registerTelegramHandlers = ({
|
||||
const dmThreadId = !params.isGroup ? params.messageThreadId : undefined;
|
||||
const threadKeys =
|
||||
dmThreadId != null
|
||||
? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
|
||||
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${params.chatId}:${dmThreadId}` })
|
||||
: null;
|
||||
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
|
||||
const storePath = resolveStorePath(cfg.session?.store, { agentId: route.agentId });
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("buildTelegramMessageContext dm thread sessions", () => {
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx?.ctxPayload?.MessageThreadId).toBe(42);
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:42");
|
||||
expect(ctx?.ctxPayload?.SessionKey).toBe("agent:main:main:thread:1234:42");
|
||||
});
|
||||
|
||||
it("keeps legacy dm session key when no thread id", async () => {
|
||||
|
||||
@@ -204,7 +204,7 @@ export const buildTelegramMessageContext = async ({
|
||||
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
|
||||
const threadKeys =
|
||||
dmThreadId != null
|
||||
? resolveThreadSessionKeys({ baseSessionKey, threadId: String(dmThreadId) })
|
||||
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${chatId}:${dmThreadId}` })
|
||||
: null;
|
||||
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
|
||||
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
|
||||
|
||||
@@ -551,7 +551,7 @@ export const registerTelegramNativeCommands = ({
|
||||
dmThreadId != null
|
||||
? resolveThreadSessionKeys({
|
||||
baseSessionKey,
|
||||
threadId: String(dmThreadId),
|
||||
threadId: `${chatId}:${dmThreadId}`,
|
||||
})
|
||||
: null;
|
||||
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
|
||||
|
||||
@@ -928,7 +928,7 @@ describe("createTelegramBot", () => {
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:99");
|
||||
expect(payload.CommandTargetSessionKey).toBe("agent:main:main:thread:12345:99");
|
||||
});
|
||||
|
||||
it("allows native DM commands for paired users", async () => {
|
||||
|
||||
@@ -6,6 +6,14 @@ export function parseTelegramReplyToMessageId(replyToId?: string | null): number
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function parseIntegerId(value: string): number | undefined {
|
||||
if (!/^-?\d+$/.test(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
export function parseTelegramThreadId(threadId?: string | number | null): number | undefined {
|
||||
if (threadId == null) {
|
||||
return undefined;
|
||||
@@ -17,6 +25,8 @@ export function parseTelegramThreadId(threadId?: string | number | null): number
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number.parseInt(trimmed, 10);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
// DM topic session keys may scope thread ids as "<chatId>:<threadId>".
|
||||
const scopedMatch = /^-?\d+:(-?\d+)$/.exec(trimmed);
|
||||
const rawThreadId = scopedMatch ? scopedMatch[1] : trimmed;
|
||||
return parseIntegerId(rawThreadId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user