Skip to content
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 61 additions & 21 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3889,8 +3889,21 @@ export class BaileysStartupService extends ChannelStartupService {
}
}

if (typeof mediaMessage['mediaKey'] === 'object') {
msg.message[mediaType].mediaKey = Uint8Array.from(Object.values(mediaMessage['mediaKey']));
if (typeof mediaMessage['mediaKey'] === 'string') {
// base64-encoded string (e.g. from HTTP request body) → Uint8Array
// This matches OwnPilot retryMediaFromMetadata: new Uint8Array(Buffer.from(base64, 'base64'))
msg.message[mediaType].mediaKey = new Uint8Array(Buffer.from(mediaMessage['mediaKey'], 'base64'));
} else if (
typeof mediaMessage['mediaKey'] === 'object' &&
Comment on lines +3899 to +3900
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Guard against mediaKey being null before treating it as a plain object.

Since typeof null === 'object', a mediaKey of null will hit this branch, be cast to Record<string, number>, and cause Object.keys(keyObj) to throw. If null is a possible value (e.g. malformed input or partial DB data), add an explicit non-null check or a more specific guard (e.g. mediaMessage['mediaKey'] && typeof mediaMessage['mediaKey'] === 'object', or an Array.isArray/instanceof check) before entering this path.

!Buffer.isBuffer(mediaMessage['mediaKey']) &&
!(mediaMessage['mediaKey'] instanceof Uint8Array)
) {
// Plain object {0:b0, 1:b1, ...} from PostgreSQL JSONB deserialization.
// CRITICAL: JSONB stores keys lexicographically ("0","1","10","11",...,"2",...,"9")
// so Object.values() gives WRONG byte order. Must sort keys numerically first.
const keyObj = mediaMessage['mediaKey'] as Record<string, number>;
const sortedKeys = Object.keys(keyObj).sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
msg.message[mediaType].mediaKey = new Uint8Array(sortedKeys.map((k) => keyObj[k]));
}

let buffer: Buffer;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Capture and log the original error in the outer catch for better debuggability.

The empty outer catch {} hides the original downloadMediaMessage error, so logs will only show failures in the reupload/fallback paths and obscure systemic issues in the initial download (auth/CDN/parsing, etc.). Please change this to catch (err) and log err (including the stack) with the explanatory message.

Suggested change
let buffer: Buffer;
let buffer: Buffer;
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
);
} catch (err) {
this.logger.error(
'Download Media failed, attempting explicit updateMediaMessage (Baileys RC9 reuploadRequest bug workaround)...',
err,
);

Expand All @@ -3903,30 +3916,57 @@ export class BaileysStartupService extends ChannelStartupService {
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
);
} catch {
this.logger.error('Download Media failed, trying to retry in 5 seconds...');
await new Promise((resolve) => setTimeout(resolve, 5000));
const mediaType = Object.keys(msg.message).find((key) => key.endsWith('Message'));
if (!mediaType) throw new Error('Could not determine mediaType for fallback');
this.logger.error('Download Media failed, attempting explicit updateMediaMessage (Baileys RC9 reuploadRequest bug workaround)...');

// Baileys RC9 bug: reuploadRequest callback never triggers because downloadMediaMessage
// checks error.status but Boom sets output.statusCode — so the automatic re-upload path
// is dead code. Fix: explicitly call updateMediaMessage to get a fresh CDN URL,
// then retry the download. This mirrors the technique used in OwnPilot retryMediaFromMetadata().
try {
const media = await downloadContentFromMessage(
{
mediaKey: msg.message?.[mediaType]?.mediaKey,
directPath: msg.message?.[mediaType]?.directPath,
url: `https://mmg.whatsapp.net${msg?.message?.[mediaType]?.directPath}`,
},
await this.mapMediaType(mediaType),
const REUPLOAD_TIMEOUT_MS = 30_000;
this.logger.info('Requesting media re-upload from sender device via updateMediaMessage...');
const updatedMsg = await Promise.race([
this.client.updateMediaMessage({ key: msg.key, message: msg.message }),
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error('updateMediaMessage timed out after 30s — sender device may be offline')),
REUPLOAD_TIMEOUT_MS,
),
),
]);
buffer = await downloadMediaMessage(
updatedMsg,
'buffer',
{},
{ logger: P({ level: 'error' }) as any, reuploadRequest: this.client.updateMediaMessage },
);
const chunks = [];
for await (const chunk of media) {
chunks.push(chunk);
this.logger.info('Download Media successful after explicit updateMediaMessage!');
} catch (reuploadErr) {
this.logger.error(`updateMediaMessage failed: ${reuploadErr?.message} — falling back to downloadContentFromMessage...`);
await new Promise((resolve) => setTimeout(resolve, 5000));
const mediaType = Object.keys(msg.message).find((key) => key.endsWith('Message'));
if (!mediaType) throw new Error('Could not determine mediaType for fallback');

try {
const media = await downloadContentFromMessage(
{
mediaKey: msg.message?.[mediaType]?.mediaKey,
directPath: msg.message?.[mediaType]?.directPath,
url: `https://mmg.whatsapp.net${msg?.message?.[mediaType]?.directPath}`,
},
await this.mapMediaType(mediaType),
{},
);
const chunks = [];
for await (const chunk of media) {
chunks.push(chunk);
}
buffer = Buffer.concat(chunks);
this.logger.info('Download Media with downloadContentFromMessage was successful!');
} catch (fallbackErr) {
this.logger.error('Download Media with downloadContentFromMessage also failed!');
throw fallbackErr;
}
buffer = Buffer.concat(chunks);
this.logger.info('Download Media with downloadContentFromMessage was successful!');
} catch (fallbackErr) {
this.logger.error('Download Media with downloadContentFromMessage also failed!');
throw fallbackErr;
}
}
const typeMessage = getContentType(msg.message);
Expand Down