From d2f1ca8f9d55b4e241a92409433f9293bcdd9a0d Mon Sep 17 00:00:00 2001 From: jp Date: Thu, 13 Aug 2026 13:16:13 +0100 Subject: [PATCH] Add MySQL contacts table + mysql backend for vm_contacts - CREATE TABLE contacts (number_e164 UNIQUE, name, email) in asterisk DB - new lookup_mysql() backend, registered as first in _BACKENDS - vm_import_contacts.py: import VCF into MySQL (527 added) - contacts.conf: backends=mysql,file - live: 16 of 130 messages resolved via MySQL contacts --- src/vm_contacts.py | 39 +++++++++++++++++- src/vm_import_contacts.py | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/vm_import_contacts.py diff --git a/src/vm_contacts.py b/src/vm_contacts.py index a5ca1e3..0ddafeb 100644 --- a/src/vm_contacts.py +++ b/src/vm_contacts.py @@ -3,6 +3,7 @@ Caller-ID -> contact name resolution for Asterisk voicemail notifications. Backends (tried in the order given by contacts.conf 'backends='): + mysql - MySQL contacts table on the asterisk DB (primary, fast, offline). file - local vCard (.vcf) or CSV export; no auth, offline, fast. google - Google People API via the Hermes OAuth token. carddav - generic CardDAV with username + app password. @@ -21,6 +22,7 @@ import configparser import json import os import re +import sys import time import urllib.error import urllib.parse @@ -311,6 +313,41 @@ def lookup_google(cfg, num_digits, n, log): return None +# ----------------------------------------------------------- mysql backend +def lookup_mysql(cfg, num_digits, n, log): + """Look up a contact in the asterisk MySQL DB contacts table. + + Uses vm_store.connect() so it shares the same asterisk DB config and + PyMySQL dependency already required by the portal. If the table does + not not exist yet, run vm_import_contacts.py to create and populate it. + """ + try: + sys_path = os.path.dirname(os.path.abspath(__file__)) + if sys_path not in sys.path: + sys.path.insert(0, sys_path) + import vm_store # noqa: E402 (local import to avoid hard dep at import time) + except Exception as e: + log("contacts mysql backend: vm_store unavailable (%s)" % e) + return None + + num = extract_number(num_digits) + if not num: + return None + try: + con = vm_store.connect() + row = con.execute( + "SELECT name, email FROM contacts " + "WHERE number_e164 = ? AND name IS NOT NULL AND name != '' " + "ORDER BY id LIMIT 1", + (num,)).fetchone() + con.close() + if row: + return (row["name"], row.get("email") or None) + except Exception as e: + log("contacts mysql backend error: %s" % e) + return None + + # ---------------------------------------------------------- carddav backend def lookup_carddav(cfg, num_digits, n, log): import ssl @@ -356,7 +393,7 @@ def lookup_carddav(cfg, num_digits, n, log): # ---------------------------------------------------------------- entrypoint -_BACKENDS = {"file": lookup_file, "google": lookup_google, "carddav": lookup_carddav} +_BACKENDS = {"mysql": lookup_mysql, "file": lookup_file, "google": lookup_google, "carddav": lookup_carddav} def resolve(caller_id, log=print): diff --git a/src/vm_import_contacts.py b/src/vm_import_contacts.py new file mode 100644 index 0000000..6a5e550 --- /dev/null +++ b/src/vm_import_contacts.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Import /var/lib/vm-transcribe/contacts.vcf into the MySQL contacts table. + +Idempotent: re-runs skip rows whose number_e164 already exists (UNIQUE key). +Caller-visible numbers are stored as the canonical E.164 form produced by +vm_contacts.normalize_uk, matching what Asterisk stores in callerid. + +Run as the asterisk user: + sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 /opt/vm-transcribe/vm_import_contacts.py +""" +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import vm_contacts +import vm_store + + +VCF_PATH = "/var/lib/vm-transcribe/contacts.vcf" +DB_TABLE = "contacts" + + +def main(): + total = added = skipped = 0 + con = vm_store.connect() + + # Ensure table exists. + con.execute( + "CREATE TABLE IF NOT EXISTS %s (" + "id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, " + "number_e164 VARCHAR(20) NOT NULL UNIQUE, " + "name VARCHAR(255) NOT NULL, " + "email VARCHAR(255), " + "created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP " + "ON UPDATE CURRENT_TIMESTAMP, " + "INDEX idx_number (number_e164), " + "INDEX idx_name (name)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" % DB_TABLE) + con.commit() + + if not os.path.exists(VCF_PATH): + print("VCF not found:", VCF_PATH) + return + + with open(VCF_PATH, encoding="utf-8", errors="replace") as fh: + entries = vm_contacts._parse_vcf(fh.read()) + + # Load existing numbers so we can skip idempotently without relying on + # INSERT IGNORE swallowing other constraint failures. + existing = { + r["number_e164"] for r in con.execute( + "SELECT number_e164 FROM %s" % DB_TABLE).fetchall() + } + + for name, nums, emails in entries: + if vm_contacts._is_junk_name(name): + continue + email = emails[0] if emails else None + for raw_num in nums: + e164 = vm_contacts.normalize_uk(vm_contacts.digits_of(raw_num)) + if not e164: + continue + total += 1 + if e164 in existing: + skipped += 1 + continue + try: + con.execute( + "INSERT INTO %s (number_e164, name, email) VALUES (?, ?, ?)" + % DB_TABLE, (e164, name, email)) + existing.add(e164) + added += 1 + except Exception as e: + print("insert error for %s (%s): %s" % (e164, name, e)) + + con.commit() + con.close() + print("vcf entries: %d, added: %d, skipped (already known): %d" % ( + total, added, skipped)) + + +if __name__ == "__main__": + main()