fix(discord): use fetch for voice upload slots

This commit is contained in:
Shadow
2026-03-03 09:47:12 -06:00
parent 66d06beec6
commit 3b3738e41e
4 changed files with 51 additions and 17 deletions

View File

@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
- Docs/tool-loop detection config keys: align `docs/tools/loop-detection.md` examples and field names with the current `tools.loopDetection` schema to prevent copy-paste validation failures from outdated keys. (#33182) Thanks @Mylszd.
- Discord/presence defaults: send an online presence update on ready when no custom presence is configured so bots no longer appear offline by default. Thanks @thewilloftheshadow.
- Discord/typing cleanup: stop typing indicators after silent/NO_REPLY runs by marking the run complete before dispatch idle cleanup. Thanks @thewilloftheshadow.
- Discord/voice messages: request upload slots with JSON fetch calls so voice message uploads no longer fail with content-type errors. Thanks @thewilloftheshadow.
- Telegram/DM draft finalization reliability: require verified final-text draft emission before treating preview finalization as delivered, and fall back to normal payload send when final draft delivery is not confirmed (preventing missing final responses and preserving media/button delivery). (#32118) Thanks @OpenCils.
- Discord/audit wildcard warnings: ignore "\*" wildcard keys when counting unresolved guild channels so doctor/status no longer warns on allow-all configs. (#33125) Thanks @thewilloftheshadow.
- Discord/channel resolution: default bare numeric recipients to channels, harden allowlist numeric ID handling with safe fallbacks, and avoid inbound WS heartbeat stalls. (#33142) Thanks @thewilloftheshadow.

View File

@@ -56,7 +56,8 @@ const allowedRawFetchCallsites = new Set([
"extensions/voice-call/src/providers/twilio/api.ts:23",
"src/channels/telegram/api.ts:8",
"src/discord/send.outbound.ts:347",
"src/discord/voice-message.ts:267",
"src/discord/voice-message.ts:264",
"src/discord/voice-message.ts:308",
"src/slack/monitor/media.ts:64",
"src/slack/monitor/media.ts:68",
"src/slack/monitor/media.ts:82",

View File

@@ -524,6 +524,7 @@ export async function sendVoiceMessageDiscord(
opts.replyTo,
request,
opts.silent,
token,
);
recordChannelActivity({

View File

@@ -13,7 +13,7 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import type { RequestClient } from "@buape/carbon";
import { RateLimitError, type RequestClient } from "@buape/carbon";
import type { RetryRunner } from "../infra/retry-policy.js";
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
import { parseFfprobeCodecAndSampleRate, runFfmpeg, runFfprobe } from "../media/ffmpeg-exec.js";
@@ -245,26 +245,57 @@ export async function sendDiscordVoiceMessage(
replyTo: string | undefined,
request: RetryRunner,
silent?: boolean,
token?: string,
): Promise<{ id: string; channel_id: string }> {
const filename = "voice-message.ogg";
const fileSize = audioBuffer.byteLength;
// Step 1: Request upload URL from Discord
const uploadUrlResponse = await request(
() =>
rest.post(`/channels/${channelId}/attachments`, {
body: {
files: [
{
filename,
file_size: fileSize,
id: "0",
},
],
},
}) as Promise<UploadUrlResponse>,
"voice-upload-url",
);
// Must use fetch() directly instead of rest.post() because @buape/carbon's
// RequestClient auto-converts requests to multipart/form-data when the body
// contains a "files" key. Discord's /attachments endpoint expects JSON, so
// the auto-conversion causes HTTP 400 "Expected Content-Type application/json".
const botToken = token;
if (!botToken) {
throw new Error("Discord bot token is required for voice message upload");
}
const uploadUrlResponse = await request(async () => {
const url = `${rest.options?.baseUrl ?? "https://discord.com/api"}/channels/${channelId}/attachments`;
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bot ${botToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
files: [{ filename, file_size: fileSize, id: "0" }],
}),
});
if (!res.ok) {
if (res.status === 429) {
const retryData = (await res.json().catch(() => ({}))) as {
message?: string;
retry_after?: number;
global?: boolean;
};
throw new RateLimitError(res, {
message: retryData.message ?? "You are being rate limited.",
retry_after: retryData.retry_after ?? 1,
global: retryData.global ?? false,
});
}
const errorBody = (await res.json().catch(() => null)) as {
code?: number;
message?: string;
} | null;
const err = new Error(`Upload URL request failed: ${res.status} ${errorBody?.message ?? ""}`);
if (errorBody?.code !== undefined) {
(err as Error & { code: number }).code = errorBody.code;
}
throw err;
}
return (await res.json()) as UploadUrlResponse;
}, "voice-upload-url");
if (!uploadUrlResponse.attachments?.[0]) {
throw new Error("Failed to get upload URL for voice message");