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