Discord bot that relays Xonotic chat moderation events via RCON — auto-kicks/bans on configurable bad words, posts mod alerts, and exposes an HTTP API for ban history.
  • Python 84.6%
  • Nix 14.4%
  • Dockerfile 1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
DerGrumpf 97ad10bfa0
Some checks failed
ci/woodpecker/push/release Pipeline was successful
ci/woodpecker/push/pytest Pipeline failed
ci/woodpecker/push/docker-build Pipeline was successful
Added: Debug Logging
2026-09-28 09:30:09 +02:00
.woodpecker Added: Cliff for Changelog generation 2026-09-27 19:17:20 +02:00
dashboards Added Dashboards exported from Grafana 2026-09-25 10:33:46 +00:00
nix Fixed: Logout Message triggered to false signal 2026-09-27 19:01:23 +02:00
src/modnotif Added: Debug Logging 2026-09-28 09:30:09 +02:00
tests Ranned: ruff 2026-09-26 19:30:18 +02:00
.env.example Added .env.example 2026-09-25 19:40:00 +02:00
.gitignore Added: Entry to .gitignore 2026-09-27 19:22:53 +02:00
badwords.toml Fixed & Removed: Critical bug in calculating incremental ban time from volatile player_uid -> player gets matched by there lowercase username; Added: max_duration with 1 Week as base value 2026-09-26 12:13:52 +02:00
cliff.toml Added: Cliff for Changelog generation 2026-09-27 19:17:20 +02:00
docker-compose.yml Added: docker-compose.yml, README.md 2026-09-27 20:13:32 +02:00
Dockerfile Fixed: Enabled MD4 in container 2026-09-27 21:42:28 +02:00
flake.lock Updated Structure 2026-08-14 22:01:41 +02:00
flake.nix Added: Docker builder and Pipeline 2026-09-27 12:26:31 +02:00
LICENSE Updated Repo 2026-09-25 19:23:00 +02:00
pyproject.toml Added: Entry to .gitignore 2026-09-27 19:26:41 +02:00
README.md Added: rcon_mode is now a attribute for a given server 2026-09-27 23:27:49 +02:00
servers.toml Added: Debug Logging 2026-09-28 09:30:09 +02:00

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