@kuralle-agents/messaging-meta 0.8.5 → 0.9.0
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/README.md +6 -6
- package/dist/graph-api/client.d.ts +6 -1
- package/dist/graph-api/client.js +8 -1
- package/dist/graph-api/errors.js +6 -2
- package/dist/instagram/client.d.ts +16 -16
- package/dist/instagram/client.js +50 -67
- package/dist/instagram/types.d.ts +9 -11
- package/dist/instagram/types.js +2 -2
- package/dist/messenger/client.d.ts +1 -1
- package/dist/messenger/client.js +7 -6
- package/dist/webhook/index.d.ts +1 -1
- package/dist/webhook/normalizer.d.ts +52 -5
- package/dist/webhook/normalizer.js +52 -4
- package/dist/whatsapp/client.d.ts +16 -5
- package/dist/whatsapp/client.js +72 -22
- package/dist/whatsapp/commerce.d.ts +7 -2
- package/dist/whatsapp/commerce.js +17 -0
- package/dist/whatsapp/index.d.ts +2 -2
- package/dist/whatsapp/index.js +1 -1
- package/dist/whatsapp/types.d.ts +32 -7
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -12,11 +12,11 @@ npm install @kuralle-agents/messaging-meta @kuralle-agents/messaging
|
|
|
12
12
|
|
|
13
13
|
Provides production-ready clients for Meta's three messaging platforms, built on a shared Graph API foundation with retry logic, rate limiting, and unified error handling.
|
|
14
14
|
|
|
15
|
-
- **WhatsApp** — full API coverage: text (auto-split at 4096 chars), media, templates, interactive buttons, list messages, CTA buttons, WhatsApp Flows, reactions, locations, contacts, commerce (single/multi-product, catalog,
|
|
16
|
-
- **Messenger** — button templates, generic templates (carousel), quick replies, sender actions, user profile lookups.
|
|
17
|
-
- **Instagram** — text (auto-split at 1000 bytes), quick replies, carousels, private replies to comments, ice breakers, message tags
|
|
15
|
+
- **WhatsApp** — full API coverage: text (auto-split at 4096 chars), media, templates, interactive buttons, list messages (max 10 rows total), CTA buttons, WhatsApp Flows, reactions, locations, contacts, typing indicators (via message id), commerce (single/multi-product, catalog, address messages, inbound order + product-inquiry parsing).
|
|
16
|
+
- **Messenger** — button templates, generic templates (carousel), quick replies, sender actions (`mark_seen` via PSID), user profile lookups.
|
|
17
|
+
- **Instagram** — text (auto-split at 1000 bytes), media by URL (audio/image/video/file), quick replies, carousels, private replies to comments, ice breakers, message tags, `mark_seen`.
|
|
18
18
|
- **Main barrel** — `GraphAPIClient`, `BaseMetaClient`, `verifySignature`, `normalizeWebhook`, error classes.
|
|
19
|
-
- **Shared Graph API** — `MetaErrorClassifier`, `SmartSplitter`, `TruncateSplitter`, `ByteLimitSplitter`, unicode utilities.
|
|
19
|
+
- **Shared Graph API** — default `v24.0`, `MetaErrorClassifier`, `SmartSplitter`, `TruncateSplitter`, `ByteLimitSplitter`, unicode utilities.
|
|
20
20
|
|
|
21
21
|
WhatsApp is available only via the `/whatsapp` subpath (see below).
|
|
22
22
|
|
|
@@ -71,7 +71,7 @@ const instagram = createInstagramClient({
|
|
|
71
71
|
Share catalog products and receive orders without leaving WhatsApp:
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
|
-
import { parseInboundOrder } from '@kuralle-agents/messaging-meta/whatsapp';
|
|
74
|
+
import { parseInboundOrder, parseProductInquiry } from '@kuralle-agents/messaging-meta/whatsapp';
|
|
75
75
|
|
|
76
76
|
// Single product, multi-product (max 10 sections / 30 products), catalog, address request
|
|
77
77
|
await whatsapp.sendProduct(to, { catalogId, productRetailerId: 'sku-1', body: { text: 'Check this out' } });
|
|
@@ -110,7 +110,7 @@ const events = normalizeWebhook(JSON.parse(rawBody));
|
|
|
110
110
|
| Import path | Contents |
|
|
111
111
|
|---|---|
|
|
112
112
|
| `@kuralle-agents/messaging-meta` | `GraphAPIClient`, `BaseMetaClient`, errors, `verifySignature`, `normalizeWebhook`, `MessengerClient`, `InstagramClient` |
|
|
113
|
-
| `@kuralle-agents/messaging-meta/whatsapp` | `WhatsAppClient`, `createWhatsAppClient`, templates, flows, commerce (`parseInboundOrder`, `parseInboundAddress`), format converter |
|
|
113
|
+
| `@kuralle-agents/messaging-meta/whatsapp` | `WhatsAppClient`, `createWhatsAppClient`, templates, flows, commerce (`parseInboundOrder`, `parseInboundAddress`, `parseProductInquiry`), format converter |
|
|
114
114
|
| `@kuralle-agents/messaging-meta/messenger` | `MessengerClient`, `createMessengerClient`, format converter |
|
|
115
115
|
| `@kuralle-agents/messaging-meta/instagram` | `InstagramClient`, `createInstagramClient`, ice breakers, format converter |
|
|
116
116
|
| `@kuralle-agents/messaging-meta/webhooks` | `verifySignature`, `normalizeWebhook` only |
|
|
@@ -16,7 +16,7 @@ export interface GraphAPIClientConfig {
|
|
|
16
16
|
accessToken: string;
|
|
17
17
|
/** App secret used for webhook signature verification and appsecret_proof. */
|
|
18
18
|
appSecret: string;
|
|
19
|
-
/** Graph API version (e.g. `"
|
|
19
|
+
/** Graph API version (e.g. `"v24.0"`). Default `"v24.0"`. */
|
|
20
20
|
apiVersion?: string;
|
|
21
21
|
/** Base URL for the Graph API. Default `"https://graph.facebook.com"`. */
|
|
22
22
|
baseUrl?: string;
|
|
@@ -52,6 +52,11 @@ export declare class GraphAPIClient {
|
|
|
52
52
|
post<T>(endpoint: string, body: unknown): Promise<T>;
|
|
53
53
|
/** POST with a multipart body (e.g. media uploads). */
|
|
54
54
|
postFormData<T>(endpoint: string, formData: FormData): Promise<T>;
|
|
55
|
+
/** DELETE with `access_token` query param applied. */
|
|
56
|
+
delete<T>(endpoint: string, opts?: {
|
|
57
|
+
params?: Record<string, string>;
|
|
58
|
+
body?: unknown;
|
|
59
|
+
}): Promise<T>;
|
|
55
60
|
/** Fetch a binary resource from an absolute CDN URL. */
|
|
56
61
|
fetchBinary(url: string): Promise<Buffer>;
|
|
57
62
|
/** App secret proof generation is left to callers that need it (not part of the HTTP layer). */
|
package/dist/graph-api/client.js
CHANGED
|
@@ -29,7 +29,7 @@ export class GraphAPIClient {
|
|
|
29
29
|
constructor(config) {
|
|
30
30
|
this.accessToken = config.accessToken;
|
|
31
31
|
this.appSecret = config.appSecret;
|
|
32
|
-
const apiVersion = config.apiVersion ?? '
|
|
32
|
+
const apiVersion = config.apiVersion ?? 'v24.0';
|
|
33
33
|
const baseUrl = (config.baseUrl ?? 'https://graph.facebook.com').replace(/\/+$/, '');
|
|
34
34
|
this.http = new HttpClient({
|
|
35
35
|
baseUrl: `${baseUrl}/${apiVersion}`,
|
|
@@ -56,6 +56,13 @@ export class GraphAPIClient {
|
|
|
56
56
|
async postFormData(endpoint, formData) {
|
|
57
57
|
return this.http.postFormData(endpoint, formData);
|
|
58
58
|
}
|
|
59
|
+
/** DELETE with `access_token` query param applied. */
|
|
60
|
+
async delete(endpoint, opts) {
|
|
61
|
+
return this.http.delete(endpoint, {
|
|
62
|
+
params: { access_token: this.accessToken, ...(opts?.params ?? {}) },
|
|
63
|
+
body: opts?.body,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
59
66
|
/** Fetch a binary resource from an absolute CDN URL. */
|
|
60
67
|
async fetchBinary(url) {
|
|
61
68
|
return this.http.fetchBinary(url);
|
package/dist/graph-api/errors.js
CHANGED
|
@@ -50,10 +50,14 @@ export function classifyMetaError(status, body, platform) {
|
|
|
50
50
|
if (status === 403 || metaCode === 10 || metaCode === 200) {
|
|
51
51
|
return new PermissionError(metaMessage, platform);
|
|
52
52
|
}
|
|
53
|
-
// --- 24-hour window closed (
|
|
54
|
-
if (metaCode === 131047) {
|
|
53
|
+
// --- 24-hour window closed (WhatsApp 131047, Messenger 1545041) ---
|
|
54
|
+
if (metaCode === 131047 || metaCode === 1545041) {
|
|
55
55
|
return new WindowClosedError(metaMessage, platform, new Date());
|
|
56
56
|
}
|
|
57
|
+
// --- Person unavailable (Messenger 551) ---
|
|
58
|
+
if (metaCode === 551 || metaSubcode === 551) {
|
|
59
|
+
return new MessagingError(metaMessage, 'person_unavailable', platform);
|
|
60
|
+
}
|
|
57
61
|
// --- Recipient not reachable (HTTP 404 or Meta code 131026) ---
|
|
58
62
|
if (status === 404 || metaCode === 131026) {
|
|
59
63
|
return new RecipientError(metaMessage, platform);
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
* inherited. Only Instagram-specific outbound paths and inbound
|
|
9
9
|
* normalization live here.
|
|
10
10
|
*
|
|
11
|
-
* Key
|
|
11
|
+
* Key differences from WhatsApp / Messenger:
|
|
12
12
|
* - Base URL is `graph.instagram.com` by default.
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
13
|
+
* - Media is sent by public URL (audio, image, video, file attachments).
|
|
14
|
+
* - Message limit is 1000 bytes (UTF-8), not characters.
|
|
15
|
+
* - Send response contains `message_id` (recipient id is implicit).
|
|
16
|
+
* - Only `HUMAN_AGENT` message tag is supported (7-day window).
|
|
17
|
+
* - Ice breakers replace persistent menus.
|
|
18
18
|
*/
|
|
19
19
|
import { type FormatConverter, type InboundMessage, type InteractiveMessage, type MediaDownload, type MediaHandle, type MediaPayload, type MediaUploadOptions, type ReactionData, type SendResult, type StatusUpdate } from '@kuralle-agents/messaging';
|
|
20
20
|
import { BaseMetaClient } from '../base-client.js';
|
|
@@ -31,8 +31,6 @@ export declare class InstagramClient extends BaseMetaClient<InstagramInbound, In
|
|
|
31
31
|
readonly platform: "instagram";
|
|
32
32
|
private readonly config;
|
|
33
33
|
private readonly formatConverterInstance;
|
|
34
|
-
private readonly mediaCache?;
|
|
35
|
-
private readonly mediaStrategy;
|
|
36
34
|
private readonly splitter;
|
|
37
35
|
constructor(config: InstagramClientConfig);
|
|
38
36
|
/** Instagram-specific format converter (plain text). */
|
|
@@ -44,19 +42,20 @@ export declare class InstagramClient extends BaseMetaClient<InstagramInbound, In
|
|
|
44
42
|
*/
|
|
45
43
|
sendText(to: string, text: string): Promise<SendResult>;
|
|
46
44
|
/**
|
|
47
|
-
* Send
|
|
48
|
-
*
|
|
49
|
-
* Uses {@link MediaStrategy} so identical Buffer uploads dedup by
|
|
50
|
-
* SHA-256 content hash (C-13.7).
|
|
45
|
+
* Send a media message by URL. Instagram messaging supports audio, image,
|
|
46
|
+
* video, and file attachments via `attachment.payload.url`.
|
|
51
47
|
*/
|
|
52
48
|
sendMedia(to: string, media: MediaPayload): Promise<SendResult>;
|
|
53
49
|
sendInteractive(to: string, msg: InteractiveMessage): Promise<SendResult>;
|
|
54
50
|
sendRaw(to: string, payload: InstagramOutbound): Promise<SendResult>;
|
|
55
|
-
/**
|
|
56
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Messenger/Instagram `mark_seen` targets the conversation partner (PSID),
|
|
53
|
+
* not a message id. Pass the user's IGSID despite the PlatformClient param name.
|
|
54
|
+
*/
|
|
55
|
+
markAsRead(recipientId: string): Promise<void>;
|
|
57
56
|
sendTypingIndicator(to: string): Promise<void>;
|
|
58
|
-
uploadMedia(
|
|
59
|
-
downloadMedia(
|
|
57
|
+
uploadMedia(_file: Buffer | ReadableStream, _options: MediaUploadOptions): Promise<MediaHandle>;
|
|
58
|
+
downloadMedia(_mediaId: string): Promise<MediaDownload>;
|
|
60
59
|
sendQuickReplies(to: string, text: string, replies: InstagramQuickReply[]): Promise<SendResult>;
|
|
61
60
|
sendGenericTemplate(to: string, template: InstagramGenericTemplate): Promise<SendResult>;
|
|
62
61
|
sendButtonTemplate(to: string, template: InstagramButtonTemplate): Promise<SendResult>;
|
|
@@ -74,6 +73,7 @@ export declare class InstagramClient extends BaseMetaClient<InstagramInbound, In
|
|
|
74
73
|
private mapMessageType;
|
|
75
74
|
private extractTextFallback;
|
|
76
75
|
private extractMedia;
|
|
76
|
+
private resolveAttachmentType;
|
|
77
77
|
private sendSingleText;
|
|
78
78
|
}
|
|
79
79
|
export {};
|
package/dist/instagram/client.js
CHANGED
|
@@ -8,15 +8,15 @@
|
|
|
8
8
|
* inherited. Only Instagram-specific outbound paths and inbound
|
|
9
9
|
* normalization live here.
|
|
10
10
|
*
|
|
11
|
-
* Key
|
|
11
|
+
* Key differences from WhatsApp / Messenger:
|
|
12
12
|
* - Base URL is `graph.instagram.com` by default.
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
13
|
+
* - Media is sent by public URL (audio, image, video, file attachments).
|
|
14
|
+
* - Message limit is 1000 bytes (UTF-8), not characters.
|
|
15
|
+
* - Send response contains `message_id` (recipient id is implicit).
|
|
16
|
+
* - Only `HUMAN_AGENT` message tag is supported (7-day window).
|
|
17
|
+
* - Ice breakers replace persistent menus.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { MessagingError, } from '@kuralle-agents/messaging';
|
|
20
20
|
import { BaseMetaClient } from '../base-client.js';
|
|
21
21
|
import { GraphAPIClient } from '../graph-api/client.js';
|
|
22
22
|
import { ByteLimitSplitter } from '../message-splitter.js';
|
|
@@ -40,8 +40,6 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
40
40
|
platform = 'instagram';
|
|
41
41
|
config;
|
|
42
42
|
formatConverterInstance;
|
|
43
|
-
mediaCache;
|
|
44
|
-
mediaStrategy;
|
|
45
43
|
splitter = new ByteLimitSplitter(MAX_MESSAGE_BYTES);
|
|
46
44
|
constructor(config) {
|
|
47
45
|
const full = config;
|
|
@@ -58,8 +56,6 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
58
56
|
super(full, graphApi);
|
|
59
57
|
this.config = full;
|
|
60
58
|
this.formatConverterInstance = new InstagramFormatConverter();
|
|
61
|
-
this.mediaCache = config.mediaCache;
|
|
62
|
-
this.mediaStrategy = new FileHashDedupStrategy(new UploadStrategy((file, opts) => this.uploadMedia(file, opts)));
|
|
63
59
|
}
|
|
64
60
|
/** Instagram-specific format converter (plain text). */
|
|
65
61
|
get formatConverter() {
|
|
@@ -82,25 +78,18 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
82
78
|
return result;
|
|
83
79
|
}
|
|
84
80
|
/**
|
|
85
|
-
* Send
|
|
86
|
-
*
|
|
87
|
-
* Uses {@link MediaStrategy} so identical Buffer uploads dedup by
|
|
88
|
-
* SHA-256 content hash (C-13.7).
|
|
81
|
+
* Send a media message by URL. Instagram messaging supports audio, image,
|
|
82
|
+
* video, and file attachments via `attachment.payload.url`.
|
|
89
83
|
*/
|
|
90
84
|
async sendMedia(to, media) {
|
|
91
|
-
if (media.
|
|
92
|
-
throw new
|
|
85
|
+
if (typeof media.data !== 'string') {
|
|
86
|
+
throw new MessagingError('Instagram messaging sends media by public URL — pass media.data as a URL string.', 'unsupported', 'instagram');
|
|
93
87
|
}
|
|
94
|
-
|
|
95
|
-
const normalized = typeof media.data === 'string' || Buffer.isBuffer(media.data)
|
|
96
|
-
? media
|
|
97
|
-
: { ...media, data: await streamToBuffer(media.data) };
|
|
98
|
-
const resolved = await this.mediaStrategy.resolve(normalized);
|
|
99
|
-
const url = resolved.kind === 'url' ? resolved.url : (resolved.handle.url ?? '');
|
|
88
|
+
const attachmentType = this.resolveAttachmentType(media.mimeType, media.type);
|
|
100
89
|
const response = await this.graphApi.post(`${this.config.igId}/messages`, {
|
|
101
90
|
recipient: { id: to },
|
|
102
91
|
message: {
|
|
103
|
-
|
|
92
|
+
attachment: { type: attachmentType, payload: { url: media.data } },
|
|
104
93
|
},
|
|
105
94
|
});
|
|
106
95
|
return this.toSendResult(to, response);
|
|
@@ -144,9 +133,15 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
144
133
|
});
|
|
145
134
|
return this.toSendResult(to, response);
|
|
146
135
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Messenger/Instagram `mark_seen` targets the conversation partner (PSID),
|
|
138
|
+
* not a message id. Pass the user's IGSID despite the PlatformClient param name.
|
|
139
|
+
*/
|
|
140
|
+
async markAsRead(recipientId) {
|
|
141
|
+
await this.graphApi.post(`${this.config.igId}/messages`, {
|
|
142
|
+
recipient: { id: recipientId },
|
|
143
|
+
sender_action: 'mark_seen',
|
|
144
|
+
});
|
|
150
145
|
}
|
|
151
146
|
async sendTypingIndicator(to) {
|
|
152
147
|
await this.graphApi.post(`${this.config.igId}/messages`, {
|
|
@@ -157,26 +152,11 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
157
152
|
// =========================================================================
|
|
158
153
|
// Media
|
|
159
154
|
// =========================================================================
|
|
160
|
-
async uploadMedia(
|
|
161
|
-
|
|
162
|
-
const blob = new Blob([buffer], { type: options.mimeType });
|
|
163
|
-
const formData = new FormData();
|
|
164
|
-
formData.append('file', blob, options.filename ?? 'file');
|
|
165
|
-
formData.append('type', options.mimeType);
|
|
166
|
-
const response = await this.graphApi.postFormData(`${this.config.igId}/media`, formData);
|
|
167
|
-
return { mediaId: response.id, url: response.url };
|
|
155
|
+
async uploadMedia(_file, _options) {
|
|
156
|
+
throw new MessagingError('Instagram messaging sends media by public URL — uploadMedia is not supported. Pass a URL via sendMedia instead.', 'unsupported', 'instagram');
|
|
168
157
|
}
|
|
169
|
-
async downloadMedia(
|
|
170
|
-
|
|
171
|
-
return this.mediaCache.getOrDownload(mediaId, async () => {
|
|
172
|
-
const mediaInfo = await this.graphApi.get(mediaId);
|
|
173
|
-
const data = await this.graphApi.fetchBinary(mediaInfo.url);
|
|
174
|
-
return { data, mimeType: mediaInfo.mime_type };
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
const mediaInfo = await this.graphApi.get(mediaId);
|
|
178
|
-
const data = await this.graphApi.fetchBinary(mediaInfo.url);
|
|
179
|
-
return { data, mimeType: mediaInfo.mime_type };
|
|
158
|
+
async downloadMedia(_mediaId) {
|
|
159
|
+
throw new MessagingError('Instagram inbound media arrives as CDN URLs in webhooks — downloadMedia is not supported.', 'unsupported', 'instagram');
|
|
180
160
|
}
|
|
181
161
|
// =========================================================================
|
|
182
162
|
// Instagram-specific — Quick Replies
|
|
@@ -253,12 +233,14 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
253
233
|
},
|
|
254
234
|
get: async () => {
|
|
255
235
|
const result = await this.graphApi.get(`${this.config.igId}/messenger_profile`, { fields: 'ice_breakers' });
|
|
256
|
-
|
|
236
|
+
const row = result.data?.[0];
|
|
237
|
+
if (!row?.call_to_actions)
|
|
238
|
+
return [];
|
|
239
|
+
return [{ call_to_actions: row.call_to_actions, locale: row.locale }];
|
|
257
240
|
},
|
|
258
241
|
delete: async () => {
|
|
259
|
-
await this.graphApi.
|
|
260
|
-
fields: ['ice_breakers'],
|
|
261
|
-
_method: 'DELETE',
|
|
242
|
+
await this.graphApi.delete(`${this.config.igId}/messenger_profile`, {
|
|
243
|
+
body: { fields: ['ice_breakers'] },
|
|
262
244
|
});
|
|
263
245
|
},
|
|
264
246
|
};
|
|
@@ -343,6 +325,8 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
343
325
|
image: 'image',
|
|
344
326
|
video: 'video',
|
|
345
327
|
audio: 'audio',
|
|
328
|
+
file: 'document',
|
|
329
|
+
document: 'document',
|
|
346
330
|
sticker: 'sticker',
|
|
347
331
|
interactive: 'interactive',
|
|
348
332
|
postback: 'interactive',
|
|
@@ -362,19 +346,32 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
362
346
|
return undefined;
|
|
363
347
|
}
|
|
364
348
|
extractMedia(msg) {
|
|
365
|
-
const mediaTypes = ['image', 'video', 'audio', 'sticker'];
|
|
349
|
+
const mediaTypes = ['image', 'video', 'audio', 'document', 'sticker'];
|
|
366
350
|
for (const type of mediaTypes) {
|
|
367
351
|
const media = msg[type];
|
|
368
|
-
if (media && 'id' in media) {
|
|
352
|
+
if (media && ('id' in media || 'url' in media)) {
|
|
369
353
|
return {
|
|
370
|
-
id: media.id,
|
|
354
|
+
id: 'id' in media ? media.id : '',
|
|
371
355
|
mimeType: 'mime_type' in media ? media.mime_type : undefined,
|
|
356
|
+
url: 'url' in media ? media.url : undefined,
|
|
372
357
|
caption: 'caption' in media ? media.caption : undefined,
|
|
358
|
+
filename: 'filename' in media ? media.filename : undefined,
|
|
373
359
|
};
|
|
374
360
|
}
|
|
375
361
|
}
|
|
376
362
|
return undefined;
|
|
377
363
|
}
|
|
364
|
+
resolveAttachmentType(mimeType, mediaType) {
|
|
365
|
+
if (mediaType === 'document')
|
|
366
|
+
return 'file';
|
|
367
|
+
if (mimeType.startsWith('image/'))
|
|
368
|
+
return 'image';
|
|
369
|
+
if (mimeType.startsWith('video/'))
|
|
370
|
+
return 'video';
|
|
371
|
+
if (mimeType.startsWith('audio/'))
|
|
372
|
+
return 'audio';
|
|
373
|
+
return 'file';
|
|
374
|
+
}
|
|
378
375
|
async sendSingleText(to, text) {
|
|
379
376
|
const response = await this.graphApi.post(`${this.config.igId}/messages`, {
|
|
380
377
|
recipient: { id: to },
|
|
@@ -383,17 +380,3 @@ export class InstagramClient extends BaseMetaClient {
|
|
|
383
380
|
return this.toSendResult(to, response);
|
|
384
381
|
}
|
|
385
382
|
}
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
// Utility
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
async function streamToBuffer(stream) {
|
|
390
|
-
const chunks = [];
|
|
391
|
-
const reader = stream.getReader();
|
|
392
|
-
for (;;) {
|
|
393
|
-
const { done, value } = await reader.read();
|
|
394
|
-
if (done)
|
|
395
|
-
break;
|
|
396
|
-
chunks.push(value);
|
|
397
|
-
}
|
|
398
|
-
return Buffer.concat(chunks);
|
|
399
|
-
}
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Key differences from WhatsApp / Messenger:
|
|
11
11
|
* - Base URL is `graph.instagram.com` (not `graph.facebook.com`).
|
|
12
|
-
* -
|
|
12
|
+
* - Media is sent by public URL (audio, image, video, file attachments).
|
|
13
13
|
* - Message limit is 1000 bytes (UTF-8), not characters.
|
|
14
|
-
* -
|
|
14
|
+
* - Quick replies support text, user_phone_number, and user_email content types.
|
|
15
15
|
* - Only `HUMAN_AGENT` message tag is supported (7-day window).
|
|
16
16
|
* - Ice breakers replace persistent menus.
|
|
17
17
|
*/
|
|
@@ -61,27 +61,25 @@ export interface InstagramThreadId {
|
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
63
|
* Raw response from the Instagram Messaging API messages endpoint.
|
|
64
|
-
*
|
|
65
|
-
* NOTE: Unlike Messenger, Instagram does NOT return `recipient_id`
|
|
66
|
-
* in the send response.
|
|
67
64
|
*/
|
|
68
65
|
export interface InstagramSendResponse {
|
|
69
66
|
/** Platform-assigned message identifier. */
|
|
70
67
|
message_id: string;
|
|
68
|
+
/** Recipient IGSID (returned by some API versions). */
|
|
69
|
+
recipient_id?: string;
|
|
71
70
|
}
|
|
72
71
|
/**
|
|
73
72
|
* A quick reply option for Instagram messages.
|
|
74
73
|
*
|
|
75
|
-
* Instagram only supports `content_type: "text"` quick replies.
|
|
76
74
|
* Maximum 13 quick replies per message.
|
|
77
75
|
*/
|
|
78
76
|
export interface InstagramQuickReply {
|
|
79
|
-
/**
|
|
80
|
-
content_type: 'text';
|
|
81
|
-
/** Display text for
|
|
82
|
-
title
|
|
77
|
+
/** Quick reply content type. */
|
|
78
|
+
content_type: 'text' | 'user_phone_number' | 'user_email';
|
|
79
|
+
/** Display text for text quick replies (max 20 chars). */
|
|
80
|
+
title?: string;
|
|
83
81
|
/** Payload string returned when the user taps this quick reply. */
|
|
84
|
-
payload
|
|
82
|
+
payload?: string;
|
|
85
83
|
}
|
|
86
84
|
/**
|
|
87
85
|
* A button within an Instagram template message.
|
package/dist/instagram/types.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Key differences from WhatsApp / Messenger:
|
|
11
11
|
* - Base URL is `graph.instagram.com` (not `graph.facebook.com`).
|
|
12
|
-
* -
|
|
12
|
+
* - Media is sent by public URL (audio, image, video, file attachments).
|
|
13
13
|
* - Message limit is 1000 bytes (UTF-8), not characters.
|
|
14
|
-
* -
|
|
14
|
+
* - Quick replies support text, user_phone_number, and user_email content types.
|
|
15
15
|
* - Only `HUMAN_AGENT` message tag is supported (7-day window).
|
|
16
16
|
* - Ice breakers replace persistent menus.
|
|
17
17
|
*/
|
|
@@ -63,7 +63,7 @@ export declare class MessengerClient extends BaseMetaClient<MessengerInbound, Me
|
|
|
63
63
|
sendMedia(to: string, media: MediaPayload): Promise<SendResult>;
|
|
64
64
|
sendInteractive(to: string, msg: InteractiveMessage): Promise<SendResult>;
|
|
65
65
|
sendRaw(to: string, payload: MessengerOutbound): Promise<SendResult>;
|
|
66
|
-
markAsRead(
|
|
66
|
+
markAsRead(recipientId: string): Promise<void>;
|
|
67
67
|
sendTypingIndicator(to: string): Promise<void>;
|
|
68
68
|
sendButtonTemplate(to: string, template: ButtonTemplate): Promise<SendResult>;
|
|
69
69
|
sendGenericTemplate(to: string, template: GenericTemplate): Promise<SendResult>;
|
package/dist/messenger/client.js
CHANGED
|
@@ -150,11 +150,12 @@ export class MessengerClient extends BaseMetaClient {
|
|
|
150
150
|
});
|
|
151
151
|
return this.toSendResult(to, response);
|
|
152
152
|
}
|
|
153
|
-
async markAsRead(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
async markAsRead(recipientId) {
|
|
154
|
+
/**
|
|
155
|
+
* Messenger/Instagram `mark_seen` targets the conversation partner (PSID),
|
|
156
|
+
* not a message id. Pass the user's PSID despite the PlatformClient param name.
|
|
157
|
+
*/
|
|
158
|
+
await this.sendSenderAction(recipientId, 'mark_seen');
|
|
158
159
|
}
|
|
159
160
|
async sendTypingIndicator(to) {
|
|
160
161
|
await this.sendSenderAction(to, 'typing_on');
|
|
@@ -231,7 +232,7 @@ export class MessengerClient extends BaseMetaClient {
|
|
|
231
232
|
return this.graphApi.post(`${this.config.pageId}/personas`, config);
|
|
232
233
|
},
|
|
233
234
|
delete: async (personaId) => {
|
|
234
|
-
await this.graphApi.
|
|
235
|
+
await this.graphApi.delete(personaId);
|
|
235
236
|
},
|
|
236
237
|
};
|
|
237
238
|
// =========================================================================
|
package/dist/webhook/index.d.ts
CHANGED
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
export { verifySignature } from './verifier.js';
|
|
7
7
|
export type { VerifySignatureOptions } from './verifier.js';
|
|
8
8
|
export { normalizeWebhook } from './normalizer.js';
|
|
9
|
-
export type { NormalizedWebhookEvents, NormalizedMessage, NormalizedStatus, NormalizedReaction, } from './normalizer.js';
|
|
9
|
+
export type { NormalizedWebhookEvents, NormalizedMessage, NormalizedStatus, NormalizedReaction, NormalizedWebhookError, WhatsAppReferral, } from './normalizer.js';
|
|
@@ -20,6 +20,19 @@ export interface NormalizedWebhookEvents {
|
|
|
20
20
|
statuses: NormalizedStatus[];
|
|
21
21
|
/** Emoji reactions on existing messages. */
|
|
22
22
|
reactions: NormalizedReaction[];
|
|
23
|
+
/** Account-level webhook errors (WhatsApp `value.errors`). */
|
|
24
|
+
errors: NormalizedWebhookError[];
|
|
25
|
+
}
|
|
26
|
+
/** Account-level error from a Meta webhook payload. */
|
|
27
|
+
export interface NormalizedWebhookError {
|
|
28
|
+
code: number;
|
|
29
|
+
title?: string;
|
|
30
|
+
message?: string;
|
|
31
|
+
error_data?: {
|
|
32
|
+
details?: string;
|
|
33
|
+
};
|
|
34
|
+
href?: string;
|
|
35
|
+
phoneNumberId?: string;
|
|
23
36
|
}
|
|
24
37
|
/** A single inbound message extracted from the webhook payload. */
|
|
25
38
|
export interface NormalizedMessage {
|
|
@@ -101,10 +114,19 @@ export interface NormalizedMessage {
|
|
|
101
114
|
currency: string;
|
|
102
115
|
}>;
|
|
103
116
|
};
|
|
104
|
-
/** Quoted/replied-to message context. */
|
|
117
|
+
/** Quoted/replied-to message context (normalized: WhatsApp's raw `context.id` maps to `message_id`). */
|
|
105
118
|
context?: {
|
|
106
119
|
message_id: string;
|
|
107
120
|
from?: string;
|
|
121
|
+
/** Set when the message was forwarded (WhatsApp). */
|
|
122
|
+
forwarded?: boolean;
|
|
123
|
+
/** Set when forwarded many times (WhatsApp). */
|
|
124
|
+
frequently_forwarded?: boolean;
|
|
125
|
+
/** Catalog product the user is asking about (WhatsApp product inquiry). */
|
|
126
|
+
referred_product?: {
|
|
127
|
+
catalog_id: string;
|
|
128
|
+
product_retailer_id: string;
|
|
129
|
+
};
|
|
108
130
|
};
|
|
109
131
|
/** Reaction (only set for WhatsApp reaction messages before they're split out). */
|
|
110
132
|
reaction?: {
|
|
@@ -112,7 +134,23 @@ export interface NormalizedMessage {
|
|
|
112
134
|
emoji: string;
|
|
113
135
|
};
|
|
114
136
|
/** Click-to-WhatsApp ad referral data. */
|
|
115
|
-
referral?:
|
|
137
|
+
referral?: WhatsAppReferral;
|
|
138
|
+
}
|
|
139
|
+
/** Click-to-WhatsApp ad referral object from WhatsApp webhooks. */
|
|
140
|
+
export interface WhatsAppReferral {
|
|
141
|
+
source_url?: string;
|
|
142
|
+
source_id?: string;
|
|
143
|
+
source_type?: string;
|
|
144
|
+
body?: string;
|
|
145
|
+
headline?: string;
|
|
146
|
+
media_type?: string;
|
|
147
|
+
image_url?: string;
|
|
148
|
+
video_url?: string;
|
|
149
|
+
thumbnail_url?: string;
|
|
150
|
+
ctwa_clid?: string;
|
|
151
|
+
welcome_message?: {
|
|
152
|
+
text?: string;
|
|
153
|
+
};
|
|
116
154
|
}
|
|
117
155
|
/** A delivery status update. */
|
|
118
156
|
export interface NormalizedStatus {
|
|
@@ -120,12 +158,16 @@ export interface NormalizedStatus {
|
|
|
120
158
|
id: string;
|
|
121
159
|
/** Recipient identifier. */
|
|
122
160
|
recipientId: string;
|
|
123
|
-
/** Status value. */
|
|
124
|
-
status: 'sent' | 'delivered' | 'read' | 'failed';
|
|
161
|
+
/** Status value (`played` is WhatsApp voice-note playback). */
|
|
162
|
+
status: 'sent' | 'delivered' | 'read' | 'failed' | 'played' | (string & {});
|
|
125
163
|
/** Unix timestamp (seconds) as a string. */
|
|
126
164
|
timestamp: string;
|
|
127
165
|
/** Phone number ID or page ID that sent the original message. */
|
|
128
166
|
phoneNumberId: string;
|
|
167
|
+
/** Recipient type (WhatsApp). */
|
|
168
|
+
recipient_type?: string;
|
|
169
|
+
/** Opaque callback data echoed from the send request (WhatsApp). */
|
|
170
|
+
biz_opaque_callback_data?: string;
|
|
129
171
|
/** Conversation metadata (WhatsApp-specific). */
|
|
130
172
|
conversation?: {
|
|
131
173
|
id: string;
|
|
@@ -138,13 +180,18 @@ export interface NormalizedStatus {
|
|
|
138
180
|
pricing?: {
|
|
139
181
|
billable: boolean;
|
|
140
182
|
pricing_model: string;
|
|
141
|
-
category: string;
|
|
183
|
+
category: string | (string & {});
|
|
184
|
+
type?: 'regular' | 'free_customer_service' | 'free_entry_point' | (string & {});
|
|
142
185
|
};
|
|
143
186
|
/** Error details when `status === "failed"`. */
|
|
144
187
|
errors?: Array<{
|
|
145
188
|
code: number;
|
|
146
189
|
title?: string;
|
|
147
190
|
message?: string;
|
|
191
|
+
error_data?: {
|
|
192
|
+
details?: string;
|
|
193
|
+
};
|
|
194
|
+
href?: string;
|
|
148
195
|
}>;
|
|
149
196
|
}
|
|
150
197
|
/** An emoji reaction on an existing message. */
|
|
@@ -41,6 +41,7 @@ export function normalizeWebhook(payload) {
|
|
|
41
41
|
messages: [],
|
|
42
42
|
statuses: [],
|
|
43
43
|
reactions: [],
|
|
44
|
+
errors: [],
|
|
44
45
|
};
|
|
45
46
|
if (!payload || typeof payload !== 'object') {
|
|
46
47
|
return result;
|
|
@@ -68,6 +69,7 @@ function normalizeWhatsAppWebhook(payload) {
|
|
|
68
69
|
messages: [],
|
|
69
70
|
statuses: [],
|
|
70
71
|
reactions: [],
|
|
72
|
+
errors: [],
|
|
71
73
|
};
|
|
72
74
|
const entries = payload.entry ?? [];
|
|
73
75
|
for (const entry of entries) {
|
|
@@ -130,12 +132,30 @@ function normalizeWhatsAppWebhook(payload) {
|
|
|
130
132
|
normalized.button = msg.button;
|
|
131
133
|
if (msg.order)
|
|
132
134
|
normalized.order = msg.order;
|
|
133
|
-
if (msg.context)
|
|
134
|
-
|
|
135
|
+
if (msg.context) {
|
|
136
|
+
// Real WhatsApp webhooks send `context.{id, from}` (plus forwarding
|
|
137
|
+
// flags / referred_product) — normalize raw `id` to `message_id`.
|
|
138
|
+
const referred = msg.context.referred_product;
|
|
139
|
+
normalized.context = {
|
|
140
|
+
message_id: msg.context.id ?? '',
|
|
141
|
+
from: msg.context.from,
|
|
142
|
+
forwarded: msg.context.forwarded,
|
|
143
|
+
frequently_forwarded: msg.context.frequently_forwarded,
|
|
144
|
+
referred_product: referred?.catalog_id && referred.product_retailer_id
|
|
145
|
+
? {
|
|
146
|
+
catalog_id: referred.catalog_id,
|
|
147
|
+
product_retailer_id: referred.product_retailer_id,
|
|
148
|
+
}
|
|
149
|
+
: undefined,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
135
152
|
if (msg.referral)
|
|
136
153
|
normalized.referral = msg.referral;
|
|
137
154
|
result.messages.push(normalized);
|
|
138
155
|
}
|
|
156
|
+
for (const err of value.errors ?? []) {
|
|
157
|
+
result.errors.push(normalizeWebhookError(err, phoneNumberId));
|
|
158
|
+
}
|
|
139
159
|
// --- Statuses ---
|
|
140
160
|
for (const status of value.statuses ?? []) {
|
|
141
161
|
const normalizedStatus = {
|
|
@@ -145,6 +165,11 @@ function normalizeWhatsAppWebhook(payload) {
|
|
|
145
165
|
timestamp: status.timestamp ?? '',
|
|
146
166
|
phoneNumberId,
|
|
147
167
|
};
|
|
168
|
+
if (status.recipient_type)
|
|
169
|
+
normalizedStatus.recipient_type = status.recipient_type;
|
|
170
|
+
if (status.biz_opaque_callback_data) {
|
|
171
|
+
normalizedStatus.biz_opaque_callback_data = status.biz_opaque_callback_data;
|
|
172
|
+
}
|
|
148
173
|
if (status.conversation) {
|
|
149
174
|
normalizedStatus.conversation = status.conversation;
|
|
150
175
|
}
|
|
@@ -174,6 +199,7 @@ function normalizePageWebhook(payload) {
|
|
|
174
199
|
messages: [],
|
|
175
200
|
statuses: [],
|
|
176
201
|
reactions: [],
|
|
202
|
+
errors: [],
|
|
177
203
|
};
|
|
178
204
|
const entries = payload.entry ?? [];
|
|
179
205
|
for (const entry of entries) {
|
|
@@ -216,6 +242,13 @@ function normalizePageWebhook(payload) {
|
|
|
216
242
|
if (msg.reply_to?.mid) {
|
|
217
243
|
normalized.context = { message_id: msg.reply_to.mid };
|
|
218
244
|
}
|
|
245
|
+
if (msg.quick_reply?.payload) {
|
|
246
|
+
normalized.type = 'postback';
|
|
247
|
+
normalized.button = {
|
|
248
|
+
text: msg.text ?? '',
|
|
249
|
+
payload: msg.quick_reply.payload,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
219
252
|
result.messages.push(normalized);
|
|
220
253
|
continue;
|
|
221
254
|
}
|
|
@@ -288,15 +321,30 @@ function resolvePageMessageType(msg) {
|
|
|
288
321
|
}
|
|
289
322
|
return 'unknown';
|
|
290
323
|
}
|
|
291
|
-
/** Normalise a status string
|
|
324
|
+
/** Normalise a status string; unknown values pass through unchanged. */
|
|
292
325
|
function normalizeStatusValue(raw) {
|
|
326
|
+
if (!raw)
|
|
327
|
+
return 'sent';
|
|
293
328
|
switch (raw) {
|
|
294
329
|
case 'sent':
|
|
295
330
|
case 'delivered':
|
|
296
331
|
case 'read':
|
|
297
332
|
case 'failed':
|
|
333
|
+
case 'played':
|
|
298
334
|
return raw;
|
|
299
335
|
default:
|
|
300
|
-
return
|
|
336
|
+
return raw;
|
|
301
337
|
}
|
|
302
338
|
}
|
|
339
|
+
function normalizeWebhookError(err, phoneNumberId) {
|
|
340
|
+
return {
|
|
341
|
+
code: typeof err.code === 'number' ? err.code : 0,
|
|
342
|
+
title: typeof err.title === 'string' ? err.title : undefined,
|
|
343
|
+
message: typeof err.message === 'string' ? err.message : undefined,
|
|
344
|
+
error_data: err.error_data && typeof err.error_data === 'object'
|
|
345
|
+
? err.error_data
|
|
346
|
+
: undefined,
|
|
347
|
+
href: typeof err.href === 'string' ? err.href : undefined,
|
|
348
|
+
phoneNumberId,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
@@ -25,7 +25,7 @@ import { type FormatConverter, type InboundMessage, type InteractiveMessage, typ
|
|
|
25
25
|
import { BaseMetaClient } from '../base-client.js';
|
|
26
26
|
import type { BaseMetaClientConfig } from '../base-client.js';
|
|
27
27
|
import type { NormalizedMessage, NormalizedReaction, NormalizedStatus } from '../webhook/normalizer.js';
|
|
28
|
-
import type { WhatsAppClientConfig, TemplateMessage, TemplateInfo, TemplateDefinition, TextOrTemplateOptions, ListMessage, ButtonMessage, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, CatalogMessage, AddressMessage, LocationPayload, ContactPayload, BusinessProfile, FlowDefinition, FlowInfo, FlowAssets } from './types.js';
|
|
28
|
+
import type { WhatsAppClientConfig, TemplateMessage, TemplateInfo, TemplateDefinition, TemplateCreateResponse, TextOrTemplateOptions, ListMessage, ButtonMessage, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, CatalogMessage, AddressMessage, LocationPayload, ContactPayload, BusinessProfile, FlowDefinition, FlowInfo, FlowAssets } from './types.js';
|
|
29
29
|
type WhatsAppInbound = NormalizedMessage;
|
|
30
30
|
type WhatsAppOutbound = Record<string, unknown>;
|
|
31
31
|
/** Internal config — base-client common fields threaded through the public config. */
|
|
@@ -61,8 +61,15 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
|
|
|
61
61
|
sendMedia(to: string, media: MediaPayload): Promise<SendResult>;
|
|
62
62
|
sendInteractive(to: string, msg: InteractiveMessage): Promise<SendResult>;
|
|
63
63
|
sendRaw(to: string, payload: WhatsAppOutbound): Promise<SendResult>;
|
|
64
|
-
markAsRead(messageId: string
|
|
65
|
-
|
|
64
|
+
markAsRead(messageId: string, opts?: {
|
|
65
|
+
typing?: boolean;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
/** Show a typing indicator for 25s (requires a recent inbound message id). */
|
|
68
|
+
sendTypingIndicatorFor(messageId: string): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* WhatsApp typing indicators are keyed by message ID, not recipient.
|
|
71
|
+
* Use {@link sendTypingIndicatorFor} with the inbound message id instead.
|
|
72
|
+
*/
|
|
66
73
|
sendTypingIndicator(_to: string): Promise<void>;
|
|
67
74
|
sendTemplate(to: string, template: TemplateMessage): Promise<SendResult>;
|
|
68
75
|
/**
|
|
@@ -116,8 +123,11 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
|
|
|
116
123
|
sendContacts(to: string, contacts: ContactPayload[]): Promise<SendResult>;
|
|
117
124
|
readonly templates: {
|
|
118
125
|
list: (wabaId: string) => Promise<TemplateInfo[]>;
|
|
119
|
-
create: (wabaId: string, template: TemplateDefinition) => Promise<
|
|
120
|
-
delete: (wabaId: string,
|
|
126
|
+
create: (wabaId: string, template: TemplateDefinition) => Promise<TemplateCreateResponse>;
|
|
127
|
+
delete: (wabaId: string, opts: {
|
|
128
|
+
name?: string;
|
|
129
|
+
hsm_id?: string;
|
|
130
|
+
}) => Promise<void>;
|
|
121
131
|
};
|
|
122
132
|
readonly phoneNumbers: {
|
|
123
133
|
requestCode: (phoneNumberId: string, method: "SMS" | "VOICE", language: string) => Promise<void>;
|
|
@@ -130,6 +140,7 @@ export declare class WhatsAppClient extends BaseMetaClient<WhatsAppInbound, What
|
|
|
130
140
|
create: (wabaId: string, flow: FlowDefinition) => Promise<FlowInfo>;
|
|
131
141
|
update: (flowId: string, flow: Partial<FlowDefinition>) => Promise<FlowInfo>;
|
|
132
142
|
publish: (flowId: string) => Promise<void>;
|
|
143
|
+
deprecate: (flowId: string) => Promise<void>;
|
|
133
144
|
delete: (flowId: string) => Promise<void>;
|
|
134
145
|
getAssets: (flowId: string) => Promise<FlowAssets>;
|
|
135
146
|
};
|
package/dist/whatsapp/client.js
CHANGED
|
@@ -137,7 +137,7 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
137
137
|
footer: msg.footer ? { text: msg.footer } : undefined,
|
|
138
138
|
flowId: msg.action.flowId,
|
|
139
139
|
flowCta: 'Continue',
|
|
140
|
-
flowToken: msg.action.flowToken
|
|
140
|
+
flowToken: msg.action.flowToken,
|
|
141
141
|
flowAction: 'navigate',
|
|
142
142
|
flowActionPayload: msg.action.parameters,
|
|
143
143
|
});
|
|
@@ -153,14 +153,25 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
153
153
|
});
|
|
154
154
|
return this.toSendResult(to, response);
|
|
155
155
|
}
|
|
156
|
-
async markAsRead(messageId) {
|
|
157
|
-
|
|
156
|
+
async markAsRead(messageId, opts) {
|
|
157
|
+
const body = {
|
|
158
158
|
messaging_product: 'whatsapp',
|
|
159
159
|
status: 'read',
|
|
160
160
|
message_id: messageId,
|
|
161
|
-
}
|
|
161
|
+
};
|
|
162
|
+
if (opts?.typing) {
|
|
163
|
+
body.typing_indicator = { type: 'text' };
|
|
164
|
+
}
|
|
165
|
+
await this.graphApi.post(`${this.config.phoneNumberId}/messages`, body);
|
|
162
166
|
}
|
|
163
|
-
/**
|
|
167
|
+
/** Show a typing indicator for 25s (requires a recent inbound message id). */
|
|
168
|
+
async sendTypingIndicatorFor(messageId) {
|
|
169
|
+
await this.markAsRead(messageId, { typing: true });
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* WhatsApp typing indicators are keyed by message ID, not recipient.
|
|
173
|
+
* Use {@link sendTypingIndicatorFor} with the inbound message id instead.
|
|
174
|
+
*/
|
|
164
175
|
async sendTypingIndicator(_to) {
|
|
165
176
|
// intentionally empty
|
|
166
177
|
}
|
|
@@ -199,6 +210,10 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
199
210
|
// WhatsApp-specific — Interactive messages
|
|
200
211
|
// =========================================================================
|
|
201
212
|
async sendListMessage(to, list) {
|
|
213
|
+
const totalRows = list.sections.reduce((sum, section) => sum + section.rows.length, 0);
|
|
214
|
+
if (totalRows > 10) {
|
|
215
|
+
throw new MessagingError(`List message exceeds maximum of 10 rows across all sections (has ${totalRows})`, 'INVALID_LIST_MESSAGE', 'whatsapp');
|
|
216
|
+
}
|
|
202
217
|
const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
|
|
203
218
|
messaging_product: 'whatsapp',
|
|
204
219
|
recipient_type: 'individual',
|
|
@@ -258,6 +273,16 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
258
273
|
return this.toSendResult(to, response);
|
|
259
274
|
}
|
|
260
275
|
async sendInteractiveFlow(to, flow) {
|
|
276
|
+
const parameters = {
|
|
277
|
+
flow_message_version: '3',
|
|
278
|
+
flow_id: flow.flowId,
|
|
279
|
+
flow_cta: flow.flowCta,
|
|
280
|
+
flow_action: flow.flowAction,
|
|
281
|
+
flow_action_payload: flow.flowActionPayload,
|
|
282
|
+
};
|
|
283
|
+
if (flow.flowToken) {
|
|
284
|
+
parameters.flow_token = flow.flowToken;
|
|
285
|
+
}
|
|
261
286
|
const response = await this.graphApi.post(`${this.config.phoneNumberId}/messages`, {
|
|
262
287
|
messaging_product: 'whatsapp',
|
|
263
288
|
recipient_type: 'individual',
|
|
@@ -269,14 +294,7 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
269
294
|
footer: flow.footer,
|
|
270
295
|
action: {
|
|
271
296
|
name: 'flow',
|
|
272
|
-
parameters
|
|
273
|
-
flow_message_version: '3',
|
|
274
|
-
flow_id: flow.flowId,
|
|
275
|
-
flow_cta: flow.flowCta,
|
|
276
|
-
flow_token: flow.flowToken,
|
|
277
|
-
flow_action: flow.flowAction,
|
|
278
|
-
flow_action_payload: flow.flowActionPayload,
|
|
279
|
-
},
|
|
297
|
+
parameters,
|
|
280
298
|
},
|
|
281
299
|
},
|
|
282
300
|
});
|
|
@@ -438,17 +456,31 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
438
456
|
// =========================================================================
|
|
439
457
|
templates = {
|
|
440
458
|
list: async (wabaId) => {
|
|
441
|
-
const
|
|
442
|
-
|
|
459
|
+
const all = [];
|
|
460
|
+
let endpoint = `${wabaId}/message_templates`;
|
|
461
|
+
let extraParams;
|
|
462
|
+
for (let page = 0; page < 50; page++) {
|
|
463
|
+
const result = await this.graphApi.get(endpoint, extraParams);
|
|
464
|
+
all.push(...result.data.map(mapListTemplateRow));
|
|
465
|
+
const next = result.paging?.next;
|
|
466
|
+
if (!next)
|
|
467
|
+
break;
|
|
468
|
+
const parsed = parseGraphPagingNext(next);
|
|
469
|
+
endpoint = parsed.endpoint;
|
|
470
|
+
extraParams = parsed.params;
|
|
471
|
+
}
|
|
472
|
+
return all;
|
|
443
473
|
},
|
|
444
474
|
create: async (wabaId, template) => {
|
|
445
475
|
return this.graphApi.post(`${wabaId}/message_templates`, template);
|
|
446
476
|
},
|
|
447
|
-
delete: async (wabaId,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
477
|
+
delete: async (wabaId, opts) => {
|
|
478
|
+
const params = {};
|
|
479
|
+
if (opts.name)
|
|
480
|
+
params.name = opts.name;
|
|
481
|
+
if (opts.hsm_id)
|
|
482
|
+
params.hsm_id = opts.hsm_id;
|
|
483
|
+
await this.graphApi.delete(`${wabaId}/message_templates`, { params });
|
|
452
484
|
},
|
|
453
485
|
};
|
|
454
486
|
// =========================================================================
|
|
@@ -494,8 +526,11 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
494
526
|
publish: async (flowId) => {
|
|
495
527
|
await this.graphApi.post(`${flowId}/publish`, {});
|
|
496
528
|
},
|
|
529
|
+
deprecate: async (flowId) => {
|
|
530
|
+
await this.graphApi.post(`${flowId}/deprecate`, {});
|
|
531
|
+
},
|
|
497
532
|
delete: async (flowId) => {
|
|
498
|
-
await this.graphApi.
|
|
533
|
+
await this.graphApi.delete(flowId);
|
|
499
534
|
},
|
|
500
535
|
getAssets: async (flowId) => {
|
|
501
536
|
return this.graphApi.get(`${flowId}/assets`);
|
|
@@ -562,7 +597,10 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
562
597
|
}
|
|
563
598
|
: undefined,
|
|
564
599
|
context: msg.context
|
|
565
|
-
? {
|
|
600
|
+
? {
|
|
601
|
+
messageId: msg.context.message_id,
|
|
602
|
+
from: msg.context.from,
|
|
603
|
+
}
|
|
566
604
|
: undefined,
|
|
567
605
|
raw: msg,
|
|
568
606
|
};
|
|
@@ -594,6 +632,7 @@ export class WhatsAppClient extends BaseMetaClient {
|
|
|
594
632
|
? {
|
|
595
633
|
model: status.pricing.pricing_model,
|
|
596
634
|
category: status.pricing.category,
|
|
635
|
+
...(status.pricing.type ? { type: status.pricing.type } : {}),
|
|
597
636
|
}
|
|
598
637
|
: undefined,
|
|
599
638
|
raw: status,
|
|
@@ -717,6 +756,17 @@ function mapListTemplateRow(raw) {
|
|
|
717
756
|
paused,
|
|
718
757
|
};
|
|
719
758
|
}
|
|
759
|
+
function parseGraphPagingNext(nextUrl) {
|
|
760
|
+
const url = new URL(nextUrl);
|
|
761
|
+
const parts = url.pathname.split('/').filter(Boolean);
|
|
762
|
+
const endpoint = parts.slice(1).join('/');
|
|
763
|
+
const params = {};
|
|
764
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
765
|
+
if (key !== 'access_token')
|
|
766
|
+
params[key] = value;
|
|
767
|
+
}
|
|
768
|
+
return { endpoint, params: Object.keys(params).length > 0 ? params : undefined };
|
|
769
|
+
}
|
|
720
770
|
// ---------------------------------------------------------------------------
|
|
721
771
|
// Utility
|
|
722
772
|
// ---------------------------------------------------------------------------
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* @see https://developers.facebook.com/documentation/business-messaging/whatsapp/webhooks/reference/messages/order
|
|
13
13
|
*/
|
|
14
14
|
import type { InboundMessage } from '@kuralle-agents/messaging';
|
|
15
|
-
import type { WhatsAppInboundOrder, WhatsAppInboundAddress } from './types.js';
|
|
16
|
-
export type { ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, } from './types.js';
|
|
15
|
+
import type { WhatsAppInboundOrder, WhatsAppInboundAddress, WhatsAppInboundProductInquiry } from './types.js';
|
|
16
|
+
export type { ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, WhatsAppInboundProductInquiry, } from './types.js';
|
|
17
17
|
/** Maximum number of sections in a multi-product message. */
|
|
18
18
|
export declare const MAX_PRODUCT_LIST_SECTIONS = 10;
|
|
19
19
|
/** Maximum number of products across all sections of a multi-product message. */
|
|
@@ -56,3 +56,8 @@ export declare function parseInboundOrder(message: InboundMessage): WhatsAppInbo
|
|
|
56
56
|
* address submission (or the payload is malformed).
|
|
57
57
|
*/
|
|
58
58
|
export declare function parseInboundAddress(message: InboundMessage): WhatsAppInboundAddress | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Extract a product inquiry from an inbound message when the user replied
|
|
61
|
+
* in the context of a catalog product message (`context.referred_product`).
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseProductInquiry(message: InboundMessage): WhatsAppInboundProductInquiry | undefined;
|
|
@@ -86,3 +86,20 @@ export function parseInboundAddress(message) {
|
|
|
86
86
|
return undefined;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Extract a product inquiry from an inbound message when the user replied
|
|
91
|
+
* in the context of a catalog product message (`context.referred_product`).
|
|
92
|
+
*/
|
|
93
|
+
export function parseProductInquiry(message) {
|
|
94
|
+
const raw = message.raw;
|
|
95
|
+
const referred = raw?.context?.referred_product;
|
|
96
|
+
if (!referred?.catalog_id || !referred.product_retailer_id) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
catalog_id: referred.catalog_id,
|
|
101
|
+
product_retailer_id: referred.product_retailer_id,
|
|
102
|
+
context_message_id: raw?.context?.message_id,
|
|
103
|
+
context_from: raw?.context?.from,
|
|
104
|
+
};
|
|
105
|
+
}
|
package/dist/whatsapp/index.d.ts
CHANGED
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
* @packageDocumentation
|
|
31
31
|
*/
|
|
32
32
|
export { WhatsAppClient, createWhatsAppClient } from './client.js';
|
|
33
|
-
export type { WhatsAppClientConfig, WhatsAppThreadId, WhatsAppSendResponse, TemplateMessage, TemplateLanguage, TemplateComponent, TemplateParameter, MediaObject, ListMessage, ListSection, ListRow, ButtonMessage, ReplyButton, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, LocationPayload, ContactPayload, BusinessProfile, TemplateDefinition, TemplateDefinitionComponent, TemplateInfo, FlowDefinition, FlowInfo, FlowAssets, WhatsAppMediaResponse, TextOrTemplateOptions, } from './types.js';
|
|
33
|
+
export type { WhatsAppClientConfig, WhatsAppThreadId, WhatsAppSendResponse, TemplateMessage, TemplateLanguage, TemplateComponent, TemplateParameter, MediaObject, ListMessage, ListSection, ListRow, ButtonMessage, ReplyButton, CTAButtonMessage, FlowInteractiveInput, ProductMessage, ProductListMessage, ProductSection, CatalogMessage, AddressMessage, WhatsAppAddressValues, WhatsAppSavedAddress, WhatsAppOrderItem, WhatsAppInboundOrder, WhatsAppInboundAddress, WhatsAppInboundProductInquiry, LocationPayload, ContactPayload, BusinessProfile, TemplateDefinition, TemplateDefinitionComponent, TemplateCreateResponse, TemplateInfo, FlowDefinition, FlowCategory, FlowInfo, FlowAssets, WhatsAppMediaResponse, TextOrTemplateOptions, } from './types.js';
|
|
34
34
|
export { buildTemplateSendPayload, buildTemplatePayload, mapOutboundTemplateComponents, } from './templates.js';
|
|
35
35
|
export type { TypedTemplateConfig, RawTemplateComponents, } from './templates.js';
|
|
36
36
|
export { WhatsAppFormatConverter } from './format.js';
|
|
37
37
|
export { splitMessage } from './split.js';
|
|
38
38
|
export { generateFlowToken, buildFlowInput } from './flows.js';
|
|
39
|
-
export { parseInboundOrder, parseInboundAddress, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
|
|
39
|
+
export { parseInboundOrder, parseInboundAddress, parseProductInquiry, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
|
package/dist/whatsapp/index.js
CHANGED
|
@@ -40,4 +40,4 @@ export { splitMessage } from './split.js';
|
|
|
40
40
|
// ─── Flows ──────────────────────────────────────────────────────────────
|
|
41
41
|
export { generateFlowToken, buildFlowInput } from './flows.js';
|
|
42
42
|
// ─── Commerce ───────────────────────────────────────────────────────────
|
|
43
|
-
export { parseInboundOrder, parseInboundAddress, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
|
|
43
|
+
export { parseInboundOrder, parseInboundAddress, parseProductInquiry, MAX_PRODUCT_LIST_SECTIONS, MAX_PRODUCT_LIST_PRODUCTS, } from './commerce.js';
|
package/dist/whatsapp/types.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface WhatsAppClientConfig {
|
|
|
27
27
|
phoneNumberId: string;
|
|
28
28
|
/** Custom verify token for webhook subscription validation. */
|
|
29
29
|
verifyToken: string;
|
|
30
|
-
/** Graph API version (e.g. `"
|
|
30
|
+
/** Graph API version (e.g. `"v24.0"`). Default `"v24.0"`. */
|
|
31
31
|
apiVersion?: string;
|
|
32
32
|
/** Base URL for the Graph API. Default `"https://graph.facebook.com"`. */
|
|
33
33
|
baseUrl?: string;
|
|
@@ -127,6 +127,8 @@ export interface TemplateParameter {
|
|
|
127
127
|
payload?: string;
|
|
128
128
|
/** Action payload (when `type` is `"action"`). */
|
|
129
129
|
action?: Record<string, unknown>;
|
|
130
|
+
/** Parameter name for named-parameter templates. */
|
|
131
|
+
parameter_name?: string;
|
|
130
132
|
}
|
|
131
133
|
/**
|
|
132
134
|
* A media object used within WhatsApp messages.
|
|
@@ -149,7 +151,7 @@ export interface MediaObject {
|
|
|
149
151
|
/**
|
|
150
152
|
* A list-style interactive message with expandable sections.
|
|
151
153
|
*
|
|
152
|
-
* Supports up to 10
|
|
154
|
+
* Supports up to 10 rows total across all sections. The `button` text
|
|
153
155
|
* appears on the collapsed list control.
|
|
154
156
|
*/
|
|
155
157
|
export interface ListMessage {
|
|
@@ -272,8 +274,8 @@ export interface FlowInteractiveInput {
|
|
|
272
274
|
flowId: string;
|
|
273
275
|
/** Call-to-action button text that opens the flow. */
|
|
274
276
|
flowCta: string;
|
|
275
|
-
/** Unique token for this flow session. */
|
|
276
|
-
flowToken
|
|
277
|
+
/** Unique token for this flow session (omit when not required). */
|
|
278
|
+
flowToken?: string;
|
|
277
279
|
/** Flow action type. */
|
|
278
280
|
flowAction: 'navigate' | 'data_exchange';
|
|
279
281
|
/** Optional initial data for the flow. */
|
|
@@ -446,6 +448,13 @@ export interface WhatsAppInboundAddress {
|
|
|
446
448
|
/** Address fields entered or confirmed by the user. */
|
|
447
449
|
values: WhatsAppAddressValues;
|
|
448
450
|
}
|
|
451
|
+
/** A product inquiry from an inbound message context.referred_product. */
|
|
452
|
+
export interface WhatsAppInboundProductInquiry {
|
|
453
|
+
catalog_id: string;
|
|
454
|
+
product_retailer_id: string;
|
|
455
|
+
context_message_id?: string;
|
|
456
|
+
context_from?: string;
|
|
457
|
+
}
|
|
449
458
|
/** A location payload for sending a map pin. */
|
|
450
459
|
export interface LocationPayload {
|
|
451
460
|
/** Latitude in decimal degrees. */
|
|
@@ -510,6 +519,14 @@ export interface TemplateDefinition {
|
|
|
510
519
|
components: TemplateDefinitionComponent[];
|
|
511
520
|
/** Whether Meta can auto-recategorize the template. */
|
|
512
521
|
allow_category_change?: boolean;
|
|
522
|
+
/** Named vs positional parameter placeholders. */
|
|
523
|
+
parameter_format?: 'named' | 'positional';
|
|
524
|
+
}
|
|
525
|
+
/** Response from creating a message template. */
|
|
526
|
+
export interface TemplateCreateResponse {
|
|
527
|
+
id: string;
|
|
528
|
+
status: string;
|
|
529
|
+
category: string;
|
|
513
530
|
}
|
|
514
531
|
/** A component within a template definition. */
|
|
515
532
|
export interface TemplateDefinitionComponent {
|
|
@@ -554,11 +571,19 @@ export interface TemplateInfo {
|
|
|
554
571
|
export interface FlowDefinition {
|
|
555
572
|
/** Flow name. */
|
|
556
573
|
name: string;
|
|
557
|
-
/** Flow categories. */
|
|
558
|
-
categories
|
|
574
|
+
/** Flow categories (at least one required by the API). */
|
|
575
|
+
categories: FlowCategory[];
|
|
559
576
|
/** Clone from an existing flow. */
|
|
560
577
|
clone_flow_id?: string;
|
|
561
|
-
|
|
578
|
+
/** Initial flow JSON definition. */
|
|
579
|
+
flow_json?: string;
|
|
580
|
+
/** Publish immediately after creation. */
|
|
581
|
+
publish?: boolean;
|
|
582
|
+
/** Endpoint URI for data_exchange flows. */
|
|
583
|
+
endpoint_uri?: string;
|
|
584
|
+
}
|
|
585
|
+
/** Valid WhatsApp Flow categories. */
|
|
586
|
+
export type FlowCategory = 'SIGN_UP' | 'SIGN_IN' | 'APPOINTMENT_BOOKING' | 'LEAD_GENERATION' | 'CONTACT_US' | 'CUSTOMER_SUPPORT' | 'SURVEY' | 'OTHER';
|
|
562
587
|
/** Information about an existing WhatsApp Flow. */
|
|
563
588
|
export interface FlowInfo {
|
|
564
589
|
/** Flow ID. */
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"url": "git+https://github.com/kuralle/kuralle-agents.git",
|
|
7
7
|
"directory": "packages/kuralle-messaging-meta"
|
|
8
8
|
},
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.9.0",
|
|
10
10
|
"description": "Meta platform clients (WhatsApp, Messenger, Instagram) for Kuralle messaging",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "dist/index.js",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"README.md"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@kuralle-agents/http-client": "0.
|
|
45
|
-
"@kuralle-agents/messaging": "0.
|
|
44
|
+
"@kuralle-agents/http-client": "0.9.0",
|
|
45
|
+
"@kuralle-agents/messaging": "0.9.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"hono": "^4.12.0"
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"redis": "^4.7.0",
|
|
59
59
|
"typescript": "^5.8.2",
|
|
60
60
|
"zod": "^4.0.0",
|
|
61
|
-
"@kuralle-agents/
|
|
62
|
-
"@kuralle-agents/
|
|
63
|
-
"@kuralle-agents/
|
|
61
|
+
"@kuralle-agents/engagement": "0.9.0",
|
|
62
|
+
"@kuralle-agents/core": "0.9.0",
|
|
63
|
+
"@kuralle-agents/hono-server": "0.9.0"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"prebuild": "rm -rf dist",
|