fix: honor telegram pairing allowlists for native commands

This commit is contained in:
Peter Steinberger
2026-01-18 22:50:57 +00:00
parent 0d543dd1ff
commit a7be3a9649
5 changed files with 82 additions and 28 deletions

View File

@@ -24,6 +24,17 @@ export const normalizeAllowFrom = (list?: Array<string | number>): NormalizedAll
}; };
}; };
export const normalizeAllowFromWithStore = (params: {
allowFrom?: Array<string | number>;
storeAllowFrom?: string[];
}): NormalizedAllowFrom => {
const combined = [
...(params.allowFrom ?? []),
...(params.storeAllowFrom ?? []),
].map((value) => String(value).trim()).filter(Boolean);
return normalizeAllowFrom(combined);
};
export const firstDefined = <T>(...values: Array<T | undefined>) => { export const firstDefined = <T>(...values: Array<T | undefined>) => {
for (const value of values) { for (const value of values) {
if (typeof value !== "undefined") return value; if (typeof value !== "undefined") return value;

View File

@@ -10,7 +10,7 @@ import { danger, logVerbose, warn } from "../globals.js";
import { resolveMedia } from "./bot/delivery.js"; import { resolveMedia } from "./bot/delivery.js";
import { resolveTelegramForumThreadId } from "./bot/helpers.js"; import { resolveTelegramForumThreadId } from "./bot/helpers.js";
import type { TelegramMessage } from "./bot/types.js"; import type { TelegramMessage } from "./bot/types.js";
import { firstDefined, isSenderAllowed, normalizeAllowFrom } from "./bot-access.js"; import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js"; import { MEDIA_GROUP_TIMEOUT_MS, type MediaGroupEntry } from "./bot-updates.js";
import { migrateTelegramGroupConfig } from "./group-migration.js"; import { migrateTelegramGroupConfig } from "./group-migration.js";
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js"; import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
@@ -205,14 +205,14 @@ export const registerTelegramHandlers = ({
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
const effectiveGroupAllow = normalizeAllowFrom([ const effectiveGroupAllow = normalizeAllowFromWithStore({
...(groupAllowOverride ?? groupAllowFrom ?? []), allowFrom: groupAllowOverride ?? groupAllowFrom,
...storeAllowFrom, storeAllowFrom,
]); });
const effectiveDmAllow = normalizeAllowFrom([ const effectiveDmAllow = normalizeAllowFromWithStore({
...(telegramCfg.allowFrom ?? []), allowFrom: telegramCfg.allowFrom,
...storeAllowFrom, storeAllowFrom,
]); });
const dmPolicy = telegramCfg.dmPolicy ?? "pairing"; const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
const senderId = callback.from?.id ? String(callback.from.id) : ""; const senderId = callback.from?.id ? String(callback.from.id) : "";
const senderUsername = callback.from?.username ?? ""; const senderUsername = callback.from?.username ?? "";
@@ -393,10 +393,10 @@ export const registerTelegramHandlers = ({
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
const effectiveGroupAllow = normalizeAllowFrom([ const effectiveGroupAllow = normalizeAllowFromWithStore({
...(groupAllowOverride ?? groupAllowFrom ?? []), allowFrom: groupAllowOverride ?? groupAllowFrom,
...storeAllowFrom, storeAllowFrom,
]); });
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
if (isGroup) { if (isGroup) {

View File

@@ -42,7 +42,7 @@ import {
import { import {
firstDefined, firstDefined,
isSenderAllowed, isSenderAllowed,
normalizeAllowFrom, normalizeAllowFromWithStore,
resolveSenderAllowMatch, resolveSenderAllowMatch,
} from "./bot-access.js"; } from "./bot-access.js";
import { upsertTelegramPairingRequest } from "./pairing-store.js"; import { upsertTelegramPairingRequest } from "./pairing-store.js";
@@ -138,12 +138,12 @@ export const buildTelegramMessageContext = async ({
}, },
}); });
const mentionRegexes = buildMentionRegexes(cfg, route.agentId); const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
const effectiveDmAllow = normalizeAllowFrom([...(allowFrom ?? []), ...storeAllowFrom]); const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom, storeAllowFrom });
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
const effectiveGroupAllow = normalizeAllowFrom([ const effectiveGroupAllow = normalizeAllowFromWithStore({
...(groupAllowOverride ?? groupAllowFrom ?? []), allowFrom: groupAllowOverride ?? groupAllowFrom,
...storeAllowFrom, storeAllowFrom,
]); });
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
if (isGroup && groupConfig?.enabled === false) { if (isGroup && groupConfig?.enabled === false) {

View File

@@ -34,7 +34,7 @@ import {
buildTelegramGroupPeerId, buildTelegramGroupPeerId,
resolveTelegramForumThreadId, resolveTelegramForumThreadId,
} from "./bot/helpers.js"; } from "./bot/helpers.js";
import { firstDefined, isSenderAllowed, normalizeAllowFrom } from "./bot-access.js"; import { firstDefined, isSenderAllowed, normalizeAllowFromWithStore } from "./bot-access.js";
import { readTelegramAllowFromStore } from "./pairing-store.js"; import { readTelegramAllowFromStore } from "./pairing-store.js";
type TelegramNativeCommandContext = Context & { match?: string }; type TelegramNativeCommandContext = Context & { match?: string };
@@ -132,10 +132,10 @@ export const registerTelegramNativeCommands = ({
const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []);
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
const effectiveGroupAllow = normalizeAllowFrom([ const effectiveGroupAllow = normalizeAllowFromWithStore({
...(groupAllowOverride ?? groupAllowFrom ?? []), allowFrom: groupAllowOverride ?? groupAllowFrom,
...storeAllowFrom, storeAllowFrom,
]); });
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
if (isGroup && groupConfig?.enabled === false) { if (isGroup && groupConfig?.enabled === false) {
@@ -194,12 +194,12 @@ export const registerTelegramNativeCommands = ({
} }
} }
const allowFromList = Array.isArray(allowFrom)
? allowFrom.map((entry) => String(entry).trim()).filter(Boolean)
: [];
const senderId = msg.from?.id ? String(msg.from.id) : ""; const senderId = msg.from?.id ? String(msg.from.id) : "";
const senderUsername = msg.from?.username ?? ""; const senderUsername = msg.from?.username ?? "";
const dmAllow = normalizeAllowFrom(allowFromList); const dmAllow = normalizeAllowFromWithStore({
allowFrom: allowFrom,
storeAllowFrom,
});
const senderAllowed = isSenderAllowed({ const senderAllowed = isSenderAllowed({
allow: dmAllow, allow: dmAllow,
senderId, senderId,

View File

@@ -2138,6 +2138,49 @@ describe("createTelegramBot", () => {
); );
}); });
it("allows native DM commands for paired users", async () => {
onSpy.mockReset();
sendMessageSpy.mockReset();
commandSpy.mockReset();
const replySpy = replyModule.__replySpy as unknown as ReturnType<typeof vi.fn>;
replySpy.mockReset();
replySpy.mockResolvedValue({ text: "response" });
loadConfig.mockReturnValue({
commands: { native: true },
channels: {
telegram: {
dmPolicy: "pairing",
},
},
});
readTelegramAllowFromStore.mockResolvedValueOnce(["12345"]);
createTelegramBot({ token: "tok" });
const handler = commandSpy.mock.calls.find((call) => call[0] === "status")?.[1] as
| ((ctx: Record<string, unknown>) => Promise<void>)
| undefined;
if (!handler) throw new Error("status command handler missing");
await handler({
message: {
chat: { id: 12345, type: "private" },
from: { id: 12345, username: "testuser" },
text: "/status",
date: 1736380800,
message_id: 42,
},
match: "",
});
expect(replySpy).toHaveBeenCalledTimes(1);
expect(
sendMessageSpy.mock.calls.some(
(call) => call[1] === "You are not authorized to use this command.",
),
).toBe(false);
});
it("streams tool summaries for native slash commands", async () => { it("streams tool summaries for native slash commands", async () => {
onSpy.mockReset(); onSpy.mockReset();
sendMessageSpy.mockReset(); sendMessageSpy.mockReset();