From f711fcca982c144dd8570ce78a5108fad24b8a8a Mon Sep 17 00:00:00 2001 From: Berg Pinheiro Date: Mon, 6 Apr 2026 13:35:45 +0700 Subject: [PATCH] [core] add Chatwoot support for WhatsApp status replies Detect WhatsApp status replies in incoming messages, append localized status context to Chatwoot content, and attach quoted status media when available while preserving current non-status behavior. closes #1995 - Reply message to WhatsApp Status Context in Chatwoot Messages fixes #1991 - Include WhatsApp Status Context in Chatwoot Messages --- .../chatwoot/consumers/waha/message.any.ts | 13 ++ src/apps/chatwoot/i18n/locales/ar-AE.yaml | 9 + src/apps/chatwoot/i18n/locales/bn-BD.yaml | 9 + src/apps/chatwoot/i18n/locales/de-DE.yaml | 9 + src/apps/chatwoot/i18n/locales/en-US.yaml | 9 + src/apps/chatwoot/i18n/locales/es-ES.yaml | 9 + src/apps/chatwoot/i18n/locales/fa-IR.yaml | 9 + src/apps/chatwoot/i18n/locales/fr-FR.yaml | 9 + src/apps/chatwoot/i18n/locales/he-IL.yaml | 9 + src/apps/chatwoot/i18n/locales/hi-IN.yaml | 9 + src/apps/chatwoot/i18n/locales/id-ID.yaml | 9 + src/apps/chatwoot/i18n/locales/it-IT.yaml | 9 + src/apps/chatwoot/i18n/locales/pa-PK.yaml | 9 + src/apps/chatwoot/i18n/locales/pt-BR.yaml | 9 + src/apps/chatwoot/i18n/locales/ru-RU.yaml | 9 + src/apps/chatwoot/i18n/locales/tr-TR.yaml | 9 + src/apps/chatwoot/i18n/locales/uk-UA.yaml | 9 + src/apps/chatwoot/i18n/locales/ur-PK.yaml | 9 + src/apps/chatwoot/i18n/locales/zh-CN.yaml | 9 + src/apps/chatwoot/i18n/templates.ts | 6 + .../to/chatwoot/StatusReplyMessage.ts | 215 ++++++++++++++++++ .../messages/to/chatwoot/TextMessage.ts | 14 +- .../chatwoot/messages/to/chatwoot/index.ts | 1 + src/apps/chatwoot/waha/engines.ts | 126 ++++++++++ src/apps/chatwoot/waha/index.ts | 2 + src/core/utils/pwa.ts | 9 + 26 files changed, 542 insertions(+), 6 deletions(-) create mode 100644 src/apps/chatwoot/messages/to/chatwoot/StatusReplyMessage.ts diff --git a/src/apps/chatwoot/consumers/waha/message.any.ts b/src/apps/chatwoot/consumers/waha/message.any.ts index b2b81b23..a8dfc712 100644 --- a/src/apps/chatwoot/consumers/waha/message.any.ts +++ b/src/apps/chatwoot/consumers/waha/message.any.ts @@ -26,6 +26,7 @@ import { PollMessage, ListMessage, ShareContactMessage, + StatusReplyMessage, TextMessage, UnsupportedMessage, AlbumMessage, @@ -86,6 +87,18 @@ export class MessageAnyHandler extends MessageBaseHandler { return msg; } + converter = new StatusReplyMessage( + this.l, + this.logger, + this.waha, + this.job, + this.session, + ); + msg = await converter.convert(payload, protoMessage); + if (msg) { + return msg; + } + converter = new TextMessage(this.l, this.logger, this.waha, this.job); msg = await converter.convert(payload, null); if (msg) { diff --git a/src/apps/chatwoot/i18n/locales/ar-AE.yaml b/src/apps/chatwoot/i18n/locales/ar-AE.yaml index 7ba1d363..84c42cae 100644 --- a/src/apps/chatwoot/i18n/locales/ar-AE.yaml +++ b/src/apps/chatwoot/i18n/locales/ar-AE.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **تم إرسال PIX نسخ ولصق** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **رد على حالة واتساب** + 🏷️ **النوع:** {{type}} + {{#quotedText}} + 📝 **نص الحالة:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **هذا النوع من الرسائل غير مدعوم في هذا الصندوق الوارد.** 📱 يرجى فتح **WhatsApp** لعرضه. diff --git a/src/apps/chatwoot/i18n/locales/bn-BD.yaml b/src/apps/chatwoot/i18n/locales/bn-BD.yaml index e88d2900..8cc78a74 100644 --- a/src/apps/chatwoot/i18n/locales/bn-BD.yaml +++ b/src/apps/chatwoot/i18n/locales/bn-BD.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX কপি এবং পেস্ট পাঠানো হয়েছে** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **WhatsApp স্ট্যাটাসের জবাব** + 🏷️ **ধরন:** {{type}} + {{#quotedText}} + 📝 **স্ট্যাটাসের টেক্সট:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **এই ধরনের বার্তা এই ইনবক্সে সমর্থিত নয়।** 📱 এটি দেখতে অনুগ্রহ করে **WhatsApp** খুলুন। diff --git a/src/apps/chatwoot/i18n/locales/de-DE.yaml b/src/apps/chatwoot/i18n/locales/de-DE.yaml index 38feb30f..195d0d65 100644 --- a/src/apps/chatwoot/i18n/locales/de-DE.yaml +++ b/src/apps/chatwoot/i18n/locales/de-DE.yaml @@ -492,6 +492,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Kopieren und Einfügen gesendet** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Antwort auf WhatsApp-Status** + 🏷️ **Typ:** {{type}} + {{#quotedText}} + 📝 **Status-Text:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **Dieser Nachrichtentyp wird in dieser Inbox nicht unterstützt.** 📱 Bitte öffne **WhatsApp**, um ihn anzusehen. diff --git a/src/apps/chatwoot/i18n/locales/en-US.yaml b/src/apps/chatwoot/i18n/locales/en-US.yaml index 235b33df..9a984f2d 100644 --- a/src/apps/chatwoot/i18n/locales/en-US.yaml +++ b/src/apps/chatwoot/i18n/locales/en-US.yaml @@ -270,6 +270,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Copy and Paste sent** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Reply to WhatsApp Status** + 🏷️ **Type:** {{type}} + {{#quotedText}} + 📝 **Status Text:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.has.media.no.media: |- {{#content}} {{{content}}} diff --git a/src/apps/chatwoot/i18n/locales/es-ES.yaml b/src/apps/chatwoot/i18n/locales/es-ES.yaml index 59989688..f99b4936 100644 --- a/src/apps/chatwoot/i18n/locales/es-ES.yaml +++ b/src/apps/chatwoot/i18n/locales/es-ES.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Copiar y Pegar enviado** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Respuesta al estado de WhatsApp** + 🏷️ **Tipo:** {{type}} + {{#quotedText}} + 📝 **Texto del Estado:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **Este tipo de mensaje no es compatible con esta bandeja de entrada.** 📱 Por favor, abre **WhatsApp** para verlo. diff --git a/src/apps/chatwoot/i18n/locales/fa-IR.yaml b/src/apps/chatwoot/i18n/locales/fa-IR.yaml index 560b3313..6d032767 100644 --- a/src/apps/chatwoot/i18n/locales/fa-IR.yaml +++ b/src/apps/chatwoot/i18n/locales/fa-IR.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX کپی و چسباندن ارسال شد** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **پاسخ به وضعیت واتساپ** + 🏷️ **نوع:** {{type}} + {{#quotedText}} + 📝 **متن وضعیت:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **این نوع پیام در این صندوق ورودی پشتیبانی نمی‌شود.** 📱 لطفاً برای مشاهده آن **WhatsApp** را باز کنید. diff --git a/src/apps/chatwoot/i18n/locales/fr-FR.yaml b/src/apps/chatwoot/i18n/locales/fr-FR.yaml index 67916463..829f00e8 100644 --- a/src/apps/chatwoot/i18n/locales/fr-FR.yaml +++ b/src/apps/chatwoot/i18n/locales/fr-FR.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Copier et Coller envoyé** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Réponse au statut WhatsApp** + 🏷️ **Type :** {{type}} + {{#quotedText}} + 📝 **Texte du Statut :** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **Ce type de message n’est pas pris en charge dans cette boîte de réception.** 📱 Veuillez ouvrir **WhatsApp** pour l’afficher. diff --git a/src/apps/chatwoot/i18n/locales/he-IL.yaml b/src/apps/chatwoot/i18n/locales/he-IL.yaml index ffa1c77e..f235b619 100644 --- a/src/apps/chatwoot/i18n/locales/he-IL.yaml +++ b/src/apps/chatwoot/i18n/locales/he-IL.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX העתק והדבק נשלח** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **תגובה לסטטוס ב-WhatsApp** + 🏷️ **סוג:** {{type}} + {{#quotedText}} + 📝 **טקסט הסטטוס:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **סוג הודעה זה אינו נתמך בתיבה זו.** 📱 אנא פתח/י את **WhatsApp** כדי לצפות בזה. diff --git a/src/apps/chatwoot/i18n/locales/hi-IN.yaml b/src/apps/chatwoot/i18n/locales/hi-IN.yaml index 9718af00..45d7c871 100644 --- a/src/apps/chatwoot/i18n/locales/hi-IN.yaml +++ b/src/apps/chatwoot/i18n/locales/hi-IN.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX कॉपी और पेस्ट भेजा गया** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **WhatsApp स्टेटस का जवाब** + 🏷️ **प्रकार:** {{type}} + {{#quotedText}} + 📝 **स्टेटस टेक्स्ट:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **इस प्रकार का संदेश इस इनबॉक्स में समर्थित नहीं है।** 📱 इसे देखने के लिए कृपया **WhatsApp** खोलें। diff --git a/src/apps/chatwoot/i18n/locales/id-ID.yaml b/src/apps/chatwoot/i18n/locales/id-ID.yaml index 17b01b04..341b8d8f 100644 --- a/src/apps/chatwoot/i18n/locales/id-ID.yaml +++ b/src/apps/chatwoot/i18n/locales/id-ID.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Salin dan Tempel terkirim** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Balasan ke Status WhatsApp** + 🏷️ **Tipe:** {{type}} + {{#quotedText}} + 📝 **Teks Status:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **Jenis pesan ini tidak didukung di kotak masuk ini.** 📱 Silakan buka **WhatsApp** untuk melihatnya. diff --git a/src/apps/chatwoot/i18n/locales/it-IT.yaml b/src/apps/chatwoot/i18n/locales/it-IT.yaml index 41a29719..4c82cbbf 100644 --- a/src/apps/chatwoot/i18n/locales/it-IT.yaml +++ b/src/apps/chatwoot/i18n/locales/it-IT.yaml @@ -270,6 +270,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Copia e Incolla inviato** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Risposta allo stato di WhatsApp** + 🏷️ **Tipo:** {{type}} + {{#quotedText}} + 📝 **Testo dello Stato:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.has.media.no.media: |- {{#content}} {{{content}}} diff --git a/src/apps/chatwoot/i18n/locales/pa-PK.yaml b/src/apps/chatwoot/i18n/locales/pa-PK.yaml index 8ddc1867..829ce79a 100644 --- a/src/apps/chatwoot/i18n/locales/pa-PK.yaml +++ b/src/apps/chatwoot/i18n/locales/pa-PK.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX ਕਾਪੀ ਅਤੇ ਪੇਸਟ ਭੇਜਿਆ ਗਿਆ** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **WhatsApp ਸਟੇਟਸ ਦਾ ਜਵਾਬ** + 🏷️ **ਕਿਸਮ:** {{type}} + {{#quotedText}} + 📝 **ਸਟੇਟਸ ਲਿਖਤ:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **ਇਹ ਸੁਨੇਹੇ ਦੀ ਕਿਸਮ ਇਸ ਇਨਬਾਕਸ ਵਿੱਚ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ।** 📱 ਇਸਨੂੰ ਦੇਖਣ ਲਈ ਕਿਰਪਾ ਕਰਕੇ **WhatsApp** ਖੋਲ੍ਹੋ। diff --git a/src/apps/chatwoot/i18n/locales/pt-BR.yaml b/src/apps/chatwoot/i18n/locales/pt-BR.yaml index 09052fc4..513efd2b 100644 --- a/src/apps/chatwoot/i18n/locales/pt-BR.yaml +++ b/src/apps/chatwoot/i18n/locales/pt-BR.yaml @@ -494,6 +494,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Copia e Cola enviado** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Resposta a Status do WhatsApp** + 🏷️ **Tipo:** {{type}} + {{#quotedText}} + 📝 **Texto do Status:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **Esse tipo de mensagem não é suportado nesta caixa de entrada.** 📱 Por favor, abra o **WhatsApp** para visualizá-la. diff --git a/src/apps/chatwoot/i18n/locales/ru-RU.yaml b/src/apps/chatwoot/i18n/locales/ru-RU.yaml index df4acab7..1d8ea10c 100644 --- a/src/apps/chatwoot/i18n/locales/ru-RU.yaml +++ b/src/apps/chatwoot/i18n/locales/ru-RU.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Копировать и вставить отправлен** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Ответ на статус WhatsApp** + 🏷️ **Тип:** {{type}} + {{#quotedText}} + 📝 **Текст Статуса:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **Этот тип сообщения не поддерживается в этом Inbox.** 📱 Пожалуйста, откройте **WhatsApp**, чтобы просмотреть. diff --git a/src/apps/chatwoot/i18n/locales/tr-TR.yaml b/src/apps/chatwoot/i18n/locales/tr-TR.yaml index e859c6b7..c1e90328 100644 --- a/src/apps/chatwoot/i18n/locales/tr-TR.yaml +++ b/src/apps/chatwoot/i18n/locales/tr-TR.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Kopyala ve Yapıştır gönderildi** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **WhatsApp Durumuna Yanıt** + 🏷️ **Tür:** {{type}} + {{#quotedText}} + 📝 **Durum Metni:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **Bu mesaj türü bu gelen kutusunda desteklenmiyor.** 📱 Görüntülemek için lütfen **WhatsApp’ı** açın. diff --git a/src/apps/chatwoot/i18n/locales/uk-UA.yaml b/src/apps/chatwoot/i18n/locales/uk-UA.yaml index 8f2821b1..c87607b8 100644 --- a/src/apps/chatwoot/i18n/locales/uk-UA.yaml +++ b/src/apps/chatwoot/i18n/locales/uk-UA.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX Копіювати та Вставити надіслано** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **Відповідь на статус WhatsApp** + 🏷️ **Тип:** {{type}} + {{#quotedText}} + 📝 **Текст Статусу:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **Цей тип повідомлення не підтримується в цих вхідних.** 📱 Будь ласка, відкрийте **WhatsApp**, щоб переглянути. diff --git a/src/apps/chatwoot/i18n/locales/ur-PK.yaml b/src/apps/chatwoot/i18n/locales/ur-PK.yaml index 87289346..27c8c526 100644 --- a/src/apps/chatwoot/i18n/locales/ur-PK.yaml +++ b/src/apps/chatwoot/i18n/locales/ur-PK.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX کاپی اور پیسٹ بھیج دیا گیا** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **WhatsApp اسٹیٹس کا جواب** + 🏷️ **قسم:** {{type}} + {{#quotedText}} + 📝 **اسٹیٹس کا متن:** {{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **اس قسم کا پیغام اس انبکس میں معاونت یافتہ نہیں ہے۔** 📱 اسے دیکھنے کے لیے براہ کرم **WhatsApp** کھولیں۔ diff --git a/src/apps/chatwoot/i18n/locales/zh-CN.yaml b/src/apps/chatwoot/i18n/locales/zh-CN.yaml index 3bf52277..676335c7 100644 --- a/src/apps/chatwoot/i18n/locales/zh-CN.yaml +++ b/src/apps/chatwoot/i18n/locales/zh-CN.yaml @@ -489,6 +489,15 @@ whatsapp.to.chatwoot.message.pix: |- 💳 **PIX 复制粘贴已发送** {{/pixData}} +whatsapp.to.chatwoot.message.status.reply: |- + 📣 **回复 WhatsApp 状态** + 🏷️ **类型:**{{type}} + {{#quotedText}} + 📝 **状态文本:**{{quotedText}} + {{/quotedText}} + + {{content}} + whatsapp.to.chatwoot.message.unsupported: |- ⚠️ **此收件箱不支持此消息类型。** 📱 请打开 **WhatsApp** 查看。 diff --git a/src/apps/chatwoot/i18n/templates.ts b/src/apps/chatwoot/i18n/templates.ts index d37239b5..95799553 100644 --- a/src/apps/chatwoot/i18n/templates.ts +++ b/src/apps/chatwoot/i18n/templates.ts @@ -54,6 +54,7 @@ export enum TKey { WA_TO_CW_MESSAGE_POLL = 'whatsapp.to.chatwoot.message.poll', WA_TO_CW_MESSAGE_EVENT = 'whatsapp.to.chatwoot.message.event', WA_TO_CW_MESSAGE_PIX = 'whatsapp.to.chatwoot.message.pix', + WA_TO_CW_MESSAGE_STATUS_REPLY = 'whatsapp.to.chatwoot.message.status.reply', WA_TO_CW_MESSAGE_UNSUPPORTED = 'whatsapp.to.chatwoot.message.unsupported', WA_TO_CW_MESSAGE_FACEBOOK_AD = 'whatsapp.to.chatwoot.message.facebook.ad', WA_TO_CW_MESSAGE_LIST = 'whatsapp.to.chatwoot.message.list', @@ -189,6 +190,11 @@ export type TemplatePayloads = { message: proto.Message | null; pixData: PixTemplatePayload; }; + [TKey.WA_TO_CW_MESSAGE_STATUS_REPLY]: { + type: string; + quotedText?: string; + content: string; + }; [TKey.WA_TO_CW_MESSAGE_UNSUPPORTED]: { details: Link }; [TKey.WA_TO_CW_MESSAGE_FACEBOOK_AD]: { payload: WAMessage; diff --git a/src/apps/chatwoot/messages/to/chatwoot/StatusReplyMessage.ts b/src/apps/chatwoot/messages/to/chatwoot/StatusReplyMessage.ts new file mode 100644 index 00000000..bb29aff4 --- /dev/null +++ b/src/apps/chatwoot/messages/to/chatwoot/StatusReplyMessage.ts @@ -0,0 +1,215 @@ +import type { proto } from '@adiwajshing/baileys'; +import { ILogger } from '@waha/apps/app_sdk/ILogger'; +import { WAHASelf, WAHASessionAPI } from '@waha/apps/app_sdk/waha/WAHASelf'; +import { ChatWootMessagePartial } from '@waha/apps/chatwoot/consumers/waha/base'; +import { Locale } from '@waha/apps/chatwoot/i18n/locale'; +import { TKey } from '@waha/apps/chatwoot/i18n/templates'; +import { WhatsappToMarkdown } from '@waha/apps/chatwoot/messages/to/chatwoot/utils/markdown'; +import { EngineHelper, QuotedMedia } from '@waha/apps/chatwoot/waha'; +import { Jid } from '@waha/core/engines/const'; +import { WAMessage } from '@waha/structures/responses.dto'; +import { Job } from 'bullmq'; +import { TextMessage } from './TextMessage'; +import { isEmptyString } from './utils/text'; +import { SendAttachment } from '@waha/apps/chatwoot/client/types'; +import esm from '@waha/vendor/esm'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const mime = require('mime-types'); + +type StatusReplyType = 'text' | 'image' | 'audio' | 'video' | 'unknown'; + +interface StatusQuotedMessageInfo { + data?: any; + id: string; + type: StatusReplyType; + text?: string; +} + +export class StatusReplyMessage extends TextMessage { + constructor( + locale: Locale, + logger: ILogger, + waha: WAHASelf, + job: Job, + private readonly session: WAHASessionAPI, + ) { + super(locale, logger, waha, job); + } + + async convert( + payload: WAMessage, + protoMessage: proto.Message | null, + ): Promise { + if (!payload.replyTo?.id) { + return null; + } + if (!EngineHelper.IsReplyToStatus(payload, protoMessage)) { + return null; + } + + let attachments: SendAttachment[] = []; + const quoted = this.getStatusQuotedMessageInfo(payload); + const wamessage = await this.fetchStatusMessage(quoted.id); + if (wamessage?.media?.url) { + const extension = mime.extension(wamessage.media.mimetype); + wamessage.media.filename = `status-message.${extension}`; + const quotedAttachments = await super.getAttachments(wamessage); + attachments = [...attachments, ...quotedAttachments]; + } else { + const replyData = payload.replyTo._data; + const quotedMedia = EngineHelper.ExtractQuotedMedia(replyData); + if (quotedMedia) { + const attachment = + await this.downloadQuotedMediaAttachment(quotedMedia); + if (attachment) { + attachments.push(attachment); + } + } + } + + // Act as regular text message, but we'll adjust some properties + const msg = await super.convert(payload, protoMessage); + // First goes Quoted attachments, then what sender sent + msg.attachments = [...attachments, ...msg.attachments]; + + // Wrap content with additional info + const quotedText = quoted.text ?? wamessage?.body ?? payload.replyTo?.body; + if ( + isEmptyString(quotedText) && + isEmptyString(msg.content) && + attachments.length === 0 + ) { + // Nothing to send. + // Must check msg.content before wrapping it in status reply + return null; + } + + msg.content = this.locale.r(TKey.WA_TO_CW_MESSAGE_STATUS_REPLY, { + type: quoted.type, + quotedText: WhatsappToMarkdown(quotedText), + content: msg.content, + }); + return msg; + } + + private getStatusQuotedMessageInfo( + payload: WAMessage, + ): StatusQuotedMessageInfo { + const replyData = payload.replyTo._data; + const imageMessage = + replyData?.imageMessage || this.getWebjsMediaData(replyData, 'image'); + if (imageMessage) { + return { + data: replyData, + id: payload.replyTo.id, + type: 'image', + text: imageMessage.caption ?? payload.replyTo.body ?? replyData?.body, + }; + } + + const videoMessage = + replyData?.videoMessage || this.getWebjsMediaData(replyData, 'video'); + if (videoMessage) { + return { + data: replyData, + id: payload.replyTo.id, + type: 'video', + text: videoMessage.caption ?? payload.replyTo.body ?? replyData?.body, + }; + } + + const audioMessage = + replyData?.audioMessage || this.getWebjsMediaData(replyData, 'audio'); + if (audioMessage) { + return { + data: replyData, + id: payload.replyTo.id, + type: 'audio', + text: audioMessage.caption ?? payload.replyTo.body ?? replyData?.body, + }; + } + + const quotedText = + replyData?.extendedTextMessage?.text ?? + replyData?.conversation ?? + payload.replyTo.body ?? + replyData?.body; + return { + data: replyData, + id: payload.replyTo.id, + type: quotedText ? 'text' : 'unknown', + text: quotedText, + }; + } + + private getWebjsMediaData( + replyData: any, + expectedKind: 'image' | 'video' | 'audio', + ): any | null { + if (!replyData) { + return null; + } + const kind = replyData.kind ?? replyData.type; + if (kind !== expectedKind) { + return null; + } + return replyData; + } + + private async downloadQuotedMediaAttachment( + quotedMedia: QuotedMedia, + ): Promise { + try { + const decrypt = !!quotedMedia.mediaKey; + const mediaKeyBuffer = decrypt + ? Buffer.from(quotedMedia.mediaKey, 'base64') + : undefined; + const stream = await esm.b.downloadContentFromMessage( + { + mediaKey: mediaKeyBuffer, + directPath: quotedMedia.directPath, + url: quotedMedia.url, + }, + quotedMedia.mediaType as any, + {}, + decrypt, + ); + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(chunk); + } + const buffer = Buffer.concat(chunks); + const extension = mime.extension(quotedMedia.mimetype); + const filename = `status-message.${extension}`; + return { + content: buffer.toString('base64'), + filename: filename, + encoding: 'base64', + }; + } catch (error) { + this.logger.warn('Failed to download quoted status media directly'); + this.logger.debug(error); + return null; + } + } + + private async fetchStatusMessage( + replyMessageId: string, + ): Promise { + try { + const quotedMessage = await this.session.getMessageById( + Jid.BROADCAST, + replyMessageId, + true, + ); + return quotedMessage || null; + } catch (error) { + this.logger.warn( + `Failed to fetch original status message '${replyMessageId}'`, + ); + this.logger.debug(error); + return null; + } + } +} diff --git a/src/apps/chatwoot/messages/to/chatwoot/TextMessage.ts b/src/apps/chatwoot/messages/to/chatwoot/TextMessage.ts index bf9b55f3..b5dcb67c 100644 --- a/src/apps/chatwoot/messages/to/chatwoot/TextMessage.ts +++ b/src/apps/chatwoot/messages/to/chatwoot/TextMessage.ts @@ -17,10 +17,10 @@ const mime = require('mime-types'); export class TextMessage implements MessageToChatWootConverter { constructor( - private readonly locale: Locale, - private readonly logger: ILogger, - private readonly waha: WAHASelf, - private readonly job: Job, + protected readonly locale: Locale, + protected readonly logger: ILogger, + protected readonly waha: WAHASelf, + protected readonly job: Job, ) {} async convert( @@ -52,12 +52,14 @@ export class TextMessage implements MessageToChatWootConverter { } return { content: WhatsappToMarkdown(content), - attachments, + attachments: attachments, private: undefined, }; } - private async getAttachments(payload: WAMessage): Promise { + protected async getAttachments( + payload: WAMessage, + ): Promise { const hasMedia = payload.media?.url; if (!hasMedia) { return []; diff --git a/src/apps/chatwoot/messages/to/chatwoot/index.ts b/src/apps/chatwoot/messages/to/chatwoot/index.ts index 6e2436d7..ae9d1398 100644 --- a/src/apps/chatwoot/messages/to/chatwoot/index.ts +++ b/src/apps/chatwoot/messages/to/chatwoot/index.ts @@ -13,6 +13,7 @@ export { EventMessage } from './EventMessage'; export { PixMessage } from './PixMessage'; export { ListMessage } from './ListMessage'; export { AlbumMessage } from './AlbumMessage'; +export { StatusReplyMessage } from './StatusReplyMessage'; export type Awaitable = T | Promise; diff --git a/src/apps/chatwoot/waha/engines.ts b/src/apps/chatwoot/waha/engines.ts index 15029772..8aabd1bd 100644 --- a/src/apps/chatwoot/waha/engines.ts +++ b/src/apps/chatwoot/waha/engines.ts @@ -1,18 +1,34 @@ import * as lodash from 'lodash'; +import type { proto } from '@adiwajshing/baileys'; import { WhatsAppMessage } from '@waha/apps/chatwoot/storage'; import { WAHAEngine } from '@waha/structures/enums.dto'; import { getEngineName } from '@waha/version'; import { Message as MessageInstance } from 'whatsapp-web.js/src/structures'; +import { Jid } from '@waha/core/engines/const'; import { isLidUser, isPnUser, toCusFormat } from '@waha/core/utils/jids'; import { parseMessageIdSerialized } from '@waha/core/utils/ids'; import { WAMessage } from '@waha/structures/responses.dto'; import { CallData } from '@waha/structures/calls.dto'; +import { getContextInfo } from '@waha/core/utils/pwa'; + +export interface QuotedMedia { + url?: string; + directPath: string; + mediaKey?: string; + mimetype: string; + mediaType: string; +} interface IEngineHelper { ChatID(message: WAMessage | any): string; CallChatID(call: CallData | any): string; + IsReplyToStatus( + message: WAMessage, + protoMessage: proto.Message | null, + ): boolean; + WhatsAppMessageKeys(message: any): WhatsAppMessage; IterateMessages( @@ -24,6 +40,8 @@ interface IEngineHelper { FilterChatIdsForMessages(chats: string[]): string[]; SupportsAllChatForMessage(): boolean; + + ExtractQuotedMedia(replyData: any): QuotedMedia | null; } class NOWEBHelper implements IEngineHelper { @@ -35,6 +53,14 @@ class NOWEBHelper implements IEngineHelper { return call.from; } + IsReplyToStatus( + message: WAMessage, + protoMessage: proto.Message | null, + ): boolean { + void message; + return getContextInfo(protoMessage)?.remoteJid === Jid.BROADCAST; + } + WhatsAppMessageKeys(message: any): WhatsAppMessage { const timestamp = parseInt(message.messageTimestamp) * 1000; return { @@ -63,6 +89,10 @@ class NOWEBHelper implements IEngineHelper { SupportsAllChatForMessage(): boolean { return true; } + + ExtractQuotedMedia(replyData: any): QuotedMedia | null { + return extractProtoQuotedMedia(replyData); + } } class GOWSHelper implements IEngineHelper { @@ -74,6 +104,14 @@ class GOWSHelper implements IEngineHelper { return call._data?.CallCreator || call.from; } + IsReplyToStatus( + message: WAMessage, + protoMessage: proto.Message | null, + ): boolean { + void message; + return getContextInfo(protoMessage)?.remoteJid === Jid.BROADCAST; + } + /** * Parse API response and get the data * API Response depends on engine right now @@ -107,6 +145,10 @@ class GOWSHelper implements IEngineHelper { ContactIsMy(contact) { return true; } + + ExtractQuotedMedia(replyData: any): QuotedMedia | null { + return extractProtoQuotedMedia(replyData, true); + } } class WEBJSHelper implements IEngineHelper { @@ -118,6 +160,14 @@ class WEBJSHelper implements IEngineHelper { return call.from; } + IsReplyToStatus( + message: WAMessage, + protoMessage: proto.Message | null, + ): boolean { + void protoMessage; + return message._data?.quotedRemoteJid === Jid.BROADCAST; + } + /** * Parse API response and get the data for WEBJS engine */ @@ -161,6 +211,10 @@ class WEBJSHelper implements IEngineHelper { ContactIsMy(contact) { return contact.isMyContact; } + + ExtractQuotedMedia(replyData: any): QuotedMedia | null { + return extractBrowserQuotedMedia(replyData); + } } class WPPHelper implements IEngineHelper { @@ -172,6 +226,14 @@ class WPPHelper implements IEngineHelper { return call.from; } + IsReplyToStatus( + message: WAMessage, + protoMessage: proto.Message | null, + ): boolean { + void protoMessage; + return message._data?.quotedRemoteJid === Jid.BROADCAST; + } + /** * Parse API response and get the data for WPP engine. * WPP returns message.id as a composite string "fromMe_chatId_msgId". @@ -217,6 +279,10 @@ class WPPHelper implements IEngineHelper { ContactIsMy(contact) { return contact.isMyContact; } + + ExtractQuotedMedia(replyData: any): QuotedMedia | null { + return extractBrowserQuotedMedia(replyData); + } } // Choose the right EngineHelper based on getEngineName() function @@ -241,6 +307,66 @@ switch (getEngineName()) { export const EngineHelper = engineHelper; +// +// Quoted media extraction helpers +// + +function extractProtoQuotedMedia( + replyData: any, + capitalUrl = false, +): QuotedMedia | null { + if (!replyData) { + return null; + } + const mediaTypes: Array<[string, string]> = [ + ['imageMessage', 'image'], + ['videoMessage', 'video'], + ['audioMessage', 'audio'], + ]; + for (const [key, mediaType] of mediaTypes) { + const msg = replyData[key]; + if (!msg) { + continue; + } + const url: string | undefined = capitalUrl ? msg.URL ?? msg.url : msg.url; + const directPath: string = msg.directPath; + const mimetype: string = msg.mimetype; + if (!directPath || !mimetype) { + continue; + } + return { + url: url, + directPath: directPath, + mediaKey: msg.mediaKey, + mimetype: mimetype, + mediaType: mediaType, + }; + } + return null; +} + +function extractBrowserQuotedMedia(replyData: any): QuotedMedia | null { + if (!replyData) { + return null; + } + const kind: string = replyData.kind ?? replyData.type; + if (kind !== 'image' && kind !== 'video' && kind !== 'audio') { + return null; + } + const directPath: string = replyData.directPath; + const mimetype: string = replyData.mimetype; + if (!directPath || !mimetype) { + return null; + } + return { + url: replyData.deprecatedMms3Url, + directPath: directPath, + mediaKey: replyData.mediaKey, + mimetype: mimetype, + mediaType: kind, + }; +} + function preferPnChats(chats: string[]): string[] { const unique = lodash.uniq(chats ?? []); const hasPn = unique.some(isPnUser); diff --git a/src/apps/chatwoot/waha/index.ts b/src/apps/chatwoot/waha/index.ts index 44f6be02..7ad032fe 100644 --- a/src/apps/chatwoot/waha/index.ts +++ b/src/apps/chatwoot/waha/index.ts @@ -1,3 +1,5 @@ import { EngineHelper } from './engines'; +import type { QuotedMedia } from './engines'; export { EngineHelper }; +export type { QuotedMedia }; diff --git a/src/core/utils/pwa.ts b/src/core/utils/pwa.ts index 4c7f08b1..94d09f0a 100644 --- a/src/core/utils/pwa.ts +++ b/src/core/utils/pwa.ts @@ -3,6 +3,7 @@ */ import { normalizeMessageContent, proto } from '@adiwajshing/baileys'; +import esm from '@waha/vendor/esm'; export function IsEditedMessage(message: proto.IMessage): boolean { message = normalizeMessageContent(message); @@ -37,3 +38,11 @@ export function IsHistorySyncNotification(message: proto.IMessage): boolean { } return true; } + +export function getContextInfo( + protoMessage: proto.Message | null, +): proto.IContextInfo | null { + const type = esm.b.getContentType(protoMessage); + const message = protoMessage[type] as any; + return message?.contextInfo; +}