WhatsApp template messages can be delivered with the correct variable
values while the Chatwoot conversation shows numeric placeholders
instead. This affects template sends where the client submits the raw
template body together with valid processed parameters; WhatsApp
delivery itself remains unchanged.
The message model currently passes outgoing content through Liquid
before saving it. Liquid interprets positional WhatsApp placeholders
such as `{{1}}` and `{{2}}` as numeric expressions, even though the
WhatsApp sender independently uses `processed_params.body` to build the
provider payload.
Fixes
https://linear.app/chatwoot/issue/PLA-192/render-whatsapp-template-values-in-conversation-transcripts
### What changed
For WhatsApp template messages, Chatwoot now substitutes positional or
named body placeholders from `processed_params.body` before saving the
transcript. Missing values remain visible as placeholders, and ordinary
outgoing Liquid messages continue through the existing rendering path.
The existing message model regression suite remains green, and the
reported payload now persists as `Hello Ahmad, Furqan is your contact.`
### Things to know
This corrects newly created messages. Existing conversation messages
that were already stored as `1`, `2`, and so on are not backfilled.
### How to reproduce
1. Open a WhatsApp conversation outside the 24-hour messaging window.
2. Send a positional template whose body contains `{{1}}` and `{{2}}`.
3. Supply values for both parameters while submitting the original
template body as the message content.
4. Observe that WhatsApp delivers the substituted values, while the
Chatwoot transcript shows `1` and `2`.
### How to test
1. Select a WhatsApp template with two body variables.
2. Enter `Ahmad` and `Furqan` as the values and send the message.
3. Confirm the recipient receives the substituted template.
4. Confirm the Chatwoot conversation also displays `Ahmad` and `Furqan`
instead of the numeric placeholders.
---------
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
132 lines
4.2 KiB
Ruby
132 lines
4.2 KiB
Ruby
module Liquidable
|
|
extend ActiveSupport::Concern
|
|
|
|
included do
|
|
before_validation :process_liquid_in_content, on: :create, if: :raw_whatsapp_template_content?
|
|
before_create :process_liquid_in_content, unless: :liquid_content_processed?
|
|
before_create :process_liquid_in_template_params
|
|
end
|
|
|
|
private
|
|
|
|
def message_drops
|
|
{
|
|
'contact' => ContactDrop.new(conversation.contact),
|
|
'agent' => UserDrop.new(sender || conversation.assignee),
|
|
'conversation' => ConversationDrop.new(conversation),
|
|
'inbox' => InboxDrop.new(inbox),
|
|
'account' => AccountDrop.new(conversation.account)
|
|
}
|
|
end
|
|
|
|
def liquid_processable_message?
|
|
content.present? && (message_type == 'outgoing' || message_type == 'template')
|
|
end
|
|
|
|
def process_liquid_in_content
|
|
return unless liquid_processable_message?
|
|
|
|
raw_whatsapp_template_content = raw_whatsapp_template_content?
|
|
self.content = if raw_whatsapp_template_content
|
|
Whatsapp::TemplateContentRendererService.new(
|
|
content: content,
|
|
body_params: whatsapp_template_body_params,
|
|
value_renderer: method(:process_liquid_value),
|
|
message_drops: message_drops
|
|
).perform
|
|
else
|
|
Liquid::Template.parse(modified_liquid_content(content)).render(message_drops)
|
|
end
|
|
mark_whatsapp_template_content_rendered if raw_whatsapp_template_content
|
|
@liquid_content_processed = true
|
|
rescue Liquid::Error
|
|
# If there is an error in the liquid syntax, we don't want to process it
|
|
end
|
|
|
|
def liquid_content_processed?
|
|
@liquid_content_processed
|
|
end
|
|
|
|
def mark_whatsapp_template_content_rendered
|
|
self.additional_attributes = additional_attributes.merge('template_params' => template_params_data.merge('content_mode' => 'rendered'))
|
|
end
|
|
|
|
def raw_whatsapp_template_content?
|
|
return false unless inbox&.channel_type == 'Channel::Whatsapp' && additional_attributes.is_a?(Hash)
|
|
|
|
template_params = additional_attributes['template_params']
|
|
return false unless template_params.is_a?(Hash)
|
|
|
|
template_params['content_mode'] == 'raw_template' &&
|
|
(template_params['processed_params'].nil? || template_params['processed_params'].is_a?(Hash))
|
|
end
|
|
|
|
def modified_liquid_content(message_content)
|
|
# This regex is used to match the code blocks in the content
|
|
# We don't want to process liquid in code blocks
|
|
message_content.gsub(/`(.*?)`/m, '{% raw %}`\\1`{% endraw %}')
|
|
end
|
|
|
|
def process_liquid_in_template_params
|
|
return unless template_params_present? && liquid_processable_template_params?
|
|
|
|
processed_params = process_liquid_in_hash(template_params_data['processed_params'])
|
|
|
|
# Update the additional_attributes with processed template_params
|
|
self.additional_attributes = additional_attributes.merge(
|
|
'template_params' => template_params_data.merge('processed_params' => processed_params)
|
|
)
|
|
rescue Liquid::Error
|
|
# If there is an error in the liquid syntax, we don't want to process it
|
|
end
|
|
|
|
def template_params_present?
|
|
additional_attributes&.dig('template_params', 'processed_params').present?
|
|
end
|
|
|
|
def liquid_processable_template_params?
|
|
message_type == 'outgoing' || message_type == 'template'
|
|
end
|
|
|
|
def template_params_data
|
|
additional_attributes['template_params']
|
|
end
|
|
|
|
def whatsapp_template_body_params
|
|
normalized_params = Whatsapp::TemplateParameterConverterService.new(template_params_data.deep_dup, nil).normalize_to_enhanced
|
|
normalized_params.dig('processed_params', 'body') || {}
|
|
end
|
|
|
|
def process_liquid_in_hash(hash)
|
|
return hash unless hash.is_a?(Hash)
|
|
|
|
hash.transform_values { |value| process_liquid_value(value) }
|
|
end
|
|
|
|
def process_liquid_value(value)
|
|
case value
|
|
when String
|
|
process_liquid_string(value)
|
|
when Hash
|
|
process_liquid_in_hash(value)
|
|
when Array
|
|
process_liquid_array(value)
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
def process_liquid_array(array)
|
|
array.map { |item| process_liquid_value(item) }
|
|
end
|
|
|
|
def process_liquid_string(string)
|
|
return string if string.blank?
|
|
|
|
template = Liquid::Template.parse(string)
|
|
template.render(message_drops)
|
|
rescue Liquid::Error
|
|
string
|
|
end
|
|
end
|