Skip to content

Commit 3654ea3

Browse files
committed
fix(telegram): centralize safe thread id parsing
1 parent fe329ff commit 3654ea3

4 files changed

Lines changed: 94 additions & 19 deletions

File tree

extensions/telegram/src/message-cache.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,46 @@ describe("telegram message cache", () => {
445445
expect(recent).toEqual([]);
446446
});
447447

448+
it("drops unsafe Telegram thread ids from live messages", async () => {
449+
const { bucketKey, entries, store } = createMemoryPersistentStore();
450+
const cache = createTelegramMessageCache({ bucketKey, persistentStore: store });
451+
await cache.record({
452+
accountId: "default",
453+
chatId: 7,
454+
msg: {
455+
chat: { id: 7, type: "supergroup", title: "Ops" },
456+
message_id: 9127,
457+
message_thread_id: Number.MAX_SAFE_INTEGER + 1,
458+
date: 1736389127,
459+
text: "Unsafe topic message",
460+
from: { id: 1, is_bot: false, first_name: "Nora" },
461+
} as Message,
462+
});
463+
464+
const persistedValue = entries.values().next().value;
465+
if (persistedValue === undefined) {
466+
throw new Error("expected persisted Telegram message cache value");
467+
}
468+
expect(persistedValue.threadId).toBeUndefined();
469+
470+
const topicRecent = await cache.recentBefore({
471+
accountId: "default",
472+
chatId: 7,
473+
threadId: Number.MAX_SAFE_INTEGER + 1,
474+
messageId: "9128",
475+
limit: 10,
476+
});
477+
const unscopedRecent = await cache.recentBefore({
478+
accountId: "default",
479+
chatId: 7,
480+
messageId: "9128",
481+
limit: 10,
482+
});
483+
484+
expect(topicRecent).toEqual([]);
485+
expect(unscopedRecent.map((entry) => entry.messageId)).toEqual(["9127"]);
486+
});
487+
448488
it("does not use unsafe message ids as recent-before cutoffs", async () => {
449489
const cache = createTelegramMessageCache();
450490
await cache.record({

extensions/telegram/src/message-cache.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ import { createHash } from "node:crypto";
22
import fs from "node:fs";
33
import type { Message } from "grammy/types";
44
import { formatLocationText } from "openclaw/plugin-sdk/channel-inbound";
5-
import {
6-
parseStrictNonNegativeInteger,
7-
parseStrictPositiveInteger,
8-
} from "openclaw/plugin-sdk/number-runtime";
5+
import { parseStrictPositiveInteger } from "openclaw/plugin-sdk/number-runtime";
96
import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime";
107
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
118
import { appendRegularFileSync, replaceFileAtomicSync } from "openclaw/plugin-sdk/security-runtime";
@@ -17,6 +14,7 @@ import {
1714
getTelegramTextParts,
1815
normalizeForwardedContext,
1916
} from "./bot/helpers.js";
17+
import { parseTelegramMessageThreadId } from "./outbound-params.js";
2018
import { getOptionalTelegramRuntime } from "./runtime.js";
2119

2220
export type TelegramReplyChainEntry = NonNullable<MsgContext["ReplyChain"]>[number];
@@ -166,6 +164,7 @@ function normalizeMessageNode(
166164
const forwardedFrom = normalizeForwardedContext(msg);
167165
const replyMessage = resolveReplyMessage(msg);
168166
const body = resolveMessageBody(msg);
167+
const threadId = normalizeTelegramCacheThreadId(params.threadId);
169168
return {
170169
sourceMessage: msg,
171170
messageId: String(msg.message_id),
@@ -181,7 +180,7 @@ function normalizeMessageNode(
181180
...(forwardedFrom?.fromId ? { forwardedFromId: forwardedFrom.fromId } : {}),
182181
...(forwardedFrom?.fromUsername ? { forwardedFromUsername: forwardedFrom.fromUsername } : {}),
183182
...(forwardedFrom?.date ? { forwardedDate: forwardedFrom.date * 1000 } : {}),
184-
...(params.threadId != null ? { threadId: String(params.threadId) } : {}),
183+
...(threadId !== undefined ? { threadId: String(threadId) } : {}),
185184
};
186185
}
187186

@@ -198,9 +197,7 @@ function normalizeRequiredMessageNode(
198197

199198
function resolveMessageThreadId(msg: Message): number | undefined {
200199
const threadId = (msg as { message_thread_id?: unknown }).message_thread_id;
201-
return typeof threadId === "number" && Number.isFinite(threadId)
202-
? Math.trunc(threadId)
203-
: undefined;
200+
return normalizeTelegramCacheThreadId(threadId);
204201
}
205202

206203
function normalizeMessageNodes(
@@ -246,7 +243,11 @@ function parseSafeMessageId(value: string | undefined): number | undefined {
246243
}
247244

248245
function parseCachedThreadId(value: unknown): number | undefined {
249-
return parseStrictNonNegativeInteger(value);
246+
return normalizeTelegramCacheThreadId(value);
247+
}
248+
249+
function normalizeTelegramCacheThreadId(value: unknown): number | undefined {
250+
return parseTelegramMessageThreadId(value);
250251
}
251252

252253
function isTelegramSourceMessage(value: unknown): value is Message {
@@ -754,7 +755,11 @@ export function createTelegramMessageCache(params?: {
754755
}) => {
755756
await hydrateMessageCacheBucket(bucket, maxMessages, scopeKey);
756757
const prefix = telegramMessageCacheKeyPrefix({ scopeKey, ...params });
757-
const threadId = params.threadId != null ? String(params.threadId) : undefined;
758+
const normalizedThreadId = normalizeTelegramCacheThreadId(params.threadId);
759+
if (params.threadId != null && normalizedThreadId === undefined) {
760+
return [];
761+
}
762+
const threadId = normalizedThreadId !== undefined ? String(normalizedThreadId) : undefined;
758763
return Array.from(messages, ([key, node]) => ({ key, node }))
759764
.filter(({ key, node }) => {
760765
if (!key.startsWith(prefix)) {

extensions/telegram/src/outbound-params.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, expect, it } from "vitest";
2-
import { parseTelegramReplyToMessageId, parseTelegramThreadId } from "./outbound-params.js";
2+
import {
3+
parseTelegramMessageThreadId,
4+
parseTelegramReplyToMessageId,
5+
parseTelegramThreadId,
6+
} from "./outbound-params.js";
37

48
describe("parseTelegramThreadId", () => {
59
it("parses numeric and scoped thread ids", () => {
@@ -12,6 +16,9 @@ describe("parseTelegramThreadId", () => {
1216

1317
it("returns undefined for invalid thread ids", () => {
1418
expect(parseTelegramThreadId("abc")).toBeUndefined();
19+
expect(parseTelegramThreadId(42.5)).toBeUndefined();
20+
expect(parseTelegramThreadId(Number.MAX_SAFE_INTEGER + 1)).toBeUndefined();
21+
expect(parseTelegramThreadId("-10099:42.5")).toBeUndefined();
1522
expect(parseTelegramThreadId("")).toBeUndefined();
1623
expect(parseTelegramThreadId(null)).toBeUndefined();
1724
expect(parseTelegramThreadId(undefined)).toBeUndefined();
@@ -26,4 +33,23 @@ describe("parseTelegramReplyToMessageId", () => {
2633
it("returns undefined for missing reply-to ids", () => {
2734
expect(parseTelegramReplyToMessageId(null)).toBeUndefined();
2835
});
36+
37+
it("returns undefined for unsafe reply-to ids", () => {
38+
expect(parseTelegramReplyToMessageId("123.5")).toBeUndefined();
39+
expect(parseTelegramReplyToMessageId(123.5)).toBeUndefined();
40+
expect(parseTelegramReplyToMessageId(Number.MAX_SAFE_INTEGER + 1)).toBeUndefined();
41+
});
42+
});
43+
44+
describe("parseTelegramMessageThreadId", () => {
45+
it("parses non-negative Telegram message thread ids", () => {
46+
expect(parseTelegramMessageThreadId("0")).toBe(0);
47+
expect(parseTelegramMessageThreadId(42)).toBe(42);
48+
});
49+
50+
it("returns undefined for unsafe Telegram message thread ids", () => {
51+
expect(parseTelegramMessageThreadId("-1")).toBeUndefined();
52+
expect(parseTelegramMessageThreadId(42.5)).toBeUndefined();
53+
expect(parseTelegramMessageThreadId(Number.MAX_SAFE_INTEGER + 1)).toBeUndefined();
54+
});
2955
});

extensions/telegram/src/outbound-params.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
import { parseStrictInteger } from "openclaw/plugin-sdk/number-runtime";
1+
import {
2+
parseStrictInteger,
3+
parseStrictNonNegativeInteger,
4+
} from "openclaw/plugin-sdk/number-runtime";
25

3-
function parseIntegerId(value: string): number | undefined {
6+
function parseIntegerId(value: unknown): number | undefined {
47
return parseStrictInteger(value);
58
}
69

10+
export function parseTelegramMessageThreadId(value: unknown): number | undefined {
11+
return parseStrictNonNegativeInteger(value);
12+
}
13+
714
export function normalizeTelegramReplyToMessageId(value: unknown): number | undefined {
8-
if (typeof value === "number") {
9-
return Number.isFinite(value) ? Math.trunc(value) : undefined;
10-
}
1115
if (typeof value !== "string") {
12-
return undefined;
16+
return parseIntegerId(value);
1317
}
1418
const trimmed = value.trim();
1519
return trimmed ? parseIntegerId(trimmed) : undefined;
1620
}
1721

18-
export function parseTelegramReplyToMessageId(replyToId?: string | null): number | undefined {
22+
export function parseTelegramReplyToMessageId(replyToId?: unknown): number | undefined {
1923
return normalizeTelegramReplyToMessageId(replyToId);
2024
}
2125

@@ -24,7 +28,7 @@ export function parseTelegramThreadId(threadId?: string | number | null): number
2428
return undefined;
2529
}
2630
if (typeof threadId === "number") {
27-
return Number.isFinite(threadId) ? Math.trunc(threadId) : undefined;
31+
return parseIntegerId(threadId);
2832
}
2933
const trimmed = threadId.trim();
3034
if (!trimmed) {

0 commit comments

Comments
 (0)