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:
Peter Steinberger
2026-08-09 09:39:47 -07:00
committed by GitHub
parent 8fdf7570a1
commit 01cf584d2a
14 changed files with 366 additions and 77 deletions
@@ -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: {
+88
View File
@@ -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(
+7 -3
View File
@@ -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 } : {}),
});
+16 -2
View File
@@ -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({
+9 -13
View File
@@ -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(