- Python 84.6%
- Nix 14.4%
- Dockerfile 1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .woodpecker | ||
| dashboards | ||
| nix | ||
| src/modnotif | ||
| tests | ||
| .env.example | ||
| .gitignore | ||
| badwords.toml | ||
| cliff.toml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| flake.lock | ||
| flake.nix | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| servers.toml | ||
Xonotic-DiscordModNotify
A Discord bot that relays Xonotic chat moderation events via RCON. It watches a Discord channel for IRC-relayed in-game chat, matches messages against a configurable bad-word list, and automatically kicks/bans offending players via RCON — posting an alert to a moderator channel for every action taken. It can also flag messages containing "mod"/"admin" keywords for human review, and exposes an optional HTTP API for querying ban history (e.g. from Grafana).
Nix / Flake usage
Run directly without installing:
nix run .
Or with custom config paths:
nix run . -- --servers-path /path/to/servers.toml --badwords-path /path/to/badwords.toml
For a declarative, persistent deployment (e.g. on NixOS), a module.nix is provided exposing a declarative option set to run the bot as a system service. Import it into your NixOS configuration and configure it there rather than invoking nix run manually.
Environment variables/secrets (see below) still need to be supplied — either via .env in the working directory, real exported environment variables, or systemd LoadCredential= (the bot checks systemd credentials first, falling back to environment variables).
Using as a flake input (NixOS module)
Import the module into a NixOS configuration for a declarative, persistent deployment:
{
inputs.modnotif.url = "git+https://git.cyperpunk.de/dergrumpf/xonotic-discordmodnotify";
outputs = { self, nixpkgs, modnotif, ... }: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
modnotif.nixosModules.default
{
services.xonotic-discordmodnotify = {
enable = true;
modChannelId = 123456789012345678;
chatChannelId = 123456789012345679;
servers = {
example1 = { ip = "127.0.0.1"; port = 26000; };
example2 = { ip = "127.0.0.1"; port = 26001; };
};
badWords = {
example_bad_word = "[*] User {name} was kicked for using a banned word ({duration} min)";
};
discordTokenFile = "/run/secrets/discord-token";
rconPasswordFiles = {
example1 = "/run/secrets/rcon-password-example1";
example2 = "/run/secrets/rcon-password-example2";
};
};
}
];
};
};
}
This runs the bot as a systemd service, generating servers.toml/badwords.toml from the Nix options and wiring secrets in via LoadCredential — no .env file needed in this mode. See nix/module.nix for the full option set (logging, database, API, incremental ban settings, etc.).
Docker usage
Pull the image:
docker pull git.cyperpunk.de/dergrumpf/xonotic-discordmodnotify:latest
Run it, mounting your config files and .env:
docker run -d \
--name modnotif \
--env-file .env \
-v "$(pwd)/servers.toml:/config/servers.toml" \
-v "$(pwd)/badwords.toml:/config/badwords.toml" \
-v "$(pwd)/ban_events.sqlite3:/config/ban_events.sqlite3" \
-p 8090:8090 \
git.cyperpunk.de/dergrumpf/xonotic-discordmodnotify:latest
Or with Docker Compose:
services:
modnotif:
image: git.cyperpunk.de/dergrumpf/xonotic-discordmodnotify:latest
container_name: modnotif
restart: unless-stopped
env_file:
- .env
volumes:
- ./servers.toml:/config/servers.toml
- ./badwords.toml:/config/badwords.toml
- ./ban_events.sqlite3:/config/ban_events.sqlite3 # persistent ban history DB
ports:
- "8090:8090" # only needed if [api] is enabled in servers.toml
docker compose up -d
The container's working directory is /config; servers.toml and badwords.toml are read from there by default.
Building from source
Requires Python 3.14+.
python -m venv .venv
.venv/bin/pip install -e .[dev]
Run the bot:
.venv/bin/modnotif --servers-path servers.toml --badwords-path badwords.toml
Configuration files
servers.toml
Defines Discord channel IDs, Xonotic server topology, and optional subsystems.
[discord]
mod_channel_id = 123456789012345678 # channel that receives mod alerts
chat_channel_id = 123456789012345679 # channel the IRC relay posts chat into
[logging]
enable = true
path = "logfile.txt"
debug = false
[database]
enable = true
path = "ban_events.sqlite3"
[api]
enable = true
host = "0.0.0.0"
port = 8090
[servers.example1]
ip = "127.0.0.1"
port = 26000
rcon_mode = 0
[servers.example2]
ip = "127.0.0.1"
port = 26001
rcon_mode = 2
Each [servers.<name>] block's <name> corresponds to a matching RCON_PASSWORD_<NAME> environment variable (see below). port here is the Xonotic server's RCON port.
rcon_mode selects the RCON authentication protocol used against the Darkplaces engine (Xonotic is Darkplaces-based). Must match the server's rcon_secure cvar.
0 = plain (password sent in cleartext — only safe on trusted/local networks) 1 = time-based (HMAC-MD4, requires synced clocks) 2 = challenge-based (HMAC-MD4, server-issued challenge)
badwords.toml
Defines the bad-word list and kick/ban behavior.
[settings]
kick_duration = 30 # minutes
incremental_bans_enabled = true # doubles the duration each repeat offense, up to max_duration
max_duration = 10080 # minutes, cap for incremental bans (default: 1 week)
[words]
# key = bad word to match (lowercase, matched case-insensitively as a whole word)
# value = kick/ban reason template, sent both via RCON and posted to the mod channel
example_bad_word = "[*] User {name} was kicked for using a banned word ({duration} min)"
HTTP API
When [api] enable = true (or services.xonotic-discordmodnotify.api.enable via the Nix module), the bot exposes a small HTTP API for querying ban history. If API_SHARED_SECRET is set, all endpoints require an X-API-Key header matching it.
| Endpoint | Query params | Description |
|---|---|---|
GET /health |
— | Health check |
GET /events |
player, server, since, limit (default 500) |
List recorded ban events, optionally filtered |
GET /stats/top-words |
limit (default 10) |
Most-triggered bad words |
GET /stats/top-players |
limit (default 10) |
Most-banned players |
Grafana dashboard
A ready-made dashboard is provided in the dashboards/ directory. It queries the API using grafana-infinity-datasource, which must be installed in your Grafana instance. Import the dashboard JSON and point its data source at your running instance's API (e.g. http://<host>:8090).
Environment variables
Set via .env (local development, loaded automatically), real exported environment variables, or systemd LoadCredential= (production).
| Variable | Required | Description |
|---|---|---|
DISCORD_TOKEN |
Yes | Discord bot token — create an application and bot at the Discord Developer Portal |
RCON_PASSWORD_<SERVERNAME> |
Yes, per server | RCON password for each server defined in servers.toml. Pattern: uppercase of the [servers.X] key, e.g. RCON_PASSWORD_EXAMPLE1 for [servers.example1] |
API_SHARED_SECRET |
No | Shared secret required in the X-API-Key header for API requests. Leave unset/empty to run the API without auth |