# @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).