@kuralle-agents/messaging-meta 0.8.0 → 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 +34 -5
- 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 +64 -5
- package/dist/webhook/normalizer.js +54 -4
- package/dist/whatsapp/client.d.ts +56 -9
- package/dist/whatsapp/client.js +209 -24
- package/dist/whatsapp/commerce.d.ts +63 -0
- package/dist/whatsapp/commerce.js +105 -0
- package/dist/whatsapp/index.d.ts +2 -1
- package/dist/whatsapp/index.js +2 -0
- package/dist/whatsapp/types.d.ts +198 -6
- 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.
|
|
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
|
|
|
@@ -66,6 +66,35 @@ const instagram = createInstagramClient({
|
|
|
66
66
|
});
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### WhatsApp commerce
|
|
70
|
+
|
|
71
|
+
Share catalog products and receive orders without leaving WhatsApp:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { parseInboundOrder, parseProductInquiry } from '@kuralle-agents/messaging-meta/whatsapp';
|
|
75
|
+
|
|
76
|
+
// Single product, multi-product (max 10 sections / 30 products), catalog, address request
|
|
77
|
+
await whatsapp.sendProduct(to, { catalogId, productRetailerId: 'sku-1', body: { text: 'Check this out' } });
|
|
78
|
+
await whatsapp.sendProductList(to, {
|
|
79
|
+
header: { type: 'text', text: 'Bestsellers' },
|
|
80
|
+
body: { text: 'Pick your favorites' },
|
|
81
|
+
catalogId,
|
|
82
|
+
sections: [{ title: 'Cakes', productRetailerIds: ['sku-1', 'sku-2'] }],
|
|
83
|
+
});
|
|
84
|
+
await whatsapp.sendCatalog(to, { body: { text: 'Browse our catalog' } });
|
|
85
|
+
await whatsapp.sendAddressRequest(to, { body: { text: 'Where should we deliver?' }, country: 'IN' });
|
|
86
|
+
|
|
87
|
+
// Inbound orders (webhook message type "order") arrive typed:
|
|
88
|
+
whatsapp.onMessage(async (msg) => {
|
|
89
|
+
const order = parseInboundOrder(msg);
|
|
90
|
+
if (order) {
|
|
91
|
+
// order.catalog_id, order.product_items[].{product_retailer_id, quantity, item_price, currency}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Address submissions arrive as `nfm_reply` interactive messages — use `parseInboundAddress(msg)` for a typed accessor. Address messages are country-gated by Meta (currently India only).
|
|
97
|
+
|
|
69
98
|
### Webhook verification without a client
|
|
70
99
|
|
|
71
100
|
```typescript
|
|
@@ -81,7 +110,7 @@ const events = normalizeWebhook(JSON.parse(rawBody));
|
|
|
81
110
|
| Import path | Contents |
|
|
82
111
|
|---|---|
|
|
83
112
|
| `@kuralle-agents/messaging-meta` | `GraphAPIClient`, `BaseMetaClient`, errors, `verifySignature`, `normalizeWebhook`, `MessengerClient`, `InstagramClient` |
|
|
84
|
-
| `@kuralle-agents/messaging-meta/whatsapp` | `WhatsAppClient`, `createWhatsAppClient`, templates, flows, format converter |
|
|
113
|
+
| `@kuralle-agents/messaging-meta/whatsapp` | `WhatsAppClient`, `createWhatsAppClient`, templates, flows, commerce (`parseInboundOrder`, `parseInboundAddress`, `parseProductInquiry`), format converter |
|
|
85
114
|
| `@kuralle-agents/messaging-meta/messenger` | `MessengerClient`, `createMessengerClient`, format converter |
|
|
86
115
|
| `@kuralle-agents/messaging-meta/instagram` | `InstagramClient`, `createInstagramClient`, ice breakers, format converter |
|
|
87
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 {
|
|
@@ -83,16 +96,37 @@ export interface NormalizedMessage {
|
|
|
83
96
|
nfm_reply?: {
|
|
84
97
|
name?: string;
|
|
85
98
|
response_json: string;
|
|
99
|
+
body?: string;
|
|
86
100
|
};
|
|
87
101
|
};
|
|
88
102
|
button?: {
|
|
89
103
|
text: string;
|
|
90
104
|
payload: string;
|
|
91
105
|
};
|
|
92
|
-
/**
|
|
106
|
+
/** Order placed from a catalog, single-, or multi-product message. */
|
|
107
|
+
order?: {
|
|
108
|
+
catalog_id: string;
|
|
109
|
+
text?: string;
|
|
110
|
+
product_items: Array<{
|
|
111
|
+
product_retailer_id: string;
|
|
112
|
+
quantity: number;
|
|
113
|
+
item_price: number;
|
|
114
|
+
currency: string;
|
|
115
|
+
}>;
|
|
116
|
+
};
|
|
117
|
+
/** Quoted/replied-to message context (normalized: WhatsApp's raw `context.id` maps to `message_id`). */
|
|
93
118
|
context?: {
|
|
94
119
|
message_id: string;
|
|
95
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
|
+
};
|
|
96
130
|
};
|
|
97
131
|
/** Reaction (only set for WhatsApp reaction messages before they're split out). */
|
|
98
132
|
reaction?: {
|
|
@@ -100,7 +134,23 @@ export interface NormalizedMessage {
|
|
|
100
134
|
emoji: string;
|
|
101
135
|
};
|
|
102
136
|
/** Click-to-WhatsApp ad referral data. */
|
|
103
|
-
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
|
+
};
|
|
104
154
|
}
|
|
105
155
|
/** A delivery status update. */
|
|
106
156
|
export interface NormalizedStatus {
|
|
@@ -108,12 +158,16 @@ export interface NormalizedStatus {
|
|
|
108
158
|
id: string;
|
|
109
159
|
/** Recipient identifier. */
|
|
110
160
|
recipientId: string;
|
|
111
|
-
/** Status value. */
|
|
112
|
-
status: 'sent' | 'delivered' | 'read' | 'failed';
|
|
161
|
+
/** Status value (`played` is WhatsApp voice-note playback). */
|
|
162
|
+
status: 'sent' | 'delivered' | 'read' | 'failed' | 'played' | (string & {});
|
|
113
163
|
/** Unix timestamp (seconds) as a string. */
|
|
114
164
|
timestamp: string;
|
|
115
165
|
/** Phone number ID or page ID that sent the original message. */
|
|
116
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;
|
|
117
171
|
/** Conversation metadata (WhatsApp-specific). */
|
|
118
172
|
conversation?: {
|
|
119
173
|
id: string;
|
|
@@ -126,13 +180,18 @@ export interface NormalizedStatus {
|
|
|
126
180
|
pricing?: {
|
|
127
181
|
billable: boolean;
|
|
128
182
|
pricing_model: string;
|
|
129
|
-
category: string;
|
|
183
|
+
category: string | (string & {});
|
|
184
|
+
type?: 'regular' | 'free_customer_service' | 'free_entry_point' | (string & {});
|
|
130
185
|
};
|
|
131
186
|
/** Error details when `status === "failed"`. */
|
|
132
187
|
errors?: Array<{
|
|
133
188
|
code: number;
|
|
134
189
|
title?: string;
|
|
135
190
|
message?: string;
|
|
191
|
+
error_data?: {
|
|
192
|
+
details?: string;
|
|
193
|
+
};
|
|
194
|
+
href?: string;
|
|
136
195
|
}>;
|
|
137
196
|
}
|
|
138
197
|
/** An emoji reaction on an existing message. */
|