# Configuration The backend services run from `/opt/vm-transcribe/`. Live secret-bearing files live there (some git-ignored). The repo ships `*.example` templates under `config/`. Most settings can also be set via environment variables, which is how the systemd unit wires production values. Env vars override file values when both are present unless noted. --- ## Environment variables ### Shared / store (`vm_store.py`, used by mailcmd, web, api, import) | Variable | Default | Meaning | |---|---|---| | `VM_DB` | `/var/lib/vm-transcribe/voicemail.db` | SQLite path — **used only as a fallback** when no MySQL is configured. | | `VM_AUDIO_DIR` | `/var/lib/vm-transcribe/audio` | Where copied recordings live (`/.wav`). | | `VM_LOG` | `/var/log/asterisk/vm_mailcmd.log` | Log path. | | `VM_MODEL_CACHE` | `/opt/vm-transcribe/models` | Whisper model download/load dir. | | `VM_WHISPER_MODEL` | `base.en` | Model name. `tiny`/`base`/`small` trade speed for accuracy. | | `VM_SPOOL` | `/var/spool/asterisk/voicemail` | Spool root to scan (import). | | `VM_ASTERISK_CONF` | `/etc/asterisk/voicemail.conf` | For PIN lookup. | ### MySQL (default store + contacts) Set these as env vars (the API unit reads `/opt/vm-transcribe/api.env`) **or** put them in `/opt/vm-transcribe/db_secret` (mode `640 root:root`, `KEY=VALUE` lines). When a credentials source exists, `vm_store` uses MySQL; otherwise it falls back to SQLite. | Variable | Default | Meaning | |---|---|---| | `VM_MYSQL_HOST` | `localhost` | MySQL host. | | `VM_MYSQL_USER` | `asterisk` | Least-privilege DB user (owns the `asterisk` DB; not root). | | `VM_MYSQL_PASSWORD` | (from `db_secret`) | DB password. | | `VM_MYSQL_DB` | `asterisk` | Database name — the **same** DB Asterisk's CDR uses. | `db_secret` key names: `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DB`. ### Portal / API hardening | Var | Default | Meaning | |---|---|---| | `VM_SESSION_HOURS` | `12` | Session lifetime. | | `VM_MAX_FAILS` | `5` | Failed attempts per (mailbox, IP) before locking. | | `VM_LOCK_MINUTES` | `15` | Lock duration. | | `VM_INSECURE_COOKIE` | unset | Set to `1`/`yes`/`true` only for plain-HTTP testing; normally cookies are `Secure`. | | `VM_BASE_PATH` | `""` | Set to e.g. `/voicemail` if served under a sub-path. | | `VM_CORS_ORIGINS` | `*` | Comma list of allowed CORS origins for the JSON API. | --- ## voicemail.conf (Asterisk) One line is all the pipeline needs: ```ini mailcmd=/opt/vm-transcribe/venv/bin/python3 /opt/vm-transcribe/vm_mailcmd.py ``` Place it in the `[general]` section (or any context). After editing: ```bash sudo asterisk -rx 'voicemail reload' ``` `install.sh` backs up the file and adds this line automatically. --- ## telegram.conf ```ini [telegram] enabled = yes token = 123456789:AAH... ; from @BotFather default_chat_id = ; fallback for unmapped mailboxes (blank = none) send_audio = yes ; voice note with caption send_transcript = yes ; full transcript as a follow-up message timeout = 20 ; seconds per send attempt [mailbox:1001] chat_id = 123456789 ; single recipient [mailbox:1002] chat_id = 5551, 5552 ; comma list fans out to several people send_transcript = no ; any global key can be overridden per mailbox [mailbox:1003] chat_id = -1001234567890 ; negative = group chat send_audio = no ; text-only DM ``` **Routing rules** (evaluated per voicemail): 1. Match the `[mailbox:N]` section for the voicemail's mailbox number. 2. `chat_id` may be a single id, a comma-separated list (fan-out), or a group id (negative). 3. If no mailbox section matches, fall back to `default_chat_id`. 4. If `enabled = no` or no route resolves, nothing is sent — email still goes out. 5. The file is re-read on every voicemail; no reload needed after editing. **Delivery fallback chain** (per chat id, tried in order): `sendVoice` → `sendDocument` → `sendMessage` (text-only). So a Telegram outage or a bad audio file still delivers the summary. > Contact resolution note: a user must message the bot once before it can DM > them. Telegram does not permit bots to initiate conversations. --- ## contacts.conf ```ini [contacts] enabled = yes backends = mysql ; ONLY 'mysql' is implemented (MySQL contacts table). # The schema still parses file/google/carddav entries, # but the registry is {"mysql": lookup_mysql} so any # other value is ignored / logged as unknown. cache_path = /var/lib/vm-transcribe/contacts_cache.json cache_ttl = 86400 [file] ; NOT USED — kept for reference only [google] ; NOT USED — kept for reference only [carddav] ; NOT USED — kept for reference only ``` **Contacts are resolved exclusively from the MySQL `contacts` table** on the `asterisk` DB. Numbers are stored canonical E.164; `normalize_uk()` collapses `+44…` and `0…` (and `0044…`) so both forms match. Matching uses the full digit string (up to 15 digits), not just last-9. Creating/updating a contact via the API backfills `messages.contact_name`/`contact_email` for matching callerids. Load the table from a vCard/CSV export with: ```bash sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \ /opt/vm-transcribe/vm_import_contacts.py /path/to/contacts.vcf ``` --- ## Portal settings (per mailbox, in MySQL) These are edited from the React portal's **Settings** page by each user; they are not config files. Every value defaults to `yes`. | Setting | Effect | |---|---| | `summarise` | Run the summariser (if off, the raw transcript is shown). | | `telegram` | Forward new voicemails to the mailbox's Telegram route (requires telegram.conf). | | `email_notify` | Send the enriched email (if off, the message is stored but not emailed). | | `highlight` | Colour the caller chip when a name is resolved from contacts. | | `max_inbox` | Keep at most N messages; oldest beyond N are auto-deleted (0 = unlimited). | --- ## systemd units Two units, both running as `asterisk` and binding loopback only. ### vm-api.service (:8098 JSON API) ```ini Environment=VM_SESSION_HOURS=12 EnvironmentFile=-/opt/vm-transcribe/api.env ; MySQL creds + VM_* overrides # Hardening: ProtectSystem=full, ProtectHome=read-only, NoNewPrivileges, # PrivateTmp, IPAddressAllow=127.0.0.1 ExecStart=/opt/vm-transcribe/venv/bin/uvicorn vm_api:app --host 127.0.0.1 --port 8098 --workers 2 ``` ### vm-portal.service (:8099 legacy HTML / audio) ```ini Environment=VM_DB=/var/lib/vm-transcribe/voicemail.db Environment=VM_AUDIO_DIR=/var/lib/vm-transcribe/audio Environment=VM_ASTERISK_CONF=/etc/asterisk/voicemail.conf Environment=VM_SESSION_HOURS=12 ExecStart=/opt/vm-transcribe/venv/bin/python -m uvicorn vm_web:app \ --host 127.0.0.1 --port 8099 --proxy-headers --forwarded-allow-ips 127.0.0.1 # Hardening: NoNewPrivileges, PrivateTmp, ProtectSystem=full, ProtectHome, # ReadWritePaths=/var/lib/vm-transcribe /var/log/asterisk, # IPAddressAllow=localhost, IPAddressDeny=any ``` > A service restart clears the in-memory lockout counters. Testing lockout will > lock you out of your own verification for `VM_LOCK_MINUTES`.