refactor: switch the whole project to English
talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d30bd95ad
commit
85fa64cfe6
95 files changed
+4707
-4854
No files matched your search
+116
-116
@@ -14,183 +14,179 @@ import (
|
||||
"code.m3ta.dev/m3tam3re/talk/internal/queue"
|
||||
)
|
||||
|
||||
// Erlaubte Werte für audio.sink (Teil des öffentlichen Vertrags; Spec:
|
||||
// „Audio-Sink wählbar inkl. null").
|
||||
// Allowed values for audio.sink (public contract).
|
||||
const (
|
||||
SinkOto = "oto"
|
||||
SinkNull = "null"
|
||||
)
|
||||
|
||||
// DefaultServerPort ist der Default-Port des Daemon-HTTP-Servers auf
|
||||
// 127.0.0.1 (Task-04-Brief: „Port Default 7800").
|
||||
// DefaultServerPort is the daemon's default HTTP port on 127.0.0.1.
|
||||
const DefaultServerPort = 7800
|
||||
|
||||
// DefaultRateLimit ist das Daemon-Gesetz gegen Spam (Task-05-Brief):
|
||||
// Default 1 Ansage / 30 s. „0s" schaltet das Limit ab.
|
||||
// DefaultRateLimit is the anti-spam law's default: one announcement per
|
||||
// 30 seconds. "0s" disables the limit.
|
||||
const DefaultRateLimit = 30 * time.Second
|
||||
|
||||
// DefaultMaxText ist die Default-Obergrenze der Ansage-Länge in Zeichen —
|
||||
// etwa zwei bis drei Sätze, rund 25 Sekunden Sprache. 0 = unbegrenzt.
|
||||
// DefaultMaxText is the default cap on announcement length in characters:
|
||||
// two to three sentences, roughly 25 seconds of speech. 0 = unlimited.
|
||||
const DefaultMaxText = 400
|
||||
|
||||
// DefaultMaxAge ist die Default-Höchstwartezeit einer Ansage in der Queue;
|
||||
// was länger parkt, ist veraltet und wird verworfen. „0s" = unbegrenzt.
|
||||
// DefaultMaxAge is the default longest wait in the queue; whatever parks
|
||||
// longer is stale and gets discarded. "0s" = unlimited.
|
||||
const DefaultMaxAge = 2 * time.Minute
|
||||
|
||||
// Config ist der effektiv geladene Konfigurations-Stand von talk. Task 02
|
||||
// führt audio.sink; Task 03 ergänzt den Provider-Pin, Provider-Endpunkte
|
||||
// und das Voice-Mapping; Task 04 den Daemon-Port (server.port, TALK_PORT);
|
||||
// Task 05 die Anti-Spam-Gesetze (queue.rate_limit, queue.allow_high_burst,
|
||||
// quiet_hours, quiet_mode); spätere Tasks ergänzen weitere Felder, ohne die
|
||||
// Auflösungs-Reihenfolge zu ändern.
|
||||
// DefaultLang is the language assumed when detection cannot decide (empty,
|
||||
// numbers, ties). Also the language of talk's own announcements such as the
|
||||
// wrap result when the label gives no hint.
|
||||
const DefaultLang = "en"
|
||||
|
||||
// Config is the effective configuration after all layers were applied.
|
||||
type Config struct {
|
||||
Audio Audio `toml:"audio"`
|
||||
|
||||
// Server steuert den Daemon-HTTP-Server (talkd) auf 127.0.0.1.
|
||||
// Server configures the daemon's HTTP server on 127.0.0.1.
|
||||
Server Server `toml:"server"`
|
||||
|
||||
// Queue steuert die Anti-Spam-Gesetze des Daemons (Task 05).
|
||||
// Queue holds the daemon's anti-spam laws.
|
||||
Queue Queue `toml:"queue"`
|
||||
|
||||
// Daemon steuert den Hintergrundprozess (Autostart).
|
||||
// Daemon configures the background process (autostart).
|
||||
Daemon Daemon `toml:"daemon"`
|
||||
|
||||
// Provider pinnt die Kette auf genau einen Provider (z. B.
|
||||
// `provider = "elevenlabs"`); leer = komplette Default-Kette
|
||||
// (elevenlabs → openai → edge → osbuiltin). Das CLI-Flag --provider
|
||||
// schlägt diesen Wert. Gültige Namen prüft die Provider-Schicht.
|
||||
// Lang is the fallback language (de | en) when detection cannot decide,
|
||||
// and the language of talk's own announcements. Default "en"; env
|
||||
// TALK_LANG.
|
||||
Lang string `toml:"lang"`
|
||||
|
||||
// Provider pins the chain to exactly one provider (for example
|
||||
// `provider = "elevenlabs"`); empty = the full default chain
|
||||
// (elevenlabs → openai → edge → osbuiltin). The --provider flag beats
|
||||
// this value. Valid names are checked by the provider layer.
|
||||
Provider string `toml:"provider"`
|
||||
|
||||
// Providers enthält per Provider-Einstellungen (Endpunkt, Modell) —
|
||||
// TOML `[providers.elevenlabs] endpoint = "…"`. Key `providers`
|
||||
// (Mehrzahl), damit der Pin ( singular `provider`) und die
|
||||
// Einstellungs-Tabelle in TOML nebeneinander existieren können.
|
||||
// Providers holds per-provider settings (endpoint, model):
|
||||
// `[providers.elevenlabs] endpoint = "…"`. The key is plural so the pin
|
||||
// (`provider`) and the settings table can coexist in TOML.
|
||||
Providers map[string]ProviderSettings `toml:"providers"`
|
||||
|
||||
// Voice ist das Voice-Mapping `voice.<provider>.<lang> = id` — z. B.
|
||||
// `[voice.elevenlabs] de = "21m00Tcm4TlvDq8ikWAM"`. Schlägt die
|
||||
// eingebauten neutralen Defaults; das CLI-Flag --voice schlägt alles.
|
||||
// Voice is the voice mapping `voice.<provider>.<lang> = id`, for example
|
||||
// `[voice.elevenlabs] de = "21m00Tcm4TlvDq8ikWAM"`. It beats the built-in
|
||||
// neutral defaults; the --voice flag beats everything.
|
||||
Voice map[string]map[string]string `toml:"voice"`
|
||||
|
||||
// Wrap steuert die Befehlsummwicklung `talk wrap` (Task 07).
|
||||
// Wrap configures `talk wrap`.
|
||||
Wrap Wrap `toml:"wrap"`
|
||||
|
||||
// QuietHours ist das Ruhezeit-Fenster "HH:MM-HH:MM" in lokaler Zeit
|
||||
// (überspannt Mitternacht, z. B. "18:00-08:00"); leer = deaktiviert.
|
||||
// Bewusst kein Default-Fenster: Ein aktives Gesetz, das nach Uhrzeit
|
||||
// verwirft, würde jeden Cron/Test in den Abendstunden stilllegen —
|
||||
// Ruhezeiten sind Opt-in wie Mute.
|
||||
// QuietHours is the quiet window "HH:MM-HH:MM" in local time (may span
|
||||
// midnight, for example "18:00-08:00"); empty = disabled. Deliberately
|
||||
// no default window: a law that discards by time of day would silence
|
||||
// every evening cron job; quiet hours are opt-in like mute.
|
||||
QuietHours string `toml:"quiet_hours"`
|
||||
|
||||
// QuietMode bestimmt das Verhalten in der Ruhezeit: "discard" (Default,
|
||||
// still verwerfen) oder "toast" (Benachrichtigung statt Audio).
|
||||
// QuietMode is the behaviour during quiet hours: "discard" (default,
|
||||
// drop silently) or "toast" (desktop notification instead of audio).
|
||||
QuietMode string `toml:"quiet_mode"`
|
||||
}
|
||||
|
||||
// Audio steuert die Audio-Ausgabe.
|
||||
// Audio configures playback.
|
||||
type Audio struct {
|
||||
// Sink wählt die Wiedergabe: "oto" (embedded Playback, Default) oder
|
||||
// "null" (verwirft — CI, Server, Tests).
|
||||
// Sink selects playback: "oto" (embedded playback, default) or "null"
|
||||
// (discard; CI, servers, tests).
|
||||
Sink string `toml:"sink"`
|
||||
}
|
||||
|
||||
// Server sind die Netz-Einstellungen des Daemons (nur Loopback; Binding
|
||||
// auf 127.0.0.1 macht cmd/talkd, die Config liefert nur den Port).
|
||||
// Server holds the daemon's network settings (loopback only; the daemon
|
||||
// binds 127.0.0.1, the config only supplies the port).
|
||||
type Server struct {
|
||||
// Port des Daemon-HTTP-Servers auf 127.0.0.1 — config
|
||||
// `[server] port = 7800`, Env-Override TALK_PORT, Default 7800.
|
||||
// Port of the daemon's HTTP server on 127.0.0.1: `[server] port = 7800`,
|
||||
// env override TALK_PORT, default 7800.
|
||||
Port int `toml:"port"`
|
||||
|
||||
// AllowedOrigins ist die CORS-Erlaubnisliste für POST /speak (Task 06):
|
||||
// exakte Origins wie "https://chat.az-gruppe.com". Der Eintrag "*"
|
||||
// erlaubt jede Origin ausdrücklich. Bewusster Default: LEER — ohne
|
||||
// Einträge sendet der Daemon gar keine CORS-Header und der Browser
|
||||
// blockt Cross-Origin-Anfragen; die Tür ist zu, bis sie konfiguriert
|
||||
// wird (Env-Override TALK_ALLOWED_ORIGINS, kommagetrennt; "off" leert).
|
||||
// AllowedOrigins is the CORS allow-list for POST /speak: exact origins
|
||||
// such as "https://chat.example.com". The entry "*" allows every origin
|
||||
// explicitly. Deliberate default: EMPTY, meaning no CORS headers at all
|
||||
// and the server-side origin guard rejects browser requests; the door
|
||||
// stays shut until configured (env TALK_ALLOWED_ORIGINS, comma-separated;
|
||||
// "off" clears it).
|
||||
AllowedOrigins []string `toml:"allowed_origins"`
|
||||
}
|
||||
|
||||
// Daemon sind die Einstellungen des Hintergrundprozesses (TOML `[daemon]`).
|
||||
// Daemon holds the background process settings (TOML `[daemon]`).
|
||||
type Daemon struct {
|
||||
// Autostart lässt talk den Daemon beim ersten Sprechbefehl selbst im
|
||||
// Hintergrund starten (Default true; Env TALK_AUTOSTART). false ist für
|
||||
// Setups, die den Daemon selbst verwalten (systemd, launchd, Task
|
||||
// Scheduler): say spricht dann ohne Daemon direkt, mit Hinweis.
|
||||
// Autostart lets talk start the daemon itself in the background on the
|
||||
// first speak command (default true; env TALK_AUTOSTART). false is for
|
||||
// setups that manage the daemon themselves (systemd, launchd, Task
|
||||
// Scheduler): without a daemon, say then speaks directly, with a note.
|
||||
Autostart bool `toml:"autostart"`
|
||||
}
|
||||
|
||||
// Wrap sind die Einstellungen von `talk wrap` (TOML `[wrap]`).
|
||||
// Wrap holds the `talk wrap` settings (TOML `[wrap]`).
|
||||
type Wrap struct {
|
||||
// Label ersetzt den Basisnamen des Befehls in der Ansage — z. B.
|
||||
// `label = "Deployment"` für „Deployment erfolgreich. Dauer: …"
|
||||
// statt „terraform erfolgreich. Dauer: …" (Env-Override
|
||||
// TALK_WRAP_LABEL schlägt die Datei).
|
||||
// Label replaces the command's base name in the announcement, for
|
||||
// example `label = "Deployment"` to hear "Deployment succeeded" instead
|
||||
// of "terraform succeeded" (env override TALK_WRAP_LABEL).
|
||||
Label string `toml:"label"`
|
||||
}
|
||||
|
||||
// Queue sind die Rate-Limit-Gesetze des Daemons (TOML `[queue]`).
|
||||
// Queue holds the daemon's anti-spam laws (TOML `[queue]`).
|
||||
type Queue struct {
|
||||
// RateLimit ist der Mindestabstand zwischen zwei Ansage-Starten als
|
||||
// Dauer-String (z. B. "30s"; "0s" = unbegrenzt). Default: 30 s.
|
||||
// RateLimit is the minimum gap between two announcement starts, as a
|
||||
// duration string ("30s"; "0s" = unlimited). Default 30 s.
|
||||
RateLimit time.Duration `toml:"rate_limit"`
|
||||
|
||||
// AllowHighBurst erlaubt priority=high, das Rate-Limit bewusst zu
|
||||
// verstoßen (Default true). Die Ruhezeit gilt davon unberührt.
|
||||
// AllowHighBurst lets priority=high deliberately break the rate limit
|
||||
// (default true). Quiet hours still apply.
|
||||
AllowHighBurst bool `toml:"allow_high_burst"`
|
||||
|
||||
// MaxText ist die Obergrenze der Ansage-Länge in Zeichen; längere
|
||||
// Texte werden an einer Wortgrenze gekürzt (Default 400; 0 =
|
||||
// unbegrenzt; Env TALK_MAX_TEXT).
|
||||
// MaxText caps announcement length in characters; longer texts are cut
|
||||
// at a word boundary (default 400; 0 = unlimited; env TALK_MAX_TEXT).
|
||||
MaxText int `toml:"max_text"`
|
||||
|
||||
// MaxAge ist die längste Wartezeit einer Ansage in der Queue; ältere
|
||||
// werden beim Fälligwerden verworfen (Default "2m"; "0s" = unbegrenzt;
|
||||
// Env TALK_MAX_AGE).
|
||||
// MaxAge is the longest an announcement may wait in the queue; older
|
||||
// ones are discarded when they come due (default "2m"; "0s" =
|
||||
// unlimited; env TALK_MAX_AGE).
|
||||
MaxAge time.Duration `toml:"max_age"`
|
||||
}
|
||||
|
||||
// ProviderSettings sind die per Provider konfigurierbaren Endpunkte und
|
||||
// Modelle (TOML `[providers.<name>]`). Leer = eingebaute Defaults; die
|
||||
// Env-Overrides TALK_ELEVENLABS_ENDPOINT / TALK_OPENAI_ENDPOINT /
|
||||
// TALK_EDGE_ENDPOINT (ausgewertet in der Provider-Schicht) schlagen die
|
||||
// Datei — Keys laufen ausschließlich über TALK_ELEVENLABS_KEY bzw.
|
||||
// TALK_OPENAI_KEY und werden nie in die Datei geschrieben.
|
||||
// ProviderSettings are the per-provider endpoint and model (TOML
|
||||
// `[providers.<name>]`). Empty = built-in defaults; the env overrides
|
||||
// TALK_ELEVENLABS_ENDPOINT / TALK_OPENAI_ENDPOINT / TALK_EDGE_ENDPOINT
|
||||
// (read by the provider layer) beat the file. Keys come exclusively from
|
||||
// TALK_ELEVENLABS_KEY / TALK_OPENAI_KEY and are never written to the file.
|
||||
type ProviderSettings struct {
|
||||
Endpoint string `toml:"endpoint"`
|
||||
Model string `toml:"model"` // nur openai (Default "tts-1")
|
||||
Model string `toml:"model"` // openai only (default "tts-1")
|
||||
}
|
||||
|
||||
// Defaults liefert die eingebaute Default-Konfiguration (unterste Schicht
|
||||
// der Auflösung).
|
||||
// Defaults returns the built-in default configuration (the lowest layer).
|
||||
func Defaults() Config {
|
||||
return Config{
|
||||
Audio: Audio{Sink: SinkOto},
|
||||
Server: Server{Port: DefaultServerPort},
|
||||
Queue: Queue{RateLimit: DefaultRateLimit, AllowHighBurst: true, MaxText: DefaultMaxText, MaxAge: DefaultMaxAge},
|
||||
Daemon: Daemon{Autostart: true},
|
||||
Lang: DefaultLang,
|
||||
QuietMode: queue.QuietModeDiscard,
|
||||
}
|
||||
}
|
||||
|
||||
// Options steuert Load. Alle Felder sind injizierbar, damit die Unit-Tests
|
||||
// ohne echte HOME-/Env-Umgebung und ohne echte Dateien auskommen.
|
||||
// Options controls Load. Every field is injectable so unit tests run without
|
||||
// a real HOME, environment or files.
|
||||
type Options struct {
|
||||
// File überschreibt den platform-nativen Pfad der config.toml (Tests
|
||||
// injizieren hier eine Temp-Datei). Leer: Dir() + "config.toml".
|
||||
// File overrides the platform-native path of config.toml (tests inject
|
||||
// a temp file). Empty: Dir() + "config.toml".
|
||||
File string
|
||||
|
||||
// Getenv liefert Umgebungsvariablen (Default: os.Getenv). Über TALK_*
|
||||
// laufen die Env-Overrides sowie die Pfadauflösung (HOME,
|
||||
// XDG_CONFIG_HOME, APPDATA).
|
||||
// Getenv supplies environment variables (default os.Getenv): the TALK_*
|
||||
// overrides and the path resolution (HOME, XDG_CONFIG_HOME, APPDATA).
|
||||
Getenv func(string) string
|
||||
}
|
||||
|
||||
// Load löst die Konfiguration schichtweise auf: Defaults → config.toml
|
||||
// (falls vorhanden) → Env-Overrides (TALK_* schlägt die Datei). Env- und
|
||||
// Datei-Validierungsfehler werden als klarer Fehler gemeldet, nie still
|
||||
// ignoriert. Fehlt das Config-Verzeichnis (z. B. keine HOME-Variable),
|
||||
// läuft talk mit Defaults + Env weiter — die Datei ist optional, kaputte
|
||||
// Umgebungen dürfen talk nicht unbrauchbar machen.
|
||||
// Load resolves the configuration in layers: defaults → config.toml (if
|
||||
// present) → env overrides (TALK_* beats the file). Env and file validation
|
||||
// errors are reported clearly, never ignored. If the config directory
|
||||
// cannot be determined (no HOME), talk runs with defaults + env: the file is
|
||||
// optional, a broken environment must not make talk unusable.
|
||||
func Load(opts Options) (Config, error) {
|
||||
if opts.Getenv == nil {
|
||||
opts.Getenv = os.Getenv
|
||||
@@ -201,7 +197,6 @@ func Load(opts Options) (Config, error) {
|
||||
if path == "" {
|
||||
dir, err := Dir(opts.Getenv)
|
||||
if err != nil {
|
||||
// Kein auffindbares Config-Verzeichnis: optionale Datei entfällt.
|
||||
return applyEnv(cfg, opts.Getenv)
|
||||
}
|
||||
path = filepath.Join(dir, fileName)
|
||||
@@ -215,7 +210,7 @@ func Load(opts Options) (Config, error) {
|
||||
return applyEnv(cfg, opts.Getenv)
|
||||
}
|
||||
|
||||
// applyEnv wendet die Env-Overrides an und validiert das Ergebnis.
|
||||
// applyEnv applies the env overrides and validates the result.
|
||||
func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
|
||||
if v := getenv("TALK_AUDIO_SINK"); v != "" {
|
||||
cfg.Audio.Sink = v
|
||||
@@ -223,10 +218,13 @@ func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
|
||||
if v := getenv("TALK_PROVIDER"); v != "" {
|
||||
cfg.Provider = v
|
||||
}
|
||||
if v := getenv("TALK_LANG"); v != "" {
|
||||
cfg.Lang = v
|
||||
}
|
||||
if v := getenv("TALK_PORT"); v != "" {
|
||||
port, err := strconv.Atoi(v)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return cfg, fmt.Errorf("TALK_PORT: ungültiger Wert %q (1–65535)", v)
|
||||
return cfg, fmt.Errorf("TALK_PORT: invalid value %q (1–65535)", v)
|
||||
}
|
||||
cfg.Server.Port = port
|
||||
}
|
||||
@@ -244,21 +242,21 @@ func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
|
||||
if v := getenv("TALK_MAX_TEXT"); v != "" {
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n < 0 {
|
||||
return cfg, fmt.Errorf("TALK_MAX_TEXT: ungültiger Wert %q (Zeichen; 0 = unbegrenzt)", v)
|
||||
return cfg, fmt.Errorf("TALK_MAX_TEXT: invalid value %q (characters; 0 = unlimited)", v)
|
||||
}
|
||||
cfg.Queue.MaxText = n
|
||||
}
|
||||
if v := getenv("TALK_MAX_AGE"); v != "" {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil || d < 0 {
|
||||
return cfg, fmt.Errorf("TALK_MAX_AGE: ungültiger Wert %q (Dauer, z. B. \"2m\"; \"0s\" = unbegrenzt)", v)
|
||||
return cfg, fmt.Errorf("TALK_MAX_AGE: invalid value %q (duration such as \"2m\"; \"0s\" = unlimited)", v)
|
||||
}
|
||||
cfg.Queue.MaxAge = d
|
||||
}
|
||||
if v := getenv("TALK_AUTOSTART"); v != "" {
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("TALK_AUTOSTART: ungültiger Wert %q (true/false)", v)
|
||||
return cfg, fmt.Errorf("TALK_AUTOSTART: invalid value %q (true/false)", v)
|
||||
}
|
||||
cfg.Daemon.Autostart = b
|
||||
}
|
||||
@@ -268,14 +266,14 @@ func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
|
||||
if v := getenv("TALK_RATE_LIMIT"); v != "" {
|
||||
d, err := time.ParseDuration(v)
|
||||
if err != nil || d < 0 {
|
||||
return cfg, fmt.Errorf("TALK_RATE_LIMIT: ungültiger Wert %q (Dauer, z. B. \"30s\"; \"0s\" = unbegrenzt)", v)
|
||||
return cfg, fmt.Errorf("TALK_RATE_LIMIT: invalid value %q (duration such as \"30s\"; \"0s\" = unlimited)", v)
|
||||
}
|
||||
cfg.Queue.RateLimit = d
|
||||
}
|
||||
if v := getenv("TALK_ALLOW_HIGH_BURST"); v != "" {
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("TALK_ALLOW_HIGH_BURST: ungültiger Wert %q (true/false)", v)
|
||||
return cfg, fmt.Errorf("TALK_ALLOW_HIGH_BURST: invalid value %q (true/false)", v)
|
||||
}
|
||||
cfg.Queue.AllowHighBurst = b
|
||||
}
|
||||
@@ -284,37 +282,39 @@ func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
|
||||
cfg.QuietHours = ""
|
||||
} else {
|
||||
if _, err := queue.ParseQuietHours(v); err != nil {
|
||||
return cfg, fmt.Errorf("TALK_QUIET_HOURS: ungültiges Fenster %q (HH:MM-HH:MM, \"off\" = deaktiviert)", v)
|
||||
return cfg, fmt.Errorf("TALK_QUIET_HOURS: invalid window %q (HH:MM-HH:MM, \"off\" = disabled)", v)
|
||||
}
|
||||
cfg.QuietHours = v
|
||||
}
|
||||
}
|
||||
if v := getenv("TALK_QUIET_MODE"); v != "" {
|
||||
if v != queue.QuietModeDiscard && v != queue.QuietModeToast {
|
||||
return cfg, fmt.Errorf("TALK_QUIET_MODE: ungültiger Wert %q (erlaubt: %q, %q)",
|
||||
return cfg, fmt.Errorf("TALK_QUIET_MODE: invalid value %q (allowed: %q, %q)",
|
||||
v, queue.QuietModeDiscard, queue.QuietModeToast)
|
||||
}
|
||||
cfg.QuietMode = v
|
||||
}
|
||||
if cfg.Queue.MaxText < 0 || cfg.Queue.MaxAge < 0 || cfg.Queue.RateLimit < 0 {
|
||||
return cfg, fmt.Errorf("queue: max_text, max_age und rate_limit dürfen nicht negativ sein (0 = unbegrenzt)")
|
||||
return cfg, fmt.Errorf("queue: max_text, max_age and rate_limit must not be negative (0 = unlimited)")
|
||||
}
|
||||
switch cfg.Audio.Sink {
|
||||
case SinkOto, SinkNull:
|
||||
default:
|
||||
return cfg, fmt.Errorf("audio.sink: ungültiger Wert %q (erlaubt: %q, %q)",
|
||||
return cfg, fmt.Errorf("audio.sink: invalid value %q (allowed: %q, %q)",
|
||||
cfg.Audio.Sink, SinkOto, SinkNull)
|
||||
}
|
||||
// allowed_origins aus der Datei prüfen (Env wurde oben geparst): Eine
|
||||
// leere Liste ist gültig (Tür zu), einzelne Einträge müssen aber
|
||||
// substanziell sein — klarer Fehler statt stiller Ignoranz.
|
||||
if cfg.Lang != "de" && cfg.Lang != "en" {
|
||||
return cfg, fmt.Errorf("lang: invalid value %q (allowed: de, en)", cfg.Lang)
|
||||
}
|
||||
// Validate allowed_origins from the file (env was parsed above): an empty
|
||||
// list is fine (door shut), but entries must have substance.
|
||||
for _, o := range cfg.Server.AllowedOrigins {
|
||||
if strings.TrimSpace(o) == "" {
|
||||
return cfg, fmt.Errorf("server.allowed_origins: leerer Eintrag in %v", cfg.Server.AllowedOrigins)
|
||||
return cfg, fmt.Errorf("server.allowed_origins: empty entry in %v", cfg.Server.AllowedOrigins)
|
||||
}
|
||||
}
|
||||
// quiet_hours aus der Datei validieren (Env wurde oben geprüft) und
|
||||
// quiet_mode leer (Datei ohne Wert) auf den Default heben.
|
||||
// Validate quiet_hours from the file (env was checked above) and lift an
|
||||
// empty quiet_mode to the default.
|
||||
if cfg.QuietHours != "" {
|
||||
if _, err := queue.ParseQuietHours(cfg.QuietHours); err != nil {
|
||||
return cfg, err
|
||||
@@ -326,16 +326,16 @@ func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// splitOrigins zerlegt eine kommagetrennte Origin-Liste (Env-Form von
|
||||
// server.allowed_origins, z. B. "https://a.example, https://b.example").
|
||||
// Whitespace um Einträge toleriert, leere Einträge sind ein klarer Fehler.
|
||||
// splitOrigins splits a comma-separated origin list (the env form of
|
||||
// server.allowed_origins, for example "https://a.example, https://b.example").
|
||||
// Whitespace around entries is tolerated; empty entries are an error.
|
||||
func splitOrigins(v string) ([]string, error) {
|
||||
parts := strings.Split(v, ",")
|
||||
origins := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" {
|
||||
return nil, fmt.Errorf("leerer Eintrag in %q (komma-getrennte Origins oder \"off\")", v)
|
||||
return nil, fmt.Errorf("empty entry in %q (comma-separated origins, or \"off\")", v)
|
||||
}
|
||||
origins = append(origins, p)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user