mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-07 14:37:53 +00:00
Implements comprehensive Twilio WhatsApp content template support (Phase
1) enabling text, media, and quick reply templates with proper parameter
conversion, sync capabilities, and feature flag protection.
### Features Implemented
**Template Types Supported**
- Basic Text Templates: Simple text with variables ({{1}}, {{2}})
- Media Templates: Image/Video/Document templates with text variables
- Quick Reply Templates: Interactive button templates
- Phase 2 (Future): List Picker, Call-to-Action, Catalog, Carousel,
Authentication templates
**Template Synchronization**
- API Endpoint: POST
/api/v1/accounts/{account_id}/inboxes/{inbox_id}/sync_templates
- Background Job: Channels::Twilio::TemplatesSyncJob
- Storage: JSONB format in channel_twilio_sms.content_templates
- Auto-categorization: UTILITY, MARKETING, AUTHENTICATION categories
### Template Examples Tested
#### Text template
```
{ "name": "greet", "language": "en" }
```
#### Template with variables
```
{ "name": "order_status", "parameters": [{"type": "body", "parameters": [{"text": "John"}]}] }
```
#### Media template with image
```
{ "name": "product_showcase", "parameters": [
{"type": "header", "parameters": [{"image": {"link": "image.jpg"}}]},
{"type": "body", "parameters": [{"text": "iPhone"}, {"text": "$999"}]}
]}
```
#### Preview
<img width="1362" height="1058" alt="CleanShot 2025-08-26 at 10 01
51@2x"
src="https://github.com/user-attachments/assets/cb280cea-08c3-44ca-8025-58a96cb3a451"
/>
<img width="1308" height="1246" alt="CleanShot 2025-08-26 at 10 02
02@2x"
src="https://github.com/user-attachments/assets/9ea8537a-61e9-40f5-844f-eaad337e1ddd"
/>
#### User guide
https://www.chatwoot.com/hc/user-guide/articles/1756195741-twilio-content-templates
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
117 lines
2.9 KiB
Ruby
117 lines
2.9 KiB
Ruby
class Twilio::TemplateSyncService
|
|
pattr_initialize [:channel!]
|
|
|
|
def call
|
|
fetch_templates_from_twilio
|
|
update_channel_templates
|
|
mark_templates_updated
|
|
rescue Twilio::REST::TwilioError => e
|
|
Rails.logger.error("Twilio template sync failed: #{e.message}")
|
|
false
|
|
end
|
|
|
|
private
|
|
|
|
def fetch_templates_from_twilio
|
|
@templates = client.content.v1.contents.list(limit: 1000)
|
|
end
|
|
|
|
def update_channel_templates
|
|
formatted_templates = @templates.map { |template| format_template(template) }
|
|
|
|
channel.update!(
|
|
content_templates: { templates: formatted_templates },
|
|
content_templates_last_updated: Time.current
|
|
)
|
|
end
|
|
|
|
def format_template(template)
|
|
{
|
|
content_sid: template.sid,
|
|
friendly_name: template.friendly_name,
|
|
language: template.language,
|
|
status: derive_status(template),
|
|
template_type: derive_template_type(template),
|
|
media_type: derive_media_type(template),
|
|
variables: template.variables || {},
|
|
category: derive_category(template),
|
|
body: extract_body_content(template),
|
|
types: template.types,
|
|
created_at: template.date_created,
|
|
updated_at: template.date_updated
|
|
}
|
|
end
|
|
|
|
def mark_templates_updated
|
|
channel.update!(content_templates_last_updated: Time.current)
|
|
end
|
|
|
|
def client
|
|
@client ||= channel.send(:client)
|
|
end
|
|
|
|
def derive_status(_template)
|
|
# For now, assume all fetched templates are approved
|
|
# In the future, this could check approval status from Twilio
|
|
'approved'
|
|
end
|
|
|
|
def derive_template_type(template)
|
|
template_types = template.types.keys
|
|
|
|
if template_types.include?('twilio/media')
|
|
'media'
|
|
elsif template_types.include?('twilio/quick-reply')
|
|
'quick_reply'
|
|
elsif template_types.include?('twilio/catalog')
|
|
'catalog'
|
|
else
|
|
'text'
|
|
end
|
|
end
|
|
|
|
def derive_media_type(template)
|
|
return nil unless derive_template_type(template) == 'media'
|
|
|
|
media_content = template.types['twilio/media']
|
|
return nil unless media_content
|
|
|
|
if media_content['image']
|
|
'image'
|
|
elsif media_content['video']
|
|
'video'
|
|
elsif media_content['document']
|
|
'document'
|
|
end
|
|
end
|
|
|
|
def derive_category(template)
|
|
# Map template friendly names or other attributes to categories
|
|
# For now, use utility as default
|
|
case template.friendly_name
|
|
when /marketing|promo|offer|sale/i
|
|
'marketing'
|
|
when /auth|otp|verify|code/i
|
|
'authentication'
|
|
else
|
|
'utility'
|
|
end
|
|
end
|
|
|
|
def extract_body_content(template)
|
|
template_types = template.types
|
|
|
|
if template_types['twilio/text']
|
|
template_types['twilio/text']['body']
|
|
elsif template_types['twilio/media']
|
|
template_types['twilio/media']['body']
|
|
elsif template_types['twilio/quick-reply']
|
|
template_types['twilio/quick-reply']['body']
|
|
elsif template_types['twilio/catalog']
|
|
template_types['twilio/catalog']['body']
|
|
else
|
|
''
|
|
end
|
|
end
|
|
end
|