# Pull Request Template ## Description This PR adds support for WhatsApp campaigns to Chatwoot, allowing businesses to reach their customers through WhatsApp. The implementation includes backend support for WhatsApp template messages, frontend UI components, and integration with the existing campaign system. Fixes #8465 Fixes https://linear.app/chatwoot/issue/CW-3390/whatsapp-campaigns ## Type of change - [x] New feature (non-breaking change which adds functionality) - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? - Tested WhatsApp campaign creation UI flow - Verified backend API endpoints for campaign creation - Tested campaign service integration with WhatsApp templates - Validated proper filtering of WhatsApp campaigns in the store ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules ## What we have changed: We have added support for WhatsApp campaigns as requested in the discussion. Ref: https://github.com/orgs/chatwoot/discussions/8465 **Note:** This implementation doesn't exactly match the maintainer's specification and variable support is missing. This is an initial implementation that provides the core WhatsApp campaign functionality. ### Changes included: **Backend:** - Added `template_params` column to campaigns table (migration + schema) - Created `Whatsapp::OneoffCampaignService` for WhatsApp campaign execution - Updated campaign model to support WhatsApp inbox types - Added template_params support to campaign controller and API **Frontend:** - Added WhatsApp campaign page, dialog, and form components - Updated campaign store to filter WhatsApp campaigns separately - Added WhatsApp-specific routes and empty state - Updated i18n translations for WhatsApp campaigns - Modified sidebar to include WhatsApp campaigns navigation This provides a foundation for WhatsApp campaigns that can be extended with variable support and other enhancements in future iterations. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
96 lines
3.4 KiB
Ruby
96 lines
3.4 KiB
Ruby
class Whatsapp::TemplateProcessorService
|
|
pattr_initialize [:channel!, :template_params, :message]
|
|
|
|
def call
|
|
if template_params.present?
|
|
process_template_with_params
|
|
else
|
|
process_template_from_message
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def process_template_with_params
|
|
[
|
|
template_params['name'],
|
|
template_params['namespace'],
|
|
template_params['language'],
|
|
processed_templates_params
|
|
]
|
|
end
|
|
|
|
def process_template_from_message
|
|
return [nil, nil, nil, nil] if message.blank?
|
|
|
|
# Delete the following logic once the update for template_params is stable
|
|
# 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.outgoing_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}}
|
|
|
|
# transform the template text into a regex string
|
|
# we need to replace the {{num}} with matchers that can be used to capture the variables
|
|
template_text = template_text.gsub(/{{\d}}/, '(.*)')
|
|
# escape if there are regex characters in the template text
|
|
template_text = Regexp.escape(template_text)
|
|
# ensuring only the variables remain as capture groups
|
|
template_text = template_text.gsub(Regexp.escape('(.*)'), '(.*)')
|
|
|
|
template_match_string = "^#{template_text}$"
|
|
Regexp.new template_match_string
|
|
end
|
|
|
|
def find_template
|
|
channel.message_templates.find do |t|
|
|
t['name'] == template_params['name'] && t['language'] == template_params['language'] && t['status']&.downcase == 'approved'
|
|
end
|
|
end
|
|
|
|
def processed_templates_params
|
|
template = find_template
|
|
return if template.blank?
|
|
|
|
parameter_format = template['parameter_format']
|
|
|
|
if parameter_format == 'NAMED'
|
|
template_params['processed_params']&.map { |key, value| { type: 'text', parameter_name: key, text: value } }
|
|
else
|
|
template_params['processed_params']&.map { |_, value| { type: 'text', text: value } }
|
|
end
|
|
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
|
|
end
|