@whitewall/blip-sdk 0.0.173 → 0.0.175
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/README.md +13 -6
- package/dist/cjs/client.js +3 -0
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/index.js +3 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/namespaces/account.js +23 -11
- package/dist/cjs/namespaces/account.js.map +1 -1
- package/dist/cjs/namespaces/analytics.js +54 -0
- package/dist/cjs/namespaces/analytics.js.map +1 -1
- package/dist/cjs/namespaces/desk.js +264 -45
- package/dist/cjs/namespaces/desk.js.map +1 -1
- package/dist/cjs/namespaces/plugins.js +1 -0
- package/dist/cjs/namespaces/plugins.js.map +1 -1
- package/dist/cjs/namespaces/portal.js +45 -0
- package/dist/cjs/namespaces/portal.js.map +1 -1
- package/dist/cjs/namespaces/tunnel.js +46 -0
- package/dist/cjs/namespaces/tunnel.js.map +1 -0
- package/dist/cjs/namespaces/whatsapp.js +2 -23
- package/dist/cjs/namespaces/whatsapp.js.map +1 -1
- package/dist/cjs/sender/multi/multisender.js +28 -9
- package/dist/cjs/sender/multi/multisender.js.map +1 -1
- package/dist/cjs/sender/sessionnegotiator.js +1 -1
- package/dist/cjs/sender/sessionnegotiator.js.map +1 -1
- package/dist/cjs/sender/tcp/tcpsender.js +35 -2
- package/dist/cjs/sender/tcp/tcpsender.js.map +1 -1
- package/dist/cjs/sender/websocket/websocketsender.js +1 -1
- package/dist/cjs/sender/websocket/websocketsender.js.map +1 -1
- package/dist/cjs/types/desk.js.map +1 -1
- package/dist/cjs/types/flow.js +344 -0
- package/dist/cjs/types/flow.js.map +1 -1
- package/dist/cjs/utils/desk.js +65 -0
- package/dist/cjs/utils/desk.js.map +1 -0
- package/dist/cjs/utils/thread.js +84 -0
- package/dist/cjs/utils/thread.js.map +1 -0
- package/dist/cjs/utils/whatsapp.js +268 -0
- package/dist/cjs/utils/whatsapp.js.map +1 -0
- package/dist/esm/client.js +3 -0
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/namespaces/account.js +19 -10
- package/dist/esm/namespaces/account.js.map +1 -1
- package/dist/esm/namespaces/analytics.js +54 -0
- package/dist/esm/namespaces/analytics.js.map +1 -1
- package/dist/esm/namespaces/desk.js +264 -45
- package/dist/esm/namespaces/desk.js.map +1 -1
- package/dist/esm/namespaces/plugins.js +1 -0
- package/dist/esm/namespaces/plugins.js.map +1 -1
- package/dist/esm/namespaces/portal.js +45 -0
- package/dist/esm/namespaces/portal.js.map +1 -1
- package/dist/esm/namespaces/tunnel.js +42 -0
- package/dist/esm/namespaces/tunnel.js.map +1 -0
- package/dist/esm/namespaces/whatsapp.js +2 -23
- package/dist/esm/namespaces/whatsapp.js.map +1 -1
- package/dist/esm/sender/multi/multisender.js +28 -9
- package/dist/esm/sender/multi/multisender.js.map +1 -1
- package/dist/esm/sender/sessionnegotiator.js +1 -1
- package/dist/esm/sender/sessionnegotiator.js.map +1 -1
- package/dist/esm/sender/tcp/tcpsender.js +1 -1
- package/dist/esm/sender/tcp/tcpsender.js.map +1 -1
- package/dist/esm/sender/websocket/websocketsender.js +1 -1
- package/dist/esm/sender/websocket/websocketsender.js.map +1 -1
- package/dist/esm/types/desk.js.map +1 -1
- package/dist/esm/types/flow.js +343 -1
- package/dist/esm/types/flow.js.map +1 -1
- package/dist/esm/utils/desk.js +59 -0
- package/dist/esm/utils/desk.js.map +1 -0
- package/dist/esm/utils/thread.js +80 -0
- package/dist/esm/utils/thread.js.map +1 -0
- package/dist/esm/utils/whatsapp.js +263 -0
- package/dist/esm/utils/whatsapp.js.map +1 -0
- package/dist/types/client.d.ts +2 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/namespaces/account.d.ts +1791 -3
- package/dist/types/namespaces/account.d.ts.map +1 -1
- package/dist/types/namespaces/analytics.d.ts +60 -0
- package/dist/types/namespaces/analytics.d.ts.map +1 -1
- package/dist/types/namespaces/desk.d.ts +124 -4
- package/dist/types/namespaces/desk.d.ts.map +1 -1
- package/dist/types/namespaces/plugins.d.ts +1 -0
- package/dist/types/namespaces/plugins.d.ts.map +1 -1
- package/dist/types/namespaces/portal.d.ts +45 -0
- package/dist/types/namespaces/portal.d.ts.map +1 -1
- package/dist/types/namespaces/tunnel.d.ts +28 -0
- package/dist/types/namespaces/tunnel.d.ts.map +1 -0
- package/dist/types/namespaces/whatsapp.d.ts +3 -2
- package/dist/types/namespaces/whatsapp.d.ts.map +1 -1
- package/dist/types/schemas/webhook.d.ts +2 -2
- package/dist/types/sender/gateway/customgatewaysender.d.ts +1 -1
- package/dist/types/sender/http/httpsender.d.ts +1 -1
- package/dist/types/sender/multi/multisender.d.ts +1 -0
- package/dist/types/sender/multi/multisender.d.ts.map +1 -1
- package/dist/types/sender/sender.d.ts +1 -1
- package/dist/types/sender/tcp/tcpsender.d.ts +1 -1
- package/dist/types/sender/websocket/websocketsender.d.ts +1 -1
- package/dist/types/types/desk.d.ts +57 -1
- package/dist/types/types/desk.d.ts.map +1 -1
- package/dist/types/types/flow.d.ts +3269 -142
- package/dist/types/types/flow.d.ts.map +1 -1
- package/dist/types/types/message.d.ts +1 -1
- package/dist/types/types/whatsapp.d.ts +10 -2
- package/dist/types/types/whatsapp.d.ts.map +1 -1
- package/dist/types/utils/desk.d.ts +6 -0
- package/dist/types/utils/desk.d.ts.map +1 -0
- package/dist/types/utils/thread.d.ts +4 -0
- package/dist/types/utils/thread.d.ts.map +1 -0
- package/dist/types/utils/whatsapp.d.ts +103 -0
- package/dist/types/utils/whatsapp.d.ts.map +1 -0
- package/package.json +8 -11
- package/src/client.ts +3 -0
- package/src/index.ts +3 -0
- package/src/namespaces/account.ts +27 -13
- package/src/namespaces/analytics.ts +154 -0
- package/src/namespaces/desk.ts +428 -77
- package/src/namespaces/plugins.ts +2 -0
- package/src/namespaces/portal.ts +110 -0
- package/src/namespaces/tunnel.ts +58 -0
- package/src/namespaces/whatsapp.ts +4 -39
- package/src/sender/multi/multisender.ts +38 -14
- package/src/sender/sessionnegotiator.ts +1 -1
- package/src/sender/tcp/tcpsender.ts +1 -1
- package/src/sender/websocket/websocketsender.ts +1 -1
- package/src/types/desk.ts +66 -1
- package/src/types/flow.ts +387 -246
- package/src/types/message.ts +1 -1
- package/src/types/whatsapp.ts +11 -2
- package/src/utils/desk.ts +78 -0
- package/src/utils/thread.ts +119 -0
- package/src/utils/whatsapp.ts +530 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import type { MessageTemplate, MessageTemplatePreview, MessageTemplateVariable } from '../types/whatsapp.ts'
|
|
2
|
+
|
|
3
|
+
type TemplateHeaderFormat = NonNullable<MessageTemplatePreview['headerFormat']>
|
|
4
|
+
|
|
5
|
+
type TemplateHeaderComponent = {
|
|
6
|
+
type: 'HEADER'
|
|
7
|
+
format: TemplateHeaderFormat
|
|
8
|
+
text?: string
|
|
9
|
+
example?: {
|
|
10
|
+
header_text?: Array<string>
|
|
11
|
+
header_handle?: Array<string>
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type TemplateBodyComponent = {
|
|
16
|
+
type: 'BODY'
|
|
17
|
+
text: string
|
|
18
|
+
example?: {
|
|
19
|
+
body_text?: [Array<string>]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type TemplateFooterComponent = {
|
|
24
|
+
type: 'FOOTER'
|
|
25
|
+
text: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type TemplateButton = {
|
|
29
|
+
type: string
|
|
30
|
+
text?: string
|
|
31
|
+
url?: string
|
|
32
|
+
phone_number?: string
|
|
33
|
+
example?: Array<string>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type TemplateButtonsComponent = {
|
|
37
|
+
type: 'BUTTONS'
|
|
38
|
+
buttons: Array<TemplateButton>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type TemplateComponent =
|
|
42
|
+
| TemplateHeaderComponent
|
|
43
|
+
| TemplateBodyComponent
|
|
44
|
+
| TemplateFooterComponent
|
|
45
|
+
| TemplateButtonsComponent
|
|
46
|
+
| {
|
|
47
|
+
type: string
|
|
48
|
+
[key: string]: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const placeholderPattern = /{{(\d+)}}/g
|
|
52
|
+
|
|
53
|
+
export type CreateMessageTemplateTextHeaderInput = {
|
|
54
|
+
type: 'HEADER'
|
|
55
|
+
format: 'TEXT'
|
|
56
|
+
text: string
|
|
57
|
+
exampleText?: Array<string>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type CreateMessageTemplateLocationHeaderInput = {
|
|
61
|
+
type: 'HEADER'
|
|
62
|
+
format: 'LOCATION'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type CreateMessageTemplateMediaHeaderInput = {
|
|
66
|
+
type: 'HEADER'
|
|
67
|
+
format: 'IMAGE' | 'VIDEO' | 'DOCUMENT'
|
|
68
|
+
exampleHandle?: string
|
|
69
|
+
exampleUrl?: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type CreateMessageTemplateBodyInput = {
|
|
73
|
+
type: 'BODY'
|
|
74
|
+
text: string
|
|
75
|
+
examples?: Array<string>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type CreateMessageTemplateFooterInput = {
|
|
79
|
+
type: 'FOOTER'
|
|
80
|
+
text: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export type CreateMessageTemplateQuickReplyButtonInput = {
|
|
84
|
+
type: 'QUICK_REPLY'
|
|
85
|
+
text: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type CreateMessageTemplatePhoneNumberButtonInput = {
|
|
89
|
+
type: 'PHONE_NUMBER'
|
|
90
|
+
text: string
|
|
91
|
+
phoneNumber: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type CreateMessageTemplateUrlButtonInput = {
|
|
95
|
+
type: 'URL'
|
|
96
|
+
text: string
|
|
97
|
+
url: string
|
|
98
|
+
example?: Array<string>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type CreateMessageTemplateButtonInput =
|
|
102
|
+
| CreateMessageTemplateQuickReplyButtonInput
|
|
103
|
+
| CreateMessageTemplatePhoneNumberButtonInput
|
|
104
|
+
| CreateMessageTemplateUrlButtonInput
|
|
105
|
+
|
|
106
|
+
export type CreateMessageTemplateButtonsInput = {
|
|
107
|
+
type: 'BUTTONS'
|
|
108
|
+
buttons: Array<CreateMessageTemplateButtonInput>
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type CreateMessageTemplateComponentInput =
|
|
112
|
+
| CreateMessageTemplateTextHeaderInput
|
|
113
|
+
| CreateMessageTemplateLocationHeaderInput
|
|
114
|
+
| CreateMessageTemplateMediaHeaderInput
|
|
115
|
+
| CreateMessageTemplateBodyInput
|
|
116
|
+
| CreateMessageTemplateFooterInput
|
|
117
|
+
| CreateMessageTemplateButtonsInput
|
|
118
|
+
|
|
119
|
+
type MessageTemplatePayloadButton =
|
|
120
|
+
| {
|
|
121
|
+
type: 'QUICK_REPLY'
|
|
122
|
+
text: string
|
|
123
|
+
}
|
|
124
|
+
| {
|
|
125
|
+
type: 'PHONE_NUMBER'
|
|
126
|
+
text: string
|
|
127
|
+
phone_number: string
|
|
128
|
+
}
|
|
129
|
+
| {
|
|
130
|
+
type: 'URL'
|
|
131
|
+
text: string
|
|
132
|
+
url: string
|
|
133
|
+
example?: Array<string>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type MessageTemplatePayloadComponent =
|
|
137
|
+
| {
|
|
138
|
+
type: 'HEADER'
|
|
139
|
+
format: 'TEXT'
|
|
140
|
+
text: string
|
|
141
|
+
example?: {
|
|
142
|
+
header_text: Array<string>
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
| {
|
|
146
|
+
type: 'HEADER'
|
|
147
|
+
format: 'LOCATION'
|
|
148
|
+
}
|
|
149
|
+
| {
|
|
150
|
+
type: 'HEADER'
|
|
151
|
+
format: 'IMAGE' | 'VIDEO' | 'DOCUMENT'
|
|
152
|
+
example: {
|
|
153
|
+
header_handle: Array<string>
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
| {
|
|
157
|
+
type: 'BODY'
|
|
158
|
+
text: string
|
|
159
|
+
example?: {
|
|
160
|
+
body_text: [Array<string>]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
| {
|
|
164
|
+
type: 'FOOTER'
|
|
165
|
+
text: string
|
|
166
|
+
}
|
|
167
|
+
| {
|
|
168
|
+
type: 'BUTTONS'
|
|
169
|
+
buttons: Array<MessageTemplatePayloadButton>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type UploadedMessageTemplateHeaderMedia = {
|
|
173
|
+
componentIndex: number
|
|
174
|
+
format: 'IMAGE' | 'VIDEO' | 'DOCUMENT'
|
|
175
|
+
sourceUrl: string
|
|
176
|
+
fileHandle: string
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function createMessageTemplatePayload(
|
|
180
|
+
input: Pick<MessageTemplate, 'name' | 'category' | 'language'> & {
|
|
181
|
+
components: Array<CreateMessageTemplateComponentInput>
|
|
182
|
+
},
|
|
183
|
+
options?: {
|
|
184
|
+
uploadHeaderMedia?: (url: string) => Promise<{ fileHandle: string }>
|
|
185
|
+
},
|
|
186
|
+
): Promise<{
|
|
187
|
+
template: Pick<MessageTemplate, 'name' | 'category' | 'language' | 'components'>
|
|
188
|
+
uploadedHeaderMedia: Array<UploadedMessageTemplateHeaderMedia>
|
|
189
|
+
}> {
|
|
190
|
+
validateComponentSet(input.components)
|
|
191
|
+
|
|
192
|
+
const uploadedHeaderMedia: Array<UploadedMessageTemplateHeaderMedia> = []
|
|
193
|
+
const components = sortTemplateComponents(
|
|
194
|
+
await Promise.all(
|
|
195
|
+
input.components.map((component, index) =>
|
|
196
|
+
normalizeTemplateComponent(component, index, uploadedHeaderMedia, options?.uploadHeaderMedia),
|
|
197
|
+
),
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
template: {
|
|
203
|
+
name: input.name,
|
|
204
|
+
category: input.category,
|
|
205
|
+
language: input.language,
|
|
206
|
+
components: components as MessageTemplate['components'],
|
|
207
|
+
},
|
|
208
|
+
uploadedHeaderMedia,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function getMessageTemplateVariables(components: Array<TemplateComponent>): Array<MessageTemplateVariable> {
|
|
213
|
+
const variables: Array<MessageTemplateVariable> = []
|
|
214
|
+
|
|
215
|
+
const header = components.find(isMediaHeaderComponent)
|
|
216
|
+
if (header) {
|
|
217
|
+
variables.push({ type: 'MEDIA', example: header.example?.header_handle?.[0] })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const body = components.find(isBodyComponent)
|
|
221
|
+
if (body?.example?.body_text?.[0]) {
|
|
222
|
+
variables.push(
|
|
223
|
+
...body.example.body_text[0].map((example) => ({
|
|
224
|
+
type: 'BODY' as const,
|
|
225
|
+
example,
|
|
226
|
+
})),
|
|
227
|
+
)
|
|
228
|
+
} else if (body) {
|
|
229
|
+
variables.push(...getPlaceholderMatches(body.text).map(() => ({ type: 'BODY' as const })))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const buttons = components.find(isButtonsComponent)
|
|
233
|
+
const urlButtons = buttons?.buttons.filter((button) => button.type === 'URL') ?? []
|
|
234
|
+
for (const button of urlButtons) {
|
|
235
|
+
if (Array.isArray(button.example) && button.example.length > 0) {
|
|
236
|
+
variables.push(
|
|
237
|
+
...button.example.map((example) => ({
|
|
238
|
+
type: 'BUTTON' as const,
|
|
239
|
+
example,
|
|
240
|
+
})),
|
|
241
|
+
)
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (typeof button.url === 'string') {
|
|
246
|
+
variables.push(...getPlaceholderMatches(button.url).map(() => ({ type: 'BUTTON' as const })))
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return variables
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function getMessageTemplatePreview(components: Array<TemplateComponent>): MessageTemplatePreview {
|
|
254
|
+
const header = components.find(isHeaderComponent)
|
|
255
|
+
const body = components.find(isBodyComponent)
|
|
256
|
+
const footer = components.find(isFooterComponent)
|
|
257
|
+
const buttons = components.find(isButtonsComponent)
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
headerFormat: header?.format,
|
|
261
|
+
headerText: getHeaderText(header),
|
|
262
|
+
bodyText: body?.text,
|
|
263
|
+
footerText: footer?.text,
|
|
264
|
+
buttons: buttons?.buttons.map(formatButtonPreview) ?? [],
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function getPlaceholderMatches(text: string): Array<string> {
|
|
269
|
+
return text.match(placeholderPattern) ?? []
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function isHeaderComponent(component: TemplateComponent): component is TemplateHeaderComponent {
|
|
273
|
+
return component.type === 'HEADER'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isMediaHeaderComponent(component: TemplateComponent): component is TemplateHeaderComponent {
|
|
277
|
+
return (
|
|
278
|
+
component.type === 'HEADER' &&
|
|
279
|
+
(component.format === 'IMAGE' || component.format === 'VIDEO' || component.format === 'DOCUMENT')
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isBodyComponent(component: TemplateComponent): component is TemplateBodyComponent {
|
|
284
|
+
return component.type === 'BODY'
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isFooterComponent(component: TemplateComponent): component is TemplateFooterComponent {
|
|
288
|
+
return component.type === 'FOOTER'
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isButtonsComponent(component: TemplateComponent): component is TemplateButtonsComponent {
|
|
292
|
+
return component.type === 'BUTTONS'
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getHeaderText(header?: TemplateHeaderComponent): string | undefined {
|
|
296
|
+
if (!header) {
|
|
297
|
+
return undefined
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (header.format === 'TEXT') {
|
|
301
|
+
return header.text ?? header.example?.header_text?.[0]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (header.format === 'LOCATION') {
|
|
305
|
+
return 'location header'
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return `${header.format.toLowerCase()} header`
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function formatButtonPreview(button: TemplateButton): string {
|
|
312
|
+
const label = typeof button.text === 'string' && button.text.trim().length > 0 ? button.text : button.type
|
|
313
|
+
|
|
314
|
+
if (button.type === 'URL' && typeof button.url === 'string') {
|
|
315
|
+
return `URL: ${label} -> ${button.url}`
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (button.type === 'PHONE_NUMBER' && typeof button.phone_number === 'string') {
|
|
319
|
+
return `PHONE_NUMBER: ${label} -> ${button.phone_number}`
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return `${button.type}: ${label}`
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validateComponentSet(components: Array<CreateMessageTemplateComponentInput>) {
|
|
326
|
+
const counts = components.reduce<Record<CreateMessageTemplateComponentInput['type'], number>>(
|
|
327
|
+
(accumulator, component) => {
|
|
328
|
+
accumulator[component.type] = (accumulator[component.type] ?? 0) + 1
|
|
329
|
+
return accumulator
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
HEADER: 0,
|
|
333
|
+
BODY: 0,
|
|
334
|
+
FOOTER: 0,
|
|
335
|
+
BUTTONS: 0,
|
|
336
|
+
},
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if (counts.BODY !== 1) {
|
|
340
|
+
throw new Error('A WhatsApp message template must include exactly one BODY component.')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (counts.HEADER > 1) {
|
|
344
|
+
throw new Error('A WhatsApp message template can include at most one HEADER component.')
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (counts.FOOTER > 1) {
|
|
348
|
+
throw new Error('A WhatsApp message template can include at most one FOOTER component.')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (counts.BUTTONS > 1) {
|
|
352
|
+
throw new Error('A WhatsApp message template can include at most one BUTTONS component.')
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function normalizeTemplateComponent(
|
|
357
|
+
component: CreateMessageTemplateComponentInput,
|
|
358
|
+
componentIndex: number,
|
|
359
|
+
uploadedHeaderMedia: Array<UploadedMessageTemplateHeaderMedia>,
|
|
360
|
+
uploadHeaderMedia?: (url: string) => Promise<{ fileHandle: string }>,
|
|
361
|
+
): Promise<MessageTemplatePayloadComponent> {
|
|
362
|
+
if (component.type === 'HEADER') {
|
|
363
|
+
if (component.format === 'TEXT') {
|
|
364
|
+
const placeholders = getPlaceholderIndexes(component.text, 'header text')
|
|
365
|
+
assertExamplesMatchPlaceholders(placeholders, component.exampleText, 'header text')
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
type: 'HEADER',
|
|
369
|
+
format: 'TEXT',
|
|
370
|
+
text: component.text,
|
|
371
|
+
...(placeholders.length > 0
|
|
372
|
+
? {
|
|
373
|
+
example: {
|
|
374
|
+
header_text: component.exampleText ?? [],
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
: {}),
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (component.format === 'LOCATION') {
|
|
382
|
+
return {
|
|
383
|
+
type: 'HEADER',
|
|
384
|
+
format: 'LOCATION',
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const sourceCount = Number(Boolean(component.exampleHandle)) + Number(Boolean(component.exampleUrl))
|
|
389
|
+
if (sourceCount !== 1) {
|
|
390
|
+
throw new Error('Provide exactly one of exampleHandle or exampleUrl for media headers.')
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (component.exampleUrl) {
|
|
394
|
+
if (!uploadHeaderMedia) {
|
|
395
|
+
throw new Error('An uploadHeaderMedia callback is required to use exampleUrl in media headers.')
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const attachment = await uploadHeaderMedia(component.exampleUrl)
|
|
399
|
+
uploadedHeaderMedia.push({
|
|
400
|
+
componentIndex,
|
|
401
|
+
format: component.format,
|
|
402
|
+
sourceUrl: component.exampleUrl,
|
|
403
|
+
fileHandle: attachment.fileHandle,
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
type: 'HEADER',
|
|
408
|
+
format: component.format,
|
|
409
|
+
example: {
|
|
410
|
+
header_handle: [attachment.fileHandle],
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
type: 'HEADER',
|
|
417
|
+
format: component.format,
|
|
418
|
+
example: {
|
|
419
|
+
header_handle: [component.exampleHandle as string],
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (component.type === 'BODY') {
|
|
425
|
+
const placeholders = getPlaceholderIndexes(component.text, 'body text')
|
|
426
|
+
assertExamplesMatchPlaceholders(placeholders, component.examples, 'body text')
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
type: 'BODY',
|
|
430
|
+
text: component.text,
|
|
431
|
+
...(placeholders.length > 0
|
|
432
|
+
? {
|
|
433
|
+
example: {
|
|
434
|
+
body_text: [component.examples ?? []],
|
|
435
|
+
},
|
|
436
|
+
}
|
|
437
|
+
: {}),
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (component.type === 'FOOTER') {
|
|
442
|
+
return {
|
|
443
|
+
type: 'FOOTER',
|
|
444
|
+
text: component.text,
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
type: 'BUTTONS',
|
|
450
|
+
buttons: component.buttons.map(normalizeButton),
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizeButton(button: CreateMessageTemplateButtonInput): MessageTemplatePayloadButton {
|
|
455
|
+
if (button.type === 'QUICK_REPLY') {
|
|
456
|
+
return {
|
|
457
|
+
type: 'QUICK_REPLY',
|
|
458
|
+
text: button.text,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (button.type === 'PHONE_NUMBER') {
|
|
463
|
+
return {
|
|
464
|
+
type: 'PHONE_NUMBER',
|
|
465
|
+
text: button.text,
|
|
466
|
+
phone_number: button.phoneNumber,
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const placeholders = getPlaceholderIndexes(button.url, `URL button "${button.text}"`)
|
|
471
|
+
assertExamplesMatchPlaceholders(placeholders, button.example, `URL button "${button.text}"`)
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
type: 'URL',
|
|
475
|
+
text: button.text,
|
|
476
|
+
url: button.url,
|
|
477
|
+
...(placeholders.length > 0 ? { example: button.example ?? [] } : {}),
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function getPlaceholderIndexes(text: string, fieldName: string): Array<number> {
|
|
482
|
+
const indexes = [...text.matchAll(placeholderPattern)].map((match) => Number.parseInt(match[1], 10))
|
|
483
|
+
if (indexes.length === 0) {
|
|
484
|
+
return []
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const uniqueIndexes = [...new Set(indexes)].sort((left, right) => left - right)
|
|
488
|
+
|
|
489
|
+
for (let index = 0; index < uniqueIndexes.length; index += 1) {
|
|
490
|
+
const expected = index + 1
|
|
491
|
+
if (uniqueIndexes[index] !== expected) {
|
|
492
|
+
throw new Error(`Invalid placeholders in ${fieldName}. Variables must be sequential and start at {{1}}.`)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return uniqueIndexes
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function assertExamplesMatchPlaceholders(
|
|
500
|
+
placeholders: Array<number>,
|
|
501
|
+
examples: Array<string> | undefined,
|
|
502
|
+
fieldName: string,
|
|
503
|
+
) {
|
|
504
|
+
if (placeholders.length === 0) {
|
|
505
|
+
if (examples && examples.length > 0) {
|
|
506
|
+
throw new Error(`Examples were provided for ${fieldName}, but the text has no variables.`)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (!examples || examples.length !== placeholders.length) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`Expected ${placeholders.length} example value(s) for ${fieldName} to match its template variables.`,
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function sortTemplateComponents(
|
|
520
|
+
components: Array<MessageTemplatePayloadComponent>,
|
|
521
|
+
): Array<MessageTemplatePayloadComponent> {
|
|
522
|
+
const order: Record<MessageTemplatePayloadComponent['type'], number> = {
|
|
523
|
+
HEADER: 0,
|
|
524
|
+
BODY: 1,
|
|
525
|
+
FOOTER: 2,
|
|
526
|
+
BUTTONS: 3,
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return [...components].sort((left, right) => order[left.type] - order[right.type])
|
|
530
|
+
}
|