mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(telegram): preserve DM topic authorization (#121117)
Carry canonical direct_messages_topic identity through authorization and config selection so channel DM topics use the same scope as delivery. Keep that identity distinct from message_thread_id.
This commit is contained in:
committed by
GitHub
parent
8fdf7570a1
commit
01cf584d2a
@@ -20,6 +20,8 @@ import {
|
||||
isTelegramCommandsAllowFromConfigured,
|
||||
resolveTelegramCommandAuthorization,
|
||||
resolveTelegramGroupAllowFromContext,
|
||||
resolveTelegramMessageThreadSpec,
|
||||
type TelegramThreadSpec,
|
||||
} from "./bot/helpers.js";
|
||||
import { enforceTelegramDmAccess, isTelegramDmAccessAllowed } from "./dm-access.js";
|
||||
import {
|
||||
@@ -95,9 +97,8 @@ export function createTelegramHandlerAuthorizationRuntime({
|
||||
cfg: OpenClawConfig;
|
||||
chatId: number;
|
||||
isGroup: boolean;
|
||||
isForum: boolean;
|
||||
senderId?: string;
|
||||
messageThreadId?: number;
|
||||
threadSpec: TelegramThreadSpec;
|
||||
}): Promise<TelegramEventAuthorizationContextValue> => {
|
||||
const authorizationCfg = params.cfg;
|
||||
const authorizationTelegramCfg = resolveTelegramAccount({
|
||||
@@ -118,8 +119,7 @@ export function createTelegramHandlerAuthorizationRuntime({
|
||||
allowFrom: authorizationSettings.allowFrom,
|
||||
senderId: params.senderId,
|
||||
isGroup: params.isGroup,
|
||||
isForum: params.isForum,
|
||||
messageThreadId: params.messageThreadId,
|
||||
threadSpec: params.threadSpec,
|
||||
groupAllowFrom: authorizationSettings.groupAllowFrom,
|
||||
readChannelAllowFromStore: telegramDeps.readChannelAllowFromStore,
|
||||
resolveTelegramGroupConfig,
|
||||
@@ -312,7 +312,6 @@ export function createTelegramHandlerAuthorizationRuntime({
|
||||
chatId: number;
|
||||
isGroup: boolean;
|
||||
isForum: boolean;
|
||||
messageThreadId?: number;
|
||||
senderId: string;
|
||||
senderUsername: string;
|
||||
requireConfiguredGroup: boolean;
|
||||
@@ -323,9 +322,8 @@ export function createTelegramHandlerAuthorizationRuntime({
|
||||
cfg: authorizationCfg,
|
||||
chatId: params.chatId,
|
||||
isGroup: params.isGroup,
|
||||
isForum: params.isForum,
|
||||
senderId: params.senderId,
|
||||
messageThreadId: params.messageThreadId,
|
||||
threadSpec: resolveTelegramMessageThreadSpec(params.msg, params.isForum),
|
||||
});
|
||||
const {
|
||||
dmPolicy,
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
isTelegramSpooledReplayUpdate,
|
||||
recordTelegramMessageProcessingResult,
|
||||
} from "./bot-processing-outcome.js";
|
||||
import { resolveTelegramForumFlag, withResolvedTelegramForumFlag } from "./bot/helpers.js";
|
||||
import {
|
||||
resolveTelegramForumFlag,
|
||||
resolveTelegramMessageThreadSpec,
|
||||
withResolvedTelegramForumFlag,
|
||||
} from "./bot/helpers.js";
|
||||
import type { TelegramGetChat } from "./bot/types.js";
|
||||
import { getTelegramCallbackQueryAnswerPromise } from "./callback-query-answer-state.js";
|
||||
import { resolveTelegramInlineButtonsScope } from "./inline-buttons.js";
|
||||
@@ -159,9 +163,8 @@ export function registerTelegramCallbackQueryHandler(
|
||||
cfg: authorizationCfg,
|
||||
chatId,
|
||||
isGroup,
|
||||
isForum,
|
||||
senderId,
|
||||
messageThreadId,
|
||||
threadSpec: resolveTelegramMessageThreadSpec(callbackMessage, isForum),
|
||||
});
|
||||
const { resolvedThreadId, dmThreadId, storeAllowFrom, groupConfig } = eventAuthContext;
|
||||
const requireTopic = (groupConfig as { requireTopic?: boolean } | undefined)?.requireTopic;
|
||||
|
||||
@@ -136,7 +136,6 @@ export function registerTelegramMessageHandlers(
|
||||
chatId: normalizedMsg.chat.id,
|
||||
isGroup,
|
||||
isForum,
|
||||
messageThreadId: normalizedMsg.message_thread_id,
|
||||
senderId: normalizedMsg.from?.id != null ? String(normalizedMsg.from.id) : "",
|
||||
senderUsername: normalizedMsg.from?.username ?? "",
|
||||
requireConfiguredGroup: params.requireConfiguredGroup,
|
||||
@@ -164,7 +163,6 @@ export function registerTelegramMessageHandlers(
|
||||
chatId: event.chatId,
|
||||
isGroup: event.isGroup,
|
||||
isForum: event.isForum,
|
||||
messageThreadId: event.messageThreadId,
|
||||
senderId: event.senderId,
|
||||
senderUsername: event.senderUsername,
|
||||
requireConfiguredGroup: event.requireConfiguredGroup,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
type TelegramSpooledReplayDeferredParticipant,
|
||||
} from "./bot-processing-outcome.js";
|
||||
import { resolveMedia } from "./bot/delivery.resolve-media.js";
|
||||
import { resolveTelegramForumThreadId } from "./bot/helpers.js";
|
||||
import { resolveTelegramMessageThreadSpec } from "./bot/helpers.js";
|
||||
import type { TelegramContext } from "./bot/types.js";
|
||||
import { resolveTelegramScopedGroupConfig } from "./group-config-helpers.js";
|
||||
import type { TelegramCachedMessageNode, TelegramReplyChainEntry } from "./message-cache.js";
|
||||
@@ -272,13 +272,7 @@ export function createTelegramHandlerMessageRuntime({
|
||||
const replyChainNodes = await buildReplyChainForMessage(params.msg);
|
||||
const isGroupConversation =
|
||||
params.msg.chat.type === "group" || params.msg.chat.type === "supergroup";
|
||||
const isForum =
|
||||
params.msg.chat.type === "supergroup" &&
|
||||
Boolean(params.msg.chat.is_forum || params.msg.is_topic_message);
|
||||
const scopedThreadId = resolveTelegramForumThreadId({
|
||||
isForum,
|
||||
messageThreadId: params.msg.message_thread_id,
|
||||
});
|
||||
const scopedThreadId = resolveTelegramMessageThreadSpec(params.msg).id;
|
||||
const { groupConfig, topicConfig } = resolveTelegramScopedGroupConfig(
|
||||
runtimeTelegramCfg,
|
||||
params.msg.chat.id,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isTelegramSpooledReplayUpdate,
|
||||
recordTelegramMessageProcessingResult,
|
||||
} from "./bot-processing-outcome.js";
|
||||
import { resolveTelegramThreadSpec } from "./bot/helpers.js";
|
||||
import { getPreparedTelegramPollAnswer } from "./poll-answer-context.js";
|
||||
import { findTelegramPollRegistryEntry, retireTelegramPollRegistryEntry } from "./poll-registry.js";
|
||||
|
||||
@@ -95,9 +96,12 @@ export function registerTelegramPollHandlers(
|
||||
cfg: authorizationCfg,
|
||||
chatId,
|
||||
isGroup,
|
||||
isForum,
|
||||
senderId,
|
||||
messageThreadId: entry.messageThreadId,
|
||||
threadSpec: resolveTelegramThreadSpec({
|
||||
isGroup,
|
||||
isForum,
|
||||
messageThreadId: entry.messageThreadId,
|
||||
}),
|
||||
});
|
||||
const senderAuthorization = await authorizeTelegramEventSender({
|
||||
chatId,
|
||||
|
||||
@@ -6,7 +6,11 @@ import { resolveTelegramAccount } from "./accounts.js";
|
||||
import type { TelegramHandlerAuthorizationRuntime } from "./bot-handlers.authorization.runtime.js";
|
||||
import type { TelegramHandlerMessageRuntime } from "./bot-handlers.message.runtime.js";
|
||||
import type { RegisterTelegramHandlerParams } from "./bot-native-commands.js";
|
||||
import { buildTelegramGroupPeerId, buildTelegramParentPeer } from "./bot/helpers.js";
|
||||
import {
|
||||
buildTelegramGroupPeerId,
|
||||
buildTelegramParentPeer,
|
||||
resolveTelegramThreadSpec,
|
||||
} from "./bot/helpers.js";
|
||||
import { resolveTelegramConversationRoute } from "./conversation-route.js";
|
||||
|
||||
/** Stable operator-facing reason for a forum reaction dropped without a known topic. */
|
||||
@@ -108,9 +112,12 @@ export function registerTelegramReactionHandler(
|
||||
cfg: authorizationCfg,
|
||||
chatId,
|
||||
isGroup,
|
||||
isForum,
|
||||
senderId,
|
||||
...(cachedForumThreadId === undefined ? {} : { messageThreadId: cachedForumThreadId }),
|
||||
threadSpec: resolveTelegramThreadSpec({
|
||||
isGroup,
|
||||
isForum,
|
||||
messageThreadId: cachedForumThreadId,
|
||||
}),
|
||||
});
|
||||
const senderAuthorization = await authorizeTelegramEventSender({
|
||||
chatId,
|
||||
|
||||
@@ -3799,6 +3799,122 @@ describe("createTelegramBot", () => {
|
||||
expect(replySpy.mock.calls.at(1)?.[0].SessionKey).toContain("thread:124:99");
|
||||
});
|
||||
|
||||
it("authorizes and routes channel-DM messages with the canonical topic identity", async () => {
|
||||
const chatId = -100123456700;
|
||||
loadConfig.mockReturnValue({
|
||||
agents: { list: [{ id: "channel-topic-agent" }] },
|
||||
channels: {
|
||||
telegram: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["701"],
|
||||
groups: {
|
||||
[String(chatId)]: {
|
||||
allowFrom: ["701"],
|
||||
requireMention: false,
|
||||
topics: {
|
||||
"77": {
|
||||
agentId: "channel-topic-agent",
|
||||
allowFrom: ["700"],
|
||||
requireMention: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await dispatchMessage({
|
||||
me: { id: 999, username: "openclaw_bot" },
|
||||
message: {
|
||||
chat: {
|
||||
id: chatId,
|
||||
type: "supergroup",
|
||||
title: "Channel Inbox",
|
||||
is_direct_messages: true,
|
||||
},
|
||||
from: { id: 700, first_name: "Ada" },
|
||||
text: "route this topic",
|
||||
date: 1736380800,
|
||||
message_id: 7700,
|
||||
direct_messages_topic: { topic_id: 77 },
|
||||
message_thread_id: 999,
|
||||
},
|
||||
});
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = requireValue(replySpy.mock.calls.at(0), "replySpy call")[0];
|
||||
expect(payload.MessageThreadId).toBe(77);
|
||||
expect(payload.OriginatingTo).toBe(`telegram:${chatId}:direct-topic:77`);
|
||||
expect(payload.SessionKey).toContain("agent:channel-topic-agent:");
|
||||
expect(payload.SessionKey).toContain(":topic:77");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "topic allows and base chat denies",
|
||||
baseAllowFrom: ["701"],
|
||||
topicAllowFrom: ["700"],
|
||||
expectedCalls: 1,
|
||||
},
|
||||
{
|
||||
name: "topic denies and base chat allows",
|
||||
baseAllowFrom: ["700"],
|
||||
topicAllowFrom: ["701"],
|
||||
expectedCalls: 0,
|
||||
},
|
||||
])("authorizes channel-DM callbacks from the canonical topic: $name", async (testCase) => {
|
||||
const chatId = -100123456701;
|
||||
const pluginHandler = vi.fn(async () => ({ handled: true }));
|
||||
expect(
|
||||
registerPluginInteractiveHandler("channel-topic-actions", {
|
||||
channel: "telegram",
|
||||
namespace: "channel-topic",
|
||||
handler: pluginHandler,
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
telegram: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: testCase.baseAllowFrom,
|
||||
groups: {
|
||||
[String(chatId)]: {
|
||||
allowFrom: testCase.baseAllowFrom,
|
||||
topics: { "77": { allowFrom: testCase.topicAllowFrom } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
createTelegramBot({ token: "tok" });
|
||||
await getCallbackHandler()({
|
||||
callbackQuery: {
|
||||
id: `channel-topic-${testCase.expectedCalls}`,
|
||||
data: "channel-topic:run",
|
||||
from: { id: 700, first_name: "Ada" },
|
||||
message: {
|
||||
chat: {
|
||||
id: chatId,
|
||||
type: "supergroup",
|
||||
title: "Channel Inbox",
|
||||
is_direct_messages: true,
|
||||
},
|
||||
date: 1736380800,
|
||||
message_id: 7701,
|
||||
direct_messages_topic: { topic_id: 77 },
|
||||
message_thread_id: 999,
|
||||
},
|
||||
},
|
||||
me: { id: 999, username: "openclaw_bot" },
|
||||
getFile: async () => ({ download: async () => new Uint8Array() }),
|
||||
});
|
||||
|
||||
expect(pluginHandler).toHaveBeenCalledTimes(testCase.expectedCalls);
|
||||
clearPluginInteractiveHandlers();
|
||||
});
|
||||
|
||||
it("routes non-default account DMs to the per-account fallback session without explicit bindings", async () => {
|
||||
loadConfig.mockReturnValue({
|
||||
channels: {
|
||||
|
||||
@@ -4660,6 +4660,94 @@ describe("createTelegramBot", () => {
|
||||
expect(mediaFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses refreshed channel-DM topic config for reply-media visibility", async () => {
|
||||
mockTelegramConfig({
|
||||
groupPolicy: "allowlist",
|
||||
contextVisibility: "allowlist",
|
||||
groups: {
|
||||
"-1010": {
|
||||
requireMention: false,
|
||||
allowFrom: ["1", "2"],
|
||||
topics: { "77": { allowFrom: ["1"], requireMention: false } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mediaFetch = vi.fn(
|
||||
async () =>
|
||||
new Response(new Uint8Array([0x89, 0x50, 0x4e, 0x47]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "image/png" },
|
||||
}),
|
||||
);
|
||||
const ssrfMock = mockPinnedHostnameResolution();
|
||||
setTelegramPluginStateRuntimeForTests();
|
||||
|
||||
try {
|
||||
const replyDelivered = waitForReplyCalls(1);
|
||||
createTelegramBot({
|
||||
token: "tok",
|
||||
telegramTransport: makeTelegramTransport(mediaFetch as typeof fetch),
|
||||
});
|
||||
const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
|
||||
const chat = {
|
||||
id: -1010,
|
||||
type: "supergroup",
|
||||
title: "Channel Inbox",
|
||||
is_direct_messages: true,
|
||||
};
|
||||
|
||||
await handler({
|
||||
me: { id: 999, username: "openclaw_bot" },
|
||||
getFile: getEmptyTelegramFile,
|
||||
message: {
|
||||
chat,
|
||||
message_id: 103,
|
||||
text: "explain this",
|
||||
date: 1736380800,
|
||||
from: { id: 1, is_bot: false, first_name: "Allowed" },
|
||||
direct_messages_topic: { topic_id: 77 },
|
||||
message_thread_id: 999,
|
||||
reply_to_message: {
|
||||
chat,
|
||||
message_id: 102,
|
||||
caption: "hidden image",
|
||||
date: 1736380750,
|
||||
from: { id: 2, is_bot: false, first_name: "Hidden" },
|
||||
photo: [{ file_id: "hidden-channel-photo-1" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
await replyDelivered;
|
||||
} finally {
|
||||
ssrfMock.mockRestore();
|
||||
clearTelegramRuntime();
|
||||
resetPluginStateStoreForTests();
|
||||
}
|
||||
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
const payload = mockMsgContextArg(
|
||||
replySpy as unknown as MockCallSource,
|
||||
0,
|
||||
0,
|
||||
"replySpy call",
|
||||
) as { ChannelStructuredContext?: unknown[] };
|
||||
const [conversationContext] = requireArray(
|
||||
payload.ChannelStructuredContext,
|
||||
"structured context",
|
||||
);
|
||||
const contextRecord = requireRecord(conversationContext, "conversation context");
|
||||
const contextPayload = requireRecord(contextRecord.payload, "conversation context payload");
|
||||
const messages = requireArray(contextPayload.messages, "conversation context messages").map(
|
||||
(message, index) => requireRecord(message, `conversation context message ${index + 1}`),
|
||||
);
|
||||
const hiddenMessage = messages.find((message) => message.message_id === "102");
|
||||
expect(hiddenMessage?.media_ref).toBe("telegram:file/hidden-channel-photo-1");
|
||||
expect(hiddenMessage?.media_path).toBeUndefined();
|
||||
expect(getFileSpy).not.toHaveBeenCalled();
|
||||
expect(mediaFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "hydrates group reply media allowed through an option-level access group",
|
||||
|
||||
@@ -183,6 +183,43 @@ describe("telegram message cache", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps an authoritative supplied thread ahead of conflicting root message metadata", async () => {
|
||||
const { bucketKey, entries, store } = createMemoryStore();
|
||||
const forum = { id: -1001, type: "supergroup", title: "QA", is_forum: true };
|
||||
const ancestor = message(904, "Lin", {
|
||||
chat: forum,
|
||||
text: "Ancestor without its own thread metadata",
|
||||
});
|
||||
const reply = message(905, "Ada", {
|
||||
chat: forum,
|
||||
text: "Reply with embedded thread metadata",
|
||||
message_thread_id: 88,
|
||||
reply_to_message: ancestor,
|
||||
});
|
||||
const root = message(906, "Grace", {
|
||||
chat: forum,
|
||||
text: "Authoritative root",
|
||||
message_thread_id: 999,
|
||||
reply_to_message: reply,
|
||||
});
|
||||
const cache = cacheFor(bucketKey, store);
|
||||
|
||||
const recorded = await record(cache, root, {
|
||||
chatId: -1001,
|
||||
threadId: 77,
|
||||
providerObservedThreadId: 77,
|
||||
});
|
||||
|
||||
expect(recorded.threadId).toBe("77");
|
||||
expect(resolveProviderObservedTelegramThreadId(recorded)).toBe(77);
|
||||
expect((await get(cache, "905", { chatId: -1001 }))?.threadId).toBe("88");
|
||||
expect((await get(cache, "904", { chatId: -1001 }))?.threadId).toBe("88");
|
||||
const persistedRoot = Array.from(entries.values()).find(
|
||||
(entry) => entry.sourceMessage.message_id === 906,
|
||||
);
|
||||
expect(persistedRoot?.threadId).toBe("77");
|
||||
});
|
||||
|
||||
it("does not resolve caller-only topic metadata as a provider-observed binding", async () => {
|
||||
const cache = createTelegramMessageCache();
|
||||
await record(
|
||||
|
||||
@@ -286,11 +286,15 @@ function normalizeMessageNodes(
|
||||
promptContextProjectionMarker?: TelegramPromptContextProjectionMarker,
|
||||
threadBinding?: TelegramMessageThreadBinding,
|
||||
) => {
|
||||
const embeddedThreadId = parseTelegramMessageThreadId(
|
||||
(message as { message_thread_id?: unknown }).message_thread_id,
|
||||
);
|
||||
const inheritedThread = parseTelegramMessageThreadId(inheritedThreadId);
|
||||
const node = normalizeMessageNode(message, {
|
||||
threadId:
|
||||
parseTelegramMessageThreadId(
|
||||
(message as { message_thread_id?: unknown }).message_thread_id,
|
||||
) ?? inheritedThreadId,
|
||||
mode === "authoritative"
|
||||
? (inheritedThread ?? embeddedThreadId)
|
||||
: (embeddedThreadId ?? inheritedThread),
|
||||
...(promptContextProjectionMarker ? { promptContextProjectionMarker } : {}),
|
||||
...(threadBinding ? { threadBinding } : {}),
|
||||
});
|
||||
|
||||
@@ -5,8 +5,9 @@ import { buildTelegramApprovalCallbackData } from "./approval-callback-data.js";
|
||||
import { buildTelegramQuestionCallbackData } from "./question-callback-data.js";
|
||||
import { getTelegramSequentialConstraints, getTelegramSequentialKey } from "./sequential-key.js";
|
||||
|
||||
const mockChat = (chat: Pick<Chat, "id"> & Partial<Pick<Chat, "type" | "is_forum">>): Chat =>
|
||||
chat as Chat;
|
||||
const mockChat = (
|
||||
chat: Pick<Chat, "id"> & Partial<Pick<Chat, "type" | "is_forum" | "is_direct_messages">>,
|
||||
): Chat => chat as Chat;
|
||||
const mockMessage = (message: Pick<Message, "chat"> & Partial<Message>): Message =>
|
||||
({
|
||||
message_id: 1,
|
||||
@@ -55,6 +56,19 @@ describe("getTelegramSequentialKey", () => {
|
||||
},
|
||||
"telegram:123",
|
||||
],
|
||||
[
|
||||
{
|
||||
message: mockMessage({
|
||||
chat: mockChat({ id: -100123, type: "supergroup", is_direct_messages: true }),
|
||||
direct_messages_topic: {
|
||||
topic_id: 77,
|
||||
user: { id: 700, is_bot: false, first_name: "Ada" },
|
||||
},
|
||||
message_thread_id: 999,
|
||||
}),
|
||||
},
|
||||
"telegram:-100123:topic:77",
|
||||
],
|
||||
[
|
||||
{
|
||||
message: mockMessage({
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
resolveTelegramBotHasTopicsEnabled,
|
||||
resolveTelegramForumThreadId,
|
||||
resolveTelegramMessageForumFlagHint,
|
||||
resolveTelegramMessageThreadSpec,
|
||||
shouldUseTelegramDmThreadSession,
|
||||
} from "./bot/helpers.js";
|
||||
import { getPreparedTelegramPollAnswer } from "./poll-answer-context.js";
|
||||
@@ -246,21 +247,16 @@ export function getTelegramSequentialKey(ctx: TelegramSequentialKeyContext): str
|
||||
}
|
||||
return "telegram:approval";
|
||||
}
|
||||
const isGroup = msg?.chat?.type === "group" || msg?.chat?.type === "supergroup";
|
||||
const messageThreadId = msg?.message_thread_id;
|
||||
const isForum = resolveTelegramMessageForumFlagHint({
|
||||
chatType: msg?.chat?.type,
|
||||
isForum: msg?.chat?.is_forum,
|
||||
isTopicMessage: msg?.is_topic_message,
|
||||
});
|
||||
const threadId = isGroup
|
||||
? resolveTelegramForumThreadId({ isForum, messageThreadId })
|
||||
: shouldUseTelegramDmThreadSession({
|
||||
dmThreadId: messageThreadId,
|
||||
const threadSpec = msg ? resolveTelegramMessageThreadSpec(msg) : undefined;
|
||||
const threadId =
|
||||
threadSpec?.scope === "dm"
|
||||
? shouldUseTelegramDmThreadSession({
|
||||
dmThreadId: threadSpec.id,
|
||||
botHasTopicsEnabled: resolveTelegramBotHasTopicsEnabled(ctx.me),
|
||||
})
|
||||
? messageThreadId
|
||||
: undefined;
|
||||
? threadSpec.id
|
||||
: undefined
|
||||
: threadSpec?.id;
|
||||
if (typeof chatId === "number") {
|
||||
return threadId != null ? `telegram:${chatId}:topic:${threadId}` : `telegram:${chatId}`;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Telegram plugin module owns supersede sender authorization policy.
|
||||
import type { Message } from "grammy/types";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { resolveTelegramDmAllow } from "./access-groups.js";
|
||||
import { mergeTelegramAccountConfig } from "./account-config.js";
|
||||
import {
|
||||
resolveTelegramCommandAuthorization,
|
||||
resolveTelegramGroupAllowFromContext,
|
||||
resolveTelegramMessageForumFlagHint,
|
||||
resolveTelegramMessageThreadSpec,
|
||||
} from "./bot/helpers.js";
|
||||
import { resolveTelegramScopedGroupConfig } from "./group-config-helpers.js";
|
||||
import { resolveTelegramCommandIngressAuthorization } from "./ingress.js";
|
||||
@@ -14,11 +15,8 @@ type UpdateSenderFacts = {
|
||||
senderId: string;
|
||||
senderUsername?: string;
|
||||
chatId: number;
|
||||
chatType?: string;
|
||||
isGroup: boolean;
|
||||
isTopicMessage?: boolean;
|
||||
isForum?: boolean;
|
||||
messageThreadId?: number;
|
||||
message: Message;
|
||||
};
|
||||
|
||||
function extractUpdateSenderFacts(update: unknown): UpdateSenderFacts | null {
|
||||
@@ -49,19 +47,8 @@ function extractUpdateSenderFacts(update: unknown): UpdateSenderFacts | null {
|
||||
senderId: String(fromObj.id),
|
||||
...(typeof fromObj.username === "string" ? { senderUsername: fromObj.username } : {}),
|
||||
chatId: chat.id,
|
||||
chatType,
|
||||
isGroup: chatType !== "private",
|
||||
...(typeof chat.is_forum === "boolean" ? { isForum: chat.is_forum } : {}),
|
||||
...(typeof (msg as { is_topic_message?: unknown }).is_topic_message === "boolean"
|
||||
? {
|
||||
isTopicMessage: (msg as { is_topic_message: boolean }).is_topic_message,
|
||||
}
|
||||
: {}),
|
||||
...(typeof (msg as { message_thread_id?: unknown }).message_thread_id === "number"
|
||||
? {
|
||||
messageThreadId: (msg as { message_thread_id: number }).message_thread_id,
|
||||
}
|
||||
: {}),
|
||||
message: msg as Message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -78,15 +65,8 @@ function extractUpdateSenderFacts(update: unknown): UpdateSenderFacts | null {
|
||||
senderId: String(from.id),
|
||||
...(typeof from.username === "string" ? { senderUsername: from.username } : {}),
|
||||
chatId: chat.id,
|
||||
chatType,
|
||||
isGroup: chatType !== "private",
|
||||
...(typeof chat.is_forum === "boolean" ? { isForum: chat.is_forum } : {}),
|
||||
...(typeof message.is_topic_message === "boolean"
|
||||
? { isTopicMessage: message.is_topic_message as boolean }
|
||||
: {}),
|
||||
...(typeof message.message_thread_id === "number"
|
||||
? { messageThreadId: message.message_thread_id as number }
|
||||
: {}),
|
||||
message: message as unknown as Message,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,13 +112,6 @@ export async function isTelegramSpooledUpdateSenderAuthorized(
|
||||
const dmPolicy = accountCfg.dmPolicy ?? "pairing";
|
||||
const allowFrom = accountCfg.allowFrom;
|
||||
const groupAllowFrom = accountCfg.groupAllowFrom ?? accountCfg.allowFrom;
|
||||
const isForum =
|
||||
resolveTelegramMessageForumFlagHint({
|
||||
chatType: facts.chatType as "private" | "group" | "supergroup" | "channel" | undefined,
|
||||
isForum: facts.isForum,
|
||||
isTopicMessage: facts.isTopicMessage,
|
||||
}) ?? false;
|
||||
|
||||
const groupAllowContext = await resolveTelegramGroupAllowFromContext({
|
||||
cfg: auth.cfg,
|
||||
chatId: facts.chatId,
|
||||
@@ -147,8 +120,7 @@ export async function isTelegramSpooledUpdateSenderAuthorized(
|
||||
allowFrom,
|
||||
senderId: facts.senderId,
|
||||
isGroup: facts.isGroup,
|
||||
isForum,
|
||||
messageThreadId: facts.messageThreadId,
|
||||
threadSpec: resolveTelegramMessageThreadSpec(facts.message),
|
||||
groupAllowFrom,
|
||||
resolveTelegramGroupConfig: (chatId, messageThreadId, cfg) => {
|
||||
const telegramCfg = mergeTelegramAccountConfig(cfg, auth.accountId);
|
||||
|
||||
@@ -44,6 +44,8 @@ function messageUpdate(params: {
|
||||
messageThreadId?: number;
|
||||
isTopicMessage?: boolean;
|
||||
isForum?: boolean;
|
||||
isDirectMessages?: boolean;
|
||||
directMessagesTopicId?: number;
|
||||
entities?: Array<{ type: string; offset: number; length: number }>;
|
||||
}) {
|
||||
return {
|
||||
@@ -55,11 +57,17 @@ function messageUpdate(params: {
|
||||
id: params.chatId ?? Number(params.senderId),
|
||||
type: params.chatType ?? "private",
|
||||
...(params.isForum !== undefined ? { is_forum: params.isForum } : {}),
|
||||
...(params.isDirectMessages !== undefined
|
||||
? { is_direct_messages: params.isDirectMessages }
|
||||
: {}),
|
||||
},
|
||||
...(params.messageThreadId !== undefined
|
||||
? { message_thread_id: params.messageThreadId }
|
||||
: {}),
|
||||
...(params.isTopicMessage !== undefined ? { is_topic_message: params.isTopicMessage } : {}),
|
||||
...(params.directMessagesTopicId !== undefined
|
||||
? { direct_messages_topic: { topic_id: params.directMessagesTopicId } }
|
||||
: {}),
|
||||
...(params.entities ? { entities: params.entities } : {}),
|
||||
},
|
||||
};
|
||||
@@ -365,6 +373,56 @@ describe("telegram ingress supersede policy", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "allows the topic sender when the base chat denies them",
|
||||
baseAllowFrom: [STRANGER_ID],
|
||||
topicAllowFrom: [OWNER_ID],
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "denies the topic sender when the base chat allows them",
|
||||
baseAllowFrom: [OWNER_ID],
|
||||
topicAllowFrom: [STRANGER_ID],
|
||||
expected: false,
|
||||
},
|
||||
])("uses channel-DM topic authorization: $name", async (testCase) => {
|
||||
const channelDmAuth = {
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: testCase.baseAllowFrom,
|
||||
groups: {
|
||||
"-1001": {
|
||||
allowFrom: testCase.baseAllowFrom,
|
||||
topics: { "77": { allowFrom: testCase.topicAllowFrom } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
accountId: "default",
|
||||
};
|
||||
const update = messageUpdate({
|
||||
updateId: 2,
|
||||
text: "stop",
|
||||
senderId: OWNER_ID,
|
||||
chatId: -1001,
|
||||
chatType: "supergroup",
|
||||
isDirectMessages: true,
|
||||
directMessagesTopicId: 77,
|
||||
messageThreadId: 999,
|
||||
});
|
||||
|
||||
expect(
|
||||
await createShouldSupersedeTelegramSpooledPending(channelDmAuth)(
|
||||
record("2", update),
|
||||
claim("1", messageUpdate({ updateId: 1, text: "prior", senderId: OWNER_ID })),
|
||||
),
|
||||
).toBe(testCase.expected);
|
||||
});
|
||||
|
||||
it("reuses ingress command gate for sender authorization", async () => {
|
||||
expect(
|
||||
await isTelegramSpooledUpdateSenderAuthorized(
|
||||
|
||||
Reference in New Issue
Block a user