parent
0899f62912
commit
d5c30760a7
7 changed files with 215 additions and 17 deletions
|
@ -4,7 +4,7 @@ class ContactInboxBuilder
|
|||
def perform
|
||||
@contact = Contact.find(contact_id)
|
||||
@inbox = @contact.account.inboxes.find(inbox_id)
|
||||
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api'].include? @inbox.channel_type
|
||||
return unless ['Channel::TwilioSms', 'Channel::Email', 'Channel::Api', 'Channel::Whatsapp'].include? @inbox.channel_type
|
||||
|
||||
source_id = @source_id || generate_source_id
|
||||
create_contact_inbox(source_id) if source_id.present?
|
||||
|
@ -14,12 +14,20 @@ class ContactInboxBuilder
|
|||
|
||||
def generate_source_id
|
||||
return twilio_source_id if @inbox.channel_type == 'Channel::TwilioSms'
|
||||
return wa_source_id if @inbox.channel_type == 'Channel::Whatsapp'
|
||||
return @contact.email if @inbox.channel_type == 'Channel::Email'
|
||||
return SecureRandom.uuid if @inbox.channel_type == 'Channel::Api'
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def wa_source_id
|
||||
return unless @contact.phone_number
|
||||
|
||||
# whatsapp doesn't want the + in e164 format
|
||||
"#{@contact.phone_number}.delete('+')"
|
||||
end
|
||||
|
||||
def twilio_source_id
|
||||
return unless @contact.phone_number
|
||||
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
#
|
||||
# Table name: channel_whatsapp
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# phone_number :string not null
|
||||
# provider :string default("default")
|
||||
# provider_config :jsonb
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
# id :bigint not null, primary key
|
||||
# message_templates :jsonb
|
||||
# message_templates_last_updated :datetime
|
||||
# phone_number :string not null
|
||||
# provider :string default("default")
|
||||
# provider_config :jsonb
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
@ -43,6 +45,10 @@ class Channel::Whatsapp < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def send_template(phone_number, template_info)
|
||||
send_template_message(phone_number, template_info)
|
||||
end
|
||||
|
||||
def media_url(media_id)
|
||||
"#{api_base_path}/media/#{media_id}"
|
||||
end
|
||||
|
@ -55,27 +61,34 @@ class Channel::Whatsapp < ApplicationRecord
|
|||
true
|
||||
end
|
||||
|
||||
def message_templates
|
||||
sync_templates
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def send_text_message(phone_number, message)
|
||||
HTTParty.post(
|
||||
response = HTTParty.post(
|
||||
"#{api_base_path}/messages",
|
||||
headers: { 'D360-API-KEY': provider_config['api_key'], 'Content-Type': 'application/json' },
|
||||
headers: api_headers,
|
||||
body: {
|
||||
to: phone_number,
|
||||
text: { body: message.content },
|
||||
type: 'text'
|
||||
}.to_json
|
||||
)
|
||||
|
||||
response.success? ? response['messages'].first['id'] : nil
|
||||
end
|
||||
|
||||
def send_attachment_message(phone_number, message)
|
||||
attachment = message.attachments.first
|
||||
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
||||
attachment_url = attachment.file_url
|
||||
HTTParty.post(
|
||||
response = HTTParty.post(
|
||||
"#{api_base_path}/messages",
|
||||
headers: { 'D360-API-KEY': provider_config['api_key'], 'Content-Type': 'application/json' },
|
||||
headers: api_headers,
|
||||
body: {
|
||||
'to' => phone_number,
|
||||
'type' => type,
|
||||
|
@ -85,6 +98,46 @@ class Channel::Whatsapp < ApplicationRecord
|
|||
}
|
||||
}.to_json
|
||||
)
|
||||
|
||||
response.success? ? response['messages'].first['id'] : nil
|
||||
end
|
||||
|
||||
def send_template_message(phone_number, template_info)
|
||||
response = HTTParty.post(
|
||||
"#{api_base_path}/messages",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
to: phone_number,
|
||||
template: template_body_parameters(template_info),
|
||||
type: 'template'
|
||||
}.to_json
|
||||
)
|
||||
|
||||
response.success? ? response['messages'].first['id'] : nil
|
||||
end
|
||||
|
||||
def template_body_parameters(template_info)
|
||||
{
|
||||
name: template_info[:name],
|
||||
namespace: template_info[:namespace],
|
||||
language: {
|
||||
policy: 'deterministic',
|
||||
code: template_info[:lang_code]
|
||||
},
|
||||
components: [{
|
||||
type: 'body',
|
||||
parameters: template_info[:parameters]
|
||||
}]
|
||||
}
|
||||
end
|
||||
|
||||
def sync_templates
|
||||
# to prevent too many api calls
|
||||
last_updated = message_templates_last_updated || 1.day.ago
|
||||
return if Time.current < (last_updated + 12.hours)
|
||||
|
||||
response = HTTParty.get("#{api_base_path}/configs/templates", headers: api_headers)
|
||||
update(message_templates: response['waba_templates'], message_templates_last_updated: Time.now.utc) if response.success?
|
||||
end
|
||||
|
||||
# Extract later into provider Service
|
||||
|
|
|
@ -6,6 +6,71 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
|
|||
end
|
||||
|
||||
def perform_reply
|
||||
channel.send_message(message.conversation.contact_inbox.source_id, message)
|
||||
# can reply checks if 24 hour limit has passed.
|
||||
if message.conversation.can_reply?
|
||||
send_on_whatsapp
|
||||
else
|
||||
send_template_message
|
||||
end
|
||||
end
|
||||
|
||||
def send_template_message
|
||||
name, namespace, lang_code, processed_parameters = processable_channel_message_template
|
||||
return if name.blank?
|
||||
|
||||
message_id = channel.send_template(message.conversation.contact_inbox.source_id, {
|
||||
name: name,
|
||||
namespace: namespace,
|
||||
lang_code: lang_code,
|
||||
parameters: processed_parameters
|
||||
})
|
||||
message.update!(source_id: message_id) if message_id.present?
|
||||
end
|
||||
|
||||
def processable_channel_message_template
|
||||
# see if we can match the message content to a template
|
||||
# An example template may look like "Your package has been shipped. It will be delivered in {{1}} business days.
|
||||
# We want to iterate over these templates with our message body and see if we can fit it to any of the templates
|
||||
# Then we use regex to parse the template varibles and convert them into the proper payload
|
||||
channel.message_templates.each do |template|
|
||||
match_obj = template_match_object(template)
|
||||
next if match_obj.blank?
|
||||
|
||||
# we have a match, now we need to parse the template variables and convert them into the wa recommended format
|
||||
processed_parameters = match_obj.captures.map { |x| { type: 'text', text: x } }
|
||||
|
||||
# no need to look up further end the search
|
||||
return [template['name'], template['namespace'], template['language'], processed_parameters]
|
||||
end
|
||||
[nil, nil, nil, nil]
|
||||
end
|
||||
|
||||
def template_match_object(template)
|
||||
body_object = validated_body_object(template)
|
||||
return if body_object.blank?
|
||||
|
||||
template_match_regex = build_template_match_regex(body_object['text'])
|
||||
message.content.match(template_match_regex)
|
||||
end
|
||||
|
||||
def build_template_match_regex(template_text)
|
||||
# Converts the whatsapp template to a comparable regex string to check against the message content
|
||||
# the variables are of the format {{num}} ex:{{1}}
|
||||
template_match_string = "^#{template_text.gsub(/{{\d}}/, '(.*)')}$"
|
||||
Regexp.new template_match_string
|
||||
end
|
||||
|
||||
def validated_body_object(template)
|
||||
# we don't care if its not approved template
|
||||
return if template['status'] != 'approved'
|
||||
|
||||
# we only care about text body object in template. if not present we discard the template
|
||||
# we don't support other forms of templates
|
||||
template['components'].find { |obj| obj['type'] == 'BODY' && obj.key?('text') }
|
||||
end
|
||||
|
||||
def send_on_whatsapp
|
||||
message_id = channel.send_message(message.conversation.contact_inbox.source_id, message)
|
||||
message.update!(source_id: message_id) if message_id.present?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
class AddTemplatesToWhatsappChannel < ActiveRecord::Migration[6.1]
|
||||
def up
|
||||
change_table :channel_whatsapp, bulk: true do |t|
|
||||
t.column :message_templates, :jsonb, default: {}
|
||||
t.column :message_templates_last_updated, :datetime
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
change_table :channel_whatsapp, bulk: true do |t|
|
||||
t.remove :message_templates
|
||||
t.remove :message_templates_last_updated
|
||||
end
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2021_11_22_112607) do
|
||||
ActiveRecord::Schema.define(version: 2021_11_29_120040) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
|
@ -270,6 +270,8 @@ ActiveRecord::Schema.define(version: 2021_11_22_112607) do
|
|||
t.jsonb "provider_config", default: {}
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.jsonb "message_templates", default: {}
|
||||
t.datetime "message_templates_last_updated"
|
||||
t.index ["phone_number"], name: "index_channel_whatsapp_on_phone_number", unique: true
|
||||
end
|
||||
|
||||
|
|
|
@ -3,6 +3,27 @@ FactoryBot.define do
|
|||
sequence(:phone_number) { |n| "+123456789#{n}1" }
|
||||
account
|
||||
provider_config { { 'api_key' => 'test_key' } }
|
||||
message_templates do
|
||||
[{ 'name' => 'sample_shipping_confirmation',
|
||||
'status' => 'approved',
|
||||
'category' => 'SHIPPING_UPDATE',
|
||||
'language' => 'id',
|
||||
'namespace' => '2342384942_32423423_23423fdsdaf',
|
||||
'components' =>
|
||||
[{ 'text' => 'Paket Anda sudah dikirim. Paket akan sampai dalam {{1}} hari kerja.', 'type' => 'BODY' },
|
||||
{ 'text' => 'Pesan ini berasal dari bisnis yang tidak terverifikasi.', 'type' => 'FOOTER' }],
|
||||
'rejected_reason' => 'NONE' },
|
||||
{ 'name' => 'sample_shipping_confirmation',
|
||||
'status' => 'approved',
|
||||
'category' => 'SHIPPING_UPDATE',
|
||||
'language' => 'en_US',
|
||||
'namespace' => '23423423_2342423_324234234_2343224',
|
||||
'components' =>
|
||||
[{ 'text' => 'Your package has been shipped. It will be delivered in {{1}} business days.', 'type' => 'BODY' },
|
||||
{ 'text' => 'This message is from an unverified business.', 'type' => 'FOOTER' }],
|
||||
'rejected_reason' => 'NONE' }]
|
||||
end
|
||||
message_templates_last_updated { Time.now.utc }
|
||||
|
||||
after(:create) do |channel_whatsapp|
|
||||
create(:inbox, channel: channel_whatsapp, account: channel_whatsapp.account)
|
||||
|
|
|
@ -7,20 +7,54 @@ describe Whatsapp::SendOnWhatsappService do
|
|||
end
|
||||
|
||||
context 'when a valid message' do
|
||||
it 'calls channel.send_message' do
|
||||
it 'calls channel.send_message when with in 24 hour limit' do
|
||||
whatsapp_request = double
|
||||
whatsapp_channel = create(:channel_whatsapp)
|
||||
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: '123456789')
|
||||
conversation = create(:conversation, contact_inbox: contact_inbox, inbox: whatsapp_channel.inbox)
|
||||
# to handle the case of 24 hour window limit.
|
||||
create(:message, message_type: :incoming, content: 'test',
|
||||
conversation: conversation)
|
||||
message = create(:message, message_type: :outgoing, content: 'test',
|
||||
conversation: conversation)
|
||||
allow(HTTParty).to receive(:post).and_return(whatsapp_request)
|
||||
allow(whatsapp_request).to receive(:success?).and_return(true)
|
||||
allow(whatsapp_request).to receive(:[]).with('messages').and_return([{ 'id' => '123456789' }])
|
||||
expect(HTTParty).to receive(:post).with(
|
||||
'https://waba.360dialog.io/v1/messages',
|
||||
headers: { 'D360-API-KEY': 'test_key', 'Content-Type': 'application/json' },
|
||||
body: { to: '123456789', text: { body: 'test' }, type: 'text' }.to_json
|
||||
headers: { 'D360-API-KEY' => 'test_key', 'Content-Type' => 'application/json' },
|
||||
body: { 'to' => '123456789', 'text' => { 'body' => 'test' }, 'type' => 'text' }.to_json
|
||||
)
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
|
||||
it 'calls channel.send_template when after 24 hour limit' do
|
||||
whatsapp_request = double
|
||||
whatsapp_channel = create(:channel_whatsapp)
|
||||
contact_inbox = create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: '123456789')
|
||||
conversation = create(:conversation, contact_inbox: contact_inbox, inbox: whatsapp_channel.inbox)
|
||||
message = create(:message, message_type: :outgoing, content: 'Your package has been shipped. It will be delivered in 3 business days.',
|
||||
conversation: conversation)
|
||||
allow(HTTParty).to receive(:post).and_return(whatsapp_request)
|
||||
allow(whatsapp_request).to receive(:success?).and_return(true)
|
||||
allow(whatsapp_request).to receive(:[]).with('messages').and_return([{ 'id' => '123456789' }])
|
||||
expect(HTTParty).to receive(:post).with(
|
||||
'https://waba.360dialog.io/v1/messages',
|
||||
headers: { 'D360-API-KEY' => 'test_key', 'Content-Type' => 'application/json' },
|
||||
body: {
|
||||
to: '123456789',
|
||||
template: {
|
||||
name: 'sample_shipping_confirmation',
|
||||
namespace: '23423423_2342423_324234234_2343224',
|
||||
language: { 'policy': 'deterministic', 'code': 'en_US' },
|
||||
components: [{ 'type': 'body', 'parameters': [{ 'type': 'text', 'text': '3' }] }]
|
||||
},
|
||||
type: 'template'
|
||||
}.to_json
|
||||
)
|
||||
described_class.new(message: message).perform
|
||||
expect(message.reload.source_id).to eq('123456789')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue