Skip to content

Adding a Channel

Channels connect OpenMotoko to messaging platforms. This guide covers implementing the ChannelAdapter interface, registering your adapter, and distributing it as a plugin.

Every channel must implement this interface:

interface ChannelAdapter {
readonly type: string
start(): Promise<void>
stop(): Promise<void>
sendMessage(message: OutboundMessage): Promise<void>
onMessage(handler: (message: InboundMessage) => void): void
}
interface InboundMessage {
id: string
channelId: string
channelType: string
chatId: string
senderId: string
senderName: string
content: string
isGroup: boolean
groupId?: string
isMention?: boolean
replyToId?: string
attachments?: Attachment[]
timestamp: number
}
interface OutboundMessage {
chatId: string
content: string
replyToId?: string
attachments?: Attachment[]
}

Here is a minimal example for a hypothetical “Rocket.Chat” adapter:

import type { ChannelAdapter, InboundMessage, OutboundMessage } from '@openmotoko/core'
export class RocketChatAdapter implements ChannelAdapter {
readonly type = 'rocketchat'
private handler?: (msg: InboundMessage) => void
private client: RocketChatClient
constructor(private config: { url: string; token: string }) {
this.client = new RocketChatClient(config.url, config.token)
}
async start(): Promise<void> {
await this.client.connect()
this.client.on('message', (raw) => {
if (!this.handler) return
this.handler({
id: raw.id,
channelId: 'rocketchat',
channelType: 'rocketchat',
chatId: raw.roomId,
senderId: raw.userId,
senderName: raw.username,
content: raw.text,
isGroup: raw.roomType !== 'dm',
timestamp: Date.now(),
})
})
}
async stop(): Promise<void> {
await this.client.disconnect()
}
async sendMessage(message: OutboundMessage): Promise<void> {
await this.client.sendMessage(message.chatId, message.content)
}
onMessage(handler: (message: InboundMessage) => void): void {
this.handler = handler
}
}

Add your channel type to the ChannelType union in packages/core/src/channels/types.ts:

type ChannelType =
| 'telegram'
| 'whatsapp'
| 'discord'
| 'rocketchat'

All channels automatically support these slash commands:

CommandDescription
/statusAgent status
/newNew conversation
/resetClear history
/compactCompress context
/model <name>Switch model
/think <level>Set thinking depth
/costShow costs
/helpList commands

Your adapter does not need to implement these; the channel manager handles them.

To distribute your channel as a standalone npm package:

@openmotoko/channel-rocketchat/
src/
adapter.ts
index.ts
package.json
{
"name": "@openmotoko/channel-rocketchat",
"version": "1.0.0",
"main": "dist/index.js",
"peerDependencies": {
"@openmotoko/core": "workspace:*"
}
}
export { RocketChatAdapter } from './adapter'
export const channelType = 'rocketchat'

Users register your plugin in openmotoko.config.ts:

const config = {
channelPlugins: [
{
packageName: '@openmotoko/channel-rocketchat',
config: {
url: 'https://chat.example.com',
token: 'bot-token',
},
},
],
}
export default config

Or install via the API:

Terminal window
curl -X POST http://localhost:3457/api/channel-plugins/install \
-H "Cookie: session=..." \
-H "Content-Type: application/json" \
-d '{"packageName": "@openmotoko/channel-rocketchat"}'

Each channel supports a per-channel message policy:

{
"channels": {
"rocketchat": {
"enabled": true,
"policy": {
"dmPolicy": "allowlist",
"allowFrom": ["user123"],
"requireMention": true
}
}
}
}
PolicyBehavior
openAccept messages from anyone
pairingRequire a pairing code
allowlistOnly accept from listed IDs