npm package: @nhtio/rocketchat-bot-sdk
Description: An SDK for integrating bot users with RocketChat
Author: Jak Giveon
Latest version: 1.20260406.2

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

```sh
npm install @nhtio/rocketchat-bot-sdk
# or
pnpm add @nhtio/rocketchat-bot-sdk
```

---

## Quick Start

```typescript
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`

```typescript
interface BotConfig {
  serverUrl: string        // e.g. "https://chat.example.com"
  credentials: BotCredentials
  e2ee?: E2EEConfig        // omit to disable E2EE
  reconnect?: ReconnectConfig
}
```

### `BotCredentials`

```typescript
interface BotCredentials {
  username: string
  userId: string
  password: string
  personalAccessToken: string
  totpSecret?: string      // Base-32 TOTP secret for MFA accounts
}
```

### `E2EEConfig`

```typescript
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.):

```typescript
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`

```typescript
interface ReconnectConfig {
  enabled?: boolean    // default: true
  maxDelay?: number    // milliseconds, default: 30000
}
```

---

## Events

Subscribe to events using the standard `.on()` / `.once()` / `.off()` API.

```typescript
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.

```typescript
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

```typescript
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:

```typescript
await bot.createRoomKey(roomId)
```

### Manual decryption

For messages fetched via `getMessage()` rather than the stream:

```typescript
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.

```typescript
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.

```typescript
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

```typescript
bot.connect(): Promise<void>
bot.disconnect(): Promise<void>
bot.state: ConnectionState
```

### Streams

```typescript
bot.subscribeToRoom(roomId: string): Promise<void>
bot.unsubscribeFromRoom(roomId: string): Promise<void>
```

### Messages

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
bot.setAway(): Promise<void>
bot.sendTyping(roomId: string, isTyping: boolean): Promise<void>
bot.subscribeToUserStatus(): Promise<void>  // then listen for 'user:status' events
```

### E2EE

```typescript
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:

```typescript
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 access
```

---

## Exported Types

```typescript
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.