""" Intradesk REST API: создание заявок и чтение истории/комментариев. Эндпоинты: POST /changes/v3/tasks — создать заявку GET /taskhistory/api/v2.0/lifetime/{id}/full — история (комментарии, смена статуса...) """ from __future__ import annotations import json import logging import httpx import config logger = logging.getLogger(__name__) async def get_task_last_comment(task_id: int) -> dict | None: """ Получить последний комментарий к заявке из Intradesk. Возвращает dict: {text, author, date} или None если нет комментариев. """ key = config.INTRADESK_TASKS_API_KEY if not key: return None url = ( f"{config.INTRADESK_BASE_URL.rstrip('/')}" f"/taskhistory/api/v2.0/lifetime/{task_id}/full" f"?ApiKey={key}&sortDirection=Desc&top=10&events=50" ) try: async with httpx.AsyncClient() as client: resp = await client.get(url, timeout=15.0) if resp.status_code != 200: logger.warning( "get_task_last_comment task=%s → HTTP %s", task_id, resp.status_code ) return None data = resp.json() except Exception as exc: logger.warning("get_task_last_comment error: %s", exc) return None # Ищем последнее событие с blockname="comment" (type=50) for entry in data.get("data") or []: for ev in (entry.get("events") or {}).get("data") or []: if ev.get("blockname") == "comment" and ev.get("type") == 50: raw_date = entry.get("eventat") or "" # "2021-03-15T11:54:36.92Z" → "2021-03-15 11:54" short_date = raw_date[:16].replace("T", " ") return { "text": ev.get("stringvalue") or "", "author": entry.get("username") or "—", "date": short_date, } return None async def get_client_open_tasks( client_id: int, page: int = 0, page_size: int = 5, ) -> dict: """ Получить незакрытые заявки клиента из Intradesk API. Возвращает {"tasks": [...], "total": int}. Каждая задача содержит поля из OData + "last_comment" (dict или None). """ key = config.INTRADESK_TASKS_API_KEY if not key: return {"tasks": [], "total": 0} skip = page * page_size url = ( f"{config.INTRADESK_BASE_URL.rstrip('/')}/tasklist/odata/v3/tasks" f"?ApiKey={key}" f"&$filter=(clientid eq {client_id}) and (closedat eq null) and (isarchived eq false)" f"&$orderby=updatedat desc" f"&$top={page_size}&$skip={skip}" f"&$count=true" ) try: async with httpx.AsyncClient() as client: resp = await client.get(url, timeout=20.0) if resp.status_code != 200: logger.warning("get_client_open_tasks client=%s → HTTP %s", client_id, resp.status_code) return {"tasks": [], "total": 0} data = resp.json() except Exception as exc: logger.warning("get_client_open_tasks error: %s", exc) return {"tasks": [], "total": 0} total = data.get("@odata.count") or 0 raw_tasks = data.get("value") or [] # Словари для разрешения имён dicts_map: dict[int, str] = {} for d in data.get("dictionaries") or []: did = d.get("id") dname = d.get("name") or "" if isinstance(dname, dict): dname = dname.get("ru") or dname.get("en") or "" if did: dicts_map[did] = dname tasks = [] for t in raw_tasks: status_id = t.get("status") last_comment = _extract_last_comment_from_lifetime(t.get("lifetime")) tasks.append({ "id": t.get("id"), "task_number": t.get("tasknumber"), "name": t.get("name") or "", "status_name": dicts_map.get(status_id, "") if status_id else "", "created_at": t.get("createdat") or "", "updated_at": t.get("updatedat") or "", "last_comment": last_comment, }) return {"tasks": tasks, "total": total} def _extract_last_comment_from_lifetime(lifetime: dict | None) -> dict | None: """Извлечь последний комментарий из поля lifetime задачи (если последнее событие — комментарий).""" if not lifetime: return None for entry in (lifetime.get("data") or []): for ev in (entry.get("events") or {}).get("data") or []: if ev.get("blockname") == "comment" and ev.get("type") == 50: raw_date = entry.get("eventat") or "" short_date = raw_date[:10] # "2026-03-26" return { "text": ev.get("stringvalue") or "", "author": entry.get("username") or "—", "date": short_date, } return None async def create_intradesk_task( name: str, description: str, client_intradesk_id: int | None = None, contact_person_id: int | None = None, attachment_urls: list[str] | None = None, ) -> dict | None: """ Создать заявку в Intradesk. Возвращает ответ API (dict с полями id, tasknumber и др.) или None при ошибке. Вложения — пока добавляются как ссылки в конец описания (до появления upload API). В dry-run режиме запрос печатается в консоль и возвращается None. """ key = config.INTRADESK_CHANGES_API_KEY if not key: logger.warning("create_intradesk_task: INTRADESK_CHANGES_API_KEY не задан") return None # Вложения — ссылки в конце описания full_desc = description if attachment_urls: full_desc += "\n\nВложения:\n" + "\n".join(attachment_urls) blocks: dict[str, str] = { "name": json.dumps({"value": name}, ensure_ascii=False), "description": json.dumps({"value": full_desc}, ensure_ascii=False), } # initiator: вариант 3 из документации — клиент + пользователь клиента. # clientid заполняется автоматически из initiatorclient, отдельно не нужен. if client_intradesk_id is not None and contact_person_id is not None: blocks["initiator"] = json.dumps( {"value": {"groupid": client_intradesk_id, "userid": contact_person_id}}, ensure_ascii=False, ) elif client_intradesk_id is not None: # только клиент (нет конкретного контактного лица) blocks["initiator"] = json.dumps( {"value": {"groupid": client_intradesk_id}}, ensure_ascii=False ) if config.INTRADESK_DEFAULT_SERVICE: blocks["service"] = json.dumps( {"value": config.INTRADESK_DEFAULT_SERVICE}, ensure_ascii=False ) if config.INTRADESK_DEFAULT_TASK_TYPE: blocks["tasktype"] = json.dumps( {"value": int(config.INTRADESK_DEFAULT_TASK_TYPE)}, ensure_ascii=False ) body = {"blocks": blocks, "Channel": "web"} url = f"{config.INTRADESK_BASE_URL.rstrip('/')}/changes/v3/tasks?ApiKey={key}" if contact_person_id is not None: url += f"&userid={contact_person_id}" if config.DRY_RUN: print( "\n[DRY-RUN] create_intradesk_task — запрос НЕ отправлен\n" f" POST {url}\n" f" Body: {json.dumps(body, ensure_ascii=False, indent=2)}\n" ) return None try: async with httpx.AsyncClient() as client: resp = await client.post( url, json=body, headers={"Content-Type": "application/json"}, timeout=20.0, ) if resp.status_code not in (200, 201): logger.error( "create_intradesk_task → HTTP %s: %s", resp.status_code, resp.text[:500], ) return None result = resp.json() # API может вернуть как "Number"/"Id" (PascalCase), так и "number"/"id" task_id = result.get("id") or result.get("Id") or result.get("ID") task_number = ( result.get("tasknumber") or result.get("number") or result.get("Number") or result.get("TaskNumber") ) logger.info("create_intradesk_task ok: id=%s number=%s", task_id, task_number) return result except Exception as exc: logger.exception("create_intradesk_task error: %s", exc) return None