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:) def toggle_typing_status(typing_status, conversation:)
return unless provider_service.respond_to?(:toggle_typing_status) 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 end
def update_presence(status) 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`. # 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 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 end
def unread_conversation(conversation) def unread_conversation(conversation)

View File

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

View File

@ -71,8 +71,8 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
end end
# TODO: See if we can unify the API versions and for both paths and make it consistent with out facebook app API versions # 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 def phone_id_path(version = 'v13.0')
"#{api_base_path}/v13.0/#{whatsapp_channel.provider_config['phone_number_id']}" "#{api_base_path}/#{version}/#{whatsapp_channel.provider_config['phone_number_id']}"
end end
def business_account_path def business_account_path
@ -181,4 +181,41 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
process_response(response, message) process_response(response, message)
end 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 end

View File

@ -186,7 +186,7 @@ RSpec.describe Channel::Whatsapp do
it 'calls provider service method' do it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil) provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil)
allow(provider_double).to receive(:toggle_typing_status) 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) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel) .with(whatsapp_channel: channel)
.and_return(provider_double) .and_return(provider_double)
@ -246,7 +246,7 @@ RSpec.describe Channel::Whatsapp do
it 'calls provider service method' do it 'calls provider service method' do
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil) 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) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel) .with(whatsapp_channel: channel)
.and_return(provider_double) .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 it 'call method when the provider config mark_as_read is nil' do
channel.update!(provider_config: {}) channel.update!(provider_config: {})
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil) 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) allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
.with(whatsapp_channel: channel) .with(whatsapp_channel: channel)
.and_return(provider_double) .and_return(provider_double)
@ -270,7 +270,7 @@ RSpec.describe Channel::Whatsapp do
end end
it 'does not call method if provider service does not implement it' do it 'does not call method if provider service does not implement it' do
channel.update!(provider: 'whatsapp_cloud') channel.update!(provider: 'default')
expect do expect do
channel.read_messages([message], conversation: conversation) 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 body: { keys: [{ id: message.source_id, remoteJid: test_send_jid, fromMe: false }] }.to_json
).to_return(status: 200, body: '', headers: {}) ).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) expect(result).to be(true)
end end
@ -495,7 +495,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
.to_return(status: 200) .to_return(status: 200)
expect do 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.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
end end
end end
@ -605,7 +605,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
) )
.to_return(status: 200) .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 expect(request).to have_been_requested
end end
@ -621,7 +621,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
) )
.to_return(status: 200) .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 expect(request).to have_been_requested
end end
@ -637,7 +637,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
) )
.to_return(status: 200) .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 expect(request).to have_been_requested
end end
@ -664,7 +664,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do
allow(Rails.logger).to receive(:error) allow(Rails.logger).to receive(:error)
expect do 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) end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(Rails.logger).to have_received(:error).with('error message') 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' }) whatsapp_channel.update!(provider_connection: { 'connection' => 'open' })
expect do 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) end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::ProviderUnavailableError)
expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close') expect(whatsapp_channel.reload.provider_connection['connection']).to eq('close')

View File

@ -312,4 +312,91 @@ describe Whatsapp::Providers::WhatsappCloudService do
end end
end 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 end