feat: whatsapp cloud service typing status and read messages (#106)

This commit is contained in:
Gabriel Jablonski 2025-09-02 15:20:13 -03:00 committed by GitHub
parent dbb41df67e
commit eaf2f99520
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 146 additions and 21 deletions

View File

@ -84,7 +84,8 @@ class Channel::Whatsapp < ApplicationRecord
def toggle_typing_status(typing_status, conversation:)
return unless provider_service.respond_to?(:toggle_typing_status)
provider_service.toggle_typing_status(conversation.contact.phone_number, typing_status)
last_message = conversation.messages.last
provider_service.toggle_typing_status(typing_status, last_message: last_message, phone_number: conversation.contact.phone_number)
end
def update_presence(status)
@ -98,7 +99,7 @@ class Channel::Whatsapp < ApplicationRecord
# NOTE: This is the default behavior, so `mark_as_read` being `nil` is the same as `true`.
return if provider_config&.dig('mark_as_read') == false
provider_service.read_messages(conversation.contact.phone_number, messages)
provider_service.read_messages(messages, phone_number: conversation.contact.phone_number)
end
def unread_conversation(conversation)

View File

@ -94,7 +94,7 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
process_response(response)
end
def toggle_typing_status(phone_number, typing_status)
def toggle_typing_status(typing_status, phone_number:, **)
@phone_number = phone_number
status_map = {
Events::Types::CONVERSATION_TYPING_ON => 'composing',
@ -136,7 +136,7 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
true
end
def read_messages(phone_number, messages)
def read_messages(messages, phone_number:, **)
@phone_number = phone_number
response = HTTParty.post(
@ -319,12 +319,12 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
method_names.each do |method_name|
original_method = instance_method(method_name)
define_method("#{method_name}_without_error_handling") do |*args, &block|
original_method.bind_call(self, *args, &block)
define_method("#{method_name}_without_error_handling") do |*args, **kwargs, &block|
original_method.bind_call(self, *args, **kwargs, &block)
end
define_method(method_name) do |*args, &block|
original_method.bind_call(self, *args, &block)
define_method(method_name) do |*args, **kwargs, &block|
original_method.bind_call(self, *args, **kwargs, &block)
rescue StandardError => e
handle_channel_error
raise e

View File

@ -71,8 +71,8 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
end
# TODO: See if we can unify the API versions and for both paths and make it consistent with out facebook app API versions
def phone_id_path
"#{api_base_path}/v13.0/#{whatsapp_channel.provider_config['phone_number_id']}"
def phone_id_path(version = 'v13.0')
"#{api_base_path}/#{version}/#{whatsapp_channel.provider_config['phone_number_id']}"
end
def business_account_path
@ -181,4 +181,41 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
process_response(response, message)
end
def toggle_typing_status(typing_status, last_message:, **)
return false unless [Events::Types::CONVERSATION_TYPING_ON, Events::Types::CONVERSATION_RECORDING].include?(typing_status)
response = HTTParty.post(
"#{phone_id_path('v23.0')}/messages",
headers: api_headers,
body: {
messaging_product: 'whatsapp',
message_id: last_message.source_id,
# NOTE: API currently only supports "typing", no "recording" status.
typing_indicator: { type: 'text' }
}.to_json
)
Rails.logger.error(response.parsed_response) unless response.success?
response.success?
end
def read_messages(messages, **)
# NOTE: Marking the last message as read automatically applies to all previous ones.
message = messages.last
response = HTTParty.post(
"#{phone_id_path('v23.0')}/messages",
headers: api_headers,
body: {
messaging_product: 'whatsapp',
message_id: message.source_id,
status: 'read'
}.to_json
)
Rails.logger.error(response.parsed_response) unless response.success?
response.success?
end
end

View File

@ -186,7 +186,7 @@ RSpec.describe Channel::Whatsapp do
it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil)
allow(provider_double).to receive(:toggle_typing_status)
.with(conversation.contact.phone_number, Events::Types::CONVERSATION_TYPING_ON)
.with(Events::Types::CONVERSATION_TYPING_ON, phone_number: conversation.contact.phone_number)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)
@ -246,7 +246,7 @@ RSpec.describe Channel::Whatsapp do
it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil)
allow(provider_double).to receive(:read_messages).with([message], conversation.contact.phone_number)
allow(provider_double).to receive(:read_messages).with([message], phone_number: conversation.contact.phone_number)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)
@ -259,7 +259,7 @@ RSpec.describe Channel::Whatsapp do
it 'call method when the provider config mark_as_read is nil' do
channel.update!(provider_config: {})
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil)
allow(provider_double).to receive(:read_messages).with([message], conversation.contact.phone_number)
allow(provider_double).to receive(:read_messages).with([message], phone_number: conversation.contact.phone_number)
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel)
.and_return(provider_double)
@ -270,7 +270,7 @@ RSpec.describe Channel::Whatsapp do
end
it 'does not call method if provider service does not implement it' do
channel.update!(provider: 'whatsapp_cloud')
channel.update!(provider: 'default')
expect do
channel.read_messages([message], conversation: conversation)

View File

@ -477,7 +477,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
body: { keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }] }.to_json
).to_return(status: 200, body: '', headers: {})
result = service.read_messages(test_send_phone_number, [message])
result = service.read_messages([message], phone_number: test_send_phone_number)
expect(result).to be(true)
end
@ -495,7 +495,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
.to_return(status: 200)
expect do
service.read_messages(test_send_phone_number, [message])
service.read_messages([message], phone_number: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
end
end
@ -605,7 +605,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
)
.to_return(status: 200)
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_ON)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, phone_number: test_send_phone_number)
expect(request).to have_been_requested
end
@ -621,7 +621,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
)
.to_return(status: 200)
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_RECORDING)
service.toggle_typing_status(Events::Types::CONVERSATION_RECORDING, phone_number: test_send_phone_number)
expect(request).to have_been_requested
end
@ -637,7 +637,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
)
.to_return(status: 200)
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_OFF)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_OFF, phone_number: test_send_phone_number)
expect(request).to have_been_requested
end
@ -664,7 +664,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
allow(Rails.logger).to receive(:error)
expect do
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_ON)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, phone_number: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message')
@ -916,7 +916,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
expect do
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_ON)
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, phone_number: test_send_phone_number)
end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')

View File

@ -312,4 +312,91 @@ describe Whatsapp::Providers::WhatsappCloudService do
end
end
end
describe '#toggle_typing_status' do
let(:conversation) { create(:conversation) }
it 'calls messages endpoint with typing indicator for "conversation.typing_on"' do
stub_request(:post, 'https://graph.facebook.com/v23.0/123456789/messages')
.with(
body: {
messaging_product: 'whatsapp',
message_id: message.source_id,
typing_indicator: { type: 'text' }
}.to_json
)
.to_return(status: 200, body: { success: true }.to_json, headers: response_headers)
expect(service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, last_message: message)).to be(true)
end
it 'calls messages endpoint with typing indicator for "conversation.recording"' do
stub_request(:post, 'https://graph.facebook.com/v23.0/123456789/messages')
.with(
body: {
messaging_product: 'whatsapp',
message_id: message.source_id,
typing_indicator: { type: 'text' }
}.to_json
)
.to_return(status: 200, body: { success: true }.to_json, headers: response_headers)
expect(service.toggle_typing_status(Events::Types::CONVERSATION_RECORDING, last_message: message)).to be(true)
end
it 'does not call messages endpoint with typing indicator for "conversation.typing_off"' do
expect(service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_OFF, last_message: message)).to be(false)
end
it 'logs error on failure' do
allow(Rails.logger).to receive(:error).with('Request failed')
stub_request(:post, 'https://graph.facebook.com/v23.0/123456789/messages')
.with(
body: {
messaging_product: 'whatsapp',
message_id: message.source_id,
typing_indicator: { type: 'text' }
}.to_json
)
.to_return(status: 500, body: 'Request failed')
service.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, last_message: message)
expect(Rails.logger).to have_received(:error)
end
end
describe '#read_messages' do
it 'calls messages endpoint to mark last message as read' do
stub_request(:post, 'https://graph.facebook.com/v23.0/123456789/messages')
.with(
body: {
messaging_product: 'whatsapp',
message_id: message.source_id,
status: 'read'
}.to_json
)
.to_return(status: 200, body: { success: true }.to_json, headers: response_headers)
messages = [create(:message), message]
expect(service.read_messages(messages)).to be(true)
end
it 'logs error on failure' do
allow(Rails.logger).to receive(:error).with('Request failed')
stub_request(:post, 'https://graph.facebook.com/v23.0/123456789/messages')
.with(
body: {
messaging_product: 'whatsapp',
message_id: message.source_id,
status: 'read'
}.to_json
)
.to_return(status: 500, body: 'Request failed')
service.read_messages([message])
expect(Rails.logger).to have_received(:error)
end
end
end