aboutsummaryrefslogtreecommitdiff

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).

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:

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:

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):

#define LLM_HOST  "api.openai.com"
#define LLM_PATH  "/v1/chat/completions"
#define LLM_MODEL "gpt-5.4-mini"

OpenRouter:

#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:

./xmppcb

If you set up the start script above, run ./start instead — it exports the secrets, cds 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/<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:

#!/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"
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:

#!/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:

#!/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:

#!/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:

#!/bin/sh
exec chpst -u xmppcb:xmppcb /home/xmppcb/xmppcb/start

chmod +x it, then link the service into the scan directory:

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)