# 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/<room>.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 <question>` | Ask anything, using recent chat as context |
| `@<name> [args]` | Run the script command `cmd/<name>` (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 `@<name>` 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)
```
