mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 14:48:28 +00:00
fix(slack): override video/* MIME to audio/* for voice messages (#14941)
* fix(slack): override video/* MIME to audio/* for voice messages * fix(slack): preserve overridden MIME in return value * test(slack): fix media monitor MIME mock wiring --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -238,6 +238,80 @@ describe("resolveSlackMedia", () => {
|
|||||||
expect(mockFetch).not.toHaveBeenCalled();
|
expect(mockFetch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("overrides video/* MIME to audio/* for slack_audio voice messages", async () => {
|
||||||
|
// saveMediaBuffer re-detects MIME from buffer bytes, so it may return
|
||||||
|
// video/mp4 for MP4 containers. Verify resolveSlackMedia preserves
|
||||||
|
// the overridden audio/* type in its return value despite this.
|
||||||
|
const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
|
||||||
|
path: "/tmp/voice.mp4",
|
||||||
|
contentType: "video/mp4",
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockResponse = new Response(Buffer.from("audio data"), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "video/mp4" },
|
||||||
|
});
|
||||||
|
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await resolveSlackMedia({
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
url_private: "https://files.slack.com/voice.mp4",
|
||||||
|
name: "audio_message.mp4",
|
||||||
|
mimetype: "video/mp4",
|
||||||
|
subtype: "slack_audio",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
token: "xoxb-test-token",
|
||||||
|
maxBytes: 16 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
// saveMediaBuffer should receive the overridden audio/mp4
|
||||||
|
expect(saveMediaBufferMock).toHaveBeenCalledWith(
|
||||||
|
expect.any(Buffer),
|
||||||
|
"audio/mp4",
|
||||||
|
"inbound",
|
||||||
|
16 * 1024 * 1024,
|
||||||
|
);
|
||||||
|
// Returned contentType must be the overridden value, not the
|
||||||
|
// re-detected video/mp4 from saveMediaBuffer
|
||||||
|
expect(result!.contentType).toBe("audio/mp4");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves original MIME for non-voice Slack files", async () => {
|
||||||
|
const saveMediaBufferMock = vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
|
||||||
|
path: "/tmp/video.mp4",
|
||||||
|
contentType: "video/mp4",
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockResponse = new Response(Buffer.from("video data"), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "content-type": "video/mp4" },
|
||||||
|
});
|
||||||
|
mockFetch.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await resolveSlackMedia({
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
url_private: "https://files.slack.com/clip.mp4",
|
||||||
|
name: "recording.mp4",
|
||||||
|
mimetype: "video/mp4",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
token: "xoxb-test-token",
|
||||||
|
maxBytes: 16 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(saveMediaBufferMock).toHaveBeenCalledWith(
|
||||||
|
expect.any(Buffer),
|
||||||
|
"video/mp4",
|
||||||
|
"inbound",
|
||||||
|
16 * 1024 * 1024,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("falls through to next file when first file returns error", async () => {
|
it("falls through to next file when first file returns error", async () => {
|
||||||
vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
|
vi.spyOn(mediaStore, "saveMediaBuffer").mockResolvedValue({
|
||||||
path: "/tmp/test.jpg",
|
path: "/tmp/test.jpg",
|
||||||
|
|||||||
@@ -115,6 +115,23 @@ export async function fetchWithSlackAuth(url: string, token: string): Promise<Re
|
|||||||
return fetch(resolvedUrl.toString(), { redirect: "follow" });
|
return fetch(resolvedUrl.toString(), { redirect: "follow" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slack voice messages (audio clips, huddle recordings) carry a `subtype` of
|
||||||
|
* `"slack_audio"` but are served with a `video/*` MIME type (e.g. `video/mp4`,
|
||||||
|
* `video/webm`). Override the primary type to `audio/` so the
|
||||||
|
* media-understanding pipeline routes them to transcription.
|
||||||
|
*/
|
||||||
|
function resolveSlackMediaMimetype(
|
||||||
|
file: SlackFile,
|
||||||
|
fetchedContentType?: string,
|
||||||
|
): string | undefined {
|
||||||
|
const mime = fetchedContentType ?? file.mimetype;
|
||||||
|
if (file.subtype === "slack_audio" && mime?.startsWith("video/")) {
|
||||||
|
return mime.replace("video/", "audio/");
|
||||||
|
}
|
||||||
|
return mime;
|
||||||
|
}
|
||||||
|
|
||||||
export async function resolveSlackMedia(params: {
|
export async function resolveSlackMedia(params: {
|
||||||
files?: SlackFile[];
|
files?: SlackFile[];
|
||||||
token: string;
|
token: string;
|
||||||
@@ -144,16 +161,17 @@ export async function resolveSlackMedia(params: {
|
|||||||
if (fetched.buffer.byteLength > params.maxBytes) {
|
if (fetched.buffer.byteLength > params.maxBytes) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const effectiveMime = resolveSlackMediaMimetype(file, fetched.contentType);
|
||||||
const saved = await saveMediaBuffer(
|
const saved = await saveMediaBuffer(
|
||||||
fetched.buffer,
|
fetched.buffer,
|
||||||
fetched.contentType ?? file.mimetype,
|
effectiveMime,
|
||||||
"inbound",
|
"inbound",
|
||||||
params.maxBytes,
|
params.maxBytes,
|
||||||
);
|
);
|
||||||
const label = fetched.fileName ?? file.name;
|
const label = fetched.fileName ?? file.name;
|
||||||
return {
|
return {
|
||||||
path: saved.path,
|
path: saved.path,
|
||||||
contentType: saved.contentType,
|
contentType: effectiveMime ?? saved.contentType,
|
||||||
placeholder: label ? `[Slack file: ${label}]` : "[Slack file]",
|
placeholder: label ? `[Slack file: ${label}]` : "[Slack file]",
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export type SlackFile = {
|
|||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
mimetype?: string;
|
mimetype?: string;
|
||||||
|
subtype?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
url_private?: string;
|
url_private?: string;
|
||||||
url_private_download?: string;
|
url_private_download?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user