mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-04 13:07:55 +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>
148 lines
3.8 KiB
JavaScript
148 lines
3.8 KiB
JavaScript
export const frontendURL = (path, params) => {
|
|
const stringifiedParams = params ? `?${new URLSearchParams(params)}` : '';
|
|
return `/app/${path}${stringifiedParams}`;
|
|
};
|
|
|
|
export const conversationUrl = ({
|
|
accountId,
|
|
activeInbox,
|
|
id,
|
|
label,
|
|
teamId,
|
|
conversationType = '',
|
|
foldersId,
|
|
}) => {
|
|
let url = `accounts/${accountId}/conversations/${id}`;
|
|
if (activeInbox) {
|
|
url = `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`;
|
|
} else if (label) {
|
|
url = `accounts/${accountId}/label/${label}/conversations/${id}`;
|
|
} else if (teamId) {
|
|
url = `accounts/${accountId}/team/${teamId}/conversations/${id}`;
|
|
} else if (foldersId && foldersId !== 0) {
|
|
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
|
|
} else if (conversationType === 'mention') {
|
|
url = `accounts/${accountId}/mentions/conversations/${id}`;
|
|
} else if (conversationType === 'participating') {
|
|
url = `accounts/${accountId}/participating/conversations/${id}`;
|
|
} else if (conversationType === 'unattended') {
|
|
url = `accounts/${accountId}/unattended/conversations/${id}`;
|
|
}
|
|
return url;
|
|
};
|
|
|
|
export const conversationListPageURL = ({
|
|
accountId,
|
|
conversationType = '',
|
|
inboxId,
|
|
label,
|
|
teamId,
|
|
customViewId,
|
|
}) => {
|
|
let url = `accounts/${accountId}/dashboard`;
|
|
if (label) {
|
|
url = `accounts/${accountId}/label/${label}`;
|
|
} else if (teamId) {
|
|
url = `accounts/${accountId}/team/${teamId}`;
|
|
} else if (inboxId) {
|
|
url = `accounts/${accountId}/inbox/${inboxId}`;
|
|
} else if (customViewId) {
|
|
url = `accounts/${accountId}/custom_view/${customViewId}`;
|
|
} else if (conversationType) {
|
|
const urlMap = {
|
|
mention: 'mentions/conversations',
|
|
unattended: 'unattended/conversations',
|
|
};
|
|
url = `accounts/${accountId}/${urlMap[conversationType]}`;
|
|
}
|
|
return frontendURL(url);
|
|
};
|
|
|
|
export const isValidURL = value => {
|
|
/* eslint-disable no-useless-escape */
|
|
const URL_REGEX =
|
|
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
|
|
return URL_REGEX.test(value);
|
|
};
|
|
|
|
export const getArticleSearchURL = ({
|
|
host,
|
|
portalSlug,
|
|
pageNumber,
|
|
locale,
|
|
status,
|
|
authorId,
|
|
categorySlug,
|
|
sort,
|
|
query,
|
|
}) => {
|
|
const queryParams = new URLSearchParams({});
|
|
|
|
const params = {
|
|
page: pageNumber,
|
|
locale,
|
|
status,
|
|
author_id: authorId,
|
|
category_slug: categorySlug,
|
|
sort,
|
|
query,
|
|
};
|
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== null && value !== undefined) {
|
|
queryParams.set(key, value);
|
|
}
|
|
});
|
|
|
|
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
|
|
};
|
|
|
|
export const hasValidAvatarUrl = avatarUrl => {
|
|
try {
|
|
const { host: avatarUrlHost } = new URL(avatarUrl);
|
|
const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
|
|
avatarUrlHost
|
|
);
|
|
return avatarUrl && !isFromGravatar;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const timeStampAppendedURL = dataUrl => {
|
|
const url = new URL(dataUrl);
|
|
if (!url.searchParams.has('t')) {
|
|
url.searchParams.append('t', Date.now());
|
|
}
|
|
|
|
return url.toString();
|
|
};
|
|
|
|
export const getHostNameFromURL = url => {
|
|
try {
|
|
return new URL(url).hostname;
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Extracts filename from a URL
|
|
* @param {string} url - The URL to extract filename from
|
|
* @returns {string} - The extracted filename or original URL if extraction fails
|
|
*/
|
|
export const extractFilenameFromUrl = url => {
|
|
if (!url || typeof url !== 'string') return url;
|
|
|
|
try {
|
|
const urlObj = new URL(url);
|
|
const pathname = urlObj.pathname;
|
|
const filename = pathname.split('/').pop();
|
|
return filename || url;
|
|
} catch (error) {
|
|
// If URL parsing fails, try to extract filename using regex
|
|
const match = url.match(/\/([^/?#]+)(?:[?#]|$)/);
|
|
return match ? match[1] : url;
|
|
}
|
|
};
|