From a5a9c8977db3e3c81b38f4bed37fd07ad46122a9 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Wed, 18 Jun 2025 12:13:13 -0300 Subject: [PATCH 01/49] feat: add messaging history set handler for WhatsApp and restore contacts at first --- .../baileys_handlers/messaging_history_set.rb | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 app/services/whatsapp/baileys_handlers/messaging_history_set.rb diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb new file mode 100644 index 0000000000000..51d3899c51bd2 --- /dev/null +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -0,0 +1,36 @@ +module Whatsapp::BaileysHandlers::MessagingHistorySet + include Whatsapp::BaileysHandlers::Helpers + include BaileysHelper + include Whatsapp::BaileysHandlers::MessagesUpsert + + private + + def process_messaging_history_set + contacts = processed_params.dig(:data, :contacts) || [] + contacts.each do |contact| + create_contact(contact) if jid_user(contact[:id]) + end + end + + # TODO: Refactor jid_type method in helpers to receive the jid as an argument and use it here + def jid_user(jid) + server = jid.split('@').last + server == 's.whatsapp.net' || server == 'c.us' + end + + # TODO: Refactor this method in helpers to receive the jid as an argument and remove it from here + def phone_number_from_jid(jid) + jid.split('@').first.split(':').first.split('_').first + end + + def create_contact(contact) + phone_number_from_jid = phone_number_from_jid(contact[:id]) + name = contact[:verifiedName].presence || contact[:name].presence || phone_number_from_jid + ::ContactInboxWithContactBuilder.new( + # FIXME: update the source_id to complete jid in future + source_id: phone_number_from_jid, + inbox: inbox, + contact_attributes: { name: name, phone_number: "+#{phone_number_from_jid}" } + ).perform + end +end \ No newline at end of file From b24726185d8046cfa969b8a79b6a314cb031e49c Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 19 Jun 2025 10:59:47 -0300 Subject: [PATCH 02/49] feat: enhance messaging history processing and message handling in WhatsApp handler --- .../baileys_handlers/messaging_history_set.rb | 216 +++++++++++++++++- 1 file changed, 209 insertions(+), 7 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 51d3899c51bd2..b350272199517 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -1,19 +1,23 @@ -module Whatsapp::BaileysHandlers::MessagingHistorySet - include Whatsapp::BaileysHandlers::Helpers - include BaileysHelper - include Whatsapp::BaileysHandlers::MessagesUpsert +module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength + # include Whatsapp::BaileysHandlers::Helpers + # include BaileysHelper private def process_messaging_history_set - contacts = processed_params.dig(:data, :contacts) || [] + contacts = params.dig(:data, :contacts) || [] contacts.each do |contact| - create_contact(contact) if jid_user(contact[:id]) + create_contact(contact) if jid_user?(contact[:id]) + end + + messages = params.dig(:data, :messages) || [] + messages.each do |message| + handle_message(message) end end # TODO: Refactor jid_type method in helpers to receive the jid as an argument and use it here - def jid_user(jid) + def jid_user?(jid) server = jid.split('@').last server == 's.whatsapp.net' || server == 'c.us' end @@ -33,4 +37,202 @@ def create_contact(contact) contact_attributes: { name: name, phone_number: "+#{phone_number_from_jid}" } ).perform end + + def handle_message(raw_message) + jid = raw_message[:key][:remoteJid] + return unless jid_user?(jid) + + id = raw_message[:key][:id] + return if message_type(raw_message[:message]).in?(%w[protocol context unsupported]) + return if find_message_by_source_id(id) || message_under_process?(id) + + cache_message_source_id_in_redis(id) + contact_inbox = find_contact_inbox(jid) + + unless contact_inbox.contact + clear_message_source_id_from_redis(id) + + Rails.logger.warn "Contact not found for message: #{id}" + return + end + + create_message(raw_message, contact_inbox) + clear_message_source_id_from_redis(id) + end + + # TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here + def message_type(message_content) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength + if message_content.key?(:conversation) || message_content.dig(:extendedTextMessage, :text).present? + 'text' + elsif message_content.key?(:imageMessage) + 'image' + elsif message_content.key?(:audioMessage) + 'audio' + elsif message_content.key?(:videoMessage) + 'video' + elsif message_content.key?(:documentMessage) || message_content.key?(:documentWithCaptionMessage) + 'file' + elsif message_content.key?(:stickerMessage) + 'sticker' + elsif message_content.key?(:reactionMessage) + 'reaction' + elsif message_content.key?(:editedMessage) + 'edited' + elsif message_content.key?(:protocolMessage) + 'protocol' + elsif message_content.key?(:messageContextInfo) + 'context' + else + 'unsupported' + end + end + + def find_message_by_source_id(source_id) + return unless source_id + + Message.find_by(source_id: source_id).presence + end + + def find_contact_inbox(jid) + phone_number = phone_number_from_jid(jid) + ::ContactInboxWithContactBuilder.new( + # FIXME: update the source_id to complete jid in future + source_id: phone_number + ).perform + end + + # TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here + def message_under_process?(source_id) + key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id) + Redis::Alfred.get(key) + end + + # TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here + def cache_message_source_id_in_redis(source_id) + key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id) + ::Redis::Alfred.setex(key, true) + end + + # TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here + def clear_message_source_id_from_redis(source_id) + key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id) + ::Redis::Alfred.delete(key) + end + + def create_message(raw_message, contact_inbox) # rubocop:disable Metrics/AbcSize + conversation = get_conversation(contact_inbox) + inbox = contact_inbox.inbox + message = conversation.messages.build( + content: message_content(raw_message[:message]), + account_id: inbox.account_id, + inbox_id: inbox.id, + source_id: raw_message[:key][:id], + sender: incoming?(raw_message) ? contact_inbox.contact : inbox.account.account_users.first.user, + sender_type: incoming?(raw_message) ? 'Contact' : 'User', + message_type: incoming?(raw_message) ? :incoming : :outgoing, + content_attributes: message_content_attributes(raw_message), + status: process_status(raw_message[:status]) || 'sent' + ) + + handle_attach_media(conversation, message, raw_message) if message_type(raw_message[:message]).in?(%w[image file video audio sticker]) + + message.save! + end + + # NOTE: See reference in app/services/whatsapp/incoming_message_base_service.rb:97 + def get_conversation(contact_inbox) + return contact_inbox.conversations.last if contact_inbox.inbox.lock_to_single_conversation + + # NOTE: if lock to single conversation is disabled, create a new conversation if previous conversation is resolved + return contact_inbox.conversations.where.not(status: :resolved).last.presence || + ::Conversation.create!(conversation_params(contact_inbox)) + end + + def conversation_params(contact_inbox) + { + account_id: contact_inbox.inbox.account_id, + inbox_id: contact_inbox.inbox.id, + contact_id: contact_inbox.contact.id, + contact_inbox_id: contact_inbox.id + } + end + + # TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here + def incoming?(raw_message) + !raw_message[:key][:fromMe] + end + + # TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here + def message_content(raw_message) + raw_message.dig(:message, :conversation) || + raw_message.dig(:message, :extendedTextMessage, :text) || + raw_message.dig(:message, :imageMessage, :caption) || + raw_message.dig(:message, :videoMessage, :caption) || + raw_message.dig(:message, :documentMessage, :caption).presence || + raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :caption) || + raw_message.dig(:message, :reactionMessage, :text) + end + + def message_content_attributes(raw_message) + { + external_created_at: baileys_extract_message_timestamp(raw_message[:messageTimestamp]), + is_unsupported: message_type(raw_message[:message]) == 'unsupported' ? true : nil + }.compact + end + + def process_status(status) + { + 'PENDING' => 'sent', + 'DELIVERY_ACK' => 'delivered', + 'READ' => 'read' + }[status] + end + + def handle_attach_media(message, conversation, raw_message) + attachment_file = download_attachment_file(conversation, raw_message) + message_type = message_type(raw_message[:message]) + file_type = file_content_type(message_type).to_s + message_mimetype = message_mimetype(message_type, raw_message) + attachment = message.attachments.build( + account_id: message.account_id, + file_type: file_type, + file: { io: attachment_file, filename: filename(raw_message, message_mimetype, file_type), content_type: message_mimetype } + ) + attachment.meta = { is_recorded_audio: true } if raw_message.dig(:message, :audioMessage, :ptt) + rescue Down::Error => e + message.update!(is_unsupported: true) + + Rails.logger.error "Failed to download attachment for message #{raw_message_id}: #{e.message}" + end + + def download_attachment_file(conversation, raw_message) + Down.download(conversation.inbox.channel.media_url(raw_message.dig(:key, :id)), headers: conversation.inbox.channel.api_headers) + end + + def file_content_type(message_type) + return :image if message_type.in?(%w[image sticker]) + return :video if message_type.in?(%w[video video_note]) + return :audio if message_type == 'audio' + + :file + end + + def filename(raw_message, message_mimetype, file_content_type) + filename = raw_message.dig[:message][:documentMessage][:fileName] + return filename if filename.present? + + ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present? + "#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}" + end + + def message_mimetype(message_type, raw_message) + { + 'image' => raw_message.dig(:message, :imageMessage, :mimetype), + 'sticker' => raw_message.dig(:message, :stickerMessage, :mimetype), + 'video' => raw_message.dig(:message, :videoMessage, :mimetype), + 'audio' => raw_message.dig(:message, :audioMessage, :mimetype), + 'file' => raw_message.dig(:message, :documentMessage, :mimetype).presence || + raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :mimetype) + }[message_type] + end end \ No newline at end of file From d773f1cd3cae14072c783d6a54d6c5550f820f30 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 19 Jun 2025 11:00:56 -0300 Subject: [PATCH 03/49] feat: include MessagingHistorySet handler in IncomingMessageBaileysService --- app/services/whatsapp/incoming_message_baileys_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/whatsapp/incoming_message_baileys_service.rb b/app/services/whatsapp/incoming_message_baileys_service.rb index 4dda31a905eee..7713b818e5edf 100644 --- a/app/services/whatsapp/incoming_message_baileys_service.rb +++ b/app/services/whatsapp/incoming_message_baileys_service.rb @@ -2,6 +2,7 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer include Whatsapp::BaileysHandlers::ConnectionUpdate include Whatsapp::BaileysHandlers::MessagesUpsert include Whatsapp::BaileysHandlers::MessagesUpdate + include Whatsapp::BaileysHandlers::MessagingHistorySet class InvalidWebhookVerifyToken < StandardError; end From 49c1ac94734bdd65e444bc616020c6368d825665 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 19 Jun 2025 11:33:31 -0300 Subject: [PATCH 04/49] feat: add options to sync messages and contacts in WhatsApp settings --- .../dashboard/i18n/locale/en/inboxMgmt.json | 13 +++++++- .../i18n/locale/pt_BR/inboxMgmt.json | 12 ++++++- .../inbox/channels/BaileysWhatsapp.vue | 32 +++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 14ebc31d47b76..5c69060101640 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -272,6 +272,10 @@ "MARK_AS_READ": { "LABEL": "Send read receipts" }, + "SYNC_FULL_HISTORY": { + "ONLY_CONTACTS_LABEL": "Sync only contacts", + "LABEL": "Sync messages and contacts" + }, "ADVANCED_OPTIONS": "Advanced options", "BAILEYS": { "SUBTITLE": "Click below to setup the WhatsApp channel using Baileys.", @@ -564,7 +568,14 @@ "UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings", "WHATSAPP_MARK_AS_READ_TITLE": "Read receipts", "WHATSAPP_MARK_AS_READ_SUBHEADER": "If turned off, when a message is viewed in Chatwoot, a read receipt will not be sent to the sender. Your messages will still be able to receive read receipts from the sender.", - "WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts" + "WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts", + + "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_TITLE": "Sync messages and contacts", + "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_SUBHEADER": "It will restore your messages and contacts from the WhatsApp.", + "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_LABEL": "Sync messages and contacts", + "WHATSAPP_FETCH_CONTACTS_TITLE": "Sync contacts", + "WHATSAPP_FETCH_CONTACTS_SUBHEADER": "It will restore your contacts from the WhatsApp.", + "WHATSAPP_FETCH_CONTACTS_LABEL": "Sync contacts" }, "HELP_CENTER": { "LABEL": "Help Center", diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json index 5217cb6ecd30f..938c0729b6c17 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json @@ -272,6 +272,10 @@ "MARK_AS_READ": { "LABEL": "Enviar confirmações de leitura" }, + "SYNC_FULL_HISTORY": { + "ONLY_CONTACTS_LABEL": "Sincronizar apenas contatos", + "LABEL": "Sincronizar mensagens e contatos" + }, "ADVANCED_OPTIONS": "Opções avançadas", "BAILEYS": { "SUBTITLE": "Clique abaixo para configurar o canal do WhatsApp usando o Baileys.", @@ -564,7 +568,13 @@ "UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat", "WHATSAPP_MARK_AS_READ_TITLE": "Confirmações de leitura", "WHATSAPP_MARK_AS_READ_SUBHEADER": "Se essa opção estiver desativada, ao visualizar uma mensagem pelo Chatwoot, não será enviada uma confirmação de leitura para o remetente. As suas mensagens ainda poderão receber confirmações de leitura.", - "WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura" + "WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura", + "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_TITLE": "Sincronizar mensagens e contatos", + "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_SUBHEADER": "Isso irá restaurar suas mensagens e contatos do WhatsApp.", + "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_LABEL": "Sincronizar mensagens e contatos", + "WHATSAPP_FETCH_CONTACTS_TITLE": "Sincronizar contatos", + "WHATSAPP_FETCH_CONTACTS_SUBHEADER": "Isso irá restaurar seus contatos do WhatsApp.", + "WHATSAPP_FETCH_CONTACTS_LABEL": "Sincronizar contatos" }, "HELP_CENTER": { "LABEL": "Centro de Ajuda", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue index b5565c2bca3f5..0ff40c3bd95eb 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue @@ -27,6 +27,8 @@ export default { providerUrl: '', showAdvancedOptions: false, markAsRead: true, + syncContacts: false, + syncFullHistory: false, }; }, computed: { @@ -53,6 +55,8 @@ export default { try { const providerConfig = { mark_as_read: this.markAsRead, + sync_contacts: this.syncContacts, + sync_full_history: this.syncFullHistory, }; if (this.apiKey || this.providerUrl) { @@ -177,6 +181,34 @@ export default { + +
+ +
+
+ +
From 2a33ae7f0bdddf1485b3ff96fa9ddab2b50fbafc Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 19 Jun 2025 12:05:19 -0300 Subject: [PATCH 05/49] feat: implement initial sync for contacts and message history in setup_channel_provider --- .../providers/whatsapp_baileys_service.rb | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index dbe76ec208a83..25ee5299487e4 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -8,16 +8,32 @@ class MessageNotSentError < StandardError; end DEFAULT_URL = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_URL', nil) DEFAULT_API_KEY = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_API_KEY', nil) - def setup_channel_provider + def setup_channel_provider # rubocop:disable Metrics/MethodLength + provider_config = whatsapp_channel.provider_config + sync_contacts = provider_config['sync_contacts'].presence + sync_history = provider_config['sync_full_history'].presence + perform_initial_sync = sync_contacts || sync_history + + # NOTE: After the initial setup on inbox creation, reset the flags and set the sync type for messages_history.set incoming event. + if perform_initial_sync + sync_type = sync_contacts ? 'contacts' : 'full_history' + config_updates = { + 'sync_contacts' => false, + 'sync_full_history' => false, + 'sync_type' => sync_type + } + whatsapp_channel.update!(provider_config: provider_config.merge(config_updates)) + end response = HTTParty.post( "#{provider_url}/connections/#{whatsapp_channel.phone_number}", headers: api_headers, body: { clientName: DEFAULT_CLIENT_NAME, webhookUrl: whatsapp_channel.inbox.callback_webhook_url, - webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token'], + webhookVerifyToken: provider_config['webhook_verify_token'], # TODO: Remove on Baileys v2, default will be false - includeMedia: false + includeMedia: false, + syncFullHistory: perform_initial_sync }.compact.to_json ) @@ -155,6 +171,22 @@ def unread_message(phone_number, message) # rubocop:disable Metrics/MethodLength process_response(response) end + def fetch_message_history(oldest_message) + @phone_number = phone_number + + response = HTTParty.post( + "#{provider_url}/connections/#{whatsapp_channel.phone_number}/fetch-message-history", + headers: api_headers, + body: { + count: 50, + oldestMsgKey: { id: oldest_message.source_id, remoteJid: remote_jid, fromMe: oldest_message.message_type == 'outgoing' }, + oldestMsgTimestamp: oldest_message.content_attributes[:external_created_at] + } + ) + + process_response(response) + end + def received_messages(phone_number, messages) @phone_number = phone_number From 83dc898c9dfaec89e4ee3784ba30ce402995bfe1 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Tue, 24 Jun 2025 23:43:52 -0300 Subject: [PATCH 06/49] chore: correct phone number variable usage in create_contact method --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index b350272199517..fc47e8e6ebe5f 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -28,13 +28,13 @@ def phone_number_from_jid(jid) end def create_contact(contact) - phone_number_from_jid = phone_number_from_jid(contact[:id]) - name = contact[:verifiedName].presence || contact[:name].presence || phone_number_from_jid + phone_number = phone_number_from_jid(contact[:id]) + name = contact[:verifiedName].presence || contact[:name].presence || phone_number ::ContactInboxWithContactBuilder.new( # FIXME: update the source_id to complete jid in future - source_id: phone_number_from_jid, + source_id: phone_number, inbox: inbox, - contact_attributes: { name: name, phone_number: "+#{phone_number_from_jid}" } + contact_attributes: { name: name, phone_number: "+#{phone_number}" } ).perform end From a999f8d3ed898d340db8a1cde9c930bccc0eaf6b Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Tue, 24 Jun 2025 23:46:23 -0300 Subject: [PATCH 07/49] refactor: rename methods for avoid impact other baileys handlers --- .../baileys_handlers/messaging_history_set.rb | 91 ++++++++++--------- 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index fc47e8e6ebe5f..1f4e3ee5eebef 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -12,7 +12,7 @@ def process_messaging_history_set messages = params.dig(:data, :messages) || [] messages.each do |message| - handle_message(message) + history_handle_message(message) end end @@ -23,12 +23,12 @@ def jid_user?(jid) end # TODO: Refactor this method in helpers to receive the jid as an argument and remove it from here - def phone_number_from_jid(jid) + def history_phone_number_from_jid(jid) jid.split('@').first.split(':').first.split('_').first end def create_contact(contact) - phone_number = phone_number_from_jid(contact[:id]) + phone_number = history_phone_number_from_jid(contact[:id]) name = contact[:verifiedName].presence || contact[:name].presence || phone_number ::ContactInboxWithContactBuilder.new( # FIXME: update the source_id to complete jid in future @@ -38,30 +38,30 @@ def create_contact(contact) ).perform end - def handle_message(raw_message) + def history_handle_message(raw_message) jid = raw_message[:key][:remoteJid] return unless jid_user?(jid) id = raw_message[:key][:id] - return if message_type(raw_message[:message]).in?(%w[protocol context unsupported]) - return if find_message_by_source_id(id) || message_under_process?(id) + return if history_message_type(raw_message[:message]).in?(%w[protocol context unsupported]) + return if history_find_message_by_source_id(id) || history_message_under_process?(id) - cache_message_source_id_in_redis(id) + history_cache_message_source_id_in_redis(id) contact_inbox = find_contact_inbox(jid) unless contact_inbox.contact - clear_message_source_id_from_redis(id) + history_clear_message_source_id_from_redis(id) Rails.logger.warn "Contact not found for message: #{id}" return end - create_message(raw_message, contact_inbox) - clear_message_source_id_from_redis(id) + history_create_message(raw_message, contact_inbox) + history_clear_message_source_id_from_redis(id) end # TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here - def message_type(message_content) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength + def history_message_type(message_content) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/MethodLength if message_content.key?(:conversation) || message_content.dig(:extendedTextMessage, :text).present? 'text' elsif message_content.key?(:imageMessage) @@ -87,14 +87,15 @@ def message_type(message_content) # rubocop:disable Metrics/CyclomaticComplexity end end - def find_message_by_source_id(source_id) + # TODO: Remove this method when include helpers in this module, after update the methods to receive arguments + def history_find_message_by_source_id(source_id) return unless source_id Message.find_by(source_id: source_id).presence end def find_contact_inbox(jid) - phone_number = phone_number_from_jid(jid) + phone_number = history_phone_number_from_jid(jid) ::ContactInboxWithContactBuilder.new( # FIXME: update the source_id to complete jid in future source_id: phone_number @@ -102,24 +103,24 @@ def find_contact_inbox(jid) end # TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here - def message_under_process?(source_id) + def history_message_under_process?(source_id) key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id) Redis::Alfred.get(key) end # TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here - def cache_message_source_id_in_redis(source_id) + def history_cache_message_source_id_in_redis(source_id) key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id) ::Redis::Alfred.setex(key, true) end # TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here - def clear_message_source_id_from_redis(source_id) + def history_clear_message_source_id_from_redis(source_id) key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id) ::Redis::Alfred.delete(key) end - def create_message(raw_message, contact_inbox) # rubocop:disable Metrics/AbcSize + def history_create_message(raw_message, contact_inbox) # rubocop:disable Metrics/AbcSize conversation = get_conversation(contact_inbox) inbox = contact_inbox.inbox message = conversation.messages.build( @@ -127,14 +128,15 @@ def create_message(raw_message, contact_inbox) # rubocop:disable Metrics/AbcSize account_id: inbox.account_id, inbox_id: inbox.id, source_id: raw_message[:key][:id], - sender: incoming?(raw_message) ? contact_inbox.contact : inbox.account.account_users.first.user, - sender_type: incoming?(raw_message) ? 'Contact' : 'User', - message_type: incoming?(raw_message) ? :incoming : :outgoing, - content_attributes: message_content_attributes(raw_message), + sender: history_incoming?(raw_message) ? contact_inbox.contact : inbox.account.account_users.first.user, + sender_type: history_incoming?(raw_message) ? 'Contact' : 'User', + history_message_type: history_incoming?(raw_message) ? :incoming : :outgoing, + content_attributes: history_message_content_attributes(raw_message), status: process_status(raw_message[:status]) || 'sent' ) - handle_attach_media(conversation, message, raw_message) if message_type(raw_message[:message]).in?(%w[image file video audio sticker]) + history_handle_attach_media(conversation, message, raw_message) if history_message_type(raw_message[:message]).in?(%w[image file video audio + sticker]) message.save! end @@ -158,7 +160,7 @@ def conversation_params(contact_inbox) end # TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here - def incoming?(raw_message) + def history_incoming?(raw_message) !raw_message[:key][:fromMe] end @@ -173,10 +175,10 @@ def message_content(raw_message) raw_message.dig(:message, :reactionMessage, :text) end - def message_content_attributes(raw_message) + def history_message_content_attributes(raw_message) { external_created_at: baileys_extract_message_timestamp(raw_message[:messageTimestamp]), - is_unsupported: message_type(raw_message[:message]) == 'unsupported' ? true : nil + is_unsupported: history_message_type(raw_message[:message]) == 'unsupported' ? true : nil }.compact end @@ -188,15 +190,16 @@ def process_status(status) }[status] end - def handle_attach_media(message, conversation, raw_message) - attachment_file = download_attachment_file(conversation, raw_message) - message_type = message_type(raw_message[:message]) - file_type = file_content_type(message_type).to_s - message_mimetype = message_mimetype(message_type, raw_message) + def history_handle_attach_media(message, conversation, raw_message) + attachment_file = history_download_attachment_file(conversation, raw_message) + history_message_type = history_message_type(raw_message[:message]) + file_type = history_file_content_type(history_message_type).to_s + history_message_mimetype = history_message_mimetype(history_message_type, raw_message) attachment = message.attachments.build( account_id: message.account_id, file_type: file_type, - file: { io: attachment_file, filename: filename(raw_message, message_mimetype, file_type), content_type: message_mimetype } + file: { io: attachment_file, history_filename: history_filename(raw_message, history_message_mimetype, file_type), + content_type: history_message_mimetype } ) attachment.meta = { is_recorded_audio: true } if raw_message.dig(:message, :audioMessage, :ptt) rescue Down::Error => e @@ -205,27 +208,27 @@ def handle_attach_media(message, conversation, raw_message) Rails.logger.error "Failed to download attachment for message #{raw_message_id}: #{e.message}" end - def download_attachment_file(conversation, raw_message) + def history_download_attachment_file(conversation, raw_message) Down.download(conversation.inbox.channel.media_url(raw_message.dig(:key, :id)), headers: conversation.inbox.channel.api_headers) end - def file_content_type(message_type) - return :image if message_type.in?(%w[image sticker]) - return :video if message_type.in?(%w[video video_note]) - return :audio if message_type == 'audio' + def history_file_content_type(history_message_type) + return :image if history_message_type.in?(%w[image sticker]) + return :video if history_message_type.in?(%w[video video_note]) + return :audio if history_message_type == 'audio' :file end - def filename(raw_message, message_mimetype, file_content_type) - filename = raw_message.dig[:message][:documentMessage][:fileName] - return filename if filename.present? + def history_filename(raw_message, history_message_mimetype, history_file_content_type) + history_filename = raw_message.dig[:message][:documentMessage][:history_filename] + return history_filename if history_filename.present? - ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present? - "#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}" + ext = ".#{history_message_mimetype.split(';').first.split('/').last}" if history_message_mimetype.present? + "#{history_file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}" end - def message_mimetype(message_type, raw_message) + def history_message_mimetype(history_message_type, raw_message) { 'image' => raw_message.dig(:message, :imageMessage, :mimetype), 'sticker' => raw_message.dig(:message, :stickerMessage, :mimetype), @@ -233,6 +236,6 @@ def message_mimetype(message_type, raw_message) 'audio' => raw_message.dig(:message, :audioMessage, :mimetype), 'file' => raw_message.dig(:message, :documentMessage, :mimetype).presence || raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :mimetype) - }[message_type] + }[history_message_type] end -end \ No newline at end of file +end From b08d42adc43854cdfcc0c2e994be5d02c16358e3 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Tue, 24 Jun 2025 23:52:52 -0300 Subject: [PATCH 08/49] fix: correct argument order in history_handle_attach_media method call --- app/services/whatsapp/baileys_handlers/messaging_history_set.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 1f4e3ee5eebef..af330708e4684 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -135,7 +135,7 @@ def history_create_message(raw_message, contact_inbox) # rubocop:disable Metrics status: process_status(raw_message[:status]) || 'sent' ) - history_handle_attach_media(conversation, message, raw_message) if history_message_type(raw_message[:message]).in?(%w[image file video audio + history_handle_attach_media(message, conversation, raw_message) if history_message_type(raw_message[:message]).in?(%w[image file video audio sticker]) message.save! From 76c21842aec371de5035486e6331c75ad606985f Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Tue, 24 Jun 2025 23:57:12 -0300 Subject: [PATCH 09/49] fix: improve error logging for attachment download failure --- app/services/whatsapp/baileys_handlers/messaging_history_set.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index af330708e4684..b931798c7f12a 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -205,7 +205,7 @@ def history_handle_attach_media(message, conversation, raw_message) rescue Down::Error => e message.update!(is_unsupported: true) - Rails.logger.error "Failed to download attachment for message #{raw_message_id}: #{e.message}" + Rails.logger.error "Failed to download attachment for message #{raw_message[:key][:id]}: #{e.message}" end def history_download_attachment_file(conversation, raw_message) From 455c69a47967b25b3d3cfb92219a578d0ccc7fdb Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Tue, 24 Jun 2025 23:58:00 -0300 Subject: [PATCH 10/49] fix: correct history_filename method in messaging_history.set --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index b931798c7f12a..7868549670927 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -220,12 +220,12 @@ def history_file_content_type(history_message_type) :file end - def history_filename(raw_message, history_message_mimetype, history_file_content_type) - history_filename = raw_message.dig[:message][:documentMessage][:history_filename] - return history_filename if history_filename.present? + def history_filename(raw_message, message_mimetype, file_content_type) + filename = raw_message[:message][:documentMessage][:history_filename] + return filename if filename.present? - ext = ".#{history_message_mimetype.split(';').first.split('/').last}" if history_message_mimetype.present? - "#{history_file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}" + ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present? + "#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}" end def history_message_mimetype(history_message_type, raw_message) From ae06267c5cad4c88939d97ce667c04dc17388b17 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Wed, 25 Jun 2025 00:04:19 -0300 Subject: [PATCH 11/49] chore: remove unused helper includes in messaging_history_set --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 7868549670927..86dc6411f3e71 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -1,7 +1,4 @@ module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength - # include Whatsapp::BaileysHandlers::Helpers - # include BaileysHelper - private def process_messaging_history_set From d00f5e8c8082137d67763946b5d42e5e58e7ec30 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Wed, 25 Jun 2025 00:04:47 -0300 Subject: [PATCH 12/49] fix: variables names in attachment handling methods --- .../baileys_handlers/messaging_history_set.rb | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 86dc6411f3e71..ec03e37f9b914 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -189,14 +189,14 @@ def process_status(status) def history_handle_attach_media(message, conversation, raw_message) attachment_file = history_download_attachment_file(conversation, raw_message) - history_message_type = history_message_type(raw_message[:message]) - file_type = history_file_content_type(history_message_type).to_s - history_message_mimetype = history_message_mimetype(history_message_type, raw_message) + message_type = history_message_type(raw_message[:message]) + file_type = history_file_content_type(message_type).to_s + message_mimetype = history_message_mimetype(message_type, raw_message) attachment = message.attachments.build( account_id: message.account_id, file_type: file_type, - file: { io: attachment_file, history_filename: history_filename(raw_message, history_message_mimetype, file_type), - content_type: history_message_mimetype } + file: { io: attachment_file, history_filename: history_filename(raw_message, message_mimetype, file_type), + content_type: message_mimetype } ) attachment.meta = { is_recorded_audio: true } if raw_message.dig(:message, :audioMessage, :ptt) rescue Down::Error => e @@ -209,10 +209,10 @@ def history_download_attachment_file(conversation, raw_message) Down.download(conversation.inbox.channel.media_url(raw_message.dig(:key, :id)), headers: conversation.inbox.channel.api_headers) end - def history_file_content_type(history_message_type) - return :image if history_message_type.in?(%w[image sticker]) - return :video if history_message_type.in?(%w[video video_note]) - return :audio if history_message_type == 'audio' + def history_file_content_type(message_type) + return :image if message_type.in?(%w[image sticker]) + return :video if message_type.in?(%w[video video_note]) + return :audio if message_type == 'audio' :file end @@ -225,7 +225,7 @@ def history_filename(raw_message, message_mimetype, file_content_type) "#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}" end - def history_message_mimetype(history_message_type, raw_message) + def history_message_mimetype(message_type, raw_message) { 'image' => raw_message.dig(:message, :imageMessage, :mimetype), 'sticker' => raw_message.dig(:message, :stickerMessage, :mimetype), @@ -233,6 +233,6 @@ def history_message_mimetype(history_message_type, raw_message) 'audio' => raw_message.dig(:message, :audioMessage, :mimetype), 'file' => raw_message.dig(:message, :documentMessage, :mimetype).presence || raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :mimetype) - }[history_message_type] + }[message_type] end end From 48e159c1d0fa8ac7fd5bfc52c646e8d8fd33b3bd Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Wed, 25 Jun 2025 00:09:45 -0300 Subject: [PATCH 13/49] fix: update fetch_message_history method to include phone_number parameter --- app/services/whatsapp/providers/whatsapp_baileys_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 25ee5299487e4..5704a9523efab 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -171,7 +171,7 @@ def unread_message(phone_number, message) # rubocop:disable Metrics/MethodLength process_response(response) end - def fetch_message_history(oldest_message) + def fetch_message_history(phone_number, oldest_message) @phone_number = phone_number response = HTTParty.post( From 35e4d2ed45354e945724a9363ef2ee70ed3b2500 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Wed, 25 Jun 2025 00:10:56 -0300 Subject: [PATCH 14/49] feat: add BAILEYS_MESSAGE_HISTORY_COUNT environment variable for message history count configuration --- .env.example | 3 +++ app/services/whatsapp/providers/whatsapp_baileys_service.rb | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index fb9ee83989684..dbcbdeeb1cbf9 100644 --- a/.env.example +++ b/.env.example @@ -266,3 +266,6 @@ BAILEYS_PROVIDER_DEFAULT_URL=http://localhost:3025 BAILEYS_PROVIDER_DEFAULT_API_KEY= RESEND_API_KEY= + +# Baileys API Whatsapp configurations +BAILEYS_MESSAGE_HISTORY_COUNT=50 diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 5704a9523efab..f43a9f783ccf7 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -178,7 +178,7 @@ def fetch_message_history(phone_number, oldest_message) "#{provider_url}/connections/#{whatsapp_channel.phone_number}/fetch-message-history", headers: api_headers, body: { - count: 50, + count: ENV.fetch('BAILEYS_MESSAGE_HISTORY_COUNT', 50).to_i, oldestMsgKey: { id: oldest_message.source_id, remoteJid: remote_jid, fromMe: oldest_message.message_type == 'outgoing' }, oldestMsgTimestamp: oldest_message.content_attributes[:external_created_at] } From a584148f9b93fced3e54105898cbaa88b90680d3 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 26 Jun 2025 08:57:14 -0300 Subject: [PATCH 15/49] fix: rename message_content method to history_message_content for clarity --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index ec03e37f9b914..450aa321ab48c 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -121,7 +121,7 @@ def history_create_message(raw_message, contact_inbox) # rubocop:disable Metrics conversation = get_conversation(contact_inbox) inbox = contact_inbox.inbox message = conversation.messages.build( - content: message_content(raw_message[:message]), + content: history_message_content(raw_message[:message]), account_id: inbox.account_id, inbox_id: inbox.id, source_id: raw_message[:key][:id], @@ -162,7 +162,7 @@ def history_incoming?(raw_message) end # TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here - def message_content(raw_message) + def history_message_content(raw_message) raw_message.dig(:message, :conversation) || raw_message.dig(:message, :extendedTextMessage, :text) || raw_message.dig(:message, :imageMessage, :caption) || From 23d88edc3f0631aca9e2596bffe05385481bb979 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 26 Jun 2025 16:42:08 -0300 Subject: [PATCH 16/49] feat: implement fetch_message_history method to retrieve message history based on the oldest message --- app/models/channel/whatsapp.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index 2620e5463a22f..adbf67fec22a5 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -117,6 +117,18 @@ def received_messages(messages, conversation) provider_service.received_messages(conversation.contact.phone_number, messages) end + def fetch_message_history(conversation) + return unless provider_service.respond_to?(:fetch_message_history) + + oldest_message = conversation.inbox.messages + .where(message_type: %w[incoming outgoing]) + .order(Arel.sql("CAST(content_attributes->>'external_created_at' AS BIGINT) ASC")) + .first + return unless oldest_message&.content_attributes&.dig('external_created_at') + + provider_service.fetch_message_history(conversation.contact.phone_number, oldest_message) + end + delegate :setup_channel_provider, to: :provider_service delegate :send_message, to: :provider_service delegate :send_template, to: :provider_service From 7f8275d3416816ebb44d25d766bcabef4be5349e Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 26 Jun 2025 18:31:02 -0300 Subject: [PATCH 17/49] fix: simplify setup_channel_provider method by removing unnecessary sync flags and logic --- .../providers/whatsapp_baileys_service.rb | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index f43a9f783ccf7..176ceb1bbde29 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -8,22 +8,9 @@ class MessageNotSentError < StandardError; end DEFAULT_URL = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_URL', nil) DEFAULT_API_KEY = ENV.fetch('BAILEYS_PROVIDER_DEFAULT_API_KEY', nil) - def setup_channel_provider # rubocop:disable Metrics/MethodLength + def setup_channel_provider provider_config = whatsapp_channel.provider_config - sync_contacts = provider_config['sync_contacts'].presence - sync_history = provider_config['sync_full_history'].presence - perform_initial_sync = sync_contacts || sync_history - - # NOTE: After the initial setup on inbox creation, reset the flags and set the sync type for messages_history.set incoming event. - if perform_initial_sync - sync_type = sync_contacts ? 'contacts' : 'full_history' - config_updates = { - 'sync_contacts' => false, - 'sync_full_history' => false, - 'sync_type' => sync_type - } - whatsapp_channel.update!(provider_config: provider_config.merge(config_updates)) - end + response = HTTParty.post( "#{provider_url}/connections/#{whatsapp_channel.phone_number}", headers: api_headers, @@ -33,7 +20,7 @@ def setup_channel_provider # rubocop:disable Metrics/MethodLength webhookVerifyToken: provider_config['webhook_verify_token'], # TODO: Remove on Baileys v2, default will be false includeMedia: false, - syncFullHistory: perform_initial_sync + syncFullHistory: provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence }.compact.to_json ) From 0ec76029e4452286f9870ca7f3a5e5eabae985e5 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 26 Jun 2025 18:35:23 -0300 Subject: [PATCH 18/49] fix: update process_messaging_history_set to conditionally sync contacts or messages based on provider configuration --- .../baileys_handlers/messaging_history_set.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 450aa321ab48c..f33d36b79568b 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -1,12 +1,18 @@ module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength private - def process_messaging_history_set - contacts = params.dig(:data, :contacts) || [] - contacts.each do |contact| - create_contact(contact) if jid_user?(contact[:id]) + def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + provider_config = inbox.channel.provider_config + + if provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence + contacts = params.dig(:data, :contacts) || [] + contacts.each do |contact| + create_contact(contact) if jid_user?(contact[:id]) + end end + return unless provider_config['sync_full_history'] + messages = params.dig(:data, :messages) || [] messages.each do |message| history_handle_message(message) From 91ff5335af27dd483f0477626f1f26874d34ff99 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 26 Jun 2025 18:46:36 -0300 Subject: [PATCH 19/49] fix: update create_contact method to include inbox parameter in ContactInboxWithContactBuilder --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index f33d36b79568b..eb1746febbd8c 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -101,7 +101,8 @@ def find_contact_inbox(jid) phone_number = history_phone_number_from_jid(jid) ::ContactInboxWithContactBuilder.new( # FIXME: update the source_id to complete jid in future - source_id: phone_number + source_id: phone_number, + inbox: inbox ).perform end From 345f3cfceb1c4a3169738c3d8602cca442cb795d Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Thu, 26 Jun 2025 18:47:18 -0300 Subject: [PATCH 20/49] fix: rename params name in message creation and attachment creation --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index eb1746febbd8c..c7a991bcb54b2 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -134,7 +134,7 @@ def history_create_message(raw_message, contact_inbox) # rubocop:disable Metrics source_id: raw_message[:key][:id], sender: history_incoming?(raw_message) ? contact_inbox.contact : inbox.account.account_users.first.user, sender_type: history_incoming?(raw_message) ? 'Contact' : 'User', - history_message_type: history_incoming?(raw_message) ? :incoming : :outgoing, + message_type: history_incoming?(raw_message) ? :incoming : :outgoing, content_attributes: history_message_content_attributes(raw_message), status: process_status(raw_message[:status]) || 'sent' ) @@ -202,7 +202,7 @@ def history_handle_attach_media(message, conversation, raw_message) attachment = message.attachments.build( account_id: message.account_id, file_type: file_type, - file: { io: attachment_file, history_filename: history_filename(raw_message, message_mimetype, file_type), + file: { io: attachment_file, filename: history_filename(raw_message, message_mimetype, file_type), content_type: message_mimetype } ) attachment.meta = { is_recorded_audio: true } if raw_message.dig(:message, :audioMessage, :ptt) From ad35f31ed5e44e39fe3b9a4ec24a351a2f9853c8 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 16:40:06 -0300 Subject: [PATCH 21/49] fix: reduce default message history count to 5 in fetch_message_history method --- app/services/whatsapp/providers/whatsapp_baileys_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 176ceb1bbde29..507415f59e5ca 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -165,7 +165,7 @@ def fetch_message_history(phone_number, oldest_message) "#{provider_url}/connections/#{whatsapp_channel.phone_number}/fetch-message-history", headers: api_headers, body: { - count: ENV.fetch('BAILEYS_MESSAGE_HISTORY_COUNT', 50).to_i, + count: ENV.fetch('BAILEYS_MESSAGE_HISTORY_COUNT', 5).to_i, oldestMsgKey: { id: oldest_message.source_id, remoteJid: remote_jid, fromMe: oldest_message.message_type == 'outgoing' }, oldestMsgTimestamp: oldest_message.content_attributes[:external_created_at] } From 2d01f7f74a7b3fb3efe879c17d53b411944cee74 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:02:48 -0300 Subject: [PATCH 22/49] fix: refactor fetch_message_history methods to streamline parameters and improve clarity --- app/models/channel/whatsapp.rb | 12 ++++++------ .../whatsapp/providers/whatsapp_baileys_service.rb | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index adbf67fec22a5..ce67db3e989f7 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -117,16 +117,16 @@ def received_messages(messages, conversation) provider_service.received_messages(conversation.contact.phone_number, messages) end - def fetch_message_history(conversation) + def fetch_message_history return unless provider_service.respond_to?(:fetch_message_history) - oldest_message = conversation.inbox.messages - .where(message_type: %w[incoming outgoing]) - .order(Arel.sql("CAST(content_attributes->>'external_created_at' AS BIGINT) ASC")) - .first + oldest_message = inbox.messages + .where(message_type: %w[incoming outgoing]) + .order(Arel.sql("CAST(content_attributes->>'external_created_at' AS BIGINT) ASC")) + .first return unless oldest_message&.content_attributes&.dig('external_created_at') - provider_service.fetch_message_history(conversation.contact.phone_number, oldest_message) + provider_service.fetch_message_history(oldest_message) end delegate :setup_channel_provider, to: :provider_service diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 507415f59e5ca..83128e241cfb2 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -158,8 +158,8 @@ def unread_message(phone_number, message) # rubocop:disable Metrics/MethodLength process_response(response) end - def fetch_message_history(phone_number, oldest_message) - @phone_number = phone_number + def fetch_message_history(oldest_message) + @phone_number = oldest_message.conversation.contact.phone_numbere_number response = HTTParty.post( "#{provider_url}/connections/#{whatsapp_channel.phone_number}/fetch-message-history", @@ -167,7 +167,7 @@ def fetch_message_history(phone_number, oldest_message) body: { count: ENV.fetch('BAILEYS_MESSAGE_HISTORY_COUNT', 5).to_i, oldestMsgKey: { id: oldest_message.source_id, remoteJid: remote_jid, fromMe: oldest_message.message_type == 'outgoing' }, - oldestMsgTimestamp: oldest_message.content_attributes[:external_created_at] + oldestMsgTimestamp: oldest_message.content_attributes[:external_created_at].to_i } ) From 5e534d26367aab0430d5ab40ad0819045ed35ff1 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:25:17 -0300 Subject: [PATCH 23/49] fix: validate presence of key, message, and messageTimestamp in history_handle_message method --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index c7a991bcb54b2..7ca075df287bb 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -1,7 +1,7 @@ module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength private - def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize,Metrics/MethodLength provider_config = inbox.channel.provider_config if provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence @@ -42,6 +42,8 @@ def create_contact(contact) end def history_handle_message(raw_message) + return unless raw_message[:key].present? && raw_message[:message].present? && raw_message[:messageTimestamp].present? + jid = raw_message[:key][:remoteJid] return unless jid_user?(jid) From 16c0c1e121af857b5e11170f20731a5058f0be71 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:26:32 -0300 Subject: [PATCH 24/49] fix: add error handling for contact and message processing in process_messaging_history_set method --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 7ca075df287bb..c2d5eae550270 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -8,15 +8,25 @@ def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity contacts = params.dig(:data, :contacts) || [] contacts.each do |contact| create_contact(contact) if jid_user?(contact[:id]) + rescue StandardError => e + Rails.logger.error "Error processing contact: #{e.message}" + Rails.logger.error e.backtrace.join("\n") end end return unless provider_config['sync_full_history'] + has_created_new_message = false messages = params.dig(:data, :messages) || [] messages.each do |message| history_handle_message(message) + has_created_new_message = true + rescue StandardError => e + Rails.logger.error "Error processing message: #{e.message}" + Rails.logger.error e.backtrace.join("\n") end + + inbox.channel.fetch_message_history if has_created_new_message end # TODO: Refactor jid_type method in helpers to receive the jid as an argument and use it here From e2b634bb3b2ba2b24699443d6dc3ec65837c3b5e Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:26:47 -0300 Subject: [PATCH 25/49] fix: add presence validation for key, message, and messageTimestamp in history_handle_message method --- app/services/whatsapp/baileys_handlers/messaging_history_set.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index c2d5eae550270..bfbcc85e0516f 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -51,7 +51,7 @@ def create_contact(contact) ).perform end - def history_handle_message(raw_message) + def history_handle_message(raw_message) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity return unless raw_message[:key].present? && raw_message[:message].present? && raw_message[:messageTimestamp].present? jid = raw_message[:key][:remoteJid] From 40e7cdaf28f88777e7fd3287775efde98d85668b Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:27:17 -0300 Subject: [PATCH 26/49] fix: update history_create_message method to use raw_message directly for content and remove media attachment handling --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index bfbcc85e0516f..744e4eb39f512 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -136,11 +136,11 @@ def history_clear_message_source_id_from_redis(source_id) ::Redis::Alfred.delete(key) end - def history_create_message(raw_message, contact_inbox) # rubocop:disable Metrics/AbcSize + def history_create_message(raw_message, contact_inbox) conversation = get_conversation(contact_inbox) inbox = contact_inbox.inbox message = conversation.messages.build( - content: history_message_content(raw_message[:message]), + content: history_message_content(raw_message), account_id: inbox.account_id, inbox_id: inbox.id, source_id: raw_message[:key][:id], @@ -151,9 +151,6 @@ def history_create_message(raw_message, contact_inbox) # rubocop:disable Metrics status: process_status(raw_message[:status]) || 'sent' ) - history_handle_attach_media(message, conversation, raw_message) if history_message_type(raw_message[:message]).in?(%w[image file video audio - sticker]) - message.save! end From b841a1bc1d8050286e0fb0de3eecc915186aaba1 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:28:02 -0300 Subject: [PATCH 27/49] fix: add contact attributes to ContactInboxWithContactBuilder in create_contact method --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 744e4eb39f512..5a1bf387563a2 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -114,7 +114,8 @@ def find_contact_inbox(jid) ::ContactInboxWithContactBuilder.new( # FIXME: update the source_id to complete jid in future source_id: phone_number, - inbox: inbox + inbox: inbox, + contact_attributes: { name: phone_number, phone_number: "+#{phone_number}" } ).perform end From 80c5bcf3a21ec218544a8d2dd97dfa9e5b444d14 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:28:38 -0300 Subject: [PATCH 28/49] fix: update history_message_content_attributes to handle multiple unsupported message types --- app/services/whatsapp/baileys_handlers/messaging_history_set.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 5a1bf387563a2..0ddb582eaa775 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -192,7 +192,7 @@ def history_message_content(raw_message) def history_message_content_attributes(raw_message) { external_created_at: baileys_extract_message_timestamp(raw_message[:messageTimestamp]), - is_unsupported: history_message_type(raw_message[:message]) == 'unsupported' ? true : nil + is_unsupported: history_message_type(raw_message[:message]).in?(%w[image file video audio sticker unsupported]) || nil }.compact end From 682d26382d41165acf81608b97da8d56333de58f Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:28:57 -0300 Subject: [PATCH 29/49] fix: remove media attachment handling methods from history_handle_attach_media and related functions --- .../baileys_handlers/messaging_history_set.rb | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 0ddb582eaa775..e1c901b469bce 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -203,53 +203,4 @@ def process_status(status) 'READ' => 'read' }[status] end - - def history_handle_attach_media(message, conversation, raw_message) - attachment_file = history_download_attachment_file(conversation, raw_message) - message_type = history_message_type(raw_message[:message]) - file_type = history_file_content_type(message_type).to_s - message_mimetype = history_message_mimetype(message_type, raw_message) - attachment = message.attachments.build( - account_id: message.account_id, - file_type: file_type, - file: { io: attachment_file, filename: history_filename(raw_message, message_mimetype, file_type), - content_type: message_mimetype } - ) - attachment.meta = { is_recorded_audio: true } if raw_message.dig(:message, :audioMessage, :ptt) - rescue Down::Error => e - message.update!(is_unsupported: true) - - Rails.logger.error "Failed to download attachment for message #{raw_message[:key][:id]}: #{e.message}" - end - - def history_download_attachment_file(conversation, raw_message) - Down.download(conversation.inbox.channel.media_url(raw_message.dig(:key, :id)), headers: conversation.inbox.channel.api_headers) - end - - def history_file_content_type(message_type) - return :image if message_type.in?(%w[image sticker]) - return :video if message_type.in?(%w[video video_note]) - return :audio if message_type == 'audio' - - :file - end - - def history_filename(raw_message, message_mimetype, file_content_type) - filename = raw_message[:message][:documentMessage][:history_filename] - return filename if filename.present? - - ext = ".#{message_mimetype.split(';').first.split('/').last}" if message_mimetype.present? - "#{file_content_type}_#{raw_message[:key][:id]}_#{Time.current.strftime('%Y%m%d')}#{ext}" - end - - def history_message_mimetype(message_type, raw_message) - { - 'image' => raw_message.dig(:message, :imageMessage, :mimetype), - 'sticker' => raw_message.dig(:message, :stickerMessage, :mimetype), - 'video' => raw_message.dig(:message, :videoMessage, :mimetype), - 'audio' => raw_message.dig(:message, :audioMessage, :mimetype), - 'file' => raw_message.dig(:message, :documentMessage, :mimetype).presence || - raw_message.dig(:message, :documentWithCaptionMessage, :message, :documentMessage, :mimetype) - }[message_type] - end end From 8f03dbc125e46085e2a3beb8ad2a3781b9216469 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:36:45 -0300 Subject: [PATCH 30/49] fix: remove redundant WhatsApp sync labels from inbox management localization files --- app/javascript/dashboard/i18n/locale/en/inboxMgmt.json | 9 +-------- .../dashboard/i18n/locale/pt_BR/inboxMgmt.json | 8 +------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 5c69060101640..ce107f840244f 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -568,14 +568,7 @@ "UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings", "WHATSAPP_MARK_AS_READ_TITLE": "Read receipts", "WHATSAPP_MARK_AS_READ_SUBHEADER": "If turned off, when a message is viewed in Chatwoot, a read receipt will not be sent to the sender. Your messages will still be able to receive read receipts from the sender.", - "WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts", - - "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_TITLE": "Sync messages and contacts", - "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_SUBHEADER": "It will restore your messages and contacts from the WhatsApp.", - "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_LABEL": "Sync messages and contacts", - "WHATSAPP_FETCH_CONTACTS_TITLE": "Sync contacts", - "WHATSAPP_FETCH_CONTACTS_SUBHEADER": "It will restore your contacts from the WhatsApp.", - "WHATSAPP_FETCH_CONTACTS_LABEL": "Sync contacts" + "WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts" }, "HELP_CENTER": { "LABEL": "Help Center", diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json index 938c0729b6c17..b34a2539724a7 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json @@ -568,13 +568,7 @@ "UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat", "WHATSAPP_MARK_AS_READ_TITLE": "Confirmações de leitura", "WHATSAPP_MARK_AS_READ_SUBHEADER": "Se essa opção estiver desativada, ao visualizar uma mensagem pelo Chatwoot, não será enviada uma confirmação de leitura para o remetente. As suas mensagens ainda poderão receber confirmações de leitura.", - "WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura", - "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_TITLE": "Sincronizar mensagens e contatos", - "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_SUBHEADER": "Isso irá restaurar suas mensagens e contatos do WhatsApp.", - "WHATSAPP_FETCH_MESSAGES_AND_CONTACTS_LABEL": "Sincronizar mensagens e contatos", - "WHATSAPP_FETCH_CONTACTS_TITLE": "Sincronizar contatos", - "WHATSAPP_FETCH_CONTACTS_SUBHEADER": "Isso irá restaurar seus contatos do WhatsApp.", - "WHATSAPP_FETCH_CONTACTS_LABEL": "Sincronizar contatos" + "WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura" }, "HELP_CENTER": { "LABEL": "Centro de Ajuda", From 471b4414dd33cb821d10468f6661d3d51ee0689f Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:37:08 -0300 Subject: [PATCH 31/49] fix: update sync label order in inbox management localization file --- app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json index b34a2539724a7..92ce2bc6c3a09 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json @@ -274,7 +274,7 @@ }, "SYNC_FULL_HISTORY": { "ONLY_CONTACTS_LABEL": "Sincronizar apenas contatos", - "LABEL": "Sincronizar mensagens e contatos" + "LABEL": "Sincronizar contatos e mensagens" }, "ADVANCED_OPTIONS": "Opções avançadas", "BAILEYS": { From 8745ec0b303393a217c87f4a5f79637108f62bb0 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Fri, 27 Jun 2025 17:40:51 -0300 Subject: [PATCH 32/49] fix: correct phone number attribute in fetch_message_history method --- app/services/whatsapp/providers/whatsapp_baileys_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 83128e241cfb2..1f9eaf0dfe521 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -159,7 +159,7 @@ def unread_message(phone_number, message) # rubocop:disable Metrics/MethodLength end def fetch_message_history(oldest_message) - @phone_number = oldest_message.conversation.contact.phone_numbere_number + @phone_number = oldest_message.conversation.contact.phone_number response = HTTParty.post( "#{provider_url}/connections/#{whatsapp_channel.phone_number}/fetch-message-history", From 48a06ad1ed5676edffca27c8fc8439561cfa1dad Mon Sep 17 00:00:00 2001 From: gabrieljablonski Date: Mon, 30 Jun 2025 11:10:11 -0300 Subject: [PATCH 33/49] fix: enhance message flooding prevention and update fetch_message_history method to handle message timestamps correctly --- app/models/message.rb | 3 ++- .../baileys_handlers/messaging_history_set.rb | 19 +++++++++++-------- .../providers/whatsapp_baileys_service.rb | 12 +++++++----- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/app/models/message.rb b/app/models/message.rb index 1f1e69d339d64..cb36be735e669 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -60,7 +60,7 @@ class Message < ApplicationRecord }.to_json.freeze before_validation :ensure_content_type - before_validation :prevent_message_flooding + before_validation :prevent_message_flooding, unless: :skip_prevent_message_flooding before_save :ensure_processed_message_content before_save :ensure_in_reply_to @@ -78,6 +78,7 @@ class Message < ApplicationRecord # when you have a temperory id in your frontend and want it echoed back via action cable attr_accessor :echo_id + attr_accessor :skip_prevent_message_flooding enum message_type: { incoming: 0, outgoing: 1, activity: 2, template: 3 } enum content_type: { diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index e1c901b469bce..72c88f3a15500 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -1,7 +1,7 @@ module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength private - def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize,Metrics/MethodLength + def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize provider_config = inbox.channel.provider_config if provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence @@ -16,17 +16,19 @@ def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity return unless provider_config['sync_full_history'] - has_created_new_message = false + oldest_message = nil messages = params.dig(:data, :messages) || [] messages.each do |message| history_handle_message(message) - has_created_new_message = true - rescue StandardError => e - Rails.logger.error "Error processing message: #{e.message}" - Rails.logger.error e.backtrace.join("\n") + + message_timestamp = baileys_extract_message_timestamp(message[:messageTimestamp]) + old_timestamp = baileys_extract_message_timestamp(oldest_message[:messageTimestamp]) if oldest_message + oldest_message = message if oldest_message.nil? || message_timestamp < old_timestamp end - inbox.channel.fetch_message_history if has_created_new_message + # return if oldest_message.blank? || baileys_extract_message_timestamp(oldest_message[:messageTimestamp]) < 3.months.ago.to_i + + # inbox.channel.fetch_message_history(oldest_message) end # TODO: Refactor jid_type method in helpers to receive the jid as an argument and use it here @@ -141,6 +143,7 @@ def history_create_message(raw_message, contact_inbox) conversation = get_conversation(contact_inbox) inbox = contact_inbox.inbox message = conversation.messages.build( + skip_prevent_message_flooding: true, content: history_message_content(raw_message), account_id: inbox.account_id, inbox_id: inbox.id, @@ -149,7 +152,7 @@ def history_create_message(raw_message, contact_inbox) sender_type: history_incoming?(raw_message) ? 'Contact' : 'User', message_type: history_incoming?(raw_message) ? :incoming : :outgoing, content_attributes: history_message_content_attributes(raw_message), - status: process_status(raw_message[:status]) || 'sent' + status: 'read' ) message.save! diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 1f9eaf0dfe521..3cdf9f5442857 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -159,15 +159,17 @@ def unread_message(phone_number, message) # rubocop:disable Metrics/MethodLength end def fetch_message_history(oldest_message) - @phone_number = oldest_message.conversation.contact.phone_number - response = HTTParty.post( "#{provider_url}/connections/#{whatsapp_channel.phone_number}/fetch-message-history", headers: api_headers, body: { - count: ENV.fetch('BAILEYS_MESSAGE_HISTORY_COUNT', 5).to_i, - oldestMsgKey: { id: oldest_message.source_id, remoteJid: remote_jid, fromMe: oldest_message.message_type == 'outgoing' }, - oldestMsgTimestamp: oldest_message.content_attributes[:external_created_at].to_i + count: ENV.fetch('BAILEYS_MESSAGE_HISTORY_COUNT', 50).to_i, + oldestMsgKey: { + id: oldest_message[:key][:id], + remoteJid: oldest_message[:key][:remoteJid], + fromMe: oldest_message[:key][:fromMe] + }, + oldestMsgTimestamp: oldest_message[:messageTimestamp] } ) From d86cf1e5c00be77d4383a35a5e3efa6f33c6be12 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 17:37:15 -0300 Subject: [PATCH 34/49] chore: update fetch_message_history method to accept oldest_message parameter and register it in the service --- app/models/channel/whatsapp.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index ce67db3e989f7..9ac4f9d3875b8 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -117,15 +117,9 @@ def received_messages(messages, conversation) provider_service.received_messages(conversation.contact.phone_number, messages) end - def fetch_message_history + def fetch_message_history(oldest_message) return unless provider_service.respond_to?(:fetch_message_history) - oldest_message = inbox.messages - .where(message_type: %w[incoming outgoing]) - .order(Arel.sql("CAST(content_attributes->>'external_created_at' AS BIGINT) ASC")) - .first - return unless oldest_message&.content_attributes&.dig('external_created_at') - provider_service.fetch_message_history(oldest_message) end From 456ff1742fc390332bac8a8707f1b5be3a5d0148 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 17:37:38 -0300 Subject: [PATCH 35/49] chore: add fetch_message_history to error handling methods in WhatsappBaileysService --- app/services/whatsapp/providers/whatsapp_baileys_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 3cdf9f5442857..426b911c5529b 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -298,5 +298,6 @@ def handle_channel_error :update_presence, :read_messages, :unread_message, - :received_messages + :received_messages, + :fetch_message_history end From 23a9246128e3c752b04be5148415b3136c34c67a Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 18:35:21 -0300 Subject: [PATCH 36/49] fix: handle blank contact IDs and update name assignment logic in create_contact method --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 72c88f3a15500..954533e04ca16 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -43,8 +43,10 @@ def history_phone_number_from_jid(jid) end def create_contact(contact) + return if contact[:id].blank? + phone_number = history_phone_number_from_jid(contact[:id]) - name = contact[:verifiedName].presence || contact[:name].presence || phone_number + name = contact[:verifiedName].presence || contact[:notify].presence || contact[:name].presence || phone_number ::ContactInboxWithContactBuilder.new( # FIXME: update the source_id to complete jid in future source_id: phone_number, From ffcc625b340a590d10c6c1dd0178d74495404a9a Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 18:35:48 -0300 Subject: [PATCH 37/49] fix: update history_cache_message_source_id_in_redis to use set with options for improved key management --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 954533e04ca16..21942593d5d1f 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -129,10 +129,10 @@ def history_message_under_process?(source_id) Redis::Alfred.get(key) end - # TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here + # TODO: Refactor this method in helpers to receive the source_id as an argument and deprecate setex, then remove it from here def history_cache_message_source_id_in_redis(source_id) key = format(Redis::RedisKeys::MESSAGE_SOURCE_KEY, id: source_id) - ::Redis::Alfred.setex(key, true) + ::Redis::Alfred.set(key, true, nx: true, ex: 1.day) end # TODO: Refactor this method in helpers to receive the source_id as an argument and remove it from here From 6327952a1ea101e13ed57b98b5b6bc60e7f6bee4 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 18:53:34 -0300 Subject: [PATCH 38/49] test: create spec in message model for skip message flooding validation when flag is set --- spec/models/message_spec.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index 7d4548c60f122..173409e4ec762 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -38,6 +38,16 @@ expect(conv_new_message.errors[:base]).to eq(['Too many messages']) end end + + it 'skips message flooding validation if skip_prevent_message_flooding is true' do + with_modified_env 'CONVERSATION_MESSAGE_PER_MINUTE_LIMIT': '2' do + conversation = message.conversation + create(:message, conversation: conversation) + conv_new_message = build(:message, conversation: message.conversation, skip_prevent_message_flooding: true) + + expect(conv_new_message.valid?).to be true + end + end end end From e93d18a46b985e0fcd61ec4d2d333a8372c137b5 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 21:10:46 -0300 Subject: [PATCH 39/49] fix: skip processing of blank contact IDs in messaging history set --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 21942593d5d1f..233cb3a84d4f6 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -7,10 +7,9 @@ def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity if provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence contacts = params.dig(:data, :contacts) || [] contacts.each do |contact| + next if contact[:id].blank? + create_contact(contact) if jid_user?(contact[:id]) - rescue StandardError => e - Rails.logger.error "Error processing contact: #{e.message}" - Rails.logger.error e.backtrace.join("\n") end end @@ -43,8 +42,6 @@ def history_phone_number_from_jid(jid) end def create_contact(contact) - return if contact[:id].blank? - phone_number = history_phone_number_from_jid(contact[:id]) name = contact[:verifiedName].presence || contact[:notify].presence || contact[:name].presence || phone_number ::ContactInboxWithContactBuilder.new( From 8cd5da308712148534449ec3b8af77c0d7b39f8a Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 21:39:40 -0300 Subject: [PATCH 40/49] refactor: remove unused process_status method from MessagingHistorySet --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 233cb3a84d4f6..9dd2a534f63d0 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -197,12 +197,4 @@ def history_message_content_attributes(raw_message) is_unsupported: history_message_type(raw_message[:message]).in?(%w[image file video audio sticker unsupported]) || nil }.compact end - - def process_status(status) - { - 'PENDING' => 'sent', - 'DELIVERY_ACK' => 'delivered', - 'READ' => 'read' - }[status] - end end From 1859630eefde966b373cc1e1985a1065d8e2fdc7 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 21:42:45 -0300 Subject: [PATCH 41/49] refactor: simplify process_messaging_history_set method by removing unused variables --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 9dd2a534f63d0..22020578cfcf2 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -1,7 +1,7 @@ module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength private - def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity,Metrics/AbcSize + def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity provider_config = inbox.channel.provider_config if provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence @@ -15,14 +15,14 @@ def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity return unless provider_config['sync_full_history'] - oldest_message = nil + # oldest_message = nil messages = params.dig(:data, :messages) || [] messages.each do |message| history_handle_message(message) - message_timestamp = baileys_extract_message_timestamp(message[:messageTimestamp]) - old_timestamp = baileys_extract_message_timestamp(oldest_message[:messageTimestamp]) if oldest_message - oldest_message = message if oldest_message.nil? || message_timestamp < old_timestamp + # message_timestamp = baileys_extract_message_timestamp(message[:messageTimestamp]) + # old_timestamp = baileys_extract_message_timestamp(oldest_message[:messageTimestamp]) if oldest_message + # oldest_message = message if oldest_message.nil? || message_timestamp < old_timestamp end # return if oldest_message.blank? || baileys_extract_message_timestamp(oldest_message[:messageTimestamp]) < 3.months.ago.to_i From 0d47da506215670d600418597264c69d2f00b893 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 21:56:36 -0300 Subject: [PATCH 42/49] refactor: simplify process_messaging_history_set and create_contact validations --- .../baileys_handlers/messaging_history_set.rb | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 22020578cfcf2..2dbf907a63a89 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -1,16 +1,14 @@ module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength private - def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity + def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity provider_config = inbox.channel.provider_config - if provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence - contacts = params.dig(:data, :contacts) || [] - contacts.each do |contact| - next if contact[:id].blank? + return unless provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence - create_contact(contact) if jid_user?(contact[:id]) - end + contacts = params.dig(:data, :contacts) || [] + contacts.each do |contact| + create_contact(contact) end return unless provider_config['sync_full_history'] @@ -30,18 +28,9 @@ def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity # inbox.channel.fetch_message_history(oldest_message) end - # TODO: Refactor jid_type method in helpers to receive the jid as an argument and use it here - def jid_user?(jid) - server = jid.split('@').last - server == 's.whatsapp.net' || server == 'c.us' - end - - # TODO: Refactor this method in helpers to receive the jid as an argument and remove it from here - def history_phone_number_from_jid(jid) - jid.split('@').first.split(':').first.split('_').first - end - def create_contact(contact) + return unless contact[:id].present? && jid_user?(contact[:id]) + phone_number = history_phone_number_from_jid(contact[:id]) name = contact[:verifiedName].presence || contact[:notify].presence || contact[:name].presence || phone_number ::ContactInboxWithContactBuilder.new( @@ -52,6 +41,17 @@ def create_contact(contact) ).perform end + # TODO: Refactor jid_type method in helpers to receive the jid as an argument and use it here + def jid_user?(jid) + server = jid.split('@').last + server == 's.whatsapp.net' || server == 'c.us' + end + + # TODO: Refactor this method in helpers to receive the jid as an argument and remove it from here + def history_phone_number_from_jid(jid) + jid.split('@').first.split(':').first.split('_').first + end + def history_handle_message(raw_message) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity return unless raw_message[:key].present? && raw_message[:message].present? && raw_message[:messageTimestamp].present? From 5f15fa45f824eefa7198b7c29706a93185cb6586 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 22:32:58 -0300 Subject: [PATCH 43/49] fix: exclude unsupported message types from ignore processing list in history_message_type --- app/services/whatsapp/baileys_handlers/messaging_history_set.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 2dbf907a63a89..fb50c66346d0f 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -59,7 +59,7 @@ def history_handle_message(raw_message) # rubocop:disable Metrics/CyclomaticComp return unless jid_user?(jid) id = raw_message[:key][:id] - return if history_message_type(raw_message[:message]).in?(%w[protocol context unsupported]) + return if history_message_type(raw_message[:message]).in?(%w[protocol context]) return if history_find_message_by_source_id(id) || history_message_under_process?(id) history_cache_message_source_id_in_redis(id) From 05a65690ac10410018630ef564b47cefc97a6d23 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 22:55:31 -0300 Subject: [PATCH 44/49] refactor: update conversation creation to use history_conversation_params method --- .../whatsapp/baileys_handlers/messaging_history_set.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index fb50c66346d0f..4d9571aa0f3ee 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -163,10 +163,11 @@ def get_conversation(contact_inbox) # NOTE: if lock to single conversation is disabled, create a new conversation if previous conversation is resolved return contact_inbox.conversations.where.not(status: :resolved).last.presence || - ::Conversation.create!(conversation_params(contact_inbox)) + ::Conversation.create!(history_conversation_params(contact_inbox)) end - def conversation_params(contact_inbox) + # TODO: Refactor this method in helpers to receive the contact_inbox as an argument and remove it from here + def history_conversation_params(contact_inbox) { account_id: contact_inbox.inbox.account_id, inbox_id: contact_inbox.inbox.id, From b1cf9c8a5465f73d2fa3871ecc9ba4081b3389bf Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 22:55:56 -0300 Subject: [PATCH 45/49] test: add specs for messaging-history.set event handling --- .../incoming_message_baileys_service_spec.rb | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/spec/services/whatsapp/incoming_message_baileys_service_spec.rb b/spec/services/whatsapp/incoming_message_baileys_service_spec.rb index 234998f86b58f..e993c9e50f448 100644 --- a/spec/services/whatsapp/incoming_message_baileys_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_baileys_service_spec.rb @@ -781,6 +781,198 @@ end end end + + context 'when processing messaging-history.set event' do + let(:timestamp) { Time.current.to_i } + let(:contact_payload) { { id: '5511912345678@s.whatsapp.net', name: 'John Doe' } } + let(:message_payload) do + { + key: { id: 'msg_123', remoteJid: '5511912345678@s.whatsapp.net', fromMe: false }, + message: { conversation: 'History message' }, + messageTimestamp: timestamp + } + end + let(:base_params) do + { + webhookVerifyToken: webhook_verify_token, + event: 'messaging-history.set', + data: { + chats: [], + contacts: [contact_payload], + messages: [message_payload] + } + } + end + + context 'when sync_contacts is disabled' do + it 'does not create contacts' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_contacts' => false })) + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.not_to change(Contact, :count) + end + end + + context 'when sync_contacts is enabled' do + it 'creates contacts' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_contacts' => true })) + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.to change(Contact, :count).by(1) + expect(Contact.last.name).to eq('John Doe') + end + + it 'does not create contact if jid is not a user' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_contacts' => true })) + contact_payload[:id] = 'status@broadcast' + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.not_to change(Contact, :count) + end + + it 'does not creat contact if do not have id' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_contacts' => true })) + base_params[:data][:contacts] = [{ name: 'John Doe' }] + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.not_to change(Contact, :count) + end + end + + context 'when sync_full_history is disabled' do + it 'does not create contacts' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => false })) + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.not_to change(Contact, :count) + end + + it 'does not create messages' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => false })) + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.not_to change(Message, :count) + end + end + + context 'when full history sync is enabled' do + it 'creates contacts' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => true })) + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.to change(Contact, :count).by(1) + expect(Contact.last.name).to eq('John Doe') + end + + it 'creates messages from history' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => true })) + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.to change(Message, :count).by(1) + + message = Message.last + expect(message).to have_attributes( + inbox_id: inbox.id, + conversation_id: inbox.conversations.last.id, + content: 'History message', + source_id: 'msg_123', + status: 'read', + sender_type: 'Contact', + message_type: 'incoming', + content_attributes: { external_created_at: timestamp } + ) + end + + it 'creates outgoing messages from history' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => true })) + message_payload[:key][:fromMe] = true + create(:account_user, account: inbox.account) + + described_class.new(inbox: inbox, params: base_params).perform + + message = Message.last + expect(message).to have_attributes( + inbox_id: inbox.id, + conversation_id: inbox.conversations.last.id, + content: 'History message', + source_id: 'msg_123', + status: 'read', + sender_type: 'User', + message_type: 'outgoing', + content_attributes: { external_created_at: timestamp } + ) + end + + it 'creates unsupported message if message type is some media' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => true })) + message_payload[:message] = { imageMessage: { caption: 'Unsupported media' } } + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.to change(Message, :count).by(1) + + message = Message.last + expect(message).to have_attributes( + inbox_id: inbox.id, + conversation_id: inbox.conversations.last.id, + source_id: 'msg_123', + content_attributes: { is_unsupported: true, external_created_at: timestamp } + ) + end + + it 'creates unsupported message if message type is unsupported' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => true })) + message_payload[:message] = { unsupported: 'message' } + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.to change(Message, :count).by(1) + + message = Message.last + expect(message).to have_attributes( + inbox_id: inbox.id, + conversation_id: inbox.conversations.last.id, + source_id: 'msg_123', + content_attributes: { is_unsupported: true, external_created_at: timestamp } + ) + end + + it 'does not create message if it already exists' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => true })) + create(:message, inbox: inbox, source_id: 'msg_123') + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.not_to change(Message, :count) + end + + it 'does not create message for protocol messages' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => true })) + message_payload[:message] = { protocolMessage: { type: 1 } } + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.not_to change(Message, :count) + end + + it 'does not create message for context messages' do + whatsapp_channel.update!(provider_config: whatsapp_channel.provider_config.merge({ 'sync_full_history' => true })) + message_payload[:message] = { 'messageContextInfo': { 'messageSecret': '********' } } + + expect do + described_class.new(inbox: inbox, params: base_params).perform + end.not_to change(Message, :count) + end + end + end end def format_message_source_key(message_id) From e38f8935164762f218e73e38367d83e31df1d4d4 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 23:13:31 -0300 Subject: [PATCH 46/49] refactor: streamline process_messaging_history_set by consolidating contact and message processing --- .../baileys_handlers/messaging_history_set.rb | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index 4d9571aa0f3ee..bdbfd1357ca52 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -1,31 +1,27 @@ module Whatsapp::BaileysHandlers::MessagingHistorySet # rubocop:disable Metrics/ModuleLength private - def process_messaging_history_set # rubocop:disable Metrics/CyclomaticComplexity + def process_messaging_history_set provider_config = inbox.channel.provider_config return unless provider_config['sync_contacts'].presence || provider_config['sync_full_history'].presence + process_contacts(params) + process_messages(params) if provider_config['sync_full_history'].presence + end + + def process_contacts(params) contacts = params.dig(:data, :contacts) || [] contacts.each do |contact| create_contact(contact) end + end - return unless provider_config['sync_full_history'] - - # oldest_message = nil + def process_messages(params) messages = params.dig(:data, :messages) || [] messages.each do |message| history_handle_message(message) - - # message_timestamp = baileys_extract_message_timestamp(message[:messageTimestamp]) - # old_timestamp = baileys_extract_message_timestamp(oldest_message[:messageTimestamp]) if oldest_message - # oldest_message = message if oldest_message.nil? || message_timestamp < old_timestamp end - - # return if oldest_message.blank? || baileys_extract_message_timestamp(oldest_message[:messageTimestamp]) < 3.months.ago.to_i - - # inbox.channel.fetch_message_history(oldest_message) end def create_contact(contact) @@ -55,10 +51,10 @@ def history_phone_number_from_jid(jid) def history_handle_message(raw_message) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity return unless raw_message[:key].present? && raw_message[:message].present? && raw_message[:messageTimestamp].present? + id = raw_message[:key][:id] jid = raw_message[:key][:remoteJid] - return unless jid_user?(jid) - id = raw_message[:key][:id] + return unless jid_user?(jid) return if history_message_type(raw_message[:message]).in?(%w[protocol context]) return if history_find_message_by_source_id(id) || history_message_under_process?(id) From 1537b1d0257f47cfa143b3090e0cb9ba6d915f92 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Mon, 30 Jun 2025 23:29:06 -0300 Subject: [PATCH 47/49] refactor: enhance history_handle_message and history_message_valid methods for improved validation and error handling --- .../baileys_handlers/messaging_history_set.rb | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb index bdbfd1357ca52..77b7b34a6a5a5 100644 --- a/app/services/whatsapp/baileys_handlers/messaging_history_set.rb +++ b/app/services/whatsapp/baileys_handlers/messaging_history_set.rb @@ -48,28 +48,38 @@ def history_phone_number_from_jid(jid) jid.split('@').first.split(':').first.split('_').first end - def history_handle_message(raw_message) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity - return unless raw_message[:key].present? && raw_message[:message].present? && raw_message[:messageTimestamp].present? + def history_handle_message(raw_message) + return unless history_message_valid?(raw_message) - id = raw_message[:key][:id] - jid = raw_message[:key][:remoteJid] - - return unless jid_user?(jid) - return if history_message_type(raw_message[:message]).in?(%w[protocol context]) - return if history_find_message_by_source_id(id) || history_message_under_process?(id) + id = raw_message.dig(:key, :id) + jid = raw_message.dig(:key, :remoteJid) history_cache_message_source_id_in_redis(id) - contact_inbox = find_contact_inbox(jid) - - unless contact_inbox.contact + begin + contact_inbox = find_contact_inbox(jid) + unless contact_inbox.contact + Rails.logger.warn "Contact not found for message: #{id}" + return + end + + history_create_message(raw_message, contact_inbox) + ensure history_clear_message_source_id_from_redis(id) - - Rails.logger.warn "Contact not found for message: #{id}" - return end + end + + def history_message_valid?(raw_message) # rubocop:disable Metrics/CyclomaticComplexity + id = raw_message.dig(:key, :id) + jid = raw_message.dig(:key, :remoteJid) - history_create_message(raw_message, contact_inbox) - history_clear_message_source_id_from_redis(id) + id.present? && + jid.present? && + raw_message[:message].present? && + raw_message[:messageTimestamp].present? && + jid_user?(jid) && + !history_message_type(raw_message[:message]).in?(%w[protocol context]) && + !history_find_message_by_source_id(id) && + !history_message_under_process?(id) end # TODO: Refactor this method in helpers to receive the raw message as an argument and remove it from here From 8cc5b26c843c2d05fd46fbc2d0edd24f2b29b747 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Tue, 1 Jul 2025 00:15:46 -0300 Subject: [PATCH 48/49] test: add specs for sync_full_history and sync_contacts in setup_channel_provider --- .../whatsapp_baileys_service_spec.rb | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb index 5a979286c63d4..de1b6ac36c70d 100644 --- a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb @@ -34,6 +34,52 @@ end end + context 'when sync_full_history is true' do + it 'includes syncFullHistory in the request body' do + whatsapp_channel.provider_config['sync_full_history'] = true + + stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}") + .with( + headers: stub_headers(whatsapp_channel), + body: { + clientName: 'chatwoot-test', + webhookUrl: whatsapp_channel.inbox.callback_webhook_url, + webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token'], + includeMedia: false, + syncFullHistory: true + }.to_json + ) + .to_return(status: 200) + + response = service.setup_channel_provider + + expect(response).to be(true) + end + end + + context 'when sync_contacts is true' do + it 'includes syncFullHistory in the request body' do + whatsapp_channel.provider_config['sync_contacts'] = true + + stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}") + .with( + headers: stub_headers(whatsapp_channel), + body: { + clientName: 'chatwoot-test', + webhookUrl: whatsapp_channel.inbox.callback_webhook_url, + webhookVerifyToken: whatsapp_channel.provider_config['webhook_verify_token'], + includeMedia: false, + syncFullHistory: true + }.to_json + ) + .to_return(status: 200) + + response = service.setup_channel_provider + + expect(response).to be(true) + end + end + context 'when response is unsuccessful' do it 'logs the error and returns false' do stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}") From dde9ed4d281e3b260abd6fbd79c92c3c287d1665 Mon Sep 17 00:00:00 2001 From: CayoPOliveira Date: Tue, 1 Jul 2025 00:16:48 -0300 Subject: [PATCH 49/49] chore: update fetch_message_history to ensure request body is properly formatted as JSON and spec --- .../providers/whatsapp_baileys_service.rb | 2 +- .../whatsapp_baileys_service_spec.rb | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 426b911c5529b..fba4e76d552b4 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -170,7 +170,7 @@ def fetch_message_history(oldest_message) fromMe: oldest_message[:key][:fromMe] }, oldestMsgTimestamp: oldest_message[:messageTimestamp] - } + }.to_json ) process_response(response) diff --git a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb index de1b6ac36c70d..9b85d7f66bd11 100644 --- a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb @@ -484,6 +484,34 @@ end end + describe '#fetch_message_history' do + it 'send fetch message history request' do + message = { + key: { id: 'msg_123', remoteJid: test_send_jid, fromMe: false }, + messageTimestamp: 123 + } + allow(ENV).to receive(:fetch).and_call_original + allow(ENV).to receive(:fetch).with('BAILEYS_MESSAGE_HISTORY_COUNT', 50).and_return(50) + stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/fetch-message-history") + .with( + headers: stub_headers(whatsapp_channel), + body: { + count: 50, + oldestMsgKey: { + id: 'msg_123', + remoteJid: test_send_jid, + fromMe: false + }, + oldestMsgTimestamp: 123 + }.to_json + ).to_return(status: 200) + + result = service.fetch_message_history(message) + + expect(result).to be(true) + end + end + describe '#received_messages' do it 'send received messages request' do stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-receipts")