feat(baileys): add reply context handling (#196)

This commit is contained in:
Gabriel Jablonski 2026-01-24 23:37:21 -03:00 committed by GitHub
parent 0de6001b97
commit b5d3250a2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 259 additions and 2 deletions

View File

@ -67,9 +67,9 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
if @message.content_attributes[:is_reaction]
@message_content = reaction_message_content
elsif @message.attachments.present?
@message_content = attachment_message_content
@message_content = attachment_message_content.merge(reply_context)
elsif @message.outgoing_content.present?
@message_content = { text: @message.outgoing_content }
@message_content = { text: @message.outgoing_content }.merge(reply_context)
else
@message.update!(is_unsupported: true)
return
@ -304,6 +304,45 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
}
end
def reply_context
reply_to_external_id = @message.content_attributes[:in_reply_to_external_id]
return {} if reply_to_external_id.blank?
reply_to_message = @message.conversation.messages.find_by(source_id: reply_to_external_id)
return {} unless reply_to_message
{
quotedMessage: {
key: {
id: reply_to_external_id,
remoteJid: remote_jid,
fromMe: reply_to_message.message_type == 'outgoing'
},
message: quoted_message_content(reply_to_message)
}
}
end
def quoted_message_content(message)
if message.attachments.present?
attachment = message.attachments.first
case attachment.file_type
when 'image'
{ imageMessage: { caption: message.content } }
when 'video'
{ videoMessage: { caption: message.content } }
when 'audio'
{ audioMessage: {} }
when 'file'
{ documentMessage: { caption: message.content, fileName: attachment.file.filename.to_s } }
else
{ conversation: message.content.to_s }
end
else
{ conversation: message.content.to_s }
end
end
def attachment_message_content # rubocop:disable Metrics/MethodLength
attachment = @message.attachments.first
buffer = attachment_to_base64(attachment)

View File

@ -437,6 +437,146 @@ describe Whatsapp::Providers::WhatsappBaileysService do
end
end
context 'when message is a reply to another message' do
let(:inbox) { whatsapp_channel.inbox }
let(:account_user) { create(:account_user, account: inbox.account) }
let(:contact) { create(:contact, account: inbox.account, name: 'John Doe', phone_number: "+#{test_send_phone_number}") }
let(:conversation) do
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: test_send_phone_number)
create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
end
it 'sends text reply to outgoing message with quotedMessage' do
original_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
message_type: 'outgoing', source_id: 'original_msg_123', content: 'Original text')
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
content: 'Reply text', content_attributes: { in_reply_to_external_id: original_message.source_id })
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: {
text: 'Reply text',
quotedMessage: {
key: {
id: 'original_msg_123',
remoteJid: test_send_jid,
fromMe: true
},
message: { conversation: 'Original text' }
}
}
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, reply_message)
expect(result).to eq('msg_123')
end
it 'sends text reply to incoming message with quotedMessage' do
original_message = create(:message, inbox: inbox, conversation: conversation, sender: contact,
message_type: 'incoming', source_id: 'incoming_msg_456', content: 'Incoming text')
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
content: 'Reply to incoming', content_attributes: { in_reply_to_external_id: original_message.source_id })
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: {
text: 'Reply to incoming',
quotedMessage: {
key: {
id: 'incoming_msg_456',
remoteJid: test_send_jid,
fromMe: false
},
message: { conversation: 'Incoming text' }
}
}
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, reply_message)
expect(result).to eq('msg_123')
end
it 'sends reply to message with image attachment' do
original_message = create(:message, inbox: inbox, conversation: conversation, sender: contact,
message_type: 'incoming', source_id: 'image_msg_789', content: 'Check this image')
original_message.attachments.create!(account_id: original_message.account_id, file_type: 'image',
file: { io: StringIO.new('fake'), filename: 'image.png' })
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
content: 'Nice image!', content_attributes: { in_reply_to_external_id: original_message.source_id })
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: {
text: 'Nice image!',
quotedMessage: {
key: {
id: 'image_msg_789',
remoteJid: test_send_jid,
fromMe: false
},
message: { imageMessage: { caption: 'Check this image' } }
}
}
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, reply_message)
expect(result).to eq('msg_123')
end
it 'sends message without quotedMessage when in_reply_to_external_id is blank' do
regular_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user, content: 'Regular message')
stub_request(:post, request_path)
.with(
headers: stub_headers(whatsapp_channel),
body: {
jid: test_send_jid,
messageContent: { text: 'Regular message' }
}.to_json
)
.to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: result_body.to_json
)
result = service.send_message(test_send_phone_number, regular_message)
expect(result).to eq('msg_123')
end
end
context 'when request is unsuccessful' do
it 'raises ProviderUnavailableError' do
stub_request(:post, request_path)

View File

@ -441,6 +441,84 @@ describe Whatsapp::Providers::WhatsappZapiService do
end
end
context 'when message is a reply to another message' do
let(:inbox) { whatsapp_channel.inbox }
let(:account_user) { create(:account_user, account: inbox.account) }
let(:contact) { create(:contact, account: inbox.account, name: 'John Doe', phone_number: "+#{test_send_phone_number}") }
let(:conversation) do
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: test_send_phone_number)
create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
end
it 'sends text reply with messageId parameter' do
original_message = create(:message, inbox: inbox, conversation: conversation, sender: contact,
message_type: 'incoming', source_id: 'original_zapi_msg_123', content: 'Original text')
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
content: 'Reply text', content_attributes: { in_reply_to_external_id: original_message.source_id })
stub_request(:post, request_path)
.with(
headers: stub_headers,
body: {
phone: test_send_phone_number,
message: 'Reply text',
messageId: 'original_zapi_msg_123'
}.to_json
)
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.send_message("+#{test_send_phone_number}", reply_message)
expect(result).to eq('msg_123')
end
it 'sends image reply with messageId parameter' do
base64_image = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
buffer = "data:image/png;base64,#{base64_image}"
original_message = create(:message, inbox: inbox, conversation: conversation, sender: contact,
message_type: 'incoming', source_id: 'original_zapi_img_456', content: 'Check this')
reply_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user,
content: 'Nice!', content_attributes: { in_reply_to_external_id: original_message.source_id })
reply_message.attachments.create!(account_id: reply_message.account_id, file_type: 'image',
file: { io: StringIO.new(Base64.decode64(base64_image)), filename: 'reply.png' })
stub_request(:post, "#{api_instance_path_with_token}/send-image")
.with(
headers: stub_headers,
body: {
phone: test_send_phone_number,
image: buffer,
caption: 'Nice!',
messageId: 'original_zapi_img_456'
}.to_json
)
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.send_message("+#{test_send_phone_number}", reply_message)
expect(result).to eq('msg_123')
end
it 'sends message without messageId when in_reply_to_external_id is blank' do
regular_message = create(:message, inbox: inbox, conversation: conversation, sender: account_user, content: 'Regular message')
stub_request(:post, request_path)
.with(
headers: stub_headers,
body: {
phone: test_send_phone_number,
message: 'Regular message'
}.to_json
)
.to_return(status: 200, body: result_body.to_json, headers: { 'Content-Type' => 'application/json' })
result = service.send_message("+#{test_send_phone_number}", regular_message)
expect(result).to eq('msg_123')
end
end
context 'when message is an image file' do
let(:base64_image) { 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=' }
let(:buffer) { "data:image/png;base64,#{base64_image}" }