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"; import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
describe("buildTelegramMessageContext dm thread sessions", () => { describe("buildTelegramMessageContext dm thread sessions", () => {
@@ -104,3 +105,45 @@ describe("buildTelegramMessageContext group sessions without forum", () => {
expect(ctx?.ctxPayload?.MessageThreadId).toBe(99); 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, buildGroupLabel,
buildSenderLabel, buildSenderLabel,
buildSenderName, buildSenderName,
resolveTelegramDirectPeerId,
buildTelegramGroupFrom, buildTelegramGroupFrom,
buildTelegramGroupPeerId, buildTelegramGroupPeerId,
buildTelegramParentPeer, buildTelegramParentPeer,
@@ -174,6 +175,7 @@ export const buildTelegramMessageContext = async ({
const msg = primaryCtx.message; const msg = primaryCtx.message;
const chatId = msg.chat.id; const chatId = msg.chat.id;
const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup"; 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 messageThreadId = (msg as { message_thread_id?: number }).message_thread_id;
const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true; const isForum = (msg.chat as { is_forum?: boolean }).is_forum === true;
const threadSpec = resolveTelegramThreadSpec({ const threadSpec = resolveTelegramThreadSpec({
@@ -191,7 +193,9 @@ export const buildTelegramMessageContext = async ({
!isGroup && groupConfig && "dmPolicy" in groupConfig !isGroup && groupConfig && "dmPolicy" in groupConfig
? (groupConfig.dmPolicy ?? dmPolicy) ? (groupConfig.dmPolicy ?? 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 }); const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
// Fresh config for bindings lookup; other routing inputs are payload-derived. // Fresh config for bindings lookup; other routing inputs are payload-derived.
const route = resolveAgentRoute({ const route = resolveAgentRoute({
@@ -235,7 +239,6 @@ export const buildTelegramMessageContext = async ({
// Group sender checks are explicit and must not inherit DM pairing-store entries. // Group sender checks are explicit and must not inherit DM pairing-store entries.
const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom); const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom);
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
const senderId = msg.from?.id ? String(msg.from.id) : "";
const senderUsername = msg.from?.username ?? ""; const senderUsername = msg.from?.username ?? "";
const baseAccess = evaluateTelegramGroupBaseAccess({ const baseAccess = evaluateTelegramGroupBaseAccess({
isGroup, isGroup,

View File

@@ -5,6 +5,7 @@ import {
describeReplyTarget, describeReplyTarget,
expandTextLinks, expandTextLinks,
normalizeForwardedContext, normalizeForwardedContext,
resolveTelegramDirectPeerId,
resolveTelegramForumThreadId, resolveTelegramForumThreadId,
} from "./helpers.js"; } 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", () => { describe("thread id normalization", () => {
it.each([ it.each([
{ {

View File

@@ -175,6 +175,24 @@ export function buildTelegramGroupPeerId(chatId: number | string, messageThreadI
return messageThreadId != null ? `${chatId}:topic:${messageThreadId}` : String(chatId); 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) { export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?: number) {
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`; return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
} }