@vicacha-devs/strapi-plugin-broadcasts 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +15 -2
- package/dist/admin/BroadcastsHomePage-CM_N_spo.mjs +1210 -0
- package/dist/admin/BroadcastsHomePage-DQueM7nj.js +1228 -0
- package/dist/admin/SettingsPage-TawvAAiv.js +1187 -0
- package/dist/admin/SettingsPage-ikcb0cfh.mjs +1169 -0
- package/dist/admin/ar-vwekVqfp.js +232 -0
- package/dist/admin/ar-wZTCd3Za.mjs +232 -0
- package/dist/admin/de-IZicxuvv.mjs +232 -0
- package/dist/admin/de-Qa2SQQfY.js +232 -0
- package/dist/admin/en-B-HnZo8U.mjs +232 -0
- package/dist/admin/en-D1byrK0s.js +232 -0
- package/dist/admin/es-Co7PmAj8.mjs +232 -0
- package/dist/admin/es-Dgylqagv.js +232 -0
- package/dist/admin/fr-B9zZn3z1.mjs +232 -0
- package/dist/admin/fr-C7-MIWnw.js +232 -0
- package/dist/admin/index-CApLw9Uu.mjs +1036 -0
- package/dist/admin/index-Dw6J7U1v.js +1053 -0
- package/dist/admin/index.js +6 -0
- package/dist/admin/index.mjs +6 -0
- package/dist/admin/it-DJU9iBqT.mjs +232 -0
- package/dist/admin/it-D_mulZtx.js +232 -0
- package/dist/admin/pt-B9swwjPu.js +232 -0
- package/dist/admin/pt-Dr8u5we0.mjs +232 -0
- package/dist/admin/ru-B0NNiMNA.js +232 -0
- package/dist/admin/ru-Bd4cEKDV.mjs +232 -0
- package/dist/admin/selectOptions-BlvNrhvc.mjs +150 -0
- package/dist/admin/selectOptions-DuHQ0KK3.js +149 -0
- package/dist/admin/src/components/AutomationAndLogsTabs.d.ts +16 -0
- package/dist/admin/src/components/BridgesSandboxTab.d.ts +8 -0
- package/dist/admin/src/components/CampaignComposerForm.d.ts +26 -0
- package/dist/admin/src/components/CampaignsTableSection.d.ts +27 -0
- package/dist/admin/src/components/DeliveryLedgerSection.d.ts +17 -0
- package/dist/admin/src/components/Initializer.d.ts +6 -0
- package/dist/admin/src/components/OrganizationEmailTab.d.ts +10 -0
- package/dist/admin/src/components/PluginIcon.d.ts +3 -0
- package/dist/admin/src/components/WhatsAppTransportTab.d.ts +11 -0
- package/dist/admin/src/index.d.ts +24 -0
- package/dist/admin/src/pages/BroadcastsHomePage.d.ts +2 -0
- package/dist/admin/src/pages/SettingsPage.d.ts +3 -0
- package/dist/admin/src/permissions.d.ts +30 -0
- package/dist/admin/src/pluginId.d.ts +2 -0
- package/dist/admin/src/translations/cmLabels.d.ts +1 -0
- package/dist/admin/src/translations/cmLabelsExtra.d.ts +1 -0
- package/dist/admin/src/translations/cmPlaceholders.d.ts +1 -0
- package/dist/admin/src/translations/cmPlaceholdersExtra.d.ts +1 -0
- package/dist/admin/src/utils/extractApiError.d.ts +1 -0
- package/dist/admin/src/utils/selectOptions.d.ts +21 -0
- package/dist/admin/tr-Cc-ItZUP.js +232 -0
- package/dist/admin/tr-DlyHa2Vk.mjs +232 -0
- package/dist/admin/zh-CLzJ7x3y.mjs +232 -0
- package/dist/admin/zh-CaqwZk-p.js +232 -0
- package/dist/server/index.js +3399 -0
- package/dist/server/index.mjs +3399 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +5 -0
- package/dist/server/src/content-types/automation-rule/index.d.ts +90 -0
- package/dist/server/src/content-types/automation-rule/schema.json.d.ts +90 -0
- package/dist/server/src/content-types/campaign/index.d.ts +118 -0
- package/dist/server/src/content-types/campaign/schema.json.d.ts +118 -0
- package/dist/server/src/content-types/delivery-log/index.d.ts +71 -0
- package/dist/server/src/content-types/delivery-log/schema.json.d.ts +71 -0
- package/dist/server/src/content-types/index.d.ts +279 -0
- package/dist/server/src/controllers/broadcastController.d.ts +32 -0
- package/dist/server/src/controllers/index.d.ts +22 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/eventHubRegistry.d.ts +5 -0
- package/dist/server/src/index.d.ts +463 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin.d.ts +17 -0
- package/dist/server/src/routes/content-api.d.ts +13 -0
- package/dist/server/src/routes/index.d.ts +31 -0
- package/dist/server/src/services/automationService.d.ts +33 -0
- package/dist/server/src/services/campaignChannelDispatch.d.ts +20 -0
- package/dist/server/src/services/campaignHelpers.d.ts +20 -0
- package/dist/server/src/services/campaignLifecycle.d.ts +23 -0
- package/dist/server/src/services/campaignService.d.ts +54 -0
- package/dist/server/src/services/deliveryLedger.d.ts +18 -0
- package/dist/server/src/services/emailDesignerBridge.d.ts +30 -0
- package/dist/server/src/services/idempotency.d.ts +5 -0
- package/dist/server/src/services/index.d.ts +119 -0
- package/dist/server/src/services/settings.d.ts +12 -0
- package/dist/server/src/services/transportService.d.ts +70 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +825 -0
- package/dist/shared/index.mjs +823 -0
- package/package.json +109 -3
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const shared = require("@vicacha-devs/strapi-plugin-calendar/shared");
|
|
4
|
+
function isSupportedCalendar(cal) {
|
|
5
|
+
if (typeof cal !== "string" || !cal.trim()) return false;
|
|
6
|
+
return shared.SUPPORTED_INTL_CALENDARS.some((item) => item.id === cal.trim());
|
|
7
|
+
}
|
|
8
|
+
function formatSecondaryDate(isoDateString, calendar = "islamic-umalqura", locale = "es") {
|
|
9
|
+
if (!isoDateString) {
|
|
10
|
+
return { calendar, formatted: "" };
|
|
11
|
+
}
|
|
12
|
+
const date = new Date(isoDateString);
|
|
13
|
+
if (Number.isNaN(date.getTime())) {
|
|
14
|
+
return { calendar, formatted: isoDateString };
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const formatter = new Intl.DateTimeFormat(`${locale}-u-ca-${calendar}`, {
|
|
18
|
+
year: "numeric",
|
|
19
|
+
month: "short",
|
|
20
|
+
day: "numeric"
|
|
21
|
+
});
|
|
22
|
+
const parts = formatter.formatToParts(date);
|
|
23
|
+
const year = Number(parts.find((p) => p.type === "year")?.value) || void 0;
|
|
24
|
+
const month = Number(parts.find((p) => p.type === "month")?.value) || void 0;
|
|
25
|
+
const day = Number(parts.find((p) => p.type === "day")?.value) || void 0;
|
|
26
|
+
return {
|
|
27
|
+
calendar,
|
|
28
|
+
formatted: formatter.format(date),
|
|
29
|
+
year,
|
|
30
|
+
month,
|
|
31
|
+
day
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
return {
|
|
35
|
+
calendar,
|
|
36
|
+
formatted: date.toISOString().slice(0, 10)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const PLUGIN_ID = "broadcasts";
|
|
41
|
+
const CAMPAIGN_UID = "plugin::broadcasts.campaign";
|
|
42
|
+
const AUTOMATION_RULE_UID = "plugin::broadcasts.automation-rule";
|
|
43
|
+
const DELIVERY_LOG_UID = "plugin::broadcasts.delivery-log";
|
|
44
|
+
const SUPPORTED_CHANNELS = [
|
|
45
|
+
"email",
|
|
46
|
+
"telegram",
|
|
47
|
+
"whatsapp"
|
|
48
|
+
];
|
|
49
|
+
const SUPPORTED_WHATSAPP_DRIVERS = [
|
|
50
|
+
"meta-cloud",
|
|
51
|
+
"openwa",
|
|
52
|
+
"evolution"
|
|
53
|
+
];
|
|
54
|
+
const DEFAULT_BROADCAST_TOPICS = [
|
|
55
|
+
"announcements",
|
|
56
|
+
"events",
|
|
57
|
+
"courses",
|
|
58
|
+
"ramadan_eid",
|
|
59
|
+
"urgent_janazah",
|
|
60
|
+
"newsletter"
|
|
61
|
+
];
|
|
62
|
+
const DEFAULT_BROADCAST_SETTINGS = {
|
|
63
|
+
primaryCalendar: "gregory",
|
|
64
|
+
secondaryCalendars: ["islamic-umalqura"],
|
|
65
|
+
defaultLocale: "es",
|
|
66
|
+
supportedLocales: ["es", "en", "ar", "tr"],
|
|
67
|
+
availableTopics: [...DEFAULT_BROADCAST_TOPICS],
|
|
68
|
+
dryRun: true,
|
|
69
|
+
enableInboxSubscriberBridge: true,
|
|
70
|
+
enableEventHubAutomations: true,
|
|
71
|
+
enableEmailDesignerBridge: true,
|
|
72
|
+
defaultEmailTemplateRef: "",
|
|
73
|
+
siteBaseUrl: "https://mezquitabogota.org",
|
|
74
|
+
organizationName: "Mezquita Central Abu Bakr As-Siddiq — Bogotá",
|
|
75
|
+
emailFromAddress: "no-reply@mezquitabogota.org",
|
|
76
|
+
emailReplyToAddress: "info@mezquitabogota.org",
|
|
77
|
+
telegramEnabled: true,
|
|
78
|
+
telegramBotToken: "",
|
|
79
|
+
telegramDefaultChatId: "@MezquitaCentralBogota",
|
|
80
|
+
telegramTopicChatMap: {},
|
|
81
|
+
telegramTopicThreadMap: {},
|
|
82
|
+
whatsappEnabled: true,
|
|
83
|
+
whatsappDriver: "meta-cloud",
|
|
84
|
+
whatsappPhoneNumberId: "",
|
|
85
|
+
whatsappAccessToken: "",
|
|
86
|
+
whatsappGatewayUrl: "",
|
|
87
|
+
whatsappGatewaySession: "default",
|
|
88
|
+
whatsappGatewayApiKey: "",
|
|
89
|
+
whatsappDefaultRecipients: [],
|
|
90
|
+
whatsappTopicGroupMap: {},
|
|
91
|
+
allowLocalhostGateways: false,
|
|
92
|
+
publicChannelLinks: [
|
|
93
|
+
{
|
|
94
|
+
id: "telegram-main",
|
|
95
|
+
channel: "telegram",
|
|
96
|
+
label: "Canal Oficial de Telegram",
|
|
97
|
+
handle: "@MezquitaCentralBogota",
|
|
98
|
+
inviteUrl: "https://t.me/MezquitaCentralBogota",
|
|
99
|
+
topic: "announcements",
|
|
100
|
+
localizedLabels: {
|
|
101
|
+
es: "Canal Oficial de Telegram",
|
|
102
|
+
en: "Official Telegram Channel",
|
|
103
|
+
ar: "القناة الرسمية على تيليجرام",
|
|
104
|
+
tr: "Resmi Telegram Kanalı"
|
|
105
|
+
},
|
|
106
|
+
localizedDescriptions: {
|
|
107
|
+
es: "Horarios de Jumu’ah, avistamiento lunar de Ramadán, cursos y avisos urgentes.",
|
|
108
|
+
en: "Jumu’ah schedules, Ramadan moon sighting, courses, and urgent community notices.",
|
|
109
|
+
ar: "مواعيد صلاة الجمعة، تحري هلال رمضان، الدورات والإعلانات العاجلة.",
|
|
110
|
+
tr: "Cuma vakitleri, Ramazan hilal gözlemi, kurslar ve acil duyurular."
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "whatsapp-community",
|
|
115
|
+
channel: "whatsapp",
|
|
116
|
+
label: "Comunidad Oficial de WhatsApp",
|
|
117
|
+
handle: "+57 310 000 0000",
|
|
118
|
+
inviteUrl: "https://whatsapp.com/channel/0029VaMezquitaBogota",
|
|
119
|
+
topic: "announcements",
|
|
120
|
+
localizedLabels: {
|
|
121
|
+
es: "Comunidad Oficial de WhatsApp",
|
|
122
|
+
en: "Official WhatsApp Community",
|
|
123
|
+
ar: "مجتمع واتساب الرسمي",
|
|
124
|
+
tr: "Resmi WhatsApp Topluluğu"
|
|
125
|
+
},
|
|
126
|
+
localizedDescriptions: {
|
|
127
|
+
es: "Alertas directas de oración funeraria (Janazah), eventos comunitarios y boletín semanal.",
|
|
128
|
+
en: "Direct Janazah funeral prayer alerts, community events, and weekly newsletter.",
|
|
129
|
+
ar: "تنبيهات صلاة الجنازة العاجلة، الفعاليات المجتمعية والنشرة الأسبوعية.",
|
|
130
|
+
tr: "Acil cenaze namazı duyuruları, topluluk etkinlikleri ve haftalık bülten."
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
};
|
|
135
|
+
const STARTER_CAMPAIGN_TEMPLATES = [
|
|
136
|
+
{
|
|
137
|
+
id: "jumuah-weekly-reminder",
|
|
138
|
+
slug: "jumuah-weekly-reminder",
|
|
139
|
+
title: "Recordatorio Semanal de Khutbah y Oración del Viernes (Jumu’ah)",
|
|
140
|
+
topic: "announcements",
|
|
141
|
+
channels: ["email", "telegram", "whatsapp"],
|
|
142
|
+
priority: "normal",
|
|
143
|
+
subject: "🕌 Jumu’ah en {{organizationName}} ({{secondaryDate}})",
|
|
144
|
+
bodyText: "As-salamu alaykum {{recipientName}}. Te esperamos este viernes para la Khutbah y oración de Jumu’ah en {{organizationName}}. Fecha Hijri: {{secondaryDate}}.",
|
|
145
|
+
bodyHtml: "<p>As-salamu alaykum <strong>{{recipientName}}</strong>,</p><p>Te esperamos este viernes para la Khutbah y oración congregacional de <strong>Jumu’ah</strong> en {{organizationName}}.</p><p>Calendario Hijri: <em>{{secondaryDate}}</em></p>",
|
|
146
|
+
ctaLabel: "Ver Horarios de Oración",
|
|
147
|
+
ctaUrl: "https://mezquitabogota.org/es/horarios",
|
|
148
|
+
translations: {
|
|
149
|
+
es: {
|
|
150
|
+
subject: "🕌 Jumu’ah en {{organizationName}} ({{secondaryDate}})",
|
|
151
|
+
bodyText: "As-salamu alaykum {{recipientName}}. Te esperamos este viernes para la Khutbah y oración de Jumu’ah en {{organizationName}}. Fecha Hijri: {{secondaryDate}}.",
|
|
152
|
+
bodyHtml: "<p>As-salamu alaykum <strong>{{recipientName}}</strong>,</p><p>Te esperamos este viernes para la Khutbah y oración congregacional de <strong>Jumu’ah</strong> en {{organizationName}}.</p><p>Calendario Hijri: <em>{{secondaryDate}}</em></p>",
|
|
153
|
+
ctaLabel: "Ver Horarios de Oración",
|
|
154
|
+
ctaUrl: "https://mezquitabogota.org/es/horarios"
|
|
155
|
+
},
|
|
156
|
+
en: {
|
|
157
|
+
subject: "🕌 Friday Jumu’ah Reminder — {{organizationName}} ({{secondaryDate}})",
|
|
158
|
+
bodyText: "As-salamu alaykum {{recipientName}}. Join us this Friday for the Jumu’ah Khutbah and congregational prayer at {{organizationName}}. Hijri date: {{secondaryDate}}.",
|
|
159
|
+
bodyHtml: "<p>As-salamu alaykum <strong>{{recipientName}}</strong>,</p><p>Join us this Friday for the <strong>Jumu’ah Khutbah</strong> and congregational prayer at {{organizationName}}.</p><p>Hijri date: <em>{{secondaryDate}}</em></p>",
|
|
160
|
+
ctaLabel: "View Prayer Schedule",
|
|
161
|
+
ctaUrl: "https://mezquitabogota.org/en/prayer-times"
|
|
162
|
+
},
|
|
163
|
+
ar: {
|
|
164
|
+
subject: "🕌 تذكير صلاة وخطبة الجمعة — {{organizationName}} ({{secondaryDate}})",
|
|
165
|
+
bodyText: "السلام عليكم {{recipientName}}، ندعوكم لحضور خطبة وصلاة الجمعة في {{organizationName}}. التاريخ الهجري: {{secondaryDate}}.",
|
|
166
|
+
bodyHtml: "<p>السلام عليكم <strong>{{recipientName}}</strong>،</p><p>ندعوكم لحضور خطبة وصلاة <strong>الجمعة</strong> في {{organizationName}}.</p><p>التاريخ الهجري: <em>{{secondaryDate}}</em></p>",
|
|
167
|
+
ctaLabel: "عرض مواقيت الصلاة",
|
|
168
|
+
ctaUrl: "https://mezquitabogota.org/ar/prayer-times"
|
|
169
|
+
},
|
|
170
|
+
tr: {
|
|
171
|
+
subject: "🕌 Cuma Hutbesi ve Namazı Hatırlatması — {{organizationName}} ({{secondaryDate}})",
|
|
172
|
+
bodyText: "Es-selamu aleyküm {{recipientName}}. Bu Cuma {{organizationName}} bünyesinde Cuma hutbesi ve namazına davetlisiniz. Hicri Tarih: {{secondaryDate}}.",
|
|
173
|
+
bodyHtml: "<p>Es-selamu aleyküm <strong>{{recipientName}}</strong>,</p><p>Bu Cuma {{organizationName}} bünyesinde <strong>Cuma Hutbesi ve Namazına</strong> davetlisiniz.</p><p>Hicri Tarih: <em>{{secondaryDate}}</em></p>",
|
|
174
|
+
ctaLabel: "Namaz Vakitlerini Gör",
|
|
175
|
+
ctaUrl: "https://mezquitabogota.org/tr/prayer-times"
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "urgent-janazah-alert",
|
|
181
|
+
slug: "urgent-janazah-alert",
|
|
182
|
+
title: "Aviso Urgente de Oración Funeraria (Salat al-Janazah)",
|
|
183
|
+
topic: "urgent_janazah",
|
|
184
|
+
channels: ["telegram", "whatsapp", "email"],
|
|
185
|
+
priority: "urgent",
|
|
186
|
+
subject: "🕯️ Aviso Urgente: Salat al-Janazah en {{organizationName}}",
|
|
187
|
+
bodyText: "Inna lillahi wa inna ilayhi raji’un. Informamos a la comunidad que se realizará la oración funeraria (Salat al-Janazah) en {{organizationName}} después de la oración de Dhuhr ({{secondaryDate}}).",
|
|
188
|
+
bodyHtml: "<p><strong>Inna lillahi wa inna ilayhi raji’un</strong></p><p>Informamos a la comunidad que se realizará la oración funeraria (<strong>Salat al-Janazah</strong>) en {{organizationName}} después de la oración de Dhuhr ({{secondaryDate}}).</p>",
|
|
189
|
+
ctaLabel: "Detalles y Ubicación",
|
|
190
|
+
ctaUrl: "https://mezquitabogota.org/es/avisos",
|
|
191
|
+
translations: {
|
|
192
|
+
es: {
|
|
193
|
+
subject: "🕯️ Aviso Urgente: Salat al-Janazah en {{organizationName}}",
|
|
194
|
+
bodyText: "Inna lillahi wa inna ilayhi raji’un. Informamos a la comunidad que se realizará la oración funeraria (Salat al-Janazah) en {{organizationName}} después de la oración de Dhuhr ({{secondaryDate}}).",
|
|
195
|
+
ctaLabel: "Detalles y Ubicación",
|
|
196
|
+
ctaUrl: "https://mezquitabogota.org/es/avisos"
|
|
197
|
+
},
|
|
198
|
+
en: {
|
|
199
|
+
subject: "🕯️ Urgent Community Notice: Salat al-Janazah at {{organizationName}}",
|
|
200
|
+
bodyText: "Inna lillahi wa inna ilayhi raji’un. Funeral prayer (Salat al-Janazah) will be held at {{organizationName}} following Dhuhr prayer ({{secondaryDate}}).",
|
|
201
|
+
ctaLabel: "Details & Location",
|
|
202
|
+
ctaUrl: "https://mezquitabogota.org/en/announcements"
|
|
203
|
+
},
|
|
204
|
+
ar: {
|
|
205
|
+
subject: "🕯️ إعلان عاجل: صلاة الجنازة في {{organizationName}}",
|
|
206
|
+
bodyText: "إنا لله وإنا إليه راجعون. تُقام صلاة الجنازة في {{organizationName}} عقب صلاة الظهر ({{secondaryDate}}).",
|
|
207
|
+
ctaLabel: "التفاصيل والموقع",
|
|
208
|
+
ctaUrl: "https://mezquitabogota.org/ar/announcements"
|
|
209
|
+
},
|
|
210
|
+
tr: {
|
|
211
|
+
subject: "🕯️ Acil Duyuru: {{organizationName}} Cenaze Namazı",
|
|
212
|
+
bodyText: "İnna lillahi ve inna ileyhi raciun. Öğle namazını müteakip {{organizationName}} camiimizde cenaze namazı kılınacaktır ({{secondaryDate}}).",
|
|
213
|
+
ctaLabel: "Detaylar ve Konum",
|
|
214
|
+
ctaUrl: "https://mezquitabogota.org/tr/announcements"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
];
|
|
219
|
+
const STARTER_AUTOMATION_RULES = [
|
|
220
|
+
{
|
|
221
|
+
slug: "auto-alert-banner-broadcast",
|
|
222
|
+
name: "Auto-Broadcast Urgent Site Alert Banners",
|
|
223
|
+
eventName: "inbox.alert.published",
|
|
224
|
+
topic: "announcements",
|
|
225
|
+
channels: ["telegram", "whatsapp"],
|
|
226
|
+
active: true,
|
|
227
|
+
priority: "urgent",
|
|
228
|
+
subjectTemplate: "🚨 {{title}} — {{organizationName}}",
|
|
229
|
+
bodyTextTemplate: "🚨 {{title}}: {{message}} ({{secondaryDate}})",
|
|
230
|
+
bodyHtmlTemplate: "<p><strong>🚨 {{title}}</strong></p><p>{{message}}</p><p><em>{{secondaryDate}}</em></p>",
|
|
231
|
+
ctaLabelTemplate: "Ver Aviso Oficial",
|
|
232
|
+
ctaUrlTemplate: "{{siteBaseUrl}}"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
slug: "auto-event-published-broadcast",
|
|
236
|
+
name: "Auto-Broadcast Newly Published Community Events",
|
|
237
|
+
eventName: "event.published",
|
|
238
|
+
targetContentType: "api::event.event",
|
|
239
|
+
topic: "events",
|
|
240
|
+
channels: ["telegram", "email"],
|
|
241
|
+
active: true,
|
|
242
|
+
priority: "normal",
|
|
243
|
+
subjectTemplate: "📅 Nuevo Evento: {{title}} ({{secondaryDate}})",
|
|
244
|
+
bodyTextTemplate: "Nuevo evento comunitario en {{organizationName}}: {{title}}. {{summary}} ({{secondaryDate}}).",
|
|
245
|
+
bodyHtmlTemplate: "<p>Nuevo evento comunitario en <strong>{{organizationName}}</strong>:</p><h3>{{title}}</h3><p>{{summary}}</p><p><em>{{secondaryDate}}</em></p>",
|
|
246
|
+
ctaLabelTemplate: "Inscribirse al Evento",
|
|
247
|
+
ctaUrlTemplate: "{{siteBaseUrl}}/events"
|
|
248
|
+
}
|
|
249
|
+
];
|
|
250
|
+
const HTML_ESCAPE_MAP = {
|
|
251
|
+
"&": "&",
|
|
252
|
+
"<": "<",
|
|
253
|
+
">": ">",
|
|
254
|
+
'"': """,
|
|
255
|
+
"'": "'"
|
|
256
|
+
};
|
|
257
|
+
const TELEGRAM_INLINE_TAGS = /* @__PURE__ */ new Set([
|
|
258
|
+
"b",
|
|
259
|
+
"strong",
|
|
260
|
+
"i",
|
|
261
|
+
"em",
|
|
262
|
+
"u",
|
|
263
|
+
"ins",
|
|
264
|
+
"s",
|
|
265
|
+
"strike",
|
|
266
|
+
"del",
|
|
267
|
+
"code",
|
|
268
|
+
"pre",
|
|
269
|
+
"tg-spoiler"
|
|
270
|
+
]);
|
|
271
|
+
function escapeHtml(raw) {
|
|
272
|
+
return String(raw).replace(
|
|
273
|
+
/[&<>"']/g,
|
|
274
|
+
(char) => HTML_ESCAPE_MAP[char] || char
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
function stripHeaderControlChars(raw) {
|
|
278
|
+
return String(raw || "").replace(/[\r\n<>]/g, "").replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
279
|
+
}
|
|
280
|
+
function extractSafeAnchorHref(attrs) {
|
|
281
|
+
const match = attrs.match(
|
|
282
|
+
/\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/i
|
|
283
|
+
);
|
|
284
|
+
const rawHref = (match?.[1] ?? match?.[2] ?? match?.[3] ?? "").replace(/&/gi, "&").trim();
|
|
285
|
+
if (!rawHref || /[\u0000-\u001f\u007f]/.test(rawHref)) return null;
|
|
286
|
+
try {
|
|
287
|
+
const parsed = new URL(rawHref);
|
|
288
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
return parsed.toString();
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function sanitizeTelegramHtml(rawHtml) {
|
|
297
|
+
if (!rawHtml) return "";
|
|
298
|
+
let text = String(rawHtml).replace(/[\u0001\u0002]/g, "").replace(/<script\b[^>]*>[\s\S]*?<\/script\s*>/gi, "").replace(/<style\b[^>]*>[\s\S]*?<\/style\s*>/gi, "").replace(/<br\s*\/?>/gi, "\n").replace(/<h[1-6](?:\s[^>]*)?>/gi, "<b>").replace(/<\/h[1-6]\s*>/gi, "</b>\n").replace(/<p(?:\s[^>]*)?>/gi, "").replace(/<\/p\s*>/gi, "\n\n").replace(/<div(?:\s[^>]*)?>/gi, "").replace(/<\/div\s*>/gi, "\n").replace(/<li(?:\s[^>]*)?>/gi, "• ").replace(/<\/li\s*>/gi, "\n");
|
|
299
|
+
let openAnchors = 0;
|
|
300
|
+
text = text.replace(
|
|
301
|
+
/<(\/?)([a-zA-Z][a-zA-Z0-9-]*)(\s[^<>]*)?(\/?)>/g,
|
|
302
|
+
(_full, slash, rawName, attrs = "", selfClose = "") => {
|
|
303
|
+
const tag = rawName.toLowerCase();
|
|
304
|
+
if (selfClose === "/") return "";
|
|
305
|
+
if (slash === "/") {
|
|
306
|
+
if (TELEGRAM_INLINE_TAGS.has(tag)) return `/${tag}`;
|
|
307
|
+
if (tag === "a" && openAnchors > 0) {
|
|
308
|
+
openAnchors -= 1;
|
|
309
|
+
return "/a";
|
|
310
|
+
}
|
|
311
|
+
return "";
|
|
312
|
+
}
|
|
313
|
+
if (TELEGRAM_INLINE_TAGS.has(tag)) return `${tag}`;
|
|
314
|
+
if (tag === "a") {
|
|
315
|
+
const safeHref = extractSafeAnchorHref(attrs);
|
|
316
|
+
if (!safeHref) return "";
|
|
317
|
+
openAnchors += 1;
|
|
318
|
+
return `a href="${escapeHtml(safeHref)}"`;
|
|
319
|
+
}
|
|
320
|
+
return "";
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
while (openAnchors > 0) {
|
|
324
|
+
text += "/a";
|
|
325
|
+
openAnchors -= 1;
|
|
326
|
+
}
|
|
327
|
+
text = text.replace(/</g, "<").replace(/>/g, ">").replace(/&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-f]+);)/gi, "&").replace(/\u0001([^\u0002]+)\u0002/g, "<$1>").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
328
|
+
return text;
|
|
329
|
+
}
|
|
330
|
+
function interpolateBroadcastTemplate(template, variables, options = {}) {
|
|
331
|
+
if (!template) return "";
|
|
332
|
+
const shouldEscape = Boolean(options.escapeHtmlValues);
|
|
333
|
+
return template.replace(
|
|
334
|
+
/\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g,
|
|
335
|
+
(_match, key) => {
|
|
336
|
+
const rawValue = variables[key];
|
|
337
|
+
if (rawValue === void 0 || rawValue === null) {
|
|
338
|
+
return "";
|
|
339
|
+
}
|
|
340
|
+
const strValue = String(rawValue);
|
|
341
|
+
return shouldEscape ? escapeHtml(strValue) : strValue;
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
function maskEmail(email) {
|
|
346
|
+
const trimmed = String(email || "").trim().toLowerCase();
|
|
347
|
+
const atIndex = trimmed.indexOf("@");
|
|
348
|
+
if (atIndex <= 0) {
|
|
349
|
+
return "***@invalid";
|
|
350
|
+
}
|
|
351
|
+
const local = trimmed.slice(0, atIndex);
|
|
352
|
+
const domain = trimmed.slice(atIndex + 1);
|
|
353
|
+
if (!domain) {
|
|
354
|
+
return "***@invalid";
|
|
355
|
+
}
|
|
356
|
+
const visiblePrefix = local.slice(0, 1);
|
|
357
|
+
return `${visiblePrefix}***@${domain}`;
|
|
358
|
+
}
|
|
359
|
+
function maskPhone(phone) {
|
|
360
|
+
const normalized = normalizeE164Phone(phone);
|
|
361
|
+
if (normalized.length <= 4) {
|
|
362
|
+
return "***";
|
|
363
|
+
}
|
|
364
|
+
const prefix = normalized.startsWith("+") ? normalized.slice(0, 3) : normalized.slice(0, 2);
|
|
365
|
+
const suffix = normalized.slice(-4);
|
|
366
|
+
return `${prefix}***${suffix}`;
|
|
367
|
+
}
|
|
368
|
+
function isWhatsAppGroupOrChannelJid(raw) {
|
|
369
|
+
const trimmed = String(raw || "").trim().toLowerCase();
|
|
370
|
+
return /^[\d-]+@g\.us$/.test(trimmed) || /^[\d-]+@newsletter$/.test(trimmed);
|
|
371
|
+
}
|
|
372
|
+
function normalizeWhatsAppTarget(raw, defaultCountryCode = "57") {
|
|
373
|
+
const trimmed = String(raw || "").trim();
|
|
374
|
+
if (!trimmed) {
|
|
375
|
+
return {
|
|
376
|
+
normalized: "",
|
|
377
|
+
isGroupOrChannel: false,
|
|
378
|
+
chatId: "",
|
|
379
|
+
evolutionNumber: ""
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const lower = trimmed.toLowerCase();
|
|
383
|
+
if (isWhatsAppGroupOrChannelJid(lower)) {
|
|
384
|
+
return {
|
|
385
|
+
normalized: lower,
|
|
386
|
+
isGroupOrChannel: true,
|
|
387
|
+
chatId: lower,
|
|
388
|
+
evolutionNumber: lower
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (lower.endsWith("@c.us") || lower.endsWith("@s.whatsapp.net")) {
|
|
392
|
+
const userPart = lower.split("@")[0];
|
|
393
|
+
const e1642 = normalizeE164Phone(userPart, defaultCountryCode);
|
|
394
|
+
const digits2 = e1642.replace(/^\+/, "");
|
|
395
|
+
return {
|
|
396
|
+
normalized: e1642,
|
|
397
|
+
isGroupOrChannel: false,
|
|
398
|
+
chatId: digits2 ? `${digits2}@c.us` : "",
|
|
399
|
+
evolutionNumber: digits2
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const e164 = normalizeE164Phone(trimmed, defaultCountryCode);
|
|
403
|
+
const digits = e164.replace(/^\+/, "");
|
|
404
|
+
return {
|
|
405
|
+
normalized: e164,
|
|
406
|
+
isGroupOrChannel: false,
|
|
407
|
+
chatId: digits ? `${digits}@c.us` : "",
|
|
408
|
+
evolutionNumber: digits
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function maskRecipientIdentifier(identifier) {
|
|
412
|
+
const trimmed = String(identifier || "").trim();
|
|
413
|
+
if (!trimmed) return "***";
|
|
414
|
+
const lower = trimmed.toLowerCase();
|
|
415
|
+
if (isWhatsAppGroupOrChannelJid(lower)) {
|
|
416
|
+
return lower;
|
|
417
|
+
}
|
|
418
|
+
if (trimmed.includes("@") && !trimmed.startsWith("@")) {
|
|
419
|
+
return maskEmail(trimmed);
|
|
420
|
+
}
|
|
421
|
+
if (trimmed.startsWith("@") || /^-\d+$/.test(trimmed)) {
|
|
422
|
+
return trimmed;
|
|
423
|
+
}
|
|
424
|
+
return maskPhone(trimmed);
|
|
425
|
+
}
|
|
426
|
+
function normalizeE164Phone(phone, defaultCountryCode = "57") {
|
|
427
|
+
const raw = String(phone || "").trim();
|
|
428
|
+
if (!raw) return "";
|
|
429
|
+
if (isWhatsAppGroupOrChannelJid(raw)) {
|
|
430
|
+
return raw.toLowerCase();
|
|
431
|
+
}
|
|
432
|
+
const hasPlus = raw.startsWith("+");
|
|
433
|
+
const digitsOnly = raw.replace(/\D+/g, "");
|
|
434
|
+
if (!digitsOnly) return "";
|
|
435
|
+
if (hasPlus) {
|
|
436
|
+
return `+${digitsOnly}`;
|
|
437
|
+
}
|
|
438
|
+
if (digitsOnly.startsWith("00") && digitsOnly.length > 4) {
|
|
439
|
+
return `+${digitsOnly.slice(2)}`;
|
|
440
|
+
}
|
|
441
|
+
if (digitsOnly.length === 10 && defaultCountryCode) {
|
|
442
|
+
return `+${defaultCountryCode.replace(/\D+/g, "")}${digitsOnly}`;
|
|
443
|
+
}
|
|
444
|
+
return `+${digitsOnly}`;
|
|
445
|
+
}
|
|
446
|
+
function resolveLocalizedCampaignCopy(campaign, locale, defaultLocale = "es") {
|
|
447
|
+
const translations = campaign.translations || {};
|
|
448
|
+
const match = translations[locale] || translations[defaultLocale];
|
|
449
|
+
return {
|
|
450
|
+
subject: match?.subject || campaign.subject,
|
|
451
|
+
bodyText: match?.bodyText || campaign.bodyText,
|
|
452
|
+
bodyHtml: match?.bodyHtml || campaign.bodyHtml,
|
|
453
|
+
ctaLabel: match?.ctaLabel || campaign.ctaLabel,
|
|
454
|
+
ctaUrl: match?.ctaUrl || campaign.ctaUrl
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
458
|
+
const BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
459
|
+
"metadata.google.internal",
|
|
460
|
+
"metadata",
|
|
461
|
+
"100.100.100.200"
|
|
462
|
+
]);
|
|
463
|
+
function parseIpv4Octets(host) {
|
|
464
|
+
const match = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
465
|
+
if (!match) return null;
|
|
466
|
+
const octets = [
|
|
467
|
+
Number(match[1]),
|
|
468
|
+
Number(match[2]),
|
|
469
|
+
Number(match[3]),
|
|
470
|
+
Number(match[4])
|
|
471
|
+
];
|
|
472
|
+
if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
return octets;
|
|
476
|
+
}
|
|
477
|
+
function isBlockedIpv4([a, b, c, d], allowLoopback) {
|
|
478
|
+
if (a === 127) {
|
|
479
|
+
return !(allowLoopback && b === 0 && c === 0 && d === 1);
|
|
480
|
+
}
|
|
481
|
+
if (a === 0 || a === 10 || a >= 224) return true;
|
|
482
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
483
|
+
if (a === 169 && b === 254) return true;
|
|
484
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
485
|
+
if (a === 192 && b === 168) return true;
|
|
486
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
function expandIpv6Groups(rawHost) {
|
|
490
|
+
if (!rawHost.includes(":") || rawHost.includes("%")) return null;
|
|
491
|
+
let candidate = rawHost;
|
|
492
|
+
const lastColon = candidate.lastIndexOf(":");
|
|
493
|
+
const tail = candidate.slice(lastColon + 1);
|
|
494
|
+
if (tail.includes(".")) {
|
|
495
|
+
const v4 = parseIpv4Octets(tail);
|
|
496
|
+
if (!v4) return null;
|
|
497
|
+
const hi = (v4[0] << 8 | v4[1]).toString(16);
|
|
498
|
+
const lo = (v4[2] << 8 | v4[3]).toString(16);
|
|
499
|
+
candidate = `${candidate.slice(0, lastColon)}:${hi}:${lo}`;
|
|
500
|
+
}
|
|
501
|
+
const dblParts = candidate.split("::");
|
|
502
|
+
if (dblParts.length > 2) return null;
|
|
503
|
+
const parsePart = (part) => {
|
|
504
|
+
if (!part) return [];
|
|
505
|
+
const tokens = part.split(":");
|
|
506
|
+
const nums = [];
|
|
507
|
+
for (const tok of tokens) {
|
|
508
|
+
if (!/^[0-9a-f]{1,4}$/i.test(tok)) return null;
|
|
509
|
+
nums.push(Number.parseInt(tok, 16));
|
|
510
|
+
}
|
|
511
|
+
return nums;
|
|
512
|
+
};
|
|
513
|
+
const left = parsePart(dblParts[0]);
|
|
514
|
+
if (!left) return null;
|
|
515
|
+
if (dblParts.length === 1) {
|
|
516
|
+
return left.length === 8 ? left : null;
|
|
517
|
+
}
|
|
518
|
+
const right = parsePart(dblParts[1]);
|
|
519
|
+
if (!right) return null;
|
|
520
|
+
const missing = 8 - (left.length + right.length);
|
|
521
|
+
if (missing < 1) return null;
|
|
522
|
+
return [...left, ...Array.from({ length: missing }, () => 0), ...right];
|
|
523
|
+
}
|
|
524
|
+
function isBlockedIpv6(groups, allowLoopback) {
|
|
525
|
+
const allZeroPrefix = groups.slice(0, 7).every((g) => g === 0);
|
|
526
|
+
if (allZeroPrefix && groups[7] === 0) return true;
|
|
527
|
+
if (allZeroPrefix && groups[7] === 1) return !allowLoopback;
|
|
528
|
+
const first = groups[0];
|
|
529
|
+
if ((first & 65024) === 64512) return true;
|
|
530
|
+
if ((first & 65472) === 65152) return true;
|
|
531
|
+
if ((first & 65280) === 65280) return true;
|
|
532
|
+
if (first === 8194) {
|
|
533
|
+
const embedded6to4 = [
|
|
534
|
+
groups[1] >> 8 & 255,
|
|
535
|
+
groups[1] & 255,
|
|
536
|
+
groups[2] >> 8 & 255,
|
|
537
|
+
groups[2] & 255
|
|
538
|
+
];
|
|
539
|
+
return isBlockedIpv4(embedded6to4, false);
|
|
540
|
+
}
|
|
541
|
+
const isMappedV4 = groups.slice(0, 5).every((g) => g === 0) && (groups[5] === 65535 || groups[5] === 0);
|
|
542
|
+
const isNat64 = groups[0] === 100 && groups[1] === 65435 && groups.slice(2, 6).every((g) => g === 0);
|
|
543
|
+
if (isMappedV4 || isNat64) {
|
|
544
|
+
const mappedQuad = [
|
|
545
|
+
groups[6] >> 8 & 255,
|
|
546
|
+
groups[6] & 255,
|
|
547
|
+
groups[7] >> 8 & 255,
|
|
548
|
+
groups[7] & 255
|
|
549
|
+
];
|
|
550
|
+
return isBlockedIpv4(mappedQuad, false);
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
function normalizeUrlHostname(rawHostname) {
|
|
555
|
+
const lower = rawHostname.toLowerCase();
|
|
556
|
+
let unbracketed = lower.startsWith("[") && lower.endsWith("]") ? lower.slice(1, -1) : lower;
|
|
557
|
+
while (unbracketed.endsWith(".")) {
|
|
558
|
+
unbracketed = unbracketed.slice(0, -1);
|
|
559
|
+
}
|
|
560
|
+
return unbracketed;
|
|
561
|
+
}
|
|
562
|
+
function evaluateGatewayHost(host, allowLocalhost) {
|
|
563
|
+
if (!host) {
|
|
564
|
+
return { isExactLoopback: false, blockedError: "Invalid gateway hostname" };
|
|
565
|
+
}
|
|
566
|
+
const isExactLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
567
|
+
if (BLOCKED_HOSTNAMES.has(host) || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) {
|
|
568
|
+
return {
|
|
569
|
+
isExactLoopback: false,
|
|
570
|
+
blockedError: "Private, loopback, and metadata hosts are forbidden (SSRF protection)"
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (host === "localhost") {
|
|
574
|
+
return {
|
|
575
|
+
isExactLoopback: true,
|
|
576
|
+
blockedError: allowLocalhost ? null : "Localhost gateway URLs are forbidden unless explicitly allowed"
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const v4 = parseIpv4Octets(host);
|
|
580
|
+
if (v4) {
|
|
581
|
+
return {
|
|
582
|
+
isExactLoopback,
|
|
583
|
+
blockedError: isBlockedIpv4(v4, allowLocalhost) ? "Private, loopback, CGNAT, and link-local IPv4 hosts are forbidden" : null
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
if (host.includes(":")) {
|
|
587
|
+
const v6 = expandIpv6Groups(host);
|
|
588
|
+
if (!v6 || isBlockedIpv6(v6, allowLocalhost)) {
|
|
589
|
+
return {
|
|
590
|
+
isExactLoopback,
|
|
591
|
+
blockedError: "Private, loopback, link-local, and mapped IPv6 hosts are forbidden"
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return { isExactLoopback, blockedError: null };
|
|
596
|
+
}
|
|
597
|
+
function validateGatewayUrl(rawUrl, options = {}) {
|
|
598
|
+
const errors = [];
|
|
599
|
+
const trimmed = String(rawUrl || "").trim();
|
|
600
|
+
if (!trimmed) {
|
|
601
|
+
return { valid: false, errors: ["Gateway URL is required"] };
|
|
602
|
+
}
|
|
603
|
+
if (trimmed.includes("\\") || /[\u0000-\u001f\u007f]/.test(trimmed)) {
|
|
604
|
+
return {
|
|
605
|
+
valid: false,
|
|
606
|
+
errors: ["Gateway URL must not contain backslashes or control chars"]
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
let parsed;
|
|
610
|
+
try {
|
|
611
|
+
parsed = new URL(trimmed);
|
|
612
|
+
} catch {
|
|
613
|
+
return { valid: false, errors: ["Invalid URL format"] };
|
|
614
|
+
}
|
|
615
|
+
if (parsed.username || parsed.password) {
|
|
616
|
+
errors.push("Gateway URL must not contain embedded credentials");
|
|
617
|
+
}
|
|
618
|
+
if (parsed.search || parsed.hash) {
|
|
619
|
+
errors.push("Gateway URL must not contain query parameters or fragments");
|
|
620
|
+
}
|
|
621
|
+
const allowLocalhost = Boolean(options.allowLocalhost);
|
|
622
|
+
const host = normalizeUrlHostname(parsed.hostname);
|
|
623
|
+
const { isExactLoopback, blockedError } = evaluateGatewayHost(
|
|
624
|
+
host,
|
|
625
|
+
allowLocalhost
|
|
626
|
+
);
|
|
627
|
+
if (blockedError) errors.push(blockedError);
|
|
628
|
+
if (parsed.protocol === "http:") {
|
|
629
|
+
if (!allowLocalhost || !isExactLoopback) {
|
|
630
|
+
errors.push("HTTPS protocol is required for external gateway URLs");
|
|
631
|
+
}
|
|
632
|
+
} else if (parsed.protocol !== "https:") {
|
|
633
|
+
errors.push(
|
|
634
|
+
"Only HTTPS (or explicitly allowed localhost HTTP) protocol is permitted"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
return { valid: errors.length === 0, errors };
|
|
638
|
+
}
|
|
639
|
+
function isSafeHttpUrl(rawUrl) {
|
|
640
|
+
if (typeof rawUrl !== "string") return false;
|
|
641
|
+
const trimmed = rawUrl.trim();
|
|
642
|
+
if (!trimmed || /[\u0000-\u001f\u007f<>\\]/.test(trimmed)) return false;
|
|
643
|
+
try {
|
|
644
|
+
const parsed = new URL(trimmed);
|
|
645
|
+
if (parsed.username || parsed.password) return false;
|
|
646
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
647
|
+
} catch {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function validatePublicChannelLinks(links) {
|
|
652
|
+
const errors = [];
|
|
653
|
+
if (!Array.isArray(links)) {
|
|
654
|
+
return { valid: false, errors: ["publicChannelLinks must be an array"] };
|
|
655
|
+
}
|
|
656
|
+
for (let i = 0; i < links.length; i += 1) {
|
|
657
|
+
const item = links[i];
|
|
658
|
+
if (!item || typeof item !== "object") {
|
|
659
|
+
errors.push(`publicChannelLinks[${i}] must be an object`);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (!isSafeHttpUrl(item.inviteUrl)) {
|
|
663
|
+
errors.push(
|
|
664
|
+
`publicChannelLinks[${i}].inviteUrl must be a valid http(s) URL`
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { valid: errors.length === 0, errors };
|
|
669
|
+
}
|
|
670
|
+
function normalizeChannels(rawChannels, fallback = ["email"]) {
|
|
671
|
+
if (!Array.isArray(rawChannels)) {
|
|
672
|
+
return [...fallback];
|
|
673
|
+
}
|
|
674
|
+
const filtered = rawChannels.map((c) => String(c).trim().toLowerCase()).filter(
|
|
675
|
+
(c) => SUPPORTED_CHANNELS.includes(c)
|
|
676
|
+
);
|
|
677
|
+
return filtered.length > 0 ? Array.from(new Set(filtered)) : [...fallback];
|
|
678
|
+
}
|
|
679
|
+
function isSupportedWhatsAppDriver(driver) {
|
|
680
|
+
return SUPPORTED_WHATSAPP_DRIVERS.includes(
|
|
681
|
+
String(driver || "").trim()
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const GATEWAY_SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
685
|
+
function isValidGatewaySession(session) {
|
|
686
|
+
return typeof session === "string" && GATEWAY_SESSION_RE.test(session.trim());
|
|
687
|
+
}
|
|
688
|
+
function validateCampaignChannelsList(rawChannels, errors) {
|
|
689
|
+
const channels = Array.isArray(rawChannels) ? rawChannels : [];
|
|
690
|
+
if (channels.length === 0) {
|
|
691
|
+
errors.push("At least one broadcast channel is required");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
for (const ch of channels) {
|
|
695
|
+
const normalized = String(ch).trim().toLowerCase();
|
|
696
|
+
if (!SUPPORTED_CHANNELS.includes(normalized)) {
|
|
697
|
+
errors.push(`Unsupported broadcast channel: ${String(ch)}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
function validateOptionalHttpUrl(urlValue, fieldName, errors) {
|
|
702
|
+
if (!urlValue) return;
|
|
703
|
+
if (!isSafeHttpUrl(urlValue)) {
|
|
704
|
+
errors.push(`${fieldName} must use http or https protocol`);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function validateCampaignInput(input) {
|
|
708
|
+
const errors = [];
|
|
709
|
+
const slug = String(input.slug || "").trim();
|
|
710
|
+
const title = String(input.title || "").trim();
|
|
711
|
+
const subject = String(input.subject || "").trim();
|
|
712
|
+
const bodyText = String(input.bodyText || "").trim();
|
|
713
|
+
if (!slug || !SLUG_REGEX.test(slug)) {
|
|
714
|
+
errors.push("Campaign slug must be lowercase kebab-case");
|
|
715
|
+
}
|
|
716
|
+
if (title.length < 3) {
|
|
717
|
+
errors.push("Campaign title must be at least 3 characters");
|
|
718
|
+
}
|
|
719
|
+
if (subject.length < 3) {
|
|
720
|
+
errors.push("Campaign subject must be at least 3 characters");
|
|
721
|
+
}
|
|
722
|
+
if (bodyText.length < 5) {
|
|
723
|
+
errors.push("Campaign bodyText must be at least 5 characters");
|
|
724
|
+
}
|
|
725
|
+
validateCampaignChannelsList(input.channels, errors);
|
|
726
|
+
validateOptionalHttpUrl(input.ctaUrl, "ctaUrl", errors);
|
|
727
|
+
validateOptionalHttpUrl(input.mediaUrl, "mediaUrl", errors);
|
|
728
|
+
if (input.translations && typeof input.translations === "object") {
|
|
729
|
+
for (const [locale, tr] of Object.entries(input.translations)) {
|
|
730
|
+
if (tr && typeof tr === "object" && tr.ctaUrl) {
|
|
731
|
+
validateOptionalHttpUrl(
|
|
732
|
+
tr.ctaUrl,
|
|
733
|
+
`translations.${locale}.ctaUrl`,
|
|
734
|
+
errors
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return { valid: errors.length === 0, errors };
|
|
740
|
+
}
|
|
741
|
+
function requireNonEmptyFields(fields, errors) {
|
|
742
|
+
for (const [val, errMessage] of fields) {
|
|
743
|
+
if (!String(val || "").trim()) {
|
|
744
|
+
errors.push(errMessage);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function validateCtaUrlTemplate(rawTemplate, errors) {
|
|
749
|
+
if (!rawTemplate) return;
|
|
750
|
+
const trimmed = String(rawTemplate).trim();
|
|
751
|
+
if (!trimmed) return;
|
|
752
|
+
const resolvedProbe = trimmed.replace(/^\{\{\s*siteBaseUrl\s*\}\}/, "https://example.org").replace(/\{\{\s*[a-zA-Z0-9_.-]+\s*\}\}/g, "segment");
|
|
753
|
+
if (!isSafeHttpUrl(resolvedProbe)) {
|
|
754
|
+
errors.push("ctaUrlTemplate must resolve to an http or https URL");
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function validateAutomationRuleInput(input) {
|
|
758
|
+
const errors = [];
|
|
759
|
+
const slug = String(input.slug || "").trim();
|
|
760
|
+
const name = String(input.name || "").trim();
|
|
761
|
+
const eventName = String(input.eventName || "").trim();
|
|
762
|
+
const targetUid = String(input.targetContentType || "").trim();
|
|
763
|
+
if (!slug || !SLUG_REGEX.test(slug)) {
|
|
764
|
+
errors.push("Automation rule slug must be lowercase kebab-case");
|
|
765
|
+
}
|
|
766
|
+
if (name.length < 3) {
|
|
767
|
+
errors.push("Automation rule name must be at least 3 characters");
|
|
768
|
+
}
|
|
769
|
+
requireNonEmptyFields(
|
|
770
|
+
[
|
|
771
|
+
[input.eventName, "eventName is required"],
|
|
772
|
+
[input.subjectTemplate, "subjectTemplate is required"],
|
|
773
|
+
[input.bodyTextTemplate, "bodyTextTemplate is required"]
|
|
774
|
+
],
|
|
775
|
+
errors
|
|
776
|
+
);
|
|
777
|
+
if (eventName.toLowerCase().startsWith("broadcasts.")) {
|
|
778
|
+
errors.push('eventName cannot start with "broadcasts." (loop protection)');
|
|
779
|
+
}
|
|
780
|
+
if (targetUid.toLowerCase().startsWith("plugin::broadcasts.")) {
|
|
781
|
+
errors.push(
|
|
782
|
+
'targetContentType cannot target "plugin::broadcasts.*" (loop protection)'
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
if (input.channels !== void 0) {
|
|
786
|
+
validateCampaignChannelsList(input.channels, errors);
|
|
787
|
+
}
|
|
788
|
+
validateCtaUrlTemplate(input.ctaUrlTemplate, errors);
|
|
789
|
+
return { valid: errors.length === 0, errors };
|
|
790
|
+
}
|
|
791
|
+
Object.defineProperty(exports, "SUPPORTED_INTL_CALENDARS", {
|
|
792
|
+
enumerable: true,
|
|
793
|
+
get: () => shared.SUPPORTED_INTL_CALENDARS
|
|
794
|
+
});
|
|
795
|
+
exports.AUTOMATION_RULE_UID = AUTOMATION_RULE_UID;
|
|
796
|
+
exports.CAMPAIGN_UID = CAMPAIGN_UID;
|
|
797
|
+
exports.DEFAULT_BROADCAST_SETTINGS = DEFAULT_BROADCAST_SETTINGS;
|
|
798
|
+
exports.DEFAULT_BROADCAST_TOPICS = DEFAULT_BROADCAST_TOPICS;
|
|
799
|
+
exports.DELIVERY_LOG_UID = DELIVERY_LOG_UID;
|
|
800
|
+
exports.PLUGIN_ID = PLUGIN_ID;
|
|
801
|
+
exports.STARTER_AUTOMATION_RULES = STARTER_AUTOMATION_RULES;
|
|
802
|
+
exports.STARTER_CAMPAIGN_TEMPLATES = STARTER_CAMPAIGN_TEMPLATES;
|
|
803
|
+
exports.SUPPORTED_CHANNELS = SUPPORTED_CHANNELS;
|
|
804
|
+
exports.SUPPORTED_WHATSAPP_DRIVERS = SUPPORTED_WHATSAPP_DRIVERS;
|
|
805
|
+
exports.escapeHtml = escapeHtml;
|
|
806
|
+
exports.formatSecondaryDate = formatSecondaryDate;
|
|
807
|
+
exports.interpolateBroadcastTemplate = interpolateBroadcastTemplate;
|
|
808
|
+
exports.isSafeHttpUrl = isSafeHttpUrl;
|
|
809
|
+
exports.isSupportedCalendar = isSupportedCalendar;
|
|
810
|
+
exports.isSupportedWhatsAppDriver = isSupportedWhatsAppDriver;
|
|
811
|
+
exports.isValidGatewaySession = isValidGatewaySession;
|
|
812
|
+
exports.isWhatsAppGroupOrChannelJid = isWhatsAppGroupOrChannelJid;
|
|
813
|
+
exports.maskEmail = maskEmail;
|
|
814
|
+
exports.maskPhone = maskPhone;
|
|
815
|
+
exports.maskRecipientIdentifier = maskRecipientIdentifier;
|
|
816
|
+
exports.normalizeChannels = normalizeChannels;
|
|
817
|
+
exports.normalizeE164Phone = normalizeE164Phone;
|
|
818
|
+
exports.normalizeWhatsAppTarget = normalizeWhatsAppTarget;
|
|
819
|
+
exports.resolveLocalizedCampaignCopy = resolveLocalizedCampaignCopy;
|
|
820
|
+
exports.sanitizeTelegramHtml = sanitizeTelegramHtml;
|
|
821
|
+
exports.stripHeaderControlChars = stripHeaderControlChars;
|
|
822
|
+
exports.validateAutomationRuleInput = validateAutomationRuleInput;
|
|
823
|
+
exports.validateCampaignInput = validateCampaignInput;
|
|
824
|
+
exports.validateGatewayUrl = validateGatewayUrl;
|
|
825
|
+
exports.validatePublicChannelLinks = validatePublicChannelLinks;
|