кеш норм
This commit is contained in:
parent
6f0e66b4ae
commit
c783fcb92b
5 changed files with 157 additions and 53 deletions
|
|
@ -31,7 +31,7 @@ from database.queries.intradesk import get_intradesk_contact_id_for_phone
|
||||||
from database.queries.organizations import get_organization
|
from database.queries.organizations import get_organization
|
||||||
from database.queries.ticket_attachments import add_attachment
|
from database.queries.ticket_attachments import add_attachment
|
||||||
from database.queries.tickets import create_ticket, set_intradesk_task_id
|
from database.queries.tickets import create_ticket, set_intradesk_task_id
|
||||||
from external.intradesk_api import create_intradesk_task
|
from external.intradesk_api import create_intradesk_task, invalidate_client_cache
|
||||||
|
|
||||||
logger = get_logger("bot.handlers.create_ticket")
|
logger = get_logger("bot.handlers.create_ticket")
|
||||||
|
|
||||||
|
|
@ -432,6 +432,8 @@ async def cb_confirm_ticket(
|
||||||
"Intradesk task created: id=%s number=%s for ticket_id=%s",
|
"Intradesk task created: id=%s number=%s for ticket_id=%s",
|
||||||
iid, intradesk_number, ticket_id,
|
iid, intradesk_number, ticket_id,
|
||||||
)
|
)
|
||||||
|
if intradesk_id:
|
||||||
|
invalidate_client_cache(intradesk_id)
|
||||||
|
|
||||||
# ── Итоговое сообщение ────────────────────────────────────────────────────
|
# ── Итоговое сообщение ────────────────────────────────────────────────────
|
||||||
lines = ["✅ Заявка успешно создана!"]
|
lines = ["✅ Заявка успешно создана!"]
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,13 @@ from bot.states import CommentStates
|
||||||
from database.connection import get_db
|
from database.connection import get_db
|
||||||
from database.queries.intradesk import get_intradesk_contact_id_for_phone
|
from database.queries.intradesk import get_intradesk_contact_id_for_phone
|
||||||
from database.queries.organizations import get_organization
|
from database.queries.organizations import get_organization
|
||||||
from external.intradesk_api import add_task_comment, get_client_open_tasks, get_task_last_comment
|
from external.intradesk_api import (
|
||||||
|
add_task_comment,
|
||||||
|
get_client_open_tasks,
|
||||||
|
get_task_last_comment,
|
||||||
|
invalidate_client_cache,
|
||||||
|
invalidate_task_comment,
|
||||||
|
)
|
||||||
|
|
||||||
router = Router(router_id="view_tickets")
|
router = Router(router_id="view_tickets")
|
||||||
|
|
||||||
|
|
@ -329,17 +335,19 @@ async def on_comment_text(
|
||||||
return
|
return
|
||||||
|
|
||||||
data = await context.get_data()
|
data = await context.get_data()
|
||||||
|
task_id = data.get("comment_task_id")
|
||||||
task_number = data.get("comment_task_number")
|
task_number = data.get("comment_task_number")
|
||||||
|
|
||||||
# Сбрасываем состояние до отправки API — защита от повторов
|
# Сбрасываем состояние до отправки API — защита от повторов
|
||||||
await context.clear()
|
await context.clear()
|
||||||
|
|
||||||
# Получаем contact_person_id
|
# Получаем данные организации (нужны и для contact_person_id, и для инвалидации кэша)
|
||||||
|
intradesk_id: int | None = None
|
||||||
contact_person_id: int | None = None
|
contact_person_id: int | None = None
|
||||||
if bot_user.get("phone") and bot_user.get("org_id"):
|
if bot_user.get("org_id"):
|
||||||
org = await get_organization(bot_user["org_id"])
|
org = await get_organization(bot_user["org_id"])
|
||||||
intradesk_id = org.get("intradesk_id") if org else None
|
intradesk_id = org.get("intradesk_id") if org else None
|
||||||
if intradesk_id:
|
if intradesk_id and bot_user.get("phone"):
|
||||||
db = get_db()
|
db = get_db()
|
||||||
contact_person_id = await get_intradesk_contact_id_for_phone(
|
contact_person_id = await get_intradesk_contact_id_for_phone(
|
||||||
db, intradesk_id, bot_user["phone"]
|
db, intradesk_id, bot_user["phone"]
|
||||||
|
|
@ -351,6 +359,13 @@ async def on_comment_text(
|
||||||
contact_person_id=contact_person_id,
|
contact_person_id=contact_person_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
# Сбрасываем кэш — следующий переход в меню/список покажет свежие данные
|
||||||
|
if task_id:
|
||||||
|
invalidate_task_comment(task_id)
|
||||||
|
if intradesk_id:
|
||||||
|
invalidate_client_cache(intradesk_id)
|
||||||
|
|
||||||
from bot.handlers.menu import build_main_menu_text
|
from bot.handlers.menu import build_main_menu_text
|
||||||
|
|
||||||
if ok:
|
if ok:
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
183
external/intradesk_api.py
vendored
183
external/intradesk_api.py
vendored
|
|
@ -3,13 +3,20 @@ Intradesk REST API: создание заявок и чтение истории
|
||||||
|
|
||||||
Эндпоинты:
|
Эндпоинты:
|
||||||
POST /changes/v3/tasks — создать заявку
|
POST /changes/v3/tasks — создать заявку
|
||||||
|
PUT /changes/v3/tasks — изменить заявку / добавить комментарий
|
||||||
GET /taskhistory/api/v2.0/lifetime/{id}/full — история (комментарии, смена статуса...)
|
GET /taskhistory/api/v2.0/lifetime/{id}/full — история (комментарии, смена статуса...)
|
||||||
|
|
||||||
|
Кэширование:
|
||||||
|
Все GET-запросы кэшируются в памяти с TTL, чтобы не дёргать API при каждом
|
||||||
|
переходе между экранами. Мутации (create/comment) сбрасывают нужные ключи.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
|
@ -17,6 +24,8 @@ import config
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# ── HTML-очистка ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_TAG_RE = re.compile(r"<[^>]+>")
|
_TAG_RE = re.compile(r"<[^>]+>")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -25,25 +34,84 @@ def _strip_html(text: str) -> str:
|
||||||
if not text:
|
if not text:
|
||||||
return text
|
return text
|
||||||
cleaned = _TAG_RE.sub("", text)
|
cleaned = _TAG_RE.sub("", text)
|
||||||
# Схлопнуть множественные пробелы/переносы
|
|
||||||
cleaned = re.sub(r"\s{2,}", " ", cleaned).strip()
|
cleaned = re.sub(r"\s{2,}", " ", cleaned).strip()
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
# ── TTL-кэш ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Структура: ключ → (expires_at: float, данные: Any)
|
||||||
|
_cache: dict[str, tuple[float, Any]] = {}
|
||||||
|
|
||||||
|
# TTL по типу данных
|
||||||
|
_TTL_COUNT = 60.0 # count_client_open_tasks
|
||||||
|
_TTL_LAST_COMMENT = 60.0 # get_client_last_comment
|
||||||
|
_TTL_OPEN_TASKS = 30.0 # get_client_open_tasks (список страниц)
|
||||||
|
_TTL_TASK_COMMENT = 30.0 # get_task_last_comment (история одной задачи)
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_get(key: str) -> tuple[bool, Any]:
|
||||||
|
"""Вернуть (True, значение) если ключ свежий, иначе (False, None)."""
|
||||||
|
entry = _cache.get(key)
|
||||||
|
if entry is None:
|
||||||
|
return False, None
|
||||||
|
expires_at, data = entry
|
||||||
|
if time.monotonic() > expires_at:
|
||||||
|
del _cache[key]
|
||||||
|
return False, None
|
||||||
|
return True, data
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_set(key: str, value: Any, ttl: float) -> None:
|
||||||
|
_cache[key] = (time.monotonic() + ttl, value)
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_delete_prefix(prefix: str) -> None:
|
||||||
|
"""Удалить все ключи начинающиеся с prefix."""
|
||||||
|
for k in [k for k in _cache if k.startswith(prefix)]:
|
||||||
|
del _cache[k]
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_client_cache(client_id: int) -> None:
|
||||||
|
"""
|
||||||
|
Сбросить кэш клиента после мутации (создание заявки и т.п.).
|
||||||
|
|
||||||
|
Очищает: счётчик, последний комментарий, все страницы списка заявок.
|
||||||
|
"""
|
||||||
|
_cache_delete_prefix(f"count:{client_id}")
|
||||||
|
_cache_delete_prefix(f"last_comment:{client_id}")
|
||||||
|
_cache_delete_prefix(f"open_tasks:{client_id}:")
|
||||||
|
logger.debug("cache invalidated for client_id=%s", client_id)
|
||||||
|
|
||||||
|
|
||||||
|
def invalidate_task_comment(task_id: int) -> None:
|
||||||
|
"""Сбросить кэш истории конкретной задачи после добавления комментария."""
|
||||||
|
_cache.pop(f"task_comment:{task_id}", None)
|
||||||
|
logger.debug("cache invalidated for task_id=%s", task_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ── API-функции ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def get_task_last_comment(task_id: int) -> dict | None:
|
async def get_task_last_comment(task_id: int) -> dict | None:
|
||||||
"""
|
"""
|
||||||
Получить последний комментарий к заявке из Intradesk.
|
Получить последний комментарий к заявке из Intradesk.
|
||||||
|
|
||||||
Возвращает dict: {text, author, date} или None если нет комментариев.
|
Возвращает dict: {text, author, date} или None если нет комментариев.
|
||||||
|
Результат кэшируется на _TTL_TASK_COMMENT секунд.
|
||||||
"""
|
"""
|
||||||
key = config.INTRADESK_TASKS_API_KEY
|
ck = f"task_comment:{task_id}"
|
||||||
if not key:
|
hit, cached = _cache_get(ck)
|
||||||
|
if hit:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
api_key = config.INTRADESK_TASKS_API_KEY
|
||||||
|
if not api_key:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
url = (
|
url = (
|
||||||
f"{config.INTRADESK_BASE_URL.rstrip('/')}"
|
f"{config.INTRADESK_BASE_URL.rstrip('/')}"
|
||||||
f"/taskhistory/api/v2.0/lifetime/{task_id}/full"
|
f"/taskhistory/api/v2.0/lifetime/{task_id}/full"
|
||||||
f"?ApiKey={key}&sortDirection=Desc&top=10&events=50"
|
f"?ApiKey={api_key}&sortDirection=Desc&top=10&events=50"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
|
|
@ -58,33 +126,43 @@ async def get_task_last_comment(task_id: int) -> dict | None:
|
||||||
logger.warning("get_task_last_comment error: %s", exc)
|
logger.warning("get_task_last_comment error: %s", exc)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Ищем последнее событие с blockname="comment" (type=50)
|
result: dict | None = None
|
||||||
for entry in data.get("data") or []:
|
for entry in data.get("data") or []:
|
||||||
for ev in (entry.get("events") or {}).get("data") or []:
|
for ev in (entry.get("events") or {}).get("data") or []:
|
||||||
if ev.get("blockname") == "comment" and ev.get("type") == 50:
|
if ev.get("blockname") == "comment" and ev.get("type") == 50:
|
||||||
raw_date = entry.get("eventat") or ""
|
raw_date = entry.get("eventat") or ""
|
||||||
# "2021-03-15T11:54:36.92Z" → "2021-03-15 11:54"
|
result = {
|
||||||
short_date = raw_date[:16].replace("T", " ")
|
|
||||||
return {
|
|
||||||
"text": _strip_html(ev.get("stringvalue") or ""),
|
"text": _strip_html(ev.get("stringvalue") or ""),
|
||||||
"author": entry.get("username") or "—",
|
"author": entry.get("username") or "—",
|
||||||
"date": short_date,
|
"date": raw_date[:16].replace("T", " "),
|
||||||
}
|
}
|
||||||
return None
|
break
|
||||||
|
if result:
|
||||||
|
break
|
||||||
|
|
||||||
|
_cache_set(ck, result, _TTL_TASK_COMMENT)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def count_client_open_tasks(client_id: int) -> int:
|
async def count_client_open_tasks(client_id: int) -> int:
|
||||||
"""Количество незакрытых заявок клиента через API.
|
|
||||||
|
|
||||||
Intradesk не возвращает @odata.count при $top=0, поэтому берём $top=1
|
|
||||||
— count в ответе отражает полный итог, а не размер страницы.
|
|
||||||
"""
|
"""
|
||||||
key = config.INTRADESK_TASKS_API_KEY
|
Количество незакрытых заявок клиента.
|
||||||
if not key:
|
|
||||||
|
Intradesk не возвращает @odata.count при $top=0, поэтому берём $top=1.
|
||||||
|
Результат кэшируется на _TTL_COUNT секунд.
|
||||||
|
"""
|
||||||
|
ck = f"count:{client_id}"
|
||||||
|
hit, cached = _cache_get(ck)
|
||||||
|
if hit:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
api_key = config.INTRADESK_TASKS_API_KEY
|
||||||
|
if not api_key:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
url = (
|
url = (
|
||||||
f"{config.INTRADESK_BASE_URL.rstrip('/')}/tasklist/odata/v3/tasks"
|
f"{config.INTRADESK_BASE_URL.rstrip('/')}/tasklist/odata/v3/tasks"
|
||||||
f"?ApiKey={key}"
|
f"?ApiKey={api_key}"
|
||||||
f"&$filter=(clientid eq {client_id}) and (closedat eq null) and (isarchived eq false)"
|
f"&$filter=(clientid eq {client_id}) and (closedat eq null) and (isarchived eq false)"
|
||||||
f"&$top=1&$count=true"
|
f"&$top=1&$count=true"
|
||||||
)
|
)
|
||||||
|
|
@ -94,31 +172,35 @@ async def count_client_open_tasks(client_id: int) -> int:
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
return 0
|
return 0
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
# @odata.count — полный итог по фильтру, не зависит от $top
|
|
||||||
count = data.get("@odata.count")
|
count = data.get("@odata.count")
|
||||||
if count is not None:
|
result = int(count) if count is not None else len(data.get("value") or [])
|
||||||
return int(count)
|
|
||||||
# Fallback: если count не вернулся — считаем по размеру value
|
|
||||||
return len(data.get("value") or [])
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
_cache_set(ck, result, _TTL_COUNT)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
async def get_client_last_comment(client_id: int) -> dict | None:
|
async def get_client_last_comment(client_id: int) -> dict | None:
|
||||||
"""
|
"""
|
||||||
Найти последний комментарий по любой незакрытой заявке клиента.
|
Найти последний комментарий по любой незакрытой заявке клиента.
|
||||||
|
|
||||||
Берём задачу с самым свежим updatedat → ищем последний комментарий в её истории.
|
Берём задачу с самым свежим updatedat → ищем комментарий в её истории.
|
||||||
Возвращает {text, author, date, task_number} или None.
|
Возвращает {text, author, date, task_number} или None.
|
||||||
|
Результат кэшируется на _TTL_LAST_COMMENT секунд.
|
||||||
"""
|
"""
|
||||||
key = config.INTRADESK_TASKS_API_KEY
|
ck = f"last_comment:{client_id}"
|
||||||
if not key:
|
hit, cached = _cache_get(ck)
|
||||||
|
if hit:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
api_key = config.INTRADESK_TASKS_API_KEY
|
||||||
|
if not api_key:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Шаг 1: самая свежая задача клиента
|
|
||||||
url = (
|
url = (
|
||||||
f"{config.INTRADESK_BASE_URL.rstrip('/')}/tasklist/odata/v3/tasks"
|
f"{config.INTRADESK_BASE_URL.rstrip('/')}/tasklist/odata/v3/tasks"
|
||||||
f"?ApiKey={key}"
|
f"?ApiKey={api_key}"
|
||||||
f"&$filter=(clientid eq {client_id}) and (closedat eq null) and (isarchived eq false)"
|
f"&$filter=(clientid eq {client_id}) and (closedat eq null) and (isarchived eq false)"
|
||||||
f"&$orderby=updatedat desc&$top=1"
|
f"&$orderby=updatedat desc&$top=1"
|
||||||
)
|
)
|
||||||
|
|
@ -133,18 +215,22 @@ async def get_client_last_comment(client_id: int) -> dict | None:
|
||||||
|
|
||||||
tasks = data.get("value") or []
|
tasks = data.get("value") or []
|
||||||
if not tasks:
|
if not tasks:
|
||||||
|
_cache_set(ck, None, _TTL_LAST_COMMENT)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
task = tasks[0]
|
task = tasks[0]
|
||||||
task_id = task.get("id")
|
task_id = task.get("id")
|
||||||
task_number = task.get("tasknumber")
|
task_number = task.get("tasknumber")
|
||||||
if not task_id:
|
if not task_id:
|
||||||
|
_cache_set(ck, None, _TTL_LAST_COMMENT)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Шаг 2: последний комментарий этой задачи
|
# get_task_last_comment имеет собственный кэш — двойного запроса не будет
|
||||||
comment = await get_task_last_comment(task_id)
|
comment = await get_task_last_comment(task_id)
|
||||||
if comment:
|
if comment:
|
||||||
comment["task_number"] = task_number
|
comment = {**comment, "task_number": task_number}
|
||||||
|
|
||||||
|
_cache_set(ck, comment, _TTL_LAST_COMMENT)
|
||||||
return comment
|
return comment
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -157,18 +243,24 @@ async def get_client_open_tasks(
|
||||||
Получить незакрытые заявки клиента из Intradesk API.
|
Получить незакрытые заявки клиента из Intradesk API.
|
||||||
|
|
||||||
Возвращает {"tasks": [...], "total": int}.
|
Возвращает {"tasks": [...], "total": int}.
|
||||||
Последний комментарий загружается параллельно через history API для каждой задачи.
|
Последний комментарий загружается параллельно через history API.
|
||||||
|
Результат кэшируется на _TTL_OPEN_TASKS секунд.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
key = config.INTRADESK_TASKS_API_KEY
|
ck = f"open_tasks:{client_id}:{page}:{page_size}"
|
||||||
if not key:
|
hit, cached = _cache_get(ck)
|
||||||
|
if hit:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
api_key = config.INTRADESK_TASKS_API_KEY
|
||||||
|
if not api_key:
|
||||||
return {"tasks": [], "total": 0}
|
return {"tasks": [], "total": 0}
|
||||||
|
|
||||||
skip = page * page_size
|
skip = page * page_size
|
||||||
url = (
|
url = (
|
||||||
f"{config.INTRADESK_BASE_URL.rstrip('/')}/tasklist/odata/v3/tasks"
|
f"{config.INTRADESK_BASE_URL.rstrip('/')}/tasklist/odata/v3/tasks"
|
||||||
f"?ApiKey={key}"
|
f"?ApiKey={api_key}"
|
||||||
f"&$filter=(clientid eq {client_id}) and (closedat eq null) and (isarchived eq false)"
|
f"&$filter=(clientid eq {client_id}) and (closedat eq null) and (isarchived eq false)"
|
||||||
f"&$orderby=updatedat desc"
|
f"&$orderby=updatedat desc"
|
||||||
f"&$top={page_size}&$skip={skip}"
|
f"&$top={page_size}&$skip={skip}"
|
||||||
|
|
@ -188,7 +280,7 @@ async def get_client_open_tasks(
|
||||||
total = data.get("@odata.count") or 0
|
total = data.get("@odata.count") or 0
|
||||||
raw_tasks = data.get("value") or []
|
raw_tasks = data.get("value") or []
|
||||||
|
|
||||||
# Словари для разрешения имён
|
# Словари для разрешения имён статусов
|
||||||
dicts_map: dict[int, str] = {}
|
dicts_map: dict[int, str] = {}
|
||||||
for d in data.get("dictionaries") or []:
|
for d in data.get("dictionaries") or []:
|
||||||
did = d.get("id")
|
did = d.get("id")
|
||||||
|
|
@ -198,7 +290,6 @@ async def get_client_open_tasks(
|
||||||
if did:
|
if did:
|
||||||
dicts_map[did] = dname
|
dicts_map[did] = dname
|
||||||
|
|
||||||
# Базовые данные задач
|
|
||||||
base_tasks = []
|
base_tasks = []
|
||||||
for t in raw_tasks:
|
for t in raw_tasks:
|
||||||
status_id = t.get("status")
|
status_id = t.get("status")
|
||||||
|
|
@ -211,7 +302,7 @@ async def get_client_open_tasks(
|
||||||
"updated_at": t.get("updatedat") or "",
|
"updated_at": t.get("updatedat") or "",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Параллельно загружаем последний комментарий для каждой задачи
|
# Параллельно загружаем последний комментарий (используют собственный кэш)
|
||||||
task_ids = [t["id"] for t in base_tasks if t["id"]]
|
task_ids = [t["id"] for t in base_tasks if t["id"]]
|
||||||
comments = await asyncio.gather(
|
comments = await asyncio.gather(
|
||||||
*[get_task_last_comment(tid) for tid in task_ids],
|
*[get_task_last_comment(tid) for tid in task_ids],
|
||||||
|
|
@ -223,22 +314,23 @@ async def get_client_open_tasks(
|
||||||
task["last_comment"] = comment if isinstance(comment, dict) else None
|
task["last_comment"] = comment if isinstance(comment, dict) else None
|
||||||
tasks.append(task)
|
tasks.append(task)
|
||||||
|
|
||||||
return {"tasks": tasks, "total": total}
|
result = {"tasks": tasks, "total": total}
|
||||||
|
_cache_set(ck, result, _TTL_OPEN_TASKS)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _extract_last_comment_from_lifetime(lifetime: dict | None) -> dict | None:
|
def _extract_last_comment_from_lifetime(lifetime: dict | None) -> dict | None:
|
||||||
"""Извлечь последний комментарий из поля lifetime задачи (если последнее событие — комментарий)."""
|
"""Извлечь последний комментарий из поля lifetime задачи."""
|
||||||
if not lifetime:
|
if not lifetime:
|
||||||
return None
|
return None
|
||||||
for entry in (lifetime.get("data") or []):
|
for entry in (lifetime.get("data") or []):
|
||||||
for ev in (entry.get("events") or {}).get("data") or []:
|
for ev in (entry.get("events") or {}).get("data") or []:
|
||||||
if ev.get("blockname") == "comment" and ev.get("type") == 50:
|
if ev.get("blockname") == "comment" and ev.get("type") == 50:
|
||||||
raw_date = entry.get("eventat") or ""
|
raw_date = entry.get("eventat") or ""
|
||||||
short_date = raw_date[:16].replace("T", " ") # "2026-03-26 11:54"
|
|
||||||
return {
|
return {
|
||||||
"text": _strip_html(ev.get("stringvalue") or ""),
|
"text": _strip_html(ev.get("stringvalue") or ""),
|
||||||
"author": entry.get("username") or "—",
|
"author": entry.get("username") or "—",
|
||||||
"date": short_date,
|
"date": raw_date[:16].replace("T", " "),
|
||||||
}
|
}
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -249,9 +341,10 @@ async def add_task_comment(
|
||||||
contact_person_id: int | None = None,
|
contact_person_id: int | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Добавить комментарий к заявке через Intradesk API (PUT /changes/v3/tasks).
|
Добавить комментарий к заявке (PUT /changes/v3/tasks).
|
||||||
|
|
||||||
Возвращает True при успехе, False при ошибке.
|
Возвращает True при успехе, False при ошибке.
|
||||||
|
Кэш задачи сбрасывается вызывающей стороной через invalidate_task_comment.
|
||||||
"""
|
"""
|
||||||
key = config.INTRADESK_CHANGES_API_KEY
|
key = config.INTRADESK_CHANGES_API_KEY
|
||||||
if not key:
|
if not key:
|
||||||
|
|
@ -307,15 +400,13 @@ async def create_intradesk_task(
|
||||||
Создать заявку в Intradesk.
|
Создать заявку в Intradesk.
|
||||||
|
|
||||||
Возвращает ответ API (dict с полями id, tasknumber и др.) или None при ошибке.
|
Возвращает ответ API (dict с полями id, tasknumber и др.) или None при ошибке.
|
||||||
Вложения — пока добавляются как ссылки в конец описания (до появления upload API).
|
Кэш клиента сбрасывается вызывающей стороной через invalidate_client_cache.
|
||||||
В dry-run режиме запрос печатается в консоль и возвращается None.
|
|
||||||
"""
|
"""
|
||||||
key = config.INTRADESK_CHANGES_API_KEY
|
key = config.INTRADESK_CHANGES_API_KEY
|
||||||
if not key:
|
if not key:
|
||||||
logger.warning("create_intradesk_task: INTRADESK_CHANGES_API_KEY не задан")
|
logger.warning("create_intradesk_task: INTRADESK_CHANGES_API_KEY не задан")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Вложения — ссылки в конце описания
|
|
||||||
full_desc = description
|
full_desc = description
|
||||||
if attachment_urls:
|
if attachment_urls:
|
||||||
full_desc += "\n\nВложения:\n" + "\n".join(attachment_urls)
|
full_desc += "\n\nВложения:\n" + "\n".join(attachment_urls)
|
||||||
|
|
@ -324,15 +415,12 @@ async def create_intradesk_task(
|
||||||
"name": json.dumps({"value": name}, ensure_ascii=False),
|
"name": json.dumps({"value": name}, ensure_ascii=False),
|
||||||
"description": json.dumps({"value": full_desc}, 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:
|
if client_intradesk_id is not None and contact_person_id is not None:
|
||||||
blocks["initiator"] = json.dumps(
|
blocks["initiator"] = json.dumps(
|
||||||
{"value": {"groupid": client_intradesk_id, "userid": contact_person_id}},
|
{"value": {"groupid": client_intradesk_id, "userid": contact_person_id}},
|
||||||
ensure_ascii=False,
|
ensure_ascii=False,
|
||||||
)
|
)
|
||||||
elif client_intradesk_id is not None:
|
elif client_intradesk_id is not None:
|
||||||
# только клиент (нет конкретного контактного лица)
|
|
||||||
blocks["initiator"] = json.dumps(
|
blocks["initiator"] = json.dumps(
|
||||||
{"value": {"groupid": client_intradesk_id}}, ensure_ascii=False
|
{"value": {"groupid": client_intradesk_id}}, ensure_ascii=False
|
||||||
)
|
)
|
||||||
|
|
@ -374,7 +462,6 @@ async def create_intradesk_task(
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
result = resp.json()
|
result = resp.json()
|
||||||
# API может вернуть как "Number"/"Id" (PascalCase), так и "number"/"id"
|
|
||||||
task_id = result.get("id") or result.get("Id") or result.get("ID")
|
task_id = result.get("id") or result.get("Id") or result.get("ID")
|
||||||
task_number = (
|
task_number = (
|
||||||
result.get("tasknumber")
|
result.get("tasknumber")
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue