import datetime from dataclasses import dataclass from typing import Optional import httpx @dataclass class CallMetadata: call_id: Optional[str] = None caller_number: Optional[str] = None callee_number: Optional[str] = None employee: Optional[str] = None direction: Optional[str] = None duration_sec: Optional[int] = None datetime: Optional[str] = None audio_url: Optional[str] = None class NovofonApiClient: def __init__(self, access_token: str = None, base_url: str = None): from config import config self.access_token = access_token or config.NOVOFON_ACCESS_TOKEN self.base_url = (base_url or config.NOVOFON_API_BASE_URL).rstrip("/") self._client = httpx.Client( headers={"Content-Type": "application/json; charset=UTF-8"}, timeout=60, ) def _request(self, method: str, params: dict) -> list: payload = { "jsonrpc": "2.0", "id": 1, "method": method, "params": params, } resp = self._client.post(self.base_url, json=payload) resp.raise_for_status() result = resp.json() if "error" in result: err = result["error"] msg = err.get("message", "") mnemonic = err.get("data", {}).get("mnemonic", "") raise RuntimeError(f"Novofon API error [{mnemonic}]: {msg}") return result.get("result", {}).get("data", []) CALL_FIELDS = [ "communication_id", "direction", "contact_phone_number", "virtual_phone_number", "start_time", "talk_duration", "total_duration", "call_records", "wav_call_records", "full_record_file_link", "employees", ] def get_calls_report( self, date_from: str, date_till: str, offset: int = 0, limit: int = 10000, sort: Optional[list] = None, ) -> list[dict]: params = { "access_token": self.access_token, "date_from": date_from, "date_till": date_till, "offset": offset, "limit": limit, "fields": self.CALL_FIELDS, } if sort: params["sort"] = sort return self._request("get.calls_report", params) def get_all_calls( self, date_from: str, date_till: Optional[str] = None, ) -> list[CallMetadata]: if " " not in date_from: date_from = f"{date_from} 00:00:00" start = datetime.datetime.strptime(date_from, "%Y-%m-%d %H:%M:%S") if date_till: if " " not in date_till: date_till = f"{date_till} 23:59:59" end = datetime.datetime.strptime(date_till, "%Y-%m-%d %H:%M:%S") else: end = datetime.datetime.utcnow() # Build chunks from oldest to newest chunks: list[tuple[str, str]] = [] chunk_start = start while chunk_start < end: chunk_end = min(chunk_start + datetime.timedelta(days=90), end) chunks.append(( chunk_start.strftime("%Y-%m-%d %H:%M:%S"), chunk_end.strftime("%Y-%m-%d %H:%M:%S"), )) chunk_start = chunk_end # Iterate chunks newest-first so --limit gets recent calls sort_desc = [{"field": "start_time", "order": "desc"}] all_calls: list[CallMetadata] = [] for from_str, till_str in reversed(chunks): offset = 0 page_size = 10000 while True: batch = self.get_calls_report( from_str, till_str, offset=offset, limit=page_size, sort=sort_desc, ) if not batch: break for item in batch: meta = self._to_metadata(item) if meta: all_calls.append(meta) if len(batch) < page_size: break offset += page_size return all_calls @staticmethod def extract_audio_url(data: dict) -> Optional[str]: full = data.get("full_record_file_link") if full: return full comm_id = data.get("communication_id") records = data.get("call_records", []) if records and comm_id: return f"https://app.novofon.ru/system/media/talk/{comm_id}/{records[0]}/" wav_records = data.get("wav_call_records", []) if wav_records and comm_id: return f"https://app.novofon.ru/system/media/wav/{comm_id}/{wav_records[0]}/" return None @staticmethod def _to_metadata(data: dict) -> Optional[CallMetadata]: comm_id = data.get("communication_id") if not comm_id: return None direction = data.get("direction") is_inbound = direction == "in" contact = data.get("contact_phone_number", "") or "" virtual = data.get("virtual_phone_number", "") or "" if is_inbound: caller_number = contact callee_number = virtual else: caller_number = virtual callee_number = contact employees = data.get("employees") or [] employee = employees[0].get("employee_full_name") if employees else None raw_time = data.get("start_time") iso_time = None if raw_time and " " in raw_time: try: dt = datetime.datetime.strptime(raw_time, "%Y-%m-%d %H:%M:%S") iso_time = dt.strftime("%Y-%m-%dT%H:%M:%SZ") except ValueError: pass duration = data.get("talk_duration") or data.get("total_duration") return CallMetadata( call_id=str(comm_id), caller_number=caller_number if caller_number else None, callee_number=callee_number if callee_number else None, employee=employee, direction="inbound" if is_inbound else "outbound", duration_sec=duration, datetime=iso_time, audio_url=NovofonApiClient.extract_audio_url(data), ) def close(self): self._client.close()