node-red-contrib-chatbot
Version:
REDBot a Chat bot for a full featured chat bot for Telegram, Facebook Messenger and Slack. Almost no coding skills required
355 lines (275 loc) • 9.82 kB
Markdown
ChatExpress is the middleware engine that powers all platform integrations in REDBot. Each supported platform (Telegram, Facebook, Slack) is a ChatExpress instance that handles:
- Receiving raw webhook payloads and transforming them into normalized messages
- Running inbound middleware chains before messages reach Node-RED flows
- Running outbound middleware chains before messages are sent to the platform API
- Storing and retrieving per-conversation state via the chat context
---
## How it works
```
[Platform Webhook]
↓
Platform.receive(rawPayload)
↓
[Inbound middleware chain] ← .in() handlers run in registration order
↓
[Node-RED flow]
↓
node.chat.send(message)
↓
[Outbound middleware chain] ← .out() handlers matched by message type
↓
[Platform API call]
```
---
## Creating a platform
```js
const ChatExpress = require('chat-platform');
const MyPlatform = new ChatExpress({
transport: 'myplatform', // unique identifier
transportDescription: 'My Platform',
color: '#336699',
// Extract IDs from raw platform payloads
chatIdKey: payload => payload.chat.id,
userIdKey: payload => payload.from.id,
messageIdKey: payload => payload.message_id,
tsKey: payload => payload.date,
// Classify incoming messages
type: payload => {
if (payload.text) return 'message';
if (payload.photo) return 'photo';
},
// Lifecycle hooks
onStart: function() {
const { token } = this.getOptions();
return new PlatformSDK(token); // returned value becomes the connector
},
onStop: function() {
const { connector } = this.getOptions();
connector.stop();
},
// Webhook routes (Express-style)
routes: {
'/redbot/myplatform': function(req, res) {
this.receive(req.body);
res.send({ status: 'ok' });
}
}
});
```
| Option | Type | Description |
|--------|------|-------------|
| `transport` | string | Unique platform identifier |
| `transportDescription` | string | Human-readable name |
| `color` | string | UI accent color (hex) |
| `chatIdKey` | `fn(payload)` | Extract conversation ID from raw payload |
| `userIdKey` | `fn(payload)` | Extract user ID |
| `messageIdKey` | `fn(payload)` | Extract message ID |
| `tsKey` | `fn(payload)` | Extract timestamp |
| `type` | `fn(payload)` | Classify message type (`'message'`, `'photo'`, etc.) |
| `language` | `fn(payload)` | Extract language code (optional) |
| `onStart` | `fn()` | Called when bot starts — return the connector instance |
| `onStop` | `fn()` | Called when bot stops |
| `onCreateMessage` | `fn(obj)` | Transform the raw message object before middleware runs |
| `routes` | object | Express route handlers keyed by path |
| `multiWebHook` | boolean | Support multiple webhook subpaths |
| `relaxChatId` | boolean | Allow null chatId (for events like inline queries) |
| `events` | object | Handlers for platform-specific event types |
---
## Middleware API
### Inbound middleware — `.in()`
Runs on every incoming message before it reaches the Node-RED flow. Use it to normalize payloads, download media, and persist user state.
```js
// Runs for all message types
MyPlatform.in(function(message) {
message.payload.content = message.originalMessage.text;
return message; // must return message (or a Promise resolving to it)
});
// Runs only for a specific type
MyPlatform.in('photo', async function(message) {
const buffer = await downloadFile(message.originalMessage.photo[0].file_id);
message.payload.content = buffer;
return message;
});
```
Runs on every outgoing message before it is sent to the platform API. Use it to map Node-RED message fields to platform-specific API parameters.
```js
// Runs for all message types (catch-all)
MyPlatform.out(function(message) {
// fallback handler
return message;
});
// Runs only for 'message' type
MyPlatform.out('message', async function(message) {
const { connector } = this.getOptions();
const param = params(message);
const result = await connector.sendMessage(
message.payload.chatId,
message.payload.content,
{ disable_notification: param('silent', false) }
);
await message.chat().set({ lastMessageId: result.message_id });
return message;
});
```
**Rules for both:**
- Must `return message` (or a Promise resolving to it) to continue the chain
- Throw a string or Error to abort the chain and surface an error
- Access bot config with `this.getOptions()` (inside regular functions, not arrow functions)
---
```js
{
payload: {
type: 'message' | 'photo' | 'location' | 'event' | ...,
content: 'text' | Buffer | { latitude, longitude } | ...,
chatId: '123456',
userId: '789',
messageId: 'msg_456',
caption: 'optional', // for media types
// additional fields set by .in() middleware
},
originalMessage: {
// Raw platform payload — Telegram update, FB event, Slack event, etc.
transport: 'telegram',
chatId: '123456',
userId: '789',
...
},
chat: function() { /* returns context store — see below */ },
client: function() { /* returns connector (platform SDK instance) */ }
}
```
---
Each conversation has a key-value store that persists across messages. Access it via `message.chat()`.
```js
const context = message.chat();
// Read one or more keys
const { firstName, language } = await context.get('firstName', 'language');
// Write (merged — doesn't overwrite unrelated keys)
await context.set({ firstName: 'Guido', language: 'it' });
// Read everything
const allVars = await context.all();
// Remove specific keys
await context.remove('tempKey');
// Wipe the conversation state
await context.clear();
```
| Provider | Config | Notes |
|----------|--------|-------|
| `memory` | — | Default. Lost on restart. |
| `plain-file` | `contextParams.path` | File-based persistence. |
| `sqlite` | — | Used in Mission Control mode. |
---
Parameters are configurable send options exposed to Node-RED node UIs. Register them so Mission Control can show them as node fields.
```js
// Register
MyPlatform.registerParam('silent', 'boolean', {
label: 'Silent notification',
default: false,
description: 'Deliver without notification sound'
});
// Read inside .out() middleware
const param = params(message);
const isSilent = param('silent', false);
```
Parameter types: `'boolean'`, `'string'`, `'select'`
For `'select'` add `options: [{ value, label }, ...]`.
For `'string'` add `placeholder` and/or `suggestions: ['{{token}}', ...]`.
---
Register the message types your platform can send. These appear in node configuration dropdowns and enable type-specific validation.
```js
MyPlatform.registerMessageType('message', 'Message', 'Send a plain text message');
MyPlatform.registerMessageType('video', 'Video', 'Send a video file', (file) => {
if (file.size > 50 * 1024 * 1024) return 'File too large (max 50 MB)';
return null; // valid
});
```
---
```js
MyPlatform.start() // initialize, calls onStart
MyPlatform.stop() // shutdown, calls onStop
// Listen for platform errors
MyPlatform.on('error', (error) => { /* ... */ });
MyPlatform.on('warning', (warning) => { /* ... */ });
// Listen for incoming messages (used internally by receiver nodes)
MyPlatform.on('message', (message) => { /* ... */ });
```
---
Use `mixin()` to attach helper methods to the platform instance:
```js
MyPlatform.mixin({
downloadFile(url) {
return fetch(url).then(r => r.buffer());
}
});
// Available as this.downloadFile() inside middlewares
MyPlatform.in(async function(message) {
const buffer = await this.downloadFile(url);
return message;
});
```
---
```js
const ChatExpress = require('chat-platform');
const SDK = require('my-platform-sdk');
const MyPlatform = new ChatExpress({
transport: 'myplatform',
transportDescription: 'My Platform',
chatIdKey: p => p.chat_id,
userIdKey: p => p.user_id,
messageIdKey: p => p.id,
tsKey: p => p.timestamp,
type: p => p.photo ? 'photo' : 'message',
onStart: function() {
const { token } = this.getOptions();
const bot = new SDK(token);
bot.on('message', payload => this.receive(payload));
return bot;
},
onStop: function() {
this.getOptions().connector.disconnect();
}
});
// Inbound: extract text
MyPlatform.in(function(message) {
message.payload.content = message.originalMessage.text;
return message;
});
// Inbound: persist user info
MyPlatform.in(async function(message) {
const ctx = message.chat();
const { firstName } = await ctx.get('firstName');
if (!firstName) {
await ctx.set({ firstName: message.originalMessage.user.name });
}
return message;
});
// Outbound: send text
MyPlatform.out('message', async function(message) {
const { connector } = this.getOptions();
await connector.send(message.payload.chatId, message.payload.content);
return message;
});
// Register types and params
MyPlatform.registerMessageType('message', 'Message', 'Send text');
MyPlatform.registerParam('silent', 'boolean', { label: 'Silent', default: false });
module.exports = MyPlatform;
```
---
| Platform | File |
|----------|------|
| Telegram | [`lib/platforms/telegram.js`](../lib/platforms/telegram.js) |
| Facebook | [`lib/platforms/facebook.js`](../lib/platforms/facebook.js) |
| Slack | [`lib/platforms/slack.js`](../lib/platforms/slack.js) |
The sender factory that boots platforms and wires them to Node-RED is at [`lib/sender-factory/index.js`](../lib/sender-factory/index.js).