diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 4d9d11cf5..6cb9d3dd0 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -3.4.2 +3.4.3 diff --git a/app/models/concerns/liquidable.rb b/app/models/concerns/liquidable.rb index 8a30977a7..8a90f5f9f 100644 --- a/app/models/concerns/liquidable.rb +++ b/app/models/concerns/liquidable.rb @@ -3,6 +3,7 @@ module Liquidable included do before_create :process_liquid_in_content + before_create :process_liquid_in_template_params end private @@ -35,4 +36,61 @@ module Liquidable # We don't want to process liquid in code blocks 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 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 diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh index 5a40ee068..37c7454f1 100644 --- a/deployment/setup_20.04.sh +++ b/deployment/setup_20.04.sh @@ -2,7 +2,7 @@ # Description: Install and manage a Chatwoot installation. # OS: Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS -# Script Version: 3.4.2 +# Script Version: 3.4.3 # Run this script as root set -eu -o errexit -o pipefail -o noclobber -o nounset @@ -19,7 +19,7 @@ fi # option --output/-o requires 1 argument LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,Upgrade:,webserver,version,web-only,worker-only,convert: OPTIONS=cdhiI:l:rsuU:wvWK -CWCTL_VERSION="3.3.0" +CWCTL_VERSION="3.4.3" pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '') CHATWOOT_HUB_URL="https://hub.2.chatwoot.com/events" @@ -430,7 +430,7 @@ function configure_systemd_services() { if [ "$DEPLOYMENT_TYPE" == "web" ]; then echo "Setting up web-only deployment" - + # Stop and disable existing services if converting if [ "$existing_full_deployment" = true ]; then echo "Converting from full deployment to web-only" @@ -449,14 +449,14 @@ function configure_systemd_services() { cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot-web.target /etc/systemd/system/chatwoot-web.target - + systemctl daemon-reload systemctl enable chatwoot-web.target systemctl start chatwoot-web.target - + elif [ "$DEPLOYMENT_TYPE" == "worker" ]; then echo "Setting up worker-only deployment" - + # Stop and disable existing services if converting if [ "$existing_full_deployment" = true ]; then echo "Converting from full deployment to worker-only" @@ -475,14 +475,14 @@ function configure_systemd_services() { cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.target /etc/systemd/system/chatwoot-worker.target - + systemctl daemon-reload systemctl enable chatwoot-worker.target systemctl start chatwoot-worker.target - + else echo "Setting up full deployment (web + worker)" - + # Stop existing specialized deployments if converting back to full if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then echo "Converting from web-only to full deployment" @@ -494,7 +494,7 @@ function configure_systemd_services() { systemctl stop chatwoot-worker.target || true systemctl disable chatwoot-worker.target || true fi - + cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot.target /etc/systemd/system/chatwoot.target @@ -538,7 +538,7 @@ function setup_ssl() { cd chatwoot sed -i "s/http:\/\/0.0.0.0:3000/https:\/\/$domain_name/g" .env EOF - + # Restart the appropriate chatwoot target if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then systemctl restart chatwoot-web.target @@ -1005,7 +1005,7 @@ EOF upgrade_redis upgrade_node get_pnpm - + sudo -i -u chatwoot << EOF # Navigate to the Chatwoot directory @@ -1098,16 +1098,16 @@ function restart() { ############################################################################## function convert_deployment() { echo "Converting Chatwoot deployment to: $DEPLOYMENT_TYPE" - + # Check if Chatwoot is installed if [ ! -d "/home/chatwoot/chatwoot" ]; then echo "Chatwoot installation not found. Use --install first." exit 1 fi - + # Run the systemd service configuration which handles conversion logic configure_systemd_services - + echo "Deployment converted successfully to: $DEPLOYMENT_TYPE" } diff --git a/spec/models/concerns/liquidable_shared.rb b/spec/models/concerns/liquidable_shared.rb index 8df526a2f..7b9f856cd 100644 --- a/spec/models/concerns/liquidable_shared.rb +++ b/spec/models/concerns/liquidable_shared.rb @@ -69,4 +69,159 @@ shared_examples_for 'liqudable' do end end end + + context 'when liquid is present in template_params' do + let(:contact) do + create(:contact, name: 'john', email: 'john@example.com', phone_number: '+912883', custom_attributes: { customer_type: 'platinum' }) + end + let(:conversation) { create(:conversation, id: 1, contact: contact, custom_attributes: { priority: 'high' }) } + + context 'when message is outgoing with template_params' do + let(:message) { build(:message, conversation: conversation, message_type: 'outgoing') } + + it 'replaces liquid variables in template_params body' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'greet', + 'category' => 'MARKETING', + 'language' => 'en', + 'processed_params' => { + 'body' => { + 'customer_name' => '{{contact.name}}', + 'customer_email' => '{{contact.email}}' + } + } + } + } + message.save! + + body_params = message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['customer_name']).to eq 'John' + expect(body_params['customer_email']).to eq 'john@example.com' + end + + it 'replaces liquid variables in nested template_params' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'header' => { + 'media_url' => 'https://example.com/{{contact.name}}.jpg' + }, + 'body' => { + 'customer_name' => '{{contact.name}}', + 'priority' => '{{conversation.custom_attribute.priority}}' + }, + 'footer' => { + 'company' => '{{account.name}}' + } + } + } + } + message.save! + + processed = message.additional_attributes['template_params']['processed_params'] + expect(processed['header']['media_url']).to eq 'https://example.com/John.jpg' + expect(processed['body']['customer_name']).to eq 'John' + expect(processed['body']['priority']).to eq 'high' + expect(processed['footer']['company']).to eq conversation.account.name + end + + it 'handles arrays in template_params' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'buttons' => [ + { 'type' => 'url', 'parameter' => 'https://example.com/{{contact.name}}' }, + { 'type' => 'text', 'parameter' => 'Hello {{contact.name}}' } + ] + } + } + } + message.save! + + buttons = message.additional_attributes['template_params']['processed_params']['buttons'] + expect(buttons[0]['parameter']).to eq 'https://example.com/John' + expect(buttons[1]['parameter']).to eq 'Hello John' + end + + it 'handles custom attributes in template_params' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'body' => { + 'customer_type' => '{{contact.custom_attribute.customer_type}}', + 'priority' => '{{conversation.custom_attribute.priority}}' + } + } + } + } + message.save! + + body_params = message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['customer_type']).to eq 'platinum' + expect(body_params['priority']).to eq 'high' + end + + it 'handles missing email with default filter in template_params' do + contact.update!(email: nil) + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'body' => { + 'customer_email' => '{{ contact.email | default: "no-email@example.com" }}' + } + } + } + } + message.save! + + body_params = message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['customer_email']).to eq 'no-email@example.com' + end + + it 'handles broken liquid syntax in template_params gracefully' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'body' => { + 'broken_liquid' => '{{contact.name} {{invalid}}' + } + } + } + } + message.save! + + body_params = message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['broken_liquid']).to eq '{{contact.name} {{invalid}}' + end + + it 'does not process template_params when message is incoming' do + incoming_message = build(:message, conversation: conversation, message_type: 'incoming') + incoming_message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'body' => { + 'customer_name' => '{{contact.name}}' + } + } + } + } + incoming_message.save! + + body_params = incoming_message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['customer_name']).to eq '{{contact.name}}' + end + + it 'does not process template_params when not present' do + message.additional_attributes = { 'other_data' => 'test' } + expect { message.save! }.not_to raise_error + end + end + end end