From b764f3997d882d3a8032a1c1bd748942b127aed0 Mon Sep 17 00:00:00 2001 From: Gabriel Jablonski Date: Fri, 2 May 2025 19:51:43 -0300 Subject: [PATCH] feat: mark recorded audio messages (#36) * feat: mark recorded audio * refactor: cleaner logic for message builder metadata * test: message builder specs * test: baileys service specs * fix: correct method call for checking attachment filename in process_metadata --- app/builders/messages/message_builder.rb | 7 +- app/javascript/dashboard/api/inbox/message.js | 4 + .../widgets/conversation/ReplyBox.vue | 6 + .../providers/whatsapp_baileys_service.rb | 3 +- .../builders/messages/message_builder_spec.rb | 8 ++ .../whatsapp_baileys_service_spec.rb | 105 +++++++++++++----- 6 files changed, 101 insertions(+), 32 deletions(-) diff --git a/app/builders/messages/message_builder.rb b/app/builders/messages/message_builder.rb index 389cbf089..7a8578fdd 100644 --- a/app/builders/messages/message_builder.rb +++ b/app/builders/messages/message_builder.rb @@ -9,6 +9,7 @@ class Messages::MessageBuilder @user = user @message_type = params[:message_type] || 'outgoing' @attachments = params[:attachments] + @is_recorded_audio = params[:is_recorded_audio] @automation_rule = content_attributes&.dig(:automation_rule_id) return unless params.instance_of?(ActionController::Parameters) @@ -67,7 +68,7 @@ class Messages::MessageBuilder account_id: @message.account_id, file: uploaded_attachment ) - + attachment.meta = process_metadata(uploaded_attachment) attachment.file_type = if uploaded_attachment.is_a?(String) file_type_by_signed_id( uploaded_attachment @@ -78,6 +79,10 @@ class Messages::MessageBuilder end end + def process_metadata(attachment) + { is_recorded_audio: true } if @is_recorded_audio && attachment.original_filename.in?(@is_recorded_audio) + end + def process_emails return unless @conversation.inbox&.inbox_type == 'Email' diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js index 8f294a0ee..80534524e 100644 --- a/app/javascript/dashboard/api/inbox/message.js +++ b/app/javascript/dashboard/api/inbox/message.js @@ -8,6 +8,7 @@ export const buildCreatePayload = ({ contentAttributes, echoId, files, + isRecordedAudio, ccEmails = '', bccEmails = '', toEmails = '', @@ -22,6 +23,7 @@ export const buildCreatePayload = ({ files.forEach(file => { payload.append('attachments[]', file); }); + payload.append('is_recorded_audio', JSON.stringify(isRecordedAudio)); payload.append('private', isPrivate); payload.append('echo_id', echoId); payload.append('cc_emails', ccEmails); @@ -60,6 +62,7 @@ class MessageApi extends ApiClient { contentAttributes, echo_id: echoId, files, + isRecordedAudio, ccEmails = '', bccEmails = '', toEmails = '', @@ -74,6 +77,7 @@ class MessageApi extends ApiClient { contentAttributes, echoId, files, + isRecordedAudio, ccEmails, bccEmails, toEmails, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 4fa83e029..5323b3e2a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -1012,11 +1012,17 @@ export default { if (this.attachedFiles && this.attachedFiles.length) { messagePayload.files = []; + messagePayload.isRecordedAudio = []; this.attachedFiles.forEach(attachment => { if (this.globalConfig.directUploadsEnabled) { messagePayload.files.push(attachment.blobSignedId); } else { messagePayload.files.push(attachment.resource.file); + if (attachment.isRecordedAudio) { + messagePayload.isRecordedAudio.push( + attachment.resource.file.name + ); + } } }); } diff --git a/app/services/whatsapp/providers/whatsapp_baileys_service.rb b/app/services/whatsapp/providers/whatsapp_baileys_service.rb index e4f1ce2cf..5984dd8b6 100644 --- a/app/services/whatsapp/providers/whatsapp_baileys_service.rb +++ b/app/services/whatsapp/providers/whatsapp_baileys_service.rb @@ -86,7 +86,7 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer } end - def attachment_message_content + def attachment_message_content # rubocop:disable Metrics/MethodLength attachment = @message.attachments.first buffer = Base64.strict_encode64(attachment.file.download) @@ -99,6 +99,7 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer content[:image] = buffer when 'audio' content[:audio] = buffer + content[:ptt] = attachment.meta['is_recorded_audio'] when 'file' content[:document] = buffer when 'sticker' diff --git a/spec/builders/messages/message_builder_spec.rb b/spec/builders/messages/message_builder_spec.rb index 891f8eb02..aab3e2a09 100644 --- a/spec/builders/messages/message_builder_spec.rb +++ b/spec/builders/messages/message_builder_spec.rb @@ -134,6 +134,14 @@ describe Messages::MessageBuilder do expect(message.attachments.first.file_type).to eq 'image' end + it 'creates attachment with is_recorded_audio metadata' do + params[:is_recorded_audio] = ['avatar.png'] + + message = message_builder + + expect(message.attachments.first.meta).to eq({ 'is_recorded_audio' => true }) + end + context 'when DIRECT_UPLOAD_ENABLED' do let(:params) do ActionController::Parameters.new({ diff --git a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb index 04341f497..365998b61 100644 --- a/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb +++ b/spec/services/whatsapp/providers/whatsapp_baileys_service_spec.rb @@ -92,6 +92,8 @@ describe Whatsapp::Providers::WhatsappBaileysService do end describe '#send_message' do + let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message" } + context 'when message does not have content nor attachments' do it 'updates the message with content attribute is_unsupported' do message.update!(content: nil) @@ -107,7 +109,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do let(:base64_image) { 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' } before do - message.attachments.new( + message.attachments.create!( account_id: message.account_id, file_type: 'image', file: { @@ -115,11 +117,10 @@ describe Whatsapp::Providers::WhatsappBaileysService do filename: 'image.png' } ) - message.save! end it 'sends the attachment message' do - stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message") + stub_request(:post, request_path) .with( headers: stub_headers(whatsapp_channel), body: { @@ -140,7 +141,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do it 'omits caption if message content is empty' do message.update!(content: nil) - stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message") + stub_request(:post, request_path) .with( headers: stub_headers(whatsapp_channel), body: { @@ -158,26 +159,31 @@ describe Whatsapp::Providers::WhatsappBaileysService do expect(result).to eq('msg_123') end - - context 'when request is unsuccessful' do - it 'raises MessageNotSentError' do - stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message") - .to_return( - status: 400, - headers: { 'Content-Type' => 'application/json' }, - body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json - ) - - expect do - service.send_message(test_send_phone_number, message) - end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::MessageNotSentError) - end - end end - context 'when message is a text' do - it 'sends the message' do - stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message") + context 'when message is an audio file' do + let(:base64_audio) { 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=' } + + before do + message.attachments.create!( + account_id: message.account_id, + file_type: 'audio', + file: { + io: StringIO.new(Base64.decode64(base64_audio)), + filename: 'audio.wav' + } + ) + end + + it 'sends the audio message' do + stub_request(:post, request_path) + .with( + headers: stub_headers(whatsapp_channel), + body: { + jid: test_send_jid, + messageContent: { fileName: 'audio.wav', caption: message.content, audio: base64_audio } + }.to_json + ) .to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, @@ -189,17 +195,41 @@ describe Whatsapp::Providers::WhatsappBaileysService do expect(result).to eq('msg_123') end - it 'raises MessageNotSentError' do - stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message") + it 'sends message with ptt true if message is recorded audio' do + message.attachments.first.update!(meta: { is_recorded_audio: true }) + + stub_request(:post, request_path) + .with( + headers: stub_headers(whatsapp_channel), + body: { + jid: test_send_jid, + messageContent: { fileName: 'audio.wav', caption: message.content, audio: base64_audio, ptt: true } + }.to_json + ) .to_return( - status: 400, + status: 200, headers: { 'Content-Type' => 'application/json' }, body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json ) - expect do - service.send_message(test_send_phone_number, message) - end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::MessageNotSentError) + result = service.send_message(test_send_phone_number, message) + + expect(result).to eq('msg_123') + end + end + + context 'when message is a text' do + it 'sends the message' do + stub_request(:post, request_path) + .to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json + ) + + result = service.send_message(test_send_phone_number, message) + + expect(result).to eq('msg_123') end end @@ -216,7 +246,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do message = create(:message, inbox: inbox, conversation: conversation, sender: account_user, message_type: 'outgoing', source_id: 'msg_123') reaction = create(:message, inbox: inbox, conversation: conversation, sender: account_user, content: '👍', content_attributes: { is_reaction: true, in_reply_to: message.id }) - stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message") + stub_request(:post, request_path) .with( headers: stub_headers(whatsapp_channel), body: { @@ -242,7 +272,7 @@ describe Whatsapp::Providers::WhatsappBaileysService do message = create(:message, inbox: inbox, conversation: conversation, sender: contact, source_id: 'msg_123') reaction = create(:message, inbox: inbox, conversation: conversation, sender: account_user, content: '👍', content_attributes: { is_reaction: true, in_reply_to: message.id }) - stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message") + stub_request(:post, request_path) .with( headers: stub_headers(whatsapp_channel), body: { @@ -264,6 +294,21 @@ describe Whatsapp::Providers::WhatsappBaileysService do expect(result).to eq('reaction_123') end end + + context 'when request is unsuccessful' do + it 'raises MessageNotSentError' do + stub_request(:post, request_path) + .to_return( + status: 400, + headers: { 'Content-Type' => 'application/json' }, + body: { 'data' => { 'key' => { 'id' => 'msg_123' } } }.to_json + ) + + expect do + service.send_message(test_send_phone_number, message) + end.to raise_error(Whatsapp::Providers::WhatsappBaileysService::MessageNotSentError) + end + end end describe '#api_headers' do