# axXezBot — TeamSpeak Server Bot

A server-side automation bot for TeamSpeak (3 and 6), built with .NET 9. It connects to a TeamSpeak server via the **ServerQuery interface** (SSH by default, plain TCP also supported) and provides a module-based system to automate and extend server behaviour.

---

## How it works

The bot maintains a live, tracked state of the server — channels, clients, server groups and channel groups — and watches for changes in real time. When something happens on the server (a client joins, a channel is edited, a text message is sent, etc.) the bot raises typed events and forwards them to all active modules. Each module call is sandboxed — an unhandled exception in one module is logged and skipped without affecting the others.

Modules can react to these events, send commands back to the server via the full ServerQuery API, and expose their own chat commands that users can trigger by sending text messages to the bot.

---

## Features

- SSH and plain TCP transport — SSH is the default and recommended; required for TeamSpeak 6
- Full ServerQuery API wrapper — channels, clients, groups, permissions, bans, API keys, tokens and more
- Live state tracking — in-memory cache of channels, clients and groups, updated on every server event and via configurable polling intervals
- UDP voice connection — full TS3 voice protocol implementation with crypto handshake, packet encryption, auto-reconnect, and real-time Opus audio streaming; modules can create voice connections to join channels and stream audio
- Module system — event-driven architecture with sandboxed module execution; built-in modules included out of the box
- Chat command handler — modules can optionally register `!commands` via `[Command]` with per-command role requirements (`Admin`, `Moderator`, `User`, `Guest`) and scope filtering (private, channel, server chat)
- Plugin loading — drop any `.NET` DLL into the `plugins/` folder to add custom modules; plugins can be loaded and unloaded at runtime without restarting the bot
- Auto-reconnect — automatically reconnects and restores full bot state if the ServerQuery connection drops
- Connection stability — built-in keep-alive, response timeout detection, and a fixed-rate send queue keep the bot running reliably without flooding the server
- Per-module JSON config files — settings are automatically serialized/deserialized
- File logging with daily log rotation

---

## Requirements

- .NET 9 Runtime (or use the self-contained single-file publish — not required when using the Docker image)
- A TeamSpeak server (3 or 6) with ServerQuery access enabled — SSH query port `10022` (default) or plain TCP port `10011`
- **Music module only:** `ffmpeg` and `yt-dlp` installed and available on PATH

---

## Quick Start

1. **Get your ServerQuery credentials.** The ServerQuery admin password is printed to the server console on first startup. The default login name is `serveradmin`. If you lost the password, restart the server with the startup parameter `serveradmin_password=yournewpassword` to reset it.

2. **Run the bot for the first time** to generate the default config:
   ```bash
   docker run --rm -v ./data:/app axzeu/axxezbot:latest
   ```
   The bot will create `data/settings/bot.json` with default values and exit immediately.

3. **Edit `data/settings/bot.json`** and fill in at minimum:
   ```json
   {
     "IP": "your-ts-server-ip",
     "Password": "your-serverquery-password"
   }
   ```

4. **Start the bot:**
   ```bash
   docker run -d --name axxezbot --restart unless-stopped -v ./data:/app axzeu/axxezbot:latest
   ```

5. **Enable modules** by editing `bot.json` and adding module names to `ActiveModules`, or use the built-in bot commands via private message in TeamSpeak.

For environment variable configuration, custom volumes, and multi-container setups see the [Docker](#docker) section.

---

## Admin Commands

Commands are sent to the bot via TeamSpeak chat. Each command defines which scopes it accepts (private, channel, or server chat) — by default commands only respond to **private messages**. All commands in this section require the Admin role except `!help`.

Use `?` as a prefix instead of `!` to get help for any command — e.g. `?bot` lists all `!bot` sub-commands, `?bot move` shows usage for `!bot move`.

### General

| Command | Description |
|---|---|
| `!help` | Lists all top-level commands (commands the invoker has no access to are hidden) |
| `!help [command]` | Shows extended help for a command or sub-command group — e.g. `!help bot` |

### Bot control — `!bot`

| Command | Description |
|---|---|
| `!bot status` | Shows running state, connected clients/channels/groups counts, loaded modules and plugins, and server version |
| `!bot stop` | Stops the bot process immediately |
| `!bot reconnect` | Force-disconnects the ServerQuery connection and lets auto-reconnect restore it |
| `!bot move <channel> [-save]` | Moves the bot to the named channel. Pass `-save` to persist as the new default channel in config |
| `!bot nick <name> [-save]` | Changes the bot's nickname. Pass `-save` to persist in config |
| `!bot updaterate` | Shows the current channel and client watcher intervals |
| `!bot updaterate <channel\|client> <ms> [-save]` | Sets the watcher interval in milliseconds (0 to disable). Pass `-save` to persist in config |

### Modules — `!module`

| Command | Description |
|---|---|
| `!module list` | Lists all available modules and whether they are loaded |
| `!module load <name>` | Loads a module by name or full name |
| `!module unload <name>` | Unloads a module by name or full name |
| `!module error <name>` | Shows the full error of a faulted module |

### Plugins — `!plugin`

| Command | Description |
|---|---|
| `!plugin enable` | Enables plugin loading and immediately rescans the `plugins/` folder |
| `!plugin disable` | Disables plugin loading and unloads all plugin-sourced modules |
| `!plugin reload` | Rescans the `plugins/` folder for new or removed DLLs |

### Roles

#### `!admin`

| Command | Description |
|---|---|
| `!admin list` | Lists all entries in the `Admins` role with online/offline status |
| `!admin add <entry>` | Adds an entry to the `Admins` role — accepts client name (resolved to UID), UID, server group ID or name |
| `!admin remove <entry>` | Removes an entry from the `Admins` role — accepts client name, UID, server group ID or name |

#### `!mod`

| Command | Description |
|---|---|
| `!mod list` | Lists all entries in the `Moderators` role |
| `!mod add <entry>` | Adds an entry to the `Moderators` role — accepts client name (resolved to UID), UID, server group ID or name |
| `!mod remove <entry>` | Removes an entry from the `Moderators` role — accepts client name, UID, server group ID or name |

#### `!user`

| Command | Description |
|---|---|
| `!user list` | Lists all entries in the `Users` role |
| `!user add <entry>` | Adds an entry to the `Users` role — accepts client name (resolved to UID), UID, server group ID or name |
| `!user remove <entry>` | Removes an entry from the `Users` role — accepts client name, UID, server group ID or name |

---

## Built-in Modules

### Greetings
Sends a configurable welcome message when a client joins the server or a specific channel. Admins receive a separate bot status message on connect (version, active module count, server info). Clients can opt out with `!greeting stop` and back in with `!greeting start`.

**Config options:** greet admins toggle, greet clients toggle, target channel, message text, greet-on-connect toggle, greet-in-every-channel toggle, ignored users list.

**Commands:** `!greeting stop`, `!greeting start`, `!greeting status`, `!greeting channel`, `!greeting message`, `!greeting toggle admin`, `!greeting toggle clients`, `!greeting toggle connect`, `!greeting toggle everywhere`

---

### Auto AFK Switch
Monitors client idle time and automatically moves inactive clients to a configurable AFK channel. Distinguishes between fully muted, mic-muted and unmuted clients with separate idle time thresholds. Excluded channels and server groups can be configured.

**Config options:** AFK channel name or ID, idle thresholds (full muted / mic muted / unmuted), excluded channels, excluded server groups. Channel and group references accept names or IDs.

**Commands:** `!afk status`, `!afk channel`, `!afk time fullmuted`, `!afk time muted`, `!afk time unmuted`, `!afk exclude channel add`, `!afk exclude channel remove`, `!afk exclude group add`, `!afk exclude group remove`

---

### User Channels
Creates temporary personal channels on demand. When a client joins a designated trigger channel, the bot creates a new channel named after that client and moves them into it. The channel is automatically cleaned up after a configurable delay when empty. Existing user channels are detected and reused across restarts.

**Config options:** parent channel name, channel group name, trigger channel name/description, delete delay.

**Commands:** `!uchannel status`, `!uchannel delay`

---

### Team Channels
Automatically manages a set of numbered sub-channels under a parent channel. Keeps a minimum number of empty channels available and creates more as they fill up, up to a configurable maximum. Removes excess empty channels when traffic drops. Channel numbering is kept consistent.

**Config options:** min/max sub-channels, name pattern (e.g. `Team $1`), delete delay, multiple channel groups supported.

**Commands:** `!tc list`, `!tc add`, `!tc remove`, `!tc delay`

---

### Exclusive Groups
Enforces mutually exclusive server group rules. If a client holds more than one group from a configured exclusive set, all but the highest-priority one are removed automatically. Runs on startup and on every server group change.

**Config options:** mutually exclusive group sets — each entry accepts a server group name or ID.

**Commands:** `!xgroups status`

---

### Music
Creates voice-connected music bots that join TeamSpeak channels and stream audio. Multiple bots can run simultaneously. Supports YouTube URLs (resolved via yt-dlp) and any direct stream URL (decoded via ffmpeg). Bot configurations are persisted and automatically restored after reconnects. Bots are identified by nickname (which must be unique on the server) and track their current channel — if moved by an admin, the new channel is saved automatically. Each bot can have its own TS3 identity and volume level, or inherit module-level defaults.

**Requirements:** `ffmpeg` and `yt-dlp` must be installed and available on PATH.

**Config options:** minimum identity security level (default 8, auto-raised to server requirement on connect), shared identity (auto-generated on first load), per-bot channel, nickname, identity, and volume. The `YtDlpArgs` list is forwarded to every yt-dlp invocation — use it for cookies, rate limits, proxies, or any other yt-dlp flag.

#### YouTube authentication

YouTube blocks unauthenticated yt-dlp requests with *"Sign in to confirm you're not a bot."* The fix is to export a `cookies.txt` file from a logged-in browser and point yt-dlp at it.

1. **Export cookies** — on any machine where you are logged into YouTube in Chrome, Edge, or Firefox, run yt-dlp locally to extract the cookies into a file:
   ```bash
   yt-dlp --cookies-from-browser chrome --cookies cookies.txt --skip-download --no-playlist "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
   ```
   Replace `chrome` with `firefox`, `edge`, or another supported browser if needed. The video URL is just a trigger — yt-dlp writes the cookies immediately and the `--skip-download` flag prevents any actual download.

2. **Place the file** in your data volume. If you mount `./data:/app`, save it as `./data/cookies.txt` — it is accessible inside the container at `/app/cookies.txt`.

3. **Configure the module** — open `settings/module_axXez.Music.json` and add the path to `YtDlpArgs`:
   ```json
   {
     "YtDlpArgs": ["--cookies", "cookies.txt"]
   }
   ```

> Cookies expire when your YouTube session ends. If requests start failing again, re-export and replace the file.

**Management commands (Admin):**

| Command | Description |
|---|---|
| `!music create <name> [channel] [ownIdentity] [security]` | Creates a music bot with the given nickname in the given channel (or your current channel). Pass `true` for `ownIdentity` to generate a dedicated TS3 identity. Optionally set a minimum security level for that identity |
| `!music remove <name>` | Removes the music bot with the given nickname and deletes its config |
| `!music name <bot> <newName>` | Renames a bot. Takes effect on next reconnect |
| `!music list` | Lists all active bots, their current channel, and queue sizes |

**Playback commands (send in any channel — the bot whose connection received the command responds):**

| Command | Role | Description |
|---|---|---|
| `!play <url> [list=true]` | Guest | Adds a URL to the queue and starts playback if idle. YouTube URLs and direct stream URLs are supported. Pass `list=true` to enqueue an entire playlist — all track URLs are resolved immediately and added to the queue |
| `!skip` | Guest | Skips the current track |
| `!pause` | Guest | Pauses playback |
| `!resume` | Guest | Resumes paused playback |
| `!playlist` | Guest | Shows the tracks currently waiting in the queue |
| `!vol [0–200]` | Guest | Gets or sets the playback volume as a percentage. Persisted per bot |
| `!stop` | Moderator | Stops playback and clears the entire queue |

---

## Configuration

All config files live in the `settings/` folder. Missing files are created automatically with default values on startup. The bot exits if required settings (such as the query password) are unset after loading the config and applying environment variables.

### Main Settings

Stored in `settings/bot.json`. Created automatically on first run.

| Setting | Env var | Default | Description |
|---|---|---|---|
| `IP` | `TS_IP` | `"localhost"` | Hostname or IP of the TeamSpeak server |
| `Port` | `TS_PORT` | `10022` | ServerQuery port — `10022` for SSH (default), `10011` for plain TCP |
| `Transport` | `TS_TRANSPORT` | `SSH` | Transport type: `SSH` (recommended) or `TCP`. Use SSH whenever the bot runs on a different host than the TeamSpeak server. Plain TCP sends credentials in cleartext and should only be used on localhost. |
| `LoginName` | `TS_LOGIN` | `"serveradmin"` | ServerQuery login name |
| `Password` | `TS_PASSWORD` | `null` | ServerQuery password (printed to server console on first startup) |
| `ServerID` | `TS_SERVER_ID` | `1` | Virtual server ID to use |
| `Name` | `TS_NAME` | `null` | Display name the bot will use on the server (falls back to `axXezBot`) |
| `DefaultChannel` | `TS_DEFAULT_CHANNEL` | `null` | Name of the channel the bot moves into on startup |
| `AutoDisableFloodProtection` | `TS_AUTO_DISABLE_FLOOD_PROTECTION` | `false` | Disables the server-side ServerQuery flood protection on startup (requires sufficient permissions) ¹ |
| `ChannelUpdateInterval` | `TS_CHANNEL_UPDATE_INTERVAL` | `10000` | Interval in ms for polling channel state changes (set to `0` to disable) |
| `ClientUpdateInterval` | `TS_CLIENT_UPDATE_INTERVAL` | `5000` | Interval in ms for polling client state changes (set to `0` to disable) ³ |
| `KeepAliveInterval` | `QUERY_KEEPALIVE_INTERVAL` | `300` | Seconds of idle time before sending a query-level keep-alive. Set to `0` to disable. |
| `CommandsPerSecond` | `QUERY_COMMANDS_PER_SECOND` | `3` | Maximum outgoing ServerQuery commands per second ² |
| `ResponseTimeout` | `QUERY_RESPONSE_TIMEOUT` | `30000` | Milliseconds to wait for a server response before declaring the connection dead and forcing a reconnect |
| `ReconnectDelay` | `QUERY_RECONNECT_DELAY` | `10000` | Delay in ms between reconnect attempts when the ServerQuery connection drops |
| `EnableLogs` | `BOT_ENABLE_LOGS` | `true` | Whether to write log output to files in `logs/` |
| `VerboseLogs` | `BOT_VERBOSE_LOGS` | `false` | When enabled, error logs include full stack traces and verbose log lines are shown |
| `EnablePlugins` | `BOT_ENABLE_PLUGINS` | `true` | Whether to load DLLs from the `plugins/` folder |
| `Admins` | `BOT_ADMINS` | `["Server Admin"]` | Entries that grant the Admin role (see below) |
| `Moderators` | `BOT_MODERATORS` | `[]` | Entries that grant the Moderator role (see below) |
| `Users` | `BOT_USERS` | `["Normal"]` | Entries that grant the User role (see below) |
| `NonGuestsAreUsers` | `BOT_NON_GUESTS_ARE_USERS` | `true` | When enabled, any client not in the server's default group automatically receives the User role, without needing to be listed in `Users` |
| `AllowGuests` | `BOT_ALLOW_GUESTS` | `false` | When enabled, clients in the server's default (Guest) group receive the Guest role and can use `CommandRole.Guest` commands. When disabled, guests cannot use any command. |
| `ActiveModules` | `BOT_ACTIVE_MODULES` | `[]` | List of fully qualified module names to load — managed automatically by the bot |
| `UserCommandCooldown` | `BOT_COMMAND_COOLDOWN` | `500` | Milliseconds between commands from the same user. Set to `0` or negative to disable. |

¹ Use with caution — disabling server-side flood protection removes the last safety net. If `CommandsPerSecond` is set too high or the bot gets spammed heavily, the TS server can crash. If you need higher throughput, consider raising the server-side threshold instead of disabling it entirely.  
² Default of `3` aligns with the TeamSpeak server's default flood protection threshold. We tested up to 18 commands/second against our live TS server running on a Hetzner CX22 (2 vCPU, Ubuntu) with query flood protection disabled — no issues.  
³ We tested it set to `1` ms on the same server — the bot handled it fine. The actual send rate is naturally throttled by the `CommandsPerSecond` setting regardless of how short the interval is.

**Role entries** — each entry in `Admins`, `Moderators`, and `Users` can be any of:
- A **client UID** — e.g. `"5BwnCnKzm7jjCZtJpvLBF8zGnDg="` (visible in TeamSpeak under *Extras → Client Info*)
- A **server group ID** — e.g. `"6"`
- A **server group name** — e.g. `"Server Admin"`

**Roles are hierarchical: Admin > Moderator > User > Guest.** Each higher role includes all lower-tier permissions.

> **Tip:** To allow guests to execute User commands, add the default server group name or ID to `Users` (e.g. `"Guest"`). The role lists take precedence over `NonGuestsAreUsers` and `AllowGuests`.

> **Note on the Guest group:** TeamSpeak assigns the default server group only to clients with no other group assigned — as soon as a client receives any other group, the default group is removed automatically.

Env vars for list settings (`BOT_ADMINS`, `BOT_MODERATORS`, `BOT_USERS`, `BOT_ACTIVE_MODULES`) accept comma-separated values — entries are merged with the JSON values.

### Module config files

Each active module gets its own file at `settings/module_<FullModuleName>.json`. The file is created with default values the first time the module is loaded. All public properties defined on the module class are automatically serialized into it.

---

## Docker

Pre-built images for `linux/amd64` and `linux/arm64` are available on Docker Hub:

```
axzeu/axxezbot:latest
```

The bot stores all runtime data (settings, plugins, logs) in its working directory. Mount a local folder to `/app` to persist configuration across container restarts.

**docker run**

```bash
docker run -d \
  --name axxezbot \
  --restart unless-stopped \
  -v /path/to/data:/app \
  axzeu/axxezbot:latest
```

**docker-compose.yml**

```yaml
services:
  axxezbot:
    image: axzeu/axxezbot:latest
    container_name: axxezbot
    restart: unless-stopped
    environment:
      - TS_IP=localhost
      - TS_PORT=10011
      - TS_LOGIN=serveradmin
      - TS_PASSWORD=your-serverquery-password
      - TS_SERVER_ID=1
    volumes:
      - ./data:/app
```

On first run the `settings/` folder will be created inside the mounted volume. All settings can also be configured via environment variables — see [Main Settings](#main-settings) for the full list.

To add plugins, place your `.NET` DLL files in a `plugins/` folder inside the mounted volume before starting the container.

---

## ServerQuery Reference

While building this bot we captured the ServerQuery communication and documented most commands, parameters and response fields in an unofficial [TeamSpeak ServerQuery Reference](serverquery-reference.md).

---

## TeamSpeak 6 Notes

TeamSpeak 6 supports BBCode only as legacy functionality. During testing we discovered a rendering bug in the TS6 BBCode parser: unknown tags (e.g. `[command]`) and an exclamation mark immediately before a valid tag (e.g. `![B]`) break the rendering of surrounding formatting. As a workaround, outgoing text messages, pokes, and broadcasts are pre-processed to escape these patterns before sending. Channel descriptions go through a shared encoding path together with other properties (passwords, names, etc.) and are not covered by this workaround.

---

## Plugin Development

Any `.NET 9` assembly placed in the `plugins/` folder is loaded at startup and can be reloaded at runtime via `!plugin reload` without restarting the bot. Plugin loading can also be toggled at runtime with `!plugin enable` / `!plugin disable`. Create a class library project targeting `net9.0`, reference the bot assembly, and subclass `Module`:

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="axXez">
      <HintPath>path/to/axXez.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="axXezBot">
      <HintPath>path/to/axXezBot.dll</HintPath>
      <Private>False</Private>
    </Reference>
  </ItemGroup>
</Project>
```

```csharp
[Module(Namespace = "MyPlugin", DisplayName = "My Module")]
public class MyModule : Module
{
    // Config properties are automatically saved/loaded as JSON
    public string MyOption { get; set; } = "default";

    protected override void OnStart() { /* setup */ }

    public override void OnClientJoined(Client client, Channel source, Channel target, EventReason reason)
    {
        Query.SendTextMessage(1, client.ID, "Welcome!");
    }

    // Simple command — just CommandContext
    [Command("hello", Description = "Says hello")]
    public CommandResponse HelloCommand(CommandContext ctx)
    {
        return ctx.Ok("Hello!");
    }

    // Typed parameters — the framework binds args automatically from the method signature.
    // Args can be passed positionally (!greet say Alice 3) or by name (!greet say -times 3 -target Alice).
    // The Parameters string in help is auto-generated as: <target> [times]
    [Command("greet say", Description = "Greets a user", Role = CommandRole.Admin)]
    public CommandResponse GreetSay(CommandContext ctx, [CommandParam("target")] string targetName, int times = 1)
    {
        for (int i = 0; i < times; i++)
            ctx.Reply($"Hello, {targetName}!");
        return ctx.Ok();
    }

    // Sub-commands — use a shared keyword prefix. Typing !greet alone shows this list.
    // !help greet also lists all sub-commands with their descriptions and parameters.
    [Command("greet bye", Description = "Says goodbye to a user", Role = CommandRole.Admin)]
    public CommandResponse GreetBye(CommandContext ctx, [CommandParam("target")] string targetName)
    {
        return ctx.Ok($"Goodbye, {targetName}!");
    }

    // Internal command execution — run any registered command programmatically on behalf of a client.
    // The invoker receives the command's reply just as if they had typed it themselves.
    // Pass suppressResponse: true to handle the output yourself via ctx.Replies instead of sending it automatically.
    [Command("greet help", Description = "Shows help for a client", Role = CommandRole.Admin)]
    public CommandResponse GreetHelp(CommandContext ctx, [CommandParam("target")] string targetName)
    {
        if (!Server.FindClient(targetName, out Client target))
            return ctx.Error($"Client '[B]{targetName}[/B]' not found.");

        Bot.ExecuteCommand("help", invoker: target);
        return ctx.Ok($"Help sent to [B]{target.Name}[/B].");
    }
}
```

Modules have access to:
- `Bot` — config, plugin management, loaded modules
- `Server` — active `ServerConnection` (connection state, reconnect, nickname, channel, update intervals)
- `Clients` / `Channels` / `ServerGroups` / `ChannelGroups` — live tracked state (shorthand for `Server.*`)
- `VoiceConnections` — read-only list of voice connections currently owned by the module
- `Query` — full ServerQuery API
- `CreateVoiceConnection(nickname, identity, options)` / `RemoveVoiceConnection(conn)` — creates or removes a UDP voice connection owned by the module; all owned connections are automatically cleaned up when the module unloads
- `Log(message)` — writes to the global logger
- `Save()` — persists the module's config

---

## Credits

A project by the [axXez](https://axxez.eu) team.

**Contributors**

| | |
|---|---|
| **[Freeman](https://github.com/Freeman370)** | Project founder |
| **[rdX](https://github.com/aXz-rdX)** | Core contributor |

**Used Libraries**

[SSH.NET](https://github.com/sshnet/SSH.NET) — SSH transport library  
[Concentus](https://github.com/lostromb/concentus) — Opus audio codec (managed implementation)  
[ffmpeg](https://ffmpeg.org) — audio decoding and transcoding (external, Music module)  
[yt-dlp](https://github.com/yt-dlp/yt-dlp) — YouTube URL resolution (external, Music module)