Files
realtime_voice_service/callback_client.py
T
2026-05-02 00:48:40 +05:00

121 lines
3.7 KiB
Python

from __future__ import annotations
import logging
import os
from typing import Any
import httpx
from realtime_voice_service.crm_client import _issue_service_token
LOGGER = logging.getLogger("uvicorn.error")
def _enabled() -> bool:
return os.getenv("CALLBACK_DISPATCH_ENABLED", "1").strip().lower() in {"1", "true", "yes"}
def _bridge_base_url() -> str:
return str(
os.getenv("ASTERISK_BRIDGE_SERVICE_URL", "http://asterisk-bridge-service:8000")
).rstrip("/")
def _secret() -> str:
return str(os.getenv("CRM_APP_TOKEN_SECRET", "")).strip()
def _request_headers() -> dict[str, str] | None:
secret = _secret()
if not secret:
return None
return {"Authorization": f"Bearer {_issue_service_token(secret)}"}
async def schedule_callback(
*,
phone: str,
in_minutes: int | None = None,
scheduled_for: str | None = None,
reason: str | None = None,
customer_name: str | None = None,
source_call_id: str | None = None,
source_interaction_id: str | None = None,
idempotency_key: str | None = None,
) -> dict[str, Any] | None:
if not _enabled():
LOGGER.info("callback dispatch disabled by env")
return None
headers = _request_headers()
if headers is None:
LOGGER.warning("callback client: CRM_APP_TOKEN_SECRET is not configured")
return None
payload: dict[str, Any] = {"phone": phone}
if in_minutes is not None:
payload["in_minutes"] = int(in_minutes)
if scheduled_for:
payload["scheduled_for"] = scheduled_for
if reason:
payload["reason"] = reason
if customer_name:
payload["customer_name"] = customer_name
if source_call_id:
payload["source_call_id"] = source_call_id
if source_interaction_id:
payload["source_interaction_id"] = source_interaction_id
if idempotency_key:
payload["idempotency_key"] = idempotency_key
url = f"{_bridge_base_url()}/asterisk/callbacks"
try:
async with httpx.AsyncClient(timeout=8.0) as client:
resp = await client.post(url, json=payload, headers=headers)
if resp.status_code in (200, 201):
data = resp.json()
LOGGER.info(
"callback scheduled: job_id=%s phone=%s scheduled_for=%s",
data.get("job_id"),
data.get("phone_e164"),
data.get("scheduled_for"),
)
return data
if resp.status_code == 409:
LOGGER.info("callback already scheduled for phone=%s: %s", phone, resp.text[:200])
return {"status": "conflict", "detail": resp.text}
LOGGER.warning(
"callback schedule failed: status=%s body=%s",
resp.status_code,
resp.text[:300],
)
return None
except Exception:
LOGGER.exception("callback client: request failed phone=%s", phone)
return None
async def cancel_callback(*, job_id: str) -> dict[str, Any] | None:
if not _enabled():
return None
headers = _request_headers()
if headers is None:
return None
url = f"{_bridge_base_url()}/asterisk/callbacks/{job_id}"
try:
async with httpx.AsyncClient(timeout=8.0) as client:
resp = await client.delete(url, headers=headers)
if resp.status_code == 200:
return resp.json()
LOGGER.warning(
"callback cancel failed: job_id=%s status=%s body=%s",
job_id,
resp.status_code,
resp.text[:300],
)
return None
except Exception:
LOGGER.exception("callback client: cancel failed job_id=%s", job_id)
return None