aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 01:33:33 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 01:33:33 +0100
commit97a26ec4e64a98a24dad7969337d2569339db9a7 (patch)
treeada17e2b1f50d053a6bfe4067296e06de848ac87
parentconfig: harden the shared temp directory (diff)
downloadsenator-sperma-97a26ec4e64a98a24dad7969337d2569339db9a7.tar.gz
senator-sperma-97a26ec4e64a98a24dad7969337d2569339db9a7.zip
docs: sync with current behavior
-rw-r--r--.env.example6
-rw-r--r--AGENTS.md49
-rw-r--r--CONTEXT.md13
-rw-r--r--README.md21
-rw-r--r--src/config.rs8
5 files changed, 57 insertions, 40 deletions
diff --git a/.env.example b/.env.example
index e67827d..83d3902 100644
--- a/.env.example
+++ b/.env.example
@@ -22,8 +22,14 @@ LLM_MODEL=
22OPENROUTER_API_KEY= 22OPENROUTER_API_KEY=
23OPENROUTER_MODEL= 23OPENROUTER_MODEL=
24 24
25# Optional - Voice recording (opt-in; no audio is kept when false)
26RECORDING_ENABLED=false
27# Rolling buffer length in seconds (1-3600).
28RECORDING_WINDOW_SECONDS=900
29
25# Optional - Local TTS 30# Optional - Local TTS
26# Directory containing <voice>.onnx, or a full path to the .onnx file. 31# Directory containing <voice>.onnx, or a full path to the .onnx file.
32# There is no external TTS fallback; leave unset to disable TTS.
27TTS_MODEL_PATH=/path/to/piper 33TTS_MODEL_PATH=/path/to/piper
28TTS_VOICE=pl_PL-bass-high 34TTS_VOICE=pl_PL-bass-high
29# Parent directory of espeak-ng-data. Docker overrides this to /usr/share. 35# Parent directory of espeak-ng-data. Docker overrides this to /usr/share.
diff --git a/AGENTS.md b/AGENTS.md
index 0c1c2d7..760e790 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,28 +4,33 @@
4 4
5``` 5```
6src/ 6src/
7├── main.rs — Entry: init tracing, dotenvy, build Config/Data, start poise+serenity 7├── main.rs — Entry: tracing, dotenvy, temp dir, Config/Data, poise+serenity
8├── config.rs — Config struct (env-driven), pub const TEMP_PATH 8├── config.rs — Config struct (env-driven), temp dir init/access
9├── error.rs — Unified Error enum + From impls, pub type Result<T> 9├── error.rs — Unified Error enum + From impls, pub type Result<T>
10├── net.rs — public-IP check for user-supplied URLs (SSRF guard)
10├── music.rs — MusicState: per-guild queue, now/next/last, skip, auto-advance 11├── music.rs — MusicState: per-guild queue, now/next/last, skip, auto-advance
12├── recording/mod.rs — per-guild rolling PCM recorder for /record
13├── tts.rs — TtsState (piper-rs in-process, bounded DashMap cache, resampler, WAV encoder)
11├── commands/ 14├── commands/
12│ ├── mod.rs — all_commands() aggregates every submodule 15│ ├── mod.rs — all_commands() aggregates every submodule
13│ ├── ai.rs — /senator (OpenRouter chat + optional TTS) 16│ ├── ai.rs — /senator (LLM chat + optional TTS)
17│ ├── autocomplete.rs — Copyparty autocomplete + trigger_name
14│ ├── cron.rs — /cron group (6 subcommands) 18│ ├── cron.rs — /cron group (6 subcommands)
15│ ├── math.rs — /math (qalc subprocess) 19│ ├── leaderboard.rs — /leaderboard group (me/messages/active/afk/total)
20│ ├── math.rs — /math (qalc subprocess, 5s timeout)
16│ ├── music.rs — /music group (grajcuj, skip, queue, stop, join) + context menu 21│ ├── music.rs — /music group (grajcuj, skip, queue, stop, join) + context menu
17│ ├── qr.rs — /qr encode + decode 22│ ├── qr.rs — /qr encode + decode (dimension/allocation limits)
23│ ├── record.rs — /record (flush this guild's buffer to Copyparty)
18│ ├── trigger.rs — /trigger group (6 subcommands) 24│ ├── trigger.rs — /trigger group (6 subcommands)
19│ ├── tts.rs — /tts (local Piper TTS → file/vc), /log_volume 25│ ├── tts.rs — /tts (local Piper TTS → file/vc)
20│ ├── utilities.rs — compress🟡, convert🟡, upload, search, download, say 26│ ├── utilities.rs — compress, convert, upload, search, download, say
21│ └── web.rs — /web (DuckDuckGo scrape) 27│ └── web.rs — /web (DuckDuckGo scrape)
22├── copyparty/mod.rs — CopypartyClient (search, upload) 28├── copyparty/mod.rs — CopypartyClient (search, upload, credential-safe URLs)
23├── database/mod.rs — DbPool, 21 query fns, 6 tables, migration 29├── database/mod.rs — DbPool, migrations, query helpers (7 tables)
24├── events/mod.rs — handle_ready, handle_voice_state_update, handle_message 30├── events/mod.rs — handle_ready, voice-state, message (triggers + LLM)
25├── openrouter/mod.rs — OpenRouterClient (chat, chat_with_history) 31├── openrouter/mod.rs — OpenRouterClient (chat, chat_with_history, timeouts)
26├── tasks/mod.rs — cron_scheduler_loop (30s polling, stub exec bodies) 32├── tasks/mod.rs — cron_scheduler_loop (60s polling, real execution)
27├── tts.rs — TtsState (piper-rs in-process, DashMap cache, pre-gen, resampler, WAV encoder) 33└── voice/mod.rs — VoiceManager (connect, disconnect, get_call), ffmpeg/symphonia inputs
28└── voice/mod.rs — VoiceManager (connect, disconnect, get_call), mp3_to_input
29``` 34```
30 35
31## Key Conventions 36## Key Conventions
@@ -35,18 +40,18 @@ src/
35- **Errors**: `crate::error::Error` enum with `?` propagation; use `Error::Custom(string)` for ad-hoc 40- **Errors**: `crate::error::Error` enum with `?` propagation; use `Error::Custom(string)` for ad-hoc
36- **Shared state**: `Data` struct in `main.rs` — passed as poise `user_data` 41- **Shared state**: `Data` struct in `main.rs` — passed as poise `user_data`
37- **Commands**: each submodule exports `pub fn commands() -> Vec<poise::Command<Data, Error>>` 42- **Commands**: each submodule exports `pub fn commands() -> Vec<poise::Command<Data, Error>>`
38- **Temp path**: always use `crate::config::TEMP_PATH` (resolves to `/tmp/senator_sperma`) 43- **Permissions**: admin commands use `required_permissions`; recording/cron/trigger are `guild_only` and scoped by `guild_id` in the DB
39- **TTS**: piper-rs in-process (no subprocess). `TtsState` in `Data` holds `OnceLock<Mutex<Piper>>` + `DashMap` cache. Synthesize via `data.tts.synthesize(text)`. Pre-generates 6 event phrases per user on first voice event. No Google TTS fallback. 44- **Temp path**: always use `crate::config::temp_path()` (initialized with mode `0700`; never hardcode `/tmp/senator_sperma`)
45- **Outbound URLs**: user-supplied `http(s)` URLs go through `crate::net::ensure_public_http_url` unless they belong to the configured Copyparty origin
46- **TTS**: piper-rs in-process (no subprocess). `TtsState` holds `OnceLock<Arc<std::sync::Mutex<Piper>>>` + a bounded `DashMap` cache. Synthesize via `data.tts.synthesize(text)`; work runs on `spawn_blocking`. No external TTS fallback.
47- **Recording**: opt-in (`RECORDING_ENABLED=false` by default), one buffer per guild.
40 48
41## Build 49## Build
42- `build.rs` — compiles C stubs for espeak-rs-sys missing sonic/audio_object symbols 50- `build.rs` — compiles C stubs for espeak-rs-sys missing sonic/audio_object symbols
43 51
44## Stubs to Fill (see COMPARISON.md)
45- `commands/utilities.rs` — `/compress`, `/convert`, `/download` upload part
46- `tasks/mod.rs` — cron execution bodies are `info!` only, no actual action
47
48## Database Tables 52## Database Tables
49`settings` (key/value), `voice_activity`, `cron_jobs`, `triggers`, `trigger_keywords`, `trigger_responses` 53`settings`, `voice_activity`, `user_stats`, `cron_jobs`, `triggers`, `trigger_keywords`, `trigger_responses`
50 54
51## Commands 55## Commands
52`/senator`, `/cron` (6), `/math`, `/music` (5: grajcuj, skip, queue, stop, join), `/qr` (2), `/trigger` (6), `/tts`, `/log_volume`, `/compress`, `/convert`, `/upload`, `/search`, `/download`, `/say`, `/web`, `/record`, `/leaderboard` 56`/senator`, `/cron` (6), `/leaderboard` (5), `/math`, `/music` (5: grajcuj, skip, queue, stop, join), `/qr` (2), `/trigger` (6), `/tts`, `/record`, `/compress`, `/convert`, `/upload`, `/search`, `/download`, `/say`, `/web`.
57Context menus: "Odtwórz", "Wyślij załączniki", "kompresja", "Pobierz z linku".
diff --git a/CONTEXT.md b/CONTEXT.md
index 278420f..5a992ec 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -10,8 +10,8 @@ holds no implementation decisions or specs (those live in `docs/adr/`).
10 Despite the name, it contains **no audio** — only timestamps and durations. 10 Despite the name, it contains **no audio** — only timestamps and durations.
11- **Recording** — capturing the *audio* of a voice channel into a rolling 11- **Recording** — capturing the *audio* of a voice channel into a rolling
12 in-memory buffer while the bot is connected. Distinct from *voice activity* 12 in-memory buffer while the bot is connected. Distinct from *voice activity*
13 (metadata). A bot "records" continuously while in a VC (unless opted out) and 13 (metadata). Recording is opt-in via `RECORDING_ENABLED` (default false); when
14 "flushes" on `/record`. 14 enabled a bot records continuously while in a VC and "flushes" on `/record`.
15- **Window** — the retention horizon of the recording buffer: how many seconds 15- **Window** — the retention horizon of the recording buffer: how many seconds
16 of the recent past are kept. Older audio is discarded as new audio arrives. 16 of the recent past are kept. Older audio is discarded as new audio arrives.
17 Configured via `RECORDING_WINDOW_SECONDS` (default 900 = 15 minutes). 17 Configured via `RECORDING_WINDOW_SECONDS` (default 900 = 15 minutes).
@@ -40,10 +40,11 @@ holds no implementation decisions or specs (those live in `docs/adr/`).
40 at the model's native sample rate (typically 22050 Hz mono), resampled to 40 at the model's native sample rate (typically 22050 Hz mono), resampled to
41 48 kHz stereo via inline linear interpolation before playback. Songbird does 41 48 kHz stereo via inline linear interpolation before playback. Songbird does
42 not resample — playing un-resampled audio results in wrong playback speed. 42 not resample — playing un-resampled audio results in wrong playback speed.
43- **TtsState** — struct held in poise `Data`: `OnceCell<Mutex<Piper>>` for the 43- **TtsState** — struct held in poise `Data`: `OnceLock<Arc<std::sync::Mutex<Piper>>>`
44 model instance, `DashMap<String, (Vec<u8>, Instant)>` for the text→audio cache. 44 for the model instance, `DashMap<String, (Vec<u8>, Instant)>` for the
45 Piper loaded lazily on first TTS call. Cache keyed by full phrase string, 45 text→audio cache. Piper is loaded lazily on first TTS call, off the async
46 evicted by TTL. No Google TTS fallback. 46 runtime. Cache is keyed by the full phrase, bounded by entries/bytes, and
47 evicted by TTL. No external TTS fallback.
47- **Pre-generation** — on first voice event for a user, synthesize all 6 event 48- **Pre-generation** — on first voice event for a user, synthesize all 6 event
48 phrases (join/leave/deafen/undeafen/mute/unmute) with their display name and 49 phrases (join/leave/deafen/undeafen/mute/unmute) with their display name and
49 cache them. ~120ms first time, instant after. Lazy — no startup cost. 50 cache them. ~120ms first time, instant after. Lazy — no startup cost.
diff --git a/README.md b/README.md
index a3da394..f314793 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ The following binaries must be available on `PATH`:
24- `imagemagick` (`magick`) — image compression and conversion for `/compress` and `/convert` 24- `imagemagick` (`magick`) — image compression and conversion for `/compress` and `/convert`
25- `qalc` (Qalculate) — used by `/math` to evaluate expressions 25- `qalc` (Qalculate) — used by `/math` to evaluate expressions
26- `yt-dlp` (optional) — used by `/download` 26- `yt-dlp` (optional) — used by `/download`
27- `piper` (optional) — local TTS for `/tts` and `/senator ... tts_vc`; falls back to Google Translate TTS if unavailable 27- `piper` model files (optional) — local TTS for `/tts` and `/senator ... tts_vc`; TTS commands fail when unset
28 28
29Unlike the Python version, QR encode/decode is handled by the pure-Rust 29Unlike the Python version, QR encode/decode is handled by the pure-Rust
30`qrcode`/`rxing` crates — no `zbar` system library required. 30`qrcode`/`rxing` crates — no `zbar` system library required.
@@ -48,8 +48,8 @@ created automatically on first run.
48 48
49For local TTS, set `TTS_MODEL_PATH` to a Piper `.onnx` model file (or a 49For local TTS, set `TTS_MODEL_PATH` to a Piper `.onnx` model file (or a
50directory of models, in which case `TTS_VOICE` selects the model, defaulting to 50directory of models, in which case `TTS_VOICE` selects the model, defaulting to
51`pl_PL-bass-high`). When `TTS_MODEL_PATH` is unset or Piper is unavailable, the 51`pl_PL-bass-high`). There is no external TTS fallback: TTS commands fail when
52bot falls back to Google Translate TTS. 52the model is missing.
53 53
54### 5. Run 54### 5. Run
55 55
@@ -132,7 +132,7 @@ servers.
132- `/music join` — join the caller's voice channel 132- `/music join` — join the caller's voice channel
133 133
134### TTS 134### TTS
135- `/tts` — local Piper TTS (Polish, Google Translate fallback), sent as a file or played in voice 135- `/tts` — local Piper TTS (Polish), sent as a file or played in voice
136 136
137### Search & Web 137### Search & Web
138- `/web` — DuckDuckGo HTML scrape (soon to use 4get) 138- `/web` — DuckDuckGo HTML scrape (soon to use 4get)
@@ -148,6 +148,8 @@ servers.
148- `/say` — repeat the given text as the bot 148- `/say` — repeat the given text as the bot
149- `/math` — evaluate an ASCII math expression through `qalc` 149- `/math` — evaluate an ASCII math expression through `qalc`
150- `/qr generowanie` / `/qr dekodowanie` — QR encode / decode 150- `/qr generowanie` / `/qr dekodowanie` — QR encode / decode
151- `/record` — upload the last recording window (requires `MANAGE_GUILD`)
152- `/leaderboard` — `me`, `messages`, `active`, `afk`, `total`
151 153
152### Triggers 154### Triggers
153- `/trigger create` — new trigger group 155- `/trigger create` — new trigger group
@@ -168,6 +170,7 @@ servers.
168### Message Context Menu 170### Message Context Menu
169- "Wyślij załączniki" (right-click message) — upload attachments to Copyparty 171- "Wyślij załączniki" (right-click message) — upload attachments to Copyparty
170- "kompresja" (right-click message) — compress all attachments 172- "kompresja" (right-click message) — compress all attachments
173- "Pobierz z linku" (right-click message) — download every URL in the message
171 174
172## Architecture 175## Architecture
173 176
@@ -181,10 +184,12 @@ in-memory trigger cache, autocomplete cache, and per-guild music queue.
181- `src/copyparty/` — Copyparty search/upload client 184- `src/copyparty/` — Copyparty search/upload client
182- `src/openrouter/` — OpenRouter chat client 185- `src/openrouter/` — OpenRouter chat client
183- `src/database/` — sqlx pool, migrations, query helpers (6 tables) 186- `src/database/` — sqlx pool, migrations, query helpers (6 tables)
184- `src/voice/` — `VoiceManager`, mp3 → songbird input 187- `src/voice/` — `VoiceManager`, ffmpeg/symphonia → songbird input
188- `src/net.rs` — blocks private/link-local hosts for user-supplied URLs
189- `src/recording/` — per-guild rolling PCM buffer for `/record`
185- `src/events/` — `handle_ready`, voice-state, message (trigger matching) 190- `src/events/` — `handle_ready`, voice-state, message (trigger matching)
186- `src/tasks/` — 30s cron scheduler poll loop 191- `src/tasks/` — 60s cron scheduler poll loop
187- `src/lang.rs` — user-facing Polish strings 192- `src/lang.rs` — user-facing Polish strings
188 193
189All temporary files go through `crate::config::TEMP_PATH` 194All temporary files go through `crate::config::temp_path()`
190(`/tmp/senator_sperma`). 195(`$TMPDIR/senator_sperma`, created with mode `0700` at startup).
diff --git a/src/config.rs b/src/config.rs
index c05c3ae..09eabeb 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -75,7 +75,7 @@ pub struct Config {
75 pub llm_api_url: String, 75 pub llm_api_url: String,
76 pub database_path: String, 76 pub database_path: String,
77 /// Optional path to the Piper TTS `.onnx` model (or a directory of models). 77 /// Optional path to the Piper TTS `.onnx` model (or a directory of models).
78 /// When unset, TTS falls back to Google Translate TTS. 78 /// When unset, TTS commands fail; there is no external fallback.
79 pub tts_model_path: Option<String>, 79 pub tts_model_path: Option<String>,
80 /// Piper voice name, used to resolve the model file when `tts_model_path` 80 /// Piper voice name, used to resolve the model file when `tts_model_path`
81 /// points at a directory. Ignored when it points at a `.onnx` file directly. 81 /// points at a directory. Ignored when it points at a `.onnx` file directly.
@@ -168,7 +168,7 @@ const ENV_VARS: &[EnvVar] = &[
168 name: "TTS_MODEL_PATH", 168 name: "TTS_MODEL_PATH",
169 required: false, 169 required: false,
170 default: None, 170 default: None,
171 description: "Path to a Piper TTS .onnx model (or a models directory). Unset => Google TTS fallback", 171 description: "Path to a Piper TTS .onnx model (or a models directory); TTS is disabled when unset",
172 }, 172 },
173 EnvVar { 173 EnvVar {
174 name: "TTS_VOICE", 174 name: "TTS_VOICE",
@@ -284,11 +284,11 @@ impl Config {
284 } 284 }
285 } 285 }
286 286
287 /// Resolve the Piper model path to pass to the `piper` CLI. 287 /// Resolve the Piper model path passed to `piper-rs` (in-process).
288 /// 288 ///
289 /// - `TTS_MODEL_PATH` set to a `.onnx` file => used verbatim. 289 /// - `TTS_MODEL_PATH` set to a `.onnx` file => used verbatim.
290 /// - `TTS_MODEL_PATH` set to a directory => `{dir}/{TTS_VOICE}.onnx`. 290 /// - `TTS_MODEL_PATH` set to a directory => `{dir}/{TTS_VOICE}.onnx`.
291 /// - `TTS_MODEL_PATH` unset => `None` (caller falls back to Google TTS). 291 /// - `TTS_MODEL_PATH` unset => `None` (TTS is unavailable).
292 pub fn piper_model_path(&self) -> Option<std::path::PathBuf> { 292 pub fn piper_model_path(&self) -> Option<std::path::PathBuf> {
293 let configured = self.tts_model_path.as_ref()?; 293 let configured = self.tts_model_path.as_ref()?;
294 let path = std::path::PathBuf::from(configured); 294 let path = std::path::PathBuf::from(configured);