UNPKG

@warriorteam/messenger-sdk

Version:

TypeScript SDK for Facebook Messenger Platform API with Conversations support

886 lines (714 loc) 25.3 kB
# @warriorteam/messenger-sdk A modern TypeScript SDK for the Facebook Messenger Platform API, designed with simplicity and type safety in mind. ## Features - 🚀 **Zero runtime dependencies** - Built with native fetch - 📝 **Full TypeScript support** - Complete type definitions with discriminated unions - 🔄 **Dual module support** - ESM and CommonJS - ✅ **Comprehensive validation** - Built-in message and template validation - 🛡️ **Error handling** - Detailed error types and messages - 📚 **Complete API coverage** - Send API, Templates, Attachments, Moderation, Profile, Conversations - 💬 **Conversations API** - Retrieve conversation history and messages for Messenger & Instagram - 🔐 **Webhook utilities** - Complete webhook type system and signature verification - 🎯 **Token override** - Per-method access token override support - 🔧 **Type-safe webhooks** - Discriminated unions for proper TypeScript narrowing ## Installation ```bash npm install @warriorteam/messenger-sdk ``` ## Requirements - Node.js 18+ (for native fetch support) - A Facebook Page Access Token ## Quick Start ```typescript import { Messenger } from '@warriorteam/messenger-sdk'; const messenger = new Messenger({ accessToken: 'YOUR_PAGE_ACCESS_TOKEN', version: 'v23.0' }); // Send a text message const result = await messenger.send.message({ recipient: { id: 'USER_PSID' }, message: { text: 'Hello from RedAI Messenger SDK!' } }); console.log('Message sent:', result.message_id); ``` ## API Reference ### Client Configuration ```typescript const messenger = new Messenger({ accessToken?: string; // Optional: Default page access token version?: string; // Optional: API version (default: 'v23.0') baseUrl?: string; // Optional: Custom base URL timeout?: number; // Optional: Request timeout in ms (default: 30000) maxRetries?: number; // Optional: Max retry attempts (default: 3) }); ``` ### Token Override Support Every API method supports per-call access token override, perfect for multi-tenant applications: ```typescript const messenger = new Messenger({ accessToken: 'default_page_token' }); // Use default token await messenger.send.message({ recipient: { id: 'USER_PSID' }, message: { text: 'Hello from default page!' } }); // Override token for this specific call await messenger.send.message({ recipient: { id: 'USER_PSID' }, message: { text: 'Hello from different page!' } }, { accessToken: 'other_page_token' }); // Works with all API methods const profile = await messenger.profile.getBasic('USER_PSID', { accessToken: 'specific_token' }); ``` ### Send API #### Send Text Message ```typescript await messenger.send.message({ recipient: { id: 'USER_PSID' }, message: { text: 'Hello World!' } }); ``` #### Send Message with Quick Replies ```typescript await messenger.send.message({ recipient: { id: 'USER_PSID' }, message: { text: 'Pick a color:', quick_replies: [ { content_type: 'text', title: 'Red', payload: 'PICKED_RED' }, { content_type: 'text', title: 'Blue', payload: 'PICKED_BLUE' } ] } }); ``` #### Sender Actions Manage conversation state and reactions: ```typescript // Typing indicators await messenger.send.typingOn('USER_PSID'); await messenger.send.typingOff('USER_PSID'); // Helper to toggle typing based on boolean await messenger.send.setTyping('USER_PSID', true); // Mark as read/seen await messenger.send.markRead('USER_PSID'); // React to a message await messenger.send.addReaction('USER_PSID', { messageId: 'MESSAGE_ID', emoji: '❤️' }); // Remove reaction await messenger.send.removeReaction('USER_PSID', 'MESSAGE_ID'); ``` #### Response Format The Send API response includes a `message_id` and optionally a `recipient_id`: ```typescript const result = await messenger.send.message({...}); console.log('Message ID:', result.message_id); // Always present console.log('Recipient ID:', result.recipient_id || 'Not provided'); // Optional ``` **Note**: `recipient_id` is only included when using `recipient.id` (PSID). It's not included when using `recipient.user_ref` or `recipient.phone_number`. ### Attachments API #### Upload Attachment from URL ```typescript const attachment = await messenger.attachments.upload({ type: 'image', url: 'https://example.com/image.jpg', is_reusable: true }); // Use the attachment ID to send await messenger.send.message({ recipient: { id: 'USER_PSID' }, message: { attachment: { type: 'image', payload: { attachment_id: attachment.attachment_id } } } }); ``` #### Send Attachment Directly ```typescript await messenger.send.message({ recipient: { id: 'USER_PSID' }, message: { attachment: { type: 'image', payload: { url: 'https://example.com/image.jpg' } } } }); ``` ### Templates API #### Generic Template ```typescript await messenger.templates.generic({ recipient: { id: 'USER_PSID' }, elements: [{ title: 'Welcome', subtitle: 'Check out our products', image_url: 'https://example.com/image.jpg', buttons: [{ type: 'web_url', title: 'Shop Now', url: 'https://example.com/shop' }] }] }); ``` #### Button Template ```typescript await messenger.templates.button({ recipient: { id: 'USER_PSID' }, text: 'What would you like to do?', buttons: [{ type: 'postback', title: 'Get Started', payload: 'GET_STARTED' }] }); ``` ### Moderation API #### User Moderation ```typescript // Block a user from messaging (can still see page content) await messenger.moderation.blockUser('USER_PSID'); // Ban a user completely (no messaging + no Facebook interactions) await messenger.moderation.banUser('USER_PSID'); // Move conversation to spam folder await messenger.moderation.moveToSpam('USER_PSID'); // Block and spam (common combo) await messenger.moderation.blockAndSpam('USER_PSID'); // Multiple users at once await messenger.moderation.blockUser(['USER_1', 'USER_2', 'USER_3']); // Unblock/unban users await messenger.moderation.unblockUser('USER_PSID'); await messenger.moderation.unbanUser('USER_PSID'); ``` ### Profile API #### Get User Profile Information ```typescript // Get basic profile info (first_name, last_name, profile_pic) const profile = await messenger.profile.getBasic('USER_PSID'); console.log(`Hello ${profile.first_name}!`); // Get comprehensive profile with all fields const fullProfile = await messenger.profile.getFull('USER_PSID'); console.log('Profile:', fullProfile); // Returns: { id, name, first_name, last_name, profile_pic, locale, timezone, gender } // Get specific fields only const customProfile = await messenger.profile.get({ psid: 'USER_PSID', fields: ['first_name', 'locale', 'timezone'] }); // Helper methods for common use cases const nameInfo = await messenger.profile.getName('USER_PSID'); const profilePic = await messenger.profile.getProfilePicture('USER_PSID'); ``` #### Personalize Messages ```typescript // Use profile info to personalize messages const profile = await messenger.profile.getName('USER_PSID'); const greeting = profile.first_name ? `Hello ${profile.first_name}! Welcome back!` : 'Hello! Welcome back!'; await messenger.send.message({ recipient: { id: 'USER_PSID' }, messaging_type: 'RESPONSE', message: { text: greeting } }); ``` **Note**: Profile API requires "Advanced User Profile Access" feature and user interaction to grant permissions. ### Conversations API Retrieve conversation history and messages between users and your Page or Instagram Business Account. #### Permissions Required **For Messenger conversations:** - `pages_manage_metadata` - `pages_read_engagement` - `pages_messaging` **For Instagram conversations:** - `instagram_basic` - `instagram_manage_messages` - `pages_manage_metadata` - Your app must be owned by a verified business #### List Conversations ```typescript // List Messenger conversations const conversations = await messenger.conversations.list('PAGE_ID', { platform: 'messenger', limit: 25 }); // List Instagram conversations const igConversations = await messenger.conversations.list('PAGE_ID', { platform: 'instagram', limit: 25 }); // Find conversation with specific user const conversationId = await messenger.conversations.findByUser( 'PAGE_ID', 'USER_INSTAGRAM_SCOPED_ID', 'instagram' ); ``` #### Get Conversation Details ```typescript // Get conversation with messages and participants const conversation = await messenger.conversations.get('CONVERSATION_ID', { fields: ['messages', 'participants'], limit: 20 }); // Access participants conversation.participants?.data.forEach(participant => { console.log(`${participant.name || participant.username} (${participant.id})`); }); // Access messages conversation.messages?.data.forEach(msg => { console.log(`Message ID: ${msg.id}, Created: ${msg.created_time}`); }); ``` #### Get Message Details ```typescript // Get full message details const message = await messenger.conversations.getMessage('MESSAGE_ID', { fields: ['id', 'created_time', 'from', 'to', 'message', 'attachments', 'reactions', 'reply_to'] }); console.log(`From: ${message.from?.username}`); console.log(`Text: ${message.message}`); // Check attachments if (message.attachments?.data) { message.attachments.data.forEach(att => { console.log(`Attachment: ${att.file_url || att.image_data?.url}`); }); } // Check reactions if (message.reactions?.data) { message.reactions.data.forEach(reaction => { console.log(`${reaction.reaction} (${reaction.users.length} users)`); }); } ``` #### Get Recent Messages (Convenience Method) ```typescript // Get the 20 most recent messages with full details const messages = await messenger.conversations.getRecentMessages('CONVERSATION_ID'); messages.forEach(msg => { const sender = msg.from?.name || msg.from?.username; const text = msg.message || '(attachment)'; console.log(`${sender}: ${text}`); }); ``` #### Pagination ```typescript // Paginate through conversations let after: string | undefined; let hasMore = true; while (hasMore) { const conversations = await messenger.conversations.list('PAGE_ID', { platform: 'messenger', limit: 25, after }); // Process conversations console.log(`Fetched ${conversations.data.length} conversations`); // Check for next page if (conversations.paging?.cursors?.after) { after = conversations.paging.cursors.after; } else { hasMore = false; } } ``` #### Important Limitations - **20 Message Limit**: You can only retrieve full details for the **20 most recent messages** in a conversation. Older messages will return an error. - **Pending Messages**: Conversations in the "pending" folder that are inactive for 30+ days are not returned. - **Private Keys**: Accounts linked with private keys (email/phone) require Advanced Access approval to retrieve conversations. ## Webhook Support The SDK provides comprehensive webhook support with full TypeScript safety through discriminated unions. ### Webhook Types and Processing ```typescript import { processWebhookEvents, extractWebhookEvents, getWebhookEventType, getWebhookPayloadEventTypes, getPageWebhookEventTypes, WebhookEventType, GenericWebhookPayload, PageWebhookPayload } from '@warriorteam/messenger-sdk'; // Process webhook with type-safe handlers app.post('/webhook', express.json(), async (req, res) => { const payload: GenericWebhookPayload = req.body; await processWebhookEvents(payload, { onMessage: async (event) => { // TypeScript knows this is MessageWebhookEvent console.log(`Received message: ${event.message.text}`); }, onMessageEdit: async (event) => { // TypeScript knows this is MessageEditWebhookEvent console.log(`Message edited to: ${event.message_edit.text}`); }, onMessageReaction: async (event) => { // TypeScript knows this is MessageReactionWebhookEvent console.log(`Reaction: ${event.reaction.reaction}`); }, onMessagingPostback: async (event) => { // TypeScript knows this is MessagingPostbackWebhookEvent console.log(`Postback: ${event.postback.payload}`); } }); res.sendStatus(200); }); ``` ### Manual Event Processing ```typescript // Extract events manually const events = extractWebhookEvents(payload); for (const event of events) { const eventType = getWebhookEventType(event); switch (eventType) { case WebhookEventType.MESSAGE: // Handle message event break; case WebhookEventType.MESSAGE_EDIT: // Handle edit event break; // ... other cases } } // Check what event types are in the payload (Messenger events) const eventTypes = getWebhookPayloadEventTypes(payload); console.log('Received Messenger event types:', eventTypes); // e.g., ['message', 'postback'] // For Page webhooks, use getPageWebhookEventTypes instead: // const pagePayload: PageWebhookPayload = req.body; // const pageEventTypes = getPageWebhookEventTypes(pagePayload); // console.log('Received Page event types:', pageEventTypes); // e.g., [WebhookEventType.FEED, WebhookEventType.VIDEOS] ``` ### Webhook Verification The SDK provides utilities for both subscription verification and signature validation: ```typescript import { verifyWebhookSubscription, verifyWebhookSignature } from '@warriorteam/messenger-sdk'; // Subscription verification (GET request) app.get('/webhook', (req, res) => { const challenge = verifyWebhookSubscription( req.query as any, process.env.VERIFY_TOKEN! ); if (challenge) { res.send(challenge); } else { res.status(403).send('Forbidden'); } }); // Signature verification (POST request) app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => { const signature = req.get('X-Hub-Signature-256'); const result = await verifyWebhookSignature( req.body, signature, process.env.APP_SECRET! ); if (!result.isValid) { return res.status(401).json({error: result.error}); } // Process webhook events... const payload = JSON.parse(req.body.toString()); // ... handle events }); ``` ### Type-Safe Event Handling The SDK uses discriminated unions for perfect TypeScript support: ```typescript import { MessengerWebhookEvent, isMessageEvent, isMessageEditEvent, isMessagingPostbackEvent } from '@warriorteam/messenger-sdk'; function handleWebhookEvent(event: MessengerWebhookEvent) { if (isMessageEvent(event)) { // TypeScript knows event.message exists console.log(`Message: ${event.message.text}`); } else if (isMessageEditEvent(event)) { // TypeScript knows event.message_edit exists console.log(`Edit: ${event.message_edit.text}`); } else if (isMessagingPostbackEvent(event)) { // TypeScript knows event.postback exists console.log(`Postback: ${event.postback.payload}`); } } ``` ### Supported Webhook Event Types #### Messenger Platform Events - `MESSAGE` - User sends a message - `MESSAGE_EDIT` - User edits a sent message - `MESSAGE_REACTION` - User reacts to a message - `MESSAGE_READ` - User reads messages (read receipts) - `MESSAGING_FEEDBACK` - User submits feedback via templates - `MESSAGING_POSTBACK` - User clicks buttons/quick replies #### Page Webhook Events - `FEED` - Page feed changes (posts, photos, videos, status updates) - `VIDEOS` - Video encoding status changes (processing, ready, error) - `LIVE_VIDEOS` - Live video status changes (live, stopped, scheduled, VOD ready) ### Page Webhook Type Helpers The SDK provides specialized type guards and helpers for Page webhook events: ```typescript import { isFeedEvent, isVideoEvent, isLiveVideoEvent, isVideoProcessing, isVideoReady, isLiveVideoProcessing, isLive, extractVideoContext, extractLiveVideoContext, getPageWebhookEventTypes, FeedActionVerb, VideoStatus, LiveVideoStatus, PageWebhookPayload } from '@warriorteam/messenger-sdk'; // Handle Page webhooks app.post('/webhook/page', express.json(), async (req, res) => { const payload: PageWebhookPayload = req.body; // Check what types of Page events are in this payload const eventTypes = getPageWebhookEventTypes(payload); console.log('Received Page event types:', eventTypes); // e.g., [WebhookEventType.FEED, WebhookEventType.VIDEOS] for (const entry of payload.entry) { for (const change of entry.changes || []) { // Feed events (posts, photos, videos, status) if (isFeedEvent(change)) { console.log(`Feed ${change.value.verb}: ${change.value.item} by ${change.value.from.id}`); if (change.value.verb === FeedActionVerb.ADD) { console.log('New post created!'); } } // Video encoding events if (isVideoEvent(change)) { const context = extractVideoContext(entry.id, entry.time, change); if (context.isReady) { console.log(`Video ${context.videoId} is ready!`); } else if (context.isProcessing) { console.log(`Video ${context.videoId} is still processing...`); } } // Live video events if (isLiveVideoEvent(change)) { const context = extractLiveVideoContext(entry.id, entry.time, change); if (context.isLive) { console.log(`Live stream ${context.videoId} is now live!`); } else if (context.isVODReady) { console.log(`Live stream ${context.videoId} VOD is ready!`); } } } } res.sendStatus(200); }); ``` **Note**: Page webhooks use a different structure (`entry[].changes[]`) compared to Messenger webhooks (`entry[].messaging[]`). The SDK provides helpers for both. ### Complete Controller Example (NestJS/Express) Here's a complete TypeScript controller for handling webhooks with proper types: ```typescript import { Controller, Post, Body, Headers, Res, Get, Query } from '@nestjs/common'; import { Response } from 'express'; import { GenericWebhookPayload, PageWebhookPayload, verifyWebhookSignature, verifyWebhookSubscription, processWebhookEvents, isFeedEvent, isVideoEvent, isLiveVideoEvent, } from '@warriorteam/messenger-sdk'; @Controller('webhook') export class WebhookController { // Subscription verification (GET) @Get() verifyWebhook(@Query() query: any, @Res() res: Response) { const challenge = verifyWebhookSubscription( query, process.env.VERIFY_TOKEN! ); if (challenge) { return res.send(challenge); } return res.status(403).send('Forbidden'); } // Messenger webhooks (POST) @Post('messenger') async handleMessenger( @Body() payload: GenericWebhookPayload, // ← Correct type for Messenger @Headers('x-hub-signature-256') signature: string, @Res() res: Response ) { // Verify signature const verification = await verifyWebhookSignature( JSON.stringify(payload), signature, process.env.APP_SECRET! ); if (!verification.isValid) { return res.status(401).json({ error: 'Invalid signature' }); } // Process events with type-safe handlers await processWebhookEvents(payload, { onMessage: async (event) => { // event is MessageWebhookEvent - fully typed! console.log(`From ${event.sender.id}: ${event.message.text}`); }, onMessagingPostback: async (event) => { // event is MessagingPostbackWebhookEvent console.log(`Button clicked: ${event.postback.payload}`); }, onMessageReaction: async (event) => { // event is MessageReactionWebhookEvent console.log(`Reaction: ${event.reaction.reaction}`); }, }); return res.sendStatus(200); } // Page webhooks (POST) @Post('page') async handlePage( @Body() payload: PageWebhookPayload, // ← Correct type for Page webhooks @Headers('x-hub-signature-256') signature: string, @Res() res: Response ) { // Verify signature const verification = await verifyWebhookSignature( JSON.stringify(payload), signature, process.env.APP_SECRET! ); if (!verification.isValid) { return res.status(401).json({ error: 'Invalid signature' }); } // Process Page events for (const entry of payload.entry) { for (const change of entry.changes || []) { if (isFeedEvent(change)) { console.log(`Page feed: ${change.value.verb} ${change.value.item}`); } if (isVideoEvent(change)) { console.log(`Video status: ${change.value.status.video_status}`); } if (isLiveVideoEvent(change)) { console.log(`Live video status: ${change.value.status}`); } } } return res.sendStatus(200); } } ``` **Key Types to Use:** - `GenericWebhookPayload` - For Messenger Platform webhooks (`entry[].messaging[]`) - `PageWebhookPayload` - For Page webhooks (`entry[].changes[]`) ## Error Handling The SDK provides specific error types for different scenarios: ```typescript import { MessengerAPIError, MessengerNetworkError, MessengerTimeoutError, MessengerConfigError } from '@warriorteam/messenger-sdk'; try { await messenger.send.message({ recipient: { id: 'USER_PSID' }, message: { text: 'Hello!' } }); } catch (error) { if (error instanceof MessengerAPIError) { console.error('API Error:', error.message, error.code); } else if (error instanceof MessengerNetworkError) { console.error('Network Error:', error.message); } else if (error instanceof MessengerTimeoutError) { console.error('Timeout Error:', error.timeout); } } ``` ## Examples Check the `examples/` directory for complete usage examples: - `send-message.ts` - Basic message sending - `upload-attachment.ts` - Attachment handling - `send-template.ts` - Template messages - `user-profile.ts` - Profile API usage - `moderation.ts` - User moderation - `conversations.ts` - Retrieve conversations and messages - `webhook-handler.ts` - Complete webhook setup with type safety - `multi-tenant.ts` - Token override for multi-tenant applications - `signature-verification.ts` - Webhook security implementation ## Development ```bash # Install dependencies npm install # Build the project npm run build # Run tests npm test # Lint code npm run lint # Format code npm run format ``` ## Changelog ### v1.5.2 (Latest) - **Added**: `getPageWebhookEventTypes()` utility function for extracting event types from Page webhooks - Returns `WebhookEventType[]` array with proper enum values (e.g., `[WebhookEventType.FEED, WebhookEventType.VIDEOS]`) - Maps Page webhook `field` values to `WebhookEventType` enum for type safety - Similar to `getWebhookPayloadEventTypes()` but for `PageWebhookPayload` structure - Properly exported from main package - **Improved**: Documentation with usage examples for Page webhook event type extraction ### v1.5.1 - **Added**: Complete NestJS/Express webhook controller example in README - **Updated**: WebhookEventType enum to include FEED, VIDEOS, LIVE_VIDEOS - **Improved**: Documentation with clear type usage for webhook handlers and Page webhook utilities ### v1.5.0 - **Added**: Conversations API for retrieving conversation history and messages - List conversations for Messenger and Instagram - Get conversation details with participants and messages - Retrieve individual message details with attachments, reactions, and replies - Convenience method for getting recent messages with full details - Find conversations by user ID - Full pagination support - Support for both Messenger and Instagram platforms - Token override support for multi-tenant applications - **Added**: Comprehensive TypeScript types for Conversations API - `Conversation`, `ConversationDetail`, `Message` types - Message attachments, reactions, shares, story replies - Image/video data types for media attachments - Request/response types for all operations ### v1.4.2 - **Fixed**: Resolved export naming conflicts for `isProcessing` function between video and live video webhooks - `isProcessing` from videos module now exported as `isVideoProcessing` - `isProcessing` from live-videos module now exported as `isLiveVideoProcessing` - Added explicit exports for all video and live video helper functions with unique names - Similar to existing pattern used for `ReferralType` and `ReferralSource` conflicts - **Improved**: Better TypeScript type safety with no export ambiguities ### v1.4.x - **Added**: Comprehensive Facebook Page webhook types - Feed webhook events (posts, photos, videos, status updates) - Video encoding webhook events (processing, ready, error states) - Live video webhook events (live, stopped, scheduled, VOD ready) - **Added**: Type guards and helper functions for all Page webhook events - **Added**: Context extraction utilities for video events ### v1.3.x - **Added**: Token override support for multi-tenant applications - **Improved**: All API methods now accept optional `RequestOptions` parameter ### v1.1.0 - **Added**: User Profile API support ## License MIT ## Support For issues and questions, please visit our [GitHub repository](https://github.com/warriorteam/messenger-sdk).