README, docs/{ARCHITECTURE,CONFIGURATION,INSTALL,OPERATIONS,SECURITY,TESTING}
and CHANGELOG were stale: they described the original SQLite + CardDAV +
zero-JS vm_web portal. Brought them in line with the actual code:
- MySQL (PyMySQL) as the default store; SQLite fallback only
- vm_contacts resolves from the MySQL contacts table (file/google/carddav removed)
- React SPA frontend + vm_api.py JSON API (:8098); vm_web.py now serves /audio/
- Apache vhost proxies /api/ -> :8098, /audio/ -> :8099, serves the SPA
- CSP relaxed to script-src 'self' 'unsafe-inline' (UI is now JavaScript)
- Added CHANGELOG 1.2.0 entry for the React/vm_api frontend work
193 lines
7.1 KiB
Markdown
193 lines
7.1 KiB
Markdown
# 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 (`<sha[:2]>/<sha>.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`.
|