better-zap 0.0.1
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 +15 -0
- package/README.md +11 -0
- package/dist/client-ColqW3Zc.d.mts +769 -0
- package/dist/client-D5Lgtacj.d.cts +769 -0
- package/dist/client.cjs +51 -0
- package/dist/client.d.cts +2 -0
- package/dist/client.d.mts +2 -0
- package/dist/client.mjs +50 -0
- package/dist/index.cjs +580 -0
- package/dist/index.d.cts +123 -0
- package/dist/index.d.mts +123 -0
- package/dist/index.mjs +566 -0
- package/package.json +45 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { createZapClient } from "./client.mjs";
|
|
2
|
+
//#region src/logger.ts
|
|
3
|
+
const LOG_LEVEL_ORDER = {
|
|
4
|
+
debug: 0,
|
|
5
|
+
info: 1,
|
|
6
|
+
warn: 2,
|
|
7
|
+
error: 3
|
|
8
|
+
};
|
|
9
|
+
const CONSOLE_METHODS = {
|
|
10
|
+
debug: "debug",
|
|
11
|
+
info: "info",
|
|
12
|
+
warn: "warn",
|
|
13
|
+
error: "error"
|
|
14
|
+
};
|
|
15
|
+
function defaultLog(level, message, context) {
|
|
16
|
+
const entry = {
|
|
17
|
+
level,
|
|
18
|
+
message,
|
|
19
|
+
context,
|
|
20
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
21
|
+
};
|
|
22
|
+
console[CONSOLE_METHODS[level]](JSON.stringify(entry));
|
|
23
|
+
}
|
|
24
|
+
function createLogger(config) {
|
|
25
|
+
if (config?.disabled) return noopLogger;
|
|
26
|
+
const minLevel = LOG_LEVEL_ORDER[config?.level ?? "info"];
|
|
27
|
+
const logFn = config?.log ?? defaultLog;
|
|
28
|
+
function emit(level, message, context) {
|
|
29
|
+
if (LOG_LEVEL_ORDER[level] >= minLevel) logFn(level, message, context ?? {});
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
debug: (msg, ctx) => emit("debug", msg, ctx),
|
|
33
|
+
info: (msg, ctx) => emit("info", msg, ctx),
|
|
34
|
+
warn: (msg, ctx) => emit("warn", msg, ctx),
|
|
35
|
+
error: (msg, ctx) => emit("error", msg, ctx)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const noopLogger = {
|
|
39
|
+
debug() {},
|
|
40
|
+
info() {},
|
|
41
|
+
warn() {},
|
|
42
|
+
error() {}
|
|
43
|
+
};
|
|
44
|
+
function serializeError(err) {
|
|
45
|
+
if (err instanceof Error) return {
|
|
46
|
+
message: err.message,
|
|
47
|
+
name: err.name,
|
|
48
|
+
stack: err.stack
|
|
49
|
+
};
|
|
50
|
+
return { message: String(err) };
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/utils/phone.ts
|
|
54
|
+
/**
|
|
55
|
+
* Formats a phone number to the international format required by Meta Cloud API.
|
|
56
|
+
* Currently defaults to Brazilian country code (55) if not provided.
|
|
57
|
+
* Normalizes Brazilian numbers to always include the 9th digit.
|
|
58
|
+
*/
|
|
59
|
+
function formatPhone(phone) {
|
|
60
|
+
const digits = phone.replace(/\D/g, "");
|
|
61
|
+
if (digits.startsWith("55") && digits.length === 13) return digits;
|
|
62
|
+
if (digits.startsWith("55") && digits.length === 12) return `55${digits.slice(2, 4)}9${digits.slice(4)}`;
|
|
63
|
+
if (digits.length === 11) return `55${digits}`;
|
|
64
|
+
if (digits.length === 10) return `55${digits.slice(0, 2)}9${digits.slice(2)}`;
|
|
65
|
+
throw new Error(`[formatPhone] Cannot normalize phone: "${phone}" (${digits.length} digits). Expected 10–13 digit Brazilian number.`);
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/utils/delay.ts
|
|
69
|
+
/**
|
|
70
|
+
* Utility function to pause execution for a given number of milliseconds.
|
|
71
|
+
* Useful for exponential backoff or rate limiting.
|
|
72
|
+
*/
|
|
73
|
+
function delay(ms) {
|
|
74
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/services/whatsapp.service.ts
|
|
78
|
+
const META_API_VERSION = "v25.0";
|
|
79
|
+
const META_BASE_URL = "https://graph.facebook.com";
|
|
80
|
+
var WhatsAppService = class {
|
|
81
|
+
baseUrl;
|
|
82
|
+
token;
|
|
83
|
+
isDev;
|
|
84
|
+
logger;
|
|
85
|
+
log;
|
|
86
|
+
constructor(config, logger, log) {
|
|
87
|
+
this.baseUrl = `${META_BASE_URL}/${META_API_VERSION}/${config.phoneId}/messages`;
|
|
88
|
+
this.token = config.token;
|
|
89
|
+
this.isDev = config.environment === "development";
|
|
90
|
+
this.logger = logger;
|
|
91
|
+
this.log = log;
|
|
92
|
+
}
|
|
93
|
+
/** Send a text message (within 24h service window only). */
|
|
94
|
+
async sendText(to, body, logging) {
|
|
95
|
+
const hasUrl = /https?:\/\/\S+/i.test(body);
|
|
96
|
+
const payload = {
|
|
97
|
+
messaging_product: "whatsapp",
|
|
98
|
+
recipient_type: "individual",
|
|
99
|
+
to: formatPhone(to),
|
|
100
|
+
type: "text",
|
|
101
|
+
text: hasUrl ? {
|
|
102
|
+
body,
|
|
103
|
+
preview_url: true
|
|
104
|
+
} : { body }
|
|
105
|
+
};
|
|
106
|
+
return this.send(payload, {
|
|
107
|
+
...logging,
|
|
108
|
+
messageType: logging?.messageType || "bot_reply",
|
|
109
|
+
content: body
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/** Send a template message (works outside service window). */
|
|
113
|
+
async sendTemplate(to, templateName, languageCode = "pt_BR", components, logging) {
|
|
114
|
+
const payload = {
|
|
115
|
+
messaging_product: "whatsapp",
|
|
116
|
+
to: formatPhone(to),
|
|
117
|
+
type: "template",
|
|
118
|
+
template: {
|
|
119
|
+
name: templateName,
|
|
120
|
+
language: { code: languageCode },
|
|
121
|
+
...components && { components }
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
return this.send(payload, logging);
|
|
125
|
+
}
|
|
126
|
+
/** Send an interactive message with reply buttons (up to 3). */
|
|
127
|
+
async sendInteractiveButtons(to, bodyText, buttons, logging) {
|
|
128
|
+
const payload = {
|
|
129
|
+
messaging_product: "whatsapp",
|
|
130
|
+
recipient_type: "individual",
|
|
131
|
+
to: formatPhone(to),
|
|
132
|
+
type: "interactive",
|
|
133
|
+
interactive: {
|
|
134
|
+
type: "button",
|
|
135
|
+
body: { text: bodyText },
|
|
136
|
+
action: { buttons: buttons.map((b) => ({
|
|
137
|
+
type: "reply",
|
|
138
|
+
reply: {
|
|
139
|
+
id: b.id,
|
|
140
|
+
title: b.title
|
|
141
|
+
}
|
|
142
|
+
})) }
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
return this.send(payload, {
|
|
146
|
+
...logging,
|
|
147
|
+
messageType: logging?.messageType || "bot_reply",
|
|
148
|
+
content: bodyText
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Send an interactive list message with sections and rows. */
|
|
152
|
+
async sendInteractiveList(to, bodyText, buttonLabel, sections, logging) {
|
|
153
|
+
const payload = {
|
|
154
|
+
messaging_product: "whatsapp",
|
|
155
|
+
recipient_type: "individual",
|
|
156
|
+
to: formatPhone(to),
|
|
157
|
+
type: "interactive",
|
|
158
|
+
interactive: {
|
|
159
|
+
type: "list",
|
|
160
|
+
body: { text: bodyText },
|
|
161
|
+
action: {
|
|
162
|
+
button: buttonLabel,
|
|
163
|
+
sections
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
return this.send(payload, {
|
|
168
|
+
...logging,
|
|
169
|
+
messageType: logging?.messageType || "bot_reply",
|
|
170
|
+
content: bodyText
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/** Send an interactive media carousel message (2-10 cards). */
|
|
174
|
+
async sendInteractiveMediaCarousel(data, logging) {
|
|
175
|
+
const { to, body, cards } = data;
|
|
176
|
+
if (cards.length < 2 || cards.length > 10) return {
|
|
177
|
+
success: false,
|
|
178
|
+
error: "[sendInteractiveMediaCarousel] cards must contain between 2 and 10 items"
|
|
179
|
+
};
|
|
180
|
+
const mappedCards = cards.map((card, index) => ({
|
|
181
|
+
card_index: index,
|
|
182
|
+
type: "cta_url",
|
|
183
|
+
header: {
|
|
184
|
+
type: card.header.type,
|
|
185
|
+
...card.header.type === "image" ? { image: { link: card.header.link } } : { video: { link: card.header.link } }
|
|
186
|
+
},
|
|
187
|
+
...card.bodyText ? { body: { text: card.bodyText } } : {},
|
|
188
|
+
action: {
|
|
189
|
+
name: "cta_url",
|
|
190
|
+
parameters: {
|
|
191
|
+
display_text: card.button.displayText,
|
|
192
|
+
url: card.button.url
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}));
|
|
196
|
+
const payload = {
|
|
197
|
+
messaging_product: "whatsapp",
|
|
198
|
+
recipient_type: "individual",
|
|
199
|
+
to: formatPhone(to),
|
|
200
|
+
type: "interactive",
|
|
201
|
+
interactive: {
|
|
202
|
+
type: "carousel",
|
|
203
|
+
body: { text: body },
|
|
204
|
+
action: { cards: mappedCards }
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
return this.send(payload, {
|
|
208
|
+
...logging,
|
|
209
|
+
messageType: logging?.messageType || "bot_reply",
|
|
210
|
+
content: body
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Send a location pin message. */
|
|
214
|
+
async sendLocation(to, latitude, longitude, name, address, logging) {
|
|
215
|
+
const payload = {
|
|
216
|
+
messaging_product: "whatsapp",
|
|
217
|
+
recipient_type: "individual",
|
|
218
|
+
to: formatPhone(to),
|
|
219
|
+
type: "location",
|
|
220
|
+
location: {
|
|
221
|
+
latitude,
|
|
222
|
+
longitude,
|
|
223
|
+
name,
|
|
224
|
+
address
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
return this.send(payload, {
|
|
228
|
+
...logging,
|
|
229
|
+
messageType: logging?.messageType || "bot_reply",
|
|
230
|
+
content: `[Localização: ${name} - ${address}]`
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Mark an inbound message as read.
|
|
235
|
+
*
|
|
236
|
+
* @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/mark-messages-as-read
|
|
237
|
+
*/
|
|
238
|
+
async markAsRead(messageId) {
|
|
239
|
+
const payload = {
|
|
240
|
+
messaging_product: "whatsapp",
|
|
241
|
+
status: "read",
|
|
242
|
+
message_id: messageId
|
|
243
|
+
};
|
|
244
|
+
if (this.isDev) {
|
|
245
|
+
this.log.debug("whatsapp.dev_send", {
|
|
246
|
+
action: "mark_as_read",
|
|
247
|
+
payload
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
success: true,
|
|
251
|
+
messageId: `dev-${Date.now()}`
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return this.performRequest(payload, 0);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Show or hide a typing indicator in the chat.
|
|
258
|
+
* When starting, the indicator auto-dismisses after 25 seconds or when a message is sent.
|
|
259
|
+
*
|
|
260
|
+
* @see https://developers.facebook.com/docs/whatsapp/cloud-api/typing-indicators/
|
|
261
|
+
*/
|
|
262
|
+
async typingIndicator(messageId, action = "typing_on") {
|
|
263
|
+
const payload = {
|
|
264
|
+
messaging_product: "whatsapp",
|
|
265
|
+
status: "read",
|
|
266
|
+
message_id: messageId,
|
|
267
|
+
typing_indicator: { type: action === "typing_on" ? "text" : void 0 }
|
|
268
|
+
};
|
|
269
|
+
if (this.isDev) {
|
|
270
|
+
this.log.debug("whatsapp.dev_send", {
|
|
271
|
+
action: "typing_indicator",
|
|
272
|
+
typingAction: action,
|
|
273
|
+
payload
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
success: true,
|
|
277
|
+
messageId: `dev-${Date.now()}`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return this.performRequest(payload, 0);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Add a reaction to a message.
|
|
284
|
+
*
|
|
285
|
+
* @see https://developers.facebook.com/docs/whatsapp/cloud-api/messages/reaction-messages
|
|
286
|
+
*/
|
|
287
|
+
async sendReaction(to, messageId, emoji) {
|
|
288
|
+
const payload = {
|
|
289
|
+
messaging_product: "whatsapp",
|
|
290
|
+
recipient_type: "individual",
|
|
291
|
+
to: formatPhone(to),
|
|
292
|
+
type: "reaction",
|
|
293
|
+
reaction: {
|
|
294
|
+
message_id: messageId,
|
|
295
|
+
emoji
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
return this.send(payload);
|
|
299
|
+
}
|
|
300
|
+
/** Core send method with retry logic (2 retries, exponential backoff). */
|
|
301
|
+
async send(payload, logging, retries = 2) {
|
|
302
|
+
if (this.isDev) {
|
|
303
|
+
this.log.debug("whatsapp.dev_send", {
|
|
304
|
+
action: "send",
|
|
305
|
+
payload
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
success: true,
|
|
309
|
+
messageId: `dev-${Date.now()}`
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
const result = await this.performRequest(payload, retries);
|
|
313
|
+
if (logging) try {
|
|
314
|
+
await this.logger.logOutgoing({
|
|
315
|
+
phone: payload.to,
|
|
316
|
+
userId: logging.userId,
|
|
317
|
+
messageType: logging.messageType,
|
|
318
|
+
content: logging.content,
|
|
319
|
+
templateName: payload.type === "template" ? payload.template.name : void 0,
|
|
320
|
+
result,
|
|
321
|
+
metadata: logging.metadata
|
|
322
|
+
});
|
|
323
|
+
} catch (logError) {
|
|
324
|
+
this.log.error("whatsapp.log_failed", serializeError(logError));
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
/** Actually performs the network request with retries. */
|
|
329
|
+
async performRequest(payload, retries) {
|
|
330
|
+
for (let attempt = 0; attempt <= retries; attempt++) try {
|
|
331
|
+
const response = await fetch(this.baseUrl, {
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: {
|
|
334
|
+
Authorization: `Bearer ${this.token}`,
|
|
335
|
+
"Content-Type": "application/json"
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify(payload)
|
|
338
|
+
});
|
|
339
|
+
if (!response.ok) {
|
|
340
|
+
let errorData = null;
|
|
341
|
+
try {
|
|
342
|
+
errorData = await response.json();
|
|
343
|
+
} catch {
|
|
344
|
+
errorData = null;
|
|
345
|
+
}
|
|
346
|
+
if (response.status === 429 && attempt < retries) {
|
|
347
|
+
await delay(1e3 * (attempt + 1));
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (response.status >= 500 && attempt < retries) {
|
|
351
|
+
await delay(500 * (attempt + 1));
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
success: false,
|
|
356
|
+
error: errorData?.error?.message || `HTTP ${response.status}`,
|
|
357
|
+
errorCode: errorData?.error?.code,
|
|
358
|
+
httpStatus: response.status
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
success: true,
|
|
363
|
+
messageId: (await response.json()).messages[0]?.id
|
|
364
|
+
};
|
|
365
|
+
} catch (error) {
|
|
366
|
+
if (attempt < retries) {
|
|
367
|
+
await delay(500 * (attempt + 1));
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
error: error instanceof Error ? error.message : "Network error"
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
return {
|
|
376
|
+
success: false,
|
|
377
|
+
error: "Max retries exceeded"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/services/message-logger.service.ts
|
|
383
|
+
const WHATSAPP_MESSAGE_TYPES = [
|
|
384
|
+
"queue_position",
|
|
385
|
+
"next_in_line",
|
|
386
|
+
"queue_optin",
|
|
387
|
+
"marketing",
|
|
388
|
+
"bot_reply",
|
|
389
|
+
"reminder",
|
|
390
|
+
"satisfaction",
|
|
391
|
+
"incoming"
|
|
392
|
+
];
|
|
393
|
+
var MessageLoggerService = class {
|
|
394
|
+
log;
|
|
395
|
+
constructor(store, log, notifier) {
|
|
396
|
+
this.store = store;
|
|
397
|
+
this.notifier = notifier;
|
|
398
|
+
this.log = log;
|
|
399
|
+
}
|
|
400
|
+
async notify(event) {
|
|
401
|
+
if (!this.notifier) return;
|
|
402
|
+
try {
|
|
403
|
+
await this.notifier.notify(event);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
this.log.error("message_logger.sync_notify_failed", serializeError(err));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if a message with this waMessageId was already processed.
|
|
410
|
+
*/
|
|
411
|
+
async isDuplicate(waMessageId) {
|
|
412
|
+
return !!await this.store.getMessageByWaId(waMessageId);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Log outgoing message for LGPD compliance
|
|
416
|
+
*/
|
|
417
|
+
async logOutgoing(params) {
|
|
418
|
+
const inserted = await this.store.createWhatsAppLog({
|
|
419
|
+
phone: params.phone,
|
|
420
|
+
userId: params.userId,
|
|
421
|
+
direction: "outgoing",
|
|
422
|
+
messageType: params.messageType,
|
|
423
|
+
content: params.content,
|
|
424
|
+
templateName: params.templateName,
|
|
425
|
+
waMessageId: params.result.messageId,
|
|
426
|
+
status: params.result.success ? "sent" : "failed",
|
|
427
|
+
errorMessage: params.result.error,
|
|
428
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
429
|
+
metadata: params.metadata
|
|
430
|
+
});
|
|
431
|
+
const conversation = await this.store.getConversationById(inserted.conversationId);
|
|
432
|
+
if (conversation) await this.notify({
|
|
433
|
+
type: "NEW_MESSAGE",
|
|
434
|
+
message: inserted,
|
|
435
|
+
conversation
|
|
436
|
+
});
|
|
437
|
+
return inserted.id;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Update message status from webhook callback.
|
|
441
|
+
* Only applies if the new status advances the lifecycle (atomic, no race conditions).
|
|
442
|
+
* Returns true if the update was applied, false if skipped.
|
|
443
|
+
*/
|
|
444
|
+
async updateStatus(waMessageId, status, timestamp, errorMessage) {
|
|
445
|
+
const updates = { status };
|
|
446
|
+
if (status === "sent") updates.sentAt = timestamp;
|
|
447
|
+
else if (status === "delivered") updates.deliveredAt = timestamp;
|
|
448
|
+
else if (status === "read") updates.readAt = timestamp;
|
|
449
|
+
else if (status === "failed") updates.errorMessage = errorMessage;
|
|
450
|
+
const updated = await this.store.updateStatusIfProgressed(waMessageId, status, updates);
|
|
451
|
+
if (updated) await this.notify({
|
|
452
|
+
type: "STATUS_UPDATE",
|
|
453
|
+
waMessageId,
|
|
454
|
+
status,
|
|
455
|
+
timestamp,
|
|
456
|
+
deliveredAt: updates.deliveredAt,
|
|
457
|
+
readAt: updates.readAt
|
|
458
|
+
});
|
|
459
|
+
return updated;
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Check if there's a recent outgoing message to this phone within N hours.
|
|
463
|
+
*
|
|
464
|
+
* @param {string} phone : The phone number to check (in E.164 format)
|
|
465
|
+
* @param {number} [withinHours=24] : Time in hours to look for incoming messages
|
|
466
|
+
*/
|
|
467
|
+
async hasRecentOutgoingMessage(phone, withinHours = 24) {
|
|
468
|
+
return this.store.hasRecentOutgoingMessage(phone, withinHours);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Log incoming message (for audit trail)
|
|
472
|
+
*/
|
|
473
|
+
async logIncoming(params) {
|
|
474
|
+
const inserted = await this.store.createWhatsAppLog({
|
|
475
|
+
phone: params.phone,
|
|
476
|
+
contactName: params.senderName,
|
|
477
|
+
waMessageId: params.waMessageId,
|
|
478
|
+
direction: "incoming",
|
|
479
|
+
messageType: "incoming",
|
|
480
|
+
content: params.content,
|
|
481
|
+
status: "delivered",
|
|
482
|
+
metadata: params.metadata,
|
|
483
|
+
sentAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
484
|
+
});
|
|
485
|
+
const conversation = await this.store.getConversationById(inserted.conversationId);
|
|
486
|
+
if (conversation) await this.notify({
|
|
487
|
+
type: "NEW_MESSAGE",
|
|
488
|
+
message: inserted,
|
|
489
|
+
conversation
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/template-registry.ts
|
|
495
|
+
const EMPTY_TEMPLATE_REGISTRY = defineTemplates({});
|
|
496
|
+
function defineTemplates(templates) {
|
|
497
|
+
return templates;
|
|
498
|
+
}
|
|
499
|
+
function hasConfiguredTemplates(templates) {
|
|
500
|
+
return Object.keys(templates).length > 0;
|
|
501
|
+
}
|
|
502
|
+
function getTemplateNames(templates) {
|
|
503
|
+
return Object.keys(templates);
|
|
504
|
+
}
|
|
505
|
+
function serializeTemplateFromRegistry(templates, templateName, options) {
|
|
506
|
+
const templateDefinition = templates[templateName];
|
|
507
|
+
if (!templateDefinition) throw new Error(`[betterZap] Template "${String(templateName)}" is not configured.`);
|
|
508
|
+
const params = options.params ?? {};
|
|
509
|
+
const components = templateDefinition.components ?? [];
|
|
510
|
+
const expectedParameterNames = components.flatMap((component) => component.parameters.map((parameter) => parameter.name));
|
|
511
|
+
const unexpectedParameterNames = Object.keys(params).filter((parameterName) => !expectedParameterNames.includes(parameterName));
|
|
512
|
+
if (unexpectedParameterNames.length > 0) throw new Error(`[betterZap] Unexpected template params for "${String(templateName)}": ${unexpectedParameterNames.join(", ")}`);
|
|
513
|
+
const serializedComponents = components.filter((component) => component.parameters.length > 0).map((component) => ({
|
|
514
|
+
type: component.type,
|
|
515
|
+
...component.type === "button" ? {
|
|
516
|
+
sub_type: component.subType,
|
|
517
|
+
index: component.index
|
|
518
|
+
} : {},
|
|
519
|
+
parameters: component.parameters.map((parameter) => {
|
|
520
|
+
if (!(parameter.name in params)) throw new Error(`[betterZap] Missing template param "${parameter.name}" for "${String(templateName)}".`);
|
|
521
|
+
return serializeTemplateParameter(parameter, params[parameter.name]);
|
|
522
|
+
})
|
|
523
|
+
}));
|
|
524
|
+
return {
|
|
525
|
+
language: options.language ?? templateDefinition.language,
|
|
526
|
+
components: serializedComponents.length > 0 ? serializedComponents : void 0
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function serializeTemplateParameter(parameter, value) {
|
|
530
|
+
switch (parameter.type) {
|
|
531
|
+
case "text": return {
|
|
532
|
+
type: "text",
|
|
533
|
+
text: value
|
|
534
|
+
};
|
|
535
|
+
case "payload": return {
|
|
536
|
+
type: "payload",
|
|
537
|
+
payload: value
|
|
538
|
+
};
|
|
539
|
+
case "location": return {
|
|
540
|
+
type: "location",
|
|
541
|
+
location: value
|
|
542
|
+
};
|
|
543
|
+
case "image": return {
|
|
544
|
+
type: "image",
|
|
545
|
+
image: value
|
|
546
|
+
};
|
|
547
|
+
case "video": return {
|
|
548
|
+
type: "video",
|
|
549
|
+
video: value
|
|
550
|
+
};
|
|
551
|
+
case "document": return {
|
|
552
|
+
type: "document",
|
|
553
|
+
document: value
|
|
554
|
+
};
|
|
555
|
+
case "currency": return {
|
|
556
|
+
type: "currency",
|
|
557
|
+
currency: value
|
|
558
|
+
};
|
|
559
|
+
case "date_time": return {
|
|
560
|
+
type: "date_time",
|
|
561
|
+
date_time: value
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
//#endregion
|
|
566
|
+
export { EMPTY_TEMPLATE_REGISTRY, MessageLoggerService, WHATSAPP_MESSAGE_TYPES, WhatsAppService, createLogger, createZapClient, defineTemplates, delay, formatPhone, getTemplateNames, hasConfiguredTemplates, noopLogger, serializeError, serializeTemplateFromRegistry };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "better-zap",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Framework-agnostic Better Zap core for typed WhatsApp integrations.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.mts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"import": "./dist/index.mjs",
|
|
19
|
+
"require": "./dist/index.cjs"
|
|
20
|
+
},
|
|
21
|
+
"./client": {
|
|
22
|
+
"types": "./dist/client.d.mts",
|
|
23
|
+
"import": "./dist/client.mjs",
|
|
24
|
+
"require": "./dist/client.cjs"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^25.4.0",
|
|
36
|
+
"tsdown": "^0.21.2",
|
|
37
|
+
"typescript": "^5.9.3",
|
|
38
|
+
"vitest": "^4.1.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run"
|
|
44
|
+
}
|
|
45
|
+
}
|