intra_max_chatbot/external/intradesk_api.py
2026-03-26 15:39:02 +03:00

235 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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