115 lines
3.5 KiB
Python
115 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
import os
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
LOGGER = logging.getLogger("uvicorn.error")
|
|
|
|
|
|
def _b64url_encode(raw: bytes) -> str:
|
|
return base64.urlsafe_b64encode(raw).decode("utf-8").rstrip("=")
|
|
|
|
|
|
def _sign_hs256(message: str, secret: str) -> str:
|
|
digest = hmac.new(
|
|
secret.encode("utf-8"),
|
|
message.encode("utf-8"),
|
|
hashlib.sha256,
|
|
).digest()
|
|
return _b64url_encode(digest)
|
|
|
|
|
|
def _issue_service_token(secret: str) -> str:
|
|
now = datetime.now(timezone.utc)
|
|
header = {"alg": "HS256", "typ": "JWT"}
|
|
payload = {
|
|
"sub": "svc:realtime-voice",
|
|
"username": "realtime-voice",
|
|
"role": "admin",
|
|
"auth_source": "service",
|
|
"iat": int(now.timestamp()),
|
|
"exp": int((now + timedelta(seconds=300)).timestamp()),
|
|
}
|
|
encoded_header = _b64url_encode(json.dumps(header, separators=(",", ":")).encode("utf-8"))
|
|
encoded_payload = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
|
|
message = f"{encoded_header}.{encoded_payload}"
|
|
return f"{message}.{_sign_hs256(message, secret)}"
|
|
|
|
|
|
def _enabled() -> bool:
|
|
return os.getenv("CRM_INTERACTION_ENABLED", "0").strip().lower() in {"1", "true", "yes"}
|
|
|
|
|
|
def _base_url() -> str:
|
|
return str(os.getenv("CRM_INTERACTION_SERVICE_URL", "http://interaction-service:8000")).rstrip("/")
|
|
|
|
|
|
def _secret() -> str:
|
|
return str(os.getenv("CRM_APP_TOKEN_SECRET", "")).strip()
|
|
|
|
|
|
def _queue_id() -> str | None:
|
|
raw = os.getenv("CRM_QUEUE_ID", "").strip()
|
|
return raw or None
|
|
|
|
|
|
async def create_interaction(*, call_id: str) -> str | None:
|
|
if not _enabled():
|
|
return None
|
|
secret = _secret()
|
|
if not secret:
|
|
LOGGER.warning("crm: CRM_APP_TOKEN_SECRET not configured, skipping interaction creation")
|
|
return None
|
|
token = _issue_service_token(secret)
|
|
payload: dict[str, Any] = {
|
|
"channel": "voice",
|
|
"subject": f"AI Voice Call [{call_id}]",
|
|
"priority": 3,
|
|
}
|
|
queue = _queue_id()
|
|
if queue:
|
|
payload["queue_id"] = queue
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
resp = await client.post(
|
|
f"{_base_url()}/interactions",
|
|
json=payload,
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
interaction_id = str(data.get("interaction_id") or "").strip()
|
|
LOGGER.info("crm: interaction created interaction_id=%s call_id=%s", interaction_id, call_id)
|
|
return interaction_id or None
|
|
except Exception:
|
|
LOGGER.exception("crm: failed to create interaction call_id=%s", call_id)
|
|
return None
|
|
|
|
|
|
async def close_interaction(interaction_id: str) -> None:
|
|
if not _enabled():
|
|
return
|
|
secret = _secret()
|
|
if not secret:
|
|
return
|
|
token = _issue_service_token(secret)
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
resp = await client.patch(
|
|
f"{_base_url()}/interactions/{interaction_id}/status",
|
|
json={"status": "closed"},
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
resp.raise_for_status()
|
|
LOGGER.info("crm: interaction closed interaction_id=%s", interaction_id)
|
|
except Exception:
|
|
LOGGER.exception("crm: failed to close interaction interaction_id=%s", interaction_id)
|