This guide walks you through creating custom bots for ChatNet. Bots are file-driven plugins that can greet users, respond to messages, call external APIs, and more. No manual SQL is needed — bots are auto-installed when an admin visits the Bots page.
Each bot lives in its own folder under src/bots/ and must contain a bot.php entry point:
src/bots/ my_bot/ bot.php ← required entry pointbot.php must define a class that extends BotBase and return an instance at the end of the file:
<?php class MyBot extends BotBase { // ... } return new MyBot();Every bot must implement these five methods. ChatNet uses them during auto-install to create the bot's user account and database row.
public function getSlug() { return 'my_bot'; } // Unique ID (matches folder name) public function getName() { return 'My Bot'; } // Display name in admin public function getDescription() { return 'Does something'; } // Short description public function getUserName() { return 'my-bot'; } // Bot's system username public function getDisplayName() { return 'My Bot'; } // Bot's display name| Method | Purpose |
|---|---|
getSlug() | Unique identifier stored in cn_bots.bot_type. Must match the folder name. |
getName() | Display name shown in the admin bot list |
getDescription() | Short description shown below the bot name |
getUserName() | Username for the bot's system user account (e.g. my-bot) |
getDisplayName() | First name for the bot's user account, shown in chat messages |
When auto-installed, ChatNet creates a system user (user_type = 5) and a cn_bots row. The bot is disabled by default — an admin must enable it.
Override getConfigFields() to define settings that appear in the admin UI. Config values are stored as JSON in the cn_bots.config column.
public function getConfigFields() { return [ [ 'name' => 'greeting', 'label' => 'Greeting Text', 'type' => 'text', 'default' => 'Hello!', 'help' => 'Message the bot sends', ], [ 'name' => 'max_length', 'label' => 'Max Length', 'type' => 'number', 'default' => 200, 'min' => 1, 'max' => 1000, ], [ 'name' => 'mode', 'label' => 'Response Mode', 'type' => 'select', 'default' => 'auto', 'options' => [ ['value' => 'auto', 'label' => 'Automatic'], ['value' => 'manual', 'label' => 'Manual'], ], ], [ 'name' => 'enabled_feature', 'label' => 'Enable Feature', 'type' => 'toggle', 'default' => false, ], ]; }| Type | Renders As |
|---|---|
text | Single-line text input |
textarea | Multi-line text area |
number | Numeric input (supports min and max) |
select | Dropdown (requires options array of {value, label}) |
toggle | On/off switch |
| Property | Required | Description |
|---|---|---|
name | Yes | Key used to store/retrieve the value |
label | Yes | Label shown in the admin form |
type | Yes | Field type (see table above) |
default | No | Default value used on first install |
help | No | Helper text shown below the field |
options | For select | Array of {value, label} objects |
min / max | For number | Minimum and maximum allowed values |
room_override | No | Set to true to allow per-room overrides |
To allow admins to override specific config fields on a per-room basis, override getRoomOverrideFields():
public function getRoomOverrideFields() { return ['greeting']; // field names that can differ per room }Also set 'room_override' => true on the corresponding fields in getConfigFields() for clarity. Use the getMergedConfig() helper at runtime to get the final config — it applies room-level overrides automatically.
Bots respond to events by overriding hook methods. All hooks receive a $context array containing event data and the bot's database rows.
Called when a user joins a room. Use this for welcome messages or join notifications.
| Context Key | Type |
|---|---|
user_id | int — the user who joined |
room_id | int — the room they joined |
bot_row | array — the bot's cn_bots row |
bot_room_row | array|null — the cn_bot_rooms row (if any) |
public function onUserJoinedRoom($context) { $config = $this->getMergedConfig($context['bot_row'], $context['bot_room_row']); $group = $this->getFirstActiveGroup($context['room_id']); if ($group) { $this->sendMessage($context['bot_row']['user_id'], $config['greeting'], $group['id']); } }Called synchronously after a message is saved. This runs inline during the message save flow, so keep it fast. Return null to do nothing, or return an async signal for slow work.
| Context Key | Type |
|---|---|
message | string — the message text |
group_id | int — the chat group ID |
room_id | int — the chat room ID |
sender_id | int — the user who sent the message |
message_type | int — message type (1 = text) |
bot_row | array — the bot's cn_bots row |
bot_room_row | array|null — the cn_bot_rooms row (if any) |
public function onMessageSent($context) { // Only handle text messages if ($context['message_type'] != 1) { return null; } if (stripos($context['message'], '!mybot') === false) { return null; // not triggered } // Option A: respond immediately (only for fast operations) $this->sendMessage($context['bot_row']['user_id'], 'Got it!', $context['group_id']); return null; // Option B: signal async processing (for slow work like API calls) return [ 'bot_slug' => $this->getSlug(), 'needs_async' => true, ]; }Called via a separate AJAX request when onMessageSent() returned needs_async => true. Use this for slow operations like AI calls, external APIs, or heavy processing.
| Context Key | Type |
|---|---|
message | string — the original message text |
group_id | int — the chat group ID |
room_id | int — the chat room ID |
bot_row | array — the bot's cn_bots row |
bot_room_row | array|null — the cn_bot_rooms row (if any) |
public function onAsyncRespond($context) { $result = file_get_contents('https://api.example.com/data'); if (!$result) { return false; } $this->sendMessage($context['bot_row']['user_id'], $result, $context['group_id']); return true; }BotBase provides these helpers that you can use in your bot:
| Method | Description |
|---|---|
$this->sendMessage($bot_user_id, $message, $group_id) | Send a chat message as the bot |
$this->getMergedConfig($bot_row, $bot_room_row) | Get config with per-room overrides applied |
$this->getFirstActiveGroup($room_id) | Get the first active chat group in a room |
You also have access to ChatNet's service container via app() — for example app('db') for database queries, app('auth') for user lookups.
Here is a minimal bot that responds when users type !ping:
<?php // src/bots/ping/bot.php class PingBot extends BotBase { public function getSlug() { return 'ping'; } public function getName() { return 'Ping Bot'; } public function getDescription() { return 'Replies with Pong when a user types !ping'; } public function getUserName() { return 'ping-bot'; } public function getDisplayName() { return 'Ping Bot'; } public function getConfigFields() { return [ [ 'name' => 'response', 'label' => 'Response Message', 'type' => 'text', 'default' => 'Pong!', 'help' => 'What the bot replies with', ], ]; } public function onMessageSent($context) { if ($context['message_type'] != 1) { return null; } if (stripos($context['message'], '!ping') === false) { return null; } $config = $this->getMergedConfig($context['bot_row'], $context['bot_room_row']); $this->sendMessage($context['bot_row']['user_id'], $config['response'], $context['group_id']); return null; } } return new PingBot();To install: Create the file at src/bots/ping/bot.php, then visit Admin > Bots — the bot will auto-install and appear in the list. Enable it and set the room mode to start using it.
src/bots/your_bot/bot.php extending BotBaseautoInstall() creates the user account and cn_bots row (disabled by default)onUserJoinedRoom, onMessageSent) to enabled bots in matching roomsonMessageSent returns needs_async => true, the client fires a follow-up AJAX call to onAsyncRespondonMessageSent() fast — it runs synchronously during message save. Use onAsyncRespond() for anything slow.message_type in onMessageSent() to avoid reacting to images, files, etc.getMergedConfig() instead of reading bot_row['config'] directly — it handles room overrides.