diff --git a/extensions/telegram/src/bot-message-dispatch.media.ts b/extensions/telegram/src/bot-message-dispatch.media.ts new file mode 100644 index 00000000000..f9bf4dbb3d1 --- /dev/null +++ b/extensions/telegram/src/bot-message-dispatch.media.ts @@ -0,0 +1,32 @@ +export type TelegramMediaContextPayload = { + MediaPath?: string; + MediaUrl?: string; + MediaType?: string; + MediaPaths?: string[]; + MediaUrls?: string[]; + MediaTypes?: string[]; +}; + +export function pruneStickerMediaFromContext( + ctxPayload: TelegramMediaContextPayload, + opts?: { stickerMediaIncluded?: boolean }, +) { + if (opts?.stickerMediaIncluded === false) { + return; + } + const nextMediaPaths = Array.isArray(ctxPayload.MediaPaths) + ? ctxPayload.MediaPaths.slice(1) + : undefined; + const nextMediaUrls = Array.isArray(ctxPayload.MediaUrls) + ? ctxPayload.MediaUrls.slice(1) + : undefined; + const nextMediaTypes = Array.isArray(ctxPayload.MediaTypes) + ? ctxPayload.MediaTypes.slice(1) + : undefined; + ctxPayload.MediaPaths = nextMediaPaths && nextMediaPaths.length > 0 ? nextMediaPaths : undefined; + ctxPayload.MediaUrls = nextMediaUrls && nextMediaUrls.length > 0 ? nextMediaUrls : undefined; + ctxPayload.MediaTypes = nextMediaTypes && nextMediaTypes.length > 0 ? nextMediaTypes : undefined; + ctxPayload.MediaPath = ctxPayload.MediaPaths?.[0]; + ctxPayload.MediaUrl = ctxPayload.MediaUrls?.[0] ?? ctxPayload.MediaPath; + ctxPayload.MediaType = ctxPayload.MediaTypes?.[0]; +} diff --git a/extensions/telegram/src/bot-message-dispatch.sticker-media.test.ts b/extensions/telegram/src/bot-message-dispatch.sticker-media.test.ts index 5e6cb118e88..e4670af398d 100644 --- a/extensions/telegram/src/bot-message-dispatch.sticker-media.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.sticker-media.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { pruneStickerMediaFromContext } from "./bot-message-dispatch.js"; +import { pruneStickerMediaFromContext } from "./bot-message-dispatch.media.js"; type MediaCtx = { MediaPath?: string; diff --git a/extensions/telegram/src/bot-message-dispatch.ts b/extensions/telegram/src/bot-message-dispatch.ts index 881690029f4..f6b84efe941 100644 --- a/extensions/telegram/src/bot-message-dispatch.ts +++ b/extensions/telegram/src/bot-message-dispatch.ts @@ -39,6 +39,7 @@ import { resolveAgentDir, resolveDefaultModelForAgent, } from "./bot-message-dispatch.agent.runtime.js"; +import { pruneStickerMediaFromContext } from "./bot-message-dispatch.media.js"; import { generateTopicLabel, getAgentScopedMediaLocalRoots, @@ -77,6 +78,8 @@ import { import { editMessageTelegram } from "./send.js"; import { cacheSticker, describeStickerImage } from "./sticker-cache.js"; +export { pruneStickerMediaFromContext } from "./bot-message-dispatch.media.js"; + const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again."; const silentReplyDispatchLogger = createSubsystemLogger("telegram/silent-reply-dispatch"); @@ -97,37 +100,6 @@ async function resolveStickerVisionSupport(cfg: OpenClawConfig, agentId: string) } } -export function pruneStickerMediaFromContext( - ctxPayload: { - MediaPath?: string; - MediaUrl?: string; - MediaType?: string; - MediaPaths?: string[]; - MediaUrls?: string[]; - MediaTypes?: string[]; - }, - opts?: { stickerMediaIncluded?: boolean }, -) { - if (opts?.stickerMediaIncluded === false) { - return; - } - const nextMediaPaths = Array.isArray(ctxPayload.MediaPaths) - ? ctxPayload.MediaPaths.slice(1) - : undefined; - const nextMediaUrls = Array.isArray(ctxPayload.MediaUrls) - ? ctxPayload.MediaUrls.slice(1) - : undefined; - const nextMediaTypes = Array.isArray(ctxPayload.MediaTypes) - ? ctxPayload.MediaTypes.slice(1) - : undefined; - ctxPayload.MediaPaths = nextMediaPaths && nextMediaPaths.length > 0 ? nextMediaPaths : undefined; - ctxPayload.MediaUrls = nextMediaUrls && nextMediaUrls.length > 0 ? nextMediaUrls : undefined; - ctxPayload.MediaTypes = nextMediaTypes && nextMediaTypes.length > 0 ? nextMediaTypes : undefined; - ctxPayload.MediaPath = ctxPayload.MediaPaths?.[0]; - ctxPayload.MediaUrl = ctxPayload.MediaUrls?.[0] ?? ctxPayload.MediaPath; - ctxPayload.MediaType = ctxPayload.MediaTypes?.[0]; -} - type DispatchTelegramMessageParams = { context: TelegramMessageContext; bot: Bot; diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index afa2ef3401f..c08cea89890 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -861,55 +861,6 @@ describe("createTelegramBot", () => { expect(replySpy).not.toHaveBeenCalled(); }); - it("blocks unauthorized DM media before download and sends pairing reply", async () => { - loadConfig.mockReturnValue({ - channels: { telegram: { dmPolicy: "pairing" } }, - }); - readChannelAllowFromStore.mockResolvedValue([]); - upsertChannelPairingRequest.mockResolvedValue({ code: "PAIRME12", created: true }); - sendMessageSpy.mockClear(); - replySpy.mockClear(); - const senderId = Number(`${Date.now()}01`.slice(-9)); - - const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation( - async () => - new Response(new Uint8Array([0xff, 0xd8, 0xff, 0x00]), { - status: 200, - headers: { "content-type": "image/jpeg" }, - }), - ); - const getFileSpy = vi.fn(async () => ({ file_path: "photos/p1.jpg" })); - - try { - createTelegramBot({ token: "tok" }); - const handler = getOnHandler("message") as (ctx: Record) => Promise; - - await handler({ - message: { - chat: { id: 1234, type: "private" }, - message_id: 411, - date: 1736380800, - photo: [{ file_id: "p1" }], - from: { id: senderId, username: "random" }, - }, - me: { username: "openclaw_bot" }, - getFile: getFileSpy, - }); - - expect(getFileSpy).not.toHaveBeenCalled(); - expect(fetchSpy).not.toHaveBeenCalled(); - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - const pairingText = String(sendMessageSpy.mock.calls[0]?.[1]); - expect(pairingText).toContain("Pairing code:"); - expect(pairingText).toContain("
");
-      expect(sendMessageSpy.mock.calls[0]?.[2]).toEqual(
-        expect.objectContaining({ parse_mode: "HTML" }),
-      );
-      expect(replySpy).not.toHaveBeenCalled();
-    } finally {
-      fetchSpy.mockRestore();
-    }
-  });
   it("blocks DM media downloads completely when dmPolicy is disabled", async () => {
     loadConfig.mockReturnValue({
       channels: { telegram: { dmPolicy: "disabled" } },
diff --git a/extensions/telegram/src/sticker-cache-store.ts b/extensions/telegram/src/sticker-cache-store.ts
index d1ad192c04d..a4b2720921d 100644
--- a/extensions/telegram/src/sticker-cache-store.ts
+++ b/extensions/telegram/src/sticker-cache-store.ts
@@ -1,7 +1,6 @@
 import path from "node:path";
 import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
 import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
-import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
 
 const CACHE_VERSION = 1;
 
@@ -41,6 +40,10 @@ function saveCache(cache: StickerCache): void {
   saveJsonFile(getCacheFile(), cache);
 }
 
+function normalizeStickerSearchText(value: unknown): string {
+  return typeof value === "string" ? value.trim().toLowerCase() : "";
+}
+
 /**
  * Get a cached sticker by its unique ID.
  */
@@ -63,12 +66,12 @@ export function cacheSticker(sticker: CachedSticker): void {
  */
 export function searchStickers(query: string, limit = 10): CachedSticker[] {
   const cache = loadCache();
-  const queryLower = normalizeLowercaseStringOrEmpty(query);
+  const queryLower = normalizeStickerSearchText(query);
   const results: Array<{ sticker: CachedSticker; score: number }> = [];
 
   for (const sticker of Object.values(cache.stickers)) {
     let score = 0;
-    const descLower = normalizeLowercaseStringOrEmpty(sticker.description);
+    const descLower = normalizeStickerSearchText(sticker.description);
 
     // Exact substring match in description
     if (descLower.includes(queryLower)) {
@@ -90,7 +93,7 @@ export function searchStickers(query: string, limit = 10): CachedSticker[] {
     }
 
     // Set name match
-    if (normalizeLowercaseStringOrEmpty(sticker.setName).includes(queryLower)) {
+    if (normalizeStickerSearchText(sticker.setName).includes(queryLower)) {
       score += 3;
     }
 
diff --git a/extensions/telegram/src/sticker-cache.test.ts b/extensions/telegram/src/sticker-cache.test.ts
index 4870a7deb0d..755970245dc 100644
--- a/extensions/telegram/src/sticker-cache.test.ts
+++ b/extensions/telegram/src/sticker-cache.test.ts
@@ -1,21 +1,31 @@
-import fs from "node:fs";
-import path from "node:path";
-import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { beforeEach, describe, expect, it, vi } from "vitest";
 import * as stickerCache from "./sticker-cache-store.js";
 
-const TEST_CACHE_DIR = "/tmp/openclaw-test-sticker-cache/telegram";
-const TEST_CACHE_FILE = path.join(TEST_CACHE_DIR, "sticker-cache.json");
+const jsonStoreMocks = vi.hoisted(() => {
+  const store: { value: unknown } = { value: null };
+  return {
+    store,
+    loadJsonFile: vi.fn(() => store.value),
+    saveJsonFile: vi.fn((_file: string, value: unknown) => {
+      store.value = structuredClone(value);
+    }),
+  };
+});
+
+vi.mock("openclaw/plugin-sdk/json-store", () => ({
+  loadJsonFile: jsonStoreMocks.loadJsonFile,
+  saveJsonFile: jsonStoreMocks.saveJsonFile,
+}));
+
+vi.mock("openclaw/plugin-sdk/state-paths", () => ({
+  resolveStateDir: () => "/tmp/openclaw-test-sticker-cache",
+}));
 
 describe("sticker-cache", () => {
   beforeEach(() => {
-    process.env.OPENCLAW_STATE_DIR = "/tmp/openclaw-test-sticker-cache";
-    fs.rmSync("/tmp/openclaw-test-sticker-cache", { recursive: true, force: true });
-    fs.mkdirSync(TEST_CACHE_DIR, { recursive: true });
-  });
-
-  afterEach(() => {
-    fs.rmSync("/tmp/openclaw-test-sticker-cache", { recursive: true, force: true });
-    delete process.env.OPENCLAW_STATE_DIR;
+    jsonStoreMocks.store.value = null;
+    jsonStoreMocks.loadJsonFile.mockClear();
+    jsonStoreMocks.saveJsonFile.mockClear();
   });
 
   describe("getCachedSticker", () => {
@@ -40,7 +50,7 @@ describe("sticker-cache", () => {
       expect(result).toEqual(sticker);
     });
 
-    it("returns null after cache is cleared", () => {
+    it("returns null after backing store is cleared", () => {
       const sticker = {
         fileId: "file123",
         fileUniqueId: "unique123",
@@ -51,8 +61,7 @@ describe("sticker-cache", () => {
       stickerCache.cacheSticker(sticker);
       expect(stickerCache.getCachedSticker("unique123")).not.toBeNull();
 
-      // Manually clear the cache file
-      fs.rmSync(TEST_CACHE_FILE, { force: true });
+      jsonStoreMocks.store.value = null;
 
       expect(stickerCache.getCachedSticker("unique123")).toBeNull();
     });