whatsapp-cloud 0.0.5 → 0.0.7
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 +14 -0
- package/docs/DEVELOPMENT.md +154 -0
- package/docs/webhooks.md +104 -0
- package/package.json +1 -1
- package/src/client/HttpClient.ts +32 -1
- package/src/client/WhatsAppClient.ts +3 -0
- package/src/index.ts +7 -0
- package/src/schemas/index.ts +1 -0
- package/src/schemas/templates/request.ts +2 -2
- package/src/schemas/templates/response.ts +2 -2
- package/src/schemas/webhooks/incoming-message.ts +72 -0
- package/src/schemas/webhooks/index.ts +3 -0
- package/src/schemas/webhooks/payload.ts +56 -0
- package/src/services/templates/TemplatesService.ts +6 -6
- package/src/services/templates/methods/create.ts +8 -6
- package/src/services/templates/methods/get.ts +3 -3
- package/src/services/templates/methods/update.ts +4 -4
- package/src/services/webhooks/WebhooksService.ts +265 -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 +1 -0
- package/src/types/templates/request.ts +4 -4
- package/src/types/templates/response.ts +3 -3
- package/src/types/webhooks/incoming-message.ts +27 -0
- package/src/types/webhooks/index.ts +3 -0
- package/src/types/webhooks/payload.ts +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# whatzapp
|
|
2
2
|
|
|
3
|
+
## 0.0.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2024d31: add audio and image handler
|
|
8
|
+
- f2501c2: add template types
|
|
9
|
+
|
|
10
|
+
## 0.0.6
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- a46c084: export types
|
|
15
|
+
- 175d774: add webhooks namespace
|
|
16
|
+
|
|
3
17
|
## 0.0.5
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Development Guide
|
|
2
|
+
|
|
3
|
+
## Local Development with npm/pnpm link
|
|
4
|
+
|
|
5
|
+
To test the SDK in your own project without publishing to npm:
|
|
6
|
+
|
|
7
|
+
### Step 1: Link the package (in whatsapp-cloud directory)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd /Users/lukas/Developer/whatsapp-cloud
|
|
11
|
+
pnpm build # Build the package first
|
|
12
|
+
pnpm link --global # Creates a global symlink
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or with npm:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm link
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Step 2: Link in your project
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd /path/to/your/project
|
|
25
|
+
pnpm link whatsapp-cloud
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or with npm:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm link whatsapp-cloud
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**What this does:** Creates a symlink in your project's `node_modules` pointing to your local package. You don't need to:
|
|
35
|
+
|
|
36
|
+
- Add it to `package.json` (link handles it)
|
|
37
|
+
- Run `pnpm install` (link is enough)
|
|
38
|
+
- Publish to npm
|
|
39
|
+
|
|
40
|
+
### Step 3: Use it in your project
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { WhatsAppClient, type IncomingTextMessage } from "whatsapp-cloud";
|
|
44
|
+
|
|
45
|
+
const client = new WhatsAppClient({
|
|
46
|
+
accessToken: "...",
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Important Notes
|
|
51
|
+
|
|
52
|
+
- **Rebuild after changes**: After making changes to whatsapp-cloud, run `pnpm build` in the whatsapp-cloud directory
|
|
53
|
+
- **Hot reload**: Some bundlers (like Next.js) may need a restart to pick up changes
|
|
54
|
+
- **Unlink**: When done, unlink with `pnpm unlink whatsapp-cloud` in your project
|
|
55
|
+
|
|
56
|
+
## Production / CI/CD
|
|
57
|
+
|
|
58
|
+
For production builds and CI/CD, you have a few options:
|
|
59
|
+
|
|
60
|
+
### Option 1: Publish to npm (Recommended)
|
|
61
|
+
|
|
62
|
+
Once ready, publish the package:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cd /Users/lukas/Developer/whatsapp-cloud
|
|
66
|
+
pnpm publish
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then in your project's `package.json`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"whatsapp-cloud": "^0.0.5"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Option 2: Git dependency (For private repos)
|
|
80
|
+
|
|
81
|
+
If your repo is private, use git dependency:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"dependencies": {
|
|
86
|
+
"whatsapp-cloud": "git+https://github.com/your-username/whatsapp-cloud.git"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Option 3: Local file path (For monorepos)
|
|
92
|
+
|
|
93
|
+
If both projects are in the same repo/monorepo:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"dependencies": {
|
|
98
|
+
"whatsapp-cloud": "file:../whatsapp-cloud"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Option 4: Conditional linking (Dev vs Prod)
|
|
104
|
+
|
|
105
|
+
Use environment detection:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"dependencies": {
|
|
110
|
+
"whatsapp-cloud": process.env.NODE_ENV === "development"
|
|
111
|
+
? "link:../whatsapp-cloud"
|
|
112
|
+
: "^0.0.5"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Note:** For CI/CD, you'll need Option 1 (npm publish) or Option 2 (git dependency). `pnpm link` only works locally.
|
|
118
|
+
|
|
119
|
+
## Type Exports
|
|
120
|
+
|
|
121
|
+
All types are properly exported and can be imported in React/Next.js projects:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Import client
|
|
125
|
+
import { WhatsAppClient } from "whatsapp-cloud";
|
|
126
|
+
|
|
127
|
+
// Import types
|
|
128
|
+
import type {
|
|
129
|
+
IncomingTextMessage,
|
|
130
|
+
IncomingMessage,
|
|
131
|
+
WebhookPayload,
|
|
132
|
+
MessageContext,
|
|
133
|
+
CreateTemplateRequest,
|
|
134
|
+
// ... all other types
|
|
135
|
+
} from "whatsapp-cloud";
|
|
136
|
+
|
|
137
|
+
// Import schemas (for validation)
|
|
138
|
+
import {
|
|
139
|
+
incomingTextMessageSchema,
|
|
140
|
+
webhookPayloadSchema,
|
|
141
|
+
// ... all other schemas
|
|
142
|
+
} from "whatsapp-cloud";
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Verifying Exports
|
|
146
|
+
|
|
147
|
+
To verify all types are exported correctly:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# In your project
|
|
151
|
+
pnpm exec tsc --noEmit --skipLibCheck
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
This will check that all imported types are available.
|
package/docs/webhooks.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Webhooks
|
|
2
|
+
|
|
3
|
+
Handle incoming WhatsApp messages and status updates via webhooks.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { WhatsAppClient } from "@whatsapp-cloud/sdk";
|
|
9
|
+
|
|
10
|
+
const client = new WhatsAppClient({
|
|
11
|
+
accessToken: process.env.WHATSAPP_ACCESS_TOKEN!,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// In your webhook endpoint
|
|
15
|
+
app.post("/webhook", async (req, res) => {
|
|
16
|
+
// handle() returns IMMEDIATELY - handlers run in background
|
|
17
|
+
client.webhooks.handle(req.body, {
|
|
18
|
+
text: async (message, context) => {
|
|
19
|
+
// This can take as long as needed (20s, 1min, etc.)
|
|
20
|
+
// Webhook already returned 200, so Meta is happy
|
|
21
|
+
|
|
22
|
+
// Process text message
|
|
23
|
+
console.log(`Received: ${message.text.body} from ${message.from}`);
|
|
24
|
+
|
|
25
|
+
// Store in database
|
|
26
|
+
await db.messages.create({
|
|
27
|
+
id: message.id,
|
|
28
|
+
from: message.from,
|
|
29
|
+
body: message.text.body,
|
|
30
|
+
phoneNumberId: context.metadata.phoneNumberId,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Send response
|
|
34
|
+
await client.messages.sendText({
|
|
35
|
+
to: `+${message.from}`,
|
|
36
|
+
text: { body: "Got it!" },
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Returns 200 IMMEDIATELY (handlers continue in background)
|
|
42
|
+
res.json({ success: true });
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Webhook Verification
|
|
47
|
+
|
|
48
|
+
Meta sends GET requests to verify your webhook endpoint:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
app.get("/webhook", (req, res) => {
|
|
52
|
+
const challenge = client.webhooks.verify(req.query, process.env.VERIFY_TOKEN);
|
|
53
|
+
if (challenge) {
|
|
54
|
+
return res.send(challenge);
|
|
55
|
+
}
|
|
56
|
+
return res.status(403).send("Forbidden");
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Low-Level API
|
|
61
|
+
|
|
62
|
+
For more control, extract messages manually:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
const messages = client.webhooks.extractMessages(payload);
|
|
66
|
+
for (const message of messages) {
|
|
67
|
+
// Custom processing
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Media Downloads
|
|
72
|
+
|
|
73
|
+
Download media files (images, audio, video, documents) from incoming messages:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
client.webhooks.handle(req.body, {
|
|
77
|
+
image: async (message, context) => {
|
|
78
|
+
// Download the image
|
|
79
|
+
const imageData = await client.webhooks.downloadMedia(message.image.id);
|
|
80
|
+
|
|
81
|
+
// Upload to your storage (S3, Cloudinary, etc.)
|
|
82
|
+
await s3.upload({
|
|
83
|
+
key: `images/${message.image.id}`,
|
|
84
|
+
body: Buffer.from(imageData),
|
|
85
|
+
contentType: message.image.mime_type || "image/jpeg",
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
audio: async (message, context) => {
|
|
90
|
+
const audioData = await client.webhooks.downloadMedia(message.audio.id);
|
|
91
|
+
// Process audio file...
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Note:** Media files are only available for a limited time. Download them as soon as possible after receiving the webhook.
|
|
97
|
+
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
- `client.webhooks.verify(query, token)` - Verify GET request, returns challenge or null
|
|
101
|
+
- `client.webhooks.extractMessages(payload)` - Extract messages from payload
|
|
102
|
+
- `client.webhooks.extractStatuses(payload)` - Extract status updates
|
|
103
|
+
- `client.webhooks.handle(payload, handlers, options?)` - Handle with type-safe callbacks
|
|
104
|
+
- `client.webhooks.downloadMedia(mediaId)` - Download media file by ID, returns ArrayBuffer
|
package/package.json
CHANGED
package/src/client/HttpClient.ts
CHANGED
|
@@ -11,7 +11,7 @@ interface APIErrorResponse {
|
|
|
11
11
|
* HTTP client for making requests to the WhatsApp Cloud API
|
|
12
12
|
*/
|
|
13
13
|
export class HttpClient {
|
|
14
|
-
|
|
14
|
+
public readonly baseURL: string;
|
|
15
15
|
public readonly accessToken: string;
|
|
16
16
|
public readonly phoneNumberId?: string;
|
|
17
17
|
public readonly businessAccountId?: string;
|
|
@@ -95,6 +95,37 @@ export class HttpClient {
|
|
|
95
95
|
return response.json() as Promise<T>;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Make a GET request and return binary data (ArrayBuffer)
|
|
100
|
+
* Useful for downloading media files
|
|
101
|
+
*/
|
|
102
|
+
async getBinary(path: string): Promise<ArrayBuffer> {
|
|
103
|
+
const url = `${this.baseURL}/${this.apiVersion}${path}`;
|
|
104
|
+
|
|
105
|
+
const response = await fetch(url, {
|
|
106
|
+
method: "GET",
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
// Try to parse error response
|
|
114
|
+
let errorMessage = `API Error: ${response.statusText}`;
|
|
115
|
+
try {
|
|
116
|
+
const error = (await response.json()) as APIErrorResponse;
|
|
117
|
+
errorMessage = `API Error: ${
|
|
118
|
+
error.error?.message || response.statusText
|
|
119
|
+
} (${error.error?.code || response.status})`;
|
|
120
|
+
} catch {
|
|
121
|
+
// If JSON parsing fails, use default message
|
|
122
|
+
}
|
|
123
|
+
throw new Error(errorMessage);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return response.arrayBuffer();
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
/**
|
|
99
130
|
* Make a PATCH request
|
|
100
131
|
*/
|
|
@@ -5,6 +5,7 @@ import { MessagesService } from "../services/messages/index";
|
|
|
5
5
|
import { AccountsService } from "../services/accounts/index";
|
|
6
6
|
import { BusinessService } from "../services/business/index";
|
|
7
7
|
import { TemplatesService } from "../services/templates/index";
|
|
8
|
+
import { WebhooksService } from "../services/webhooks/index";
|
|
8
9
|
import { ZodError } from "zod";
|
|
9
10
|
import { transformZodError } from "../utils/zod-error";
|
|
10
11
|
import type { DebugTokenResponse } from "../types/debug";
|
|
@@ -17,6 +18,7 @@ export class WhatsAppClient {
|
|
|
17
18
|
public readonly accounts: AccountsService;
|
|
18
19
|
public readonly business: BusinessService;
|
|
19
20
|
public readonly templates: TemplatesService;
|
|
21
|
+
public readonly webhooks: WebhooksService;
|
|
20
22
|
|
|
21
23
|
private readonly httpClient: HttpClient;
|
|
22
24
|
|
|
@@ -40,6 +42,7 @@ export class WhatsAppClient {
|
|
|
40
42
|
this.accounts = new AccountsService(this.httpClient);
|
|
41
43
|
this.business = new BusinessService(this.httpClient);
|
|
42
44
|
this.templates = new TemplatesService(this.httpClient);
|
|
45
|
+
this.webhooks = new WebhooksService(this.httpClient);
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
/**
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,13 @@ export * from "./schemas/index";
|
|
|
7
7
|
// Export types (primary export point)
|
|
8
8
|
export type * from "./types/index";
|
|
9
9
|
|
|
10
|
+
// Export webhook handler types (convenience exports)
|
|
11
|
+
export type {
|
|
12
|
+
MessageContext,
|
|
13
|
+
MessageHandlers,
|
|
14
|
+
HandleOptions,
|
|
15
|
+
} from "./services/webhooks/index";
|
|
16
|
+
|
|
10
17
|
// Export errors for error handling
|
|
11
18
|
export {
|
|
12
19
|
WhatsAppError,
|
package/src/schemas/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { componentSchema } from "./component";
|
|
|
5
5
|
* Schema for creating a template
|
|
6
6
|
* Simplified - no variables/examples for now
|
|
7
7
|
*/
|
|
8
|
-
export const
|
|
8
|
+
export const templateCreateSchema = z.object({
|
|
9
9
|
name: z.string().min(1).max(512, "Template name must be 512 characters or less"),
|
|
10
10
|
language: z.string().min(2).max(5, "Language code must be 2-5 characters (e.g., 'en' or 'en_US')"),
|
|
11
11
|
category: z.enum(["AUTHENTICATION", "MARKETING", "UTILITY"]),
|
|
@@ -49,7 +49,7 @@ export const createTemplateRequestSchema = z.object({
|
|
|
49
49
|
* Schema for updating a template
|
|
50
50
|
* All fields optional - only update what's provided
|
|
51
51
|
*/
|
|
52
|
-
export const
|
|
52
|
+
export const templateUpdateSchema = z.object({
|
|
53
53
|
category: z.enum(["AUTHENTICATION", "MARKETING", "UTILITY"]).optional(),
|
|
54
54
|
components: z.array(componentSchema).optional(),
|
|
55
55
|
language: z.string().min(2).max(5).optional(),
|
|
@@ -2,9 +2,9 @@ import { z } from "zod";
|
|
|
2
2
|
import { componentSchema } from "./component";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Schema for template
|
|
5
|
+
* Schema for template (the base/select model - what you get from API)
|
|
6
6
|
*/
|
|
7
|
-
export const
|
|
7
|
+
export const templateSchema = z.object({
|
|
8
8
|
id: z.string(),
|
|
9
9
|
name: z.string(),
|
|
10
10
|
language: z.string(),
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base fields present in ALL incoming messages
|
|
5
|
+
*/
|
|
6
|
+
const baseIncomingMessageSchema = z.object({
|
|
7
|
+
from: z.string(), // WhatsApp ID (phone number without +)
|
|
8
|
+
id: z.string(), // Message ID (wamid.*)
|
|
9
|
+
timestamp: z.string(), // Unix timestamp as string
|
|
10
|
+
type: z.string(), // Message type discriminator
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Text content in incoming text messages
|
|
15
|
+
* Note: Incoming messages don't have preview_url like outgoing
|
|
16
|
+
*/
|
|
17
|
+
const incomingTextContentSchema = z.object({
|
|
18
|
+
body: z.string(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Audio content in incoming audio messages
|
|
23
|
+
*/
|
|
24
|
+
const incomingAudioContentSchema = z.object({
|
|
25
|
+
id: z.string(), // Media ID for downloading
|
|
26
|
+
mime_type: z.string().optional(), // e.g., "audio/ogg; codecs=opus"
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Image content in incoming image messages
|
|
31
|
+
*/
|
|
32
|
+
const incomingImageContentSchema = z.object({
|
|
33
|
+
id: z.string(), // Media ID for downloading
|
|
34
|
+
mime_type: z.string().optional(), // e.g., "image/jpeg"
|
|
35
|
+
caption: z.string().optional(), // Optional caption text
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Incoming text message schema
|
|
40
|
+
* Uses discriminated union pattern (type: "text")
|
|
41
|
+
*/
|
|
42
|
+
export const incomingTextMessageSchema = baseIncomingMessageSchema.extend({
|
|
43
|
+
type: z.literal("text"),
|
|
44
|
+
text: incomingTextContentSchema,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Incoming audio message schema
|
|
49
|
+
* Uses discriminated union pattern (type: "audio")
|
|
50
|
+
*/
|
|
51
|
+
export const incomingAudioMessageSchema = baseIncomingMessageSchema.extend({
|
|
52
|
+
type: z.literal("audio"),
|
|
53
|
+
audio: incomingAudioContentSchema,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Incoming image message schema
|
|
58
|
+
* Uses discriminated union pattern (type: "image")
|
|
59
|
+
*/
|
|
60
|
+
export const incomingImageMessageSchema = baseIncomingMessageSchema.extend({
|
|
61
|
+
type: z.literal("image"),
|
|
62
|
+
image: incomingImageContentSchema,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Union of all incoming message types
|
|
67
|
+
*/
|
|
68
|
+
export const incomingMessageSchema = z.discriminatedUnion("type", [
|
|
69
|
+
incomingTextMessageSchema,
|
|
70
|
+
incomingAudioMessageSchema,
|
|
71
|
+
incomingImageMessageSchema,
|
|
72
|
+
]);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { incomingMessageSchema } from "./incoming-message";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contact information in webhook
|
|
6
|
+
*/
|
|
7
|
+
const contactSchema = z.object({
|
|
8
|
+
profile: z.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
}),
|
|
11
|
+
wa_id: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Metadata in webhook value
|
|
16
|
+
*/
|
|
17
|
+
const webhookMetadataSchema = z.object({
|
|
18
|
+
display_phone_number: z.string(),
|
|
19
|
+
phone_number_id: z.string(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Webhook value (the actual data)
|
|
24
|
+
*/
|
|
25
|
+
const webhookValueSchema = z.object({
|
|
26
|
+
messaging_product: z.literal("whatsapp"),
|
|
27
|
+
metadata: webhookMetadataSchema,
|
|
28
|
+
contacts: z.array(contactSchema).optional(),
|
|
29
|
+
messages: z.array(incomingMessageSchema).optional(), // Incoming messages
|
|
30
|
+
statuses: z.array(z.any()).optional(), // Status updates (for later)
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Webhook change entry
|
|
35
|
+
*/
|
|
36
|
+
const webhookChangeSchema = z.object({
|
|
37
|
+
value: webhookValueSchema,
|
|
38
|
+
field: z.literal("messages"), // For now: only messages field
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Webhook entry
|
|
43
|
+
*/
|
|
44
|
+
const webhookEntrySchema = z.object({
|
|
45
|
+
id: z.string(), // WABA ID
|
|
46
|
+
changes: z.array(webhookChangeSchema),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Full webhook payload schema
|
|
51
|
+
*/
|
|
52
|
+
export const webhookPayloadSchema = z.object({
|
|
53
|
+
object: z.literal("whatsapp_business_account"),
|
|
54
|
+
entry: z.array(webhookEntrySchema),
|
|
55
|
+
});
|
|
56
|
+
|
|
@@ -7,15 +7,15 @@ import { updateTemplate } from "./methods/update";
|
|
|
7
7
|
import { deleteTemplate } from "./methods/delete";
|
|
8
8
|
import { WhatsAppValidationError } from "../../errors";
|
|
9
9
|
import type {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
TemplateCreate,
|
|
11
|
+
TemplateUpdate,
|
|
12
12
|
ListTemplatesRequest,
|
|
13
13
|
DeleteTemplateRequest,
|
|
14
14
|
} from "../../types/templates/request";
|
|
15
15
|
import type {
|
|
16
16
|
CreateTemplateResponse,
|
|
17
17
|
ListTemplatesResponse,
|
|
18
|
-
|
|
18
|
+
Template,
|
|
19
19
|
UpdateTemplateResponse,
|
|
20
20
|
DeleteTemplateResponse,
|
|
21
21
|
} from "../../types/templates/response";
|
|
@@ -54,7 +54,7 @@ export class TemplatesService {
|
|
|
54
54
|
* @param businessAccountId - Optional WABA ID (overrides client config)
|
|
55
55
|
*/
|
|
56
56
|
async create(
|
|
57
|
-
request:
|
|
57
|
+
request: TemplateCreate,
|
|
58
58
|
businessAccountId?: string
|
|
59
59
|
): Promise<CreateTemplateResponse> {
|
|
60
60
|
const client = this.getClient(businessAccountId);
|
|
@@ -82,7 +82,7 @@ export class TemplatesService {
|
|
|
82
82
|
*
|
|
83
83
|
* @param templateId - Template ID
|
|
84
84
|
*/
|
|
85
|
-
async get(templateId: string): Promise<
|
|
85
|
+
async get(templateId: string): Promise<Template> {
|
|
86
86
|
return getTemplate(this.httpClient, templateId);
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -96,7 +96,7 @@ export class TemplatesService {
|
|
|
96
96
|
*/
|
|
97
97
|
async update(
|
|
98
98
|
templateId: string,
|
|
99
|
-
request:
|
|
99
|
+
request: TemplateUpdate
|
|
100
100
|
): Promise<UpdateTemplateResponse> {
|
|
101
101
|
return updateTemplate(this.httpClient, templateId, request);
|
|
102
102
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TemplatesClient } from "../TemplatesClient";
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
2
|
+
import { templateCreateSchema } from "../../../schemas/templates/request";
|
|
3
|
+
import type { TemplateCreate } from "../../../types/templates/request";
|
|
4
4
|
import type { CreateTemplateResponse } from "../../../types/templates/response";
|
|
5
5
|
import { transformZodError } from "../../../utils/zod-error";
|
|
6
6
|
|
|
@@ -12,16 +12,18 @@ import { transformZodError } from "../../../utils/zod-error";
|
|
|
12
12
|
*/
|
|
13
13
|
export async function createTemplate(
|
|
14
14
|
templatesClient: TemplatesClient,
|
|
15
|
-
request:
|
|
15
|
+
request: TemplateCreate
|
|
16
16
|
): Promise<CreateTemplateResponse> {
|
|
17
17
|
// Validate request with schema - throws WhatsAppValidationError if invalid
|
|
18
|
-
const result =
|
|
18
|
+
const result = templateCreateSchema.safeParse(request);
|
|
19
19
|
if (!result.success) {
|
|
20
20
|
throw transformZodError(result.error);
|
|
21
21
|
}
|
|
22
22
|
const data = result.data;
|
|
23
23
|
|
|
24
24
|
// Make API request - templatesClient handles the WABA ID prefix automatically
|
|
25
|
-
return templatesClient.post<CreateTemplateResponse>(
|
|
25
|
+
return templatesClient.post<CreateTemplateResponse>(
|
|
26
|
+
"/message_templates",
|
|
27
|
+
data
|
|
28
|
+
);
|
|
26
29
|
}
|
|
27
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClient } from "../../../client/HttpClient";
|
|
2
|
-
import type {
|
|
2
|
+
import type { Template } from "../../../types/templates/response";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Get a template by ID
|
|
@@ -12,12 +12,12 @@ import type { TemplateResponse } from "../../../types/templates/response";
|
|
|
12
12
|
export async function getTemplate(
|
|
13
13
|
httpClient: HttpClient,
|
|
14
14
|
templateId: string
|
|
15
|
-
): Promise<
|
|
15
|
+
): Promise<Template> {
|
|
16
16
|
if (!templateId || templateId.trim().length === 0) {
|
|
17
17
|
throw new Error("Template ID is required");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Make API request - template ID is used directly, no WABA prefix
|
|
21
|
-
return httpClient.get<
|
|
21
|
+
return httpClient.get<Template>(`/${templateId}`);
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from "../../../client/HttpClient";
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
2
|
+
import { templateUpdateSchema } from "../../../schemas/templates/request";
|
|
3
|
+
import type { TemplateUpdate } from "../../../types/templates/request";
|
|
4
4
|
import type { UpdateTemplateResponse } from "../../../types/templates/response";
|
|
5
5
|
import { transformZodError } from "../../../utils/zod-error";
|
|
6
6
|
|
|
@@ -16,14 +16,14 @@ import { transformZodError } from "../../../utils/zod-error";
|
|
|
16
16
|
export async function updateTemplate(
|
|
17
17
|
httpClient: HttpClient,
|
|
18
18
|
templateId: string,
|
|
19
|
-
request:
|
|
19
|
+
request: TemplateUpdate
|
|
20
20
|
): Promise<UpdateTemplateResponse> {
|
|
21
21
|
if (!templateId || templateId.trim().length === 0) {
|
|
22
22
|
throw new Error("Template ID is required");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// Validate request with schema - throws WhatsAppValidationError if invalid
|
|
26
|
-
const result =
|
|
26
|
+
const result = templateUpdateSchema.safeParse(request);
|
|
27
27
|
if (!result.success) {
|
|
28
28
|
throw transformZodError(result.error);
|
|
29
29
|
}
|
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
IncomingAudioMessage,
|
|
10
|
+
IncomingImageMessage,
|
|
11
|
+
IncomingMessage,
|
|
12
|
+
} from "../../types/webhooks";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Context provided to message handlers
|
|
16
|
+
* Contains metadata and contact info (message is passed separately)
|
|
17
|
+
*/
|
|
18
|
+
export type MessageContext = {
|
|
19
|
+
metadata: {
|
|
20
|
+
phoneNumberId: string;
|
|
21
|
+
displayPhoneNumber: string;
|
|
22
|
+
wabaId: string;
|
|
23
|
+
};
|
|
24
|
+
contact?: {
|
|
25
|
+
name: string;
|
|
26
|
+
waId: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Handler functions for different message types
|
|
32
|
+
* Receives message and context separately - message is the focus, context is optional metadata
|
|
33
|
+
*/
|
|
34
|
+
export type MessageHandlers = {
|
|
35
|
+
text?: (
|
|
36
|
+
message: IncomingTextMessage,
|
|
37
|
+
context: MessageContext
|
|
38
|
+
) => Promise<void> | void;
|
|
39
|
+
audio?: (
|
|
40
|
+
message: IncomingAudioMessage,
|
|
41
|
+
context: MessageContext
|
|
42
|
+
) => Promise<void> | void;
|
|
43
|
+
image?: (
|
|
44
|
+
message: IncomingImageMessage,
|
|
45
|
+
context: MessageContext
|
|
46
|
+
) => Promise<void> | void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Options for handle() method
|
|
51
|
+
*/
|
|
52
|
+
export type HandleOptions = {
|
|
53
|
+
/**
|
|
54
|
+
* Error handler called when a message handler throws an error
|
|
55
|
+
* If not provided, errors are logged and processing continues
|
|
56
|
+
*/
|
|
57
|
+
onError?: (error: Error, message: IncomingMessage) => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Webhooks service for handling incoming webhook payloads
|
|
62
|
+
*
|
|
63
|
+
* Provides utilities for extracting messages and a convenience handler
|
|
64
|
+
* for type-safe message processing.
|
|
65
|
+
*/
|
|
66
|
+
export class WebhooksService {
|
|
67
|
+
constructor(private readonly httpClient: HttpClient) {}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Verify webhook GET request from Meta
|
|
71
|
+
*
|
|
72
|
+
* Meta sends GET requests to verify webhook endpoints during setup.
|
|
73
|
+
* Returns the challenge string if valid, null if invalid.
|
|
74
|
+
*
|
|
75
|
+
* @param query - Query parameters from GET request
|
|
76
|
+
* @param verifyToken - Your verification token (stored on your server)
|
|
77
|
+
* @returns Challenge string if valid, null if invalid
|
|
78
|
+
*/
|
|
79
|
+
verify(
|
|
80
|
+
query: {
|
|
81
|
+
"hub.mode"?: string;
|
|
82
|
+
"hub.verify_token"?: string;
|
|
83
|
+
"hub.challenge"?: string;
|
|
84
|
+
},
|
|
85
|
+
verifyToken: string
|
|
86
|
+
): string | null {
|
|
87
|
+
return verifyWebhook(query, verifyToken);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Extract all incoming messages from webhook payload
|
|
92
|
+
*
|
|
93
|
+
* Low-level utility that flattens the nested webhook structure
|
|
94
|
+
* and returns messages directly.
|
|
95
|
+
*
|
|
96
|
+
* @param payload - Webhook payload from Meta
|
|
97
|
+
* @returns Flat array of incoming messages
|
|
98
|
+
*/
|
|
99
|
+
extractMessages(payload: WebhookPayload): IncomingMessage[] {
|
|
100
|
+
return extractMessages(payload);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract status updates from webhook payload
|
|
105
|
+
*
|
|
106
|
+
* Low-level utility for extracting status updates for outgoing messages.
|
|
107
|
+
*
|
|
108
|
+
* @param payload - Webhook payload from Meta
|
|
109
|
+
* @returns Flat array of status updates
|
|
110
|
+
*/
|
|
111
|
+
extractStatuses(payload: WebhookPayload): unknown[] {
|
|
112
|
+
return extractStatuses(payload);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Download media file by media ID
|
|
117
|
+
*
|
|
118
|
+
* Downloads media files (images, audio, video, documents) from WhatsApp servers.
|
|
119
|
+
* Uses the access token from the client configuration automatically.
|
|
120
|
+
*
|
|
121
|
+
* @param mediaId - Media ID from incoming message (e.g., message.image.id, message.audio.id)
|
|
122
|
+
* @returns Promise resolving to ArrayBuffer containing the media file
|
|
123
|
+
* @throws Error if download fails or media ID is invalid
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* client.webhooks.handle(req.body, {
|
|
128
|
+
* image: async (message, context) => {
|
|
129
|
+
* const mediaData = await client.webhooks.downloadMedia(message.image.id);
|
|
130
|
+
* // Upload to S3, save to disk, etc.
|
|
131
|
+
* await s3.upload({ key: message.image.id, body: Buffer.from(mediaData) });
|
|
132
|
+
* },
|
|
133
|
+
* });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
async downloadMedia(mediaId: string): Promise<ArrayBuffer> {
|
|
137
|
+
if (!mediaId || mediaId.trim().length === 0) {
|
|
138
|
+
throw new Error("Media ID is required");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// WhatsApp API endpoint: GET /{version}/{media-id}
|
|
142
|
+
// Use HttpClient's getBinary method which handles baseURL, apiVersion, and auth automatically
|
|
143
|
+
return this.httpClient.getBinary(`/${mediaId}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate webhook payload structure
|
|
148
|
+
*
|
|
149
|
+
* Validates the payload against the schema. Logs errors if malformed
|
|
150
|
+
* but doesn't throw, allowing processing to continue.
|
|
151
|
+
*
|
|
152
|
+
* @param payload - Raw payload to validate
|
|
153
|
+
* @returns Validated payload if valid, original payload if invalid (with logged error)
|
|
154
|
+
*/
|
|
155
|
+
private validatePayload(payload: unknown): WebhookPayload {
|
|
156
|
+
const result = webhookPayloadSchema.safeParse(payload);
|
|
157
|
+
if (!result.success) {
|
|
158
|
+
console.error(
|
|
159
|
+
"Webhook payload validation failed:",
|
|
160
|
+
result.error.format()
|
|
161
|
+
);
|
|
162
|
+
// Return as-is (TypeScript will treat it as WebhookPayload, but it's actually invalid)
|
|
163
|
+
// This allows processing to continue, but handlers should be defensive
|
|
164
|
+
return payload as WebhookPayload;
|
|
165
|
+
}
|
|
166
|
+
return result.data;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle webhook payload with type-safe callbacks
|
|
171
|
+
*
|
|
172
|
+
* High-level convenience method that extracts messages and dispatches
|
|
173
|
+
* them to appropriate handlers based on message type.
|
|
174
|
+
*
|
|
175
|
+
* **Important**: This method returns quickly to allow fast webhook responses.
|
|
176
|
+
* Handlers are processed asynchronously. If you need to await handler completion,
|
|
177
|
+
* use the low-level `extractMessages()` method instead.
|
|
178
|
+
*
|
|
179
|
+
* @param payload - Webhook payload from Meta (will be validated)
|
|
180
|
+
* @param handlers - Object with handler functions for each message type
|
|
181
|
+
* @param options - Optional error handling configuration
|
|
182
|
+
*/
|
|
183
|
+
handle(
|
|
184
|
+
payload: unknown,
|
|
185
|
+
handlers: MessageHandlers,
|
|
186
|
+
options?: HandleOptions
|
|
187
|
+
): void {
|
|
188
|
+
// Validate payload (logs error if malformed, but continues)
|
|
189
|
+
const validatedPayload = this.validatePayload(payload);
|
|
190
|
+
|
|
191
|
+
// Extract metadata and contacts from payload for context
|
|
192
|
+
for (const entry of validatedPayload.entry) {
|
|
193
|
+
for (const change of entry.changes) {
|
|
194
|
+
if (change.field === "messages" && change.value.messages) {
|
|
195
|
+
const metadata = {
|
|
196
|
+
phoneNumberId: change.value.metadata.phone_number_id,
|
|
197
|
+
displayPhoneNumber: change.value.metadata.display_phone_number,
|
|
198
|
+
wabaId: entry.id,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const contacts = change.value.contacts || [];
|
|
202
|
+
|
|
203
|
+
// Process each message with its context
|
|
204
|
+
for (const message of change.value.messages) {
|
|
205
|
+
// Find contact for this message (match by wa_id)
|
|
206
|
+
const contact = contacts.find((c) => c.wa_id === message.from);
|
|
207
|
+
|
|
208
|
+
// Build context (metadata + contact, no message duplication)
|
|
209
|
+
const context: MessageContext = {
|
|
210
|
+
metadata,
|
|
211
|
+
...(contact && {
|
|
212
|
+
contact: {
|
|
213
|
+
name: contact.profile.name,
|
|
214
|
+
waId: contact.wa_id,
|
|
215
|
+
},
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// Process handler asynchronously (don't await)
|
|
220
|
+
// This allows long-running handlers without blocking webhook response
|
|
221
|
+
Promise.resolve()
|
|
222
|
+
.then(async () => {
|
|
223
|
+
// Type-safe dispatch based on message type
|
|
224
|
+
switch (message.type) {
|
|
225
|
+
case "text":
|
|
226
|
+
if (handlers.text) {
|
|
227
|
+
await handlers.text(message, context);
|
|
228
|
+
}
|
|
229
|
+
break;
|
|
230
|
+
|
|
231
|
+
case "audio":
|
|
232
|
+
if (handlers.audio) {
|
|
233
|
+
await handlers.audio(message, context);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case "image":
|
|
238
|
+
if (handlers.image) {
|
|
239
|
+
await handlers.image(message, context);
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
|
|
243
|
+
default:
|
|
244
|
+
// Unhandled message type - silently continue
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
})
|
|
248
|
+
.catch((error) => {
|
|
249
|
+
// Handle errors in handler execution
|
|
250
|
+
if (options?.onError) {
|
|
251
|
+
options.onError(error as Error, message);
|
|
252
|
+
} else {
|
|
253
|
+
// Default: log and continue (don't break webhook response)
|
|
254
|
+
console.error(
|
|
255
|
+
`Error handling ${message.type} message ${message.id}:`,
|
|
256
|
+
error
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { WebhookPayload } from "../../../types/webhooks";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract status updates from webhook payload
|
|
5
|
+
*
|
|
6
|
+
* Flattens the nested structure: entry[].changes[].value.statuses[]
|
|
7
|
+
* Returns a flat array of status updates
|
|
8
|
+
*
|
|
9
|
+
* @param payload - Webhook payload from Meta
|
|
10
|
+
* @returns Flat array of status updates
|
|
11
|
+
*/
|
|
12
|
+
export function extractStatuses(payload: WebhookPayload): unknown[] {
|
|
13
|
+
const statuses: unknown[] = [];
|
|
14
|
+
|
|
15
|
+
for (const entry of payload.entry) {
|
|
16
|
+
for (const change of entry.changes) {
|
|
17
|
+
if (change.field === "messages" && change.value.statuses) {
|
|
18
|
+
statuses.push(...change.value.statuses);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return statuses;
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verify webhook GET request from Meta
|
|
3
|
+
*
|
|
4
|
+
* Meta sends GET requests to verify webhook endpoints:
|
|
5
|
+
* GET /webhook?hub.mode=subscribe&hub.challenge=<CHALLENGE>&hub.verify_token=<TOKEN>
|
|
6
|
+
*
|
|
7
|
+
* @param query - Query parameters from GET request
|
|
8
|
+
* @param verifyToken - Your verification token (stored on your server)
|
|
9
|
+
* @returns Challenge string if valid, null if invalid
|
|
10
|
+
*/
|
|
11
|
+
export function verifyWebhook(
|
|
12
|
+
query: {
|
|
13
|
+
"hub.mode"?: string;
|
|
14
|
+
"hub.verify_token"?: string;
|
|
15
|
+
"hub.challenge"?: string;
|
|
16
|
+
},
|
|
17
|
+
verifyToken: string
|
|
18
|
+
): string | null {
|
|
19
|
+
const mode = query["hub.mode"];
|
|
20
|
+
const token = query["hub.verify_token"];
|
|
21
|
+
const challenge = query["hub.challenge"];
|
|
22
|
+
|
|
23
|
+
// Verify mode is "subscribe" and token matches
|
|
24
|
+
if (mode === "subscribe" && token === verifyToken && challenge) {
|
|
25
|
+
return challenge;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
templateCreateSchema,
|
|
4
|
+
templateUpdateSchema,
|
|
5
5
|
listTemplatesRequestSchema,
|
|
6
6
|
deleteTemplateRequestSchema,
|
|
7
7
|
} from "../../schemas/templates/request";
|
|
@@ -9,12 +9,12 @@ import {
|
|
|
9
9
|
/**
|
|
10
10
|
* Type for creating a template
|
|
11
11
|
*/
|
|
12
|
-
export type
|
|
12
|
+
export type TemplateCreate = z.infer<typeof templateCreateSchema>;
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Type for updating a template
|
|
16
16
|
*/
|
|
17
|
-
export type
|
|
17
|
+
export type TemplateUpdate = z.infer<typeof templateUpdateSchema>;
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Type for listing templates
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
templateSchema,
|
|
4
4
|
createTemplateResponseSchema,
|
|
5
5
|
listTemplatesResponseSchema,
|
|
6
6
|
updateTemplateResponseSchema,
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from "../../schemas/templates/response";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Type for a
|
|
11
|
+
* Type for a template (the base/select model - what you get from API)
|
|
12
12
|
*/
|
|
13
|
-
export type
|
|
13
|
+
export type Template = z.infer<typeof templateSchema>;
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Type for create template response
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
incomingTextMessageSchema,
|
|
4
|
+
incomingAudioMessageSchema,
|
|
5
|
+
incomingImageMessageSchema,
|
|
6
|
+
incomingMessageSchema,
|
|
7
|
+
} from "../../schemas/webhooks/incoming-message";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Type for incoming text message
|
|
11
|
+
*/
|
|
12
|
+
export type IncomingTextMessage = z.infer<typeof incomingTextMessageSchema>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type for incoming audio message
|
|
16
|
+
*/
|
|
17
|
+
export type IncomingAudioMessage = z.infer<typeof incomingAudioMessageSchema>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Type for incoming image message
|
|
21
|
+
*/
|
|
22
|
+
export type IncomingImageMessage = z.infer<typeof incomingImageMessageSchema>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Union type for all incoming message types
|
|
26
|
+
*/
|
|
27
|
+
export type IncomingMessage = z.infer<typeof incomingMessageSchema>;
|