перед кешем
This commit is contained in:
parent
7fab711886
commit
6f0e66b4ae
9 changed files with 369 additions and 46 deletions
|
|
@ -44,12 +44,13 @@ async def cleanup_ticket_flow(bot, context: BaseContext) -> None:
|
|||
Вызывается из любого обработчика, который прерывает этот поток
|
||||
(главное меню, список заявок, повторное нажатие «Создать заявку»).
|
||||
"""
|
||||
from bot.states import TicketStates # avoid circular at module level
|
||||
from bot.states import TicketStates, CommentStates # avoid circular at module level
|
||||
state = await context.get_state()
|
||||
creation_states = [
|
||||
TicketStates.waiting_description,
|
||||
TicketStates.collecting_attachments,
|
||||
TicketStates.confirming,
|
||||
CommentStates.waiting_text,
|
||||
]
|
||||
if state not in creation_states:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,47 +1,79 @@
|
|||
"""Обработчик главного меню."""
|
||||
import asyncio
|
||||
|
||||
from maxapi.context import BaseContext
|
||||
from maxapi.dispatcher import Router
|
||||
from maxapi.filters import F
|
||||
from maxapi.types.updates.message_callback import MessageCallback
|
||||
from maxapi.context import BaseContext
|
||||
|
||||
from bot.keyboards import main_menu_kb
|
||||
from bot.send_helper import bot_reply
|
||||
from database.connection import get_db
|
||||
from database.queries.intradesk import (
|
||||
count_active_tasks_for_local_org,
|
||||
count_tasks_for_local_org,
|
||||
)
|
||||
from database.queries.organizations import get_organization
|
||||
from external.intradesk_api import count_client_open_tasks, get_client_last_comment
|
||||
|
||||
router = Router(router_id="menu")
|
||||
|
||||
|
||||
async def build_main_menu_text(bot_user: dict) -> str:
|
||||
"""Формирует текст главного меню с именем объекта и статистикой заявок."""
|
||||
"""Формирует текст главного меню — данные берутся из Intradesk API."""
|
||||
org_id = bot_user.get("org_id")
|
||||
org_name: str | None = None
|
||||
intradesk_id: int | None = None
|
||||
|
||||
if org_id:
|
||||
org = await get_organization(org_id)
|
||||
org_name = org["name"] if org else None
|
||||
intradesk_id = org.get("intradesk_id") if org else None
|
||||
|
||||
lines: list[str] = []
|
||||
if org_name:
|
||||
lines.append(f"🏢 {org_name}")
|
||||
else:
|
||||
lines.append("🏠 Главное меню")
|
||||
lines.append(f"🏢 {org_name}" if org_name else "🏠 Поддержка")
|
||||
lines.append("━" * 16)
|
||||
|
||||
if org_id:
|
||||
db = get_db()
|
||||
total = await count_tasks_for_local_org(db, org_id)
|
||||
active = await count_active_tasks_for_local_org(db, org_id)
|
||||
if intradesk_id:
|
||||
# Счётчик активных и последний комментарий — параллельно
|
||||
active, last_comment = await asyncio.gather(
|
||||
count_client_open_tasks(intradesk_id),
|
||||
get_client_last_comment(intradesk_id),
|
||||
)
|
||||
|
||||
if total == 0:
|
||||
lines.append("Заявок пока нет.")
|
||||
if active == 0:
|
||||
lines += [
|
||||
"📭 Активных заявок нет",
|
||||
"",
|
||||
"Нажмите «Создать заявку» и мы",
|
||||
"свяжемся с вами в ближайшее время.",
|
||||
]
|
||||
else:
|
||||
lines.append(f"Заявок: {total} (активных: {active})")
|
||||
lines.append(f"📋 Активных заявок: {active}")
|
||||
|
||||
lines.append("\nВыберите действие:")
|
||||
if last_comment and last_comment.get("text"):
|
||||
ctext = last_comment["text"]
|
||||
if len(ctext) > 120:
|
||||
ctext = ctext[:120] + "…"
|
||||
raw_date = last_comment.get("date") or ""
|
||||
# raw_date приходит как "2026-03-26 11:54"
|
||||
date_part = raw_date[:10]
|
||||
time_part = raw_date[11:16] if len(raw_date) >= 16 else ""
|
||||
if len(date_part) == 10:
|
||||
y, m, d = date_part.split("-")
|
||||
short_date = f"{d}.{m}" + (f" {time_part}" if time_part else "")
|
||||
else:
|
||||
short_date = raw_date
|
||||
author = (last_comment.get("author") or "").strip()
|
||||
task_num = last_comment.get("task_number")
|
||||
num_str = f" (заявка #{task_num})" if task_num else ""
|
||||
lines += [
|
||||
"",
|
||||
f"💬 Последний ответ{num_str}:",
|
||||
f"«{ctext}»",
|
||||
f"— {author}, {short_date}",
|
||||
]
|
||||
else:
|
||||
lines += ["", "💬 Новых ответов нет"]
|
||||
else:
|
||||
lines.append("📭 Нет данных об организации")
|
||||
|
||||
lines += ["", "━" * 16, "Выберите действие 👇"]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -68,11 +68,13 @@ async def on_bot_started(event: BotStarted, context: BaseContext) -> None:
|
|||
if bot_user["is_authorized"]:
|
||||
logger.info("✅ bot_started returning user | user_id=%s", user.user_id)
|
||||
await context.set_state(None)
|
||||
from bot.handlers.menu import build_main_menu_text
|
||||
menu_text = "👋 С возвращением!\n\n" + await build_main_menu_text(bot_user)
|
||||
await bot_send(
|
||||
event.bot,
|
||||
chat_id=event.chat_id,
|
||||
user_id=user.user_id,
|
||||
text="👋 С возвращением! Выберите действие:",
|
||||
text=menu_text,
|
||||
attachments=[main_menu_kb()],
|
||||
)
|
||||
return
|
||||
|
|
@ -141,11 +143,13 @@ async def on_start_command(event: MessageCreated, context: BaseContext) -> None:
|
|||
return
|
||||
|
||||
await context.set_state(None)
|
||||
from bot.handlers.menu import build_main_menu_text
|
||||
menu_text = await build_main_menu_text(bot_user)
|
||||
await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
text="Главное меню:",
|
||||
text=menu_text,
|
||||
attachments=[main_menu_kb()],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,13 @@ from maxapi.filters import F
|
|||
from maxapi.types.updates.message_callback import MessageCallback
|
||||
|
||||
from bot.filters import PayloadStartsWith
|
||||
from bot.keyboards import iticket_detail_kb, menu_only_kb
|
||||
from bot.send_helper import bot_reply
|
||||
from bot.keyboards import iticket_detail_kb, main_menu_kb, menu_only_kb
|
||||
from bot.send_helper import bot_reply, bot_send
|
||||
from bot.states import CommentStates
|
||||
from database.connection import get_db
|
||||
from database.queries.intradesk import get_intradesk_contact_id_for_phone
|
||||
from database.queries.organizations import get_organization
|
||||
from external.intradesk_api import get_client_open_tasks, get_task_last_comment
|
||||
from external.intradesk_api import add_task_comment, get_client_open_tasks, get_task_last_comment
|
||||
|
||||
router = Router(router_id="view_tickets")
|
||||
|
||||
|
|
@ -42,19 +44,22 @@ def _status_icon(status_name: str) -> str:
|
|||
|
||||
|
||||
def _fmt_date(iso: str) -> str:
|
||||
"""'2026-03-26T09:57:11Z' → '26.03.2026'"""
|
||||
"""'2026-03-26T09:57:11Z' → '26.03.2026 09:57'"""
|
||||
if not iso or len(iso) < 10:
|
||||
return iso or "—"
|
||||
y, m, d = iso[:10].split("-")
|
||||
return f"{d}.{m}.{y}"
|
||||
# iso может быть "2026-03-26T09:57:11Z" или "2026-03-26 09:57"
|
||||
time_part = iso[11:16] if len(iso) >= 16 else ""
|
||||
return f"{d}.{m}.{y}" + (f" {time_part}" if time_part else "")
|
||||
|
||||
|
||||
def _fmt_date_short(iso: str) -> str:
|
||||
"""'2026-03-26' → '26.03'"""
|
||||
"""'2026-03-26 11:54' → '26.03 11:54'"""
|
||||
if not iso or len(iso) < 10:
|
||||
return iso or ""
|
||||
y, m, d = iso[:10].split("-")
|
||||
return f"{d}.{m}"
|
||||
time_part = iso[11:16] if len(iso) >= 16 else ""
|
||||
return f"{d}.{m}" + (f" {time_part}" if time_part else "")
|
||||
|
||||
|
||||
def _build_tasks_text(tasks: list[dict], page: int, total: int) -> str:
|
||||
|
|
@ -79,9 +84,11 @@ def _build_tasks_text(tasks: list[dict], page: int, total: int) -> str:
|
|||
comment = t.get("last_comment")
|
||||
if comment and comment.get("text"):
|
||||
ctext = comment["text"][:80] + ("…" if len(comment["text"]) > 80 else "")
|
||||
author_short = (comment.get("author") or "").split()[0] # только фамилия
|
||||
author_short = (comment.get("author") or "—").split()[0]
|
||||
cdate = _fmt_date_short(comment.get("date") or "")
|
||||
lines.append(f'💬 "{ctext}" — {author_short} ({cdate})')
|
||||
else:
|
||||
lines.append("💬 Нет комментариев")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
|
@ -247,7 +254,125 @@ async def cb_iticket_detail(
|
|||
await bot_reply(
|
||||
event,
|
||||
text="\n".join(lines),
|
||||
attachments=[iticket_detail_kb()],
|
||||
attachments=[iticket_detail_kb(task_id=task["id"])],
|
||||
user_id=bot_user["max_user_id"],
|
||||
chat_id=event.message.recipient.chat_id if event.message else None,
|
||||
)
|
||||
|
||||
|
||||
# ── Оставить комментарий ──────────────────────────────────────────────────────
|
||||
|
||||
@router.message_callback(PayloadStartsWith("iticket_comment:"))
|
||||
async def cb_add_comment(
|
||||
event: MessageCallback, context: BaseContext, bot_user: dict
|
||||
) -> None:
|
||||
try:
|
||||
task_id = int(event.callback.payload.split(":")[1])
|
||||
except (IndexError, ValueError):
|
||||
await event.answer(notification="Ошибка")
|
||||
return
|
||||
|
||||
await event.answer(notification="")
|
||||
|
||||
# Ищем task_number в кэше контекста
|
||||
data = await context.get_data()
|
||||
task = next((t for t in (data.get("tasks") or []) if t.get("id") == task_id), None)
|
||||
task_number = task.get("task_number") if task else None
|
||||
|
||||
await context.set_state(CommentStates.waiting_text)
|
||||
await context.set_data({
|
||||
"comment_task_id": task_id,
|
||||
"comment_task_number": task_number,
|
||||
"flow_message_ids": [],
|
||||
})
|
||||
|
||||
chat_id = event.message.recipient.chat_id if event.message else None
|
||||
if chat_id is None:
|
||||
return
|
||||
|
||||
num_str = f" к заявке #{task_number}" if task_number else ""
|
||||
mid = await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=bot_user["max_user_id"],
|
||||
text=(
|
||||
f"💬 Напишите комментарий{num_str}:\n\n"
|
||||
"Для отмены нажмите «🏠 Главное меню»."
|
||||
),
|
||||
attachments=[menu_only_kb()],
|
||||
)
|
||||
# Сохраняем mid для возможной очистки
|
||||
if mid:
|
||||
d = await context.get_data()
|
||||
d["flow_message_ids"] = [mid]
|
||||
await context.set_data(d)
|
||||
|
||||
|
||||
@router.message_created(states=CommentStates.waiting_text)
|
||||
async def on_comment_text(
|
||||
event: MessageCreated, context: BaseContext, bot_user: dict
|
||||
) -> None:
|
||||
chat_id, user_id = event.get_ids()
|
||||
if chat_id is None:
|
||||
return
|
||||
|
||||
body = event.message.body
|
||||
text = (body.text or "").strip() if body else ""
|
||||
if not text:
|
||||
await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
text="❗ Комментарий не может быть пустым. Напишите текст:",
|
||||
attachments=[menu_only_kb()],
|
||||
)
|
||||
return
|
||||
|
||||
data = await context.get_data()
|
||||
task_number = data.get("comment_task_number")
|
||||
|
||||
# Сбрасываем состояние до отправки API — защита от повторов
|
||||
await context.clear()
|
||||
|
||||
# Получаем contact_person_id
|
||||
contact_person_id: int | None = None
|
||||
if bot_user.get("phone") and bot_user.get("org_id"):
|
||||
org = await get_organization(bot_user["org_id"])
|
||||
intradesk_id = org.get("intradesk_id") if org else None
|
||||
if intradesk_id:
|
||||
db = get_db()
|
||||
contact_person_id = await get_intradesk_contact_id_for_phone(
|
||||
db, intradesk_id, bot_user["phone"]
|
||||
)
|
||||
|
||||
ok = await add_task_comment(
|
||||
task_number=task_number,
|
||||
text=text,
|
||||
contact_person_id=contact_person_id,
|
||||
)
|
||||
|
||||
from bot.handlers.menu import build_main_menu_text
|
||||
|
||||
if ok:
|
||||
num_str = f" к заявке #{task_number}" if task_number else ""
|
||||
await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
text=f"✅ Комментарий{num_str} успешно отправлен!",
|
||||
)
|
||||
else:
|
||||
await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
text="⚠️ Не удалось отправить комментарий. Попробуйте позже.",
|
||||
)
|
||||
|
||||
await bot_send(
|
||||
event.bot,
|
||||
chat_id=chat_id,
|
||||
user_id=user_id,
|
||||
text=await build_main_menu_text(bot_user),
|
||||
attachments=[main_menu_kb()],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -118,14 +118,17 @@ def intradesk_tasks_list_kb(
|
|||
return builder.as_markup()
|
||||
|
||||
|
||||
def iticket_detail_kb() -> Attachment:
|
||||
def iticket_detail_kb(task_id: int | None = None) -> Attachment:
|
||||
"""Клавиатура внутри карточки Intradesk-задачи."""
|
||||
return (
|
||||
InlineKeyboardBuilder()
|
||||
.row(CallbackButton(text="← К списку заявок", payload="itickets_page:0"))
|
||||
.row(CallbackButton(text="🏠 Главное меню", payload="main_menu"))
|
||||
.as_markup()
|
||||
)
|
||||
builder = InlineKeyboardBuilder()
|
||||
if task_id is not None:
|
||||
builder.row(CallbackButton(
|
||||
text="💬 Оставить комментарий",
|
||||
payload=f"iticket_comment:{task_id}",
|
||||
))
|
||||
builder.row(CallbackButton(text="← К списку заявок", payload="itickets_page:0"))
|
||||
builder.row(CallbackButton(text="🏠 Главное меню", payload="main_menu"))
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def ticket_detail_kb(page: int = 0) -> Attachment:
|
||||
|
|
|
|||
|
|
@ -9,3 +9,7 @@ class TicketStates(StatesGroup):
|
|||
waiting_description = State() # Ожидаем первое сообщение с описанием
|
||||
collecting_attachments = State() # Накапливаем доп. файлы/текст
|
||||
confirming = State() # Предпросмотр — ждём подтверждения
|
||||
|
||||
|
||||
class CommentStates(StatesGroup):
|
||||
waiting_text = State() # Ожидаем текст комментария к заявке
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
170
external/intradesk_api.py
vendored
170
external/intradesk_api.py
vendored
|
|
@ -9,6 +9,7 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
import httpx
|
||||
|
||||
|
|
@ -16,6 +17,18 @@ import config
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_TAG_RE = re.compile(r"<[^>]+>")
|
||||
|
||||
|
||||
def _strip_html(text: str) -> str:
|
||||
"""Убрать HTML-теги и лишние пробелы из текста комментария."""
|
||||
if not text:
|
||||
return text
|
||||
cleaned = _TAG_RE.sub("", text)
|
||||
# Схлопнуть множественные пробелы/переносы
|
||||
cleaned = re.sub(r"\s{2,}", " ", cleaned).strip()
|
||||
return cleaned
|
||||
|
||||
|
||||
async def get_task_last_comment(task_id: int) -> dict | None:
|
||||
"""
|
||||
|
|
@ -53,13 +66,88 @@ async def get_task_last_comment(task_id: int) -> dict | None:
|
|||
# "2021-03-15T11:54:36.92Z" → "2021-03-15 11:54"
|
||||
short_date = raw_date[:16].replace("T", " ")
|
||||
return {
|
||||
"text": ev.get("stringvalue") or "",
|
||||
"text": _strip_html(ev.get("stringvalue") or ""),
|
||||
"author": entry.get("username") or "—",
|
||||
"date": short_date,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
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:
|
||||
return 0
|
||||
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"&$top=1&$count=true"
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, timeout=10.0)
|
||||
if resp.status_code != 200:
|
||||
return 0
|
||||
data = resp.json()
|
||||
# @odata.count — полный итог по фильтру, не зависит от $top
|
||||
count = data.get("@odata.count")
|
||||
if count is not None:
|
||||
return int(count)
|
||||
# Fallback: если count не вернулся — считаем по размеру value
|
||||
return len(data.get("value") or [])
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
async def get_client_last_comment(client_id: int) -> dict | None:
|
||||
"""
|
||||
Найти последний комментарий по любой незакрытой заявке клиента.
|
||||
|
||||
Берём задачу с самым свежим updatedat → ищем последний комментарий в её истории.
|
||||
Возвращает {text, author, date, task_number} или None.
|
||||
"""
|
||||
key = config.INTRADESK_TASKS_API_KEY
|
||||
if not key:
|
||||
return None
|
||||
|
||||
# Шаг 1: самая свежая задача клиента
|
||||
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&$top=1"
|
||||
)
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(url, timeout=15.0)
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
data = resp.json()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
tasks = data.get("value") or []
|
||||
if not tasks:
|
||||
return None
|
||||
|
||||
task = tasks[0]
|
||||
task_id = task.get("id")
|
||||
task_number = task.get("tasknumber")
|
||||
if not task_id:
|
||||
return None
|
||||
|
||||
# Шаг 2: последний комментарий этой задачи
|
||||
comment = await get_task_last_comment(task_id)
|
||||
if comment:
|
||||
comment["task_number"] = task_number
|
||||
return comment
|
||||
|
||||
|
||||
async def get_client_open_tasks(
|
||||
client_id: int,
|
||||
page: int = 0,
|
||||
|
|
@ -69,8 +157,10 @@ async def get_client_open_tasks(
|
|||
Получить незакрытые заявки клиента из Intradesk API.
|
||||
|
||||
Возвращает {"tasks": [...], "total": int}.
|
||||
Каждая задача содержит поля из OData + "last_comment" (dict или None).
|
||||
Последний комментарий загружается параллельно через history API для каждой задачи.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
key = config.INTRADESK_TASKS_API_KEY
|
||||
if not key:
|
||||
return {"tasks": [], "total": 0}
|
||||
|
|
@ -108,20 +198,31 @@ async def get_client_open_tasks(
|
|||
if did:
|
||||
dicts_map[did] = dname
|
||||
|
||||
tasks = []
|
||||
# Базовые данные задач
|
||||
base_tasks = []
|
||||
for t in raw_tasks:
|
||||
status_id = t.get("status")
|
||||
last_comment = _extract_last_comment_from_lifetime(t.get("lifetime"))
|
||||
tasks.append({
|
||||
base_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,
|
||||
})
|
||||
|
||||
# Параллельно загружаем последний комментарий для каждой задачи
|
||||
task_ids = [t["id"] for t in base_tasks if t["id"]]
|
||||
comments = await asyncio.gather(
|
||||
*[get_task_last_comment(tid) for tid in task_ids],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
tasks = []
|
||||
for task, comment in zip(base_tasks, comments):
|
||||
task["last_comment"] = comment if isinstance(comment, dict) else None
|
||||
tasks.append(task)
|
||||
|
||||
return {"tasks": tasks, "total": total}
|
||||
|
||||
|
||||
|
|
@ -133,15 +234,68 @@ def _extract_last_comment_from_lifetime(lifetime: dict | None) -> dict | None:
|
|||
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"
|
||||
short_date = raw_date[:16].replace("T", " ") # "2026-03-26 11:54"
|
||||
return {
|
||||
"text": ev.get("stringvalue") or "",
|
||||
"text": _strip_html(ev.get("stringvalue") or ""),
|
||||
"author": entry.get("username") or "—",
|
||||
"date": short_date,
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
async def add_task_comment(
|
||||
task_number: int,
|
||||
text: str,
|
||||
contact_person_id: int | None = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Добавить комментарий к заявке через Intradesk API (PUT /changes/v3/tasks).
|
||||
|
||||
Возвращает True при успехе, False при ошибке.
|
||||
"""
|
||||
key = config.INTRADESK_CHANGES_API_KEY
|
||||
if not key:
|
||||
logger.warning("add_task_comment: INTRADESK_CHANGES_API_KEY не задан")
|
||||
return False
|
||||
|
||||
blocks: dict[str, str] = {
|
||||
"comment": json.dumps({"value": text}, ensure_ascii=False),
|
||||
}
|
||||
body = {"number": task_number, "blocks": blocks, "Channel": "api"}
|
||||
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] add_task_comment — запрос НЕ отправлен\n"
|
||||
f" PUT {url}\n"
|
||||
f" Body: {json.dumps(body, ensure_ascii=False, indent=2)}\n"
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.put(
|
||||
url,
|
||||
json=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=20.0,
|
||||
)
|
||||
if resp.status_code not in (200, 201):
|
||||
logger.error(
|
||||
"add_task_comment → HTTP %s: %s",
|
||||
resp.status_code,
|
||||
resp.text[:500],
|
||||
)
|
||||
return False
|
||||
logger.info("add_task_comment ok: task_number=%s", task_number)
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.exception("add_task_comment error: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
async def create_intradesk_task(
|
||||
name: str,
|
||||
description: str,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue