# xmppcb A lean XMPP AI chatbot in C. It joins MUC rooms, logs messages, answers `@bot` commands via an OpenAI-compatible LLM HTTP API, and runs `@scriptname` commands backed by local scripts. Inspired by the suckless spirit: one source file, one library dependency (OpenSSL), fixed buffers, no runtime config parser. There is **no end-to-end OMEMO/OTR encryption**. Transport is still encrypted: STARTTLS for the XMPP connection, HTTPS for the LLM API. ## Build Needs a C compiler and OpenSSL development headers (`apt install libssl-dev`). ```sh make ``` The makefile copies `config.def.h` to `config.h` on first build. Edit `config.h` (not the `.def`) to set your server, rooms and model. ## Configure Non-secret settings live in `config.h`. Recompile after changing it. Secrets are read from the environment, never stored on disk by the bot. Export them by hand: ```sh export XMPP_PASSWORD='the-bot-account-password' export LLM_API_KEY='sk-...' ``` Or use the bundled `start` script, which sets both variables and then execs the bot. Copy the template, fill in your secrets, and lock it down so only you can read it: ```sh cp start.sample start chmod 700 start ``` `instruction.md` holds the system prompt; it is read once at startup. On a fresh checkout, copy `instruction.md.sample` to `instruction.md`, then edit it and restart to change the bot's behaviour. ### Choosing the LLM model and provider The bot speaks the OpenAI-compatible `/v1/chat/completions` protocol, so it works with any provider that does — OpenAI, OpenRouter, Together, a local Ollama/llama.cpp server, etc. Four macros in `config.h` select it: | Macro | Purpose | |-------------|--------------------------| | `LLM_HOST` | API hostname | | `LLM_PORT` | API port (usually 443) | | `LLM_PATH` | chat/completions path | | `LLM_MODEL` | model identifier | OpenAI (default): ```c #define LLM_HOST "api.openai.com" #define LLM_PATH "/v1/chat/completions" #define LLM_MODEL "gpt-5.4-mini" ``` OpenRouter: ```c #define LLM_HOST "openrouter.ai" #define LLM_PATH "/api/v1/chat/completions" #define LLM_MODEL "anthropic/claude-sonnet-4-6" ``` Change the macros, run `make` again, and set `LLM_API_KEY` to a key for that provider. The bot sends `max_completion_tokens`; a few non-OpenAI servers still expect the older `max_tokens` field and would need a source change. ## Run Run from the directory containing `instruction.md` and `config.h`: ```sh ./xmppcb ``` If you set up the `start` script above, run `./start` instead — it exports the secrets, `cd`s into this directory and execs the bot. It reconnects automatically after a dropped connection. On an authentication failure it exits with status 1 (no retry loop). Logs go to stderr. Per-room chat history is appended to `history/.log`. ## Usage In a joined room: | Command | Effect | |---|---| | `@bot help` | Show commands | | `@bot summarize [period]` | Summarise the conversation | | `@bot tasks [period]` | Extract action items | | `@bot ` | Ask anything, using recent chat as context | | `@ [args]` | Run the script command `cmd/` (see below) | Period tokens: `1h 1d 2d 1w 2w 1m`. Without one, the last 50 messages are used. ## Script commands Besides the `@bot` builtins, any executable you drop into the `cmd/` directory becomes an `@` command — no recompile, no restart. Example, `cmd/weather`: ```sh #!/bin/sh # @weather [location] - current weather via wttr.in loc=$(printf '%s' "${1:-Krakow}" | tr ' ' '+') curl -fsS --max-time 8 "https://wttr.in/${loc}?format=3" ``` ```sh chmod +x cmd/weather ``` Then in any joined room `@weather Wroclaw` makes the bot run the script, capture its output (stdout and stderr) and post it back. `@bot help` lists the script commands it finds. How it works and the safety boundaries: - The text after the command name is passed as **one argv element** — `@weather New York` runs `cmd/weather "New York"`. The bot never hands it to a shell, so there is no command injection; quote `"$1"` inside your script and you are safe. - Command names allow only letters, digits, `-` and `_`, so they cannot escape the `cmd/` directory. - A script is SIGKILLed after `CMD_TIMEOUT_SEC` seconds (default 10); the bot is single-threaded, so it is paused meanwhile. - Output is capped at ~4 KB; longer output is truncated with `...[cut]`. - Unknown `@names` are ignored silently, so the bot does not react to `@mentions` of people. Security: anyone in a joined room can run anything in `cmd/`. The command set is whatever lives in that directory — keep it curated, and do not let scripts print secrets. `CMD_DIR` and `CMD_TIMEOUT_SEC` are in `config.h`. Two example scripts ship in `cmd/`: `weather` and `uptime`. ## Run under a supervisor The bot does not daemonise and does not restart itself after it exits. Use a process supervisor so it restarts on crash and comes up at boot. Each example runs the `start` script, so no secrets land in the supervisor config. Replace `/home/xmppcb/xmppcb` and the `xmppcb` user/group with yours. The scripts below are example templates, not tested on every release — check them against your OS version before relying on them. ### FreeBSD (rc.d) `/usr/local/etc/rc.d/xmppcb`: ```sh #!/bin/sh # PROVIDE: xmppcb # REQUIRE: NETWORKING # KEYWORD: shutdown . /etc/rc.subr name=xmppcb rcvar=xmppcb_enable load_rc_config $name : ${xmppcb_enable:="NO"} : ${xmppcb_user:="xmppcb"} : ${xmppcb_dir:="/home/xmppcb/xmppcb"} pidfile="/var/run/${name}.pid" command="/usr/sbin/daemon" command_args="-r -P ${pidfile} -u ${xmppcb_user} ${xmppcb_dir}/start" run_rc_command "$1" ``` `chmod +x` it, then `sysrc xmppcb_enable=YES` and `service xmppcb start`. `daemon -r` restarts the bot if it dies. ### OpenBSD (rc.d) `/etc/rc.d/xmppcb`: ```sh #!/bin/ksh daemon="/home/xmppcb/xmppcb/start" daemon_user="_xmppcb" . /etc/rc.d/rc.subr rc_bg=YES rc_cmd $1 ``` `chmod +x` it, then `rcctl enable xmppcb` and `rcctl start xmppcb`. OpenBSD's rc.d starts the bot at boot but does not respawn it on crash; pair it with a cron check or a supervisor if you need that. ### OpenRC `/etc/init.d/xmppcb`: ```sh #!/sbin/openrc-run description="xmppcb XMPP AI bot" command="/home/xmppcb/xmppcb/start" command_user="xmppcb:xmppcb" supervisor="supervise-daemon" respawn_delay=10 depend() { need net } ``` `chmod +x` it, then `rc-update add xmppcb default` and `rc-service xmppcb start`. `supervise-daemon` handles the restart. ### sysvinit sysvinit respawns processes listed in `/etc/inittab`. Add one line: ``` xb:2345:respawn:/bin/su -s /bin/sh -c /home/xmppcb/xmppcb/start xmppcb ``` Then run `telinit q` to reload. The `respawn` action gives you both boot start and restart-on-crash without a separate init script. ### runit `/etc/sv/xmppcb/run`: ```sh #!/bin/sh exec chpst -u xmppcb:xmppcb /home/xmppcb/xmppcb/start ``` `chmod +x` it, then link the service into the scan directory: ```sh ln -s /etc/sv/xmppcb /var/service/ ``` (some distributions use `/etc/service` or `/run/runit/service`). runit supervises and restarts the bot automatically. ## Notes and limitations - The bot only sees messages sent while it is in the room. - LLM calls and script commands are blocking: the bot pauses while a reply or a `cmd/` script runs. Adequate for a low-traffic bot. - The XML parser is intentionally narrow — it understands only the XMPP stanzas this bot needs, not arbitrary XML. - Resource use is small: ~37 KB binary, a few MB RSS. ## Layout ``` xmppcb/ ├── xmppcb.c all logic: TCP, TLS, XML, SASL, MUC, HTTP, LLM ├── config.def.h config template (committed) ├── config.h your config (copied from .def, gitignored) ├── instruction.md.sample system prompt template (committed) ├── start.sample run-script template (committed) ├── cmd/ @scriptname command scripts (weather, uptime, fx) ├── makefile └── history/ per-room chat logs (generated, gitignored) ```