@nhtio/rocketchat-bot-sdk
v1.20260406.2
Published
An SDK for integrating bot users with RocketChat
Readme
@nhtio/rocketchat-bot-sdk
A TypeScript SDK for running Rocket.Chat bot accounts as sidecar Node.js processes. Connects to Rocket.Chat via both DDP WebSocket (real-time events) and the REST API, with built-in support for end-to-end encryption (E2EE), a typed event emitter, and a lifecycle hook system.
Requirements
- Node.js 18 or later
- Rocket.Chat 8.2.0 or later
Installation
npm install @nhtio/rocketchat-bot-sdk
# or
pnpm add @nhtio/rocketchat-bot-sdkQuick Start
import { RocketChatBot } from '@nhtio/rocketchat-bot-sdk'
const bot = new RocketChatBot({
serverUrl: 'https://chat.example.com',
credentials: {
username: 'my-bot',
userId: 'botUserId',
password: 'botPassword',
personalAccessToken: 'botPersonalAccessToken',
},
})
// Listen for messages before connecting
bot.on('message', (msg) => {
if (msg.isOwn) return // ignore messages sent by this bot
console.log(`[${msg.rid}] ${msg.u.username}: ${msg.msg}`)
if (msg.msg === '!ping') {
bot.sendMessage({ rid: msg.rid, msg: 'pong!' })
}
})
await bot.connect()
// Subscribe to a room to receive its messages
await bot.subscribeToRoom('GENERAL')Configuration
BotConfig
interface BotConfig {
serverUrl: string // e.g. "https://chat.example.com"
credentials: BotCredentials
e2ee?: E2EEConfig // omit to disable E2EE
reconnect?: ReconnectConfig
}BotCredentials
interface BotCredentials {
username: string
userId: string
password: string
personalAccessToken: string
totpSecret?: string // Base-32 TOTP secret for MFA accounts
}E2EEConfig
interface E2EEConfig {
passphrase: string // used to protect the bot's RSA private key
storage: E2EEStorageContract // your backend for key persistence
}E2EEStorageContract is an interface you implement to control where E2EE keys are stored (filesystem, database, KMS, etc.):
interface E2EEStorageContract {
getPublicKey(): Promise<string | null>
setPublicKey(key: string): Promise<void>
getPrivateKey(): Promise<string | null>
setPrivateKey(key: string): Promise<void>
getRoomKey(roomId: string, keyId: string): Promise<string | null>
setRoomKey(roomId: string, keyId: string, key: string): Promise<void>
getOldRoomKeys(roomId: string): Promise<Array<{ keyId: string; key: string }>>
setOldRoomKeys(roomId: string, keys: Array<{ keyId: string; key: string }>): Promise<void>
}ReconnectConfig
interface ReconnectConfig {
enabled?: boolean // default: true
maxDelay?: number // milliseconds, default: 30000
}Events
Subscribe to events using the standard .on() / .once() / .off() API.
bot.on('message', (msg) => { /* ReceivedMessage */ })Connection events
| Event | Arguments | Description |
| -------------- | ---------------------------------- | -------------------------------------------------- |
| connected | — | DDP session established and bot authenticated |
| disconnected | reason: string | Connection lost or intentionally closed |
| reconnecting | attempt: number, delayMs: number | About to attempt reconnection |
| logged_in | userId: string | Bot successfully authenticated |
| state:change | state, previous: ConnectionState | Internal state machine transition |
| error | error: Error | Unrecoverable error (e.g. reconnect limit reached) |
| ddp:changed | collection, id, fields | Raw DDP changed notification |
Message events
All message events receive a ReceivedMessage object, which extends RCMessage with an isOwn: boolean flag. isOwn is true when the message was sent by the bot itself.
| Event | Arguments | Description |
| ----------------- | ---------------------- | -------------------------------- |
| message | msg: ReceivedMessage | New message in a subscribed room |
| message:updated | msg: ReceivedMessage | An existing message was edited |
| message:deleted | { _id, rid } | A message was deleted |
Room events
| Event | Arguments | Description |
| ---------------------- | ---------------------------- | ------------------------------------------ |
| room:changed | room: RCRoom | Room metadata was updated |
| subscription:changed | sub: RCSubscription | The bot's room subscription record changed |
| typing | roomId, username, isTyping | A user started or stopped typing |
User events
| Event | Arguments | Description |
| ------------- | -------------------------- | -------------------------------------------------------------------- |
| user:status | userId, username, status | A user's presence changed — requires subscribeToUserStatus() first |
E2EE events
| Event | Arguments | Description |
| ---------------- | ----------------------------------------- | ---------------------------------------------- |
| e2ee:ready | — | E2EE keys loaded, encryption layer operational |
| e2ee:decrypted | msg: ReceivedMessage, plaintext: string | An encrypted message was decrypted |
| e2ee:error | error: Error, context: string | An E2EE operation failed |
Hooks
Hooks let you intercept operations before and after they execute. Hooks run on a per-instance basis and support async middleware with error recovery.
import { RocketChatBot } from '@nhtio/rocketchat-bot-sdk'
const bot = new RocketChatBot(config)
// Log every outgoing message
bot.hooks.hook('beforeSendMessage', async ([message]) => {
console.log('Sending:', message.msg)
})
// Prevent messages containing a blocked word
bot.hooks.hook('beforeSendMessage', async ([message]) => {
if (message.msg.includes('badword')) {
throw new Error('Message blocked')
}
})Available hooks
| Hook | Before args | After args |
| ---------------------------------------- | ----------------- | ---------------------------------- |
| beforeConnect / afterConnect | — | error \| null |
| beforeDisconnect / afterDisconnect | — | error \| null |
| beforeSendMessage / afterSendMessage | OutgoingMessage | error \| null, RCMessage \| null |
| beforeJoinRoom / afterJoinRoom | roomId: string | error \| null |
| beforeLeaveRoom / afterLeaveRoom | roomId: string | error \| null |
End-to-End Encryption (E2EE)
When E2EE is configured, the SDK automatically encrypts outgoing messages and decrypts incoming ones for rooms that have an active E2EE key.
Setup
import { RocketChatBot, type E2EEStorageContract } from '@nhtio/rocketchat-bot-sdk'
import { readFile, writeFile } from 'node:fs/promises'
// Minimal filesystem-backed key store
const storage: E2EEStorageContract = {
async getPublicKey() { return readFile('.keys/pub', 'utf8').catch(() => null) },
async setPublicKey(k) { await writeFile('.keys/pub', k) },
async getPrivateKey() { return readFile('.keys/priv', 'utf8').catch(() => null) },
async setPrivateKey(k) { await writeFile('.keys/priv', k) },
async getRoomKey(rid, kid) {
return readFile(`.keys/${rid}-${kid}`, 'utf8').catch(() => null)
},
async setRoomKey(rid, kid, k) { await writeFile(`.keys/${rid}-${kid}`, k) },
async getOldRoomKeys(rid) {
const raw = await readFile(`.keys/${rid}-old.json`, 'utf8').catch(() => '[]')
return JSON.parse(raw)
},
async setOldRoomKeys(rid, keys) {
await writeFile(`.keys/${rid}-old.json`, JSON.stringify(keys))
},
}
const bot = new RocketChatBot({
serverUrl: 'https://chat.example.com',
credentials: { /* ... */ },
e2ee: {
passphrase: process.env.BOT_E2EE_PASSPHRASE!,
storage,
},
})
bot.on('e2ee:ready', () => console.log('E2EE ready'))
// Encrypted messages arrive already decrypted via the 'message' event.
// The e2ee:decrypted event fires alongside it for inspection.
bot.on('e2ee:decrypted', (msg, plaintext) => {
console.log('Decrypted:', plaintext)
})
await bot.connect()Creating a room key
If you create a new encrypted room, generate its key before sending messages:
await bot.createRoomKey(roomId)Manual decryption
For messages fetched via getMessage() rather than the stream:
const raw = await bot.getMessage(messageId)
if (raw.content) {
const plaintext = await bot.decryptMessage(raw.rid, raw.content)
}Files
uploadFile follows Rocket.Chat's two-step media protocol. When E2EE is active for the target room the file bytes are automatically encrypted before upload.
import { readFile } from 'node:fs/promises'
const content = await readFile('./report.pdf')
const msg = await bot.uploadFile({
rid: roomId,
file: content,
filename: 'report.pdf',
mimetype: 'application/pdf',
msg: 'Here is the weekly report.', // optional message text
description: 'Weekly report', // optional attachment description
tmid: threadParentId, // optional: post into a thread
})
console.log('Uploaded, message ID:', msg._id)downloadFile fetches the attachment from a received message. For E2EE rooms the raw bytes are decrypted automatically using the per-file AES-CTR key stored in the message envelope.
bot.on('message', async (msg) => {
if (msg.attachments?.length) {
const { buffer, filename } = await bot.downloadFile(msg)
// buffer is already decrypted when the room uses E2EE
await writeFile(filename ?? 'download', buffer)
}
})API Reference
Connection
bot.connect(): Promise<void>
bot.disconnect(): Promise<void>
bot.state: ConnectionStateStreams
bot.subscribeToRoom(roomId: string): Promise<void>
bot.unsubscribeFromRoom(roomId: string): Promise<void>Messages
bot.sendMessage(message: OutgoingMessage): Promise<RCMessage>
bot.postMessage(params: PostMessageParams): Promise<RCMessage>
bot.updateMessage(msgId: string, roomId: string, text: string): Promise<RCMessage>
bot.deleteMessage(msgId: string, roomId: string): Promise<void>
bot.getMessage(msgId: string): Promise<RCMessage>
bot.getThreadMessages(tmid: string, count?: number, offset?: number): Promise<{ messages: RCMessage[]; total: number }>
bot.getHistory(roomId: string, opts?: HistoryOptions): Promise<{ messages: RCMessage[] }>
bot.syncMessages(roomId: string, lastUpdate: Date | string): Promise<{ updated: RCMessage[]; deleted: { _id: string }[] }>
bot.getMentionedMessages(roomId: string, count?: number, offset?: number): Promise<{ messages: RCMessage[]; total: number }>
bot.getThreadsList(roomId: string, opts?: ThreadListOptions): Promise<{ threads: RCMessage[]; total: number }>
bot.getPinnedMessages(roomId: string, count?: number, offset?: number): Promise<{ messages: RCMessage[]; total: number }>
bot.getStarredMessages(roomId: string, count?: number, offset?: number): Promise<{ messages: RCMessage[]; total: number }>
bot.getDeletedMessages(roomId: string, since: Date | string): Promise<{ messages: { _id: string }[]; total: number }>
bot.getDiscussions(roomId: string, opts?: DiscussionListOptions): Promise<{ messages: RCMessage[]; total: number }>
bot.markAsRead(rid: string): Promise<void>
bot.reactToMessage(messageId: string, emoji: string, shouldReact?: boolean): Promise<void>
bot.pinMessage(messageId: string): Promise<void>
bot.unpinMessage(messageId: string): Promise<void>
bot.starMessage(messageId: string): Promise<void>
bot.unstarMessage(messageId: string): Promise<void>
bot.followMessage(mid: string): Promise<void>
bot.unfollowMessage(mid: string): Promise<void>
bot.reportMessage(messageId: string, description: string): Promise<void>Files
bot.uploadFile(params: FileUploadParams): Promise<RCMessage>
bot.downloadFile(message: RCMessage): Promise<{ buffer: Uint8Array; filename?: string }>
interface FileUploadParams {
rid: string
file: Buffer | Uint8Array
filename: string
mimetype?: string
description?: string
tmid?: string
msg?: string
}Rooms
bot.createChannel(params: CreateChannelParams): Promise<RCRoom>
bot.createGroup(params: CreateGroupParams): Promise<RCRoom>
bot.createDM(params: CreateDMParams): Promise<RCRoom>
bot.createDiscussion(params: CreateDiscussionParams): Promise<RCRoom>
bot.joinChannel(roomId: string): Promise<void>
bot.leaveRoom(roomId: string): Promise<void>
bot.inviteToChannel(roomId: string, userId: string): Promise<void>
bot.inviteToGroup(roomId: string, userId: string): Promise<void>
bot.kickFromRoom(roomId: string, userId: string): Promise<void>
bot.muteUser(roomId: string, user: { userId: string } | { username: string }): Promise<void>
bot.unmuteUser(roomId: string, user: { userId: string } | { username: string }): Promise<void>
bot.getRoomInfo(roomId: string): Promise<RCRoom>
bot.getRoomMembers(roomId: string, count?: number, offset?: number): Promise<{ members: RCUser[]; total: number }>
bot.getRoomRoles(roomId: string): Promise<RCRoomRole[]>
bot.isMember(roomId: string, user: { userId: string } | { username: string }): Promise<boolean>
bot.getSubscriptions(): Promise<RCSubscription[]>
bot.getRooms(): Promise<RCRoom[]>
bot.setRoomTopic(roomId: string, topic: string): Promise<void>
bot.setRoomAnnouncement(roomId: string, announcement: string): Promise<void>
bot.setRoomDescription(roomId: string, description: string): Promise<void>
bot.renameRoom(roomId: string, name: string): Promise<void>
bot.setRoomReadOnly(roomId: string, readOnly: boolean): Promise<void>
bot.setRoomType(roomId: string, type: 'c' | 'p'): Promise<void>
bot.archiveRoom(roomId: string): Promise<void>
bot.unarchiveRoom(roomId: string): Promise<void>
bot.favoriteRoom(roomId: string, favorite: boolean): Promise<void>
bot.hideRoom(roomId: string): Promise<void>
bot.runSlashCommand(command: string, roomId: string, params?: string, tmid?: string): Promise<void>Users
bot.getUserInfo(query: { userId: string } | { username: string }): Promise<RCUser>
bot.setStatus(options: SetStatusParams): Promise<void>
bot.getStatus(query?: { userId: string } | { username: string }): Promise<{ status: UserStatus; message: string; connectionStatus: string }>Presence & Typing
bot.setAway(): Promise<void>
bot.sendTyping(roomId: string, isTyping: boolean): Promise<void>
bot.subscribeToUserStatus(): Promise<void> // then listen for 'user:status' eventsE2EE
bot.e2eeReady: boolean
bot.hasRoomKey(roomId: string): boolean
bot.createRoomKey(roomId: string): Promise<void>
bot.decryptMessage(roomId: string, encryptedContent: string | { algorithm: string; kid: string; iv: string; ciphertext: string }): Promise<string | null>DDP passthrough
For advanced use cases that require direct access to the DDP connection:
bot.callMethod<T>(method: string, params?: unknown[]): Promise<T>
bot.subscribe(name: string, params?: unknown[]): Promise<string>
bot.unsubscribe(id: string): Promise<void>
bot.rest: RESTClient // direct REST client accessExported Types
import type {
BotConfig,
BotCredentials,
E2EEConfig,
E2EEStorageContract,
ReconnectConfig,
BotEventMap,
BotHookEvents,
ConnectionState,
RCUser,
RCRoom,
RCRoomRole,
RCSubscription,
RCMessage,
ReceivedMessage,
RCAttachment,
EJSONDate,
UserStatus,
OutgoingMessage,
PostMessageParams,
FileUploadParams,
CreateChannelParams,
CreateGroupParams,
CreateDMParams,
CreateDiscussionParams,
HistoryOptions,
ThreadListOptions,
DiscussionListOptions,
SetStatusParams,
} from '@nhtio/rocketchat-bot-sdk'License
MIT — Copyright © 2025-present New Horizon Technology LTD (nht.io)
This package is published for public use under the MIT License. The source repository is private as this library is a work-product of NHT. Bug reports and questions can be directed to the package maintainer.
