telegram: route dm sessions by sender id

This commit is contained in:
bmendonca3
2026-03-02 14:56:26 -07:00
committed by Peter Steinberger
parent 2c39731846
commit 317075ef3d
4 changed files with 82 additions and 3 deletions

View File

@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { afterEach, describe, expect, it } from "vitest";
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
describe("buildTelegramMessageContext dm thread sessions", () => {
@@ -104,3 +105,45 @@ describe("buildTelegramMessageContext group sessions without forum", () => {
expect(ctx?.ctxPayload?.MessageThreadId).toBe(99);
});
});
describe("buildTelegramMessageContext direct peer routing", () => {
afterEach(() => {
clearRuntimeConfigSnapshot();
});
it("isolates dm sessions by sender id when chat id differs", async () => {
const runtimeCfg = {
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
channels: { telegram: {} },
messages: { groupChat: { mentionPatterns: [] } },
session: { dmScope: "per-channel-peer" as const },
};
setRuntimeConfigSnapshot(runtimeCfg);
const baseMessage = {
chat: { id: 777777777, type: "private" as const },
date: 1700000000,
text: "hello",
};
const first = await buildTelegramMessageContextForTest({
cfg: runtimeCfg,
message: {
...baseMessage,
message_id: 1,
from: { id: 123456789, first_name: "Alice" },
},
});
const second = await buildTelegramMessageContextForTest({
cfg: runtimeCfg,
message: {
...baseMessage,
message_id: 2,
from: { id: 987654321, first_name: "Bob" },
},
});
expect(first?.ctxPayload?.SessionKey).toBe("agent:main:telegram:direct:123456789");
expect(second?.ctxPayload?.SessionKey).toBe("agent:main:telegram:direct:987654321");
});
});

View File

@@ -52,6 +52,7 @@ import {
buildGroupLabel,
buildSenderLabel,
buildSenderName,
resolveTelegramDirectPeerId,
buildTelegramGroupFrom,
buildTelegramGroupPeerId,
buildTelegramParentPeer,
@@ -174,6 +175,7 @@ export const buildTelegramMessageContext = async ({
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;
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
const threadSpec = resolveTelegramThreadSpec({
@@ -191,7 +193,9 @@ export const buildTelegramMessageContext = async ({
!isGroup && groupConfig && "dmPolicy" in groupConfig
? (groupConfig.dmPolicy ?? dmPolicy)
: dmPolicy;
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
const peerId = isGroup
? buildTelegramGroupPeerId(chatId, resolvedThreadId)
: resolveTelegramDirectPeerId({ chatId, senderId });
const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
// Fresh config for bindings lookup; other routing inputs are payload-derived.
const route = resolveAgentRoute({
@@ -235,7 +239,6 @@ export const buildTelegramMessageContext = async ({
// Group sender checks are explicit and must not inherit DM pairing-store entries.
const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom);
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
const senderId = msg.from?.id ? String(msg.from.id) : "";
const senderUsername = msg.from?.username ?? "";
const baseAccess = evaluateTelegramGroupBaseAccess({
isGroup,

View File

@@ -5,6 +5,7 @@ import {
describeReplyTarget,
expandTextLinks,
normalizeForwardedContext,
resolveTelegramDirectPeerId,
resolveTelegramForumThreadId,
} from "./helpers.js";
@@ -53,6 +54,20 @@ describe("buildTypingThreadParams", () => {
});
});
describe("resolveTelegramDirectPeerId", () => {
it("prefers sender id when available", () => {
expect(resolveTelegramDirectPeerId({ chatId: 777777777, senderId: 123456789 })).toBe(
"123456789",
);
});
it("falls back to chat id when sender id is missing", () => {
expect(resolveTelegramDirectPeerId({ chatId: 777777777, senderId: undefined })).toBe(
"777777777",
);
});
});
describe("thread id normalization", () => {
it.each([
{

View File

@@ -175,6 +175,24 @@ export function buildTelegramGroupPeerId(chatId: number | string, messageThreadI
return messageThreadId != null ? `${chatId}:topic:${messageThreadId}` : String(chatId);
}
/**
* Resolve the direct-message peer identifier for Telegram routing/session keys.
*
* In some Telegram DM deliveries (for example certain business/chat bridge flows),
* `chat.id` can differ from the actual sender user id. Prefer sender id when present
* so per-peer DM scopes isolate users correctly.
*/
export function resolveTelegramDirectPeerId(params: {
chatId: number | string;
senderId?: number | string | null;
}) {
const senderId = params.senderId != null ? String(params.senderId).trim() : "";
if (senderId) {
return senderId;
}
return String(params.chatId);
}
export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?: number) {
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
}