feat: support reply to for outgoing message in WhatsApp (#8107)
- This PR enables replies to WhatsApp.
This commit is contained in:
@@ -964,6 +964,19 @@ export default {
|
|||||||
(item, index) => itemIndex !== index
|
(item, index) => itemIndex !== index
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
setReplyToInPayload(payload) {
|
||||||
|
if (this.inReplyTo?.id) {
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
contentAttributes: {
|
||||||
|
...payload.contentAttributes,
|
||||||
|
in_reply_to: this.inReplyTo.id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
},
|
||||||
getMessagePayloadForWhatsapp(message) {
|
getMessagePayloadForWhatsapp(message) {
|
||||||
const multipleMessagePayload = [];
|
const multipleMessagePayload = [];
|
||||||
|
|
||||||
@@ -973,41 +986,41 @@ export default {
|
|||||||
const attachedFile = this.globalConfig.directUploadsEnabled
|
const attachedFile = this.globalConfig.directUploadsEnabled
|
||||||
? attachment.blobSignedId
|
? attachment.blobSignedId
|
||||||
: attachment.resource.file;
|
: attachment.resource.file;
|
||||||
const attachmentPayload = {
|
let attachmentPayload = {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
files: [attachedFile],
|
files: [attachedFile],
|
||||||
private: false,
|
private: false,
|
||||||
message: caption,
|
message: caption,
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
|
||||||
multipleMessagePayload.push(attachmentPayload);
|
multipleMessagePayload.push(attachmentPayload);
|
||||||
caption = '';
|
caption = '';
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const messagePayload = {
|
let messagePayload = {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
message,
|
message,
|
||||||
private: false,
|
private: false,
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
messagePayload = this.setReplyToInPayload(messagePayload);
|
||||||
|
|
||||||
multipleMessagePayload.push(messagePayload);
|
multipleMessagePayload.push(messagePayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
return multipleMessagePayload;
|
return multipleMessagePayload;
|
||||||
},
|
},
|
||||||
getMessagePayload(message) {
|
getMessagePayload(message) {
|
||||||
const messagePayload = {
|
let messagePayload = {
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
message,
|
message,
|
||||||
private: this.isPrivate,
|
private: this.isPrivate,
|
||||||
sender: this.sender,
|
sender: this.sender,
|
||||||
};
|
};
|
||||||
|
messagePayload = this.setReplyToInPayload(messagePayload);
|
||||||
if (this.inReplyTo?.id) {
|
|
||||||
messagePayload.contentAttributes = {
|
|
||||||
in_reply_to: this.inReplyTo.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.attachedFiles && this.attachedFiles.length) {
|
if (this.attachedFiles && this.attachedFiles.length) {
|
||||||
messagePayload.files = [];
|
messagePayload.files = [];
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="px-2 py-1.5 -mx-2 rounded-sm min-w-[15rem] mb-2"
|
class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50':
|
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50':
|
||||||
messageType === MESSAGE_TYPE.INCOMING,
|
messageType === MESSAGE_TYPE.INCOMING,
|
||||||
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
|
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
|
||||||
|
'-mx-2': message.content,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<message-preview
|
<message-preview
|
||||||
|
|||||||
@@ -239,13 +239,11 @@ class Message < ApplicationRecord
|
|||||||
in_reply_to = content_attributes[:in_reply_to]
|
in_reply_to = content_attributes[:in_reply_to]
|
||||||
in_reply_to_external_id = content_attributes[:in_reply_to_external_id]
|
in_reply_to_external_id = content_attributes[:in_reply_to_external_id]
|
||||||
|
|
||||||
if in_reply_to.present? && in_reply_to_external_id.blank?
|
Messages::InReplyToMessageBuilder.new(
|
||||||
message = conversation.messages.find_by(id: in_reply_to)
|
message: self,
|
||||||
content_attributes[:in_reply_to_external_id] = message.try(:source_id)
|
in_reply_to: in_reply_to,
|
||||||
elsif in_reply_to_external_id.present? && in_reply_to.blank?
|
in_reply_to_external_id: in_reply_to_external_id
|
||||||
message = conversation.messages.find_by(source_id: in_reply_to_external_id)
|
).perform
|
||||||
content_attributes[:in_reply_to] = message.try(:id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_content_type
|
def ensure_content_type
|
||||||
|
|||||||
24
app/services/messages/in_reply_to_message_builder.rb
Normal file
24
app/services/messages/in_reply_to_message_builder.rb
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
class Messages::InReplyToMessageBuilder
|
||||||
|
pattr_initialize [:message!, :in_reply_to!, :in_reply_to_external_id!]
|
||||||
|
|
||||||
|
delegate :conversation, to: :message
|
||||||
|
|
||||||
|
def perform
|
||||||
|
set_in_reply_to_attribute if @in_reply_to.present? || @in_reply_to_external_id.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_in_reply_to_attribute
|
||||||
|
@message.content_attributes[:in_reply_to_external_id] = in_reply_to_message.try(:source_id)
|
||||||
|
@message.content_attributes[:in_reply_to] = in_reply_to_message.try(:id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def in_reply_to_message
|
||||||
|
return conversation.messages.find_by(id: @in_reply_to) if @in_reply_to.present?
|
||||||
|
|
||||||
|
return conversation.messages.find_by(source_id: @in_reply_to_external_id) if @in_reply_to_external_id
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -144,8 +144,7 @@ class Whatsapp::IncomingMessageBaseService
|
|||||||
message_type: :incoming,
|
message_type: :incoming,
|
||||||
sender: @contact,
|
sender: @contact,
|
||||||
source_id: message[:id].to_s,
|
source_id: message[:id].to_s,
|
||||||
in_reply_to_external_id: @in_reply_to_external_id,
|
in_reply_to_external_id: @in_reply_to_external_id
|
||||||
in_reply_to: @in_reply_to
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -88,15 +88,7 @@ module Whatsapp::IncomingMessageServiceHelpers
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_in_reply_to(message)
|
def process_in_reply_to(message)
|
||||||
return if message['context'].blank?
|
@in_reply_to_external_id = message['context']&.[]('id')
|
||||||
|
|
||||||
@in_reply_to_external_id = message['context']['id']
|
|
||||||
|
|
||||||
return if @in_reply_to_external_id.blank?
|
|
||||||
|
|
||||||
in_reply_to_message = Message.find_by(source_id: @in_reply_to_external_id)
|
|
||||||
|
|
||||||
@in_reply_to = in_reply_to_message.id if in_reply_to_message.present?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_message_by_source_id(source_id)
|
def find_message_by_source_id(source_id)
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
|||||||
headers: api_headers,
|
headers: api_headers,
|
||||||
body: {
|
body: {
|
||||||
messaging_product: 'whatsapp',
|
messaging_product: 'whatsapp',
|
||||||
|
context: whatsapp_reply_context(message),
|
||||||
to: phone_number,
|
to: phone_number,
|
||||||
text: { body: message.content },
|
text: { body: message.content },
|
||||||
type: 'text'
|
type: 'text'
|
||||||
@@ -100,6 +101,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
|||||||
headers: api_headers,
|
headers: api_headers,
|
||||||
body: {
|
body: {
|
||||||
:messaging_product => 'whatsapp',
|
:messaging_product => 'whatsapp',
|
||||||
|
:context => whatsapp_reply_context(message),
|
||||||
'to' => phone_number,
|
'to' => phone_number,
|
||||||
'type' => type,
|
'type' => type,
|
||||||
type.to_s => type_content
|
type.to_s => type_content
|
||||||
@@ -132,6 +134,15 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def whatsapp_reply_context(message)
|
||||||
|
reply_to = message.content_attributes[:in_reply_to_external_id]
|
||||||
|
return nil if reply_to.blank?
|
||||||
|
|
||||||
|
{
|
||||||
|
message_id: reply_to
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def send_interactive_text_message(phone_number, message)
|
def send_interactive_text_message(phone_number, message)
|
||||||
payload = create_payload_based_on_items(message)
|
payload = create_payload_based_on_items(message)
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,18 @@ require 'rails_helper'
|
|||||||
describe Whatsapp::Providers::WhatsappCloudService do
|
describe Whatsapp::Providers::WhatsappCloudService do
|
||||||
subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }
|
subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }
|
||||||
|
|
||||||
|
let(:conversation) { create(:conversation, inbox: whatsapp_channel.inbox) }
|
||||||
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false) }
|
let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false) }
|
||||||
let(:message) { create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox) }
|
|
||||||
|
let(:message) do
|
||||||
|
create(:message, conversation: conversation, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox, source_id: 'external_id')
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:message_with_reply) do
|
||||||
|
create(:message, conversation: conversation, message_type: :outgoing, content: 'reply', inbox: whatsapp_channel.inbox,
|
||||||
|
content_attributes: { in_reply_to: message.id })
|
||||||
|
end
|
||||||
|
|
||||||
let(:response_headers) { { 'Content-Type' => 'application/json' } }
|
let(:response_headers) { { 'Content-Type' => 'application/json' } }
|
||||||
let(:whatsapp_response) { { messages: [{ id: 'message_id' }] } }
|
let(:whatsapp_response) { { messages: [{ id: 'message_id' }] } }
|
||||||
|
|
||||||
@@ -19,6 +29,7 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
|||||||
.with(
|
.with(
|
||||||
body: {
|
body: {
|
||||||
messaging_product: 'whatsapp',
|
messaging_product: 'whatsapp',
|
||||||
|
context: nil,
|
||||||
to: '+123456789',
|
to: '+123456789',
|
||||||
text: { body: message.content },
|
text: { body: message.content },
|
||||||
type: 'text'
|
type: 'text'
|
||||||
@@ -28,6 +39,23 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
|||||||
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
expect(service.send_message('+123456789', message)).to eq 'message_id'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'calls message endpoints for a reply to messages' do
|
||||||
|
stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
|
||||||
|
.with(
|
||||||
|
body: {
|
||||||
|
messaging_product: 'whatsapp',
|
||||||
|
context: {
|
||||||
|
message_id: message.source_id
|
||||||
|
},
|
||||||
|
to: '+123456789',
|
||||||
|
text: { body: message_with_reply.content },
|
||||||
|
type: 'text'
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||||
|
expect(service.send_message('+123456789', message_with_reply)).to eq 'message_id'
|
||||||
|
end
|
||||||
|
|
||||||
it 'calls message endpoints for image attachment message messages' do
|
it 'calls message endpoints for image attachment message messages' do
|
||||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
|
||||||
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
||||||
|
|||||||
Reference in New Issue
Block a user