From 508f075bc3776f09a745049107e7e3dbdbcd529f Mon Sep 17 00:00:00 2001 From: Edney Matias Date: Tue, 11 Aug 2026 12:38:48 -0300 Subject: [PATCH 1/2] feat(chatwoot): capture ad-referral metadata from WhatsApp ads and add webp thumbnail fallback Extracts sourceId, sourceType, mediaType and mediaUrl from externalAdReply alongside the existing title/body/thumbnailUrl/sourceUrl fields, and exposes them as a normalized referral object under the outgoing message's content_attributes. Also hardens the ads-thumbnail pipeline: a failed webp decode in Jimp (Jimp core has no webp codec) previously threw an unhandled exception that aborted message delivery after the Chatwoot conversation shell had already been created, leaving an empty conversation behind. Both the thumbnail download and the Jimp resize now fall back gracefully (raw, unresized image) instead of failing the whole send. --- .../chatwoot/services/chatwoot.service.ts | 93 +++++++++++++++---- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index 906fff1881..a352da37fa 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -1057,6 +1057,7 @@ export class ChatwootService { messageBody?: any, sourceId?: string, quotedMsg?: MessageModel, + referral?: Record, ) { if (sourceId && this.isImportHistoryAvailable()) { const messageAlreadySaved = await chatwootImport.getExistingSourceIds([sourceId], conversationId); @@ -1079,17 +1080,24 @@ export class ChatwootService { const sourceReplyId = quotedMsg?.chatwootMessageId || null; + let contentAttributes: Record = {}; + if (messageBody && instance) { const replyToIds = await this.getReplyToIds(messageBody, instance); if (replyToIds.in_reply_to || replyToIds.in_reply_to_external_id) { - const content = JSON.stringify({ - ...replyToIds, - }); - data.append('content_attributes', content); + contentAttributes = { ...contentAttributes, ...replyToIds }; } } + if (referral) { + contentAttributes = { ...contentAttributes, referral }; + } + + if (Object.keys(contentAttributes).length > 0) { + data.append('content_attributes', JSON.stringify(contentAttributes)); + } + if (sourceReplyId) { data.append('source_reply_id', sourceReplyId.toString()); } @@ -1727,21 +1735,55 @@ export class ChatwootService { body: string; thumbnailUrl: string; sourceUrl: string; + sourceId?: string; + sourceType?: string; + mediaType?: string; + mediaUrl?: string; } + const externalAdReply = + msg.extendedTextMessage?.contextInfo?.externalAdReply || + msg.contextInfo?.externalAdReply || + msg.message?.extendedTextMessage?.contextInfo?.externalAdReply || + msg.message?.contextInfo?.externalAdReply; + const adsMessage: AdsMessage | undefined = { - title: msg.extendedTextMessage?.contextInfo?.externalAdReply?.title || msg.contextInfo?.externalAdReply?.title, - body: msg.extendedTextMessage?.contextInfo?.externalAdReply?.body || msg.contextInfo?.externalAdReply?.body, - thumbnailUrl: - msg.extendedTextMessage?.contextInfo?.externalAdReply?.thumbnailUrl || - msg.contextInfo?.externalAdReply?.thumbnailUrl, - sourceUrl: - msg.extendedTextMessage?.contextInfo?.externalAdReply?.sourceUrl || msg.contextInfo?.externalAdReply?.sourceUrl, + title: externalAdReply?.title, + body: externalAdReply?.body, + thumbnailUrl: externalAdReply?.thumbnailUrl, + sourceUrl: externalAdReply?.sourceUrl, + sourceId: externalAdReply?.sourceId, + sourceType: externalAdReply?.sourceType, + mediaType: externalAdReply?.mediaType, + mediaUrl: externalAdReply?.mediaUrl, }; return adsMessage; } + private buildReferralAttributes(adsMessage: { + title?: string; + body?: string; + thumbnailUrl?: string; + sourceUrl?: string; + sourceId?: string; + sourceType?: string; + mediaType?: string; + mediaUrl?: string; + }) { + const referral: Record = {}; + + if (adsMessage.sourceId) referral.source_id = adsMessage.sourceId; + if (adsMessage.sourceType) referral.source_type = adsMessage.sourceType; + if (adsMessage.sourceUrl) referral.source_url = adsMessage.sourceUrl; + if (adsMessage.title) referral.headline = adsMessage.title; + if (adsMessage.body) referral.body = adsMessage.body; + if (adsMessage.mediaType) referral.media_type = adsMessage.mediaType; + if (adsMessage.thumbnailUrl) referral.image_url = adsMessage.thumbnailUrl; + + return Object.keys(referral).length > 0 ? referral : undefined; + } + private getReactionMessage(msg: any) { interface ReactionMessage { key: { @@ -2212,7 +2254,13 @@ export class ChatwootService { const isAdsMessage = (adsMessage && adsMessage.title) || adsMessage.body || adsMessage.thumbnailUrl; if (isAdsMessage) { - const imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' }); + let imgBuffer; + try { + imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' }); + } catch (error) { + this.logger.warn(`Failed to download ads thumbnail: ${error?.message || error}`); + return; + } const extension = mimeTypes.extension(imgBuffer.headers['content-type']); const mimeType = extension && mimeTypes.lookup(extension); @@ -2226,12 +2274,19 @@ export class ChatwootService { const nameFile = `${random}.${mimeTypes.extension(mimeType)}`; const fileData = Buffer.from(imgBuffer.data, 'binary'); - const img = await Jimp.read(fileData); - await img.cover({ - w: 320, - h: 180, - }); - const processedBuffer = await img.getBuffer(JimpMime.png); + let processedBuffer: Buffer = fileData; + try { + const img = await Jimp.read(fileData); + await img.cover({ + w: 320, + h: 180, + }); + processedBuffer = await img.getBuffer(JimpMime.png); + } catch (error) { + this.logger.warn( + `Failed to process ads thumbnail with Jimp, sending raw image: ${error?.message || error}`, + ); + } const fileStream = new Readable(); fileStream._read = () => {}; // _read is required but you can noop it @@ -2256,6 +2311,8 @@ export class ChatwootService { instance, body, 'WAID:' + body.key.id, + quotedMsg, + this.buildReferralAttributes(adsMessage), ); if (!send) { From 9439537df4835ac9313e9fa3bdc9f9fc15781ed6 Mon Sep 17 00:00:00 2001 From: Edney Matias Date: Tue, 11 Aug 2026 12:52:45 -0300 Subject: [PATCH 2/2] fix(chatwoot): keep ad-referral messages flowing when thumbnail fetch fails Previously, a failed thumbnail download or unresolved mimetype aborted the entire ad message send, silently dropping both the text content and the referral metadata. Now falls back to a text-only message via createMessage(), which gains an optional referral parameter to carry the attribution data through that path. --- .../chatwoot/services/chatwoot.service.ts | 61 ++++++++++++++----- 1 file changed, 45 insertions(+), 16 deletions(-) diff --git a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts index a352da37fa..5a684fee0a 100644 --- a/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts +++ b/src/api/integrations/chatbot/chatwoot/services/chatwoot.service.ts @@ -931,6 +931,7 @@ export class ChatwootService { messageBody?: any, sourceId?: string, quotedMsg?: MessageModel, + referral?: Record, ) { const client = await this.clientCw(instance); @@ -943,6 +944,12 @@ export class ChatwootService { const sourceReplyId = quotedMsg?.chatwootMessageId || null; + let contentAttributes: Record = { ...replyToIds }; + + if (referral) { + contentAttributes = { ...contentAttributes, referral }; + } + const message = await client.messages.create({ accountId: this.provider.accountId, conversationId: conversationId, @@ -952,9 +959,7 @@ export class ChatwootService { attachments: attachments, private: privateMessage || false, source_id: sourceId, - content_attributes: { - ...replyToIds, - }, + content_attributes: contentAttributes, source_reply_id: sourceReplyId ? sourceReplyId.toString() : null, }, }); @@ -2254,12 +2259,45 @@ export class ChatwootService { const isAdsMessage = (adsMessage && adsMessage.title) || adsMessage.body || adsMessage.thumbnailUrl; if (isAdsMessage) { + const truncStr = (str: string, len: number) => { + if (!str) return ''; + + return str.length > len ? str.substring(0, len) + '...' : str; + }; + + const title = truncStr(adsMessage.title, 40); + const description = truncStr(adsMessage?.body, 75); + const referralAttributes = this.buildReferralAttributes(adsMessage); + const adCaption = `${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`; + + const sendWithoutThumbnail = async () => { + const send = await this.createMessage( + instance, + getConversation, + adCaption, + messageType, + false, + [], + body, + 'WAID:' + body.key.id, + quotedMsg, + referralAttributes, + ); + + if (!send) { + this.logger.warn('message not sent'); + return; + } + + return send; + }; + let imgBuffer; try { imgBuffer = await axios.get(adsMessage.thumbnailUrl, { responseType: 'arraybuffer' }); } catch (error) { this.logger.warn(`Failed to download ads thumbnail: ${error?.message || error}`); - return; + return sendWithoutThumbnail(); } const extension = mimeTypes.extension(imgBuffer.headers['content-type']); @@ -2267,7 +2305,7 @@ export class ChatwootService { if (!mimeType) { this.logger.warn('mimetype of Ads message not found'); - return; + return sendWithoutThumbnail(); } const random = Math.random().toString(36).substring(7); @@ -2293,26 +2331,17 @@ export class ChatwootService { fileStream.push(processedBuffer); fileStream.push(null); - const truncStr = (str: string, len: number) => { - if (!str) return ''; - - return str.length > len ? str.substring(0, len) + '...' : str; - }; - - const title = truncStr(adsMessage.title, 40); - const description = truncStr(adsMessage?.body, 75); - const send = await this.sendData( getConversation, fileStream, nameFile, messageType, - `${bodyMessage}\n\n\n**${title}**\n${description}\n${adsMessage.sourceUrl}`, + adCaption, instance, body, 'WAID:' + body.key.id, quotedMsg, - this.buildReferralAttributes(adsMessage), + referralAttributes, ); if (!send) {