import json import hmac import hashlib from typing import Any, Dict, Optional from urllib.parse import urljoin import requests from django.conf import settings from django.core.exceptions import ImproperlyConfigured class PlugNMeetError(Exception): def __init__(self, message: str, *, status_code: Optional[int] = None, response_data: Optional[Dict[str, Any]] = None): super().__init__(message) self.status_code = status_code self.response_data = response_data or {} class PlugNMeetClient: def __init__(self, *, base_url: Optional[str] = None, api_key: Optional[str] = None, api_secret: Optional[str] = None, timeout: Optional[float] = None): self.base_url = (base_url or getattr(settings, "PLUGNMEET_SERVER_URL", "")).rstrip("/") self.api_key = api_key or getattr(settings, "PLUGNMEET_API_KEY", "") self.api_secret = api_secret or getattr(settings, "PLUGNMEET_API_SECRET", "") self.timeout = timeout or getattr(settings, "PLUGNMEET_TIMEOUT", 10.0) if not self.base_url or not self.api_key or not self.api_secret: raise ImproperlyConfigured("PlugNMeet integration settings are incomplete.") def create_room(self, payload: Dict[str, Any]) -> Dict[str, Any]: # Convert entire payload keys to camelCase as required by PlugNMeet protocol prepared = self._camelize_dict(payload) return self._post("/auth/room/create", prepared) def get_join_token(self, payload: Dict[str, Any]) -> Dict[str, Any]: # Convert entire payload keys to camelCase as required by PlugNMeet protocol prepared = self._camelize_dict(payload) return self._post("/auth/room/getJoinToken", prepared) def is_room_active(self, room_id: str) -> Dict[str, Any]: return self._post("/auth/room/isRoomActive", {"roomId": room_id}) def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]: url = urljoin(f"{self.base_url}/", path.lstrip("/")) body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")) headers = { "Content-Type": "application/json", "API-KEY": self.api_key, "HASH-SIGNATURE": self._build_signature(body), } try: response = requests.post(url, data=body.encode("utf-8"), headers=headers, timeout=self.timeout) except requests.RequestException as exc: raise PlugNMeetError("Failed to reach PlugNMeet server.") from exc if response.status_code >= 400: response_data = self._safe_json(response) raise PlugNMeetError( "PlugNMeet server returned an error.", status_code=response.status_code, response_data=response_data, ) data = self._safe_json(response) if data is None: raise PlugNMeetError("PlugNMeet server returned an invalid response format.") if isinstance(data, dict) and data.get('status') is False: error_message = data.get('msg') or data.get('message') or "PlugNMeet operation failed." raise PlugNMeetError( error_message, status_code=response.status_code, response_data=data, ) return data @staticmethod def _snake_to_camel(key: str) -> str: parts = key.split("_") if not parts: return key return parts[0] + "".join(p.capitalize() or "" for p in parts[1:]) def _camelize_dict(self, obj: Any) -> Any: if isinstance(obj, dict): return {self._snake_to_camel(k): self._camelize_dict(v) for k, v in obj.items()} if isinstance(obj, list): return [self._camelize_dict(v) for v in obj] return obj def _build_signature(self, body: str) -> str: digest = hmac.new(self.api_secret.encode("utf-8"), body.encode("utf-8"), hashlib.sha256) return digest.hexdigest() @staticmethod def _safe_json(response: requests.Response) -> Optional[Dict[str, Any]]: try: return response.json() except ValueError: return None __all__ = ["PlugNMeetClient", "PlugNMeetError"]