@ariaflowagents/messaging-meta 0.8.1

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.
Files changed (106) hide show
  1. package/README.md +236 -0
  2. package/dist/graph-api/client.d.ts +125 -0
  3. package/dist/graph-api/client.d.ts.map +1 -0
  4. package/dist/graph-api/client.js +204 -0
  5. package/dist/graph-api/client.js.map +1 -0
  6. package/dist/graph-api/errors.d.ts +41 -0
  7. package/dist/graph-api/errors.d.ts.map +1 -0
  8. package/dist/graph-api/errors.js +72 -0
  9. package/dist/graph-api/errors.js.map +1 -0
  10. package/dist/graph-api/index.d.ts +15 -0
  11. package/dist/graph-api/index.d.ts.map +1 -0
  12. package/dist/graph-api/index.js +11 -0
  13. package/dist/graph-api/index.js.map +1 -0
  14. package/dist/graph-api/rate-limiter.d.ts +90 -0
  15. package/dist/graph-api/rate-limiter.d.ts.map +1 -0
  16. package/dist/graph-api/rate-limiter.js +172 -0
  17. package/dist/graph-api/rate-limiter.js.map +1 -0
  18. package/dist/graph-api/retry.d.ts +55 -0
  19. package/dist/graph-api/retry.d.ts.map +1 -0
  20. package/dist/graph-api/retry.js +103 -0
  21. package/dist/graph-api/retry.js.map +1 -0
  22. package/dist/index.d.ts +36 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +38 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/instagram/client.d.ts +365 -0
  27. package/dist/instagram/client.d.ts.map +1 -0
  28. package/dist/instagram/client.js +857 -0
  29. package/dist/instagram/client.js.map +1 -0
  30. package/dist/instagram/format.d.ts +62 -0
  31. package/dist/instagram/format.d.ts.map +1 -0
  32. package/dist/instagram/format.js +92 -0
  33. package/dist/instagram/format.js.map +1 -0
  34. package/dist/instagram/ice-breakers.d.ts +63 -0
  35. package/dist/instagram/ice-breakers.d.ts.map +1 -0
  36. package/dist/instagram/ice-breakers.js +87 -0
  37. package/dist/instagram/ice-breakers.js.map +1 -0
  38. package/dist/instagram/index.d.ts +44 -0
  39. package/dist/instagram/index.d.ts.map +1 -0
  40. package/dist/instagram/index.js +46 -0
  41. package/dist/instagram/index.js.map +1 -0
  42. package/dist/instagram/types.d.ts +188 -0
  43. package/dist/instagram/types.d.ts.map +1 -0
  44. package/dist/instagram/types.js +19 -0
  45. package/dist/instagram/types.js.map +1 -0
  46. package/dist/messenger/client.d.ts +339 -0
  47. package/dist/messenger/client.d.ts.map +1 -0
  48. package/dist/messenger/client.js +782 -0
  49. package/dist/messenger/client.js.map +1 -0
  50. package/dist/messenger/format.d.ts +69 -0
  51. package/dist/messenger/format.d.ts.map +1 -0
  52. package/dist/messenger/format.js +98 -0
  53. package/dist/messenger/format.js.map +1 -0
  54. package/dist/messenger/index.d.ts +34 -0
  55. package/dist/messenger/index.d.ts.map +1 -0
  56. package/dist/messenger/index.js +35 -0
  57. package/dist/messenger/index.js.map +1 -0
  58. package/dist/messenger/types.d.ts +181 -0
  59. package/dist/messenger/types.d.ts.map +1 -0
  60. package/dist/messenger/types.js +10 -0
  61. package/dist/messenger/types.js.map +1 -0
  62. package/dist/server.d.ts +31 -0
  63. package/dist/server.d.ts.map +1 -0
  64. package/dist/server.js +29 -0
  65. package/dist/server.js.map +1 -0
  66. package/dist/webhook/index.d.ts +10 -0
  67. package/dist/webhook/index.d.ts.map +1 -0
  68. package/dist/webhook/index.js +8 -0
  69. package/dist/webhook/index.js.map +1 -0
  70. package/dist/webhook/normalizer.d.ts +169 -0
  71. package/dist/webhook/normalizer.d.ts.map +1 -0
  72. package/dist/webhook/normalizer.js +301 -0
  73. package/dist/webhook/normalizer.js.map +1 -0
  74. package/dist/webhook/verifier.d.ts +45 -0
  75. package/dist/webhook/verifier.d.ts.map +1 -0
  76. package/dist/webhook/verifier.js +62 -0
  77. package/dist/webhook/verifier.js.map +1 -0
  78. package/dist/whatsapp/client.d.ts +481 -0
  79. package/dist/whatsapp/client.d.ts.map +1 -0
  80. package/dist/whatsapp/client.js +1043 -0
  81. package/dist/whatsapp/client.js.map +1 -0
  82. package/dist/whatsapp/flows.d.ts +74 -0
  83. package/dist/whatsapp/flows.d.ts.map +1 -0
  84. package/dist/whatsapp/flows.js +77 -0
  85. package/dist/whatsapp/flows.js.map +1 -0
  86. package/dist/whatsapp/format.d.ts +78 -0
  87. package/dist/whatsapp/format.d.ts.map +1 -0
  88. package/dist/whatsapp/format.js +195 -0
  89. package/dist/whatsapp/format.js.map +1 -0
  90. package/dist/whatsapp/index.d.ts +39 -0
  91. package/dist/whatsapp/index.d.ts.map +1 -0
  92. package/dist/whatsapp/index.js +42 -0
  93. package/dist/whatsapp/index.js.map +1 -0
  94. package/dist/whatsapp/split.d.ts +35 -0
  95. package/dist/whatsapp/split.d.ts.map +1 -0
  96. package/dist/whatsapp/split.js +76 -0
  97. package/dist/whatsapp/split.js.map +1 -0
  98. package/dist/whatsapp/templates.d.ts +129 -0
  99. package/dist/whatsapp/templates.d.ts.map +1 -0
  100. package/dist/whatsapp/templates.js +125 -0
  101. package/dist/whatsapp/templates.js.map +1 -0
  102. package/dist/whatsapp/types.d.ts +440 -0
  103. package/dist/whatsapp/types.d.ts.map +1 -0
  104. package/dist/whatsapp/types.js +11 -0
  105. package/dist/whatsapp/types.js.map +1 -0
  106. package/package.json +31 -0
@@ -0,0 +1,782 @@
1
+ /**
2
+ * @module messenger/client
3
+ *
4
+ * Facebook Messenger Platform client implementing the {@link PlatformClient}
5
+ * interface from `@ariaflowagents/messaging`.
6
+ *
7
+ * Provides a complete, production-ready integration with Meta's Messenger
8
+ * Platform including:
9
+ *
10
+ * - Sending text, media, interactive (button/generic templates), and quick replies
11
+ * - Webhook handling with signature verification
12
+ * - Sender actions (typing indicators, mark-seen)
13
+ * - User profile retrieval
14
+ * - Persona management (create, delete)
15
+ * - Media upload and download
16
+ * - Messenger-specific format conversion (plain text)
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import { createMessengerClient } from '@ariaflowagents/messaging-meta/messenger';
21
+ *
22
+ * const client = createMessengerClient({
23
+ * pageAccessToken: process.env.MESSENGER_PAGE_ACCESS_TOKEN!,
24
+ * appSecret: process.env.META_APP_SECRET!,
25
+ * pageId: process.env.MESSENGER_PAGE_ID!,
26
+ * verifyToken: process.env.MESSENGER_VERIFY_TOKEN!,
27
+ * });
28
+ *
29
+ * client.onMessage(async (msg) => {
30
+ * await client.sendText(msg.from.id, `Echo: ${msg.text}`);
31
+ * });
32
+ * ```
33
+ */
34
+ import { MessagingError, } from '@ariaflowagents/messaging';
35
+ import { GraphAPIClient } from '../graph-api/client.js';
36
+ import { verifySignature } from '../webhook/verifier.js';
37
+ import { normalizeWebhook } from '../webhook/normalizer.js';
38
+ import { MessengerFormatConverter } from './format.js';
39
+ // ---------------------------------------------------------------------------
40
+ // Constants
41
+ // ---------------------------------------------------------------------------
42
+ /** Maximum text message length for the Messenger Platform. */
43
+ const MESSENGER_TEXT_LIMIT = 2000;
44
+ // ---------------------------------------------------------------------------
45
+ // Factory
46
+ // ---------------------------------------------------------------------------
47
+ /**
48
+ * Create a new {@link MessengerClient} instance.
49
+ *
50
+ * This is the recommended entry point. It constructs the client with all
51
+ * internal dependencies wired up (Graph API client, format converter).
52
+ *
53
+ * @param config - Messenger client configuration.
54
+ * @returns A fully configured {@link MessengerClient}.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const messenger = createMessengerClient({
59
+ * pageAccessToken: process.env.MESSENGER_PAGE_ACCESS_TOKEN!,
60
+ * appSecret: process.env.META_APP_SECRET!,
61
+ * pageId: process.env.MESSENGER_PAGE_ID!,
62
+ * verifyToken: process.env.MESSENGER_VERIFY_TOKEN!,
63
+ * });
64
+ * ```
65
+ */
66
+ export function createMessengerClient(config) {
67
+ return new MessengerClient(config);
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // MessengerClient
71
+ // ---------------------------------------------------------------------------
72
+ /**
73
+ * Full-featured Facebook Messenger Platform client.
74
+ *
75
+ * Implements the `PlatformClient` interface from `@ariaflowagents/messaging`
76
+ * so it can be plugged into the messaging adapter layer, while also exposing
77
+ * Messenger-specific capabilities (button templates, generic templates,
78
+ * quick replies, personas, user profiles).
79
+ *
80
+ * Use {@link createMessengerClient} to instantiate.
81
+ */
82
+ export class MessengerClient {
83
+ /** @inheritdoc */
84
+ platform = 'messenger';
85
+ graphApi;
86
+ config;
87
+ messageHandlers = [];
88
+ statusHandlers = [];
89
+ reactionHandlers = [];
90
+ formatConverterInstance;
91
+ mediaCache;
92
+ constructor(config) {
93
+ this.config = config;
94
+ this.graphApi = new GraphAPIClient({
95
+ accessToken: config.pageAccessToken,
96
+ appSecret: config.appSecret,
97
+ apiVersion: config.apiVersion,
98
+ baseUrl: config.baseUrl,
99
+ retry: config.retry,
100
+ rateLimiter: config.rateLimiter,
101
+ logger: config.logger,
102
+ });
103
+ this.formatConverterInstance = new MessengerFormatConverter();
104
+ this.mediaCache = config.mediaCache;
105
+ }
106
+ /** Messenger-specific format converter. */
107
+ get formatConverter() {
108
+ return this.formatConverterInstance;
109
+ }
110
+ // =========================================================================
111
+ // Lifecycle — handler registration
112
+ // =========================================================================
113
+ /**
114
+ * Register a handler for inbound messages.
115
+ *
116
+ * Multiple handlers can be registered; they are called in order for
117
+ * each incoming message.
118
+ *
119
+ * @param handler - Callback invoked with the normalised message and raw payload.
120
+ */
121
+ onMessage(handler) {
122
+ this.messageHandlers.push(handler);
123
+ }
124
+ /**
125
+ * Register a handler for delivery/read status updates.
126
+ *
127
+ * @param handler - Callback invoked with the normalised status update.
128
+ */
129
+ onStatus(handler) {
130
+ this.statusHandlers.push(handler);
131
+ }
132
+ /**
133
+ * Register a handler for reaction events.
134
+ *
135
+ * @param handler - Callback invoked with the normalised reaction data.
136
+ */
137
+ onReaction(handler) {
138
+ this.reactionHandlers.push(handler);
139
+ }
140
+ // =========================================================================
141
+ // Webhook handling
142
+ // =========================================================================
143
+ /**
144
+ * Handle an incoming webhook request from Meta.
145
+ *
146
+ * - **GET** requests are treated as subscription verification challenges.
147
+ * - **POST** requests are validated via HMAC-SHA256 signature, normalised,
148
+ * and dispatched to registered handlers.
149
+ *
150
+ * @param request - The incoming HTTP request.
151
+ * @returns An HTTP response (200 OK, 401 Unauthorized, or 403 Forbidden).
152
+ */
153
+ async handleWebhook(request) {
154
+ if (request.method === 'GET') {
155
+ return this.handleVerification(request);
156
+ }
157
+ const rawBody = await request.text();
158
+ const signature = request.headers.get('x-hub-signature-256');
159
+ if (!signature ||
160
+ !verifySignature({
161
+ appSecret: this.config.appSecret,
162
+ rawBody,
163
+ signatureHeader: signature,
164
+ })) {
165
+ return new Response('Unauthorized', { status: 401 });
166
+ }
167
+ const payload = JSON.parse(rawBody);
168
+ const events = normalizeWebhook(payload);
169
+ // Dispatch messages
170
+ for (const msg of events.messages) {
171
+ const inbound = this.toInboundMessage(msg);
172
+ for (const handler of this.messageHandlers) {
173
+ await handler(inbound, msg);
174
+ }
175
+ }
176
+ // Dispatch statuses
177
+ for (const status of events.statuses) {
178
+ const update = this.toStatusUpdate(status);
179
+ for (const handler of this.statusHandlers) {
180
+ await handler(update);
181
+ }
182
+ }
183
+ // Dispatch reactions
184
+ for (const reaction of events.reactions) {
185
+ const data = this.toReactionData(reaction);
186
+ for (const handler of this.reactionHandlers) {
187
+ await handler(data);
188
+ }
189
+ }
190
+ return new Response('OK', { status: 200 });
191
+ }
192
+ /**
193
+ * Handle webhook verification (GET) from Meta.
194
+ *
195
+ * @param request - The GET request with `hub.mode`, `hub.verify_token`, and `hub.challenge`.
196
+ * @returns 200 with the challenge string, or 403 Forbidden.
197
+ */
198
+ handleVerification(request) {
199
+ const url = new URL(request.url);
200
+ const mode = url.searchParams.get('hub.mode');
201
+ const token = url.searchParams.get('hub.verify_token');
202
+ const challenge = url.searchParams.get('hub.challenge');
203
+ if (mode === 'subscribe' && token === this.config.verifyToken) {
204
+ return new Response(challenge ?? '', { status: 200 });
205
+ }
206
+ return new Response('Forbidden', { status: 403 });
207
+ }
208
+ // =========================================================================
209
+ // Outbound — core PlatformClient methods
210
+ // =========================================================================
211
+ /**
212
+ * Send a text message.
213
+ *
214
+ * Messages exceeding the 2000-character Messenger limit are truncated.
215
+ *
216
+ * @param to - Recipient PSID.
217
+ * @param text - The text content to send.
218
+ * @returns The send result.
219
+ */
220
+ async sendText(to, text) {
221
+ const truncated = text.length > MESSENGER_TEXT_LIMIT
222
+ ? text.slice(0, MESSENGER_TEXT_LIMIT)
223
+ : text;
224
+ const response = await this.graphApi.post(`${this.config.pageId}/messages`, {
225
+ recipient: { id: to },
226
+ messaging_type: 'RESPONSE',
227
+ message: { text: truncated },
228
+ });
229
+ return this.toSendResult(to, response);
230
+ }
231
+ /**
232
+ * Send a media message (image, video, audio, or file).
233
+ *
234
+ * Accepts URLs (string), Buffers (uploaded automatically), or ReadableStreams.
235
+ *
236
+ * @param to - Recipient PSID.
237
+ * @param media - The media payload to send.
238
+ * @returns The send result.
239
+ */
240
+ async sendMedia(to, media) {
241
+ const attachmentType = this.resolveAttachmentType(media.mimeType);
242
+ if (typeof media.data === 'string') {
243
+ // Send via URL
244
+ const response = await this.graphApi.post(`${this.config.pageId}/messages`, {
245
+ recipient: { id: to },
246
+ messaging_type: 'RESPONSE',
247
+ message: {
248
+ attachment: {
249
+ type: attachmentType,
250
+ payload: { url: media.data, is_reusable: true },
251
+ },
252
+ },
253
+ });
254
+ return this.toSendResult(to, response);
255
+ }
256
+ // Buffer or ReadableStream — upload first
257
+ const buffer = Buffer.isBuffer(media.data)
258
+ ? media.data
259
+ : await streamToBuffer(media.data);
260
+ const handle = await this.uploadMedia(buffer, {
261
+ mimeType: media.mimeType,
262
+ filename: media.filename,
263
+ });
264
+ const response = await this.graphApi.post(`${this.config.pageId}/messages`, {
265
+ recipient: { id: to },
266
+ messaging_type: 'RESPONSE',
267
+ message: {
268
+ attachment: {
269
+ type: attachmentType,
270
+ payload: { attachment_id: handle.mediaId },
271
+ },
272
+ },
273
+ });
274
+ return this.toSendResult(to, response);
275
+ }
276
+ /**
277
+ * Send an interactive message (buttons or list).
278
+ *
279
+ * Converts the platform-agnostic {@link InteractiveMessage} into
280
+ * Messenger's template format:
281
+ * - `action.type === 'buttons'` maps to a button template
282
+ * - `action.type === 'list'` maps to a generic template (carousel)
283
+ *
284
+ * @param to - Recipient PSID.
285
+ * @param msg - The interactive message definition.
286
+ * @returns The send result.
287
+ * @throws {@link MessagingError} if the interactive type is unsupported.
288
+ */
289
+ async sendInteractive(to, msg) {
290
+ if (msg.action.type === 'buttons') {
291
+ return this.sendButtonTemplate(to, {
292
+ text: msg.body,
293
+ buttons: msg.action.buttons.map((b) => ({
294
+ type: 'postback',
295
+ title: b.title,
296
+ payload: b.id,
297
+ })),
298
+ });
299
+ }
300
+ if (msg.action.type === 'list') {
301
+ // Map list sections to generic template elements
302
+ const elements = msg.action.sections.flatMap((section) => section.rows.map((row) => ({
303
+ title: row.title,
304
+ subtitle: row.description,
305
+ buttons: [
306
+ {
307
+ type: 'postback',
308
+ title: row.title.slice(0, 20),
309
+ payload: row.id,
310
+ },
311
+ ],
312
+ })));
313
+ return this.sendGenericTemplate(to, { elements: elements.slice(0, 10) });
314
+ }
315
+ throw new MessagingError(`Unsupported interactive type: ${msg.action.type}`, 'UNSUPPORTED_TYPE', 'messenger');
316
+ }
317
+ /**
318
+ * Send a raw platform-specific payload.
319
+ *
320
+ * This is an escape hatch for features not covered by the normalised
321
+ * interface. The payload is merged with the standard messaging envelope.
322
+ *
323
+ * @param to - Recipient PSID.
324
+ * @param payload - Raw Messenger API payload fields.
325
+ * @returns The send result.
326
+ */
327
+ async sendRaw(to, payload) {
328
+ const response = await this.graphApi.post(`${this.config.pageId}/messages`, {
329
+ recipient: { id: to },
330
+ messaging_type: 'RESPONSE',
331
+ ...payload,
332
+ });
333
+ return this.toSendResult(to, response);
334
+ }
335
+ /**
336
+ * Mark a conversation as seen.
337
+ *
338
+ * Sends a `mark_seen` sender action to the recipient. Note that unlike
339
+ * WhatsApp, Messenger's mark-seen operates on the recipient (PSID) rather
340
+ * than a specific message ID.
341
+ *
342
+ * @param messageId - The message ID (used to identify the recipient via
343
+ * stored mapping; falls back to treating it as a PSID).
344
+ */
345
+ async markAsRead(messageId) {
346
+ // Messenger's mark_seen is a sender action that requires the recipient ID,
347
+ // not a message ID. We accept the recipient PSID here since the platform
348
+ // does not support marking individual messages.
349
+ await this.sendSenderAction(messageId, 'mark_seen');
350
+ }
351
+ /**
352
+ * Send a typing indicator to a recipient.
353
+ *
354
+ * Sends the `typing_on` sender action. The indicator is automatically
355
+ * dismissed after 20 seconds or when a message is sent.
356
+ *
357
+ * @param to - Recipient PSID.
358
+ */
359
+ async sendTypingIndicator(to) {
360
+ await this.sendSenderAction(to, 'typing_on');
361
+ }
362
+ // =========================================================================
363
+ // Messenger-specific — Templates
364
+ // =========================================================================
365
+ /**
366
+ * Send a button template message.
367
+ *
368
+ * Displays a text message with up to 3 buttons below it. Buttons can be
369
+ * postback (returns a payload to the webhook) or web URL (opens a browser).
370
+ *
371
+ * @param to - Recipient PSID.
372
+ * @param template - The button template payload.
373
+ * @returns The send result.
374
+ */
375
+ async sendButtonTemplate(to, template) {
376
+ const response = await this.graphApi.post(`${this.config.pageId}/messages`, {
377
+ recipient: { id: to },
378
+ messaging_type: 'RESPONSE',
379
+ message: {
380
+ attachment: {
381
+ type: 'template',
382
+ payload: {
383
+ template_type: 'button',
384
+ text: template.text,
385
+ buttons: template.buttons.slice(0, 3),
386
+ },
387
+ },
388
+ },
389
+ });
390
+ return this.toSendResult(to, response);
391
+ }
392
+ /**
393
+ * Send a generic template (carousel) message.
394
+ *
395
+ * Displays a horizontally scrollable set of cards, each with an image,
396
+ * title, subtitle, and optional buttons.
397
+ *
398
+ * @param to - Recipient PSID.
399
+ * @param template - The generic template payload.
400
+ * @returns The send result.
401
+ */
402
+ async sendGenericTemplate(to, template) {
403
+ const response = await this.graphApi.post(`${this.config.pageId}/messages`, {
404
+ recipient: { id: to },
405
+ messaging_type: 'RESPONSE',
406
+ message: {
407
+ attachment: {
408
+ type: 'template',
409
+ payload: {
410
+ template_type: 'generic',
411
+ elements: template.elements.slice(0, 10),
412
+ },
413
+ },
414
+ },
415
+ });
416
+ return this.toSendResult(to, response);
417
+ }
418
+ /**
419
+ * Send quick reply options with a text message.
420
+ *
421
+ * Quick replies appear as pill-shaped buttons above the composer and
422
+ * disappear once the user taps one or types a message.
423
+ *
424
+ * @param to - Recipient PSID.
425
+ * @param text - The text message to display above the quick replies.
426
+ * @param replies - Array of quick reply options (max 13).
427
+ * @returns The send result.
428
+ */
429
+ async sendQuickReplies(to, text, replies) {
430
+ const response = await this.graphApi.post(`${this.config.pageId}/messages`, {
431
+ recipient: { id: to },
432
+ messaging_type: 'RESPONSE',
433
+ message: {
434
+ text,
435
+ quick_replies: replies.slice(0, 13),
436
+ },
437
+ });
438
+ return this.toSendResult(to, response);
439
+ }
440
+ // =========================================================================
441
+ // Messenger-specific — Sender Actions
442
+ // =========================================================================
443
+ /**
444
+ * Send a sender action (typing indicator or mark-seen).
445
+ *
446
+ * Sender actions cannot be combined with a `message` field — they must
447
+ * be sent as separate requests.
448
+ *
449
+ * @param to - Recipient PSID.
450
+ * @param action - The sender action (`"typing_on"`, `"typing_off"`, or `"mark_seen"`).
451
+ */
452
+ async sendSenderAction(to, action) {
453
+ await this.graphApi.post(`${this.config.pageId}/messages`, {
454
+ recipient: { id: to },
455
+ sender_action: action,
456
+ });
457
+ }
458
+ // =========================================================================
459
+ // Messenger-specific — User Profile
460
+ // =========================================================================
461
+ /**
462
+ * Retrieve a user's public profile information.
463
+ *
464
+ * Returns the first name, last name, and profile picture URL for the
465
+ * given PSID.
466
+ *
467
+ * @param psid - The user's page-scoped ID.
468
+ * @returns The user's profile information.
469
+ */
470
+ async getUserProfile(psid) {
471
+ return this.graphApi.get(psid, {
472
+ fields: 'first_name,last_name,profile_pic',
473
+ });
474
+ }
475
+ // =========================================================================
476
+ // Messenger-specific — Persona API
477
+ // =========================================================================
478
+ /**
479
+ * Persona management operations.
480
+ *
481
+ * Personas allow the bot to respond as a named human agent with a
482
+ * custom profile picture. Useful for the HUMAN_AGENT message tag flow.
483
+ */
484
+ personas = {
485
+ /**
486
+ * Create a new persona for the page.
487
+ *
488
+ * @param config - The persona name and profile picture URL.
489
+ * @returns The created persona with its ID.
490
+ */
491
+ create: async (config) => {
492
+ return this.graphApi.post(`${this.config.pageId}/personas`, config);
493
+ },
494
+ /**
495
+ * Delete a persona.
496
+ *
497
+ * @param personaId - The persona ID to delete.
498
+ */
499
+ delete: async (personaId) => {
500
+ await this.graphApi.post(personaId, { _method: 'DELETE' });
501
+ },
502
+ };
503
+ // =========================================================================
504
+ // Media
505
+ // =========================================================================
506
+ /**
507
+ * Upload media to Messenger for later use in messages.
508
+ *
509
+ * Uses the Attachment Upload API to upload files and receive an
510
+ * `attachment_id` that can be reused across messages.
511
+ *
512
+ * @param file - The file content as a Buffer or ReadableStream.
513
+ * @param options - Upload options including MIME type and optional filename.
514
+ * @returns A handle containing the platform-assigned media ID.
515
+ */
516
+ async uploadMedia(file, options) {
517
+ const buffer = Buffer.isBuffer(file) ? file : await streamToBuffer(file);
518
+ const blob = new Blob([buffer], { type: options.mimeType });
519
+ const attachmentType = this.resolveAttachmentType(options.mimeType);
520
+ const formData = new FormData();
521
+ formData.append('message', JSON.stringify({
522
+ attachment: {
523
+ type: attachmentType,
524
+ payload: { is_reusable: true },
525
+ },
526
+ }));
527
+ formData.append('filedata', blob, options.filename ?? 'file');
528
+ const response = await this.graphApi.postFormData(`${this.config.pageId}/message_attachments`, formData);
529
+ return { mediaId: response.attachment_id };
530
+ }
531
+ /**
532
+ * Download media from a URL.
533
+ *
534
+ * Messenger media URLs are typically publicly accessible (unlike WhatsApp
535
+ * which requires a two-step media-ID-to-URL lookup). This method fetches
536
+ * the binary content from the provided URL.
537
+ *
538
+ * @param mediaUrl - The media URL to download from.
539
+ * @returns The downloaded media content and MIME type.
540
+ */
541
+ async downloadMedia(mediaUrl) {
542
+ if (this.mediaCache) {
543
+ return this.mediaCache.getOrDownload(mediaUrl, async () => {
544
+ const data = await this.graphApi.fetchBinary(mediaUrl);
545
+ const mimeType = inferMimeType(mediaUrl);
546
+ return { data, mimeType };
547
+ });
548
+ }
549
+ const data = await this.graphApi.fetchBinary(mediaUrl);
550
+ // Attempt to infer MIME type from URL extension
551
+ const mimeType = inferMimeType(mediaUrl);
552
+ return { data, mimeType };
553
+ }
554
+ // =========================================================================
555
+ // Webhook router
556
+ // =========================================================================
557
+ /**
558
+ * Returns a Hono sub-application that handles webhook routes.
559
+ *
560
+ * Mounts GET (verification) and POST (event delivery) handlers at the
561
+ * root path. Use this to integrate the webhook into an existing Hono app.
562
+ *
563
+ * Requires `hono` to be installed as a peer dependency. The import is
564
+ * performed dynamically to avoid a hard compile-time dependency.
565
+ *
566
+ * @returns A Hono application with webhook routes.
567
+ *
568
+ * @example
569
+ * ```ts
570
+ * import { Hono } from 'hono';
571
+ *
572
+ * const app = new Hono();
573
+ * app.route('/webhooks/messenger', client.webhookRouter());
574
+ * ```
575
+ */
576
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
577
+ webhookRouter() {
578
+ // Dynamic import to avoid hard dependency on Hono at the module level.
579
+ // The caller must have Hono installed (it's a peer dependency via @ariaflowagents/messaging).
580
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
581
+ const { Hono } = require('hono');
582
+ const router = new Hono();
583
+ const self = this;
584
+ router.all('/*', async (c) => {
585
+ return self.handleWebhook(c.req.raw);
586
+ });
587
+ return router;
588
+ }
589
+ // =========================================================================
590
+ // Private — conversion helpers
591
+ // =========================================================================
592
+ /**
593
+ * Convert a normalised webhook message to a platform-agnostic {@link InboundMessage}.
594
+ */
595
+ toInboundMessage(msg) {
596
+ const threadId = `messenger:${this.config.pageId}:${msg.from}`;
597
+ return {
598
+ id: msg.id,
599
+ platform: 'messenger',
600
+ threadId,
601
+ from: {
602
+ id: msg.from,
603
+ name: msg.contactName,
604
+ },
605
+ timestamp: new Date(parseInt(msg.timestamp, 10) * 1000),
606
+ type: this.mapMessageType(msg.type),
607
+ text: msg.text?.body ?? this.extractTextFallback(msg),
608
+ media: this.extractMedia(msg),
609
+ location: msg.location,
610
+ interactive: msg.interactive
611
+ ? {
612
+ type: msg.interactive.type,
613
+ id: msg.interactive.button_reply?.id ??
614
+ msg.interactive.list_reply?.id ??
615
+ '',
616
+ title: msg.interactive.button_reply?.title ??
617
+ msg.interactive.list_reply?.title,
618
+ description: msg.interactive.list_reply?.description,
619
+ }
620
+ : undefined,
621
+ context: msg.context
622
+ ? { messageId: msg.context.message_id, from: msg.context.from }
623
+ : undefined,
624
+ raw: msg,
625
+ };
626
+ }
627
+ /**
628
+ * Convert a normalised webhook status to a platform-agnostic {@link StatusUpdate}.
629
+ */
630
+ toStatusUpdate(status) {
631
+ return {
632
+ messageId: status.id,
633
+ status: status.status,
634
+ timestamp: new Date(parseInt(status.timestamp, 10) * 1000),
635
+ recipientId: status.recipientId,
636
+ threadId: `messenger:${status.phoneNumberId}:${status.recipientId}`,
637
+ raw: status,
638
+ };
639
+ }
640
+ /**
641
+ * Convert a normalised webhook reaction to a platform-agnostic {@link ReactionData}.
642
+ */
643
+ toReactionData(reaction) {
644
+ return {
645
+ messageId: reaction.messageId,
646
+ emoji: reaction.emoji,
647
+ action: reaction.emoji ? 'react' : 'unreact',
648
+ userId: reaction.from,
649
+ };
650
+ }
651
+ /**
652
+ * Convert a {@link MessengerSendResponse} to a platform-agnostic {@link SendResult}.
653
+ */
654
+ toSendResult(to, response) {
655
+ return {
656
+ messageId: response.message_id ?? '',
657
+ threadId: `messenger:${this.config.pageId}:${to}`,
658
+ timestamp: new Date(),
659
+ raw: response,
660
+ };
661
+ }
662
+ /**
663
+ * Map a Messenger message type string to the normalised {@link InboundMessage} type union.
664
+ */
665
+ mapMessageType(type) {
666
+ const typeMap = {
667
+ text: 'text',
668
+ image: 'image',
669
+ video: 'video',
670
+ audio: 'audio',
671
+ file: 'document',
672
+ document: 'document',
673
+ sticker: 'sticker',
674
+ location: 'location',
675
+ postback: 'interactive',
676
+ interactive: 'interactive',
677
+ attachment: 'unknown',
678
+ };
679
+ return typeMap[type] ?? 'unknown';
680
+ }
681
+ /**
682
+ * Extract a text fallback from non-text message types.
683
+ */
684
+ extractTextFallback(msg) {
685
+ if (msg.image?.caption)
686
+ return msg.image.caption;
687
+ if (msg.video?.caption)
688
+ return msg.video.caption;
689
+ if (msg.document?.caption)
690
+ return msg.document.caption;
691
+ if (msg.button)
692
+ return msg.button.text;
693
+ if (msg.interactive?.button_reply)
694
+ return msg.interactive.button_reply.title;
695
+ if (msg.interactive?.list_reply)
696
+ return msg.interactive.list_reply.title;
697
+ if (msg.location) {
698
+ return msg.location.name ?? `${msg.location.latitude},${msg.location.longitude}`;
699
+ }
700
+ return undefined;
701
+ }
702
+ /**
703
+ * Extract a {@link MediaReference} from a normalised message if it contains media.
704
+ */
705
+ extractMedia(msg) {
706
+ const mediaTypes = ['image', 'video', 'audio', 'document', 'sticker'];
707
+ for (const type of mediaTypes) {
708
+ const media = msg[type];
709
+ if (media && ('id' in media || 'url' in media)) {
710
+ return {
711
+ id: 'id' in media ? media.id : '',
712
+ mimeType: 'mime_type' in media ? media.mime_type : undefined,
713
+ url: 'url' in media ? media.url : undefined,
714
+ caption: 'caption' in media ? media.caption : undefined,
715
+ filename: 'filename' in media ? media.filename : undefined,
716
+ };
717
+ }
718
+ }
719
+ return undefined;
720
+ }
721
+ /**
722
+ * Resolve a MIME type to a Messenger attachment type string.
723
+ */
724
+ resolveAttachmentType(mimeType) {
725
+ if (mimeType.startsWith('image/'))
726
+ return 'image';
727
+ if (mimeType.startsWith('video/'))
728
+ return 'video';
729
+ if (mimeType.startsWith('audio/'))
730
+ return 'audio';
731
+ return 'file';
732
+ }
733
+ }
734
+ // ---------------------------------------------------------------------------
735
+ // Utility
736
+ // ---------------------------------------------------------------------------
737
+ /**
738
+ * Convert a `ReadableStream` to a `Buffer`.
739
+ *
740
+ * @param stream - The readable stream to consume.
741
+ * @returns A Buffer containing all chunks from the stream.
742
+ */
743
+ async function streamToBuffer(stream) {
744
+ const chunks = [];
745
+ const reader = stream.getReader();
746
+ for (;;) {
747
+ const { done, value } = await reader.read();
748
+ if (done)
749
+ break;
750
+ chunks.push(value);
751
+ }
752
+ return Buffer.concat(chunks);
753
+ }
754
+ /**
755
+ * Infer a MIME type from a URL based on file extension.
756
+ *
757
+ * @param url - The URL to infer from.
758
+ * @returns A best-guess MIME type, defaulting to `"application/octet-stream"`.
759
+ */
760
+ function inferMimeType(url) {
761
+ const extMatch = url.match(/\.(\w+)(?:\?|$)/);
762
+ if (!extMatch)
763
+ return 'application/octet-stream';
764
+ const ext = extMatch[1].toLowerCase();
765
+ const mimeMap = {
766
+ jpg: 'image/jpeg',
767
+ jpeg: 'image/jpeg',
768
+ png: 'image/png',
769
+ gif: 'image/gif',
770
+ webp: 'image/webp',
771
+ mp4: 'video/mp4',
772
+ mov: 'video/quicktime',
773
+ mp3: 'audio/mpeg',
774
+ ogg: 'audio/ogg',
775
+ wav: 'audio/wav',
776
+ pdf: 'application/pdf',
777
+ doc: 'application/msword',
778
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
779
+ };
780
+ return mimeMap[ext] ?? 'application/octet-stream';
781
+ }
782
+ //# sourceMappingURL=client.js.map