Files
2026-05-01 17:18:35 +05:00

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)