feat(telegram): improve DM topics support (#30579) (thanks @kesor)

This commit is contained in:
Ayaan Zaidi
2026-03-02 09:06:10 +05:30
committed by Ayaan Zaidi
parent aafc4d56e3
commit c13b35b83d
16 changed files with 335 additions and 44 deletions

View File

@@ -18,7 +18,11 @@ 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 type {
TelegramDirectConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../config/types.js";
import { danger, logVerbose, warn } from "../globals.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { MediaFetchError } from "../media/fetch.js";
@@ -608,22 +612,30 @@ export const registerTelegramHandlers = ({
const resolveTelegramEventAuthorizationContext = async (params: {
chatId: number;
isGroup: boolean;
isForum: boolean;
messageThreadId?: number;
groupAllowContext?: TelegramGroupAllowContext;
}): Promise<TelegramEventAuthorizationContext> => {
const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
const groupAllowContext =
params.groupAllowContext ??
(await resolveTelegramGroupAllowFromContext({
chatId: params.chatId,
accountId,
isGroup: params.isGroup,
isForum: params.isForum,
messageThreadId: params.messageThreadId,
groupAllowFrom,
resolveTelegramGroupConfig,
}));
return { dmPolicy, ...groupAllowContext };
// Use direct config dmPolicy override if available for DMs
const effectiveDmPolicy =
!params.isGroup &&
groupAllowContext.groupConfig &&
"dmPolicy" in groupAllowContext.groupConfig
? (groupAllowContext.groupConfig.dmPolicy ?? telegramCfg.dmPolicy ?? "pairing")
: (telegramCfg.dmPolicy ?? "pairing");
return { dmPolicy: effectiveDmPolicy, ...groupAllowContext };
};
const authorizeTelegramEventSender = (params: {
@@ -642,6 +654,7 @@ export const registerTelegramHandlers = ({
storeAllowFrom,
groupConfig,
topicConfig,
groupAllowOverride,
effectiveGroupAllow,
hasGroupAllowOverride,
} = context;
@@ -677,8 +690,10 @@ export const registerTelegramHandlers = ({
return { allowed: false, reason: "direct-disabled" };
}
if (dmPolicy !== "open") {
// For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom
const dmAllowFrom = groupAllowOverride ?? allowFrom;
const effectiveDmAllow = normalizeDmAllowFromWithStore({
allowFrom,
allowFrom: dmAllowFrom,
storeAllowFrom,
dmPolicy,
});
@@ -729,6 +744,7 @@ export const registerTelegramHandlers = ({
}
const eventAuthContext = await resolveTelegramEventAuthorizationContext({
chatId,
isGroup,
isForum,
});
const senderAuthorization = authorizeTelegramEventSender({
@@ -744,6 +760,20 @@ export const registerTelegramHandlers = ({
return;
}
// Enforce requireTopic for DM reactions: since Telegram doesn't provide messageThreadId
// for reactions, we cannot determine if the reaction came from a topic, so block all
// reactions if requireTopic is enabled for this DM.
if (!isGroup) {
const requireTopic = (eventAuthContext.groupConfig as TelegramDirectConfig | undefined)
?.requireTopic;
if (requireTopic === true) {
logVerbose(
`Blocked telegram reaction in DM ${chatId}: requireTopic=true but topic unknown for reactions`,
);
return;
}
}
// Detect added reactions.
const oldEmojis = new Set(
reaction.old_reaction
@@ -811,6 +841,7 @@ export const registerTelegramHandlers = ({
msg: Message;
chatId: number;
resolvedThreadId?: number;
dmThreadId?: number;
storeAllowFrom: string[];
sendOversizeWarning: boolean;
oversizeLogMessage: string;
@@ -820,6 +851,7 @@ export const registerTelegramHandlers = ({
msg,
chatId,
resolvedThreadId,
dmThreadId,
storeAllowFrom,
sendOversizeWarning,
oversizeLogMessage,
@@ -832,7 +864,9 @@ export const registerTelegramHandlers = ({
if (text && !isCommandLike) {
const nowMs = Date.now();
const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
const key = `text:${chatId}:${resolvedThreadId ?? "main"}:${senderId}`;
// Use resolvedThreadId for forum groups, dmThreadId for DM topics
const threadId = resolvedThreadId ?? dmThreadId;
const key = `text:${chatId}:${threadId ?? "main"}:${senderId}`;
const existing = textFragmentBuffer.get(key);
if (existing) {
@@ -970,8 +1004,9 @@ export const registerTelegramHandlers = ({
]
: [];
const senderId = msg.from?.id ? String(msg.from.id) : "";
const conversationThreadId = resolvedThreadId ?? dmThreadId;
const conversationKey =
resolvedThreadId != null ? `${chatId}:topic:${resolvedThreadId}` : String(chatId);
conversationThreadId != null ? `${chatId}:topic:${conversationThreadId}` : String(chatId);
const debounceLane = resolveTelegramDebounceLane(msg);
const debounceKey = senderId
? `telegram:${accountId ?? "default"}:${conversationKey}:${senderId}:${debounceLane}`
@@ -1065,10 +1100,18 @@ export const registerTelegramHandlers = ({
const isForum = callbackMessage.chat.is_forum === true;
const eventAuthContext = await resolveTelegramEventAuthorizationContext({
chatId,
isGroup,
isForum,
messageThreadId,
});
const { resolvedThreadId, storeAllowFrom } = eventAuthContext;
const { resolvedThreadId, dmThreadId, storeAllowFrom, groupConfig } = eventAuthContext;
const requireTopic = (groupConfig as { requireTopic?: boolean } | undefined)?.requireTopic;
if (!isGroup && requireTopic === true && dmThreadId == null) {
logVerbose(
`Blocked telegram callback in DM ${chatId}: requireTopic=true but no topic present`,
);
return;
}
const senderId = callback.from?.id ? String(callback.from.id) : "";
const senderUsername = callback.from?.username ?? "";
const authorizationMode: TelegramEventAuthorizationMode =
@@ -1323,20 +1366,25 @@ export const registerTelegramHandlers = ({
}
const eventAuthContext = await resolveTelegramEventAuthorizationContext({
chatId: event.chatId,
isGroup: event.isGroup,
isForum: event.isForum,
messageThreadId: event.messageThreadId,
});
const {
dmPolicy,
resolvedThreadId,
dmThreadId,
storeAllowFrom,
groupConfig,
topicConfig,
groupAllowOverride,
effectiveGroupAllow,
hasGroupAllowOverride,
} = eventAuthContext;
// For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom
const dmAllowFrom = groupAllowOverride ?? allowFrom;
const effectiveDmAllow = normalizeDmAllowFromWithStore({
allowFrom,
allowFrom: dmAllowFrom,
storeAllowFrom,
dmPolicy,
});
@@ -1384,6 +1432,7 @@ export const registerTelegramHandlers = ({
msg: event.msg,
chatId: event.chatId,
resolvedThreadId,
dmThreadId,
storeAllowFrom,
sendOversizeWarning: event.sendOversizeWarning,
oversizeLogMessage: event.oversizeLogMessage,

View File

@@ -30,7 +30,12 @@ import {
import type { OpenClawConfig } from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { readSessionUpdatedAt, resolveStorePath } from "../config/sessions.js";
import type { DmPolicy, TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js";
import type {
DmPolicy,
TelegramDirectConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../config/types.js";
import { logVerbose, shouldLogVerbose } from "../globals.js";
import { recordChannelActivity } from "../infra/channel-activity.js";
import { resolveAgentRoute } from "../routing/resolve-route.js";
@@ -87,7 +92,10 @@ type TelegramLogger = {
type ResolveTelegramGroupConfig = (
chatId: string | number,
messageThreadId?: number,
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
) => {
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
topicConfig?: TelegramTopicConfig;
};
type ResolveGroupActivation = (params: {
chatId: string | number;
@@ -174,7 +182,14 @@ export const buildTelegramMessageContext = async ({
});
const resolvedThreadId = threadSpec.scope === "forum" ? threadSpec.id : undefined;
const replyThreadId = threadSpec.id;
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, resolvedThreadId);
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
const threadIdForConfig = resolvedThreadId ?? dmThreadId;
const { groupConfig, topicConfig } = resolveTelegramGroupConfig(chatId, threadIdForConfig);
// Use direct config dmPolicy override if available for DMs
const effectiveDmPolicy =
!isGroup && groupConfig && "dmPolicy" in groupConfig
? (groupConfig.dmPolicy ?? dmPolicy)
: dmPolicy;
const peerId = isGroup ? buildTelegramGroupPeerId(chatId, resolvedThreadId) : String(chatId);
const parentPeer = buildTelegramParentPeer({ isGroup, resolvedThreadId, chatId });
// Fresh config for bindings lookup; other routing inputs are payload-derived.
@@ -200,16 +215,22 @@ export const buildTelegramMessageContext = async ({
return null;
}
const baseSessionKey = route.sessionKey;
// DMs: use raw messageThreadId for thread sessions (not forum topic ids)
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
// DMs: use thread suffix for session isolation (works regardless of dmScope)
const threadKeys =
dmThreadId != null
? resolveThreadSessionKeys({ baseSessionKey, threadId: `${chatId}:${dmThreadId}` })
: null;
const sessionKey = threadKeys?.sessionKey ?? baseSessionKey;
const mentionRegexes = buildMentionRegexes(cfg, route.agentId);
const effectiveDmAllow = normalizeDmAllowFromWithStore({ allowFrom, storeAllowFrom, dmPolicy });
// Calculate groupAllowOverride first - it's needed for both DM and group allowlist checks
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
// For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom
const dmAllowFrom = groupAllowOverride ?? allowFrom;
const effectiveDmAllow = normalizeDmAllowFromWithStore({
allowFrom: dmAllowFrom,
storeAllowFrom,
dmPolicy: effectiveDmPolicy,
});
// Group sender checks are explicit and must not inherit DM pairing-store entries.
const effectiveGroupAllow = normalizeAllowFrom(groupAllowOverride ?? groupAllowFrom);
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
@@ -237,7 +258,11 @@ export const buildTelegramMessageContext = async ({
);
return null;
}
logVerbose(`Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`);
logVerbose(
isGroup
? `Blocked telegram group sender ${senderId || "unknown"} (group allowFrom override)`
: `Blocked telegram DM sender ${senderId || "unknown"} (DM allowFrom override)`,
);
return null;
}
@@ -252,10 +277,17 @@ export const buildTelegramMessageContext = async ({
const requireMention = firstDefined(
activationOverride,
topicConfig?.requireMention,
groupConfig?.requireMention,
(groupConfig as TelegramGroupConfig | undefined)?.requireMention,
baseRequireMention,
);
const requireTopic = (groupConfig as TelegramDirectConfig | undefined)?.requireTopic;
const topicRequiredButMissing = !isGroup && requireTopic === true && dmThreadId == null;
if (topicRequiredButMissing) {
logVerbose(`Blocked telegram DM ${chatId}: requireTopic=true but no topic present`);
return null;
}
const sendTyping = async () => {
await withTelegramApiErrorLogging({
operation: "sendChatAction",
@@ -287,7 +319,7 @@ export const buildTelegramMessageContext = async ({
if (
!(await enforceTelegramDmAccess({
isGroup,
dmPolicy,
dmPolicy: effectiveDmPolicy,
msg,
chatId,
effectiveDmAllow,
@@ -669,7 +701,7 @@ export const buildTelegramMessageContext = async ({
ChatType: isGroup ? "group" : "direct",
ConversationLabel: conversationLabel,
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
GroupSystemPrompt: isGroup || (!isGroup && groupConfig) ? groupSystemPrompt : undefined,
SenderName: senderName,
SenderId: senderId || undefined,
SenderUsername: senderUsername || undefined,

View File

@@ -26,6 +26,7 @@ import {
import type {
ReplyToMode,
TelegramAccountConfig,
TelegramDirectConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../config/types.js";
@@ -172,6 +173,7 @@ async function resolveTelegramCommandAuth(params: {
const groupAllowContext = await resolveTelegramGroupAllowFromContext({
chatId,
accountId,
isGroup,
isForum,
messageThreadId,
groupAllowFrom,
@@ -179,12 +181,26 @@ async function resolveTelegramCommandAuth(params: {
});
const {
resolvedThreadId,
dmThreadId,
storeAllowFrom,
groupConfig,
topicConfig,
groupAllowOverride,
effectiveGroupAllow,
hasGroupAllowOverride,
} = groupAllowContext;
// Use direct config dmPolicy override if available for DMs
const effectiveDmPolicy =
!isGroup && groupConfig && "dmPolicy" in groupConfig
? (groupConfig.dmPolicy ?? telegramCfg.dmPolicy ?? "pairing")
: (telegramCfg.dmPolicy ?? "pairing");
const requireTopic = (groupConfig as TelegramDirectConfig | undefined)?.requireTopic;
if (!isGroup && requireTopic === true && dmThreadId == null) {
logVerbose(`Blocked telegram command in DM ${chatId}: requireTopic=true but no topic present`);
return null;
}
// For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom
const dmAllowFrom = groupAllowOverride ?? allowFrom;
const senderId = msg.from?.id ? String(msg.from.id) : "";
const senderUsername = msg.from?.username ?? "";
@@ -254,9 +270,9 @@ async function resolveTelegramCommandAuth(params: {
}
const dmAllow = normalizeDmAllowFromWithStore({
allowFrom: allowFrom,
allowFrom: dmAllowFrom,
storeAllowFrom: isGroup ? [] : storeAllowFrom,
dmPolicy: telegramCfg.dmPolicy ?? "pairing",
dmPolicy: effectiveDmPolicy,
});
const senderAllowed = isSenderAllowed({
allow: dmAllow,
@@ -575,7 +591,7 @@ export const registerTelegramNativeCommands = ({
ChatType: isGroup ? "group" : "direct",
ConversationLabel: conversationLabel,
GroupSubject: isGroup ? (msg.chat.title ?? undefined) : undefined,
GroupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
GroupSystemPrompt: isGroup || (!isGroup && groupConfig) ? groupSystemPrompt : undefined,
SenderName: buildSenderName(msg),
SenderId: senderId || undefined,
SenderUsername: senderUsername || undefined,

View File

@@ -588,6 +588,87 @@ describe("createTelegramBot", () => {
}
});
it("isolates inbound debounce by DM topic thread id", async () => {
const DEBOUNCE_MS = 4321;
onSpy.mockClear();
replySpy.mockClear();
loadConfig.mockReturnValue({
agents: {
defaults: {
envelopeTimezone: "utc",
},
},
messages: {
inbound: {
debounceMs: DEBOUNCE_MS,
},
},
channels: {
telegram: {
dmPolicy: "open",
allowFrom: ["*"],
},
},
});
const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout");
try {
createTelegramBot({ token: "tok" });
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
await handler({
message: {
chat: { id: 7, type: "private" },
text: "topic-100",
date: 1736380800,
message_id: 201,
message_thread_id: 100,
from: { id: 42, first_name: "Ada" },
},
me: { username: "openclaw_bot" },
getFile: async () => ({}),
});
await handler({
message: {
chat: { id: 7, type: "private" },
text: "topic-200",
date: 1736380801,
message_id: 202,
message_thread_id: 200,
from: { id: 42, first_name: "Ada" },
},
me: { username: "openclaw_bot" },
getFile: async () => ({}),
});
expect(replySpy).not.toHaveBeenCalled();
const debounceTimerIndexes = setTimeoutSpy.mock.calls
.map((call, index) => ({ index, delay: call[1] }))
.filter((entry) => entry.delay === DEBOUNCE_MS)
.map((entry) => entry.index);
expect(debounceTimerIndexes.length).toBeGreaterThanOrEqual(2);
for (const index of debounceTimerIndexes) {
clearTimeout(setTimeoutSpy.mock.results[index]?.value as ReturnType<typeof setTimeout>);
}
for (const index of debounceTimerIndexes) {
const flushTimer = setTimeoutSpy.mock.calls[index]?.[0] as (() => unknown) | undefined;
await flushTimer?.();
}
await vi.waitFor(() => {
expect(replySpy).toHaveBeenCalledTimes(2);
});
const threadIds = replySpy.mock.calls
.map((call) => (call[0] as { MessageThreadId?: number }).MessageThreadId)
.toSorted((a, b) => (a ?? 0) - (b ?? 0));
expect(threadIds).toEqual([100, 200]);
} finally {
setTimeoutSpy.mockRestore();
}
});
it("handles quote-only replies without reply metadata", async () => {
onSpy.mockClear();
sendMessageSpy.mockClear();

View File

@@ -270,12 +270,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
const dmPolicy = telegramCfg.dmPolicy ?? "pairing";
const allowFrom = opts.allowFrom ?? telegramCfg.allowFrom;
const groupAllowFrom =
opts.groupAllowFrom ??
telegramCfg.groupAllowFrom ??
(telegramCfg.allowFrom && telegramCfg.allowFrom.length > 0
? telegramCfg.allowFrom
: undefined) ??
(opts.allowFrom && opts.allowFrom.length > 0 ? opts.allowFrom : undefined);
opts.groupAllowFrom ?? telegramCfg.groupAllowFrom ?? telegramCfg.allowFrom ?? allowFrom;
const replyToMode = opts.replyToMode ?? telegramCfg.replyToMode ?? "off";
const nativeEnabled = resolveNativeCommandsEnabled({
providerId: "telegram",
@@ -339,11 +334,25 @@ export function createTelegramBot(opts: TelegramBotOptions) {
});
const resolveTelegramGroupConfig = (chatId: string | number, messageThreadId?: number) => {
const groups = telegramCfg.groups;
const direct = telegramCfg.direct;
const chatIdStr = String(chatId);
const isDm = !chatIdStr.startsWith("-");
if (isDm) {
const directConfig = direct?.[chatIdStr] ?? direct?.["*"];
if (directConfig) {
const topicConfig =
messageThreadId != null ? directConfig.topics?.[String(messageThreadId)] : undefined;
return { groupConfig: directConfig, topicConfig };
}
// DMs without direct config: don't fall through to groups lookup
return { groupConfig: undefined, topicConfig: undefined };
}
if (!groups) {
return { groupConfig: undefined, topicConfig: undefined };
}
const groupKey = String(chatId);
const groupConfig = groups[groupKey] ?? groups["*"];
const groupConfig = groups[chatIdStr] ?? groups["*"];
const topicConfig =
messageThreadId != null ? groupConfig?.topics?.[String(messageThreadId)] : undefined;
return { groupConfig, topicConfig };

View File

@@ -1,7 +1,11 @@
import type { Chat, Message, MessageOrigin, User } from "@grammyjs/types";
import { formatLocationText, type NormalizedLocation } from "../../channels/location.js";
import { resolveTelegramPreviewStreamMode } from "../../config/discord-preview-streaming.js";
import type { TelegramGroupConfig, TelegramTopicConfig } from "../../config/types.js";
import type {
TelegramDirectConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../../config/types.js";
import { readChannelAllowFromStore } from "../../pairing/pairing-store.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { firstDefined, normalizeAllowFrom, type NormalizedAllowFrom } from "../bot-access.js";
@@ -17,33 +21,43 @@ export type TelegramThreadSpec = {
export async function resolveTelegramGroupAllowFromContext(params: {
chatId: string | number;
accountId?: string;
isGroup?: boolean;
isForum?: boolean;
messageThreadId?: number | null;
groupAllowFrom?: Array<string | number>;
resolveTelegramGroupConfig: (
chatId: string | number,
messageThreadId?: number,
) => { groupConfig?: TelegramGroupConfig; topicConfig?: TelegramTopicConfig };
) => {
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
topicConfig?: TelegramTopicConfig;
};
}): Promise<{
resolvedThreadId?: number;
dmThreadId?: number;
storeAllowFrom: string[];
groupConfig?: TelegramGroupConfig;
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
topicConfig?: TelegramTopicConfig;
groupAllowOverride?: Array<string | number>;
effectiveGroupAllow: NormalizedAllowFrom;
hasGroupAllowOverride: boolean;
}> {
const accountId = normalizeAccountId(params.accountId);
const resolvedThreadId = resolveTelegramForumThreadId({
// Use resolveTelegramThreadSpec to handle both forum groups AND DM topics
const threadSpec = resolveTelegramThreadSpec({
isGroup: params.isGroup ?? false,
isForum: params.isForum,
messageThreadId: params.messageThreadId,
});
const resolvedThreadId = threadSpec.scope === "forum" ? threadSpec.id : undefined;
const dmThreadId = threadSpec.scope === "dm" ? threadSpec.id : undefined;
const threadIdForConfig = resolvedThreadId ?? dmThreadId;
const storeAllowFrom = await readChannelAllowFromStore("telegram", process.env, accountId).catch(
() => [],
);
const { groupConfig, topicConfig } = params.resolveTelegramGroupConfig(
params.chatId,
resolvedThreadId,
threadIdForConfig,
);
const groupAllowOverride = firstDefined(topicConfig?.allowFrom, groupConfig?.allowFrom);
// Group sender access must remain explicit (groupAllowFrom/per-group allowFrom only).
@@ -52,6 +66,7 @@ export async function resolveTelegramGroupAllowFromContext(params: {
const hasGroupAllowOverride = typeof groupAllowOverride !== "undefined";
return {
resolvedThreadId,
dmThreadId,
storeAllowFrom,
groupConfig,
topicConfig,

View File

@@ -3,6 +3,7 @@ import type { ChannelGroupPolicy } from "../config/group-policy.js";
import { resolveOpenProviderRuntimeGroupPolicy } from "../config/runtime-group-policy.js";
import type {
TelegramAccountConfig,
TelegramDirectConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../config/types.js";
@@ -20,7 +21,7 @@ export type TelegramGroupBaseAccessResult =
export const evaluateTelegramGroupBaseAccess = (params: {
isGroup: boolean;
groupConfig?: TelegramGroupConfig;
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
topicConfig?: TelegramTopicConfig;
hasGroupAllowOverride: boolean;
effectiveGroupAllow: NormalizedAllowFrom;
@@ -29,15 +30,34 @@ export const evaluateTelegramGroupBaseAccess = (params: {
enforceAllowOverride: boolean;
requireSenderForAllowOverride: boolean;
}): TelegramGroupBaseAccessResult => {
if (!params.isGroup) {
return { allowed: true };
}
// Check enabled flags for both groups and DMs
if (params.groupConfig?.enabled === false) {
return { allowed: false, reason: "group-disabled" };
}
if (params.topicConfig?.enabled === false) {
return { allowed: false, reason: "topic-disabled" };
}
if (!params.isGroup) {
// For DMs, check allowFrom override if present
if (params.enforceAllowOverride && params.hasGroupAllowOverride) {
if (!params.effectiveGroupAllow.hasEntries) {
return { allowed: false, reason: "group-override-unauthorized" };
}
const senderId = params.senderId ?? "";
if (params.requireSenderForAllowOverride && !senderId) {
return { allowed: false, reason: "group-override-unauthorized" };
}
const allowed = isSenderAllowed({
allow: params.effectiveGroupAllow,
senderId,
senderUsername: params.senderUsername ?? "",
});
if (!allowed) {
return { allowed: false, reason: "group-override-unauthorized" };
}
}
return { allowed: true };
}
if (!params.enforceAllowOverride || !params.hasGroupAllowOverride) {
return { allowed: true };
}

View File

@@ -1,8 +1,12 @@
import type { TelegramGroupConfig, TelegramTopicConfig } from "../config/types.js";
import type {
TelegramDirectConfig,
TelegramGroupConfig,
TelegramTopicConfig,
} from "../config/types.js";
import { firstDefined } from "./bot-access.js";
export function resolveTelegramGroupPromptSettings(params: {
groupConfig?: TelegramGroupConfig;
groupConfig?: TelegramGroupConfig | TelegramDirectConfig;
topicConfig?: TelegramTopicConfig;
}): {
skillFilter: string[] | undefined;