refactor: simplify telegram event authorization flow

This commit is contained in:
Peter Steinberger
2026-02-26 01:13:57 +01:00
parent 496a76c03b
commit 046feb6b0e
2 changed files with 143 additions and 156 deletions

View File

@@ -17,6 +17,7 @@ import { resolveChannelConfigWrites } from "../channels/plugins/config-writes.js
import { loadConfig } from "../config/config.js"; import { loadConfig } from "../config/config.js";
import { writeConfigFile } from "../config/io.js"; import { writeConfigFile } from "../config/io.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.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 type { TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js";
import { danger, logVerbose, warn } from "../globals.js"; import { danger, logVerbose, warn } from "../globals.js";
import { enqueueSystemEvent } from "../infra/system-events.js"; import { enqueueSystemEvent } from "../infra/system-events.js";
@@ -507,54 +508,87 @@ export const registerTelegramHandlers = ({
return false; return false;
}; };
const isTelegramEventSenderAuthorized = async (params: { type TelegramGroupAllowContext = Awaited<ReturnType<typeof resolveTelegramGroupAllowFromContext>>;
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; chatId: number;
chatTitle?: string;
isGroup: boolean;
isForum: boolean; isForum: boolean;
messageThreadId?: number; messageThreadId?: number;
senderId: string; groupAllowContext?: TelegramGroupAllowContext;
senderUsername: string; }): Promise<TelegramEventAuthorizationContext> => {
enforceDirectAuthorization: boolean;
enforceGroupAllowlistAuthorization: boolean;
deniedDmReason: string;
deniedGroupReason: string;
groupAllowContext?: Awaited<ReturnType<typeof resolveTelegramGroupAllowFromContext>>;
}) => {
const {
chatId,
chatTitle,
isGroup,
isForum,
messageThreadId,
senderId,
senderUsername,
enforceDirectAuthorization,
enforceGroupAllowlistAuthorization,
deniedDmReason,
deniedGroupReason,
groupAllowContext: preResolvedGroupAllowContext,
} = params;
const dmPolicy = telegramCfg.dmPolicy ?? "pairing"; const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
const groupAllowContext = const groupAllowContext =
preResolvedGroupAllowContext ?? params.groupAllowContext ??
(await resolveTelegramGroupAllowFromContext({ (await resolveTelegramGroupAllowFromContext({
chatId, chatId: params.chatId,
accountId, accountId,
dmPolicy, dmPolicy,
isForum, isForum: params.isForum,
messageThreadId, messageThreadId: params.messageThreadId,
groupAllowFrom, groupAllowFrom,
resolveTelegramGroupConfig, 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 { const {
dmPolicy,
resolvedThreadId, resolvedThreadId,
storeAllowFrom, storeAllowFrom,
groupConfig, groupConfig,
topicConfig, topicConfig,
effectiveGroupAllow, effectiveGroupAllow,
hasGroupAllowOverride, hasGroupAllowOverride,
} = groupAllowContext; } = context;
const authRules = TELEGRAM_EVENT_AUTH_RULES[mode];
const {
enforceDirectAuthorization,
enforceGroupAllowlistAuthorization,
deniedDmReason,
deniedGroupReason,
} = authRules;
if ( if (
shouldSkipGroupMessage({ shouldSkipGroupMessage({
isGroup, isGroup,
@@ -569,7 +603,7 @@ export const registerTelegramHandlers = ({
topicConfig, topicConfig,
}) })
) { ) {
return false; return { allowed: false, reason: "group-policy" };
} }
if (!isGroup && enforceDirectAuthorization) { if (!isGroup && enforceDirectAuthorization) {
@@ -577,7 +611,7 @@ export const registerTelegramHandlers = ({
logVerbose( logVerbose(
`Blocked telegram direct event from ${senderId || "unknown"} (${deniedDmReason})`, `Blocked telegram direct event from ${senderId || "unknown"} (${deniedDmReason})`,
); );
return false; return { allowed: false, reason: "direct-disabled" };
} }
if (dmPolicy !== "open") { if (dmPolicy !== "open") {
const effectiveDmAllow = normalizeAllowFromWithStore({ const effectiveDmAllow = normalizeAllowFromWithStore({
@@ -587,17 +621,17 @@ export const registerTelegramHandlers = ({
}); });
if (!isAllowlistAuthorized(effectiveDmAllow, senderId, senderUsername)) { if (!isAllowlistAuthorized(effectiveDmAllow, senderId, senderUsername)) {
logVerbose(`Blocked telegram direct sender ${senderId || "unknown"} (${deniedDmReason})`); logVerbose(`Blocked telegram direct sender ${senderId || "unknown"} (${deniedDmReason})`);
return false; return { allowed: false, reason: "direct-unauthorized" };
} }
} }
} }
if (isGroup && enforceGroupAllowlistAuthorization) { if (isGroup && enforceGroupAllowlistAuthorization) {
if (!isAllowlistAuthorized(effectiveGroupAllow, senderId, senderUsername)) { if (!isAllowlistAuthorized(effectiveGroupAllow, senderId, senderUsername)) {
logVerbose(`Blocked telegram group sender ${senderId || "unknown"} (${deniedGroupReason})`); 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. // Handle emoji reactions to messages.
@@ -630,19 +664,20 @@ export const registerTelegramHandlers = ({
if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) { if (reactionMode === "own" && !wasSentByBot(chatId, messageId)) {
return; return;
} }
const senderAuthorized = await isTelegramEventSenderAuthorized({ const eventAuthContext = await resolveTelegramEventAuthorizationContext({
chatId,
isForum,
});
const senderAuthorization = authorizeTelegramEventSender({
chatId, chatId,
chatTitle: reaction.chat.title, chatTitle: reaction.chat.title,
isGroup, isGroup,
isForum,
senderId, senderId,
senderUsername, senderUsername,
enforceDirectAuthorization: true, mode: "reaction",
enforceGroupAllowlistAuthorization: false, context: eventAuthContext,
deniedDmReason: "reaction unauthorized by dm policy/allowlist",
deniedGroupReason: "reaction unauthorized by group allowlist",
}); });
if (!senderAuthorized) { if (!senderAuthorization.allowed) {
return; return;
} }
@@ -965,33 +1000,26 @@ export const registerTelegramHandlers = ({
const messageThreadId = callbackMessage.message_thread_id; const messageThreadId = callbackMessage.message_thread_id;
const isForum = callbackMessage.chat.is_forum === true; const isForum = callbackMessage.chat.is_forum === true;
const groupAllowContext = await resolveTelegramGroupAllowFromContext({ const eventAuthContext = await resolveTelegramEventAuthorizationContext({
chatId, chatId,
accountId,
dmPolicy: telegramCfg.dmPolicy ?? "pairing",
isForum, isForum,
messageThreadId, messageThreadId,
groupAllowFrom,
resolveTelegramGroupConfig,
}); });
const { resolvedThreadId, storeAllowFrom } = groupAllowContext; const { resolvedThreadId, storeAllowFrom } = eventAuthContext;
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 ?? "";
const senderAuthorized = await isTelegramEventSenderAuthorized({ const authorizationMode: TelegramEventAuthorizationMode =
inlineButtonsScope === "allowlist" ? "callback-allowlist" : "callback-scope";
const senderAuthorization = authorizeTelegramEventSender({
chatId, chatId,
chatTitle: callbackMessage.chat.title, chatTitle: callbackMessage.chat.title,
isGroup, isGroup,
isForum,
messageThreadId,
senderId, senderId,
senderUsername, senderUsername,
enforceDirectAuthorization: inlineButtonsScope === "allowlist", mode: authorizationMode,
enforceGroupAllowlistAuthorization: inlineButtonsScope === "allowlist", context: eventAuthContext,
deniedDmReason: "callback unauthorized by inlineButtonsScope allowlist",
deniedGroupReason: "callback unauthorized by inlineButtonsScope allowlist",
groupAllowContext,
}); });
if (!senderAuthorized) { if (!senderAuthorization.allowed) {
return; return;
} }
@@ -1230,25 +1258,20 @@ export const registerTelegramHandlers = ({
if (shouldSkipUpdate(event.ctxForDedupe)) { if (shouldSkipUpdate(event.ctxForDedupe)) {
return; return;
} }
const dmPolicy = telegramCfg.dmPolicy ?? "pairing"; const eventAuthContext = await resolveTelegramEventAuthorizationContext({
const groupAllowContext = await resolveTelegramGroupAllowFromContext({
chatId: event.chatId, chatId: event.chatId,
accountId,
dmPolicy,
isForum: event.isForum, isForum: event.isForum,
messageThreadId: event.messageThreadId, messageThreadId: event.messageThreadId,
groupAllowFrom,
resolveTelegramGroupConfig,
}); });
const { const {
dmPolicy,
resolvedThreadId, resolvedThreadId,
storeAllowFrom, storeAllowFrom,
groupConfig, groupConfig,
topicConfig, topicConfig,
effectiveGroupAllow, effectiveGroupAllow,
hasGroupAllowOverride, hasGroupAllowOverride,
} = groupAllowContext; } = eventAuthContext;
const effectiveDmAllow = normalizeAllowFromWithStore({ const effectiveDmAllow = normalizeAllowFromWithStore({
allowFrom, allowFrom,
storeAllowFrom, storeAllowFrom,

View File

@@ -832,24 +832,12 @@ describe("createTelegramBot", () => {
); );
}); });
it("blocks reaction when dmPolicy is disabled", async () => { it.each([
onSpy.mockClear(); {
enqueueSystemEventSpy.mockClear(); name: "blocks reaction when dmPolicy is disabled",
updateId: 510,
loadConfig.mockReturnValue({ channelConfig: { dmPolicy: "disabled", reactionNotifications: "all" },
channels: { reaction: {
telegram: { dmPolicy: "disabled", reactionNotifications: "all" },
},
});
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message_reaction") as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
update: { update_id: 510 },
messageReaction: {
chat: { id: 1234, type: "private" }, chat: { id: 1234, type: "private" },
message_id: 42, message_id: 42,
user: { id: 9, first_name: "Ada" }, user: { id: 9, first_name: "Ada" },
@@ -857,29 +845,17 @@ describe("createTelegramBot", () => {
old_reaction: [], old_reaction: [],
new_reaction: [{ type: "emoji", emoji: "👍" }], new_reaction: [{ type: "emoji", emoji: "👍" }],
}, },
}); expectedEnqueueCalls: 0,
},
expect(enqueueSystemEventSpy).not.toHaveBeenCalled(); {
}); name: "blocks reaction in allowlist mode for unauthorized direct sender",
updateId: 511,
it("blocks reaction in allowlist mode for unauthorized direct sender", async () => { channelConfig: {
onSpy.mockClear(); dmPolicy: "allowlist",
enqueueSystemEventSpy.mockClear(); allowFrom: ["12345"],
reactionNotifications: "all",
loadConfig.mockReturnValue({
channels: {
telegram: { dmPolicy: "allowlist", allowFrom: ["12345"], reactionNotifications: "all" },
}, },
}); reaction: {
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message_reaction") as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
update: { update_id: 511 },
messageReaction: {
chat: { id: 1234, type: "private" }, chat: { id: 1234, type: "private" },
message_id: 42, message_id: 42,
user: { id: 9, first_name: "Ada" }, user: { id: 9, first_name: "Ada" },
@@ -887,29 +863,13 @@ describe("createTelegramBot", () => {
old_reaction: [], old_reaction: [],
new_reaction: [{ type: "emoji", emoji: "👍" }], new_reaction: [{ type: "emoji", emoji: "👍" }],
}, },
}); expectedEnqueueCalls: 0,
},
expect(enqueueSystemEventSpy).not.toHaveBeenCalled(); {
}); name: "allows reaction in allowlist mode for authorized direct sender",
updateId: 512,
it("allows reaction in allowlist mode for authorized direct sender", async () => { channelConfig: { dmPolicy: "allowlist", allowFrom: ["9"], reactionNotifications: "all" },
onSpy.mockClear(); reaction: {
enqueueSystemEventSpy.mockClear();
loadConfig.mockReturnValue({
channels: {
telegram: { dmPolicy: "allowlist", allowFrom: ["9"], reactionNotifications: "all" },
},
});
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message_reaction") as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
update: { update_id: 512 },
messageReaction: {
chat: { id: 1234, type: "private" }, chat: { id: 1234, type: "private" },
message_id: 42, message_id: 42,
user: { id: 9, first_name: "Ada" }, user: { id: 9, first_name: "Ada" },
@@ -917,34 +877,18 @@ describe("createTelegramBot", () => {
old_reaction: [], old_reaction: [],
new_reaction: [{ type: "emoji", emoji: "👍" }], new_reaction: [{ type: "emoji", emoji: "👍" }],
}, },
}); expectedEnqueueCalls: 1,
},
expect(enqueueSystemEventSpy).toHaveBeenCalledTimes(1); {
}); name: "blocks reaction in group allowlist mode for unauthorized sender",
updateId: 513,
it("blocks reaction in group allowlist mode for unauthorized sender", async () => { channelConfig: {
onSpy.mockClear(); dmPolicy: "open",
enqueueSystemEventSpy.mockClear(); groupPolicy: "allowlist",
groupAllowFrom: ["12345"],
loadConfig.mockReturnValue({ reactionNotifications: "all",
channels: {
telegram: {
dmPolicy: "open",
groupPolicy: "allowlist",
groupAllowFrom: ["12345"],
reactionNotifications: "all",
},
}, },
}); reaction: {
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message_reaction") as (
ctx: Record<string, unknown>,
) => Promise<void>;
await handler({
update: { update_id: 513 },
messageReaction: {
chat: { id: 9999, type: "supergroup" }, chat: { id: 9999, type: "supergroup" },
message_id: 77, message_id: 77,
user: { id: 9, first_name: "Ada" }, user: { id: 9, first_name: "Ada" },
@@ -952,9 +896,29 @@ describe("createTelegramBot", () => {
old_reaction: [], old_reaction: [],
new_reaction: [{ type: "emoji", emoji: "🔥" }], 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<string, unknown>,
) => Promise<void>;
await handler({
update: { update_id: updateId },
messageReaction: reaction,
});
expect(enqueueSystemEventSpy).toHaveBeenCalledTimes(expectedEnqueueCalls);
}); });
it("skips reaction when reactionNotifications is off", async () => { it("skips reaction when reactionNotifications is off", async () => {