275 lines
10 KiB
Python
275 lines
10 KiB
Python
import json
|
||
import logging
|
||
|
||
import config
|
||
import database
|
||
from intradesk import reference
|
||
|
||
log = logging.getLogger(__name__)
|
||
|
||
# Маппинг blockname → entity в БД справочников
|
||
BLOCK_TO_ENTITY: dict[str, str] = {
|
||
"status": "statuses",
|
||
"priority": "priorities",
|
||
"executorgroup": "groups",
|
||
"executor": "employees",
|
||
"service": "services",
|
||
"tasktype": "tasktypes",
|
||
"initiator": "employees",
|
||
"workflow": "workflows",
|
||
"sla": "sla",
|
||
}
|
||
|
||
# Человекочитаемые названия полей
|
||
FIELD_LABELS = {
|
||
"status": "Статус",
|
||
"priority": "Приоритет",
|
||
"executor": "Исполнитель",
|
||
"executorgroup": "Группа",
|
||
"service": "Услуга",
|
||
"tasktype": "Тип заявки",
|
||
"initiator": "Инициатор",
|
||
"resolutiondateplan": "Плановый срок",
|
||
"closedat": "Закрыта",
|
||
"description": "Описание",
|
||
"name": "Название",
|
||
"workflow": "Бизнес-процесс",
|
||
"sla": "SLA",
|
||
"tags": "Теги",
|
||
}
|
||
|
||
# Поля, изменение которых не выводится в уведомлении
|
||
IGNORED_FIELDS = {
|
||
"lifetime", "updatedat", "updatedby", "updatedchannel",
|
||
"reactiondatefact", "resolutiondatefact", "resolutionleftminutes",
|
||
"reactiondateplan", "ismassincidentactive",
|
||
}
|
||
|
||
|
||
def _parse_json_field(raw):
|
||
"""Парсит JSON-строку или возвращает как есть."""
|
||
if raw is None:
|
||
return None
|
||
if isinstance(raw, (dict, list)):
|
||
return raw
|
||
if isinstance(raw, str):
|
||
try:
|
||
return json.loads(raw)
|
||
except Exception:
|
||
return raw
|
||
return raw
|
||
|
||
|
||
def _scalar(raw):
|
||
"""Возвращает скалярное значение поля Fields (Value или Id)."""
|
||
parsed = _parse_json_field(raw)
|
||
if isinstance(parsed, dict):
|
||
return parsed.get("Value") or parsed.get("Id")
|
||
return parsed
|
||
|
||
|
||
def _extract_name(v: dict | None, blockname: str = "") -> str | None:
|
||
"""
|
||
Достаёт человекочитаемое имя из объекта valuenew/valueold.
|
||
|
||
Обрабатывает три варианта поля name:
|
||
1. строка: {"id": 8452, "name": "Иванов"}
|
||
2. объект {ru, en}: {"id": 96, "name": {"ru": "Выполнена", "en": null}}
|
||
3. отсутствует/null: {"id": 68045, "name": null} → резольвинг через БД
|
||
"""
|
||
if not v:
|
||
return None
|
||
|
||
raw_name = v.get("currentname") or v.get("name")
|
||
|
||
# name — объект {"ru": ..., "en": ...}
|
||
if isinstance(raw_name, dict):
|
||
raw_name = raw_name.get("ru") or raw_name.get("en") or None
|
||
|
||
if raw_name:
|
||
return str(raw_name).strip() or None
|
||
|
||
# name отсутствует или null — резольвинг через БД/статик
|
||
item_id = v.get("id")
|
||
entity = BLOCK_TO_ENTITY.get(blockname)
|
||
if item_id and entity:
|
||
return reference.resolve(entity, item_id) or str(item_id)
|
||
|
||
return str(item_id) if item_id else None
|
||
|
||
|
||
def get_executor_group(fields: dict) -> str | None:
|
||
"""Группа исполнителей (Type=10 в taskusergroups)."""
|
||
raw = _parse_json_field(fields.get("taskusergroups", {}))
|
||
if not isinstance(raw, dict):
|
||
return None
|
||
for group in raw.get("TaskUserGroups", []):
|
||
if group.get("Type") == 10:
|
||
return group.get("Name", "").strip() or None
|
||
return None
|
||
|
||
|
||
def get_initiator(fields: dict) -> str | None:
|
||
"""Инициатор заявки (Type=10 в taskusers)."""
|
||
raw = _parse_json_field(fields.get("taskusers", {}))
|
||
if not isinstance(raw, dict):
|
||
return None
|
||
for user in raw.get("TaskUsers", []):
|
||
if user.get("Type") == 10:
|
||
return user.get("Name", "").strip() or None
|
||
return None
|
||
|
||
|
||
def is_final(fields: dict) -> bool:
|
||
"""Возвращает True если статус задачи финальный (закрыта)."""
|
||
raw = _parse_json_field(fields.get("status", {}))
|
||
if isinstance(raw, dict):
|
||
return bool(raw.get("IsFinal", False))
|
||
return False
|
||
|
||
|
||
def parse_lifetime(fields: dict) -> tuple[list[str], list[str]]:
|
||
"""
|
||
Разбирает lifetime-поле вебхука.
|
||
|
||
Возвращает:
|
||
changes — строки вида «• Поле: было → стало»
|
||
comments — строки вида «*Автор*: текст»
|
||
"""
|
||
changes: list[str] = []
|
||
comments: list[str] = []
|
||
|
||
lifetime = _parse_json_field(fields.get("lifetime", "{}"))
|
||
if not isinstance(lifetime, dict):
|
||
return changes, comments
|
||
|
||
for entry in lifetime.get("Data", []):
|
||
username = entry.get("username", "Неизвестный")
|
||
|
||
for event in entry.get("events", {}).get("Data", []):
|
||
etype = event.get("type")
|
||
blockname = event.get("blockname", "")
|
||
|
||
# ── Комментарии ────────────────────────────────────────────────
|
||
if etype == 50:
|
||
text = event.get("stringvalue")
|
||
if text:
|
||
comments.append(f"*{username}*: {text}")
|
||
continue
|
||
|
||
# Приватный комментарий (необязательно показывать, но пусть будет)
|
||
if etype == 55:
|
||
text = event.get("stringvalue")
|
||
if text:
|
||
comments.append(f"*{username}* 🔒: {text}")
|
||
continue
|
||
|
||
if blockname in IGNORED_FIELDS:
|
||
continue
|
||
|
||
label = FIELD_LABELS.get(blockname, reference.event_name(etype))
|
||
|
||
# ── Изменения с entityvalue / entitylanguagevalue ──────────────
|
||
entity_data = event.get("entityvalue") or event.get("entitylanguagevalue")
|
||
if entity_data:
|
||
val_new = entity_data.get("valuenew") or {}
|
||
val_old = entity_data.get("valueold")
|
||
|
||
new_str = _extract_name(val_new, blockname)
|
||
old_str = _extract_name(val_old, blockname) if val_old else None
|
||
|
||
if not new_str:
|
||
continue
|
||
|
||
if old_str:
|
||
changes.append(f"• {label}: {old_str} → *{new_str}*")
|
||
else:
|
||
changes.append(f"• {label}: *{new_str}*")
|
||
continue
|
||
|
||
# ── Строковые изменения (название, описание) ───────────────────
|
||
str_val = event.get("stringvalue")
|
||
if str_val:
|
||
changes.append(f"• {label}: *{str_val[:80]}{'…' if len(str_val) > 80 else ''}*")
|
||
continue
|
||
|
||
# ── Событие без значения (теги, файлы, эскалация) ─────────────
|
||
if etype not in (185, 250): # пропускаем служебные
|
||
changes.append(f"• {label}")
|
||
|
||
return changes, comments
|
||
|
||
|
||
def _resolve_field(fields: dict, field: str, entity: str) -> str | None:
|
||
"""Резолвит ID поля верхнего уровня через справочники."""
|
||
raw = _parse_json_field(fields.get(field, {}))
|
||
if not isinstance(raw, dict):
|
||
return None
|
||
item_id = raw.get("Id") or raw.get("Value")
|
||
if not item_id:
|
||
return None
|
||
return reference.resolve(entity, item_id) or str(item_id)
|
||
|
||
|
||
def format_notification(payload: dict) -> str:
|
||
"""Полная карточка задачи для основного поста в канале."""
|
||
op_type = payload.get("Event", {}).get("OperationType", "")
|
||
fields = payload.get("Fields", {})
|
||
|
||
number = fields.get("Number", "?")
|
||
name = _scalar(fields.get("name")) or "—"
|
||
executor_grp = get_executor_group(fields)
|
||
initiator = get_initiator(fields)
|
||
|
||
status = _resolve_field(fields, "status", "statuses")
|
||
priority = _resolve_field(fields, "priority", "priorities")
|
||
service = _resolve_field(fields, "service", "services")
|
||
tasktype = _resolve_field(fields, "tasktype", "tasktypes")
|
||
|
||
changes, comments = parse_lifetime(fields)
|
||
|
||
header = f"🆕 *Новая заявка \\#{number}*" if op_type == "Created" \
|
||
else f"🔄 *Заявка \\#{number} обновлена*"
|
||
|
||
lines = [header, name, ""]
|
||
|
||
if status:
|
||
lines.append(f"📌 Статус: {status}")
|
||
if priority:
|
||
lines.append(f"🔺 Приоритет: {priority}")
|
||
if service:
|
||
lines.append(f"🗂 Услуга: {service}")
|
||
if tasktype:
|
||
lines.append(f"📋 Тип: {tasktype}")
|
||
if executor_grp:
|
||
lines.append(f"👥 Группа: {executor_grp}")
|
||
if initiator:
|
||
lines.append(f"👤 Инициатор: {initiator}")
|
||
if any([status, priority, service, tasktype, executor_grp, initiator]):
|
||
lines.append("")
|
||
|
||
if changes:
|
||
lines.extend(changes)
|
||
lines.append("")
|
||
|
||
if comments:
|
||
lines.append("💬 *Комментарии:*")
|
||
lines.extend(comments)
|
||
lines.append("")
|
||
|
||
task_url = config.BASE_TASK_URL + str(number)
|
||
lines.append(f"[Открыть заявку]({task_url})")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def format_update_summary(changes: list[str], comments: list[str]) -> str | None:
|
||
"""Короткий reply-пост при обновлении (триггерит пуш подписчикам канала)."""
|
||
if not changes and not comments:
|
||
return None
|
||
lines = ["🔄 *Изменено:*"]
|
||
lines.extend(changes)
|
||
if comments:
|
||
lines.append("💬 *Комментарии:*")
|
||
lines.extend(comments)
|
||
return "\n".join(lines)
|