# rustbot A small, modular IRC bot in Rust — **zero dependencies**, with its own IRC protocol layer built to the [modern IRC client spec](https://modern.ircdocs.horse/). ## Structure | File | Layer | Responsibility | |---------------|------------------|-----------------------------------------------------------| | `src/irc.rs` | protocol + I/O | Parse/serialize IRC messages (incl. IRCv3 tags), TCP transport | | `src/bot.rs` | behavior | Registration, event loop, command handling | | `src/main.rs` | wiring | Config from env vars, connect/reconnect supervisor | The layers are deliberately separable: `irc.rs` knows nothing about the bot, and `bot.rs` knows nothing about how the process is launched. ## Build & run ```sh cargo build # or: cargo build --release cargo test # unit tests for the message parser cargo run ``` ## Configuration Settings are layered, each overriding the previous: 1. **Built-in defaults** (`Config::default()` in `src/bot.rs`) 2. **A config file** — `key = value`, see [`rustbot.conf`](rustbot.conf) 3. **Environment variables** — handy for one-off overrides The config file is found via: the **first CLI argument**, else `RUSTBOT_CONFIG`, else `./rustbot.conf` if present. ```sh cargo run # uses ./rustbot.conf cargo run -- /etc/rustbot.conf # explicit path RUSTBOT_CONFIG=/etc/rustbot.conf cargo run ``` Config file keys: `server`, `port`, `tls`, `nick`, `user`, `realname`, `channels` (comma/space-separated), `prefix`, `password`, `sasl_user`, `sasl_pass`. Whole-line `#`/`;` comments only — inline comments would clash with channel `#`s. Environment overrides: `IRC_SERVER`, `IRC_PORT`, `IRC_TLS`, `IRC_NICK`, `IRC_REALNAME`, `IRC_CHANNELS`, `IRC_PREFIX`, `IRC_PASSWORD`, `IRC_SASL_USER`, `IRC_SASL_PASS`. ```sh IRC_NICK=mybot IRC_CHANNELS='#test' cargo run # override the file for one run ``` > If you put a real `password` in `rustbot.conf`, add it to `.gitignore` so the > secret isn't committed. ## Built-in commands With the default `!` prefix: - `!ping` → `pong` - `!echo ` → echoes text back - `!hello` → greets the sender - `!help` → lists commands Add your own in `Bot::handle_command` in `src/bot.rs`. ## Deployment (systemd) Runs as a system service (`User=debian`), auto-restarts on crash, and starts on boot. The unit is kept in the repo as [`rustbot.service`](rustbot.service). ```sh cargo build --release sudo cp rustbot.service /etc/systemd/system/rustbot.service sudo systemctl daemon-reload sudo systemctl enable --now rustbot # start now + on boot systemctl status rustbot # health journalctl -u rustbot -f # live logs (<< in, >> out) sudo systemctl restart rustbot # after a rebuild ``` One-off overrides without touching `rustbot.conf`: put `IRC_*` lines in `/etc/default/rustbot` (env wins over the config file), then restart. ## IRCv3 The bot negotiates capabilities (`CAP LS 302` → `CAP REQ` → `CAP END`) and enables everything useful the server offers: `message-tags`, `server-time`, `batch`, `echo-message`, `account-tag`, `extended-join`, `multi-prefix`, `away-notify`, `chghost`, `setname`, `userhost-in-names`, `cap-notify`, `labeled-response`. Two of these do real work: - **`server-time` + `batch`** let the bot recognise **replayed history**. On join, InspIRCd replays recent channel lines (wrapped in a `chathistory` batch). Without this the bot re-answered old commands on every reconnect; now such messages are ignored (see `Bot::is_historical`). - **`sasl`** (PLAIN) authenticates to services during negotiation. Set `sasl_user`/`sasl_pass` in the config (or `IRC_SASL_USER`/`IRC_SASL_PASS`). base64 is hand-rolled, so this stays dependency-free. ## TLS Set `tls = true` and the port defaults to 6697. The transport wraps the socket with `native-tls`, which verifies certificates against the **system OpenSSL trust store** — so TLS security fixes arrive via OS updates rather than a vendored crypto crate. Plaintext still works with `tls = false` / `port = 6667`. Plain TCP and TLS share one code path: `Connection` holds a single `Box`, since the single-threaded event loop never needs the old read/write socket split (which TLS can't do anyway). ## Roadmap / natural next steps - **Read timeout + client-sent PING** to detect dead connections faster. - **STARTTLS** (the `tls` cap) for networks without a dedicated TLS port. - Split commands into a registry/trait once there are many of them.