better-zap 0.0.1 → 0.0.3
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/dist/{client-ColqW3Zc.d.mts → client-CRwYOHIG.d.cts} +38 -17
- package/dist/{client-D5Lgtacj.d.cts → client-s6x3lYea.d.mts} +38 -17
- package/dist/client.cjs +25 -3
- package/dist/client.d.cts +2 -2
- package/dist/client.d.mts +2 -2
- package/dist/client.mjs +25 -4
- package/dist/index.cjs +122 -11
- package/dist/index.d.cts +10 -2
- package/dist/index.d.mts +10 -2
- package/dist/index.mjs +117 -13
- package/package.json +2 -2
|
@@ -317,9 +317,16 @@ interface SendResult {
|
|
|
317
317
|
messageId?: string;
|
|
318
318
|
error?: string;
|
|
319
319
|
errorCode?: number;
|
|
320
|
+
code?: string;
|
|
320
321
|
httpStatus?: number;
|
|
322
|
+
details?: Record<string, any>;
|
|
321
323
|
}
|
|
322
|
-
type
|
|
324
|
+
type FreeformMessageWindow = {
|
|
325
|
+
isOpen: boolean;
|
|
326
|
+
lastIncomingMessageAt: string | null;
|
|
327
|
+
expiresAt: string | null;
|
|
328
|
+
};
|
|
329
|
+
type ConversationRecord = {
|
|
323
330
|
id: string;
|
|
324
331
|
phone: string;
|
|
325
332
|
contactName: string | null;
|
|
@@ -331,6 +338,9 @@ type Conversation = {
|
|
|
331
338
|
messageCount: number;
|
|
332
339
|
lastIncomingMessageAt: string | null;
|
|
333
340
|
};
|
|
341
|
+
type Conversation = ConversationRecord & {
|
|
342
|
+
freeformMessageWindow: FreeformMessageWindow;
|
|
343
|
+
};
|
|
334
344
|
type UIMessageStatus = "sent" | "delivered" | "read" | "failed";
|
|
335
345
|
type UIMessage = {
|
|
336
346
|
id: string;
|
|
@@ -452,19 +462,9 @@ interface WhatsAppLogStore {
|
|
|
452
462
|
* Progression: sent(1) → delivered(2) → read(3). failed(4) always wins.
|
|
453
463
|
*/
|
|
454
464
|
updateStatusIfProgressed(waMessageId: string, newStatus: WhatsAppStatus, updates: Partial<WhatsAppLogRecord>): Promise<boolean>;
|
|
455
|
-
getConversationById(conversationId: string): Promise<
|
|
456
|
-
getConversationByPhone(phone: string): Promise<
|
|
457
|
-
getConversations(): Promise<
|
|
458
|
-
id: string;
|
|
459
|
-
phone: string;
|
|
460
|
-
contactName: string | null;
|
|
461
|
-
unreadCount: number;
|
|
462
|
-
status: string;
|
|
463
|
-
lastMessageAt: string;
|
|
464
|
-
lastMessagePreview: string | null;
|
|
465
|
-
lastDirection: string;
|
|
466
|
-
messageCount: number;
|
|
467
|
-
}>>;
|
|
465
|
+
getConversationById(conversationId: string): Promise<ConversationRecord | null>;
|
|
466
|
+
getConversationByPhone(phone: string): Promise<ConversationRecord | null>;
|
|
467
|
+
getConversations(): Promise<ConversationRecord[]>;
|
|
468
468
|
getMessagesByConversationPaginated(conversationId: string, cursor?: string | null, limit?: number): Promise<Array<{
|
|
469
469
|
id: string;
|
|
470
470
|
phone?: string;
|
|
@@ -478,7 +478,7 @@ interface WhatsAppLogStore {
|
|
|
478
478
|
}>>;
|
|
479
479
|
/**
|
|
480
480
|
* Check if there's a recent outgoing message to this phone within N hours.
|
|
481
|
-
* Used
|
|
481
|
+
* Used for consumer-defined cooldown checks.
|
|
482
482
|
*/
|
|
483
483
|
hasRecentOutgoingMessage(phone: string, withinHours: number): Promise<boolean>;
|
|
484
484
|
}
|
|
@@ -491,6 +491,12 @@ declare class MessageLoggerService {
|
|
|
491
491
|
private log;
|
|
492
492
|
constructor(store: WhatsAppLogStore, log: Logger, notifier?: MessageLoggerNotifier | undefined);
|
|
493
493
|
private notify;
|
|
494
|
+
getConversationById(conversationId: string): Promise<Conversation | null>;
|
|
495
|
+
getConversationByPhone(phone: string): Promise<Conversation | null>;
|
|
496
|
+
getConversations(): Promise<Conversation[]>;
|
|
497
|
+
/** @deprecated Prefer `getFreeformMessageWindow()`. */
|
|
498
|
+
getCustomerCareWindow(phone: string): Promise<FreeformMessageWindow>;
|
|
499
|
+
getFreeformMessageWindow(phone: string): Promise<FreeformMessageWindow>;
|
|
494
500
|
/**
|
|
495
501
|
* Check if a message with this waMessageId was already processed.
|
|
496
502
|
*/
|
|
@@ -527,6 +533,7 @@ declare class MessageLoggerService {
|
|
|
527
533
|
phone: string;
|
|
528
534
|
waMessageId: string;
|
|
529
535
|
content: string;
|
|
536
|
+
sentAt: string;
|
|
530
537
|
senderName?: string;
|
|
531
538
|
metadata?: Record<string, unknown>;
|
|
532
539
|
}): Promise<void>;
|
|
@@ -548,7 +555,7 @@ declare class WhatsAppService {
|
|
|
548
555
|
private logger;
|
|
549
556
|
private log;
|
|
550
557
|
constructor(config: WhatsAppConfig, logger: MessageLoggerService, log: Logger);
|
|
551
|
-
/** Send a text message
|
|
558
|
+
/** Send a text message within the 24h free-form message window only. */
|
|
552
559
|
sendText(to: string, body: string, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
|
|
553
560
|
/** Send a template message (works outside service window). */
|
|
554
561
|
sendTemplate(to: string, templateName: string, languageCode?: string, components?: TemplateComponent[], logging?: OutgoingLoggingMetadata): Promise<SendResult>;
|
|
@@ -591,6 +598,7 @@ declare class WhatsAppService {
|
|
|
591
598
|
sendReaction(to: string, messageId: string, emoji: string): Promise<SendResult>;
|
|
592
599
|
/** Core send method with retry logic (2 retries, exponential backoff). */
|
|
593
600
|
private send;
|
|
601
|
+
private logSendResult;
|
|
594
602
|
/** Actually performs the network request with retries. */
|
|
595
603
|
private performRequest;
|
|
596
604
|
}
|
|
@@ -627,6 +635,7 @@ type TemplateParameterInputMap = {
|
|
|
627
635
|
type SupportedTemplateParameterType = keyof TemplateParameterInputMap;
|
|
628
636
|
interface TemplateParameterDefinition<TType extends SupportedTemplateParameterType = SupportedTemplateParameterType> {
|
|
629
637
|
name: string;
|
|
638
|
+
parameterName?: string;
|
|
630
639
|
type: TType;
|
|
631
640
|
}
|
|
632
641
|
type TemplateComponentDefinition = {
|
|
@@ -692,6 +701,18 @@ interface ZapClientOptions<TTemplates extends TemplateRegistry = {}> {
|
|
|
692
701
|
*/
|
|
693
702
|
templates?: TTemplates;
|
|
694
703
|
}
|
|
704
|
+
declare class BetterZapClientError extends Error {
|
|
705
|
+
status: number;
|
|
706
|
+
code?: string;
|
|
707
|
+
details?: unknown;
|
|
708
|
+
body?: unknown;
|
|
709
|
+
constructor(message: string, options: {
|
|
710
|
+
status: number;
|
|
711
|
+
code?: string;
|
|
712
|
+
details?: unknown;
|
|
713
|
+
body?: unknown;
|
|
714
|
+
});
|
|
715
|
+
}
|
|
695
716
|
interface SendTextParams {
|
|
696
717
|
to: string;
|
|
697
718
|
body: string;
|
|
@@ -766,4 +787,4 @@ interface ZapClient<TTemplates extends TemplateRegistry = {}> {
|
|
|
766
787
|
}
|
|
767
788
|
declare function createZapClient<TTemplates extends TemplateRegistry = {}>(options?: ZapClientOptions<TTemplates>): ZapClient<TTemplates>;
|
|
768
789
|
//#endregion
|
|
769
|
-
export {
|
|
790
|
+
export { UIMessage as $, createLogger as A, ConversationRecord as B, WhatsAppLogRecord as C, LogLevel as D, WhatsAppStatus as E, NewMessageEvent as F, MessageError as G, IncomingMessage as H, StatusUpdateEvent as I, SendMessageError as J, MessageStatus as K, SyncEvent as L, serializeError as M, ConversationSummary as N, Logger as O, ConversationUpdateEvent as P, TemplateParameter as Q, WhatsAppConfig as R, WhatsAppDirection as S, WhatsAppMessageType as T, InteractiveMediaCarouselCardInput as U, FreeformMessageWindow as V, MediaMessage as W, SendResult as X, SendMessageResponse as Y, TemplateComponent as Z, OutgoingLoggingMetadata as _, SupportedTemplateParameterType as a, WebhookPayload as at, MessageLoggerService as b, TemplateName as c, WhatsAppInteractiveButtonsMessage as ct, TemplateParams as d, WhatsAppLocationMessage as dt, UIMessageStatus as et, TemplateRegistry as f, WhatsAppTemplateMessage as ft, serializeTemplateFromRegistry as g, hasConfiguredTemplates as h, EMPTY_TEMPLATE_REGISTRY as i, WebhookError as it, noopLogger as j, LoggerConfig as k, TemplateParameterDefinition as l, WhatsAppInteractiveListMessage as lt, getTemplateNames as m, ZapClient as n, WebhookContact as nt, TemplateComponentDefinition as o, WebhookValue as ot, defineTemplates as p, WhatsAppTextMessage as pt, SendInteractiveMediaCarouselData as q, createZapClient as r, WebhookEntry as rt, TemplateDefinition as s, WhatsAppCarouselCard as st, BetterZapClientError as t, WebhookChange as tt, TemplateParameterInputMap as u, WhatsAppInteractiveMediaCarouselMessage as ut, WhatsAppService as v, WhatsAppLogStore as w, WHATSAPP_MESSAGE_TYPES as x, MessageLoggerNotifier as y, Conversation as z };
|
|
@@ -317,9 +317,16 @@ interface SendResult {
|
|
|
317
317
|
messageId?: string;
|
|
318
318
|
error?: string;
|
|
319
319
|
errorCode?: number;
|
|
320
|
+
code?: string;
|
|
320
321
|
httpStatus?: number;
|
|
322
|
+
details?: Record<string, any>;
|
|
321
323
|
}
|
|
322
|
-
type
|
|
324
|
+
type FreeformMessageWindow = {
|
|
325
|
+
isOpen: boolean;
|
|
326
|
+
lastIncomingMessageAt: string | null;
|
|
327
|
+
expiresAt: string | null;
|
|
328
|
+
};
|
|
329
|
+
type ConversationRecord = {
|
|
323
330
|
id: string;
|
|
324
331
|
phone: string;
|
|
325
332
|
contactName: string | null;
|
|
@@ -331,6 +338,9 @@ type Conversation = {
|
|
|
331
338
|
messageCount: number;
|
|
332
339
|
lastIncomingMessageAt: string | null;
|
|
333
340
|
};
|
|
341
|
+
type Conversation = ConversationRecord & {
|
|
342
|
+
freeformMessageWindow: FreeformMessageWindow;
|
|
343
|
+
};
|
|
334
344
|
type UIMessageStatus = "sent" | "delivered" | "read" | "failed";
|
|
335
345
|
type UIMessage = {
|
|
336
346
|
id: string;
|
|
@@ -452,19 +462,9 @@ interface WhatsAppLogStore {
|
|
|
452
462
|
* Progression: sent(1) → delivered(2) → read(3). failed(4) always wins.
|
|
453
463
|
*/
|
|
454
464
|
updateStatusIfProgressed(waMessageId: string, newStatus: WhatsAppStatus, updates: Partial<WhatsAppLogRecord>): Promise<boolean>;
|
|
455
|
-
getConversationById(conversationId: string): Promise<
|
|
456
|
-
getConversationByPhone(phone: string): Promise<
|
|
457
|
-
getConversations(): Promise<
|
|
458
|
-
id: string;
|
|
459
|
-
phone: string;
|
|
460
|
-
contactName: string | null;
|
|
461
|
-
unreadCount: number;
|
|
462
|
-
status: string;
|
|
463
|
-
lastMessageAt: string;
|
|
464
|
-
lastMessagePreview: string | null;
|
|
465
|
-
lastDirection: string;
|
|
466
|
-
messageCount: number;
|
|
467
|
-
}>>;
|
|
465
|
+
getConversationById(conversationId: string): Promise<ConversationRecord | null>;
|
|
466
|
+
getConversationByPhone(phone: string): Promise<ConversationRecord | null>;
|
|
467
|
+
getConversations(): Promise<ConversationRecord[]>;
|
|
468
468
|
getMessagesByConversationPaginated(conversationId: string, cursor?: string | null, limit?: number): Promise<Array<{
|
|
469
469
|
id: string;
|
|
470
470
|
phone?: string;
|
|
@@ -478,7 +478,7 @@ interface WhatsAppLogStore {
|
|
|
478
478
|
}>>;
|
|
479
479
|
/**
|
|
480
480
|
* Check if there's a recent outgoing message to this phone within N hours.
|
|
481
|
-
* Used
|
|
481
|
+
* Used for consumer-defined cooldown checks.
|
|
482
482
|
*/
|
|
483
483
|
hasRecentOutgoingMessage(phone: string, withinHours: number): Promise<boolean>;
|
|
484
484
|
}
|
|
@@ -491,6 +491,12 @@ declare class MessageLoggerService {
|
|
|
491
491
|
private log;
|
|
492
492
|
constructor(store: WhatsAppLogStore, log: Logger, notifier?: MessageLoggerNotifier | undefined);
|
|
493
493
|
private notify;
|
|
494
|
+
getConversationById(conversationId: string): Promise<Conversation | null>;
|
|
495
|
+
getConversationByPhone(phone: string): Promise<Conversation | null>;
|
|
496
|
+
getConversations(): Promise<Conversation[]>;
|
|
497
|
+
/** @deprecated Prefer `getFreeformMessageWindow()`. */
|
|
498
|
+
getCustomerCareWindow(phone: string): Promise<FreeformMessageWindow>;
|
|
499
|
+
getFreeformMessageWindow(phone: string): Promise<FreeformMessageWindow>;
|
|
494
500
|
/**
|
|
495
501
|
* Check if a message with this waMessageId was already processed.
|
|
496
502
|
*/
|
|
@@ -527,6 +533,7 @@ declare class MessageLoggerService {
|
|
|
527
533
|
phone: string;
|
|
528
534
|
waMessageId: string;
|
|
529
535
|
content: string;
|
|
536
|
+
sentAt: string;
|
|
530
537
|
senderName?: string;
|
|
531
538
|
metadata?: Record<string, unknown>;
|
|
532
539
|
}): Promise<void>;
|
|
@@ -548,7 +555,7 @@ declare class WhatsAppService {
|
|
|
548
555
|
private logger;
|
|
549
556
|
private log;
|
|
550
557
|
constructor(config: WhatsAppConfig, logger: MessageLoggerService, log: Logger);
|
|
551
|
-
/** Send a text message
|
|
558
|
+
/** Send a text message within the 24h free-form message window only. */
|
|
552
559
|
sendText(to: string, body: string, logging?: Omit<OutgoingLoggingMetadata, "content">): Promise<SendResult>;
|
|
553
560
|
/** Send a template message (works outside service window). */
|
|
554
561
|
sendTemplate(to: string, templateName: string, languageCode?: string, components?: TemplateComponent[], logging?: OutgoingLoggingMetadata): Promise<SendResult>;
|
|
@@ -591,6 +598,7 @@ declare class WhatsAppService {
|
|
|
591
598
|
sendReaction(to: string, messageId: string, emoji: string): Promise<SendResult>;
|
|
592
599
|
/** Core send method with retry logic (2 retries, exponential backoff). */
|
|
593
600
|
private send;
|
|
601
|
+
private logSendResult;
|
|
594
602
|
/** Actually performs the network request with retries. */
|
|
595
603
|
private performRequest;
|
|
596
604
|
}
|
|
@@ -627,6 +635,7 @@ type TemplateParameterInputMap = {
|
|
|
627
635
|
type SupportedTemplateParameterType = keyof TemplateParameterInputMap;
|
|
628
636
|
interface TemplateParameterDefinition<TType extends SupportedTemplateParameterType = SupportedTemplateParameterType> {
|
|
629
637
|
name: string;
|
|
638
|
+
parameterName?: string;
|
|
630
639
|
type: TType;
|
|
631
640
|
}
|
|
632
641
|
type TemplateComponentDefinition = {
|
|
@@ -692,6 +701,18 @@ interface ZapClientOptions<TTemplates extends TemplateRegistry = {}> {
|
|
|
692
701
|
*/
|
|
693
702
|
templates?: TTemplates;
|
|
694
703
|
}
|
|
704
|
+
declare class BetterZapClientError extends Error {
|
|
705
|
+
status: number;
|
|
706
|
+
code?: string;
|
|
707
|
+
details?: unknown;
|
|
708
|
+
body?: unknown;
|
|
709
|
+
constructor(message: string, options: {
|
|
710
|
+
status: number;
|
|
711
|
+
code?: string;
|
|
712
|
+
details?: unknown;
|
|
713
|
+
body?: unknown;
|
|
714
|
+
});
|
|
715
|
+
}
|
|
695
716
|
interface SendTextParams {
|
|
696
717
|
to: string;
|
|
697
718
|
body: string;
|
|
@@ -766,4 +787,4 @@ interface ZapClient<TTemplates extends TemplateRegistry = {}> {
|
|
|
766
787
|
}
|
|
767
788
|
declare function createZapClient<TTemplates extends TemplateRegistry = {}>(options?: ZapClientOptions<TTemplates>): ZapClient<TTemplates>;
|
|
768
789
|
//#endregion
|
|
769
|
-
export {
|
|
790
|
+
export { UIMessage as $, createLogger as A, ConversationRecord as B, WhatsAppLogRecord as C, LogLevel as D, WhatsAppStatus as E, NewMessageEvent as F, MessageError as G, IncomingMessage as H, StatusUpdateEvent as I, SendMessageError as J, MessageStatus as K, SyncEvent as L, serializeError as M, ConversationSummary as N, Logger as O, ConversationUpdateEvent as P, TemplateParameter as Q, WhatsAppConfig as R, WhatsAppDirection as S, WhatsAppMessageType as T, InteractiveMediaCarouselCardInput as U, FreeformMessageWindow as V, MediaMessage as W, SendResult as X, SendMessageResponse as Y, TemplateComponent as Z, OutgoingLoggingMetadata as _, SupportedTemplateParameterType as a, WebhookPayload as at, MessageLoggerService as b, TemplateName as c, WhatsAppInteractiveButtonsMessage as ct, TemplateParams as d, WhatsAppLocationMessage as dt, UIMessageStatus as et, TemplateRegistry as f, WhatsAppTemplateMessage as ft, serializeTemplateFromRegistry as g, hasConfiguredTemplates as h, EMPTY_TEMPLATE_REGISTRY as i, WebhookError as it, noopLogger as j, LoggerConfig as k, TemplateParameterDefinition as l, WhatsAppInteractiveListMessage as lt, getTemplateNames as m, ZapClient as n, WebhookContact as nt, TemplateComponentDefinition as o, WebhookValue as ot, defineTemplates as p, WhatsAppTextMessage as pt, SendInteractiveMediaCarouselData as q, createZapClient as r, WebhookEntry as rt, TemplateDefinition as s, WhatsAppCarouselCard as st, BetterZapClientError as t, WebhookChange as tt, TemplateParameterInputMap as u, WhatsAppInteractiveMediaCarouselMessage as ut, WhatsAppService as v, WhatsAppLogStore as w, WHATSAPP_MESSAGE_TYPES as x, MessageLoggerNotifier as y, Conversation as z };
|
package/dist/client.cjs
CHANGED
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
//#region src/client.ts
|
|
3
|
+
var BetterZapClientError = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
details;
|
|
7
|
+
body;
|
|
8
|
+
constructor(message, options) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "BetterZapClientError";
|
|
11
|
+
this.status = options.status;
|
|
12
|
+
this.code = options.code;
|
|
13
|
+
this.details = options.details;
|
|
14
|
+
this.body = options.body;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
3
17
|
function createZapClient(options) {
|
|
4
18
|
const baseURL = options?.baseURL ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
5
19
|
const basePath = options?.basePath ?? "/api/whatsapp";
|
|
6
20
|
const fetchFn = options?.fetch ?? fetch;
|
|
7
21
|
async function request(path, init) {
|
|
8
22
|
const response = await fetchFn(`${baseURL}${basePath}${path}`, init);
|
|
23
|
+
const payload = await response.json().catch(() => null);
|
|
9
24
|
if (!response.ok) {
|
|
10
|
-
const error =
|
|
11
|
-
throw new
|
|
25
|
+
const error = payload;
|
|
26
|
+
throw new BetterZapClientError(error?.error || `Request failed: ${response.status}`, {
|
|
27
|
+
status: response.status,
|
|
28
|
+
code: error?.code,
|
|
29
|
+
details: error?.details,
|
|
30
|
+
body: payload
|
|
31
|
+
});
|
|
12
32
|
}
|
|
13
|
-
return
|
|
33
|
+
return payload;
|
|
14
34
|
}
|
|
15
35
|
function post(path, body) {
|
|
16
36
|
return request(path, {
|
|
@@ -34,6 +54,7 @@ function createZapClient(options) {
|
|
|
34
54
|
conversations: {
|
|
35
55
|
list: () => request("/conversations"),
|
|
36
56
|
get: (phone) => request(`/conversations/${normalizePhone(phone)}`).catch((err) => {
|
|
57
|
+
if (err instanceof BetterZapClientError && err.status === 404) return null;
|
|
37
58
|
if (err.message?.includes("Conversation not found")) return null;
|
|
38
59
|
throw err;
|
|
39
60
|
}),
|
|
@@ -48,4 +69,5 @@ function createZapClient(options) {
|
|
|
48
69
|
};
|
|
49
70
|
}
|
|
50
71
|
//#endregion
|
|
72
|
+
exports.BetterZapClientError = BetterZapClientError;
|
|
51
73
|
exports.createZapClient = createZapClient;
|
package/dist/client.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as createZapClient, t as
|
|
2
|
-
export { ZapClient, createZapClient };
|
|
1
|
+
import { n as ZapClient, r as createZapClient, t as BetterZapClientError } from "./client-CRwYOHIG.cjs";
|
|
2
|
+
export { BetterZapClientError, ZapClient, createZapClient };
|
package/dist/client.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as createZapClient, t as
|
|
2
|
-
export { ZapClient, createZapClient };
|
|
1
|
+
import { n as ZapClient, r as createZapClient, t as BetterZapClientError } from "./client-s6x3lYea.mjs";
|
|
2
|
+
export { BetterZapClientError, ZapClient, createZapClient };
|
package/dist/client.mjs
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
//#region src/client.ts
|
|
2
|
+
var BetterZapClientError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
code;
|
|
5
|
+
details;
|
|
6
|
+
body;
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "BetterZapClientError";
|
|
10
|
+
this.status = options.status;
|
|
11
|
+
this.code = options.code;
|
|
12
|
+
this.details = options.details;
|
|
13
|
+
this.body = options.body;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
2
16
|
function createZapClient(options) {
|
|
3
17
|
const baseURL = options?.baseURL ?? (typeof window !== "undefined" ? window.location.origin : "");
|
|
4
18
|
const basePath = options?.basePath ?? "/api/whatsapp";
|
|
5
19
|
const fetchFn = options?.fetch ?? fetch;
|
|
6
20
|
async function request(path, init) {
|
|
7
21
|
const response = await fetchFn(`${baseURL}${basePath}${path}`, init);
|
|
22
|
+
const payload = await response.json().catch(() => null);
|
|
8
23
|
if (!response.ok) {
|
|
9
|
-
const error =
|
|
10
|
-
throw new
|
|
24
|
+
const error = payload;
|
|
25
|
+
throw new BetterZapClientError(error?.error || `Request failed: ${response.status}`, {
|
|
26
|
+
status: response.status,
|
|
27
|
+
code: error?.code,
|
|
28
|
+
details: error?.details,
|
|
29
|
+
body: payload
|
|
30
|
+
});
|
|
11
31
|
}
|
|
12
|
-
return
|
|
32
|
+
return payload;
|
|
13
33
|
}
|
|
14
34
|
function post(path, body) {
|
|
15
35
|
return request(path, {
|
|
@@ -33,6 +53,7 @@ function createZapClient(options) {
|
|
|
33
53
|
conversations: {
|
|
34
54
|
list: () => request("/conversations"),
|
|
35
55
|
get: (phone) => request(`/conversations/${normalizePhone(phone)}`).catch((err) => {
|
|
56
|
+
if (err instanceof BetterZapClientError && err.status === 404) return null;
|
|
36
57
|
if (err.message?.includes("Conversation not found")) return null;
|
|
37
58
|
throw err;
|
|
38
59
|
}),
|
|
@@ -47,4 +68,4 @@ function createZapClient(options) {
|
|
|
47
68
|
};
|
|
48
69
|
}
|
|
49
70
|
//#endregion
|
|
50
|
-
export { createZapClient };
|
|
71
|
+
export { BetterZapClientError, createZapClient };
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_client = require("./client.cjs");
|
|
3
|
+
//#region src/freeform-message-window.ts
|
|
4
|
+
const FREEFORM_MESSAGE_WINDOW_MS = 1440 * 60 * 1e3;
|
|
5
|
+
function toTimestamp(value) {
|
|
6
|
+
if (!value) return null;
|
|
7
|
+
const timestamp = new Date(value).getTime();
|
|
8
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
9
|
+
}
|
|
10
|
+
function createFreeformMessageWindow(lastIncomingMessageAt, now = /* @__PURE__ */ new Date()) {
|
|
11
|
+
const lastIncomingTimestamp = toTimestamp(lastIncomingMessageAt);
|
|
12
|
+
if (lastIncomingTimestamp == null) return {
|
|
13
|
+
isOpen: false,
|
|
14
|
+
lastIncomingMessageAt: null,
|
|
15
|
+
expiresAt: null
|
|
16
|
+
};
|
|
17
|
+
const expiresAtTimestamp = lastIncomingTimestamp + FREEFORM_MESSAGE_WINDOW_MS;
|
|
18
|
+
return {
|
|
19
|
+
isOpen: now.getTime() < expiresAtTimestamp,
|
|
20
|
+
lastIncomingMessageAt,
|
|
21
|
+
expiresAt: new Date(expiresAtTimestamp).toISOString()
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeConversationRecord(record, now = /* @__PURE__ */ new Date()) {
|
|
25
|
+
const freeformMessageWindow = createFreeformMessageWindow(record.lastIncomingMessageAt, now);
|
|
26
|
+
return {
|
|
27
|
+
...record,
|
|
28
|
+
lastIncomingMessageAt: freeformMessageWindow.lastIncomingMessageAt,
|
|
29
|
+
freeformMessageWindow
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function normalizeConversationRecords(records, now = /* @__PURE__ */ new Date()) {
|
|
33
|
+
return records.map((record) => normalizeConversationRecord(record, now));
|
|
34
|
+
}
|
|
35
|
+
function getLatestIncomingMessageAt(messages) {
|
|
36
|
+
if (!messages?.length) return null;
|
|
37
|
+
let latestIncomingMessageAt = null;
|
|
38
|
+
let latestTimestamp = -Infinity;
|
|
39
|
+
for (const message of messages) {
|
|
40
|
+
if (message.direction !== "incoming") continue;
|
|
41
|
+
const timestamp = toTimestamp(message.sentAt);
|
|
42
|
+
if (timestamp == null || timestamp <= latestTimestamp) continue;
|
|
43
|
+
latestIncomingMessageAt = message.sentAt;
|
|
44
|
+
latestTimestamp = timestamp;
|
|
45
|
+
}
|
|
46
|
+
return latestIncomingMessageAt;
|
|
47
|
+
}
|
|
48
|
+
function resolveConversationFreeformMessageWindow(conversation, messages, now = /* @__PURE__ */ new Date()) {
|
|
49
|
+
const baseLastIncomingMessageAt = conversation?.freeformMessageWindow?.lastIncomingMessageAt ?? conversation?.lastIncomingMessageAt ?? null;
|
|
50
|
+
const latestIncomingMessageAt = getLatestIncomingMessageAt(messages);
|
|
51
|
+
if (!latestIncomingMessageAt) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
|
|
52
|
+
const baseTimestamp = toTimestamp(baseLastIncomingMessageAt);
|
|
53
|
+
const latestTimestamp = toTimestamp(latestIncomingMessageAt);
|
|
54
|
+
if (latestTimestamp == null) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
|
|
55
|
+
if (baseTimestamp != null && latestTimestamp <= baseTimestamp) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
|
|
56
|
+
return createFreeformMessageWindow(latestIncomingMessageAt, now);
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
3
59
|
//#region src/logger.ts
|
|
4
60
|
const LOG_LEVEL_ORDER = {
|
|
5
61
|
debug: 0,
|
|
@@ -78,6 +134,7 @@ function delay(ms) {
|
|
|
78
134
|
//#region src/services/whatsapp.service.ts
|
|
79
135
|
const META_API_VERSION = "v25.0";
|
|
80
136
|
const META_BASE_URL = "https://graph.facebook.com";
|
|
137
|
+
const CONTEXT_WINDOW_CLOSED_ERROR = "Free-form message window is closed.";
|
|
81
138
|
var WhatsAppService = class {
|
|
82
139
|
baseUrl;
|
|
83
140
|
token;
|
|
@@ -91,15 +148,31 @@ var WhatsAppService = class {
|
|
|
91
148
|
this.logger = logger;
|
|
92
149
|
this.log = log;
|
|
93
150
|
}
|
|
94
|
-
/** Send a text message
|
|
151
|
+
/** Send a text message within the 24h free-form message window only. */
|
|
95
152
|
async sendText(to, body, logging) {
|
|
96
|
-
const
|
|
153
|
+
const normalizedPhone = formatPhone(to);
|
|
154
|
+
const freeformMessageWindow = await this.logger.getFreeformMessageWindow(normalizedPhone);
|
|
155
|
+
if (!freeformMessageWindow.isOpen) {
|
|
156
|
+
const result = {
|
|
157
|
+
success: false,
|
|
158
|
+
error: CONTEXT_WINDOW_CLOSED_ERROR,
|
|
159
|
+
code: "CONTEXT_WINDOW_CLOSED",
|
|
160
|
+
httpStatus: 409,
|
|
161
|
+
details: { freeformMessageWindow }
|
|
162
|
+
};
|
|
163
|
+
await this.logSendResult(normalizedPhone, logging ? {
|
|
164
|
+
...logging,
|
|
165
|
+
messageType: logging.messageType || "bot_reply",
|
|
166
|
+
content: body
|
|
167
|
+
} : void 0, result);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
97
170
|
const payload = {
|
|
98
171
|
messaging_product: "whatsapp",
|
|
99
172
|
recipient_type: "individual",
|
|
100
|
-
to:
|
|
173
|
+
to: normalizedPhone,
|
|
101
174
|
type: "text",
|
|
102
|
-
text:
|
|
175
|
+
text: /https?:\/\/\S+/i.test(body) ? {
|
|
103
176
|
body,
|
|
104
177
|
preview_url: true
|
|
105
178
|
} : { body }
|
|
@@ -311,20 +384,24 @@ var WhatsAppService = class {
|
|
|
311
384
|
};
|
|
312
385
|
}
|
|
313
386
|
const result = await this.performRequest(payload, retries);
|
|
314
|
-
|
|
387
|
+
await this.logSendResult(payload.to, logging, result, payload.type === "template" ? payload.template.name : void 0);
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
390
|
+
async logSendResult(phone, logging, result, templateName) {
|
|
391
|
+
if (!logging) return;
|
|
392
|
+
try {
|
|
315
393
|
await this.logger.logOutgoing({
|
|
316
|
-
phone
|
|
394
|
+
phone,
|
|
317
395
|
userId: logging.userId,
|
|
318
396
|
messageType: logging.messageType,
|
|
319
397
|
content: logging.content,
|
|
320
|
-
templateName
|
|
398
|
+
templateName,
|
|
321
399
|
result,
|
|
322
400
|
metadata: logging.metadata
|
|
323
401
|
});
|
|
324
402
|
} catch (logError) {
|
|
325
403
|
this.log.error("whatsapp.log_failed", serializeError(logError));
|
|
326
404
|
}
|
|
327
|
-
return result;
|
|
328
405
|
}
|
|
329
406
|
/** Actually performs the network request with retries. */
|
|
330
407
|
async performRequest(payload, retries) {
|
|
@@ -406,6 +483,24 @@ var MessageLoggerService = class {
|
|
|
406
483
|
this.log.error("message_logger.sync_notify_failed", serializeError(err));
|
|
407
484
|
}
|
|
408
485
|
}
|
|
486
|
+
async getConversationById(conversationId) {
|
|
487
|
+
const conversation = await this.store.getConversationById(conversationId);
|
|
488
|
+
return conversation ? normalizeConversationRecord(conversation) : null;
|
|
489
|
+
}
|
|
490
|
+
async getConversationByPhone(phone) {
|
|
491
|
+
const conversation = await this.store.getConversationByPhone(phone);
|
|
492
|
+
return conversation ? normalizeConversationRecord(conversation) : null;
|
|
493
|
+
}
|
|
494
|
+
async getConversations() {
|
|
495
|
+
return normalizeConversationRecords(await this.store.getConversations());
|
|
496
|
+
}
|
|
497
|
+
/** @deprecated Prefer `getFreeformMessageWindow()`. */
|
|
498
|
+
async getCustomerCareWindow(phone) {
|
|
499
|
+
return this.getFreeformMessageWindow(phone);
|
|
500
|
+
}
|
|
501
|
+
async getFreeformMessageWindow(phone) {
|
|
502
|
+
return (await this.getConversationByPhone(phone))?.freeformMessageWindow ?? createFreeformMessageWindow(null);
|
|
503
|
+
}
|
|
409
504
|
/**
|
|
410
505
|
* Check if a message with this waMessageId was already processed.
|
|
411
506
|
*/
|
|
@@ -429,7 +524,7 @@ var MessageLoggerService = class {
|
|
|
429
524
|
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
430
525
|
metadata: params.metadata
|
|
431
526
|
});
|
|
432
|
-
const conversation = await this.
|
|
527
|
+
const conversation = await this.getConversationById(inserted.conversationId);
|
|
433
528
|
if (conversation) await this.notify({
|
|
434
529
|
type: "NEW_MESSAGE",
|
|
435
530
|
message: inserted,
|
|
@@ -481,9 +576,9 @@ var MessageLoggerService = class {
|
|
|
481
576
|
content: params.content,
|
|
482
577
|
status: "delivered",
|
|
483
578
|
metadata: params.metadata,
|
|
484
|
-
sentAt:
|
|
579
|
+
sentAt: params.sentAt
|
|
485
580
|
});
|
|
486
|
-
const conversation = await this.
|
|
581
|
+
const conversation = await this.getConversationById(inserted.conversationId);
|
|
487
582
|
if (conversation) await this.notify({
|
|
488
583
|
type: "NEW_MESSAGE",
|
|
489
584
|
message: inserted,
|
|
@@ -528,53 +623,69 @@ function serializeTemplateFromRegistry(templates, templateName, options) {
|
|
|
528
623
|
};
|
|
529
624
|
}
|
|
530
625
|
function serializeTemplateParameter(parameter, value) {
|
|
626
|
+
const metaParameterName = parameter.parameterName ? { parameter_name: parameter.parameterName } : {};
|
|
531
627
|
switch (parameter.type) {
|
|
532
628
|
case "text": return {
|
|
533
629
|
type: "text",
|
|
630
|
+
...metaParameterName,
|
|
534
631
|
text: value
|
|
535
632
|
};
|
|
536
633
|
case "payload": return {
|
|
537
634
|
type: "payload",
|
|
635
|
+
...metaParameterName,
|
|
538
636
|
payload: value
|
|
539
637
|
};
|
|
540
638
|
case "location": return {
|
|
541
639
|
type: "location",
|
|
640
|
+
...metaParameterName,
|
|
542
641
|
location: value
|
|
543
642
|
};
|
|
544
643
|
case "image": return {
|
|
545
644
|
type: "image",
|
|
645
|
+
...metaParameterName,
|
|
546
646
|
image: value
|
|
547
647
|
};
|
|
548
648
|
case "video": return {
|
|
549
649
|
type: "video",
|
|
650
|
+
...metaParameterName,
|
|
550
651
|
video: value
|
|
551
652
|
};
|
|
552
653
|
case "document": return {
|
|
553
654
|
type: "document",
|
|
655
|
+
...metaParameterName,
|
|
554
656
|
document: value
|
|
555
657
|
};
|
|
556
658
|
case "currency": return {
|
|
557
659
|
type: "currency",
|
|
660
|
+
...metaParameterName,
|
|
558
661
|
currency: value
|
|
559
662
|
};
|
|
560
663
|
case "date_time": return {
|
|
561
664
|
type: "date_time",
|
|
665
|
+
...metaParameterName,
|
|
562
666
|
date_time: value
|
|
563
667
|
};
|
|
564
668
|
}
|
|
565
669
|
}
|
|
566
670
|
//#endregion
|
|
671
|
+
exports.BetterZapClientError = require_client.BetterZapClientError;
|
|
567
672
|
exports.EMPTY_TEMPLATE_REGISTRY = EMPTY_TEMPLATE_REGISTRY;
|
|
673
|
+
exports.FREEFORM_MESSAGE_WINDOW_MS = FREEFORM_MESSAGE_WINDOW_MS;
|
|
568
674
|
exports.MessageLoggerService = MessageLoggerService;
|
|
569
675
|
exports.WHATSAPP_MESSAGE_TYPES = WHATSAPP_MESSAGE_TYPES;
|
|
570
676
|
exports.WhatsAppService = WhatsAppService;
|
|
677
|
+
exports.createFreeformMessageWindow = createFreeformMessageWindow;
|
|
571
678
|
exports.createLogger = createLogger;
|
|
572
679
|
exports.createZapClient = require_client.createZapClient;
|
|
573
680
|
exports.defineTemplates = defineTemplates;
|
|
574
681
|
exports.delay = delay;
|
|
575
682
|
exports.formatPhone = formatPhone;
|
|
683
|
+
exports.getLatestIncomingMessageAt = getLatestIncomingMessageAt;
|
|
576
684
|
exports.getTemplateNames = getTemplateNames;
|
|
577
685
|
exports.hasConfiguredTemplates = hasConfiguredTemplates;
|
|
578
686
|
exports.noopLogger = noopLogger;
|
|
687
|
+
exports.normalizeConversationRecord = normalizeConversationRecord;
|
|
688
|
+
exports.normalizeConversationRecords = normalizeConversationRecords;
|
|
689
|
+
exports.resolveConversationFreeformMessageWindow = resolveConversationFreeformMessageWindow;
|
|
579
690
|
exports.serializeError = serializeError;
|
|
580
691
|
exports.serializeTemplateFromRegistry = serializeTemplateFromRegistry;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as UIMessage, A as createLogger, B as ConversationRecord, C as WhatsAppLogRecord, D as LogLevel, E as WhatsAppStatus, F as NewMessageEvent, G as MessageError, H as IncomingMessage, I as StatusUpdateEvent, J as SendMessageError, K as MessageStatus, L as SyncEvent, M as serializeError, N as ConversationSummary, O as Logger, P as ConversationUpdateEvent, Q as TemplateParameter, R as WhatsAppConfig, S as WhatsAppDirection, T as WhatsAppMessageType, U as InteractiveMediaCarouselCardInput, V as FreeformMessageWindow, W as MediaMessage, X as SendResult, Y as SendMessageResponse, Z as TemplateComponent, _ as OutgoingLoggingMetadata, a as SupportedTemplateParameterType, at as WebhookPayload, b as MessageLoggerService, c as TemplateName, ct as WhatsAppInteractiveButtonsMessage, d as TemplateParams, dt as WhatsAppLocationMessage, et as UIMessageStatus, f as TemplateRegistry, ft as WhatsAppTemplateMessage, g as serializeTemplateFromRegistry, h as hasConfiguredTemplates, i as EMPTY_TEMPLATE_REGISTRY, it as WebhookError, j as noopLogger, k as LoggerConfig, l as TemplateParameterDefinition, lt as WhatsAppInteractiveListMessage, m as getTemplateNames, n as ZapClient, nt as WebhookContact, o as TemplateComponentDefinition, ot as WebhookValue, p as defineTemplates, pt as WhatsAppTextMessage, q as SendInteractiveMediaCarouselData, r as createZapClient, rt as WebhookEntry, s as TemplateDefinition, st as WhatsAppCarouselCard, t as BetterZapClientError, tt as WebhookChange, u as TemplateParameterInputMap, ut as WhatsAppInteractiveMediaCarouselMessage, v as WhatsAppService, w as WhatsAppLogStore, x as WHATSAPP_MESSAGE_TYPES, y as MessageLoggerNotifier, z as Conversation } from "./client-CRwYOHIG.cjs";
|
|
2
2
|
|
|
3
|
+
//#region src/freeform-message-window.d.ts
|
|
4
|
+
declare const FREEFORM_MESSAGE_WINDOW_MS: number;
|
|
5
|
+
declare function createFreeformMessageWindow(lastIncomingMessageAt: string | null, now?: Date): FreeformMessageWindow;
|
|
6
|
+
declare function normalizeConversationRecord(record: ConversationRecord, now?: Date): Conversation;
|
|
7
|
+
declare function normalizeConversationRecords(records: ConversationRecord[], now?: Date): Conversation[];
|
|
8
|
+
declare function getLatestIncomingMessageAt(messages: UIMessage[] | undefined): string | null;
|
|
9
|
+
declare function resolveConversationFreeformMessageWindow(conversation: Pick<Conversation, "freeformMessageWindow" | "lastIncomingMessageAt"> | null | undefined, messages?: UIMessage[], now?: Date): FreeformMessageWindow;
|
|
10
|
+
//#endregion
|
|
3
11
|
//#region src/events.d.ts
|
|
4
12
|
type MessageContext = {
|
|
5
13
|
message: IncomingMessage;
|
|
@@ -120,4 +128,4 @@ interface BetterZapApi<TTemplates extends TemplateRegistry = {}> {
|
|
|
120
128
|
};
|
|
121
129
|
}
|
|
122
130
|
//#endregion
|
|
123
|
-
export { type Awaitable, type BetterZapApi, type BetterZapContext, type BetterZapCoreConfig, type BetterZapCoreContext, type BetterZapCoreServices, type BetterZapDatabase, type BetterZapPlugin, type BetterZapPluginInitContext, type BetterZapPluginInitResult, type BetterZapServices, type Conversation, type ConversationSummary, type ConversationUpdateEvent, EMPTY_TEMPLATE_REGISTRY, type IncomingMessage, type InferBetterZapPluginContext, type InferBetterZapPluginServices, type InteractiveMediaCarouselCardInput, type LogLevel, type Logger, type LoggerConfig, type MediaMessage, type MessageContext, type MessageError, type MessageLoggerNotifier, MessageLoggerService, type MessageStatus, type NewMessageEvent, type OutgoingLoggingMetadata, type SendInteractiveMediaCarouselData, type SendMessageError, type SendMessageResponse, type SendResult, type StatusContext, type StatusUpdateEvent, type SupportedTemplateParameterType, type SyncEvent, type TemplateComponent, type TemplateComponentDefinition, type TemplateDefinition, type TemplateName, type TemplateParameter, type TemplateParameterDefinition, type TemplateParameterInputMap, type TemplateParams, type TemplateRegistry, type UIMessage, type UIMessageStatus, WHATSAPP_MESSAGE_TYPES, type WebhookChange, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookPayload, type WebhookValue, type WhatsAppCarouselCard, type WhatsAppConfig, type WhatsAppDirection, type WhatsAppInteractiveButtonsMessage, type WhatsAppInteractiveListMessage, type WhatsAppInteractiveMediaCarouselMessage, type WhatsAppLocationMessage, type WhatsAppLogRecord, type WhatsAppLogStore, type WhatsAppMessageType, WhatsAppService, type WhatsAppStatus, type WhatsAppTemplateMessage, type WhatsAppTextMessage, type ZapClient, createLogger, createZapClient, defineTemplates, delay, formatPhone, getTemplateNames, hasConfiguredTemplates, noopLogger, serializeError, serializeTemplateFromRegistry };
|
|
131
|
+
export { type Awaitable, type BetterZapApi, BetterZapClientError, type BetterZapContext, type BetterZapCoreConfig, type BetterZapCoreContext, type BetterZapCoreServices, type BetterZapDatabase, type BetterZapPlugin, type BetterZapPluginInitContext, type BetterZapPluginInitResult, type BetterZapServices, type Conversation, type ConversationRecord, type ConversationSummary, type ConversationUpdateEvent, EMPTY_TEMPLATE_REGISTRY, FREEFORM_MESSAGE_WINDOW_MS, type FreeformMessageWindow, type IncomingMessage, type InferBetterZapPluginContext, type InferBetterZapPluginServices, type InteractiveMediaCarouselCardInput, type LogLevel, type Logger, type LoggerConfig, type MediaMessage, type MessageContext, type MessageError, type MessageLoggerNotifier, MessageLoggerService, type MessageStatus, type NewMessageEvent, type OutgoingLoggingMetadata, type SendInteractiveMediaCarouselData, type SendMessageError, type SendMessageResponse, type SendResult, type StatusContext, type StatusUpdateEvent, type SupportedTemplateParameterType, type SyncEvent, type TemplateComponent, type TemplateComponentDefinition, type TemplateDefinition, type TemplateName, type TemplateParameter, type TemplateParameterDefinition, type TemplateParameterInputMap, type TemplateParams, type TemplateRegistry, type UIMessage, type UIMessageStatus, WHATSAPP_MESSAGE_TYPES, type WebhookChange, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookPayload, type WebhookValue, type WhatsAppCarouselCard, type WhatsAppConfig, type WhatsAppDirection, type WhatsAppInteractiveButtonsMessage, type WhatsAppInteractiveListMessage, type WhatsAppInteractiveMediaCarouselMessage, type WhatsAppLocationMessage, type WhatsAppLogRecord, type WhatsAppLogStore, type WhatsAppMessageType, WhatsAppService, type WhatsAppStatus, type WhatsAppTemplateMessage, type WhatsAppTextMessage, type ZapClient, createFreeformMessageWindow, createLogger, createZapClient, defineTemplates, delay, formatPhone, getLatestIncomingMessageAt, getTemplateNames, hasConfiguredTemplates, noopLogger, normalizeConversationRecord, normalizeConversationRecords, resolveConversationFreeformMessageWindow, serializeError, serializeTemplateFromRegistry };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as UIMessage, A as createLogger, B as ConversationRecord, C as WhatsAppLogRecord, D as LogLevel, E as WhatsAppStatus, F as NewMessageEvent, G as MessageError, H as IncomingMessage, I as StatusUpdateEvent, J as SendMessageError, K as MessageStatus, L as SyncEvent, M as serializeError, N as ConversationSummary, O as Logger, P as ConversationUpdateEvent, Q as TemplateParameter, R as WhatsAppConfig, S as WhatsAppDirection, T as WhatsAppMessageType, U as InteractiveMediaCarouselCardInput, V as FreeformMessageWindow, W as MediaMessage, X as SendResult, Y as SendMessageResponse, Z as TemplateComponent, _ as OutgoingLoggingMetadata, a as SupportedTemplateParameterType, at as WebhookPayload, b as MessageLoggerService, c as TemplateName, ct as WhatsAppInteractiveButtonsMessage, d as TemplateParams, dt as WhatsAppLocationMessage, et as UIMessageStatus, f as TemplateRegistry, ft as WhatsAppTemplateMessage, g as serializeTemplateFromRegistry, h as hasConfiguredTemplates, i as EMPTY_TEMPLATE_REGISTRY, it as WebhookError, j as noopLogger, k as LoggerConfig, l as TemplateParameterDefinition, lt as WhatsAppInteractiveListMessage, m as getTemplateNames, n as ZapClient, nt as WebhookContact, o as TemplateComponentDefinition, ot as WebhookValue, p as defineTemplates, pt as WhatsAppTextMessage, q as SendInteractiveMediaCarouselData, r as createZapClient, rt as WebhookEntry, s as TemplateDefinition, st as WhatsAppCarouselCard, t as BetterZapClientError, tt as WebhookChange, u as TemplateParameterInputMap, ut as WhatsAppInteractiveMediaCarouselMessage, v as WhatsAppService, w as WhatsAppLogStore, x as WHATSAPP_MESSAGE_TYPES, y as MessageLoggerNotifier, z as Conversation } from "./client-s6x3lYea.mjs";
|
|
2
2
|
|
|
3
|
+
//#region src/freeform-message-window.d.ts
|
|
4
|
+
declare const FREEFORM_MESSAGE_WINDOW_MS: number;
|
|
5
|
+
declare function createFreeformMessageWindow(lastIncomingMessageAt: string | null, now?: Date): FreeformMessageWindow;
|
|
6
|
+
declare function normalizeConversationRecord(record: ConversationRecord, now?: Date): Conversation;
|
|
7
|
+
declare function normalizeConversationRecords(records: ConversationRecord[], now?: Date): Conversation[];
|
|
8
|
+
declare function getLatestIncomingMessageAt(messages: UIMessage[] | undefined): string | null;
|
|
9
|
+
declare function resolveConversationFreeformMessageWindow(conversation: Pick<Conversation, "freeformMessageWindow" | "lastIncomingMessageAt"> | null | undefined, messages?: UIMessage[], now?: Date): FreeformMessageWindow;
|
|
10
|
+
//#endregion
|
|
3
11
|
//#region src/events.d.ts
|
|
4
12
|
type MessageContext = {
|
|
5
13
|
message: IncomingMessage;
|
|
@@ -120,4 +128,4 @@ interface BetterZapApi<TTemplates extends TemplateRegistry = {}> {
|
|
|
120
128
|
};
|
|
121
129
|
}
|
|
122
130
|
//#endregion
|
|
123
|
-
export { type Awaitable, type BetterZapApi, type BetterZapContext, type BetterZapCoreConfig, type BetterZapCoreContext, type BetterZapCoreServices, type BetterZapDatabase, type BetterZapPlugin, type BetterZapPluginInitContext, type BetterZapPluginInitResult, type BetterZapServices, type Conversation, type ConversationSummary, type ConversationUpdateEvent, EMPTY_TEMPLATE_REGISTRY, type IncomingMessage, type InferBetterZapPluginContext, type InferBetterZapPluginServices, type InteractiveMediaCarouselCardInput, type LogLevel, type Logger, type LoggerConfig, type MediaMessage, type MessageContext, type MessageError, type MessageLoggerNotifier, MessageLoggerService, type MessageStatus, type NewMessageEvent, type OutgoingLoggingMetadata, type SendInteractiveMediaCarouselData, type SendMessageError, type SendMessageResponse, type SendResult, type StatusContext, type StatusUpdateEvent, type SupportedTemplateParameterType, type SyncEvent, type TemplateComponent, type TemplateComponentDefinition, type TemplateDefinition, type TemplateName, type TemplateParameter, type TemplateParameterDefinition, type TemplateParameterInputMap, type TemplateParams, type TemplateRegistry, type UIMessage, type UIMessageStatus, WHATSAPP_MESSAGE_TYPES, type WebhookChange, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookPayload, type WebhookValue, type WhatsAppCarouselCard, type WhatsAppConfig, type WhatsAppDirection, type WhatsAppInteractiveButtonsMessage, type WhatsAppInteractiveListMessage, type WhatsAppInteractiveMediaCarouselMessage, type WhatsAppLocationMessage, type WhatsAppLogRecord, type WhatsAppLogStore, type WhatsAppMessageType, WhatsAppService, type WhatsAppStatus, type WhatsAppTemplateMessage, type WhatsAppTextMessage, type ZapClient, createLogger, createZapClient, defineTemplates, delay, formatPhone, getTemplateNames, hasConfiguredTemplates, noopLogger, serializeError, serializeTemplateFromRegistry };
|
|
131
|
+
export { type Awaitable, type BetterZapApi, BetterZapClientError, type BetterZapContext, type BetterZapCoreConfig, type BetterZapCoreContext, type BetterZapCoreServices, type BetterZapDatabase, type BetterZapPlugin, type BetterZapPluginInitContext, type BetterZapPluginInitResult, type BetterZapServices, type Conversation, type ConversationRecord, type ConversationSummary, type ConversationUpdateEvent, EMPTY_TEMPLATE_REGISTRY, FREEFORM_MESSAGE_WINDOW_MS, type FreeformMessageWindow, type IncomingMessage, type InferBetterZapPluginContext, type InferBetterZapPluginServices, type InteractiveMediaCarouselCardInput, type LogLevel, type Logger, type LoggerConfig, type MediaMessage, type MessageContext, type MessageError, type MessageLoggerNotifier, MessageLoggerService, type MessageStatus, type NewMessageEvent, type OutgoingLoggingMetadata, type SendInteractiveMediaCarouselData, type SendMessageError, type SendMessageResponse, type SendResult, type StatusContext, type StatusUpdateEvent, type SupportedTemplateParameterType, type SyncEvent, type TemplateComponent, type TemplateComponentDefinition, type TemplateDefinition, type TemplateName, type TemplateParameter, type TemplateParameterDefinition, type TemplateParameterInputMap, type TemplateParams, type TemplateRegistry, type UIMessage, type UIMessageStatus, WHATSAPP_MESSAGE_TYPES, type WebhookChange, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookPayload, type WebhookValue, type WhatsAppCarouselCard, type WhatsAppConfig, type WhatsAppDirection, type WhatsAppInteractiveButtonsMessage, type WhatsAppInteractiveListMessage, type WhatsAppInteractiveMediaCarouselMessage, type WhatsAppLocationMessage, type WhatsAppLogRecord, type WhatsAppLogStore, type WhatsAppMessageType, WhatsAppService, type WhatsAppStatus, type WhatsAppTemplateMessage, type WhatsAppTextMessage, type ZapClient, createFreeformMessageWindow, createLogger, createZapClient, defineTemplates, delay, formatPhone, getLatestIncomingMessageAt, getTemplateNames, hasConfiguredTemplates, noopLogger, normalizeConversationRecord, normalizeConversationRecords, resolveConversationFreeformMessageWindow, serializeError, serializeTemplateFromRegistry };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,60 @@
|
|
|
1
|
-
import { createZapClient } from "./client.mjs";
|
|
1
|
+
import { BetterZapClientError, createZapClient } from "./client.mjs";
|
|
2
|
+
//#region src/freeform-message-window.ts
|
|
3
|
+
const FREEFORM_MESSAGE_WINDOW_MS = 1440 * 60 * 1e3;
|
|
4
|
+
function toTimestamp(value) {
|
|
5
|
+
if (!value) return null;
|
|
6
|
+
const timestamp = new Date(value).getTime();
|
|
7
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
8
|
+
}
|
|
9
|
+
function createFreeformMessageWindow(lastIncomingMessageAt, now = /* @__PURE__ */ new Date()) {
|
|
10
|
+
const lastIncomingTimestamp = toTimestamp(lastIncomingMessageAt);
|
|
11
|
+
if (lastIncomingTimestamp == null) return {
|
|
12
|
+
isOpen: false,
|
|
13
|
+
lastIncomingMessageAt: null,
|
|
14
|
+
expiresAt: null
|
|
15
|
+
};
|
|
16
|
+
const expiresAtTimestamp = lastIncomingTimestamp + FREEFORM_MESSAGE_WINDOW_MS;
|
|
17
|
+
return {
|
|
18
|
+
isOpen: now.getTime() < expiresAtTimestamp,
|
|
19
|
+
lastIncomingMessageAt,
|
|
20
|
+
expiresAt: new Date(expiresAtTimestamp).toISOString()
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function normalizeConversationRecord(record, now = /* @__PURE__ */ new Date()) {
|
|
24
|
+
const freeformMessageWindow = createFreeformMessageWindow(record.lastIncomingMessageAt, now);
|
|
25
|
+
return {
|
|
26
|
+
...record,
|
|
27
|
+
lastIncomingMessageAt: freeformMessageWindow.lastIncomingMessageAt,
|
|
28
|
+
freeformMessageWindow
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function normalizeConversationRecords(records, now = /* @__PURE__ */ new Date()) {
|
|
32
|
+
return records.map((record) => normalizeConversationRecord(record, now));
|
|
33
|
+
}
|
|
34
|
+
function getLatestIncomingMessageAt(messages) {
|
|
35
|
+
if (!messages?.length) return null;
|
|
36
|
+
let latestIncomingMessageAt = null;
|
|
37
|
+
let latestTimestamp = -Infinity;
|
|
38
|
+
for (const message of messages) {
|
|
39
|
+
if (message.direction !== "incoming") continue;
|
|
40
|
+
const timestamp = toTimestamp(message.sentAt);
|
|
41
|
+
if (timestamp == null || timestamp <= latestTimestamp) continue;
|
|
42
|
+
latestIncomingMessageAt = message.sentAt;
|
|
43
|
+
latestTimestamp = timestamp;
|
|
44
|
+
}
|
|
45
|
+
return latestIncomingMessageAt;
|
|
46
|
+
}
|
|
47
|
+
function resolveConversationFreeformMessageWindow(conversation, messages, now = /* @__PURE__ */ new Date()) {
|
|
48
|
+
const baseLastIncomingMessageAt = conversation?.freeformMessageWindow?.lastIncomingMessageAt ?? conversation?.lastIncomingMessageAt ?? null;
|
|
49
|
+
const latestIncomingMessageAt = getLatestIncomingMessageAt(messages);
|
|
50
|
+
if (!latestIncomingMessageAt) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
|
|
51
|
+
const baseTimestamp = toTimestamp(baseLastIncomingMessageAt);
|
|
52
|
+
const latestTimestamp = toTimestamp(latestIncomingMessageAt);
|
|
53
|
+
if (latestTimestamp == null) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
|
|
54
|
+
if (baseTimestamp != null && latestTimestamp <= baseTimestamp) return createFreeformMessageWindow(baseLastIncomingMessageAt, now);
|
|
55
|
+
return createFreeformMessageWindow(latestIncomingMessageAt, now);
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
2
58
|
//#region src/logger.ts
|
|
3
59
|
const LOG_LEVEL_ORDER = {
|
|
4
60
|
debug: 0,
|
|
@@ -77,6 +133,7 @@ function delay(ms) {
|
|
|
77
133
|
//#region src/services/whatsapp.service.ts
|
|
78
134
|
const META_API_VERSION = "v25.0";
|
|
79
135
|
const META_BASE_URL = "https://graph.facebook.com";
|
|
136
|
+
const CONTEXT_WINDOW_CLOSED_ERROR = "Free-form message window is closed.";
|
|
80
137
|
var WhatsAppService = class {
|
|
81
138
|
baseUrl;
|
|
82
139
|
token;
|
|
@@ -90,15 +147,31 @@ var WhatsAppService = class {
|
|
|
90
147
|
this.logger = logger;
|
|
91
148
|
this.log = log;
|
|
92
149
|
}
|
|
93
|
-
/** Send a text message
|
|
150
|
+
/** Send a text message within the 24h free-form message window only. */
|
|
94
151
|
async sendText(to, body, logging) {
|
|
95
|
-
const
|
|
152
|
+
const normalizedPhone = formatPhone(to);
|
|
153
|
+
const freeformMessageWindow = await this.logger.getFreeformMessageWindow(normalizedPhone);
|
|
154
|
+
if (!freeformMessageWindow.isOpen) {
|
|
155
|
+
const result = {
|
|
156
|
+
success: false,
|
|
157
|
+
error: CONTEXT_WINDOW_CLOSED_ERROR,
|
|
158
|
+
code: "CONTEXT_WINDOW_CLOSED",
|
|
159
|
+
httpStatus: 409,
|
|
160
|
+
details: { freeformMessageWindow }
|
|
161
|
+
};
|
|
162
|
+
await this.logSendResult(normalizedPhone, logging ? {
|
|
163
|
+
...logging,
|
|
164
|
+
messageType: logging.messageType || "bot_reply",
|
|
165
|
+
content: body
|
|
166
|
+
} : void 0, result);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
96
169
|
const payload = {
|
|
97
170
|
messaging_product: "whatsapp",
|
|
98
171
|
recipient_type: "individual",
|
|
99
|
-
to:
|
|
172
|
+
to: normalizedPhone,
|
|
100
173
|
type: "text",
|
|
101
|
-
text:
|
|
174
|
+
text: /https?:\/\/\S+/i.test(body) ? {
|
|
102
175
|
body,
|
|
103
176
|
preview_url: true
|
|
104
177
|
} : { body }
|
|
@@ -310,20 +383,24 @@ var WhatsAppService = class {
|
|
|
310
383
|
};
|
|
311
384
|
}
|
|
312
385
|
const result = await this.performRequest(payload, retries);
|
|
313
|
-
|
|
386
|
+
await this.logSendResult(payload.to, logging, result, payload.type === "template" ? payload.template.name : void 0);
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
async logSendResult(phone, logging, result, templateName) {
|
|
390
|
+
if (!logging) return;
|
|
391
|
+
try {
|
|
314
392
|
await this.logger.logOutgoing({
|
|
315
|
-
phone
|
|
393
|
+
phone,
|
|
316
394
|
userId: logging.userId,
|
|
317
395
|
messageType: logging.messageType,
|
|
318
396
|
content: logging.content,
|
|
319
|
-
templateName
|
|
397
|
+
templateName,
|
|
320
398
|
result,
|
|
321
399
|
metadata: logging.metadata
|
|
322
400
|
});
|
|
323
401
|
} catch (logError) {
|
|
324
402
|
this.log.error("whatsapp.log_failed", serializeError(logError));
|
|
325
403
|
}
|
|
326
|
-
return result;
|
|
327
404
|
}
|
|
328
405
|
/** Actually performs the network request with retries. */
|
|
329
406
|
async performRequest(payload, retries) {
|
|
@@ -405,6 +482,24 @@ var MessageLoggerService = class {
|
|
|
405
482
|
this.log.error("message_logger.sync_notify_failed", serializeError(err));
|
|
406
483
|
}
|
|
407
484
|
}
|
|
485
|
+
async getConversationById(conversationId) {
|
|
486
|
+
const conversation = await this.store.getConversationById(conversationId);
|
|
487
|
+
return conversation ? normalizeConversationRecord(conversation) : null;
|
|
488
|
+
}
|
|
489
|
+
async getConversationByPhone(phone) {
|
|
490
|
+
const conversation = await this.store.getConversationByPhone(phone);
|
|
491
|
+
return conversation ? normalizeConversationRecord(conversation) : null;
|
|
492
|
+
}
|
|
493
|
+
async getConversations() {
|
|
494
|
+
return normalizeConversationRecords(await this.store.getConversations());
|
|
495
|
+
}
|
|
496
|
+
/** @deprecated Prefer `getFreeformMessageWindow()`. */
|
|
497
|
+
async getCustomerCareWindow(phone) {
|
|
498
|
+
return this.getFreeformMessageWindow(phone);
|
|
499
|
+
}
|
|
500
|
+
async getFreeformMessageWindow(phone) {
|
|
501
|
+
return (await this.getConversationByPhone(phone))?.freeformMessageWindow ?? createFreeformMessageWindow(null);
|
|
502
|
+
}
|
|
408
503
|
/**
|
|
409
504
|
* Check if a message with this waMessageId was already processed.
|
|
410
505
|
*/
|
|
@@ -428,7 +523,7 @@ var MessageLoggerService = class {
|
|
|
428
523
|
sentAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
429
524
|
metadata: params.metadata
|
|
430
525
|
});
|
|
431
|
-
const conversation = await this.
|
|
526
|
+
const conversation = await this.getConversationById(inserted.conversationId);
|
|
432
527
|
if (conversation) await this.notify({
|
|
433
528
|
type: "NEW_MESSAGE",
|
|
434
529
|
message: inserted,
|
|
@@ -480,9 +575,9 @@ var MessageLoggerService = class {
|
|
|
480
575
|
content: params.content,
|
|
481
576
|
status: "delivered",
|
|
482
577
|
metadata: params.metadata,
|
|
483
|
-
sentAt:
|
|
578
|
+
sentAt: params.sentAt
|
|
484
579
|
});
|
|
485
|
-
const conversation = await this.
|
|
580
|
+
const conversation = await this.getConversationById(inserted.conversationId);
|
|
486
581
|
if (conversation) await this.notify({
|
|
487
582
|
type: "NEW_MESSAGE",
|
|
488
583
|
message: inserted,
|
|
@@ -527,40 +622,49 @@ function serializeTemplateFromRegistry(templates, templateName, options) {
|
|
|
527
622
|
};
|
|
528
623
|
}
|
|
529
624
|
function serializeTemplateParameter(parameter, value) {
|
|
625
|
+
const metaParameterName = parameter.parameterName ? { parameter_name: parameter.parameterName } : {};
|
|
530
626
|
switch (parameter.type) {
|
|
531
627
|
case "text": return {
|
|
532
628
|
type: "text",
|
|
629
|
+
...metaParameterName,
|
|
533
630
|
text: value
|
|
534
631
|
};
|
|
535
632
|
case "payload": return {
|
|
536
633
|
type: "payload",
|
|
634
|
+
...metaParameterName,
|
|
537
635
|
payload: value
|
|
538
636
|
};
|
|
539
637
|
case "location": return {
|
|
540
638
|
type: "location",
|
|
639
|
+
...metaParameterName,
|
|
541
640
|
location: value
|
|
542
641
|
};
|
|
543
642
|
case "image": return {
|
|
544
643
|
type: "image",
|
|
644
|
+
...metaParameterName,
|
|
545
645
|
image: value
|
|
546
646
|
};
|
|
547
647
|
case "video": return {
|
|
548
648
|
type: "video",
|
|
649
|
+
...metaParameterName,
|
|
549
650
|
video: value
|
|
550
651
|
};
|
|
551
652
|
case "document": return {
|
|
552
653
|
type: "document",
|
|
654
|
+
...metaParameterName,
|
|
553
655
|
document: value
|
|
554
656
|
};
|
|
555
657
|
case "currency": return {
|
|
556
658
|
type: "currency",
|
|
659
|
+
...metaParameterName,
|
|
557
660
|
currency: value
|
|
558
661
|
};
|
|
559
662
|
case "date_time": return {
|
|
560
663
|
type: "date_time",
|
|
664
|
+
...metaParameterName,
|
|
561
665
|
date_time: value
|
|
562
666
|
};
|
|
563
667
|
}
|
|
564
668
|
}
|
|
565
669
|
//#endregion
|
|
566
|
-
export { EMPTY_TEMPLATE_REGISTRY, MessageLoggerService, WHATSAPP_MESSAGE_TYPES, WhatsAppService, createLogger, createZapClient, defineTemplates, delay, formatPhone, getTemplateNames, hasConfiguredTemplates, noopLogger, serializeError, serializeTemplateFromRegistry };
|
|
670
|
+
export { BetterZapClientError, EMPTY_TEMPLATE_REGISTRY, FREEFORM_MESSAGE_WINDOW_MS, MessageLoggerService, WHATSAPP_MESSAGE_TYPES, WhatsAppService, createFreeformMessageWindow, createLogger, createZapClient, defineTemplates, delay, formatPhone, getLatestIncomingMessageAt, getTemplateNames, hasConfiguredTemplates, noopLogger, normalizeConversationRecord, normalizeConversationRecords, resolveConversationFreeformMessageWindow, serializeError, serializeTemplateFromRegistry };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-zap",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Framework-agnostic Better Zap core for typed WhatsApp integrations.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^25.4.0",
|
|
36
36
|
"tsdown": "^0.21.2",
|
|
37
|
-
"typescript": "^
|
|
37
|
+
"typescript": "^6.0.2",
|
|
38
38
|
"vitest": "^4.1.0"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|