feat: baileys messages reactions (#33)
* fix: update send_message method to handle baileys-api send-message changes * chore: simplify update of is_unsupported * feat: add support for message reactions in WhatsApp Baileys service * chore: update context description for unsupported message in send_message spec * feat: enhance incoming messages Baileys service to support message reactions and external source ID handling * fix: correct typo in comment for is_reaction attribute in Message model * chore: simplify message handling by removing external source ID checks and unused methods * fix: update comment to reflect necessary change for source_id in set_contact method * fix: refactor message handling for reactions and simplify jid generation * fix: add comment to clarify behavior of find_message_by_source_id method * fix: update unsupported message handling * chore: specs for reaction messages * fix: simplify message update handling by removing redundant validation method * fix: update reaction message handling to use in_reply_to_external_id and fix spec * fix: enhance message handling for unsupported types and update specs * fix: update reaction message handling to set is_reaction flag and enhance specs * fix: update specs for send message in reaction * revert: remove mb0 from input * fix: streamline jid generation in send_message_request * refactor: specs * refactor: move content attributes to helper * test: spec for empty reaction * refactor: streamline references to replied message * test: refactor spec * test: refactor spec --------- Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
This commit is contained in:
parent
8dc893d322
commit
8e913236e5
@ -63,7 +63,6 @@ export default {
|
|||||||
<label class="input-container">
|
<label class="input-container">
|
||||||
<span v-if="label">{{ label }}</span>
|
<span v-if="label">{{ label }}</span>
|
||||||
<input
|
<input
|
||||||
class="!mb-0"
|
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
:type="type"
|
:type="type"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
|
|||||||
@ -101,9 +101,10 @@ class Message < ApplicationRecord
|
|||||||
# [:deleted] : Used to denote whether the message was deleted by the agent
|
# [:deleted] : Used to denote whether the message was deleted by the agent
|
||||||
# [:external_created_at] : Can specify if the message was created at a different timestamp externally
|
# [:external_created_at] : Can specify if the message was created at a different timestamp externally
|
||||||
# [:external_error : Can specify if the message creation failed due to an error at external API
|
# [:external_error : Can specify if the message creation failed due to an error at external API
|
||||||
|
# [:is_reaction] : Used to denote if the message is a reaction and differentiate it from a simple reply message
|
||||||
store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted,
|
store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted,
|
||||||
:external_created_at, :story_sender, :story_id, :external_error,
|
:external_created_at, :story_sender, :story_id, :external_error,
|
||||||
:translations, :in_reply_to_external_id, :is_unsupported], coder: JSON
|
:translations, :in_reply_to_external_id, :is_unsupported, :is_reaction], coder: JSON
|
||||||
|
|
||||||
store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id
|
store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id
|
||||||
|
|
||||||
|
|||||||
@ -67,6 +67,7 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
|
|||||||
def set_contact
|
def set_contact
|
||||||
push_name = contact_name
|
push_name = contact_name
|
||||||
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
|
# FIXME: update the source_id to complete jid in future
|
||||||
source_id: phone_number_from_jid,
|
source_id: phone_number_from_jid,
|
||||||
inbox: inbox,
|
inbox: inbox,
|
||||||
contact_attributes: { name: push_name, phone_number: "+#{phone_number_from_jid}" }
|
contact_attributes: { name: push_name, phone_number: "+#{phone_number_from_jid}" }
|
||||||
@ -100,6 +101,8 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
|
|||||||
case message_type
|
case message_type
|
||||||
when 'text'
|
when 'text'
|
||||||
create_message
|
create_message
|
||||||
|
when 'reaction'
|
||||||
|
create_message if message_content.present?
|
||||||
when 'image', 'file', 'video', 'audio', 'sticker'
|
when 'image', 'file', 'video', 'audio', 'sticker'
|
||||||
create_message
|
create_message
|
||||||
attach_media
|
attach_media
|
||||||
@ -148,6 +151,7 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
|
|||||||
return 'poll' if msg.key?(:pollCreationMessageV3)
|
return 'poll' if msg.key?(:pollCreationMessageV3)
|
||||||
return 'event' if msg.key?(:eventMessage)
|
return 'event' if msg.key?(:eventMessage)
|
||||||
return 'sticker' if msg.key?(:stickerMessage)
|
return 'sticker' if msg.key?(:stickerMessage)
|
||||||
|
return 'reaction' if msg.key?(:reactionMessage)
|
||||||
|
|
||||||
'unsupported'
|
'unsupported'
|
||||||
end
|
end
|
||||||
@ -165,10 +169,19 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
|
|||||||
sender: sender,
|
sender: sender,
|
||||||
sender_type: sender_type,
|
sender_type: sender_type,
|
||||||
message_type: message_type,
|
message_type: message_type,
|
||||||
in_reply_to_external_id: nil
|
content_attributes: message_content_attributes
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def message_content_attributes
|
||||||
|
return unless message_type == 'reaction'
|
||||||
|
|
||||||
|
{
|
||||||
|
in_reply_to_external_id: @raw_message.dig(:message, :reactionMessage, :key, :id),
|
||||||
|
is_reaction: true
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def incoming?
|
def incoming?
|
||||||
!@raw_message[:key][:fromMe]
|
!@raw_message[:key][:fromMe]
|
||||||
end
|
end
|
||||||
@ -231,6 +244,8 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
|
|||||||
@raw_message.dig(:message, :imageMessage, :caption)
|
@raw_message.dig(:message, :imageMessage, :caption)
|
||||||
when 'video'
|
when 'video'
|
||||||
@raw_message.dig(:message, :videoMessage, :caption)
|
@raw_message.dig(:message, :videoMessage, :caption)
|
||||||
|
when 'reaction'
|
||||||
|
@raw_message.dig(:message, :reactionMessage, :text)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -263,17 +278,12 @@ class Whatsapp::IncomingMessageBaileysService < Whatsapp::IncomingMessageBaseSer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_update
|
def handle_update
|
||||||
raise MessageNotFoundError unless valid_update_message?
|
raise MessageNotFoundError unless find_message_by_source_id(message_id)
|
||||||
|
|
||||||
update_status if @raw_message.dig(:update, :status).present?
|
update_status if @raw_message.dig(:update, :status).present?
|
||||||
update_message_content if @raw_message.dig(:update, :message).present?
|
update_message_content if @raw_message.dig(:update, :message).present?
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_update_message?
|
|
||||||
@message = find_message_by_source_id(message_id)
|
|
||||||
@message.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_status
|
def update_status
|
||||||
status = status_mapper
|
status = status_mapper
|
||||||
@message.update!(status: status) if status.present? && status_transition_allowed?(status)
|
@message.update!(status: status) if status.present? && status_transition_allowed?(status)
|
||||||
|
|||||||
@ -33,7 +33,9 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
|||||||
@message = message
|
@message = message
|
||||||
@phone_number = phone_number
|
@phone_number = phone_number
|
||||||
|
|
||||||
if message.attachments.present?
|
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
|
||||||
elsif message.content.present?
|
elsif message.content.present?
|
||||||
@message_content = { text: @message.content }
|
@message_content = { text: @message.content }
|
||||||
@ -74,6 +76,15 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
|||||||
whatsapp_channel.provider_config['api_key'].presence || DEFAULT_API_KEY
|
whatsapp_channel.provider_config['api_key'].presence || DEFAULT_API_KEY
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reaction_message_content
|
||||||
|
{
|
||||||
|
react: { key: { id: @message.in_reply_to_external_id,
|
||||||
|
remoteJid: remote_jid,
|
||||||
|
fromMe: true },
|
||||||
|
text: @message.content }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def attachment_message_content
|
def attachment_message_content
|
||||||
attachment = @message.attachments.first
|
attachment = @message.attachments.first
|
||||||
buffer = Base64.strict_encode64(attachment.file.download)
|
buffer = Base64.strict_encode64(attachment.file.download)
|
||||||
@ -103,7 +114,7 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
|||||||
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-message",
|
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/send-message",
|
||||||
headers: api_headers,
|
headers: api_headers,
|
||||||
body: {
|
body: {
|
||||||
jid: "#{@phone_number.delete('+')}@s.whatsapp.net",
|
jid: remote_jid,
|
||||||
messageContent: @message_content
|
messageContent: @message_content
|
||||||
}.to_json
|
}.to_json
|
||||||
)
|
)
|
||||||
@ -118,6 +129,10 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
|||||||
response.success?
|
response.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remote_jid
|
||||||
|
"#{@phone_number.delete('+')}@s.whatsapp.net"
|
||||||
|
end
|
||||||
|
|
||||||
private_class_method def self.with_error_handling(*method_names)
|
private_class_method def self.with_error_handling(*method_names)
|
||||||
method_names.each do |method_name|
|
method_names.each do |method_name|
|
||||||
original_method = instance_method(method_name)
|
original_method = instance_method(method_name)
|
||||||
|
|||||||
@ -164,7 +164,7 @@ describe Whatsapp::IncomingMessageBaileysService do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates an alert of unsupported message' do
|
it 'creates an unsupported message' do
|
||||||
described_class.new(inbox: inbox, params: params).perform
|
described_class.new(inbox: inbox, params: params).perform
|
||||||
|
|
||||||
conversation = inbox.conversations.last
|
conversation = inbox.conversations.last
|
||||||
@ -373,6 +373,51 @@ describe Whatsapp::IncomingMessageBaileysService do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when message type is reaction' do
|
||||||
|
let(:phone_number) { '5511912345678' }
|
||||||
|
let(:jid) { "#{phone_number}@s.whatsapp.net" }
|
||||||
|
let(:raw_message) do
|
||||||
|
{
|
||||||
|
key: { id: 'reaction_123', remoteJid: jid, fromMe: false },
|
||||||
|
message: { reactionMessage: { key: { remoteJid: jid, fromMe: true, id: 'msg_123' }, text: '👍' } },
|
||||||
|
pushName: 'John Doe'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
webhookVerifyToken: webhook_verify_token,
|
||||||
|
event: 'messages.upsert',
|
||||||
|
data: {
|
||||||
|
type: 'notify',
|
||||||
|
messages: [raw_message]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let!(:message) do
|
||||||
|
contact = create(:contact, account: inbox.account, name: phone_number)
|
||||||
|
contact_inbox = create(:contact_inbox, inbox: inbox, contact: contact, source_id: phone_number)
|
||||||
|
conversation = create(:conversation, inbox: inbox, contact_inbox: contact_inbox)
|
||||||
|
create(:message, inbox: inbox, conversation: conversation, source_id: 'msg_123')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates the reaction' do
|
||||||
|
described_class.new(inbox: inbox, params: params).perform
|
||||||
|
|
||||||
|
reaction = message.conversation.messages.last
|
||||||
|
expect(reaction.is_reaction).to be(true)
|
||||||
|
expect(reaction.in_reply_to).to eq(message.id)
|
||||||
|
expect(reaction.in_reply_to_external_id).to eq(message.source_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not create the reaction if content is empty' do
|
||||||
|
raw_message[:message][:reactionMessage][:text] = ''
|
||||||
|
|
||||||
|
described_class.new(inbox: inbox, params: params).perform
|
||||||
|
|
||||||
|
expect(message.conversation.messages.count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when message type is image' do
|
context 'when message type is image' do
|
||||||
let(:raw_message) do
|
let(:raw_message) do
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,8 +6,8 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
|||||||
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false) }
|
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false) }
|
||||||
let(:message) { create(:message) }
|
let(:message) { create(:message) }
|
||||||
|
|
||||||
let(:test_send_phone_number) { '+5511987654321' }
|
let(:test_send_phone_number) { '551187654321' }
|
||||||
let(:test_send_jid) { '5511987654321@s.whatsapp.net' }
|
let(:test_send_jid) { '551187654321@s.whatsapp.net' }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_CLIENT_NAME', 'chatwoot-test')
|
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_CLIENT_NAME', 'chatwoot-test')
|
||||||
@ -92,6 +92,39 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
|||||||
end
|
end
|
||||||
|
|
||||||
describe '#send_message' do
|
describe '#send_message' do
|
||||||
|
context 'when message is a reaction' do
|
||||||
|
let(:inbox) { whatsapp_channel.inbox }
|
||||||
|
let(:contact) { create(:contact, account: inbox.account, name: 'John Doe', phone_number: "+#{test_send_phone_number}") }
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, inbox: inbox, contact: contact, source_id: test_send_phone_number) }
|
||||||
|
let(:conversation) { create(:conversation, inbox: inbox, contact_inbox: contact_inbox) }
|
||||||
|
let!(:message) { create(:message, inbox: inbox, conversation: conversation, sender: contact, source_id: 'msg_123') }
|
||||||
|
let(:reaction) do
|
||||||
|
create(:message, inbox: inbox, conversation: conversation, sender: contact, content: '👍',
|
||||||
|
content_attributes: { is_reaction: true, in_reply_to: message.id })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends the reaction message' do
|
||||||
|
stub_request(:post, "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/send-message")
|
||||||
|
.with(
|
||||||
|
headers: stub_headers(whatsapp_channel),
|
||||||
|
body: {
|
||||||
|
jid: test_send_jid,
|
||||||
|
messageContent: { react: { key: { id: message.source_id,
|
||||||
|
remoteJid: test_send_jid,
|
||||||
|
fromMe: true },
|
||||||
|
text: '👍' } }
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
.to_return(
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type' => 'application/json' },
|
||||||
|
body: { 'data' => { 'key' => { 'id' => 'reaction_123' } } }.to_json
|
||||||
|
)
|
||||||
|
|
||||||
|
result = service.send_message(test_send_phone_number, reaction)
|
||||||
|
|
||||||
|
expect(result).to eq('reaction_123')
|
||||||
|
|
||||||
context 'when message does not have content nor attachments' do
|
context 'when message does not have content nor attachments' do
|
||||||
it 'updates the message with content attribute is_unsupported' do
|
it 'updates the message with content attribute is_unsupported' do
|
||||||
message.update!(content: nil)
|
message.update!(content: nil)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user