From 046feb6b0eee7ec4984a9840ae3c7da982f4c081 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 01:13:57 +0100 Subject: [PATCH] refactor: simplify telegram event authorization flow --- src/telegram/bot-handlers.ts | 153 ++++++++++++++++++++--------------- src/telegram/bot.test.ts | 146 +++++++++++++-------------------- 2 files changed, 143 insertions(+), 156 deletions(-) diff --git a/src/telegram/bot-handlers.ts b/src/telegram/bot-handlers.ts index e7660717293..a3b4d46a677 100644 --- a/src/telegram/bot-handlers.ts +++ b/src/telegram/bot-handlers.ts @@ -17,6 +17,7 @@ import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js import { loadConfig } from "../config/config.js"; import { writeConfigFile } from "../config/io.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; +import type { DmPolicy } from "../config/types.base.js"; import type { TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js"; import { danger, logVerbose, warn } from "../globals.js"; import { enqueueSystemEvent } from "../infra/system-events.js"; @@ -507,54 +508,87 @@ export const registerTelegramHandlers = ({ return false; }; - const isTelegramEventSenderAuthorized = async (params: { + type TelegramGroupAllowContext = Awaited>; + type TelegramEventAuthorizationMode = "reaction" | "callback-scope" | "callback-allowlist"; + type TelegramEventAuthorizationResult = { allowed: true } | { allowed: false; reason: string }; + type TelegramEventAuthorizationContext = TelegramGroupAllowContext & { dmPolicy: DmPolicy }; + + const TELEGRAM_EVENT_AUTH_RULES: Record< + TelegramEventAuthorizationMode, + { + enforceDirectAuthorization: boolean; + enforceGroupAllowlistAuthorization: boolean; + deniedDmReason: string; + deniedGroupReason: string; + } + > = { + reaction: { + enforceDirectAuthorization: true, + enforceGroupAllowlistAuthorization: false, + deniedDmReason: "reaction unauthorized by dm policy/allowlist", + deniedGroupReason: "reaction unauthorized by group allowlist", + }, + "callback-scope": { + enforceDirectAuthorization: false, + enforceGroupAllowlistAuthorization: false, + deniedDmReason: "callback unauthorized by inlineButtonsScope", + deniedGroupReason: "callback unauthorized by inlineButtonsScope", + }, + "callback-allowlist": { + enforceDirectAuthorization: true, + enforceGroupAllowlistAuthorization: true, + deniedDmReason: "callback unauthorized by inlineButtonsScope allowlist", + deniedGroupReason: "callback unauthorized by inlineButtonsScope allowlist", + }, + }; + + const resolveTelegramEventAuthorizationContext = async (params: { chatId: number; - chatTitle?: string; - isGroup: boolean; isForum: boolean; messageThreadId?: number; - senderId: string; - senderUsername: string; - enforceDirectAuthorization: boolean; - enforceGroupAllowlistAuthorization: boolean; - deniedDmReason: string; - deniedGroupReason: string; - groupAllowContext?: Awaited>; - }) => { - const { - chatId, - chatTitle, - isGroup, - isForum, - messageThreadId, - senderId, - senderUsername, - enforceDirectAuthorization, - enforceGroupAllowlistAuthorization, - deniedDmReason, - deniedGroupReason, - groupAllowContext: preResolvedGroupAllowContext, - } = params; + groupAllowContext?: TelegramGroupAllowContext; + }): Promise => { const dmPolicy = telegramCfg.dmPolicy ?? "pairing"; const groupAllowContext = - preResolvedGroupAllowContext ?? + params.groupAllowContext ?? (await resolveTelegramGroupAllowFromContext({ - chatId, + chatId: params.chatId, accountId, dmPolicy, - isForum, - messageThreadId, + isForum: params.isForum, + messageThreadId: params.messageThreadId, groupAllowFrom, resolveTelegramGroupConfig, })); + return { dmPolicy, ...groupAllowContext }; + }; + + const authorizeTelegramEventSender = (params: { + chatId: number; + chatTitle?: string; + isGroup: boolean; + senderId: string; + senderUsername: string; + mode: TelegramEventAuthorizationMode; + context: TelegramEventAuthorizationContext; + }): TelegramEventAuthorizationResult => { + const { chatId, chatTitle, isGroup, senderId, senderUsername, mode, context } = params; const { + dmPolicy, resolvedThreadId, storeAllowFrom, groupConfig, topicConfig, effectiveGroupAllow, hasGroupAllowOverride, - } = groupAllowContext; + } = context; + const authRules = TELEGRAM_EVENT_AUTH_RULES[mode]; + const { + enforceDirectAuthorization, + enforceGroupAllowlistAuthorization, + deniedDmReason, + deniedGroupReason, + } = authRules; if ( shouldSkipGroupMessage({ isGroup, @@ -569,7 +603,7 @@ export const registerTelegramHandlers = ({ topicConfig, }) ) { - return false; + return { allowed: false, reason: "group-policy" }; } if (!isGroup && enforceDirectAuthorization) { @@ -577,7 +611,7 @@ export const registerTelegramHandlers = ({ logVerbose( `Blocked telegram direct event from ${senderId || "unknown"} (${deniedDmReason})`, ); - return false; + return { allowed: false, reason: "direct-disabled" }; } if (dmPolicy !== "open") { const effectiveDmAllow = normalizeAllowFromWithStore({ @@ -587,17 +621,17 @@ export const registerTelegramHandlers = ({ }); if (!isAllowlistAuthorized(effectiveDmAllow, senderId, senderUsername)) { logVerbose(`Blocked telegram direct sender ${senderId || "unknown"} (${deniedDmReason})`); - return false; + return { allowed: false, reason: "direct-unauthorized" }; } } } if (isGroup && enforceGroupAllowlistAuthorization) { if (!isAllowlistAuthorized(effectiveGroupAllow, senderId, senderUsername)) { logVerbose(`Blocked telegram group sender ${senderId || "unknown"} (${deniedGroupReason})`); - return false; + return { allowed: false, reason: "group-unauthorized" }; } } - return true; + return { allowed: true }; }; // Handle emoji reactions to messages. @@ -630,19 +664,20 @@ export const registerTelegramHandlers = ({ if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) { return; } - const senderAuthorized = await isTelegramEventSenderAuthorized({ + const eventAuthContext = await resolveTelegramEventAuthorizationContext({ + chatId, + isForum, + }); + const senderAuthorization = authorizeTelegramEventSender({ chatId, chatTitle: reaction.chat.title, isGroup, - isForum, senderId, senderUsername, - enforceDirectAuthorization: true, - enforceGroupAllowlistAuthorization: false, - deniedDmReason: "reaction unauthorized by dm policy/allowlist", - deniedGroupReason: "reaction unauthorized by group allowlist", + mode: "reaction", + context: eventAuthContext, }); - if (!senderAuthorized) { + if (!senderAuthorization.allowed) { return; } @@ -965,33 +1000,26 @@ export const registerTelegramHandlers = ({ const messageThreadId = callbackMessage.message_thread_id; const isForum = callbackMessage.chat.is_forum === true; - const groupAllowContext = await resolveTelegramGroupAllowFromContext({ + const eventAuthContext = await resolveTelegramEventAuthorizationContext({ chatId, - accountId, - dmPolicy: telegramCfg.dmPolicy ?? "pairing", isForum, messageThreadId, - groupAllowFrom, - resolveTelegramGroupConfig, }); - const { resolvedThreadId, storeAllowFrom } = groupAllowContext; + const { resolvedThreadId, storeAllowFrom } = eventAuthContext; const senderId = callback.from?.id ? String(callback.from.id) : ""; const senderUsername = callback.from?.username ?? ""; - const senderAuthorized = await isTelegramEventSenderAuthorized({ + const authorizationMode: TelegramEventAuthorizationMode = + inlineButtonsScope === "allowlist" ? "callback-allowlist" : "callback-scope"; + const senderAuthorization = authorizeTelegramEventSender({ chatId, chatTitle: callbackMessage.chat.title, isGroup, - isForum, - messageThreadId, senderId, senderUsername, - enforceDirectAuthorization: inlineButtonsScope === "allowlist", - enforceGroupAllowlistAuthorization: inlineButtonsScope === "allowlist", - deniedDmReason: "callback unauthorized by inlineButtonsScope allowlist", - deniedGroupReason: "callback unauthorized by inlineButtonsScope allowlist", - groupAllowContext, + mode: authorizationMode, + context: eventAuthContext, }); - if (!senderAuthorized) { + if (!senderAuthorization.allowed) { return; } @@ -1230,25 +1258,20 @@ export const registerTelegramHandlers = ({ if (shouldSkipUpdate(event.ctxForDedupe)) { return; } - const dmPolicy = telegramCfg.dmPolicy ?? "pairing"; - - const groupAllowContext = await resolveTelegramGroupAllowFromContext({ + const eventAuthContext = await resolveTelegramEventAuthorizationContext({ chatId: event.chatId, - accountId, - dmPolicy, isForum: event.isForum, messageThreadId: event.messageThreadId, - groupAllowFrom, - resolveTelegramGroupConfig, }); const { + dmPolicy, resolvedThreadId, storeAllowFrom, groupConfig, topicConfig, effectiveGroupAllow, hasGroupAllowOverride, - } = groupAllowContext; + } = eventAuthContext; const effectiveDmAllow = normalizeAllowFromWithStore({ allowFrom, storeAllowFrom, diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index 4a605abb170..e7e326d0e36 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -832,24 +832,12 @@ describe("createTelegramBot", () => { ); }); - it("blocks reaction when dmPolicy is disabled", async () => { - onSpy.mockClear(); - enqueueSystemEventSpy.mockClear(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "disabled", reactionNotifications: "all" }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message_reaction") as ( - ctx: Record, - ) => Promise; - - await handler({ - update: { update_id: 510 }, - messageReaction: { + it.each([ + { + name: "blocks reaction when dmPolicy is disabled", + updateId: 510, + channelConfig: { dmPolicy: "disabled", reactionNotifications: "all" }, + reaction: { chat: { id: 1234, type: "private" }, message_id: 42, user: { id: 9, first_name: "Ada" }, @@ -857,29 +845,17 @@ describe("createTelegramBot", () => { old_reaction: [], new_reaction: [{ type: "emoji", emoji: "👍" }], }, - }); - - expect(enqueueSystemEventSpy).not.toHaveBeenCalled(); - }); - - it("blocks reaction in allowlist mode for unauthorized direct sender", async () => { - onSpy.mockClear(); - enqueueSystemEventSpy.mockClear(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "allowlist", allowFrom: ["12345"], reactionNotifications: "all" }, + expectedEnqueueCalls: 0, + }, + { + name: "blocks reaction in allowlist mode for unauthorized direct sender", + updateId: 511, + channelConfig: { + dmPolicy: "allowlist", + allowFrom: ["12345"], + reactionNotifications: "all", }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message_reaction") as ( - ctx: Record, - ) => Promise; - - await handler({ - update: { update_id: 511 }, - messageReaction: { + reaction: { chat: { id: 1234, type: "private" }, message_id: 42, user: { id: 9, first_name: "Ada" }, @@ -887,29 +863,13 @@ describe("createTelegramBot", () => { old_reaction: [], new_reaction: [{ type: "emoji", emoji: "👍" }], }, - }); - - expect(enqueueSystemEventSpy).not.toHaveBeenCalled(); - }); - - it("allows reaction in allowlist mode for authorized direct sender", async () => { - onSpy.mockClear(); - enqueueSystemEventSpy.mockClear(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { dmPolicy: "allowlist", allowFrom: ["9"], reactionNotifications: "all" }, - }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message_reaction") as ( - ctx: Record, - ) => Promise; - - await handler({ - update: { update_id: 512 }, - messageReaction: { + expectedEnqueueCalls: 0, + }, + { + name: "allows reaction in allowlist mode for authorized direct sender", + updateId: 512, + channelConfig: { dmPolicy: "allowlist", allowFrom: ["9"], reactionNotifications: "all" }, + reaction: { chat: { id: 1234, type: "private" }, message_id: 42, user: { id: 9, first_name: "Ada" }, @@ -917,34 +877,18 @@ describe("createTelegramBot", () => { old_reaction: [], new_reaction: [{ type: "emoji", emoji: "👍" }], }, - }); - - expect(enqueueSystemEventSpy).toHaveBeenCalledTimes(1); - }); - - it("blocks reaction in group allowlist mode for unauthorized sender", async () => { - onSpy.mockClear(); - enqueueSystemEventSpy.mockClear(); - - loadConfig.mockReturnValue({ - channels: { - telegram: { - dmPolicy: "open", - groupPolicy: "allowlist", - groupAllowFrom: ["12345"], - reactionNotifications: "all", - }, + expectedEnqueueCalls: 1, + }, + { + name: "blocks reaction in group allowlist mode for unauthorized sender", + updateId: 513, + channelConfig: { + dmPolicy: "open", + groupPolicy: "allowlist", + groupAllowFrom: ["12345"], + reactionNotifications: "all", }, - }); - - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message_reaction") as ( - ctx: Record, - ) => Promise; - - await handler({ - update: { update_id: 513 }, - messageReaction: { + reaction: { chat: { id: 9999, type: "supergroup" }, message_id: 77, user: { id: 9, first_name: "Ada" }, @@ -952,9 +896,29 @@ describe("createTelegramBot", () => { old_reaction: [], new_reaction: [{ type: "emoji", emoji: "🔥" }], }, + expectedEnqueueCalls: 0, + }, + ])("$name", async ({ updateId, channelConfig, reaction, expectedEnqueueCalls }) => { + onSpy.mockClear(); + enqueueSystemEventSpy.mockClear(); + + loadConfig.mockReturnValue({ + channels: { + telegram: channelConfig, + }, }); - expect(enqueueSystemEventSpy).not.toHaveBeenCalled(); + createTelegramBot({ token: "tok" }); + const handler = getOnHandler("message_reaction") as ( + ctx: Record, + ) => Promise; + + await handler({ + update: { update_id: updateId }, + messageReaction: reaction, + }); + + expect(enqueueSystemEventSpy).toHaveBeenCalledTimes(expectedEnqueueCalls); }); it("skips reaction when reactionNotifications is off", async () => {