# 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. A C reimplementation of the Python `bot.py` in the parent directory, written in the suckless spirit: one source file, one library dependency (OpenSSL), fixed buffers, no runtime config parser. There is **no end-to-end / OMEMO 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: ```sh export XMPP_PASSWORD='the-bot-account-password' export LLM_API_KEY='sk-...' ``` `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 ``` 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. Use a process supervisor so it restarts and starts at boot. systemd (`/etc/systemd/system/xmppcb.service`): ```ini [Unit] Description=xmppcb XMPP AI bot After=network-online.target [Service] WorkingDirectory=/home/youruser/xmpp-bot/xmppcb ExecStart=/home/youruser/xmpp-bot/xmppcb/xmppcb Environment=XMPP_PASSWORD=... Environment=LLM_API_KEY=... Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ``` Prefer an `EnvironmentFile=` with mode 600 over inline secrets. ## 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) ├── instruction.md system prompt in use (gitignored) ├── start.sample run-script template (committed) ├── start run script exporting the secrets (gitignored) ├── cmd/ @scriptname command scripts (weather, uptime, fx) ├── makefile └── history/ per-room chat logs (generated, gitignored) ```