diff --git a/src/telegram/bot-access.ts b/src/telegram/bot-access.ts index d135a6479..1ac35b064 100644 --- a/src/telegram/bot-access.ts +++ b/src/telegram/bot-access.ts @@ -24,6 +24,17 @@ export const normalizeAllowFrom = (list?: Array): NormalizedAll }; }; +export const normalizeAllowFromWithStore = (params: { + allowFrom?: Array; + storeAllowFrom?: string[]; +}): NormalizedAllowFrom => { + const combined = [ + ...(params.allowFrom ?? []), + ...(params.storeAllowFrom ?? []), + ].map((value) => String(value).trim()).filter(Boolean); + return normalizeAllowFrom(combined); +}; + export const firstDefined = (...values: Array) => { for (const value of values) { if (typeof value !== "undefined") return value; diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index 0bf5e7c28..a17a65b3e 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -10,7 +10,7 @@ import { danger, logVerbose, warn } from "../globals.js"; import { resolveMedia } from "./bot/delivery.js"; import { resolveTelegramForumThreadId } from "./bot/helpers.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 { migrateTelegramGroupConfig } from "./group-migration.js"; import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js"; @@ -205,14 +205,14 @@ export const registerTelegramHandlers = ({ const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); - const effectiveGroupAllow = normalizeAllowFrom([ - ...(groupAllowOverride ?? groupAllowFrom ?? []), - ...storeAllowFrom, - ]); - const effectiveDmAllow = normalizeAllowFrom([ - ...(telegramCfg.allowFrom ?? []), - ...storeAllowFrom, - ]); + const effectiveGroupAllow = normalizeAllowFromWithStore({ + allowFrom: groupAllowOverride ?? groupAllowFrom, + storeAllowFrom, + }); + const effectiveDmAllow = normalizeAllowFromWithStore({ + allowFrom: telegramCfg.allowFrom, + storeAllowFrom, + }); const dmPolicy = telegramCfg.dmPolicy ?? "pairing"; const senderId = callback.from?.id ? String(callback.from.id) : ""; const senderUsername = callback.from?.username ?? ""; @@ -393,10 +393,10 @@ export const registerTelegramHandlers = ({ const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); - const effectiveGroupAllow = normalizeAllowFrom([ - ...(groupAllowOverride ?? groupAllowFrom ?? []), - ...storeAllowFrom, - ]); + const effectiveGroupAllow = normalizeAllowFromWithStore({ + allowFrom: groupAllowOverride ?? groupAllowFrom, + storeAllowFrom, + }); const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; if (isGroup) { diff --git a/src/telegram/bot-message-context.ts b/src/telegram/bot-message-context.ts index 8c01b0d5f..60512040f 100644 --- a/src/telegram/bot-message-context.ts +++ b/src/telegram/bot-message-context.ts @@ -42,7 +42,7 @@ import { import { firstDefined, isSenderAllowed, - normalizeAllowFrom, + normalizeAllowFromWithStore, resolveSenderAllowMatch, } from "./bot-access.js"; import { upsertTelegramPairingRequest } from "./pairing-store.js"; @@ -138,12 +138,12 @@ export const buildTelegramMessageContext = async ({ }, }); const mentionRegexes = buildMentionRegexes(cfg, route.agentId); - const effectiveDmAllow = normalizeAllowFrom([...(allowFrom ?? []), ...storeAllowFrom]); + const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom, storeAllowFrom }); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); - const effectiveGroupAllow = normalizeAllowFrom([ - ...(groupAllowOverride ?? groupAllowFrom ?? []), - ...storeAllowFrom, - ]); + const effectiveGroupAllow = normalizeAllowFromWithStore({ + allowFrom: groupAllowOverride ?? groupAllowFrom, + storeAllowFrom, + }); const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; if (isGroup && groupConfig?.enabled === false) { diff --git a/src/telegram/bot-native-commands.ts b/src/telegram/bot-native-commands.ts index 06c84efe9..11e83dcc3 100644 --- a/src/telegram/bot-native-commands.ts +++ b/src/telegram/bot-native-commands.ts @@ -34,7 +34,7 @@ import { buildTelegramGroupPeerId, resolveTelegramForumThreadId, } 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"; type TelegramNativeCommandContext = Context & { match?: string }; @@ -132,10 +132,10 @@ export const registerTelegramNativeCommands = ({ const storeAllowFrom = await readTelegramAllowFromStore().catch(() => []); const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId); const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom); - const effectiveGroupAllow = normalizeAllowFrom([ - ...(groupAllowOverride ?? groupAllowFrom ?? []), - ...storeAllowFrom, - ]); + const effectiveGroupAllow = normalizeAllowFromWithStore({ + allowFrom: groupAllowOverride ?? groupAllowFrom, + storeAllowFrom, + }); const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined"; 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 senderUsername = msg.from?.username ?? ""; - const dmAllow = normalizeAllowFrom(allowFromList); + const dmAllow = normalizeAllowFromWithStore({ + allowFrom: allowFrom, + storeAllowFrom, + }); const senderAllowed = isSenderAllowed({ allow: dmAllow, senderId, diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index d7b37daa3..4166ddb19 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -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; + 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) => Promise) + | 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 () => { onSpy.mockReset(); sendMessageSpy.mockReset();