fix(whatsapp): add error handling to messages.upsert handler (#695)
Wrap the inner message processing loop in a try-catch to prevent a single malformed or edge-case message from crashing the entire handler. Logs the error with remoteJid for debugging while continuing to process remaining messages in the batch. Co-authored-by: Ethan M <ethan@nanoclaw.local> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -173,67 +173,74 @@ export class WhatsAppChannel implements Channel {
|
|||||||
|
|
||||||
this.sock.ev.on('messages.upsert', async ({ messages }) => {
|
this.sock.ev.on('messages.upsert', async ({ messages }) => {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (!msg.message) continue;
|
try {
|
||||||
// Unwrap container types (viewOnceMessageV2, ephemeralMessage,
|
if (!msg.message) continue;
|
||||||
// editedMessage, etc.) so that conversation, extendedTextMessage,
|
// Unwrap container types (viewOnceMessageV2, ephemeralMessage,
|
||||||
// imageMessage, etc. are accessible at the top level.
|
// editedMessage, etc.) so that conversation, extendedTextMessage,
|
||||||
const normalized = normalizeMessageContent(msg.message);
|
// imageMessage, etc. are accessible at the top level.
|
||||||
if (!normalized) continue;
|
const normalized = normalizeMessageContent(msg.message);
|
||||||
const rawJid = msg.key.remoteJid;
|
if (!normalized) continue;
|
||||||
if (!rawJid || rawJid === 'status@broadcast') continue;
|
const rawJid = msg.key.remoteJid;
|
||||||
|
if (!rawJid || rawJid === 'status@broadcast') continue;
|
||||||
|
|
||||||
// Translate LID JID to phone JID if applicable
|
// Translate LID JID to phone JID if applicable
|
||||||
const chatJid = await this.translateJid(rawJid);
|
const chatJid = await this.translateJid(rawJid);
|
||||||
|
|
||||||
const timestamp = new Date(
|
const timestamp = new Date(
|
||||||
Number(msg.messageTimestamp) * 1000,
|
Number(msg.messageTimestamp) * 1000,
|
||||||
).toISOString();
|
).toISOString();
|
||||||
|
|
||||||
// Always notify about chat metadata for group discovery
|
// Always notify about chat metadata for group discovery
|
||||||
const isGroup = chatJid.endsWith('@g.us');
|
const isGroup = chatJid.endsWith('@g.us');
|
||||||
this.opts.onChatMetadata(
|
this.opts.onChatMetadata(
|
||||||
chatJid,
|
chatJid,
|
||||||
timestamp,
|
|
||||||
undefined,
|
|
||||||
'whatsapp',
|
|
||||||
isGroup,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only deliver full message for registered groups
|
|
||||||
const groups = this.opts.registeredGroups();
|
|
||||||
if (groups[chatJid]) {
|
|
||||||
const content =
|
|
||||||
normalized.conversation ||
|
|
||||||
normalized.extendedTextMessage?.text ||
|
|
||||||
normalized.imageMessage?.caption ||
|
|
||||||
normalized.videoMessage?.caption ||
|
|
||||||
'';
|
|
||||||
|
|
||||||
// Skip protocol messages with no text content (encryption keys, read receipts, etc.)
|
|
||||||
if (!content) continue;
|
|
||||||
|
|
||||||
const sender = msg.key.participant || msg.key.remoteJid || '';
|
|
||||||
const senderName = msg.pushName || sender.split('@')[0];
|
|
||||||
|
|
||||||
const fromMe = msg.key.fromMe || false;
|
|
||||||
// Detect bot messages: with own number, fromMe is reliable
|
|
||||||
// since only the bot sends from that number.
|
|
||||||
// With shared number, bot messages carry the assistant name prefix
|
|
||||||
// (even in DMs/self-chat) so we check for that.
|
|
||||||
const isBotMessage = ASSISTANT_HAS_OWN_NUMBER
|
|
||||||
? fromMe
|
|
||||||
: content.startsWith(`${ASSISTANT_NAME}:`);
|
|
||||||
|
|
||||||
this.opts.onMessage(chatJid, {
|
|
||||||
id: msg.key.id || '',
|
|
||||||
chat_jid: chatJid,
|
|
||||||
sender,
|
|
||||||
sender_name: senderName,
|
|
||||||
content,
|
|
||||||
timestamp,
|
timestamp,
|
||||||
is_from_me: fromMe,
|
undefined,
|
||||||
is_bot_message: isBotMessage,
|
'whatsapp',
|
||||||
});
|
isGroup,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only deliver full message for registered groups
|
||||||
|
const groups = this.opts.registeredGroups();
|
||||||
|
if (groups[chatJid]) {
|
||||||
|
const content =
|
||||||
|
normalized.conversation ||
|
||||||
|
normalized.extendedTextMessage?.text ||
|
||||||
|
normalized.imageMessage?.caption ||
|
||||||
|
normalized.videoMessage?.caption ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
// Skip protocol messages with no text content (encryption keys, read receipts, etc.)
|
||||||
|
if (!content) continue;
|
||||||
|
|
||||||
|
const sender = msg.key.participant || msg.key.remoteJid || '';
|
||||||
|
const senderName = msg.pushName || sender.split('@')[0];
|
||||||
|
|
||||||
|
const fromMe = msg.key.fromMe || false;
|
||||||
|
// Detect bot messages: with own number, fromMe is reliable
|
||||||
|
// since only the bot sends from that number.
|
||||||
|
// With shared number, bot messages carry the assistant name prefix
|
||||||
|
// (even in DMs/self-chat) so we check for that.
|
||||||
|
const isBotMessage = ASSISTANT_HAS_OWN_NUMBER
|
||||||
|
? fromMe
|
||||||
|
: content.startsWith(`${ASSISTANT_NAME}:`);
|
||||||
|
|
||||||
|
this.opts.onMessage(chatJid, {
|
||||||
|
id: msg.key.id || '',
|
||||||
|
chat_jid: chatJid,
|
||||||
|
sender,
|
||||||
|
sender_name: senderName,
|
||||||
|
content,
|
||||||
|
timestamp,
|
||||||
|
is_from_me: fromMe,
|
||||||
|
is_bot_message: isBotMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
{ err, remoteJid: msg.key?.remoteJid },
|
||||||
|
'Error processing incoming message',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user