mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-05 08:31:41 +00:00
fix(msteams): display decoded attachment filenames (#115127)
This commit is contained in:
committed by
GitHub
parent
28630a9a65
commit
ecd22f5e2d
@@ -102,6 +102,17 @@ describe("msteams media-helpers", () => {
|
||||
expect(await extractFilename("https://example.com/images/2024/photo.png")).toBe("photo.png");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["https://example.com/files/My%20report.pdf", "My report.pdf"],
|
||||
["https://example.com/files/r%C3%A9sum%C3%A9.pdf", "résumé.pdf"],
|
||||
["https://example.com/files/100%25.png", "100%.png"],
|
||||
["https://example.com/files/bad%ZZ.pdf", "bad%ZZ.pdf"],
|
||||
["https://example.com/files/folder%2Fsecret.png", "folder%2Fsecret.png"],
|
||||
["https://example.com/files/folder%5Csecret.png", "folder%5Csecret.png"],
|
||||
])("preserves the safe display filename from %s", async (url, expected) => {
|
||||
expect(await extractFilename(url)).toBe(expected);
|
||||
});
|
||||
|
||||
it("handles URLs without extension by deriving from MIME", async () => {
|
||||
// Now defaults to application/octet-stream → .bin fallback
|
||||
expect(await extractFilename("https://example.com/images/photo")).toBe("photo.bin");
|
||||
|
||||
@@ -45,8 +45,24 @@ export async function extractFilename(url: string): Promise<string> {
|
||||
// Try to extract from URL pathname
|
||||
try {
|
||||
const pathname = new URL(url).pathname;
|
||||
const basename = path.basename(pathname);
|
||||
const existingExt = getFileExtension(pathname);
|
||||
let basename = path.basename(pathname);
|
||||
if (basename.includes("%")) {
|
||||
try {
|
||||
const decodedBasename = decodeURIComponent(basename);
|
||||
// Attachment names are display values; never turn escaped delimiters
|
||||
// into a different filesystem or URL path.
|
||||
if (
|
||||
!decodedBasename.includes("/") &&
|
||||
!decodedBasename.includes("\\") &&
|
||||
!decodedBasename.includes("\0")
|
||||
) {
|
||||
basename = decodedBasename;
|
||||
}
|
||||
} catch {
|
||||
// Keep malformed percent escapes as the original literal filename.
|
||||
}
|
||||
}
|
||||
const existingExt = getFileExtension(basename);
|
||||
if (basename && existingExt) {
|
||||
return basename;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { SILENT_REPLY_TOKEN } from "openclaw/plugin-sdk/reply-chunking";
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
|
||||
import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
|
||||
import { withServer } from "openclaw/plugin-sdk/test-env";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { StoredConversationReference } from "./conversation-store.js";
|
||||
const graphUploadMockState = vi.hoisted(() => ({
|
||||
@@ -762,6 +763,56 @@ describe("msteams messenger", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("sends decoded attachment filenames over the Bot Framework HTTP transport", async () => {
|
||||
const receivedAttachments: Array<{ name: string; contentUrl: string }> = [];
|
||||
|
||||
await withServer(
|
||||
(request, response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
request.on("data", (chunk: Buffer) => chunks.push(Buffer.from(chunk)));
|
||||
request.on("end", () => {
|
||||
const activity = JSON.parse(Buffer.concat(chunks).toString("utf8")) as {
|
||||
attachments?: Array<{ name: string; contentUrl: string }>;
|
||||
};
|
||||
receivedAttachments.push(
|
||||
...(activity.attachments ?? []).map(({ name, contentUrl }) => ({ name, contentUrl })),
|
||||
);
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ id: `message-${receivedAttachments.length}` }));
|
||||
});
|
||||
},
|
||||
async (baseUrl) => {
|
||||
const app = createMockApp({
|
||||
createFn: async (activity) => {
|
||||
const response = await fetch(`${baseUrl}/v3/conversations/test/activities`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(activity),
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
});
|
||||
const encodedNames = ["My%20report.pdf", "r%C3%A9sum%C3%A9.pdf", "100%25.png"];
|
||||
|
||||
await sendMSTeamsMessages({
|
||||
replyStyle: "top-level",
|
||||
app,
|
||||
appId: "app123",
|
||||
conversationRef: baseRef,
|
||||
messages: encodedNames.map((name) => ({
|
||||
mediaUrl: `${baseUrl}/files/${name}`,
|
||||
})),
|
||||
});
|
||||
|
||||
expect(receivedAttachments).toEqual([
|
||||
{ name: "My report.pdf", contentUrl: `${baseUrl}/files/My%20report.pdf` },
|
||||
{ name: "résumé.pdf", contentUrl: `${baseUrl}/files/r%C3%A9sum%C3%A9.pdf` },
|
||||
{ name: "100%.png", contentUrl: `${baseUrl}/files/100%25.png` },
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves mention entities alongside AI entity", async () => {
|
||||
const activity = await buildActivity({ text: "hi <at>@User</at>" }, baseRef);
|
||||
const entities = activity.entities as Array<Record<string, unknown>>;
|
||||
|
||||
Reference in New Issue
Block a user