whatsapp-cloud 0.0.4 → 0.0.6
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/CHANGELOG.md +13 -0
- package/agent_docs/INCOMING_MESSAGES_BRAINSTORM.md +500 -0
- package/cloud-api-docs/webhooks/endpoint.md +112 -0
- package/cloud-api-docs/webhooks/overview.md +154 -0
- package/docs/DEVELOPMENT.md +154 -0
- package/docs/webhooks.md +76 -0
- package/package.json +6 -2
- package/src/client/HttpClient.ts +43 -6
- package/src/client/WhatsAppClient.ts +6 -0
- package/src/examples/main.ts +9 -0
- package/src/examples/template.ts +134 -0
- package/src/index.ts +7 -0
- package/src/schemas/client.ts +2 -2
- package/src/schemas/index.ts +2 -0
- package/src/schemas/templates/component.ts +145 -0
- package/src/schemas/templates/index.ts +4 -0
- package/src/schemas/templates/request.ts +78 -0
- package/src/schemas/templates/response.ts +64 -0
- package/src/schemas/webhooks/incoming-message.ts +38 -0
- package/src/schemas/webhooks/index.ts +3 -0
- package/src/schemas/webhooks/payload.ts +56 -0
- package/src/services/accounts/AccountsClient.ts +6 -14
- package/src/services/accounts/AccountsService.ts +19 -21
- package/src/services/accounts/methods/list-phone-numbers.ts +1 -2
- package/src/services/business/BusinessClient.ts +1 -9
- package/src/services/business/BusinessService.ts +19 -21
- package/src/services/business/methods/list-accounts.ts +1 -2
- package/src/services/messages/MessagesClient.ts +2 -6
- package/src/services/messages/MessagesService.ts +42 -22
- package/src/services/templates/TemplatesClient.ts +35 -0
- package/src/services/templates/TemplatesService.ts +117 -0
- package/src/services/templates/index.ts +3 -0
- package/src/services/templates/methods/create.ts +27 -0
- package/src/services/templates/methods/delete.ts +38 -0
- package/src/services/templates/methods/get.ts +23 -0
- package/src/services/templates/methods/list.ts +36 -0
- package/src/services/templates/methods/update.ts +35 -0
- package/src/services/webhooks/WebhooksService.ts +214 -0
- package/src/services/webhooks/index.ts +3 -0
- package/src/services/webhooks/utils/extract-messages.ts +25 -0
- package/src/services/webhooks/utils/extract-statuses.ts +25 -0
- package/src/services/webhooks/utils/verify.ts +29 -0
- package/src/types/index.ts +2 -0
- package/src/types/templates/component.ts +33 -0
- package/src/types/templates/index.ts +4 -0
- package/src/types/templates/request.ts +28 -0
- package/src/types/templates/response.ts +34 -0
- package/src/types/webhooks/incoming-message.ts +16 -0
- package/src/types/webhooks/index.ts +3 -0
- package/src/types/webhooks/payload.ts +8 -0
- package/tsconfig.json +2 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { HttpClient } from "../../client/HttpClient";
|
|
2
2
|
import { sendText } from "./methods/send-text";
|
|
3
3
|
import { sendImage } from "./methods/send-image";
|
|
4
4
|
import { sendLocation } from "./methods/send-location";
|
|
@@ -16,62 +16,82 @@ import type { MessageResponse } from "../../types/messages/response";
|
|
|
16
16
|
/**
|
|
17
17
|
* Messages service for sending WhatsApp messages
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* This service handles message operations.
|
|
20
|
+
* It supports both a globally configured phoneNumberId (in WhatsAppClient)
|
|
21
|
+
* and per-request phoneNumberId overrides.
|
|
22
22
|
*/
|
|
23
23
|
export class MessagesService {
|
|
24
|
-
private readonly
|
|
24
|
+
constructor(private readonly httpClient: HttpClient) {}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Helper to create a Scoped Client (prefer override, fallback to config)
|
|
28
|
+
*/
|
|
29
|
+
private getClient(overrideId?: string): MessagesClient {
|
|
30
|
+
const id = overrideId || this.httpClient.phoneNumberId;
|
|
31
|
+
if (!id) {
|
|
29
32
|
throw new WhatsAppValidationError(
|
|
30
|
-
"phoneNumberId is required
|
|
33
|
+
"phoneNumberId is required. Provide it in WhatsAppClient config or as a parameter.",
|
|
31
34
|
"phoneNumberId"
|
|
32
35
|
);
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
httpClient,
|
|
38
|
-
httpClient.phoneNumberId
|
|
39
|
-
);
|
|
38
|
+
// Just wrap the existing httpClient
|
|
39
|
+
return new MessagesClient(this.httpClient, id);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Send a text message
|
|
44
44
|
*
|
|
45
45
|
* @param request - Text message request (to, text)
|
|
46
|
+
* @param phoneNumberId - Optional phone number ID (overrides client config)
|
|
46
47
|
*/
|
|
47
|
-
async sendText(
|
|
48
|
-
|
|
48
|
+
async sendText(
|
|
49
|
+
request: SendTextRequest,
|
|
50
|
+
phoneNumberId?: string
|
|
51
|
+
): Promise<MessageResponse> {
|
|
52
|
+
const client = this.getClient(phoneNumberId);
|
|
53
|
+
return sendText(client, request);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
52
57
|
* Send an image message
|
|
53
58
|
*
|
|
54
59
|
* @param request - Image message request (to, image)
|
|
60
|
+
* @param phoneNumberId - Optional phone number ID (overrides client config)
|
|
55
61
|
*/
|
|
56
|
-
async sendImage(
|
|
57
|
-
|
|
62
|
+
async sendImage(
|
|
63
|
+
request: SendImageRequest,
|
|
64
|
+
phoneNumberId?: string
|
|
65
|
+
): Promise<MessageResponse> {
|
|
66
|
+
const client = this.getClient(phoneNumberId);
|
|
67
|
+
return sendImage(client, request);
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/**
|
|
61
71
|
* Send a location message
|
|
62
72
|
*
|
|
63
73
|
* @param request - Location message request (to, location)
|
|
74
|
+
* @param phoneNumberId - Optional phone number ID (overrides client config)
|
|
64
75
|
*/
|
|
65
|
-
async sendLocation(
|
|
66
|
-
|
|
76
|
+
async sendLocation(
|
|
77
|
+
request: SendLocationRequest,
|
|
78
|
+
phoneNumberId?: string
|
|
79
|
+
): Promise<MessageResponse> {
|
|
80
|
+
const client = this.getClient(phoneNumberId);
|
|
81
|
+
return sendLocation(client, request);
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
/**
|
|
70
85
|
* Send a reaction message
|
|
71
86
|
*
|
|
72
87
|
* @param request - Reaction message request (to, reaction)
|
|
88
|
+
* @param phoneNumberId - Optional phone number ID (overrides client config)
|
|
73
89
|
*/
|
|
74
|
-
async sendReaction(
|
|
75
|
-
|
|
90
|
+
async sendReaction(
|
|
91
|
+
request: SendReactionRequest,
|
|
92
|
+
phoneNumberId?: string
|
|
93
|
+
): Promise<MessageResponse> {
|
|
94
|
+
const client = this.getClient(phoneNumberId);
|
|
95
|
+
return sendReaction(client, request);
|
|
76
96
|
}
|
|
77
97
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HttpClient } from "../../client/HttpClient";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Templates client - wraps HttpClient with WABA ID as base endpoint
|
|
5
|
+
*
|
|
6
|
+
* This client automatically prepends `/${businessAccountId}` to all request paths.
|
|
7
|
+
* Similar to AccountsClient, since templates are scoped to WABA.
|
|
8
|
+
*/
|
|
9
|
+
export class TemplatesClient {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly httpClient: HttpClient,
|
|
12
|
+
private readonly businessAccountId: string
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Make a GET request with WABA ID prefix
|
|
17
|
+
*/
|
|
18
|
+
async get<T>(path: string): Promise<T> {
|
|
19
|
+
return this.httpClient.get<T>(`/${this.businessAccountId}${path}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Make a POST request with WABA ID prefix
|
|
24
|
+
*/
|
|
25
|
+
async post<T>(path: string, body: unknown): Promise<T> {
|
|
26
|
+
return this.httpClient.post<T>(`/${this.businessAccountId}${path}`, body);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Make a DELETE request with WABA ID prefix
|
|
31
|
+
*/
|
|
32
|
+
async delete<T>(path: string): Promise<T> {
|
|
33
|
+
return this.httpClient.delete<T>(`/${this.businessAccountId}${path}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { HttpClient } from "../../client/HttpClient";
|
|
2
|
+
import { TemplatesClient } from "./TemplatesClient";
|
|
3
|
+
import { createTemplate } from "./methods/create";
|
|
4
|
+
import { listTemplates } from "./methods/list";
|
|
5
|
+
import { getTemplate } from "./methods/get";
|
|
6
|
+
import { updateTemplate } from "./methods/update";
|
|
7
|
+
import { deleteTemplate } from "./methods/delete";
|
|
8
|
+
import { WhatsAppValidationError } from "../../errors";
|
|
9
|
+
import type {
|
|
10
|
+
CreateTemplateRequest,
|
|
11
|
+
UpdateTemplateRequest,
|
|
12
|
+
ListTemplatesRequest,
|
|
13
|
+
DeleteTemplateRequest,
|
|
14
|
+
} from "../../types/templates/request";
|
|
15
|
+
import type {
|
|
16
|
+
CreateTemplateResponse,
|
|
17
|
+
ListTemplatesResponse,
|
|
18
|
+
TemplateResponse,
|
|
19
|
+
UpdateTemplateResponse,
|
|
20
|
+
DeleteTemplateResponse,
|
|
21
|
+
} from "../../types/templates/response";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Templates service for managing message templates
|
|
25
|
+
*
|
|
26
|
+
* This service handles template operations like creating, listing, and deleting templates.
|
|
27
|
+
* It supports both a globally configured businessAccountId (in WhatsAppClient)
|
|
28
|
+
* and per-request businessAccountId overrides.
|
|
29
|
+
*
|
|
30
|
+
* Note: Get and Update operations use template ID directly (no WABA prefix needed).
|
|
31
|
+
*/
|
|
32
|
+
export class TemplatesService {
|
|
33
|
+
constructor(private readonly httpClient: HttpClient) {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Helper to create a Scoped Client (prefer override, fallback to config)
|
|
37
|
+
*/
|
|
38
|
+
private getClient(overrideId?: string): TemplatesClient {
|
|
39
|
+
const id = overrideId || this.httpClient.businessAccountId;
|
|
40
|
+
if (!id) {
|
|
41
|
+
throw new WhatsAppValidationError(
|
|
42
|
+
"businessAccountId (WABA ID) is required for templates. Provide it in WhatsAppClient config or as a parameter.",
|
|
43
|
+
"businessAccountId"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return new TemplatesClient(this.httpClient, id);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a message template
|
|
52
|
+
*
|
|
53
|
+
* @param request - Template creation request
|
|
54
|
+
* @param businessAccountId - Optional WABA ID (overrides client config)
|
|
55
|
+
*/
|
|
56
|
+
async create(
|
|
57
|
+
request: CreateTemplateRequest,
|
|
58
|
+
businessAccountId?: string
|
|
59
|
+
): Promise<CreateTemplateResponse> {
|
|
60
|
+
const client = this.getClient(businessAccountId);
|
|
61
|
+
return createTemplate(client, request);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* List message templates
|
|
66
|
+
*
|
|
67
|
+
* @param options - Optional filter options (name)
|
|
68
|
+
* @param businessAccountId - Optional WABA ID (overrides client config)
|
|
69
|
+
*/
|
|
70
|
+
async list(
|
|
71
|
+
options?: ListTemplatesRequest,
|
|
72
|
+
businessAccountId?: string
|
|
73
|
+
): Promise<ListTemplatesResponse> {
|
|
74
|
+
const client = this.getClient(businessAccountId);
|
|
75
|
+
return listTemplates(client, options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get a template by ID
|
|
80
|
+
*
|
|
81
|
+
* Note: This uses the template ID directly (no WABA prefix needed)
|
|
82
|
+
*
|
|
83
|
+
* @param templateId - Template ID
|
|
84
|
+
*/
|
|
85
|
+
async get(templateId: string): Promise<TemplateResponse> {
|
|
86
|
+
return getTemplate(this.httpClient, templateId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Update a template
|
|
91
|
+
*
|
|
92
|
+
* Note: This uses the template ID directly (no WABA prefix needed)
|
|
93
|
+
*
|
|
94
|
+
* @param templateId - Template ID
|
|
95
|
+
* @param request - Template update request
|
|
96
|
+
*/
|
|
97
|
+
async update(
|
|
98
|
+
templateId: string,
|
|
99
|
+
request: UpdateTemplateRequest
|
|
100
|
+
): Promise<UpdateTemplateResponse> {
|
|
101
|
+
return updateTemplate(this.httpClient, templateId, request);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Delete a template
|
|
106
|
+
*
|
|
107
|
+
* @param options - Delete options (name or hsm_id)
|
|
108
|
+
* @param businessAccountId - Optional WABA ID (overrides client config)
|
|
109
|
+
*/
|
|
110
|
+
async delete(
|
|
111
|
+
options: DeleteTemplateRequest,
|
|
112
|
+
businessAccountId?: string
|
|
113
|
+
): Promise<DeleteTemplateResponse> {
|
|
114
|
+
const client = this.getClient(businessAccountId);
|
|
115
|
+
return deleteTemplate(client, options);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TemplatesClient } from "../TemplatesClient";
|
|
2
|
+
import { createTemplateRequestSchema } from "../../../schemas/templates/request";
|
|
3
|
+
import type { CreateTemplateRequest } from "../../../types/templates/request";
|
|
4
|
+
import type { CreateTemplateResponse } from "../../../types/templates/response";
|
|
5
|
+
import { transformZodError } from "../../../utils/zod-error";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a message template
|
|
9
|
+
*
|
|
10
|
+
* @param templatesClient - Templates client with WABA ID baked in
|
|
11
|
+
* @param request - Template creation request
|
|
12
|
+
*/
|
|
13
|
+
export async function createTemplate(
|
|
14
|
+
templatesClient: TemplatesClient,
|
|
15
|
+
request: CreateTemplateRequest
|
|
16
|
+
): Promise<CreateTemplateResponse> {
|
|
17
|
+
// Validate request with schema - throws WhatsAppValidationError if invalid
|
|
18
|
+
const result = createTemplateRequestSchema.safeParse(request);
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
throw transformZodError(result.error);
|
|
21
|
+
}
|
|
22
|
+
const data = result.data;
|
|
23
|
+
|
|
24
|
+
// Make API request - templatesClient handles the WABA ID prefix automatically
|
|
25
|
+
return templatesClient.post<CreateTemplateResponse>("/message_templates", data);
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TemplatesClient } from "../TemplatesClient";
|
|
2
|
+
import { deleteTemplateRequestSchema } from "../../../schemas/templates/request";
|
|
3
|
+
import type { DeleteTemplateRequest } from "../../../types/templates/request";
|
|
4
|
+
import type { DeleteTemplateResponse } from "../../../types/templates/response";
|
|
5
|
+
import { transformZodError } from "../../../utils/zod-error";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Delete a template
|
|
9
|
+
*
|
|
10
|
+
* @param templatesClient - Templates client with WABA ID baked in
|
|
11
|
+
* @param options - Delete options (name or hsm_id)
|
|
12
|
+
*/
|
|
13
|
+
export async function deleteTemplate(
|
|
14
|
+
templatesClient: TemplatesClient,
|
|
15
|
+
options: DeleteTemplateRequest
|
|
16
|
+
): Promise<DeleteTemplateResponse> {
|
|
17
|
+
// Validate request with schema - throws WhatsAppValidationError if invalid
|
|
18
|
+
const result = deleteTemplateRequestSchema.safeParse(options);
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
throw transformZodError(result.error);
|
|
21
|
+
}
|
|
22
|
+
const data = result.data;
|
|
23
|
+
|
|
24
|
+
// Build query string
|
|
25
|
+
const params = new URLSearchParams();
|
|
26
|
+
if (data.name) {
|
|
27
|
+
params.append("name", data.name);
|
|
28
|
+
}
|
|
29
|
+
if (data.hsm_id) {
|
|
30
|
+
params.append("hsm_id", data.hsm_id);
|
|
31
|
+
}
|
|
32
|
+
const queryString = params.toString();
|
|
33
|
+
const path = `/message_templates?${queryString}`;
|
|
34
|
+
|
|
35
|
+
// Make API request - templatesClient handles the WABA ID prefix automatically
|
|
36
|
+
return templatesClient.delete<DeleteTemplateResponse>(path);
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HttpClient } from "../../../client/HttpClient";
|
|
2
|
+
import type { TemplateResponse } from "../../../types/templates/response";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get a template by ID
|
|
6
|
+
*
|
|
7
|
+
* Note: This uses the template ID directly (no WABA prefix needed)
|
|
8
|
+
*
|
|
9
|
+
* @param httpClient - HTTP client
|
|
10
|
+
* @param templateId - Template ID
|
|
11
|
+
*/
|
|
12
|
+
export async function getTemplate(
|
|
13
|
+
httpClient: HttpClient,
|
|
14
|
+
templateId: string
|
|
15
|
+
): Promise<TemplateResponse> {
|
|
16
|
+
if (!templateId || templateId.trim().length === 0) {
|
|
17
|
+
throw new Error("Template ID is required");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Make API request - template ID is used directly, no WABA prefix
|
|
21
|
+
return httpClient.get<TemplateResponse>(`/${templateId}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TemplatesClient } from "../TemplatesClient";
|
|
2
|
+
import { listTemplatesRequestSchema } from "../../../schemas/templates/request";
|
|
3
|
+
import type { ListTemplatesRequest } from "../../../types/templates/request";
|
|
4
|
+
import type { ListTemplatesResponse } from "../../../types/templates/response";
|
|
5
|
+
import { transformZodError } from "../../../utils/zod-error";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* List message templates
|
|
9
|
+
*
|
|
10
|
+
* @param templatesClient - Templates client with WABA ID baked in
|
|
11
|
+
* @param options - Optional filter options (name)
|
|
12
|
+
*/
|
|
13
|
+
export async function listTemplates(
|
|
14
|
+
templatesClient: TemplatesClient,
|
|
15
|
+
options?: ListTemplatesRequest
|
|
16
|
+
): Promise<ListTemplatesResponse> {
|
|
17
|
+
// Validate options if provided
|
|
18
|
+
if (options) {
|
|
19
|
+
const result = listTemplatesRequestSchema.safeParse(options);
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
throw transformZodError(result.error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Build query string
|
|
26
|
+
const params = new URLSearchParams();
|
|
27
|
+
if (options?.name) {
|
|
28
|
+
params.append("name", options.name);
|
|
29
|
+
}
|
|
30
|
+
const queryString = params.toString();
|
|
31
|
+
const path = queryString ? `/message_templates?${queryString}` : "/message_templates";
|
|
32
|
+
|
|
33
|
+
// Make API request - templatesClient handles the WABA ID prefix automatically
|
|
34
|
+
return templatesClient.get<ListTemplatesResponse>(path);
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HttpClient } from "../../../client/HttpClient";
|
|
2
|
+
import { updateTemplateRequestSchema } from "../../../schemas/templates/request";
|
|
3
|
+
import type { UpdateTemplateRequest } from "../../../types/templates/request";
|
|
4
|
+
import type { UpdateTemplateResponse } from "../../../types/templates/response";
|
|
5
|
+
import { transformZodError } from "../../../utils/zod-error";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Update a template
|
|
9
|
+
*
|
|
10
|
+
* Note: This uses the template ID directly (no WABA prefix needed)
|
|
11
|
+
*
|
|
12
|
+
* @param httpClient - HTTP client
|
|
13
|
+
* @param templateId - Template ID
|
|
14
|
+
* @param request - Template update request
|
|
15
|
+
*/
|
|
16
|
+
export async function updateTemplate(
|
|
17
|
+
httpClient: HttpClient,
|
|
18
|
+
templateId: string,
|
|
19
|
+
request: UpdateTemplateRequest
|
|
20
|
+
): Promise<UpdateTemplateResponse> {
|
|
21
|
+
if (!templateId || templateId.trim().length === 0) {
|
|
22
|
+
throw new Error("Template ID is required");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate request with schema - throws WhatsAppValidationError if invalid
|
|
26
|
+
const result = updateTemplateRequestSchema.safeParse(request);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
throw transformZodError(result.error);
|
|
29
|
+
}
|
|
30
|
+
const data = result.data;
|
|
31
|
+
|
|
32
|
+
// Make API request - template ID is used directly, no WABA prefix
|
|
33
|
+
return httpClient.post<UpdateTemplateResponse>(`/${templateId}`, data);
|
|
34
|
+
}
|
|
35
|
+
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { HttpClient } from "../../client/HttpClient";
|
|
2
|
+
import { extractMessages } from "./utils/extract-messages";
|
|
3
|
+
import { extractStatuses } from "./utils/extract-statuses";
|
|
4
|
+
import { verifyWebhook } from "./utils/verify";
|
|
5
|
+
import { webhookPayloadSchema } from "../../schemas/webhooks/payload";
|
|
6
|
+
import type {
|
|
7
|
+
WebhookPayload,
|
|
8
|
+
IncomingTextMessage,
|
|
9
|
+
IncomingMessage,
|
|
10
|
+
} from "../../types/webhooks";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Context provided to message handlers
|
|
14
|
+
* Contains metadata and contact info (message is passed separately)
|
|
15
|
+
*/
|
|
16
|
+
export type MessageContext = {
|
|
17
|
+
metadata: {
|
|
18
|
+
phoneNumberId: string;
|
|
19
|
+
displayPhoneNumber: string;
|
|
20
|
+
wabaId: string;
|
|
21
|
+
};
|
|
22
|
+
contact?: {
|
|
23
|
+
name: string;
|
|
24
|
+
waId: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handler functions for different message types
|
|
30
|
+
* Receives message and context separately - message is the focus, context is optional metadata
|
|
31
|
+
*/
|
|
32
|
+
export type MessageHandlers = {
|
|
33
|
+
text?: (
|
|
34
|
+
message: IncomingTextMessage,
|
|
35
|
+
context: MessageContext
|
|
36
|
+
) => Promise<void> | void;
|
|
37
|
+
// Future: image, audio, video, etc.
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Options for handle() method
|
|
42
|
+
*/
|
|
43
|
+
export type HandleOptions = {
|
|
44
|
+
/**
|
|
45
|
+
* Error handler called when a message handler throws an error
|
|
46
|
+
* If not provided, errors are logged and processing continues
|
|
47
|
+
*/
|
|
48
|
+
onError?: (error: Error, message: IncomingMessage) => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Webhooks service for handling incoming webhook payloads
|
|
53
|
+
*
|
|
54
|
+
* Provides utilities for extracting messages and a convenience handler
|
|
55
|
+
* for type-safe message processing.
|
|
56
|
+
*/
|
|
57
|
+
export class WebhooksService {
|
|
58
|
+
constructor(private readonly httpClient: HttpClient) {}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Verify webhook GET request from Meta
|
|
62
|
+
*
|
|
63
|
+
* Meta sends GET requests to verify webhook endpoints during setup.
|
|
64
|
+
* Returns the challenge string if valid, null if invalid.
|
|
65
|
+
*
|
|
66
|
+
* @param query - Query parameters from GET request
|
|
67
|
+
* @param verifyToken - Your verification token (stored on your server)
|
|
68
|
+
* @returns Challenge string if valid, null if invalid
|
|
69
|
+
*/
|
|
70
|
+
verify(
|
|
71
|
+
query: {
|
|
72
|
+
"hub.mode"?: string;
|
|
73
|
+
"hub.verify_token"?: string;
|
|
74
|
+
"hub.challenge"?: string;
|
|
75
|
+
},
|
|
76
|
+
verifyToken: string
|
|
77
|
+
): string | null {
|
|
78
|
+
return verifyWebhook(query, verifyToken);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract all incoming messages from webhook payload
|
|
83
|
+
*
|
|
84
|
+
* Low-level utility that flattens the nested webhook structure
|
|
85
|
+
* and returns messages directly.
|
|
86
|
+
*
|
|
87
|
+
* @param payload - Webhook payload from Meta
|
|
88
|
+
* @returns Flat array of incoming messages
|
|
89
|
+
*/
|
|
90
|
+
extractMessages(payload: WebhookPayload): IncomingMessage[] {
|
|
91
|
+
return extractMessages(payload);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Extract status updates from webhook payload
|
|
96
|
+
*
|
|
97
|
+
* Low-level utility for extracting status updates for outgoing messages.
|
|
98
|
+
*
|
|
99
|
+
* @param payload - Webhook payload from Meta
|
|
100
|
+
* @returns Flat array of status updates
|
|
101
|
+
*/
|
|
102
|
+
extractStatuses(payload: WebhookPayload): unknown[] {
|
|
103
|
+
return extractStatuses(payload);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate webhook payload structure
|
|
108
|
+
*
|
|
109
|
+
* Validates the payload against the schema. Logs errors if malformed
|
|
110
|
+
* but doesn't throw, allowing processing to continue.
|
|
111
|
+
*
|
|
112
|
+
* @param payload - Raw payload to validate
|
|
113
|
+
* @returns Validated payload if valid, original payload if invalid (with logged error)
|
|
114
|
+
*/
|
|
115
|
+
private validatePayload(payload: unknown): WebhookPayload {
|
|
116
|
+
const result = webhookPayloadSchema.safeParse(payload);
|
|
117
|
+
if (!result.success) {
|
|
118
|
+
console.error(
|
|
119
|
+
"Webhook payload validation failed:",
|
|
120
|
+
result.error.format()
|
|
121
|
+
);
|
|
122
|
+
// Return as-is (TypeScript will treat it as WebhookPayload, but it's actually invalid)
|
|
123
|
+
// This allows processing to continue, but handlers should be defensive
|
|
124
|
+
return payload as WebhookPayload;
|
|
125
|
+
}
|
|
126
|
+
return result.data;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Handle webhook payload with type-safe callbacks
|
|
131
|
+
*
|
|
132
|
+
* High-level convenience method that extracts messages and dispatches
|
|
133
|
+
* them to appropriate handlers based on message type.
|
|
134
|
+
*
|
|
135
|
+
* **Important**: This method returns quickly to allow fast webhook responses.
|
|
136
|
+
* Handlers are processed asynchronously. If you need to await handler completion,
|
|
137
|
+
* use the low-level `extractMessages()` method instead.
|
|
138
|
+
*
|
|
139
|
+
* @param payload - Webhook payload from Meta (will be validated)
|
|
140
|
+
* @param handlers - Object with handler functions for each message type
|
|
141
|
+
* @param options - Optional error handling configuration
|
|
142
|
+
*/
|
|
143
|
+
handle(
|
|
144
|
+
payload: unknown,
|
|
145
|
+
handlers: MessageHandlers,
|
|
146
|
+
options?: HandleOptions
|
|
147
|
+
): void {
|
|
148
|
+
// Validate payload (logs error if malformed, but continues)
|
|
149
|
+
const validatedPayload = this.validatePayload(payload);
|
|
150
|
+
|
|
151
|
+
// Extract metadata and contacts from payload for context
|
|
152
|
+
for (const entry of validatedPayload.entry) {
|
|
153
|
+
for (const change of entry.changes) {
|
|
154
|
+
if (change.field === "messages" && change.value.messages) {
|
|
155
|
+
const metadata = {
|
|
156
|
+
phoneNumberId: change.value.metadata.phone_number_id,
|
|
157
|
+
displayPhoneNumber: change.value.metadata.display_phone_number,
|
|
158
|
+
wabaId: entry.id,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const contacts = change.value.contacts || [];
|
|
162
|
+
|
|
163
|
+
// Process each message with its context
|
|
164
|
+
for (const message of change.value.messages) {
|
|
165
|
+
// Find contact for this message (match by wa_id)
|
|
166
|
+
const contact = contacts.find((c) => c.wa_id === message.from);
|
|
167
|
+
|
|
168
|
+
// Build context (metadata + contact, no message duplication)
|
|
169
|
+
const context: MessageContext = {
|
|
170
|
+
metadata,
|
|
171
|
+
...(contact && {
|
|
172
|
+
contact: {
|
|
173
|
+
name: contact.profile.name,
|
|
174
|
+
waId: contact.wa_id,
|
|
175
|
+
},
|
|
176
|
+
}),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// Process handler asynchronously (don't await)
|
|
180
|
+
// This allows long-running handlers without blocking webhook response
|
|
181
|
+
Promise.resolve()
|
|
182
|
+
.then(async () => {
|
|
183
|
+
// Type-safe dispatch based on message type
|
|
184
|
+
switch (message.type) {
|
|
185
|
+
case "text":
|
|
186
|
+
if (handlers.text) {
|
|
187
|
+
await handlers.text(message, context);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
// Future: image, audio, video, etc.
|
|
192
|
+
default:
|
|
193
|
+
// Unhandled message type - silently continue
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
.catch((error) => {
|
|
198
|
+
// Handle errors in handler execution
|
|
199
|
+
if (options?.onError) {
|
|
200
|
+
options.onError(error as Error, message);
|
|
201
|
+
} else {
|
|
202
|
+
// Default: log and continue (don't break webhook response)
|
|
203
|
+
console.error(
|
|
204
|
+
`Error handling ${message.type} message ${message.id}:`,
|
|
205
|
+
error
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WebhookPayload } from "../../../types/webhooks";
|
|
2
|
+
import type { IncomingMessage } from "../../../types/webhooks/incoming-message";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extract all incoming messages from webhook payload
|
|
6
|
+
*
|
|
7
|
+
* Flattens the nested structure: entry[].changes[].value.messages[]
|
|
8
|
+
* Returns a flat array of messages directly
|
|
9
|
+
*
|
|
10
|
+
* @param payload - Webhook payload from Meta
|
|
11
|
+
* @returns Flat array of incoming messages
|
|
12
|
+
*/
|
|
13
|
+
export function extractMessages(payload: WebhookPayload): IncomingMessage[] {
|
|
14
|
+
const messages: IncomingMessage[] = [];
|
|
15
|
+
|
|
16
|
+
for (const entry of payload.entry) {
|
|
17
|
+
for (const change of entry.changes) {
|
|
18
|
+
if (change.field === "messages" && change.value.messages) {
|
|
19
|
+
messages.push(...change.value.messages);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return messages;
|
|
25
|
+
}
|