refactor(telegram): centralize access authorization

This commit is contained in:
Ayaan Zaidi
2026-05-07 08:55:36 +05:30
parent 6554e85ad6
commit 53efb6747d
6 changed files with 180 additions and 158 deletions

View File

@@ -1,9 +1,14 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import type { DmPolicy, OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import {
expandAllowFromWithAccessGroups,
parseAccessGroupAllowFromEntry,
} from "openclaw/plugin-sdk/security-runtime";
import { isSenderAllowed, normalizeAllowFrom } from "./bot-access.js";
import {
isSenderAllowed,
normalizeAllowFrom,
normalizeDmAllowFromWithStore,
type NormalizedAllowFrom,
} from "./bot-access.js";
export async function expandTelegramAllowFromWithAccessGroups(params: {
cfg?: OpenClawConfig;
@@ -34,3 +39,34 @@ export async function expandTelegramAllowFromWithAccessGroups(params: {
? expanded.filter((entry) => parseAccessGroupAllowFromEntry(entry) == null)
: expanded;
}
export async function resolveTelegramDmAllow(params: {
cfg?: OpenClawConfig;
allowFrom?: Array<string | number>;
groupAllowOverride?: Array<string | number>;
storeAllowFrom?: string[];
dmPolicy?: DmPolicy;
accountId?: string;
senderId?: string;
}): Promise<{
allowFrom?: Array<string | number>;
expandedAllowFrom: string[];
effectiveAllow: NormalizedAllowFrom;
}> {
const allowFrom = params.groupAllowOverride ?? params.allowFrom;
const expandedAllowFrom = await expandTelegramAllowFromWithAccessGroups({
cfg: params.cfg,
allowFrom,
accountId: params.accountId,
senderId: params.senderId,
});
return {
allowFrom,
expandedAllowFrom,
effectiveAllow: normalizeDmAllowFromWithStore({
allowFrom: expandedAllowFrom,
storeAllowFrom: params.storeAllowFrom,
dmPolicy: params.dmPolicy,
}),
};
}

View File

@@ -4,6 +4,11 @@ import {
mergeDmAllowFromSources,
type AllowlistMatch,
} from "openclaw/plugin-sdk/allow-from";
import type {
DmPolicy,
TelegramDirectConfig,
TelegramGroupConfig,
} from "openclaw/plugin-sdk/config-types";
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
@@ -67,6 +72,17 @@ export const normalizeDmAllowFromWithStore = (params: {
dmPolicy?: string;
}): NormalizedAllowFrom => normalizeAllowFrom(mergeDmAllowFromSources(params));
export function resolveTelegramEffectiveDmPolicy(params: {
isGroup: boolean;
groupConfig?: TelegramDirectConfig | TelegramGroupConfig;
dmPolicy?: DmPolicy;
}): DmPolicy {
if (!params.isGroup && params.groupConfig && "dmPolicy" in params.groupConfig) {
return params.groupConfig.dmPolicy ?? params.dmPolicy ?? "pairing";
}
return params.dmPolicy ?? "pairing";
}
export const isSenderAllowed = (params: {
allow: NormalizedAllowFrom;
senderId?: string;

View File

@@ -7,10 +7,7 @@ import {
resolveInboundDebounceMs,
} from "openclaw/plugin-sdk/channel-inbound-debounce";
import { resolveStoredModelOverride } from "openclaw/plugin-sdk/command-auth";
import {
resolveCommandAuthorization,
resolveCommandAuthorizedFromAuthorizers,
} from "openclaw/plugin-sdk/command-auth-native";
import { resolveCommandAuthorizedFromAuthorizers } from "openclaw/plugin-sdk/command-auth-native";
import { buildCommandsMessagePaginated } from "openclaw/plugin-sdk/command-status";
import { replaceConfigFile } from "openclaw/plugin-sdk/config-mutation";
import type { DmPolicy, OpenClawConfig } from "openclaw/plugin-sdk/config-types";
@@ -34,12 +31,16 @@ import {
resolveSessionStoreEntry,
updateSessionStore,
} from "openclaw/plugin-sdk/session-store-runtime";
import { expandTelegramAllowFromWithAccessGroups } from "./access-groups.js";
import {
expandTelegramAllowFromWithAccessGroups,
resolveTelegramDmAllow,
} from "./access-groups.js";
import { resolveTelegramAccount, resolveTelegramMediaRuntimeOptions } from "./accounts.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
import {
isSenderAllowed,
normalizeDmAllowFromWithStore,
resolveTelegramEffectiveDmPolicy,
type NormalizedAllowFrom,
} from "./bot-access.js";
import {
@@ -71,9 +72,10 @@ import {
import { resolveMedia } from "./bot/delivery.resolve-media.js";
import {
getTelegramTextParts,
buildTelegramGroupFrom,
buildTelegramGroupPeerId,
buildTelegramParentPeer,
isTelegramCommandsAllowFromConfigured,
resolveTelegramCommandAuthorization,
resolveTelegramForumFlag,
resolveTelegramForumThreadId,
resolveTelegramGroupAllowFromContext,
@@ -728,13 +730,11 @@ export const registerTelegramHandlers = ({
readChannelAllowFromStore: telegramDeps.readChannelAllowFromStore,
resolveTelegramGroupConfig,
}));
// 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");
const effectiveDmPolicy = resolveTelegramEffectiveDmPolicy({
isGroup: params.isGroup,
groupConfig: groupAllowContext.groupConfig,
dmPolicy: telegramCfg.dmPolicy,
});
return { dmPolicy: effectiveDmPolicy, ...groupAllowContext };
};
@@ -831,27 +831,15 @@ export const registerTelegramHandlers = ({
const { chatId, isGroup, senderId, senderUsername, context, cfg } = params;
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
const dmAllowFrom = context.groupAllowOverride ?? allowFrom;
const commandsAllowFrom = cfg.commands?.allowFrom;
const commandsAllowFromConfigured =
commandsAllowFrom != null &&
typeof commandsAllowFrom === "object" &&
(Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"]));
if (commandsAllowFromConfigured) {
return resolveCommandAuthorization({
ctx: {
Provider: "telegram",
Surface: "telegram",
OriginatingChannel: "telegram",
AccountId: accountId,
ChatType: isGroup ? "group" : "direct",
From: isGroup
? buildTelegramGroupFrom(chatId, context.resolvedThreadId)
: `telegram:${chatId}`,
SenderId: senderId || undefined,
SenderUsername: senderUsername || undefined,
},
if (isTelegramCommandsAllowFromConfigured(cfg)) {
return resolveTelegramCommandAuthorization({
cfg,
commandAuthorized: false,
accountId,
chatId,
isGroup,
resolvedThreadId: context.resolvedThreadId,
senderId,
senderUsername,
}).isAuthorizedSender;
}
@@ -896,7 +884,6 @@ export const registerTelegramHandlers = ({
});
};
// Handle emoji reactions to messages.
bot.on("message_reaction", async (ctx) => {
try {
const reaction = ctx.messageReaction;
@@ -915,7 +902,6 @@ export const registerTelegramHandlers = ({
const isGroup = reaction.chat.type === "group" || reaction.chat.type === "supergroup";
const isForum = reaction.chat.is_forum === true;
// Resolve reaction notification mode (default: "own").
const reactionMode = telegramCfg.reactionNotifications ?? "own";
if (reactionMode === "off") {
return;
@@ -963,7 +949,6 @@ export const registerTelegramHandlers = ({
}
}
// Detect added reactions.
const oldEmojis = new Set(
reaction.old_reaction
.filter((r): r is ReactionTypeEmoji => r.type === "emoji")
@@ -977,7 +962,6 @@ export const registerTelegramHandlers = ({
return;
}
// Build sender label.
const senderName = user
? [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username
: undefined;
@@ -1001,7 +985,6 @@ export const registerTelegramHandlers = ({
: undefined;
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.
const route = resolveAgentRoute({
cfg: telegramDeps.getRuntimeConfig(),
channel: "telegram",
@@ -1011,7 +994,6 @@ export const registerTelegramHandlers = ({
});
const sessionKey = route.sessionKey;
// Enqueue system event for each added reaction.
for (const r of addedReactions) {
const emoji = r.emoji;
const text = `Telegram reaction added: ${emoji} by ${senderLabel} on msg ${messageId}`;
@@ -1047,14 +1029,11 @@ export const registerTelegramHandlers = ({
oversizeLogMessage,
} = params;
// Text fragment handling - Telegram splits long pastes into multiple inbound messages (~4096 chars).
// We buffer “near-limit” messages and append immediately-following parts.
const text = typeof msg.text === "string" ? msg.text : undefined;
const isCommandLike = (text ?? "").trim().startsWith("/");
if (text && !isCommandLike) {
const nowMs = Date.now();
const senderId = msg.from?.id != null ? String(msg.from.id) : "unknown";
// 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);
@@ -1087,7 +1066,6 @@ export const registerTelegramHandlers = ({
}
}
// Not appendable (or limits exceeded): flush buffered entry first, then continue normally.
clearTimeout(existing.timer);
textFragmentBuffer.delete(key);
textFragmentProcessing = textFragmentProcessing
@@ -1111,7 +1089,6 @@ export const registerTelegramHandlers = ({
}
}
// Media group handling - buffer multi-image messages
const mediaGroupId = msg.media_group_id;
if (mediaGroupId) {
const existing = mediaGroupBuffer.get(mediaGroupId);
@@ -1186,8 +1163,6 @@ export const registerTelegramHandlers = ({
return;
}
// Skip sticker-only messages where the sticker was skipped (animated/video)
// These have no media and no text content to process.
const hasText = Boolean(getTelegramTextParts(msg).text.trim());
if (msg.sticker && !media && !hasText) {
logVerbose("telegram: skipping sticker-only message (unsupported sticker type)");
@@ -1240,7 +1215,6 @@ export const registerTelegramHandlers = ({
typeof (ctx as { answerCallbackQuery?: unknown }).answerCallbackQuery === "function"
? () => ctx.answerCallbackQuery()
: () => bot.api.answerCallbackQuery(callback.id);
// Answer immediately to prevent Telegram from retrying while we process
await withTelegramApiErrorLogging({
operation: "answerCallbackQuery",
runtime,
@@ -1573,7 +1547,6 @@ export const registerTelegramHandlers = ({
return;
}
// Model selection callback handler (mdl_prov, mdl_list_*, mdl_sel_*, mdl_back)
const modelCallback = parseModelCallbackData(data);
if (modelCallback) {
if (
@@ -1660,7 +1633,6 @@ export const registerTelegramHandlers = ({
const { provider, page } = modelCallback;
const modelSet = byProvider.get(provider);
if (!modelSet || modelSet.size === 0) {
// Provider not found or no models - show providers list
const providerInfos: ProviderInfo[] = providers.map((p) => ({
id: p,
count: byProvider.get(p)?.size ?? 0,
@@ -1681,7 +1653,6 @@ export const registerTelegramHandlers = ({
const totalPages = calculateTotalPages(models.length, pageSize);
const safePage = Math.max(1, Math.min(page, totalPages));
// Resolve current model from session (prefer overrides)
const currentModel = sessionState.model;
const buttons = buildModelsKeyboard({
@@ -1744,7 +1715,6 @@ export const registerTelegramHandlers = ({
return;
}
// Directly set model override in session
try {
// Use the fresh runtimeCfg (loaded at callback entry) so store path
// and default-model resolution stay consistent with the next
@@ -1782,7 +1752,6 @@ export const registerTelegramHandlers = ({
throw new TelegramRetryableCallbackError(err);
}
// Update message to show success with visual feedback
const escapeHtml = (text: string) =>
text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const actionText = isDefaultSelection
@@ -1832,7 +1801,6 @@ export const registerTelegramHandlers = ({
}
});
// Handle group migration to supergroup (chat ID changes)
bot.on("message:migrate_to_chat_id", async (ctx) => {
try {
const msg = ctx.message;
@@ -1854,7 +1822,6 @@ export const registerTelegramHandlers = ({
return;
}
// Check if old chat ID has config and migrate it
const currentConfig = telegramDeps.getRuntimeConfig();
const migration = migrateTelegramGroupConfig({
cfg: currentConfig,
@@ -1927,16 +1894,12 @@ export const registerTelegramHandlers = ({
effectiveGroupAllow,
hasGroupAllowOverride,
} = eventAuthContext;
// For DMs, prefer per-DM/topic allowFrom (groupAllowOverride) over account-level allowFrom
const dmAllowFrom = groupAllowOverride ?? allowFrom;
const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({
const dmAllow = await resolveTelegramDmAllow({
cfg,
allowFrom: dmAllowFrom,
groupAllowOverride,
allowFrom,
accountId,
senderId: event.senderId,
});
const effectiveDmAllow = normalizeDmAllowFromWithStore({
allowFrom: expandedDmAllowFrom,
storeAllowFrom,
dmPolicy,
});
@@ -1969,7 +1932,7 @@ export const registerTelegramHandlers = ({
dmPolicy,
msg: event.msg,
chatId: event.chatId,
effectiveDmAllow,
effectiveDmAllow: dmAllow.effectiveAllow,
accountId,
bot,
logger,
@@ -2031,9 +1994,6 @@ export const registerTelegramHandlers = ({
});
});
// Handle channel posts — enables bot-to-bot communication via Telegram channels.
// Telegram bots cannot see other bot messages in groups, but CAN in channels.
// This handler normalizes channel_post updates into the standard message pipeline.
bot.on("channel_post", async (ctx) => {
const post = ctx.channelPost;
if (!post) {

View File

@@ -8,10 +8,17 @@ import type { TelegramDirectConfig, TelegramGroupConfig } from "openclaw/plugin-
import { deriveLastRoutePolicy } from "openclaw/plugin-sdk/routing";
import { normalizeAccountId, resolveThreadSessionKeys } from "openclaw/plugin-sdk/routing";
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
import { expandTelegramAllowFromWithAccessGroups } from "./access-groups.js";
import {
expandTelegramAllowFromWithAccessGroups,
resolveTelegramDmAllow,
} from "./access-groups.js";
import { mergeTelegramAccountConfig, resolveDefaultTelegramAccountId } from "./accounts.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
import { firstDefined, normalizeAllowFrom, normalizeDmAllowFromWithStore } from "./bot-access.js";
import {
firstDefined,
normalizeAllowFrom,
resolveTelegramEffectiveDmPolicy,
} from "./bot-access.js";
import { resolveTelegramInboundBody } from "./bot-message-context.body.js";
import {
buildTelegramInboundContextPayload,
@@ -217,12 +224,11 @@ export const buildTelegramMessageContext = async ({
const telegramGroupConfig = isGroup
? (groupConfig as TelegramGroupConfig | undefined)
: undefined;
// Use direct config dmPolicy override if available for DMs
const effectiveDmPolicy =
!isGroup && groupConfig && "dmPolicy" in groupConfig
? (groupConfig.dmPolicy ?? dmPolicy)
: dmPolicy;
// Fresh config for bindings lookup; other routing inputs are payload-derived.
const effectiveDmPolicy = resolveTelegramEffectiveDmPolicy({
isGroup,
groupConfig,
dmPolicy,
});
const freshCfg =
loadFreshConfig?.() ??
(runtime?.getRuntimeConfig ?? (await loadTelegramMessageContextRuntime()).getRuntimeConfig)();
@@ -255,16 +261,16 @@ export const buildTelegramMessageContext = async ({
});
return null;
}
// 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 [expandedDmAllowFrom, expandedGroupAllowFrom] = await Promise.all([
expandTelegramAllowFromWithAccessGroups({
const [dmAllow, expandedGroupAllowFrom] = await Promise.all([
resolveTelegramDmAllow({
cfg: freshCfg,
allowFrom: dmAllowFrom,
groupAllowOverride,
allowFrom,
accountId: account.accountId,
senderId,
storeAllowFrom,
dmPolicy: effectiveDmPolicy,
}),
expandTelegramAllowFromWithAccessGroups({
cfg: freshCfg,
@@ -273,12 +279,6 @@ export const buildTelegramMessageContext = async ({
senderId,
}),
]);
const effectiveDmAllow = normalizeDmAllowFromWithStore({
allowFrom: expandedDmAllowFrom,
storeAllowFrom,
dmPolicy: effectiveDmPolicy,
});
// Group sender checks are explicit and must not inherit DM pairing-store entries.
const effectiveGroupAllow = normalizeAllowFrom(expandedGroupAllowFrom);
const hasGroupAllowOverride = groupAllowOverride !== undefined;
const senderUsername = msg.from?.username ?? "";
@@ -353,7 +353,7 @@ export const buildTelegramMessageContext = async ({
dmPolicy: effectiveDmPolicy,
msg,
chatId,
effectiveDmAllow,
effectiveDmAllow: dmAllow.effectiveAllow,
accountId: account.accountId,
bot,
logger,
@@ -417,7 +417,6 @@ export const buildTelegramMessageContext = async ({
mainSessionKey: route.mainSessionKey,
}),
};
// Compute requireMention after access checks and final route selection.
const activationOverride = resolveGroupActivation({
chatId,
messageThreadId: resolvedThreadId,
@@ -456,7 +455,7 @@ export const buildTelegramMessageContext = async ({
routeAgentId: route.agentId,
sessionKey,
effectiveGroupAllow,
effectiveDmAllow,
effectiveDmAllow: dmAllow.effectiveAllow,
groupConfig,
topicConfig,
requireMention,
@@ -473,7 +472,6 @@ export const buildTelegramMessageContext = async ({
return null;
}
// ACK reactions
const ackReaction = resolveAckReaction(cfg, route.agentId, {
channel: "telegram",
accountId: account.accountId,
@@ -494,7 +492,6 @@ export const buildTelegramMessageContext = async ({
shouldBypassMention: bodyResult.shouldBypassMention,
}),
);
// Status Reactions controller (lifecycle reactions)
const statusReactionsConfig = cfg.messages?.statusReactions;
const statusReactionsEnabled =
statusReactionsConfig?.enabled === true && Boolean(reactionApi) && shouldSendAckReaction;
@@ -546,7 +543,6 @@ export const buildTelegramMessageContext = async ({
]);
}
},
// Telegram replaces atomically — no removeReaction needed
},
initialEmoji: ackReaction,
emojis: resolvedStatusReactionEmojis ?? undefined,
@@ -557,7 +553,6 @@ export const buildTelegramMessageContext = async ({
})
: null;
// When status reactions are enabled, setQueued() replaces the simple ack reaction
const ackReactionPromise: Promise<boolean> | null = statusReactionController
? shouldSendAckReaction
? Promise.resolve(statusReactionController.setQueued()).then(
@@ -608,7 +603,7 @@ export const buildTelegramMessageContext = async ({
: {}),
locationData: bodyResult.locationData,
options,
dmAllowFrom,
dmAllowFrom: dmAllow.allowFrom,
effectiveGroupAllow,
commandAuthorized: bodyResult.commandAuthorized,
topicName,

View File

@@ -9,7 +9,6 @@ import {
} from "openclaw/plugin-sdk/agent-runtime";
import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-streaming";
import {
resolveCommandAuthorization,
resolveCommandAuthorizedFromAuthorizers,
resolveNativeCommandSessionTargets,
} from "openclaw/plugin-sdk/command-auth-native";
@@ -51,10 +50,10 @@ import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "openclaw/plugin-sdk/text-runtime";
import { expandTelegramAllowFromWithAccessGroups } from "./access-groups.js";
import { resolveTelegramDmAllow } from "./access-groups.js";
import { resolveTelegramAccount } from "./accounts.js";
import { withTelegramApiErrorLogging } from "./api-logging.js";
import { isSenderAllowed, normalizeDmAllowFromWithStore } from "./bot-access.js";
import { isSenderAllowed, resolveTelegramEffectiveDmPolicy } from "./bot-access.js";
import type { TelegramBotDeps } from "./bot-deps.js";
import type { TelegramMediaRef } from "./bot-message-context.js";
import type { TelegramMessageContextOptions } from "./bot-message-context.types.js";
@@ -75,6 +74,8 @@ import {
buildSenderName,
buildTelegramGroupFrom,
extractTelegramForumFlag,
isTelegramCommandsAllowFromConfigured,
resolveTelegramCommandAuthorization,
resolveTelegramForumFlag,
resolveTelegramGroupAllowFromContext,
resolveTelegramThreadSpec,
@@ -343,9 +344,7 @@ async function cleanupTelegramProgressPlaceholder(params: {
runtime: params.runtime,
fn: () => params.bot.api.deleteMessage(params.chatId, progressMessageId),
});
} catch {
// Best-effort cleanup before fallback or suppression exits.
}
} catch {}
}
async function resolveTelegramNativeCommandThreadContext(params: {
@@ -514,54 +513,46 @@ async function resolveTelegramCommandAuth(params: {
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 effectiveDmPolicy = resolveTelegramEffectiveDmPolicy({
isGroup,
groupConfig,
dmPolicy: telegramCfg.dmPolicy,
});
const requireTopic =
!isGroup && groupConfig && "requireTopic" in groupConfig ? groupConfig.requireTopic : undefined;
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 commandsAllowFrom = cfg.commands?.allowFrom;
const commandsAllowFromConfigured =
commandsAllowFrom != null &&
typeof commandsAllowFrom === "object" &&
(Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"]));
const dmAllow = await resolveTelegramDmAllow({
cfg,
groupAllowOverride,
allowFrom,
accountId,
senderId,
storeAllowFrom: isGroup ? [] : storeAllowFrom,
dmPolicy: effectiveDmPolicy,
});
const commandsAllowFromConfigured = isTelegramCommandsAllowFromConfigured(cfg);
const commandsAllowFromAccess = commandsAllowFromConfigured
? resolveCommandAuthorization({
ctx: {
Provider: "telegram",
Surface: "telegram",
OriginatingChannel: "telegram",
AccountId: accountId,
ChatType: isGroup ? "group" : "direct",
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
SenderId: senderId || undefined,
SenderUsername: senderUsername || undefined,
},
? resolveTelegramCommandAuthorization({
cfg,
// commands.allowFrom is the only auth source when configured.
commandAuthorized: false,
accountId,
chatId,
isGroup,
resolvedThreadId,
senderId,
senderUsername,
})
: null;
const ownerAccess = resolveCommandAuthorization({
ctx: {
Provider: "telegram",
Surface: "telegram",
OriginatingChannel: "telegram",
AccountId: accountId,
ChatType: isGroup ? "group" : "direct",
From: isGroup ? buildTelegramGroupFrom(chatId, resolvedThreadId) : `telegram:${chatId}`,
SenderId: senderId || undefined,
SenderUsername: senderUsername || undefined,
},
const ownerAccess = resolveTelegramCommandAuthorization({
cfg,
commandAuthorized: false,
accountId,
chatId,
isGroup,
resolvedThreadId,
senderId,
senderUsername,
});
const sendAuthMessage = async (text: string) => {
@@ -629,19 +620,8 @@ async function resolveTelegramCommandAuth(params: {
}
}
const expandedDmAllowFrom = await expandTelegramAllowFromWithAccessGroups({
cfg,
allowFrom: dmAllowFrom,
accountId,
senderId,
});
const dmAllow = normalizeDmAllowFromWithStore({
allowFrom: expandedDmAllowFrom,
storeAllowFrom: isGroup ? [] : storeAllowFrom,
dmPolicy: effectiveDmPolicy,
});
const senderAllowed = isSenderAllowed({
allow: dmAllow,
allow: dmAllow.effectiveAllow,
senderId,
senderUsername,
});
@@ -654,7 +634,7 @@ async function resolveTelegramCommandAuth(params: {
: resolveCommandAuthorizedFromAuthorizers({
useAccessGroups,
authorizers: [
{ configured: dmAllow.hasEntries, allowed: senderAllowed },
{ configured: dmAllow.effectiveAllow.hasEntries, allowed: senderAllowed },
...(isGroup
? [{ configured: effectiveGroupAllow.hasEntries, allowed: groupSenderAllowed }]
: []),
@@ -1168,7 +1148,6 @@ export const registerTelegramNativeCommands = ({
CommandTargetSessionKey: commandTargetSessionKey,
MessageThreadId: threadSpec.id,
IsForum: isForum,
// Originating context for sub-agent announce routing
OriginatingChannel: "telegram" as const,
OriginatingTo: originatingTo,
});
@@ -1348,9 +1327,7 @@ export const registerTelegramNativeCommands = ({
if (typeof maybeMessageId === "number") {
progressMessageId = maybeMessageId;
}
} catch {
// Fall back to the normal final reply path if the placeholder send fails.
}
} catch {}
}
const sessionFileContext = await resolveTelegramCommandSessionFile({
@@ -1430,9 +1407,7 @@ export const registerTelegramNativeCommands = ({
groupId: isGroup ? String(chatId) : undefined,
});
return;
} catch {
// Fall through to cleanup + normal delivered reply if editing fails.
}
} catch {}
}
await cleanupTelegramProgressPlaceholder({
bot,

View File

@@ -1,5 +1,9 @@
import type { Chat, Message } from "@grammyjs/types";
import { formatLocationText } from "openclaw/plugin-sdk/channel-inbound";
import {
resolveCommandAuthorization,
type CommandAuthorization,
} from "openclaw/plugin-sdk/command-auth-native";
import type {
OpenClawConfig,
TelegramAccountConfig,
@@ -380,6 +384,42 @@ export function buildTelegramGroupFrom(chatId: number | string, messageThreadId?
return `telegram:group:${buildTelegramGroupPeerId(chatId, messageThreadId)}`;
}
export function isTelegramCommandsAllowFromConfigured(cfg: OpenClawConfig): boolean {
const commandsAllowFrom = cfg.commands?.allowFrom;
return (
commandsAllowFrom != null &&
typeof commandsAllowFrom === "object" &&
(Array.isArray(commandsAllowFrom.telegram) || Array.isArray(commandsAllowFrom["*"]))
);
}
export function resolveTelegramCommandAuthorization(params: {
cfg: OpenClawConfig;
accountId: string;
chatId: number;
isGroup: boolean;
resolvedThreadId?: number;
senderId?: string;
senderUsername?: string;
}): CommandAuthorization {
return resolveCommandAuthorization({
ctx: {
Provider: "telegram",
Surface: "telegram",
OriginatingChannel: "telegram",
AccountId: params.accountId,
ChatType: params.isGroup ? "group" : "direct",
From: params.isGroup
? buildTelegramGroupFrom(params.chatId, params.resolvedThreadId)
: `telegram:${params.chatId}`,
SenderId: params.senderId || undefined,
SenderUsername: params.senderUsername || undefined,
},
cfg: params.cfg,
commandAuthorized: false,
});
}
/**
* Build parentPeer for forum topic binding inheritance.
* When a message comes from a forum topic, the peer ID includes the topic suffix