# Senator Sperma (Rust) A Discord bot rebuilt in Rust from the original Python implementation. Music playback via songbird, AI chat via OpenRouter or anything OpenAI compatible (ollama.cpp and such), TTS, web/file search scheduled jobs, per-server triggers, QR codes, media conversion, and misc utilities. Built on poise 0.6 + serenity 0.12 + songbird 0.6, with async SQLite (sqlx 0.9) for persistence. ## Setup ### 1. Clone ```bash git clone https://codeberg.org/salmonik/senator-sperma cd senator-sperma ``` ### 2. System Dependencies The following binaries must be available on `PATH`: - `ffmpeg` — audio decoding/encoding for playback, conversion, and compression - `imagemagick` (`magick`) — image compression and conversion for `/compress` and `/convert` - `qalc` (Qalculate) — used by `/math` to evaluate expressions - `yt-dlp` (optional) — used by `/download` - `piper` model files (optional) — local TTS for `/tts` and `/senator ... tts_vc`; TTS commands fail when unset Unlike the Python version, QR encode/decode is handled by the pure-Rust `qrcode`/`rxing` crates — no `zbar` system library required. ### 3. Build Requires a recent Rust toolchain (edition 2024). ```bash cargo build --release ``` ### 4. Configure Environment Create a `.env` in the project root look at `.env.example` on what to put there On launch, missing variables are reported via tracing (`error!` for required, `warn!` for optional with defaults). The database and schema migrations are created automatically on first run. For local TTS, set `TTS_MODEL_PATH` to a Piper `.onnx` model file (or a directory of models, in which case `TTS_VOICE` selects the model, defaulting to `pl_PL-bass-high`). There is no external TTS fallback: TTS commands fail when the model is missing. ### 5. Run ```bash cargo run --release ``` ## Docker The image builds and runs on Debian trixie, so it does not depend on the host's gcc/glibc version. This is the recommended way to deploy on Debian when developing on a rolling-release distro. ```bash docker compose up -d --build ``` Or without Compose: ```bash docker buildx build --load -t senator-sperma:latest . docker run -d --name senator-sperma --restart unless-stopped \ --env-file .env \ -e TTS_MODEL_PATH=/app/piper \ -e PIPER_ESPEAKNG_DATA_DIRECTORY=/usr/share \ -v "$PWD/data:/app/data" \ -v "$PWD/piper:/app/piper:ro" \ senator-sperma ``` Notes: - `data/` holds the SQLite database and is mounted for persistence. Create it first (`mkdir -p data`) so it is owned by your user rather than root. - `piper/` holds the Piper model and is mounted read-only; the container overrides `TTS_MODEL_PATH` to `/app/piper`, so the absolute host path in `.env` is harmless. Put the `.onnx` and `.onnx.json` files there (the same directory used for local runs). - The espeak-ng data used by Piper is baked into the image at `/usr/share/espeak-ng-data`; the container sets `PIPER_ESPEAKNG_DATA_DIRECTORY=/usr/share`. This overrides the host path in `.env`. - The container runs as uid 1000. Make sure `data/` is writable by that uid. - `TZ` defaults to `Europe/Warsaw` for cron jobs; override it in `compose.yaml` or with `-e TZ=...` if needed. - If Copyparty or a local LLM runs on the host, use the host's LAN IP (for example `http://10.0.0.3:2137/...`) and make sure the service listens on `0.0.0.0` or that LAN IP, not only `127.0.0.1`. `localhost` inside the container is the container itself. - After editing `.env`, recreate the container: `docker compose up -d --force-recreate`. `docker compose restart` does not reload `env_file` values. Verify what the container actually sees with: `docker compose exec senator-sperma env | grep -E 'LLM_API_URL|TTS_MODEL_PATH|PIPER_ESPEAKNG'` and check the resolved LLM URL with `docker compose logs | grep 'LLM client'`. - To test the LLM endpoint from inside the container: `docker compose exec senator-sperma python3 -c 'import urllib.request; print(urllib.request.urlopen("http://10.0.0.3:2137/v1/models", timeout=5).status)'` If that fails, the host service is probably listening only on `127.0.0.1`; bind it to `0.0.0.0` (or the LAN IP) and allow the port through the firewall. - If the build fails to resolve `crates.io` or `deb.debian.org`, run `docker buildx build --load --network=host -t senator-sperma:latest .`; `compose.yaml` already sets `network: host` for builds on this host. - yt-dlp is pinned to a specific upstream release and verified against the release checksum at image build time. Bump `YTDLP_VERSION` in the Dockerfile to update it; pick a release that is at least a week old. ## Commands Slash commands are autoregistered globally on startup. Some are registered under their original Polish verb names to match the UI expected by existing servers. ### AI - `/senator` — chat with the OpenRouter model ### Music (songbird) - `/music grajcuj` — play from Copyparty search, a direct URL, or an uploaded file; optional `position` (`now`, `next`, `last`, default `last`) - `/music queue` — show the current track followed by the pending queue - `/music skip` — stop the current track and play the next queued one - `/music stop` — stop playback, clear the queue, and leave voice - `/music join` — join the caller's voice channel ### TTS - `/tts` — local Piper TTS (Polish), sent as a file or played in voice ### Search & Web - `/web` — DuckDuckGo HTML scrape (soon to use 4get) - `/search` — search Copyparty and return a direct download URL - `/download` — fetch media from any yt-dlp-supported URL ### Media Tools - `/compress` (alias `kompresja`) — compress a media attachment to low quality - `/convert` (alias `konwersja`) — convert media between formats ### Utilities - `/upload` — push Discord attachments into Copyparty - `/say` — repeat the given text as the bot - `/math` — evaluate an ASCII math expression through `qalc` - `/qr generowanie` / `/qr dekodowanie` — QR encode / decode - `/record` — upload the last recording window (requires `MANAGE_GUILD`) - `/leaderboard` — `me`, `messages`, `active`, `afk`, `total` ### Triggers - `/trigger create` — new trigger group - `/trigger add` — add keywords/responses to a group - `/trigger remove` — remove a keyword/response - `/trigger delete` — delete a group - `/trigger list` — list a group's keywords and responses - `/trigger help` — usage info ### Cron - `/cron dodaj_audio` — schedule audio playback - `/cron dodaj_wiadomość` — schedule a periodic message - `/cron pokaż` — list all cron jobs - `/cron usuń` — remove a cron job - `/cron pauza` — pause a job - `/cron wznów` — resume a job ### Message Context Menu - "Wyślij załączniki" (right-click message) — upload attachments to Copyparty - "kompresja" (right-click message) — compress all attachments - "Pobierz z linku" (right-click message) — download every URL in the message ## Architecture The runtime is organised around a shared `Data` struct (poise `user_data`) that holds the DB pool, HTTP client, Copyparty client, voice manager, the in-memory trigger cache, autocomplete cache, and per-guild music queue. - `src/main.rs` — boot: tracing, dotenvy, Config, DB pool, framework, cron task - `src/config.rs` — env-driven `Config` + variable validation - `src/commands/` — one module per command group - `src/copyparty/` — Copyparty search/upload client - `src/openrouter/` — OpenRouter chat client - `src/database/` — sqlx pool, migrations, query helpers (6 tables) - `src/voice/` — `VoiceManager`, ffmpeg/symphonia → songbird input - `src/net.rs` — blocks private/link-local hosts for user-supplied URLs - `src/recording/` — per-guild rolling PCM buffer for `/record` - `src/events/` — `handle_ready`, voice-state, message (trigger matching) - `src/tasks/` — 60s cron scheduler poll loop - `src/lang.rs` — user-facing Polish strings All temporary files go through `crate::config::temp_path()` (`$TMPDIR/senator_sperma`, created with mode `0700` at startup).