mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix(telegram): isolate sent-message cache stores
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import type { Bot } from "grammy";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { importFreshModule } from "../../../test/helpers/import-fresh.js";
|
||||
@@ -22,7 +23,6 @@ const {
|
||||
loadConfig,
|
||||
loadWebMedia,
|
||||
maybePersistResolvedTelegramTarget,
|
||||
resolveStorePath,
|
||||
} = getTelegramSendTestMocks();
|
||||
const {
|
||||
buildInlineKeyboard,
|
||||
@@ -39,6 +39,8 @@ const {
|
||||
unpinMessageTelegram,
|
||||
} = await importTelegramSendModule();
|
||||
|
||||
const TELEGRAM_TEST_CFG = {};
|
||||
|
||||
async function expectChatNotFoundWithChatId(
|
||||
action: Promise<unknown>,
|
||||
expectedChatId: string,
|
||||
@@ -100,6 +102,7 @@ function mockLoadedMedia({
|
||||
|
||||
describe("sent-message-cache", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
clearSentMessageCache();
|
||||
});
|
||||
|
||||
@@ -131,10 +134,10 @@ describe("sent-message-cache", () => {
|
||||
|
||||
it("keeps sent-message ownership across restart", async () => {
|
||||
const persistedStorePath = `/tmp/openclaw-telegram-send-tests-${process.pid}-restart.json`;
|
||||
resolveStorePath.mockReturnValue(persistedStorePath);
|
||||
const sentMessageCfg = { session: { store: persistedStorePath } };
|
||||
|
||||
recordSentMessage(123, 1);
|
||||
expect(wasSentByBot(123, 1)).toBe(true);
|
||||
recordSentMessage(123, 1, sentMessageCfg);
|
||||
expect(wasSentByBot(123, 1, sentMessageCfg)).toBe(true);
|
||||
|
||||
resetSentMessageCacheForTest();
|
||||
|
||||
@@ -144,12 +147,51 @@ describe("sent-message-cache", () => {
|
||||
);
|
||||
|
||||
try {
|
||||
expect(restartedCache.wasSentByBot(123, 1)).toBe(true);
|
||||
expect(restartedCache.wasSentByBot(123, 1, sentMessageCfg)).toBe(true);
|
||||
} finally {
|
||||
restartedCache.clearSentMessageCache();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps expired custom-store cleanup away from the default store", () => {
|
||||
const customStorePath = `/tmp/openclaw-telegram-send-tests-${process.pid}-custom-cleanup.json`;
|
||||
const customCfg = { session: { store: customStorePath } };
|
||||
const startedAt = new Date("2026-01-01T00:00:00.000Z");
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(startedAt);
|
||||
|
||||
try {
|
||||
recordSentMessage(123, 2, customCfg);
|
||||
|
||||
vi.setSystemTime(startedAt.getTime() + 24 * 60 * 60 * 1000 + 1);
|
||||
recordSentMessage(123, 1);
|
||||
|
||||
expect(wasSentByBot(123, 2, customCfg)).toBe(false);
|
||||
expect(wasSentByBot(123, 1)).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(customStorePath, { force: true });
|
||||
fs.rmSync(`${customStorePath}.telegram-sent-messages.json`, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps default and custom stores isolated while both are loaded", () => {
|
||||
const customStorePath = `/tmp/openclaw-telegram-send-tests-${process.pid}-custom-isolated.json`;
|
||||
const customCfg = { session: { store: customStorePath } };
|
||||
|
||||
try {
|
||||
recordSentMessage(123, 1);
|
||||
recordSentMessage(123, 2, customCfg);
|
||||
|
||||
expect(wasSentByBot(123, 1)).toBe(true);
|
||||
expect(wasSentByBot(123, 2)).toBe(false);
|
||||
expect(wasSentByBot(123, 1, customCfg)).toBe(false);
|
||||
expect(wasSentByBot(123, 2, customCfg)).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(customStorePath, { force: true });
|
||||
fs.rmSync(`${customStorePath}.telegram-sent-messages.json`, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("shares sent-message state across distinct module instances", async () => {
|
||||
const cacheA = await importFreshModule<typeof import("./sent-message-cache.js")>(
|
||||
import.meta.url,
|
||||
@@ -267,6 +309,8 @@ describe("sendMessageTelegram", () => {
|
||||
botApi.sendChatAction.mockResolvedValue(true);
|
||||
|
||||
await sendTypingTelegram("telegram:group:-1001234567890:topic:271", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
accountId: "default",
|
||||
});
|
||||
|
||||
@@ -286,8 +330,16 @@ describe("sendMessageTelegram", () => {
|
||||
botApi.pinChatMessage.mockResolvedValue(true);
|
||||
botApi.unpinChatMessage.mockResolvedValue(true);
|
||||
|
||||
await pinMessageTelegram("-1001234567890", 101, { accountId: "default" });
|
||||
await unpinMessageTelegram("-1001234567890", 101, { accountId: "default" });
|
||||
await pinMessageTelegram("-1001234567890", 101, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
accountId: "default",
|
||||
});
|
||||
await unpinMessageTelegram("-1001234567890", 101, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
accountId: "default",
|
||||
});
|
||||
|
||||
expect(botApi.pinChatMessage).toHaveBeenCalledWith("-1001234567890", 101, {
|
||||
disable_notification: true,
|
||||
@@ -306,6 +358,8 @@ describe("sendMessageTelegram", () => {
|
||||
botApi.pinChatMessage.mockResolvedValue(true);
|
||||
|
||||
await pinMessageTelegram("-1001234567890", 101, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
accountId: "default",
|
||||
notify: true,
|
||||
});
|
||||
@@ -326,6 +380,8 @@ describe("sendMessageTelegram", () => {
|
||||
botApi.editForumTopic.mockResolvedValue(true);
|
||||
|
||||
await renameForumTopicTelegram("-1001234567890", 271, "Codex Thread", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
accountId: "default",
|
||||
});
|
||||
|
||||
@@ -345,6 +401,8 @@ describe("sendMessageTelegram", () => {
|
||||
botApi.editForumTopic.mockResolvedValue(true);
|
||||
|
||||
await editForumTopicTelegram("-1001234567890", 271, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
accountId: "default",
|
||||
name: "Codex Thread",
|
||||
iconCustomEmojiId: "emoji-123",
|
||||
@@ -367,6 +425,8 @@ describe("sendMessageTelegram", () => {
|
||||
botApi.editForumTopic.mockResolvedValue(true);
|
||||
|
||||
await editForumTopicTelegram("telegram:group:-1001234567890:topic:271", 271, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
accountId: "default",
|
||||
name: "Codex Thread",
|
||||
});
|
||||
@@ -379,11 +439,13 @@ describe("sendMessageTelegram", () => {
|
||||
it("rejects empty topic edits", async () => {
|
||||
await expect(
|
||||
editForumTopicTelegram("-1001234567890", 271, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
accountId: "default",
|
||||
}),
|
||||
).rejects.toThrow("Telegram forum topic update requires a name or iconCustomEmojiId");
|
||||
await expect(
|
||||
editForumTopicTelegram("-1001234567890", 271, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
accountId: "default",
|
||||
iconCustomEmojiId: " ",
|
||||
}),
|
||||
@@ -395,7 +457,7 @@ describe("sendMessageTelegram", () => {
|
||||
{
|
||||
name: "global telegram timeout",
|
||||
cfg: { channels: { telegram: { timeoutSeconds: 60 } } },
|
||||
opts: { token: "tok" },
|
||||
opts: { cfg: TELEGRAM_TEST_CFG, token: "tok" },
|
||||
expectedTimeout: 60,
|
||||
},
|
||||
{
|
||||
@@ -408,7 +470,7 @@ describe("sendMessageTelegram", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
opts: { token: "tok", accountId: "foo" },
|
||||
opts: { cfg: TELEGRAM_TEST_CFG, token: "tok", accountId: "foo" },
|
||||
expectedTimeout: 61,
|
||||
},
|
||||
] as const;
|
||||
@@ -419,7 +481,7 @@ describe("sendMessageTelegram", () => {
|
||||
message_id: 1,
|
||||
chat: { id: "123" },
|
||||
});
|
||||
await sendMessageTelegram("123", "hi", testCase.opts);
|
||||
await sendMessageTelegram("123", "hi", { ...testCase.opts, cfg: testCase.cfg });
|
||||
expect(botCtorSpy, testCase.name).toHaveBeenCalledWith(
|
||||
"tok",
|
||||
expect.objectContaining({
|
||||
@@ -478,6 +540,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
const res = await sendMessageTelegram(testCase.chatId, testCase.text, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
...testCase.options,
|
||||
@@ -539,13 +602,18 @@ describe("sendMessageTelegram", () => {
|
||||
},
|
||||
] as const;
|
||||
for (const testCase of cases) {
|
||||
loadConfig.mockReturnValue({
|
||||
const cfg = {
|
||||
channels: { telegram: { linkPreview: false } },
|
||||
});
|
||||
};
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
const api = { sendMessage: testCase.sendMessage } as unknown as {
|
||||
sendMessage: typeof testCase.sendMessage;
|
||||
};
|
||||
await sendMessageTelegram("123", testCase.text, { token: "tok", api });
|
||||
await sendMessageTelegram("123", testCase.text, {
|
||||
cfg,
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
expect(testCase.sendMessage.mock.calls, testCase.name).toEqual(testCase.expectedCalls);
|
||||
}
|
||||
});
|
||||
@@ -560,6 +628,7 @@ describe("sendMessageTelegram", () => {
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram("123", "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
}),
|
||||
@@ -577,6 +646,7 @@ describe("sendMessageTelegram", () => {
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram("123", "caption", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.png",
|
||||
@@ -595,7 +665,7 @@ describe("sendMessageTelegram", () => {
|
||||
chat: { id: "123" },
|
||||
});
|
||||
try {
|
||||
await sendMessageTelegram("123", "hi", { token: "tok" });
|
||||
await sendMessageTelegram("123", "hi", { cfg: TELEGRAM_TEST_CFG, token: "tok" });
|
||||
const clientFetch = (botCtorSpy.mock.calls[0]?.[1] as { client?: { fetch?: unknown } })
|
||||
?.client?.fetch;
|
||||
expect(clientFetch).toBeTypeOf("function");
|
||||
@@ -620,6 +690,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
await sendMessageTelegram("telegram:123", "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
@@ -641,6 +712,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
await sendMessageTelegram("https://t.me/mychannel", "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
@@ -667,6 +739,7 @@ describe("sendMessageTelegram", () => {
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram("@missingchannel", "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
}),
|
||||
@@ -690,6 +763,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
await sendMessageTelegram(chatId, "photo in topic", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.jpg",
|
||||
@@ -727,6 +801,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, longText, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.jpg",
|
||||
@@ -762,6 +837,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, shortText, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.jpg",
|
||||
@@ -794,6 +870,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
await sendMessageTelegram(chatId, caption, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.jpg",
|
||||
@@ -830,6 +907,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, text, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/video.mp4",
|
||||
@@ -860,6 +938,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, text, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/video.mp4",
|
||||
@@ -930,6 +1009,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const sendOptions: NonNullable<Parameters<typeof sendMessageTelegram>[2]> = {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/video.mp4",
|
||||
@@ -975,6 +1055,7 @@ describe("sendMessageTelegram", () => {
|
||||
const setTimeoutSpy = vi.spyOn(global, "setTimeout");
|
||||
|
||||
const promise = sendMessageTelegram(chatId, "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1000, jitter: 0 },
|
||||
@@ -1010,6 +1091,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
const promise = sendMessageTelegram(chatId, "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1000, jitter: 0 },
|
||||
@@ -1030,6 +1112,7 @@ describe("sendMessageTelegram", () => {
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram(chatId, "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
retry: { attempts: 3, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
|
||||
@@ -1051,6 +1134,7 @@ describe("sendMessageTelegram", () => {
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram(chatId, "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
|
||||
@@ -1075,6 +1159,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, "caption", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/fun",
|
||||
@@ -1124,6 +1209,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, "caption", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: testCase.mediaUrl,
|
||||
@@ -1164,6 +1250,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, "caption", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.png",
|
||||
@@ -1198,6 +1285,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, "caption", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.png",
|
||||
@@ -1228,6 +1316,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, "caption", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/report.pdf",
|
||||
@@ -1338,6 +1427,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
await sendMessageTelegram(testCase.chatId, testCase.text, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: testCase.mediaUrl,
|
||||
@@ -1394,6 +1484,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
await sendMessageTelegram(testCase.chatId, testCase.text, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
messageThreadId: 271,
|
||||
@@ -1426,6 +1517,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
const res = await sendMessageTelegram(testCase.chatId, testCase.text, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
messageThreadId: 271,
|
||||
@@ -1490,6 +1582,7 @@ describe("sendMessageTelegram", () => {
|
||||
|
||||
await expect(
|
||||
sendMessageTelegram(testCase.chatId, testCase.text, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
...testCase.opts,
|
||||
@@ -1512,6 +1605,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
await sendMessageTelegram(chatId, "hi", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
silent: true,
|
||||
@@ -1537,6 +1631,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
await sendMessageTelegram(chatId, "_oops_", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
silent: true,
|
||||
@@ -1559,6 +1654,7 @@ describe("sendMessageTelegram", () => {
|
||||
};
|
||||
|
||||
await sendMessageTelegram(`telegram:group:${chatId}:topic:271`, "hello forum", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
@@ -1590,6 +1686,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
const res = await sendMessageTelegram(chatId, "photo", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.jpg",
|
||||
@@ -1625,6 +1722,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
await sendMessageTelegram(chatId, "photo", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.jpg",
|
||||
@@ -1645,13 +1743,14 @@ describe("sendMessageTelegram", () => {
|
||||
const api = { sendPhoto } as unknown as {
|
||||
sendPhoto: typeof sendPhoto;
|
||||
};
|
||||
loadConfig.mockReturnValue({
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: {
|
||||
mediaMaxMb: 42,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
loadConfig.mockReturnValue(cfg);
|
||||
|
||||
mockLoadedMedia({
|
||||
buffer: Buffer.from("fake-image"),
|
||||
@@ -1660,6 +1759,7 @@ describe("sendMessageTelegram", () => {
|
||||
});
|
||||
|
||||
await sendMessageTelegram(chatId, "photo", {
|
||||
cfg,
|
||||
token: "tok",
|
||||
api,
|
||||
mediaUrl: "https://example.com/photo.jpg",
|
||||
@@ -1682,6 +1782,7 @@ describe("sendMessageTelegram", () => {
|
||||
const api = { sendMessage } as unknown as { sendMessage: typeof sendMessage };
|
||||
|
||||
const res = await sendMessageTelegram(chatId, htmlText, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
textMode: "html",
|
||||
@@ -1718,6 +1819,7 @@ describe("sendMessageTelegram", () => {
|
||||
const api = { sendMessage } as unknown as { sendMessage: typeof sendMessage };
|
||||
|
||||
const res = await sendMessageTelegram(chatId, htmlText, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
textMode: "html",
|
||||
@@ -1747,6 +1849,7 @@ describe("sendMessageTelegram", () => {
|
||||
const api = { sendMessage } as unknown as { sendMessage: typeof sendMessage };
|
||||
|
||||
const res = await sendMessageTelegram(chatId, htmlText, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
textMode: "html",
|
||||
@@ -1773,6 +1876,7 @@ describe("sendMessageTelegram", () => {
|
||||
const api = { sendMessage } as unknown as { sendMessage: typeof sendMessage };
|
||||
|
||||
const res = await sendMessageTelegram(chatId, htmlText, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
textMode: "html",
|
||||
@@ -1819,6 +1923,7 @@ describe("reactMessageTelegram", () => {
|
||||
};
|
||||
|
||||
await reactMessageTelegram(testCase.target, testCase.messageId, testCase.emoji, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
...(testCase.remove ? { remove: true } : {}),
|
||||
@@ -1836,6 +1941,7 @@ describe("reactMessageTelegram", () => {
|
||||
};
|
||||
|
||||
await reactMessageTelegram("@mychannel", 456, "✅", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
@@ -1881,6 +1987,7 @@ describe("sendStickerTelegram", () => {
|
||||
};
|
||||
|
||||
const res = await sendStickerTelegram(chatId, testCase.fileId, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
});
|
||||
@@ -1893,9 +2000,9 @@ describe("sendStickerTelegram", () => {
|
||||
|
||||
it("throws error when fileId is blank", async () => {
|
||||
for (const fileId of ["", " "]) {
|
||||
await expect(sendStickerTelegram("123", fileId, { token: "tok" })).rejects.toThrow(
|
||||
/file_id is required/i,
|
||||
);
|
||||
await expect(
|
||||
sendStickerTelegram("123", fileId, { cfg: TELEGRAM_TEST_CFG, token: "tok" }),
|
||||
).rejects.toThrow(/file_id is required/i);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1914,6 +2021,7 @@ describe("sendStickerTelegram", () => {
|
||||
};
|
||||
|
||||
const res = await sendStickerTelegram(chatId, "fileId123", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
messageThreadId: 271,
|
||||
@@ -1937,6 +2045,7 @@ describe("sendStickerTelegram", () => {
|
||||
|
||||
await expect(
|
||||
sendStickerTelegram(chatId, "fileId123", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
}),
|
||||
@@ -1954,6 +2063,7 @@ describe("sendStickerTelegram", () => {
|
||||
|
||||
await expect(
|
||||
sendStickerTelegram(chatId, "fileId123", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 0, jitter: 0 },
|
||||
@@ -1981,6 +2091,7 @@ describe("sendStickerTelegram", () => {
|
||||
const setTimeoutSpy = vi.spyOn(global, "setTimeout");
|
||||
|
||||
const promise = sendStickerTelegram(chatId, "fileId123", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
retry: { attempts: 2, minDelayMs: 0, maxDelayMs: 1000, jitter: 0 },
|
||||
@@ -2010,6 +2121,7 @@ describe("shared send behaviors", () => {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
await sendMessageTelegram(chatId, "reply text", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
replyToMessageId: 100,
|
||||
@@ -2034,6 +2146,7 @@ describe("shared send behaviors", () => {
|
||||
sendSticker: typeof sendSticker;
|
||||
};
|
||||
await sendStickerTelegram(chatId, fileId, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
replyToMessageId: 500,
|
||||
@@ -2070,11 +2183,13 @@ describe("shared send behaviors", () => {
|
||||
};
|
||||
|
||||
await sendMessageTelegram(chatId, "reply text", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
replyToMessageId: invalidReplyToMessageId as unknown as number,
|
||||
});
|
||||
await sendStickerTelegram(chatId, "CAACAgIAAxkBAAI...sticker_file_id", {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
replyToMessageId: invalidReplyToMessageId as unknown as number,
|
||||
@@ -2107,7 +2222,7 @@ describe("shared send behaviors", () => {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
await expectChatNotFoundWithChatId(
|
||||
sendMessageTelegram(chatId, "hi", { token: "tok", api }),
|
||||
sendMessageTelegram(chatId, "hi", { cfg: TELEGRAM_TEST_CFG, token: "tok", api }),
|
||||
chatId,
|
||||
);
|
||||
},
|
||||
@@ -2122,7 +2237,7 @@ describe("shared send behaviors", () => {
|
||||
sendSticker: typeof sendSticker;
|
||||
};
|
||||
await expectChatNotFoundWithChatId(
|
||||
sendStickerTelegram(chatId, "fileId123", { token: "tok", api }),
|
||||
sendStickerTelegram(chatId, "fileId123", { cfg: TELEGRAM_TEST_CFG, token: "tok", api }),
|
||||
chatId,
|
||||
);
|
||||
},
|
||||
@@ -2145,7 +2260,7 @@ describe("shared send behaviors", () => {
|
||||
sendMessage: typeof sendMessage;
|
||||
};
|
||||
await expectTelegramMembershipErrorWithChatId(
|
||||
sendMessageTelegram(chatId, "hi", { token: "tok", api }),
|
||||
sendMessageTelegram(chatId, "hi", { cfg: TELEGRAM_TEST_CFG, token: "tok", api }),
|
||||
chatId,
|
||||
/bot is not a member of the channel chat/i,
|
||||
);
|
||||
@@ -2160,7 +2275,7 @@ describe("shared send behaviors", () => {
|
||||
sendSticker: typeof sendSticker;
|
||||
};
|
||||
await expectTelegramMembershipErrorWithChatId(
|
||||
sendStickerTelegram(chatId, "fileId123", { token: "tok", api }),
|
||||
sendStickerTelegram(chatId, "fileId123", { cfg: TELEGRAM_TEST_CFG, token: "tok", api }),
|
||||
chatId,
|
||||
/bot was kicked from the group chat/i,
|
||||
);
|
||||
@@ -2305,6 +2420,7 @@ describe("sendPollTelegram", () => {
|
||||
"https://t.me/mychannel",
|
||||
{ question: " Q ", options: [" A ", "B "] },
|
||||
{
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "t",
|
||||
api: api as unknown as Bot["api"],
|
||||
gatewayClientScopes: ["operator.admin"],
|
||||
@@ -2329,7 +2445,7 @@ describe("sendPollTelegram", () => {
|
||||
const res = await sendPollTelegram(
|
||||
"123",
|
||||
{ question: " Q ", options: [" A ", "B "], durationSeconds: 60 },
|
||||
{ token: "t", api: api as unknown as Bot["api"] },
|
||||
{ cfg: TELEGRAM_TEST_CFG, token: "t", api: api as unknown as Bot["api"] },
|
||||
);
|
||||
|
||||
expect(res).toEqual({ messageId: "123", chatId: "555", pollId: "p1" });
|
||||
@@ -2357,7 +2473,12 @@ describe("sendPollTelegram", () => {
|
||||
const res = await sendPollTelegram(
|
||||
"-100123",
|
||||
{ question: "Q", options: ["A", "B"] },
|
||||
{ token: "t", api: api as unknown as Bot["api"], messageThreadId: 99 },
|
||||
{
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "t",
|
||||
api: api as unknown as Bot["api"],
|
||||
messageThreadId: 99,
|
||||
},
|
||||
);
|
||||
|
||||
expect(res).toEqual({ messageId: "1", chatId: "2", pollId: "p2" });
|
||||
@@ -2376,7 +2497,7 @@ describe("sendPollTelegram", () => {
|
||||
sendPollTelegram(
|
||||
"123",
|
||||
{ question: "Q", options: ["A", "B"], durationHours: 1 },
|
||||
{ token: "t", api: api as unknown as Bot["api"] },
|
||||
{ cfg: TELEGRAM_TEST_CFG, token: "t", api: api as unknown as Bot["api"] },
|
||||
),
|
||||
).rejects.toThrow(/durationHours is not supported/i);
|
||||
|
||||
@@ -2392,7 +2513,7 @@ describe("sendPollTelegram", () => {
|
||||
sendPollTelegram(
|
||||
"123",
|
||||
{ question: "Q", options: ["A", "B"] },
|
||||
{ token: "t", api: api as unknown as Bot["api"] },
|
||||
{ cfg: TELEGRAM_TEST_CFG, token: "t", api: api as unknown as Bot["api"] },
|
||||
),
|
||||
).rejects.toThrow(/returned no message_id/i);
|
||||
});
|
||||
@@ -2440,6 +2561,7 @@ describe("createForumTopicTelegram", () => {
|
||||
const api = { createForumTopic } as unknown as Bot["api"];
|
||||
|
||||
const result = await createForumTopicTelegram(testCase.target, testCase.title, {
|
||||
cfg: TELEGRAM_TEST_CFG,
|
||||
token: "tok",
|
||||
api,
|
||||
...("options" in testCase ? testCase.options : {}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { loadConfig, resolveStorePath } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveStorePath, type OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
|
||||
const TTL_MS = 24 * 60 * 60 * 1000;
|
||||
@@ -8,9 +8,13 @@ const TELEGRAM_SENT_MESSAGES_STATE_KEY = Symbol.for("openclaw.telegramSentMessag
|
||||
|
||||
type SentMessageStore = Map<string, Map<string, number>>;
|
||||
|
||||
type SentMessageBucket = {
|
||||
persistedPath: string;
|
||||
store: SentMessageStore;
|
||||
};
|
||||
|
||||
type SentMessageState = {
|
||||
persistedPath?: string;
|
||||
store?: SentMessageStore;
|
||||
bucketsByPath: Map<string, SentMessageBucket>;
|
||||
};
|
||||
|
||||
function getSentMessageState(): SentMessageState {
|
||||
@@ -19,7 +23,9 @@ function getSentMessageState(): SentMessageState {
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const state: SentMessageState = {};
|
||||
const state: SentMessageState = {
|
||||
bucketsByPath: new Map(),
|
||||
};
|
||||
globalStore[TELEGRAM_SENT_MESSAGES_STATE_KEY] = state;
|
||||
return state;
|
||||
}
|
||||
@@ -28,19 +34,23 @@ function createSentMessageStore(): SentMessageStore {
|
||||
return new Map<string, Map<string, number>>();
|
||||
}
|
||||
|
||||
function resolveSentMessageStorePath(): string {
|
||||
const cfg = loadConfig();
|
||||
return `${resolveStorePath(cfg.session?.store)}.telegram-sent-messages.json`;
|
||||
function resolveSentMessageStorePath(cfg?: Pick<OpenClawConfig, "session">): string {
|
||||
return `${resolveStorePath(cfg?.session?.store)}.telegram-sent-messages.json`;
|
||||
}
|
||||
|
||||
function cleanupExpired(scopeKey: string, entry: Map<string, number>, now: number): void {
|
||||
function cleanupExpired(
|
||||
store: SentMessageStore,
|
||||
scopeKey: string,
|
||||
entry: Map<string, number>,
|
||||
now: number,
|
||||
): void {
|
||||
for (const [id, timestamp] of entry) {
|
||||
if (now - timestamp > TTL_MS) {
|
||||
entry.delete(id);
|
||||
}
|
||||
}
|
||||
if (entry.size === 0) {
|
||||
getSentMessages().delete(scopeKey);
|
||||
store.delete(scopeKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,46 +85,55 @@ function readPersistedSentMessages(filePath: string): SentMessageStore {
|
||||
}
|
||||
}
|
||||
|
||||
function getSentMessages(): SentMessageStore {
|
||||
function getSentMessageBucket(cfg?: Pick<OpenClawConfig, "session">): SentMessageBucket {
|
||||
const state = getSentMessageState();
|
||||
const persistedPath = resolveSentMessageStorePath();
|
||||
if (!state.store || state.persistedPath !== persistedPath) {
|
||||
state.store = readPersistedSentMessages(persistedPath);
|
||||
state.persistedPath = persistedPath;
|
||||
const persistedPath = resolveSentMessageStorePath(cfg);
|
||||
const existing = state.bucketsByPath.get(persistedPath);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
return state.store;
|
||||
const bucket = {
|
||||
persistedPath,
|
||||
store: readPersistedSentMessages(persistedPath),
|
||||
};
|
||||
state.bucketsByPath.set(persistedPath, bucket);
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function persistSentMessages(): void {
|
||||
const state = getSentMessageState();
|
||||
const store = state.store;
|
||||
const filePath = state.persistedPath;
|
||||
if (!store || !filePath) {
|
||||
return;
|
||||
}
|
||||
function getSentMessages(cfg?: Pick<OpenClawConfig, "session">): SentMessageStore {
|
||||
return getSentMessageBucket(cfg).store;
|
||||
}
|
||||
|
||||
function persistSentMessages(bucket: SentMessageBucket): void {
|
||||
const { store, persistedPath } = bucket;
|
||||
const now = Date.now();
|
||||
const serialized: Record<string, Record<string, number>> = {};
|
||||
for (const [chatId, entry] of store) {
|
||||
cleanupExpired(chatId, entry, now);
|
||||
cleanupExpired(store, chatId, entry, now);
|
||||
if (entry.size > 0) {
|
||||
serialized[chatId] = Object.fromEntries(entry);
|
||||
}
|
||||
}
|
||||
if (Object.keys(serialized).length === 0) {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
fs.rmSync(persistedPath, { force: true });
|
||||
return;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
const tempPath = `${filePath}.${process.pid}.tmp`;
|
||||
fs.mkdirSync(path.dirname(persistedPath), { recursive: true });
|
||||
const tempPath = `${persistedPath}.${process.pid}.tmp`;
|
||||
fs.writeFileSync(tempPath, JSON.stringify(serialized), "utf-8");
|
||||
fs.renameSync(tempPath, filePath);
|
||||
fs.renameSync(tempPath, persistedPath);
|
||||
}
|
||||
|
||||
export function recordSentMessage(chatId: number | string, messageId: number): void {
|
||||
export function recordSentMessage(
|
||||
chatId: number | string,
|
||||
messageId: number,
|
||||
cfg?: Pick<OpenClawConfig, "session">,
|
||||
): void {
|
||||
const scopeKey = String(chatId);
|
||||
const idKey = String(messageId);
|
||||
const now = Date.now();
|
||||
const store = getSentMessages();
|
||||
const bucket = getSentMessageBucket(cfg);
|
||||
const { store } = bucket;
|
||||
let entry = store.get(scopeKey);
|
||||
if (!entry) {
|
||||
entry = new Map<string, number>();
|
||||
@@ -122,34 +141,40 @@ export function recordSentMessage(chatId: number | string, messageId: number): v
|
||||
}
|
||||
entry.set(idKey, now);
|
||||
if (entry.size > 100) {
|
||||
cleanupExpired(scopeKey, entry, now);
|
||||
cleanupExpired(store, scopeKey, entry, now);
|
||||
}
|
||||
try {
|
||||
persistSentMessages();
|
||||
persistSentMessages(bucket);
|
||||
} catch (error) {
|
||||
logVerbose(`telegram: failed to persist sent-message cache: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function wasSentByBot(chatId: number | string, messageId: number): boolean {
|
||||
export function wasSentByBot(
|
||||
chatId: number | string,
|
||||
messageId: number,
|
||||
cfg?: Pick<OpenClawConfig, "session">,
|
||||
): boolean {
|
||||
const scopeKey = String(chatId);
|
||||
const idKey = String(messageId);
|
||||
const entry = getSentMessages().get(scopeKey);
|
||||
const store = getSentMessages(cfg);
|
||||
const entry = store.get(scopeKey);
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
cleanupExpired(scopeKey, entry, Date.now());
|
||||
cleanupExpired(store, scopeKey, entry, Date.now());
|
||||
return entry.has(idKey);
|
||||
}
|
||||
|
||||
export function clearSentMessageCache(): void {
|
||||
const state = getSentMessageState();
|
||||
getSentMessages().clear();
|
||||
if (state.persistedPath) {
|
||||
fs.rmSync(state.persistedPath, { force: true });
|
||||
for (const bucket of state.bucketsByPath.values()) {
|
||||
bucket.store.clear();
|
||||
fs.rmSync(bucket.persistedPath, { force: true });
|
||||
}
|
||||
state.bucketsByPath.clear();
|
||||
}
|
||||
|
||||
export function resetSentMessageCacheForTest(): void {
|
||||
getSentMessageState().store = undefined;
|
||||
getSentMessageState().bucketsByPath.clear();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user