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 <contact@gabrieljablonski.com>
This commit is contained in:
Cayo P. R. Oliveira 2025-05-23 17:19:01 -03:00 committed by GitHub
parent 21133c3383
commit 548c0351ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 94 additions and 19 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)