better-zap 0.0.1 → 0.0.2
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-B_VYCUHu.d.cts} +37 -17
- package/dist/{client-D5Lgtacj.d.cts → client-D-wgralM.d.mts} +37 -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 +113 -11
- package/dist/index.d.cts +10 -2
- package/dist/index.d.mts +10 -2
- package/dist/index.mjs +108 -13
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -692,6 +700,18 @@ interface ZapClientOptions<TTemplates extends TemplateRegistry = {}> {
|
|
|
692
700
|
*/
|
|
693
701
|
templates?: TTemplates;
|
|
694
702
|
}
|
|
703
|
+
declare class BetterZapClientError extends Error {
|
|
704
|
+
status: number;
|
|
705
|
+
code?: string;
|
|
706
|
+
details?: unknown;
|
|
707
|
+
body?: unknown;
|
|
708
|
+
constructor(message: string, options: {
|
|
709
|
+
status: number;
|
|
710
|
+
code?: string;
|
|
711
|
+
details?: unknown;
|
|
712
|
+
body?: unknown;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
695
715
|
interface SendTextParams {
|
|
696
716
|
to: string;
|
|
697
717
|
body: string;
|
|
@@ -766,4 +786,4 @@ interface ZapClient<TTemplates extends TemplateRegistry = {}> {
|
|
|
766
786
|
}
|
|
767
787
|
declare function createZapClient<TTemplates extends TemplateRegistry = {}>(options?: ZapClientOptions<TTemplates>): ZapClient<TTemplates>;
|
|
768
788
|
//#endregion
|
|
769
|
-
export {
|
|
789
|
+
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
|
}
|
|
@@ -692,6 +700,18 @@ interface ZapClientOptions<TTemplates extends TemplateRegistry = {}> {
|
|
|
692
700
|
*/
|
|
693
701
|
templates?: TTemplates;
|
|
694
702
|
}
|
|
703
|
+
declare class BetterZapClientError extends Error {
|
|
704
|
+
status: number;
|
|
705
|
+
code?: string;
|
|
706
|
+
details?: unknown;
|
|
707
|
+
body?: unknown;
|
|
708
|
+
constructor(message: string, options: {
|
|
709
|
+
status: number;
|
|
710
|
+
code?: string;
|
|
711
|
+
details?: unknown;
|
|
712
|
+
body?: unknown;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
695
715
|
interface SendTextParams {
|
|
696
716
|
to: string;
|
|
697
717
|
body: string;
|
|
@@ -766,4 +786,4 @@ interface ZapClient<TTemplates extends TemplateRegistry = {}> {
|
|
|
766
786
|
}
|
|
767
787
|
declare function createZapClient<TTemplates extends TemplateRegistry = {}>(options?: ZapClientOptions<TTemplates>): ZapClient<TTemplates>;
|
|
768
788
|
//#endregion
|
|
769
|
-
export {
|
|
789
|
+
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-B_VYCUHu.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-D-wgralM.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,
|
|
@@ -564,17 +659,24 @@ function serializeTemplateParameter(parameter, value) {
|
|
|
564
659
|
}
|
|
565
660
|
}
|
|
566
661
|
//#endregion
|
|
662
|
+
exports.BetterZapClientError = require_client.BetterZapClientError;
|
|
567
663
|
exports.EMPTY_TEMPLATE_REGISTRY = EMPTY_TEMPLATE_REGISTRY;
|
|
664
|
+
exports.FREEFORM_MESSAGE_WINDOW_MS = FREEFORM_MESSAGE_WINDOW_MS;
|
|
568
665
|
exports.MessageLoggerService = MessageLoggerService;
|
|
569
666
|
exports.WHATSAPP_MESSAGE_TYPES = WHATSAPP_MESSAGE_TYPES;
|
|
570
667
|
exports.WhatsAppService = WhatsAppService;
|
|
668
|
+
exports.createFreeformMessageWindow = createFreeformMessageWindow;
|
|
571
669
|
exports.createLogger = createLogger;
|
|
572
670
|
exports.createZapClient = require_client.createZapClient;
|
|
573
671
|
exports.defineTemplates = defineTemplates;
|
|
574
672
|
exports.delay = delay;
|
|
575
673
|
exports.formatPhone = formatPhone;
|
|
674
|
+
exports.getLatestIncomingMessageAt = getLatestIncomingMessageAt;
|
|
576
675
|
exports.getTemplateNames = getTemplateNames;
|
|
577
676
|
exports.hasConfiguredTemplates = hasConfiguredTemplates;
|
|
578
677
|
exports.noopLogger = noopLogger;
|
|
678
|
+
exports.normalizeConversationRecord = normalizeConversationRecord;
|
|
679
|
+
exports.normalizeConversationRecords = normalizeConversationRecords;
|
|
680
|
+
exports.resolveConversationFreeformMessageWindow = resolveConversationFreeformMessageWindow;
|
|
579
681
|
exports.serializeError = serializeError;
|
|
580
682
|
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-B_VYCUHu.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-D-wgralM.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,
|
|
@@ -563,4 +658,4 @@ function serializeTemplateParameter(parameter, value) {
|
|
|
563
658
|
}
|
|
564
659
|
}
|
|
565
660
|
//#endregion
|
|
566
|
-
export { EMPTY_TEMPLATE_REGISTRY, MessageLoggerService, WHATSAPP_MESSAGE_TYPES, WhatsAppService, createLogger, createZapClient, defineTemplates, delay, formatPhone, getTemplateNames, hasConfiguredTemplates, noopLogger, serializeError, serializeTemplateFromRegistry };
|
|
661
|
+
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 };
|