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
This commit is contained in:
@ -3,6 +3,7 @@
|
|||||||
Caller-ID -> contact name resolution for Asterisk voicemail notifications.
|
Caller-ID -> contact name resolution for Asterisk voicemail notifications.
|
||||||
|
|
||||||
Backends (tried in the order given by contacts.conf 'backends='):
|
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.
|
file - local vCard (.vcf) or CSV export; no auth, offline, fast.
|
||||||
google - Google People API via the Hermes OAuth token.
|
google - Google People API via the Hermes OAuth token.
|
||||||
carddav - generic CardDAV with username + app password.
|
carddav - generic CardDAV with username + app password.
|
||||||
@ -21,6 +22,7 @@ import configparser
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
@ -311,6 +313,41 @@ def lookup_google(cfg, num_digits, n, log):
|
|||||||
return None
|
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
|
# ---------------------------------------------------------- carddav backend
|
||||||
def lookup_carddav(cfg, num_digits, n, log):
|
def lookup_carddav(cfg, num_digits, n, log):
|
||||||
import ssl
|
import ssl
|
||||||
@ -356,7 +393,7 @@ def lookup_carddav(cfg, num_digits, n, log):
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------- entrypoint
|
# ---------------------------------------------------------------- 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):
|
def resolve(caller_id, log=print):
|
||||||
|
|||||||
84
src/vm_import_contacts.py
Normal file
84
src/vm_import_contacts.py
Normal file
@ -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()
|
||||||
Reference in New Issue
Block a user