Fix /api/contacts 500 error from datetime formatting
- _fmt_time crashed when PyMySQL returned datetime.datetime objects - updated_at now renders as 'Thu 13 Aug 2026, 13:49'
This commit is contained in:
605
src/vm_api.py
Normal file
605
src/vm_api.py
Normal file
@ -0,0 +1,605 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Voicemail API — pure JSON backend for the React portal.
|
||||
|
||||
Endpoints:
|
||||
POST /api/login
|
||||
POST /api/logout
|
||||
GET /api/messages
|
||||
POST /api/messages/{id}/read
|
||||
POST /api/messages/{id}/delete
|
||||
GET /api/settings
|
||||
POST /api/settings
|
||||
GET /api/contacts
|
||||
POST /api/contacts
|
||||
GET /api/contacts/{id}
|
||||
PUT /api/contacts/{id}
|
||||
DELETE /api/contacts/{id}
|
||||
POST /api/contacts/import_vcf
|
||||
GET /api/contacts/history/{number}
|
||||
GET /api/healthz
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import time
|
||||
|
||||
from fastapi import Cookie, FastAPI, Form, HTTPException, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from pydantic import BaseModel, EmailStr
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import vm_auth
|
||||
import vm_contacts
|
||||
import vm_store
|
||||
|
||||
SESSION_HOURS = int(os.environ.get("VM_SESSION_HOURS", "12"))
|
||||
COOKIE = "vm_session"
|
||||
BASE = os.environ.get("VM_BASE_PATH", "")
|
||||
SECURE_COOKIE = os.environ.get("VM_INSECURE_COOKIE", "") not in ("1", "yes", "true")
|
||||
|
||||
app = FastAPI(title="Voicemail API", docs_url=None, openapi_url=None)
|
||||
|
||||
# CORS — allow the React dev server and any static frontend host
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=os.environ.get("VM_CORS_ORIGINS", "*").split(","),
|
||||
allow_credentials=True,
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ sessions
|
||||
def new_session(mailbox):
|
||||
con = vm_store.connect()
|
||||
tok = secrets.token_urlsafe(32)
|
||||
now = int(time.time())
|
||||
con.execute("INSERT INTO sessions (token, mailbox, created_at, expires_at)"
|
||||
" VALUES (?,?,?,?)",
|
||||
(tok, str(mailbox), now, now + SESSION_HOURS * 3600))
|
||||
con.execute("DELETE FROM sessions WHERE expires_at < ?", (now,))
|
||||
con.commit()
|
||||
con.close()
|
||||
return tok
|
||||
|
||||
|
||||
def session_mailbox(token):
|
||||
if not token:
|
||||
return None
|
||||
con = vm_store.connect()
|
||||
r = con.execute("SELECT mailbox, expires_at FROM sessions WHERE token=?",
|
||||
(token,)).fetchone()
|
||||
con.close()
|
||||
if not r or r["expires_at"] < time.time():
|
||||
return None
|
||||
return r["mailbox"]
|
||||
|
||||
|
||||
def require(token):
|
||||
mb = session_mailbox(token)
|
||||
if not mb:
|
||||
raise HTTPException(status_code=401, detail="login")
|
||||
return mb
|
||||
|
||||
|
||||
# ------------------------------------------------------------ brute-force lockout
|
||||
MAX_FAILS = int(os.environ.get("VM_MAX_FAILS", "5"))
|
||||
LOCK_MINUTES = int(os.environ.get("VM_LOCK_MINUTES", "15"))
|
||||
_fails = {}
|
||||
|
||||
|
||||
def _lock_key(mailbox, ip):
|
||||
return (str(mailbox).strip(), ip)
|
||||
|
||||
|
||||
def _lock_check(mailbox, ip):
|
||||
e = _fails.get(_lock_key(mailbox, ip))
|
||||
if not e or e[0] < MAX_FAILS:
|
||||
return 0
|
||||
elapsed = time.time() - e[1]
|
||||
if elapsed > LOCK_MINUTES * 60:
|
||||
_fails.pop(_lock_key(mailbox, ip), None)
|
||||
return 0
|
||||
return max(1, int((LOCK_MINUTES * 60 - elapsed) // 60) + 1)
|
||||
|
||||
|
||||
def _lock_fail(mailbox, ip):
|
||||
k = _lock_key(mailbox, ip)
|
||||
e = _fails.get(k)
|
||||
now = time.time()
|
||||
if not e or now - e[1] > LOCK_MINUTES * 60:
|
||||
_fails[k] = [1, now]
|
||||
else:
|
||||
e[0] += 1
|
||||
|
||||
|
||||
def _lock_clear(mailbox, ip):
|
||||
_fails.pop(_lock_key(mailbox, ip), None)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ helpers
|
||||
def _fmt_time(ts):
|
||||
if not ts:
|
||||
return ""
|
||||
if hasattr(ts, "strftime"):
|
||||
return ts.strftime("%a %d %b %Y, %H:%M")
|
||||
return time.strftime("%a %d %b %Y, %H:%M", time.localtime(ts))
|
||||
|
||||
|
||||
def _fmt_dur(sec):
|
||||
if not sec:
|
||||
return ""
|
||||
return "%d:%02d" % (sec // 60, sec % 60)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ models
|
||||
class LoginReq(BaseModel):
|
||||
mailbox: str
|
||||
pin: str
|
||||
|
||||
|
||||
class ContactIn(BaseModel):
|
||||
name: str
|
||||
number: str = ""
|
||||
email: str | None = None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ routes
|
||||
@app.get("/api/healthz")
|
||||
def healthz():
|
||||
con = vm_store.connect()
|
||||
n = con.execute("SELECT COUNT(*) c FROM messages").fetchone()["c"]
|
||||
con.close()
|
||||
return {"ok": True, "messages": n}
|
||||
|
||||
|
||||
@app.post("/api/login")
|
||||
def login(req: LoginReq, request: Request, response: Response):
|
||||
ip = request.client.host if request.client else "?"
|
||||
locked = _lock_check(req.mailbox, ip)
|
||||
if locked:
|
||||
raise HTTPException(403, "Too many failed attempts. Try again in %d minutes." % locked)
|
||||
|
||||
info = vm_auth.check_login(req.mailbox, req.pin)
|
||||
if not info:
|
||||
_lock_fail(req.mailbox, ip)
|
||||
raise HTTPException(401, "Incorrect mailbox or PIN")
|
||||
_lock_clear(req.mailbox, ip)
|
||||
tok = new_session(req.mailbox.strip())
|
||||
response.set_cookie(COOKIE, tok, httponly=True, samesite="lax",
|
||||
secure=SECURE_COOKIE, max_age=SESSION_HOURS * 3600,
|
||||
path=BASE + "/")
|
||||
boxes = vm_auth.parse_mailboxes()
|
||||
return {
|
||||
"mailbox": req.mailbox.strip(),
|
||||
"name": boxes.get(req.mailbox.strip(), {}).get("name", ""),
|
||||
"token": tok,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/logout")
|
||||
def logout(vm_session: str = Cookie(default=None), response: Response = None):
|
||||
if vm_session:
|
||||
con = vm_store.connect()
|
||||
con.execute("DELETE FROM sessions WHERE token=?", (vm_session,))
|
||||
con.commit()
|
||||
con.close()
|
||||
response.delete_cookie(COOKIE, path=BASE + "/")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
def _current_user(vm_session: str = Cookie(default=None)):
|
||||
mb = require(vm_session)
|
||||
boxes = vm_auth.parse_mailboxes()
|
||||
return mb, boxes.get(mb, {}).get("name", "")
|
||||
|
||||
|
||||
@app.get("/api/messages")
|
||||
def list_messages(vm_session: str = Cookie(default=None),
|
||||
q: str = "", date_from: str = "", date_to: str = "", unread: str = ""):
|
||||
mb, name = _current_user(vm_session)
|
||||
con = vm_store.connect()
|
||||
|
||||
where = ["mailbox = ?"]
|
||||
params = [mb]
|
||||
if q:
|
||||
like = "%" + q.replace("%", "%%") + "%"
|
||||
where.append("(contact_name LIKE ? OR callerid LIKE ? OR summary LIKE ? OR transcript LIKE ?)")
|
||||
params.extend([like, like, like, like])
|
||||
if date_from:
|
||||
where.append("origtime >= ?")
|
||||
params.append(int(date_from))
|
||||
if date_to:
|
||||
where.append("origtime < ?")
|
||||
params.append(int(date_to) + 86400)
|
||||
if unread:
|
||||
where.append("is_read = 0")
|
||||
|
||||
sql = ("SELECT id, origtime, duration, callerid, contact_name, contact_email, "
|
||||
"summary, is_read, audio_sha, audio_ext FROM messages WHERE "
|
||||
+ " AND ".join(where) + " ORDER BY origtime DESC, id DESC")
|
||||
rows = con.execute(sql, params).fetchall()
|
||||
con.close()
|
||||
|
||||
out = []
|
||||
for r in rows:
|
||||
out.append({
|
||||
"id": r["id"],
|
||||
"origtime": r["origtime"],
|
||||
"time": _fmt_time(r["origtime"]),
|
||||
"duration": r["duration"],
|
||||
"duration_fmt": _fmt_dur(r["duration"]),
|
||||
"callerid": r["callerid"],
|
||||
"contact_name": r.get("contact_name"),
|
||||
"contact_email": r.get("contact_email"),
|
||||
"summary": r["summary"],
|
||||
"transcript": r.get("transcript"),
|
||||
"intents": r.get("intents"),
|
||||
"numbers": r.get("numbers"),
|
||||
"tags": json.loads(r["intents"] or "[]"),
|
||||
"is_read": bool(r["is_read"]),
|
||||
"has_audio": bool(r["audio_sha"]),
|
||||
})
|
||||
return {"mailbox": mb, "name": name, "messages": out}
|
||||
|
||||
|
||||
@app.post("/api/messages/{msg_id}/read")
|
||||
def toggle_read(msg_id: int, vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
con = vm_store.connect()
|
||||
con.execute("UPDATE messages SET is_read = 1 - is_read WHERE id=? AND mailbox=?",
|
||||
(msg_id, mb))
|
||||
con.commit()
|
||||
con.close()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/messages/{msg_id}/delete")
|
||||
def delete_message(msg_id: int, vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
con = vm_store.connect()
|
||||
r = con.execute("SELECT * FROM messages WHERE id=? AND mailbox=?",
|
||||
(msg_id, mb)).fetchone()
|
||||
if not r:
|
||||
con.close()
|
||||
raise HTTPException(404, "not found")
|
||||
if r["audio_sha"]:
|
||||
others = con.execute("SELECT COUNT(*) c FROM messages "
|
||||
"WHERE audio_sha=? AND id<>?",
|
||||
(r["audio_sha"], msg_id)).fetchone()["c"]
|
||||
if not others:
|
||||
try:
|
||||
os.unlink(vm_store.audio_path(r["audio_sha"], r["audio_ext"] or "wav"))
|
||||
except OSError:
|
||||
pass
|
||||
if r.get("spool_path"):
|
||||
base = os.path.splitext(r["spool_path"])[0]
|
||||
for ext in (".wav", ".WAV", ".gsm", ".txt", ".wav49"):
|
||||
try:
|
||||
os.unlink(base + ext)
|
||||
except OSError:
|
||||
pass
|
||||
con.execute("DELETE FROM messages WHERE id=? AND mailbox=?", (msg_id, mb))
|
||||
con.commit()
|
||||
con.close()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/api/settings")
|
||||
def get_settings(vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
con = vm_store.connect()
|
||||
cur = vm_store.get_settings(con, mb)
|
||||
con.close()
|
||||
return cur
|
||||
|
||||
|
||||
@app.post("/api/settings")
|
||||
async def save_settings(request: Request, vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
form = await request.form()
|
||||
con = vm_store.connect()
|
||||
for key, (default, _label) in vm_store.USER_SETTINGS.items():
|
||||
if default in ("yes", "no"):
|
||||
vm_store.set_setting(con, mb, key, "yes" if form.get(key) else "no")
|
||||
else:
|
||||
vm_store.set_setting(con, mb, key, (form.get(key) or "").strip())
|
||||
con.close()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/api/contacts")
|
||||
def list_contacts(vm_session: str = Cookie(default=None), q: str = ""):
|
||||
_current_user(vm_session)
|
||||
con = vm_store.connect()
|
||||
if q:
|
||||
like = "%" + q.replace("%", "%%") + "%"
|
||||
rows = con.execute(
|
||||
"SELECT id, number_e164, name, email, updated_at FROM contacts "
|
||||
"WHERE name LIKE ? OR number_e164 LIKE ? OR email LIKE ? "
|
||||
"ORDER BY name ASC, id DESC",
|
||||
(like, like, like),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = con.execute(
|
||||
"SELECT id, number_e164, name, email, updated_at FROM contacts "
|
||||
"ORDER BY name ASC, id DESC"
|
||||
).fetchall()
|
||||
con.close()
|
||||
return [{"id": r["id"], "number_e164": r.get("number_e164"),
|
||||
"name": r["name"], "email": r.get("email"),
|
||||
"updated_at": _fmt_time(r["updated_at"])} for r in rows]
|
||||
|
||||
|
||||
@app.get("/api/contacts/{cid}")
|
||||
def get_contact(cid: int, vm_session: str = Cookie(default=None)):
|
||||
_current_user(vm_session)
|
||||
con = vm_store.connect()
|
||||
c = con.execute("SELECT * FROM contacts WHERE id=?", (cid,)).fetchone()
|
||||
con.close()
|
||||
if not c:
|
||||
raise HTTPException(404, "contact not found")
|
||||
return {"id": c["id"], "number_e164": c.get("number_e164"),
|
||||
"name": c["name"], "email": c.get("email")}
|
||||
|
||||
|
||||
@app.post("/api/contacts")
|
||||
async def create_contact(body: ContactIn, vm_session: str = Cookie(default=None)):
|
||||
_current_user(vm_session)
|
||||
normalized = vm_contacts.normalize_uk(vm_contacts.digits_of(body.number)) if body.number else ""
|
||||
con = vm_store.connect()
|
||||
try:
|
||||
con.execute(
|
||||
"INSERT INTO contacts (number_e164, name, email) VALUES (?, ?, ?)",
|
||||
(normalized, body.name, body.email),
|
||||
)
|
||||
con.commit()
|
||||
cid = con.lastrowid
|
||||
finally:
|
||||
con.close()
|
||||
return {"id": cid, "number_e164": normalized, "name": body.name, "email": body.email}
|
||||
|
||||
|
||||
@app.put("/api/contacts/{cid}")
|
||||
async def update_contact(cid: int, body: ContactIn, vm_session: str = Cookie(default=None)):
|
||||
_current_user(vm_session)
|
||||
con = vm_store.connect()
|
||||
try:
|
||||
row = con.execute("SELECT id, number_e164 FROM contacts WHERE id=?", (cid,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "contact not found")
|
||||
normalized = vm_contacts.normalize_uk(vm_contacts.digits_of(body.number)) if body.number else ""
|
||||
con.execute(
|
||||
"UPDATE contacts SET name=?, email=?, number_e164=?, updated_at=NOW() WHERE id=?",
|
||||
(body.name, body.email, normalized, cid),
|
||||
)
|
||||
con.execute(
|
||||
"UPDATE messages SET contact_name=?, contact_email=? "
|
||||
"WHERE callerid LIKE ? AND (contact_name IS NULL OR contact_name = '')",
|
||||
(body.name, body.email, "%%%s%%" % normalized),
|
||||
)
|
||||
con.commit()
|
||||
finally:
|
||||
con.close()
|
||||
return {"id": cid, "number_e164": normalized, "name": body.name, "email": body.email}
|
||||
|
||||
|
||||
@app.delete("/api/contacts/{cid}")
|
||||
def delete_contact(cid: int, vm_session: str = Cookie(default=None)):
|
||||
_current_user(vm_session)
|
||||
con = vm_store.connect()
|
||||
con.execute("DELETE FROM contacts WHERE id=?", (cid,))
|
||||
con.commit()
|
||||
con.close()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/contacts/import_vcf")
|
||||
def import_vcf(vm_session: str = Cookie(default=None)):
|
||||
_current_user(vm_session)
|
||||
path = "/var/lib/vm-transcribe/contacts.vcf"
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(404, "VCF not found")
|
||||
try:
|
||||
with open(path, encoding="utf-8", errors="replace") as fh:
|
||||
entries = vm_contacts._parse_vcf(fh.read())
|
||||
except Exception as e:
|
||||
raise HTTPException(400, "VCF parse error: %s" % e)
|
||||
|
||||
con = vm_store.connect()
|
||||
existing = {
|
||||
r["number_e164"] for r in con.execute("SELECT number_e164 FROM contacts").fetchall()
|
||||
}
|
||||
added = skipped = 0
|
||||
for name, nums, emails in entries:
|
||||
if vm_contacts._is_junk_name(name):
|
||||
skipped += 1
|
||||
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 or e164 in existing:
|
||||
skipped += 1
|
||||
continue
|
||||
try:
|
||||
con.execute(
|
||||
"INSERT INTO contacts (number_e164, name, email) VALUES (?, ?, ?)",
|
||||
(e164, name, email),
|
||||
)
|
||||
existing.add(e164)
|
||||
added += 1
|
||||
except Exception:
|
||||
skipped += 1
|
||||
con.commit()
|
||||
con.close()
|
||||
return {"ok": True, "added": added, "skipped": skipped}
|
||||
|
||||
|
||||
@app.get("/api/contacts/history/{num}")
|
||||
def contact_history(num: str, vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
normalized = vm_contacts.normalize_uk(vm_contacts.digits_of(num)) or num
|
||||
con = vm_store.connect()
|
||||
rows = con.execute(
|
||||
"SELECT id, origtime, duration, callerid, contact_name, contact_email, "
|
||||
"summary, is_read, audio_sha FROM messages WHERE mailbox=? AND callerid LIKE ? "
|
||||
"ORDER BY origtime DESC, id DESC",
|
||||
(mb, "%%%s%%" % normalized),
|
||||
).fetchall()
|
||||
c = con.execute(
|
||||
"SELECT name FROM contacts WHERE number_e164 = ? LIMIT 1",
|
||||
(normalized,),
|
||||
).fetchone()
|
||||
con.close()
|
||||
who = c["name"] if c and c.get("name") else num
|
||||
out = []
|
||||
for r in rows:
|
||||
out.append({
|
||||
"id": r["id"],
|
||||
"origtime": r["origtime"],
|
||||
"time": _fmt_time(r["origtime"]),
|
||||
"duration": r["duration"],
|
||||
"duration_fmt": _fmt_dur(r["duration"]),
|
||||
"callerid": r["callerid"],
|
||||
"contact_name": r.get("contact_name"),
|
||||
"contact_email": r.get("contact_email"),
|
||||
"summary": r["summary"],
|
||||
"transcript": r.get("transcript"),
|
||||
"intents": r.get("intents"),
|
||||
"numbers": r.get("numbers"),
|
||||
"tags": json.loads(r["intents"] or "[]"),
|
||||
"is_read": bool(r["is_read"]),
|
||||
"has_audio": bool(r["audio_sha"]),
|
||||
})
|
||||
return {"contact": who, "number": normalized, "messages": out}
|
||||
|
||||
|
||||
@app.get("/api/transcript/{msg_id}")
|
||||
def get_transcript(msg_id: int, vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
con = vm_store.connect()
|
||||
r = con.execute(
|
||||
"SELECT transcript FROM messages WHERE id=? AND mailbox=?",
|
||||
(msg_id, mb),
|
||||
).fetchone()
|
||||
con.close()
|
||||
if not r:
|
||||
raise HTTPException(404, "not found")
|
||||
return {"id": msg_id, "transcript": r["transcript"] or ""}
|
||||
|
||||
|
||||
@app.get("/api/audio/{msg_id}/info")
|
||||
def audio_info(msg_id: int, vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
con = vm_store.connect()
|
||||
r = con.execute("SELECT audio_sha, audio_ext FROM messages WHERE id=? AND mailbox=?",
|
||||
(msg_id, mb)).fetchone()
|
||||
con.close()
|
||||
if not r or not r["audio_sha"]:
|
||||
raise HTTPException(404, "not found")
|
||||
p = vm_store.audio_path(r["audio_sha"], r["audio_ext"] or "wav")
|
||||
return {"id": msg_id, "path": p, "exists": os.path.exists(p)}
|
||||
|
||||
|
||||
class AddContactIn(BaseModel):
|
||||
number: str = ""
|
||||
name: str
|
||||
email: str | None = None
|
||||
|
||||
|
||||
@app.post("/api/add_contact")
|
||||
async def add_contact(body: AddContactIn, vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
num = (body.number or "").strip()
|
||||
name = (body.name or "").strip()
|
||||
email = (body.email or "").strip() or None
|
||||
if not name:
|
||||
raise HTTPException(400, "Name is required")
|
||||
normalized = vm_contacts.normalize_uk(vm_contacts.digits_of(num)) if num else None
|
||||
con = vm_store.connect()
|
||||
if normalized:
|
||||
row = con.execute(
|
||||
"SELECT id FROM contacts WHERE number_e164 = ?", (normalized,)
|
||||
).fetchone()
|
||||
if row and row.get("id"):
|
||||
con.execute(
|
||||
"UPDATE contacts SET name=?, email=?, updated_at=NOW() WHERE id=?",
|
||||
(name, email, row["id"]),
|
||||
)
|
||||
else:
|
||||
con.execute(
|
||||
"INSERT INTO contacts (number_e164, name, email) VALUES (?, ?, ?)",
|
||||
(normalized, name, email),
|
||||
)
|
||||
con.execute(
|
||||
"UPDATE messages SET contact_name=?, contact_email=? "
|
||||
"WHERE callerid LIKE ? AND (contact_name IS NULL OR contact_name = '')",
|
||||
(name, email, "%%%s%%" % normalized),
|
||||
)
|
||||
con.commit()
|
||||
con.close()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/contacts/delete_all")
|
||||
def delete_all_contacts(vm_session: str = Cookie(default=None)):
|
||||
_current_user(vm_session)
|
||||
con = vm_store.connect()
|
||||
con.execute("DELETE FROM contacts")
|
||||
con.commit()
|
||||
con.close()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
class AddContactIn(BaseModel):
|
||||
number: str = ""
|
||||
name: str
|
||||
email: str | None = None
|
||||
|
||||
|
||||
@app.post("/api/add_contact")
|
||||
async def add_contact(body: AddContactIn, vm_session: str = Cookie(default=None)):
|
||||
mb = _current_user(vm_session)[0]
|
||||
num = (body.number or "").strip()
|
||||
name = (body.name or "").strip()
|
||||
email = (body.email or "").strip() or None
|
||||
if not name:
|
||||
raise HTTPException(400, "Name is required")
|
||||
normalized = vm_contacts.normalize_uk(vm_contacts.digits_of(num)) if num else None
|
||||
con = vm_store.connect()
|
||||
if normalized:
|
||||
row = con.execute(
|
||||
"SELECT id FROM contacts WHERE number_e164 = ?", (normalized,)
|
||||
).fetchone()
|
||||
if row and row.get("id"):
|
||||
con.execute(
|
||||
"UPDATE contacts SET name=?, email=?, updated_at=NOW() WHERE id=?",
|
||||
(name, email, row["id"]),
|
||||
)
|
||||
else:
|
||||
con.execute(
|
||||
"INSERT INTO contacts (number_e164, name, email) VALUES (?, ?, ?)",
|
||||
(normalized, name, email),
|
||||
)
|
||||
con.execute(
|
||||
"UPDATE messages SET contact_name=?, contact_email=? "
|
||||
"WHERE callerid LIKE ? AND (contact_name IS NULL OR contact_name = '')",
|
||||
(name, email, "%%%s%%" % normalized),
|
||||
)
|
||||
con.commit()
|
||||
con.close()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.post("/api/contacts/delete_all")
|
||||
def delete_all_contacts(vm_session: str = Cookie(default=None)):
|
||||
_current_user(vm_session)
|
||||
con = vm_store.connect()
|
||||
con.execute("DELETE FROM contacts")
|
||||
con.commit()
|
||||
con.close()
|
||||
return {"ok": True}
|
||||
Reference in New Issue
Block a user