chore: Whatsapp templates fix for 360 dialog (#3479)

Fixes: #3426
This commit is contained in:
Sojan Jose 2021-11-30 20:50:35 +05:30 committed by GitHub
parent 0899f62912
commit d5c30760a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 215 additions and 17 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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