## Description This implementation adds support for the `media_name` parameter for WhatsApp document templates, resolving the issue where documents appear as "untitled" when sent via templates. **Problem solved:** Documents sent via WhatsApp templates always appeared as "untitled" because Chatwoot didn't process the `filename` field required by the WhatsApp API. **Solution:** Added support for the `media_name` parameter that maps to the WhatsApp API's `filename` field. ## Type of change - [x] New feature (non-breaking change which adds functionality) - [x] This change requires a documentation update ## How Has This Been Tested? Created and executed **7 comprehensive test scenarios**: 1. ✅ Document without `media_name` (backward compatibility) 2. ✅ Document with valid `media_name` 3. ✅ Document with blank `media_name` 4. ✅ Document with null `media_name` 5. ✅ Image with `media_name` (ignored as expected) 6. ✅ Video with `media_name` (ignored as expected) 7. ✅ Blank URL (returns nil appropriately) **All tests passed** and confirmed **100% backward compatibility**. ## Technical Implementation **Backend Changes:** - `PopulateTemplateParametersService`: Added `media_name` parameter support - `TemplateProcessorService`: Pass `media_name` to parameter builder - `WhatsappCloudService`: Updated documentation with `media_name` example **Frontend Changes:** - `WhatsAppTemplateParser.vue`: Added UI field for document filename input - `templateHelper.js`: Include `media_name` for document templates - `whatsappTemplates.json`: Added translation key for document name placeholder **Key Features:** - 🔄 **100% Backward Compatible** - Existing templates continue working - 📝 **Document Filename Support** - Users can specify custom filenames - 🎯 **Document-Only Feature** - Only affects document media types - ✅ **Comprehensive Testing** - All edge cases covered ## Expected Behavior **Before:** ```ruby # All documents appear as "untitled" { type: 'document', document: { link: 'https://example.com/document.pdf' } } ``` **After:** ```ruby # With media_name - displays custom filename { type: 'document', document: { link: 'https://example.com/document.pdf', filename: 'Invoice_2025.pdf' } } # Without media_name - works as before { type: 'document', document: { link: 'https://example.com/document.pdf' } } ``` ## 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 - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
152 lines
4.3 KiB
Ruby
152 lines
4.3 KiB
Ruby
class Whatsapp::PopulateTemplateParametersService
|
|
def build_parameter(value)
|
|
case value
|
|
when String
|
|
build_string_parameter(value)
|
|
when Hash
|
|
build_hash_parameter(value)
|
|
else
|
|
{ type: 'text', text: value.to_s }
|
|
end
|
|
end
|
|
|
|
def build_button_parameter(button)
|
|
return { type: 'text', text: '' } if button.blank?
|
|
|
|
case button['type']
|
|
when 'copy_code'
|
|
coupon_code = button['parameter'].to_s.strip
|
|
raise ArgumentError, 'Coupon code cannot be empty' if coupon_code.blank?
|
|
raise ArgumentError, 'Coupon code cannot exceed 15 characters' if coupon_code.length > 15
|
|
|
|
{
|
|
type: 'coupon_code',
|
|
coupon_code: coupon_code
|
|
}
|
|
else
|
|
# For URL buttons and other button types, treat parameter as text
|
|
# If parameter is blank, use empty string (required for URL buttons)
|
|
{ type: 'text', text: button['parameter'].to_s.strip }
|
|
end
|
|
end
|
|
|
|
def build_media_parameter(url, media_type, media_name = nil)
|
|
return nil if url.blank?
|
|
|
|
sanitized_url = sanitize_parameter(url)
|
|
validate_url(sanitized_url)
|
|
build_media_type_parameter(sanitized_url, media_type.downcase, media_name)
|
|
end
|
|
|
|
def build_named_parameter(parameter_name, value)
|
|
sanitized_value = sanitize_parameter(value.to_s)
|
|
{ type: 'text', parameter_name: parameter_name, text: sanitized_value }
|
|
end
|
|
|
|
private
|
|
|
|
def build_string_parameter(value)
|
|
sanitized_value = sanitize_parameter(value)
|
|
if rich_formatting?(sanitized_value)
|
|
build_rich_text_parameter(sanitized_value)
|
|
else
|
|
{ type: 'text', text: sanitized_value }
|
|
end
|
|
end
|
|
|
|
def build_hash_parameter(value)
|
|
case value['type']
|
|
when 'currency'
|
|
build_currency_parameter(value)
|
|
when 'date_time'
|
|
build_date_time_parameter(value)
|
|
else
|
|
{ type: 'text', text: value.to_s }
|
|
end
|
|
end
|
|
|
|
def build_currency_parameter(value)
|
|
{
|
|
type: 'currency',
|
|
currency: {
|
|
fallback_value: value['fallback_value'],
|
|
code: value['code'],
|
|
amount_1000: value['amount_1000']
|
|
}
|
|
}
|
|
end
|
|
|
|
def build_date_time_parameter(value)
|
|
{
|
|
type: 'date_time',
|
|
date_time: {
|
|
fallback_value: value['fallback_value'],
|
|
day_of_week: value['day_of_week'],
|
|
day_of_month: value['day_of_month'],
|
|
month: value['month'],
|
|
year: value['year']
|
|
}
|
|
}
|
|
end
|
|
|
|
def build_media_type_parameter(sanitized_url, media_type, media_name = nil)
|
|
case media_type
|
|
when 'image'
|
|
build_image_parameter(sanitized_url)
|
|
when 'video'
|
|
build_video_parameter(sanitized_url)
|
|
when 'document'
|
|
build_document_parameter(sanitized_url, media_name)
|
|
else
|
|
raise ArgumentError, "Unsupported media type: #{media_type}"
|
|
end
|
|
end
|
|
|
|
def build_image_parameter(url)
|
|
{ type: 'image', image: { link: url } }
|
|
end
|
|
|
|
def build_video_parameter(url)
|
|
{ type: 'video', video: { link: url } }
|
|
end
|
|
|
|
def build_document_parameter(url, media_name = nil)
|
|
document_params = { link: url }
|
|
document_params[:filename] = media_name if media_name.present?
|
|
|
|
{ type: 'document', document: document_params }
|
|
end
|
|
|
|
def rich_formatting?(text)
|
|
# Check if text contains WhatsApp rich formatting markers
|
|
text.match?(/\*[^*]+\*/) || # Bold: *text*
|
|
text.match?(/_[^_]+_/) || # Italic: _text_
|
|
text.match?(/~[^~]+~/) || # Strikethrough: ~text~
|
|
text.match?(/```[^`]+```/) # Monospace: ```text```
|
|
end
|
|
|
|
def build_rich_text_parameter(text)
|
|
# WhatsApp supports rich text formatting in templates
|
|
# This preserves the formatting markers for the API
|
|
{ type: 'text', text: text }
|
|
end
|
|
|
|
def sanitize_parameter(value)
|
|
# Basic sanitization - remove dangerous characters and limit length
|
|
sanitized = value.to_s.strip
|
|
sanitized = sanitized.gsub(/[<>\"']/, '') # Remove potential HTML/JS chars
|
|
sanitized[0...1000] # Limit length to prevent DoS
|
|
end
|
|
|
|
def validate_url(url)
|
|
return if url.blank?
|
|
|
|
uri = URI.parse(url)
|
|
raise ArgumentError, "Invalid URL scheme: #{uri.scheme}. Only http and https are allowed" unless %w[http https].include?(uri.scheme)
|
|
raise ArgumentError, 'URL too long (max 2000 characters)' if url.length > 2000
|
|
|
|
rescue URI::InvalidURIError => e
|
|
raise ArgumentError, "Invalid URL format: #{e.message}. Please enter a valid URL like https://example.com/document.pdf"
|
|
end
|
|
end
|