From 548c0351ecd973a51f7f8f534b863787defb7085 Mon Sep 17 00:00:00 2001 From: "Cayo P. R. Oliveira" Date: Fri, 23 May 2025 17:19:01 -0300 Subject: [PATCH] Fix/baileys unread message (#50) * fix: correct structure of lastMessages in message content * fix: update send_message to store external_created_at timestamp with result from request * fix: filter incoming messages for unread conversation logic * fix: improve extraction of message timestamp in send_message method * feat: enhance message timestamp extraction and update logic in send_message method * refactor: send_message to improve error handling and update external_created_at timestamp * chore: move message timestamp extraction to helper * fix: use message timestamp extract on incoming * fix: update unread_conversation to handle last message correctly for Baileys provider --------- Co-authored-by: gabrieljablonski --- app/helpers/baileys_helper.rb | 13 +++++ app/models/channel/whatsapp.rb | 1 + .../incoming_message_baileys_service.rb | 6 +-- .../providers/whatsapp_baileys_service.rb | 19 +++++-- spec/helpers/baileys_helper_spec.rb | 20 ++++++++ spec/models/channel/whatsapp_spec.rb | 4 +- .../whatsapp_baileys_service_spec.rb | 50 +++++++++++++++---- 7 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 app/helpers/baileys_helper.rb create mode 100644 spec/helpers/baileys_helper_spec.rb diff --git a/app/helpers/baileys_helper.rb b/app/helpers/baileys_helper.rb new file mode 100644 index 000000000..ea5591212 --- /dev/null +++ b/app/helpers/baileys_helper.rb @@ -0,0 +1,13 @@ +module BaileysHelper + def extract_baileys_message_timestamp(timestamp) + # NOTE: Timestamp might be in this format {"low"=>1748003165, "high"=>0, "unsigned"=>true} + if timestamp.is_a?(Hash) && timestamp.key?('low') + low = timestamp['low'].to_i + high = timestamp.fetch('high', 0).to_i + return (high << 32) | low + end + + # NOTE: Timestamp might be a string or a number + timestamp.to_i + end +end diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index 111afb45b..66e3a3883 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -100,6 +100,7 @@ class Channel::Whatsapp < ApplicationRecord def unread_conversation(conversation) return unless provider_service.respond_to?(:unread_message) + # NOTE: For the Baileys provider, the last message is required even if it is an outgoing message. last_message = conversation.messages.last provider_service.unread_message(conversation.contact.phone_number, last_message) if last_message end diff --git a/app/services/whatsapp/incoming_message_baileys_service.rb b/app/services/whatsapp/incoming_message_baileys_service.rb index 650bdae1c..2327e2791 100644 --- a/app/services/whatsapp/incoming_message_baileys_service.rb +++ b/app/services/whatsapp/incoming_message_baileys_service.rb @@ -1,4 +1,6 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseService # rubocop:disable Metrics/ClassLength + include BaileysHelper + class InvalidWebhookVerifyToken < StandardError; end class MessageNotFoundError < StandardError; end class AttachmentNotFoundError < StandardError; end @@ -171,9 +173,7 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer end def message_content_attributes - content_attributes = { - external_created_at: @raw_message[:messageTimestamp] - } + content_attributes = { external_created_at: extract_baileys_message_timestamp(@raw_message[:messageTimestamp]) } if message_type == 'reaction' content_attributes[:in_reply_to_external_id] = @raw_message.dig(:message, :reactionMessage, :key, :id) content_attributes[:is_reaction] = true diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index e3c38db4e..e55a005af 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -1,4 +1,6 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseService # rubocop:disable Metrics/ClassLength + include BaileysHelper + class MessageContentTypeNotSupported < StandardError; end class MessageNotSentError < StandardError; end @@ -139,14 +141,14 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer jid: remote_jid, mod: { markRead: false, - lastMessages: { + lastMessages: [{ key: { id: message.source_id, remoteJid: remote_jid, fromMe: message.message_type == 'outgoing' }, messageTimestamp: message.content_attributes[:external_created_at] - } + }] } }.to_json ) @@ -209,9 +211,10 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer }.to_json ) - return response.parsed_response.dig('data', 'key', 'id') if process_response(response) + raise MessageNotSentError unless process_response(response) - raise MessageNotSentError + update_external_created_at(response) + response.parsed_response.dig('data', 'key', 'id') end def process_response(response) @@ -223,6 +226,14 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer "#{@phone_number.delete('+')}@s.whatsapp.net" end + def update_external_created_at(response) + timestamp = response.parsed_response.dig('data', 'messageTimestamp') + return unless timestamp + + external_created_at = extract_baileys_message_timestamp(timestamp) + @message.update!(external_created_at: external_created_at) + end + private_class_method def self.with_error_handling(*method_names) method_names.each do |method_name| original_method = instance_method(method_name) diff --git a/spec/helpers/baileys_helper_spec.rb b/spec/helpers/baileys_helper_spec.rb new file mode 100644 index 000000000..eb9a1b43d --- /dev/null +++ b/spec/helpers/baileys_helper_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.describe BaileysHelper do + let(:timestamp) { 1_748_003_165 } + let(:timestamp_hash) { { 'low' => timestamp, 'high' => 123, 'unsigned' => true } } + + it 'extracts the timestamp from a string' do + expect(extract_baileys_message_timestamp(timestamp.to_s)).to eq(timestamp) + end + + it 'extracts the timestamp from an int' do + expect(extract_baileys_message_timestamp(timestamp)).to eq(timestamp) + end + + it 'extracts the timestamp from a hash' do + expected_timestamp = timestamp + (timestamp_hash['high'] << 32) + + expect(extract_baileys_message_timestamp(timestamp_hash)).to eq(expected_timestamp) + end +end diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index 8b6606945..bc597e6dc 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -155,7 +155,7 @@ RSpec.describe Channel::Whatsapp do let(:conversation) { create(:conversation) } it 'calls provider service method' do - message = create(:message, conversation: conversation) + message = create(:message, conversation: conversation, message_type: 'incoming') provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, unread_message: nil) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new).with(whatsapp_channel: channel).and_return(provider_double) allow(provider_double).to receive(:unread_message).with(conversation.contact.phone_number, [message]) @@ -167,7 +167,7 @@ RSpec.describe Channel::Whatsapp do it 'does not call method if provider service does not implement it' do # NOTE: This message ensures that there are messages but the provider does not implement the method. - create(:message, conversation: conversation) + create(:message, conversation: conversation, message_type: 'incoming') provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new).with(whatsapp_channel: channel).and_return(provider_double) diff --git a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb index 33d0f856e..e6c50358a 100644 --- a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb @@ -95,6 +95,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do describe '#send_message' do let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message" } + let(:result_body) { { 'data' => { 'key' => { 'id' => 'msg_123' } } } } context 'when message is unsupported' do it 'updates the message with content attribute is_unsupported' do @@ -132,7 +133,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json + body: result_body.to_json ) result = service.send_message(test_send_phone_number, message) @@ -153,7 +154,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json + body: result_body.to_json ) result = service.send_message(test_send_phone_number, message) @@ -188,7 +189,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json + body: result_body.to_json ) result = service.send_message(test_send_phone_number, message) @@ -210,7 +211,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json + body: result_body.to_json ) result = service.send_message(test_send_phone_number, message) @@ -222,16 +223,43 @@ describe Whatsapp::Providers::WhatsappBaileysService do context 'when message is a text' do it 'sends the message' do stub_request(:post, request_path) + .with( + headers: stub_headers(whatsapp_channel), + body: { + jid: test_send_jid, + messageContent: { text: message.content } + }.to_json + ) .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, - body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json + body: result_body.to_json ) result = service.send_message(test_send_phone_number, message) expect(result).to eq('msg_123') end + + it 'updates the message external_created_at' do + stub_request(:post, request_path) + .with( + headers: stub_headers(whatsapp_channel), + body: { + jid: test_send_jid, + messageContent: { text: message.content } + }.to_json + ) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { 'data' => { 'key' => { 'id' => 'msg_123' }, 'messageTimestamp' => 1_748_003_165 } }.to_json + ) + + service.send_message(test_send_phone_number, message) + + expect(message.reload.content_attributes['external_created_at']).to eq(1_748_003_165) + end end context 'when message is a reaction' do @@ -302,7 +330,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do .to_return( status: 400, headers: { 'Content-Type' => 'application/json' }, - body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json + body: result_body.to_json ) expect do @@ -394,10 +422,12 @@ describe Whatsapp::Providers::WhatsappBaileysService do jid: test_send_jid, mod: { markRead: false, - lastMessages: { - key: { id: 'msg_123', remoteJid: test_send_jid, fromMe: false }, - messageTimestamp: 123 - } + lastMessages: [ + { + key: { id: 'msg_123', remoteJid: test_send_jid, fromMe: false }, + messageTimestamp: 123 + } + ] } }.to_json ).to_return(status: 200)