From 6a5c0b515d7d7467e5ec74412ffba955497856ad Mon Sep 17 00:00:00 2001 From: "Cayo P. R. Oliveira" Date: Tue, 17 Jun 2025 11:05:28 -0300 Subject: [PATCH] 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 --- app/models/channel/whatsapp.rb | 6 +++ .../baileys_handlers/messages_upsert.rb | 2 + .../providers/whatsapp_baileys_service.rb | 26 +++++++++++-- spec/factories/channel/channel_whatsapp.rb | 2 + spec/models/channel/whatsapp_spec.rb | 26 +++++++++++++ .../incoming_message_baileys_service_spec.rb | 37 ++++++++++++++++++- .../whatsapp_baileys_service_spec.rb | 16 ++++++++ 7 files changed, 111 insertions(+), 4 deletions(-) diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index f198a91e1..2620e5463 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -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 diff --git a/app/services/whatsapp/baileys_handlers/messages_upsert.rb b/app/services/whatsapp/baileys_handlers/messages_upsert.rb index 9f61e8acd..1d3f086d2 100644 --- a/app/services/whatsapp/baileys_handlers/messages_upsert.rb +++ b/app/services/whatsapp/baileys_handlers/messages_upsert.rb @@ -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 diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index 1dea14a54..dbe76ec20 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -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 diff --git a/spec/factories/channel/channel_whatsapp.rb b/spec/factories/channel/channel_whatsapp.rb index fd0786a00..33f125312 100644 --- a/spec/factories/channel/channel_whatsapp.rb +++ b/spec/factories/channel/channel_whatsapp.rb @@ -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 }) diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index 9150ba01e..8f7c9811d 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -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 diff --git a/spec/services/whatsapp/incoming_message_baileys_service_spec.rb b/spec/services/whatsapp/incoming_message_baileys_service_spec.rb index 5062ac8d2..234998f86 100644 --- a/spec/services/whatsapp/incoming_message_baileys_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_baileys_service_spec.rb @@ -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 diff --git a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb index e6c50358a..5a979286c 100644 --- a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb @@ -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" }