npm package: @mundobobba/bot-sdk
Description: Official SDK for building bots on the Mundo Bobba platform
Author: Mundo Bobba
Latest version: 1.0.0

# @mundobobba/bot-sdk

Official SDK for building bots on the [Mundo Bobba](https://mundobobba.com) platform.

[![npm version](https://img.shields.io/npm/v/@mundobobba/bot-sdk.svg)](https://www.npmjs.com/package/@mundobobba/bot-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Installation

```bash
npm install @mundobobba/bot-sdk
```

### System requirements (for music bots)

- [yt-dlp](https://github.com/yt-dlp/yt-dlp) installed and available in PATH
- [FFmpeg](https://ffmpeg.org/) installed and available in PATH
- A `cookies.txt` file from YouTube in your bot's working directory

## Quick start

```ts
import { MundobobbaBot } from '@mundobobba/bot-sdk';

const bot = new MundobobbaBot({
  token: process.env.BOT_TOKEN!,
  prefix: '!',
});

bot.on('ready', () => {
  console.log(`Bot online as ${bot.botName}`);
});

bot.textCommand('ping', (ctx) => {
  ctx.reply('Pong!');
});
```

## Getting your token

1. Go to [mundobobba.com](https://mundobobba.com) and sign in
2. Navigate to **Messages > Bots**
3. Create a new bot and copy the generated token
4. Invite the bot to a conversation using the invite link

---

## Text commands

Register commands that respond to messages starting with your prefix.

```ts
const bot = new MundobobbaBot({ token: '...', prefix: '!' });

// Simple reply
bot.textCommand('hello', (ctx) => {
  ctx.reply(`Hello, ${ctx.invokedBy.name}!`);
});

// Access command arguments
bot.textCommand('echo', (ctx) => {
  ctx.reply(ctx.args || 'Nothing to echo.');
});

// Built-in commands: !ping, !uptime, !help
bot.useBuiltins();
```

### Command context

Every command handler receives a `CommandContext` with:

| Property | Type | Description |
|---|---|---|
| `command` | `string` | Command name |
| `args` | `string` | Arguments after the command |
| `convId` | `number` | Conversation ID |
| `invokedBy` | `{ id, name }` | User who invoked |
| `reply(message)` | `function` | Public reply (everyone sees) |
| `replyEphemeral(message)` | `function` | Reply only the invoker sees |
| `deferReply()` | `function` | Show "thinking..." indicator |
| `followUp(message)` | `function` | Additional message in same block |

---

## Embeds

Send rich cards with title, description, color, fields, images, and more.

```ts
bot.textCommand('stats', async (ctx) => {
  const user = await bot.getUser(ctx.invokedBy.id);

  ctx.reply({
    embed: {
      title: user.name,
      description: `Level ${user.level}`,
      color: '#5865F2',
      thumbnail: `https://habbo.com/avatar/${user.name}`,
      fields: [
        { name: 'Coins', value: user.coin.toLocaleString(), inline: true },
        { name: 'XP', value: user.xp.toLocaleString(), inline: true },
      ],
      footer: { text: 'Mundo Bobba' },
      timestamp: new Date().toISOString(),
    },
  });
});
```

---

## Buttons

Add interactive buttons to your messages.

```ts
bot.textCommand('confirm', (ctx) => {
  ctx.reply({
    content: 'Are you sure?',
    components: [{
      type: 'action_row',
      components: [
        { type: 'button', style: 'success', label: 'Yes', customId: 'yes' },
        { type: 'button', style: 'danger', label: 'No', customId: 'no' },
        { type: 'button', style: 'link', label: 'Docs', url: 'https://mundobobba.com' },
      ],
    }],
  });
});

bot.on('interaction', (ctx) => {
  if (ctx.customId === 'yes') ctx.reply('Confirmed!');
  if (ctx.customId === 'no') ctx.replyEphemeral('Cancelled.');
});
```

### Button styles

| Style | Appearance |
|---|---|
| `'primary'` | Blue (default action) |
| `'secondary'` | Gray (secondary action) |
| `'success'` | Green (confirm) |
| `'danger'` | Red (destructive) |
| `'link'` | Opens a URL (no interaction event) |

---

## Select menus

Add dropdown menus to your messages.

```ts
bot.textCommand('class', (ctx) => {
  ctx.reply({
    embed: { title: 'Choose your class', color: '#FF9900' },
    components: [{
      type: 'action_row',
      components: [{
        type: 'select',
        customId: 'class_select',
        placeholder: 'Select a class...',
        options: [
          { label: 'Warrior', value: 'warrior', emoji: '⚔️' },
          { label: 'Mage', value: 'mage', emoji: '🧙' },
          { label: 'Archer', value: 'archer', emoji: '🏹' },
        ],
      }],
    }],
  });
});

bot.on('interaction', (ctx) => {
  if (ctx.customId === 'class_select') {
    ctx.reply(`You chose: ${ctx.values[0]}`);
  }
});
```

---

## Deferred replies

Show a "thinking..." indicator while processing.

```ts
bot.textCommand('weather', async (ctx) => {
  const deferred = ctx.deferReply();

  const data = await fetchWeatherAPI(ctx.args);

  deferred.editReply({
    embed: {
      title: `Weather in ${data.city}`,
      description: `${data.temp} - ${data.condition}`,
      color: '#87CEEB',
    },
  });
});
```

---

## Ephemeral messages

Send messages only the invoker can see.

```ts
bot.textCommand('secret', (ctx) => {
  ctx.replyEphemeral(`Only you can see this, ${ctx.invokedBy.name}!`);
});
```

---

## Modals / Forms

Open a form dialog and handle submissions.

```ts
bot.textCommand('feedback', (ctx) => {
  bot.showModal(ctx.convId, ctx.invokedBy.id, {
    customId: 'feedback_form',
    title: 'Send Feedback',
    fields: [
      { type: 'text', customId: 'subject', label: 'Subject', required: true },
      { type: 'textarea', customId: 'body', label: 'Message', maxLength: 500 },
    ],
  });
});

bot.on('modalSubmit', (ctx) => {
  if (ctx.customId === 'feedback_form') {
    ctx.reply(`Thanks for your feedback, ${ctx.invokedBy.name}!`);
    console.log(ctx.values); // { subject: '...', body: '...' }
  }
});
```

---

## Collectors

Wait for messages or interactions with a timeout.

```ts
// Wait for a message
bot.textCommand('ask', async (ctx) => {
  ctx.reply('What is your favorite color? (15s)');

  const messages = await bot.awaitMessages(ctx.convId, {
    filter: (m) => m.senderId === ctx.invokedBy.id,
    timeout: 15_000,
    max: 1,
  });

  if (messages.length > 0) {
    ctx.followUp(`You said: ${messages[0].content}`);
  } else {
    ctx.followUp('Time is up!');
  }
});

// Wait for a button click
const clicks = await bot.awaitInteraction(ctx.convId, {
  filter: (i) => i.customId === 'confirm',
  timeout: 30_000,
  max: 1,
});
```

---

## Paginated embeds

Navigate between pages using buttons.

```ts
import { PaginatedEmbed } from '@mundobobba/bot-sdk';

bot.textCommand('rules', (ctx) => {
  const pages = [
    { title: 'Rules — General', description: '1. Be respectful\n2. No spam', color: '#5865F2' },
    { title: 'Rules — Chat', description: '3. Use correct channels\n4. No flood', color: '#5865F2' },
    { title: 'Rules — Trade', description: '5. No scam\n6. Fair prices', color: '#5865F2' },
  ];

  const paginator = new PaginatedEmbed(pages);
  paginator.send(ctx);

  // Handle page navigation
  bot.on('interaction', (i) => paginator.handleInteraction(i));
});
```

---

## Confirm dialogs

Ask yes/no questions with a Promise.

```ts
import { ConfirmDialog } from '@mundobobba/bot-sdk';

bot.textCommand('reset', async (ctx) => {
  const confirmed = await ConfirmDialog.ask(ctx, {
    title: 'Confirm Reset',
    description: 'Are you sure you want to reset your data?',
    color: '#FF0000',
  });

  if (confirmed) ctx.followUp('Data has been reset.');
  else ctx.followUp('Cancelled.');
});

// Required: handle confirm dialog interactions
bot.on('interaction', (ctx) => ConfirmDialog.handleInteraction(ctx.customId));
```

---

## Presence

Set the bot's status and activity.

```ts
bot.setPresence({
  status: 'online',
  activity: { type: 'listening', name: 'Lo-fi Hip Hop' },
});
```

| Status | Description |
|---|---|
| `'online'` | Green indicator |
| `'idle'` | Yellow indicator |
| `'dnd'` | Red indicator |

| Activity type | Display |
|---|---|
| `'playing'` | Playing [name] |
| `'listening'` | Listening to [name] |
| `'watching'` | Watching [name] |
| `'streaming'` | Streaming [name] |

---

## Event listeners

```ts
// Any chat message in conversations with the bot
bot.on('message', (msg) => {
  console.log(`${msg.senderName}: ${msg.content}`);
});

// User reacted to a message
bot.on('reaction', (data) => {
  console.log(`${data.userName} reacted with ${data.emoji}`);
});

// User joined a conversation
bot.on('memberJoin', (data) => {
  bot.sendMessage(data.convId, `Welcome, ${data.userName}!`);
});

// User left a conversation
bot.on('memberLeave', (data) => {
  bot.sendMessage(data.convId, `${data.userName} left.`);
});

// Someone mentioned @BotName
bot.on('mention', (data) => {
  bot.sendMessage(data.convId, `You called, ${data.userName}?`);
});

// A message was edited
bot.on('messageEdit', (data) => {
  console.log(`Message ${data.messageId} was edited.`);
});

// A message was deleted
bot.on('messageDelete', (data) => {
  console.log(`Message ${data.messageId} was deleted.`);
});
```

---

## Platform API

Query platform data directly from your bot.

```ts
// Get user info
const user = await bot.getUser(userId);
console.log(user.name, user.level, user.coin);

// Get user permissions
const perms = await bot.getPermissions(userId);
if (!perms.isAdmin) ctx.replyEphemeral('No permission.');

// Get conversation info
const conv = await bot.getConversation(convId);
console.log(conv.name, conv.memberCount, conv.members);

// Get user profile (full)
const profile = await bot.api('getUserProfile', { name: 'PlayerOne' });
```

---

## Scheduled messages

Send messages at a specific time.

```ts
await bot.schedule(convId, 'Good morning!', new Date('2026-04-06T08:00:00Z'));

await bot.schedule(convId, {
  embed: { title: 'Reminder', description: 'Meeting starts now!', color: '#FF0000' },
}, new Date(Date.now() + 60_000)); // 1 minute from now
```

---

## Music bot

Build a music bot with audio streaming.

```ts
import { MundobobbaBot, AudioStreamer, YouTubeResolver, QueueService } from '@mundobobba/bot-sdk';

const TOKEN = process.env.BOT_TOKEN!;
const bot = new MundobobbaBot({ token: TOKEN, prefix: '!' });
const streamer = new AudioStreamer({ token: TOKEN });
const youtube = new YouTubeResolver();
const queue = new QueueService();

bot.textCommand('play', async (ctx) => {
  const result = await youtube.search(ctx.args);
  if (!result) { ctx.reply('Not found.'); return; }

  if (streamer.isStreaming(ctx.convId)) {
    queue.add(ctx.convId, { ...result, addedBy: ctx.invokedBy.name });
    ctx.reply(`Added to queue: ${result.title}`);
    return;
  }

  ctx.reply(`Now playing: ${result.title}`);
  bot.broadcastMusicPlaying(ctx.convId, result);

  await streamer.start(ctx.convId, result.url, (convId) => {
    const next = queue.shift(convId);
    if (!next) { bot.broadcastMusicStopped(convId); return; }
    bot.broadcastMusicPlaying(convId, next);
    streamer.start(convId, next.url, () => {});
  });
});

bot.textCommand('stop', (ctx) => {
  streamer.stop(ctx.convId);
  queue.clear(ctx.convId);
  bot.broadcastMusicStopped(ctx.convId);
  ctx.reply('Stopped.');
});
```

---

## Rate limiter

Prevent command spam.

```ts
import { RateLimiter } from '@mundobobba/bot-sdk';

// Per user (default)
const limiter = new RateLimiter();

// Per conversation
const limiter = new RateLimiter({ mode: 'conversation' });

// Custom cooldowns
const limiter = new RateLimiter({
  cooldowns: { play: 5000, skip: 2000 },
});

bot.textCommand('play', (ctx) => {
  const result = limiter.check(ctx.invokedBy.id, 'play');
  if (!result.allowed) {
    ctx.replyEphemeral(`Wait ${result.remainingSec}s.`);
    return;
  }
  // ... handle command
});
```

---

## Edit and delete messages

```ts
// Edit a bot message
bot.editMessage('bot_123', convId, 'Updated content');

// Delete a bot message
bot.deleteMessage('bot_123', convId);
```

---

## Slash commands

Register commands that appear in the platform's command picker.

```ts
bot.on('ready', () => {
  bot.registerCommands([
    {
      name: 'play',
      description: 'Play a song',
      options: [{ name: 'query', description: 'Song name or URL', required: true }],
    },
    { name: 'stop', description: 'Stop playback' },
  ]);
});

bot.on('command', (ctx) => {
  if (ctx.command === 'play') {
    // Handle slash command
  }
});
```

---

## API Reference

### MundobobbaBot

| Method | Description |
|---|---|
| `textCommand(name, handler)` | Register a text command |
| `useBuiltins()` | Register ping, uptime, help |
| `registerCommands(commands)` | Register slash commands |
| `sendMessage(convId, message)` | Send public message |
| `sendEphemeral(convId, message, userId)` | Send ephemeral message |
| `editMessage(msgId, convId, message)` | Edit bot message |
| `deleteMessage(msgId, convId)` | Delete bot message |
| `showModal(convId, userId, modal)` | Open modal dialog |
| `setPresence(presence)` | Set bot status |
| `broadcastMusicPlaying(convId, info)` | Broadcast now playing |
| `broadcastMusicStopped(convId)` | Broadcast music stopped |
| `broadcastQueueUpdated(convId, queue)` | Update queue UI |
| `api(method, params)` | Call platform API |
| `getUser(userId)` | Get user info |
| `getConversation(convId)` | Get conversation info |
| `getPermissions(userId)` | Get user permissions |
| `schedule(convId, message, date)` | Schedule a message |
| `awaitMessages(convId, options)` | Wait for messages |
| `awaitInteraction(convId, options)` | Wait for interactions |

### Events

| Event | Payload | Description |
|---|---|---|
| `ready` | — | Bot connected |
| `command` | `CommandContext` | Slash command invoked |
| `message` | `ChatMessage` | Any chat message |
| `interaction` | `ComponentInteraction` | Button/select interaction |
| `reaction` | `ReactionEvent` | Message reaction |
| `memberJoin` | `MemberEvent` | User joined |
| `memberLeave` | `MemberEvent` | User left |
| `mention` | `{ userId, userName, convId, content }` | Bot was mentioned |
| `modalSubmit` | `ModalSubmitEvent` | Modal form submitted |
| `messageEdit` | `{ messageId, content, convId }` | Message edited |
| `messageDelete` | `{ messageId, convId }` | Message deleted |
| `disconnect` | — | WebSocket disconnected |
| `error` | `Error` | WebSocket error |

---

## License

MIT