fix: send receipt to received messages (#70)

* feat: add received messages handling in WhatsApp services

* feat: ensure received messages are processed in the conversation

* tests: add received messages specs

* tests: add spec for call provider received_messages after save incoming message

* test: add spec to prevent calling received_messages for outgoing messages

* feat: add received_messages to error handling methods

* test: add spec for sending received messages request in WhatsappBaileysService

* fix: update fromMe flag in WhatsappBaileysService

* test: add spec to verify received_messages is not called for protocol messages

* test: improve received_messages stub

---------

Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
This commit is contained in:
Cayo P. R. Oliveira 2025-06-17 11:05:28 -03:00 committed by GitHub
parent a355b53464
commit 6a5c0b515d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 111 additions and 4 deletions

View File

@ -111,6 +111,12 @@ class Channel::Whatsapp < ApplicationRecord
provider_service.disconnect_channel_provider
end
def received_messages(messages, conversation)
return unless provider_service.respond_to?(:received_messages)
provider_service.received_messages(conversation.contact.phone_number, messages)
end
delegate :setup_channel_provider, to: :provider_service
delegate :send_message, to: :provider_service
delegate :send_template, to: :provider_service

View File

@ -75,6 +75,8 @@ module Whatsapp::BaileysHandlers::MessagesUpsert
handle_attach_media if attach_media
@message.save!
inbox.channel.received_messages([@message], @conversation) if incoming?
end
def message_content_attributes

View File

@ -121,8 +121,7 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
{
id: message.source_id,
remoteJid: remote_jid,
# NOTE: It only makes sense to mark received messages as read
fromMe: false
fromMe: message.message_type == 'outgoing'
}
end
}.to_json
@ -156,6 +155,26 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
process_response(response)
end
def received_messages(phone_number, messages)
@phone_number = phone_number
response = HTTParty.post(
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-receipts",
headers: api_headers,
body: {
keys: messages.map do |message|
{
id: message.source_id,
remoteJid: remote_jid,
fromMe: message.message_type == 'outgoing'
}
end
}.to_json
)
process_response(response)
end
private
def provider_url
@ -257,5 +276,6 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
:toggle_typing_status,
:update_presence,
:read_messages,
:unread_message
:unread_message,
:received_messages
end

View File

@ -69,12 +69,14 @@ FactoryBot.define do
transient do
sync_templates { true }
validate_provider_config { true }
received_messages { true }
end
before(:create) do |channel_whatsapp, options|
# since factory already has the required message templates, we just need to bypass it getting updated
channel_whatsapp.define_singleton_method(:sync_templates) { nil } unless options.sync_templates
channel_whatsapp.define_singleton_method(:validate_provider_config) { nil } unless options.validate_provider_config
channel_whatsapp.define_singleton_method(:received_messages) { |_, _| nil } unless options.received_messages
if channel_whatsapp.provider == 'baileys'
channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'provider_url' => 'https://baileys.api',
'phone_number_id' => '123456789', 'mark_as_read' => true })

View File

@ -208,6 +208,32 @@ RSpec.describe Channel::Whatsapp do
end
end
describe '#received_messages' do
let(:channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false, sync_templates: false) }
let(:conversation) { create(:conversation) }
let(:messages) { [create(:message, conversation: conversation)] }
it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, received_messages: nil)
allow(provider_double).to receive(:received_messages).with(conversation.contact.phone_number, messages)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)
channel.received_messages(messages, conversation)
expect(provider_double).to have_received(:received_messages)
end
it 'does not call method if provider service does not implement it' do
channel.update!(provider: 'whatsapp_cloud')
expect do
channel.received_messages(messages, conversation)
end.not_to raise_error
end
end
describe 'callbacks' do
describe '#disconnect_channel_provider' do
context 'when provider is baileys' do

View File

@ -7,7 +7,8 @@ describe Whatsapp::IncomingMessageBaileysService do
create(:channel_whatsapp,
provider: 'baileys',
provider_config: { webhook_verify_token: webhook_verify_token },
validate_provider_config: false)
validate_provider_config: false,
received_messages: false)
end
let(:inbox) { whatsapp_channel.inbox }
@ -198,6 +199,16 @@ describe Whatsapp::IncomingMessageBaileysService do
expect(inbox.messages).to be_empty
expect(inbox.contact_inboxes).to be_empty
end
it 'does not call channel received_messages method' do
raw_message[:message] = { protocolMessage: { type: 1 } }
allow(inbox.channel).to receive(:received_messages)
described_class.new(inbox: inbox, params: params).perform
expect(inbox.channel).not_to have_received(:received_messages)
end
end
context 'when message is context message' do
@ -233,6 +244,30 @@ describe Whatsapp::IncomingMessageBaileysService do
end
end
context 'when message is saved' do
it 'calls channel received_messages method' do
allow(inbox.channel).to receive(:received_messages)
described_class.new(inbox: inbox, params: params).perform
conversation = inbox.conversations.last
message = conversation.messages.last
expect(inbox.channel).to have_received(:received_messages).with([message], conversation)
end
it 'does not call channel received_messages method if message is outgoing' do
raw_message[:key][:fromMe] = true
create(:account_user, account: inbox.account)
allow(inbox.channel).to receive(:received_messages)
described_class.new(inbox: inbox, params: params).perform
expect(inbox.channel).not_to have_received(:received_messages)
end
end
context 'when message type is text' do
context 'when has key conversation' do # rubocop:disable RSpec/NestedGroups
it 'creates an incoming message' do

View File

@ -438,6 +438,22 @@ describe Whatsapp::Providers::WhatsappBaileysService do
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")
.with(
headers: stub_headers(whatsapp_channel),
body: {
keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }]
}.to_json
).to_return(status: 200)
result = service.received_messages(test_send_phone_number, [message])
expect(result).to be(true)
end
end
describe '#toggle_typing_status' do
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/presence" }