Initial commit: VPN Telegram bot (sanitized defaults)
This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
"""Тикеты поддержки — пользователь и админ в Telegram."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
|
||||
from aiogram import Bot, F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from config import Settings
|
||||
from keyboards import (
|
||||
admin_tickets_root_inline,
|
||||
ticket_cancel_inline,
|
||||
ticket_view_inline,
|
||||
tickets_list_inline,
|
||||
tickets_root_inline,
|
||||
)
|
||||
from services.db import Database
|
||||
from services.ticket_ai import try_ai_ticket_reply
|
||||
from services.tickets import (
|
||||
BODY_MAX,
|
||||
MAX_OPEN_PER_USER,
|
||||
SUBJECT_MAX,
|
||||
clamp_body,
|
||||
clamp_subject,
|
||||
format_admin_ticket_card,
|
||||
format_ticket_card,
|
||||
notify_admins_new_ticket,
|
||||
notify_admins_ticket_reply,
|
||||
notify_user_ticket_reply,
|
||||
status_label,
|
||||
)
|
||||
|
||||
router = Router(name="tickets")
|
||||
|
||||
|
||||
class TicketCreateStates(StatesGroup):
|
||||
waiting_subject = State()
|
||||
waiting_body = State()
|
||||
|
||||
|
||||
class TicketReplyStates(StatesGroup):
|
||||
waiting_body = State()
|
||||
|
||||
|
||||
class AdminTicketReplyStates(StatesGroup):
|
||||
waiting_body = State()
|
||||
|
||||
|
||||
TICKETS_INTRO = (
|
||||
"<b>🎫 Поддержка</b>\n\n"
|
||||
"Создай тикет — ответим здесь или в Mini App.\n"
|
||||
f"Одновременно можно держать до <b>{MAX_OPEN_PER_USER}</b> открытых обращений."
|
||||
)
|
||||
|
||||
|
||||
def _admin_tickets_home_text(stats: dict) -> str:
|
||||
return (
|
||||
"<b>🎫 Тикеты поддержки</b>\n\n"
|
||||
f"Открытых: <b>{stats.get('open', 0)}</b>\n"
|
||||
f"С ответом: <b>{stats.get('answered', 0)}</b>\n"
|
||||
f"Закрытых: <b>{stats.get('closed', 0)}</b>\n"
|
||||
f"Всего: <b>{stats.get('total', 0)}</b>"
|
||||
)
|
||||
|
||||
|
||||
# ─── User ───────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@router.message(F.text == "🎫 Тикеты")
|
||||
async def cmd_tickets(message: Message, state: FSMContext, db: Database) -> None:
|
||||
await state.clear()
|
||||
uid = message.from_user.id if message.from_user else 0
|
||||
open_n = db.count_open_tickets(uid) if uid else 0
|
||||
await message.answer(
|
||||
TICKETS_INTRO,
|
||||
reply_markup=tickets_root_inline(has_open=open_n > 0),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "tix:root")
|
||||
async def cb_tickets_root(callback: CallbackQuery, state: FSMContext, db: Database) -> None:
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
uid = callback.from_user.id if callback.from_user else 0
|
||||
open_n = db.count_open_tickets(uid) if uid else 0
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
TICKETS_INTRO,
|
||||
reply_markup=tickets_root_inline(has_open=open_n > 0),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.in_({"tix:list", "tix:list:open"}))
|
||||
async def cb_tickets_list(callback: CallbackQuery, db: Database) -> None:
|
||||
await callback.answer()
|
||||
uid = callback.from_user.id if callback.from_user else 0
|
||||
only_open = (callback.data or "").endswith(":open")
|
||||
tickets = db.list_tickets_for_user(
|
||||
uid,
|
||||
limit=20,
|
||||
include_closed=not only_open,
|
||||
)
|
||||
if not tickets:
|
||||
text = "Открытых тикетов нет." if only_open else "Тикетов пока нет."
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=tickets_root_inline(has_open=False),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
title = "<b>🟢 Открытые тикеты</b>" if only_open else "<b>📋 Мои тикеты</b>"
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
title + "\n\nВыбери обращение:",
|
||||
reply_markup=tickets_list_inline(tickets),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tix:view:"))
|
||||
async def cb_ticket_view(callback: CallbackQuery, state: FSMContext, db: Database) -> None:
|
||||
await state.clear()
|
||||
try:
|
||||
tid = int((callback.data or "").rsplit(":", 1)[-1])
|
||||
except ValueError:
|
||||
await callback.answer("Некорректный тикет", show_alert=True)
|
||||
return
|
||||
ticket = db.get_ticket(tid)
|
||||
uid = callback.from_user.id if callback.from_user else 0
|
||||
if not ticket or int(ticket["telegram_id"]) != uid:
|
||||
await callback.answer("Тикет не найден", show_alert=True)
|
||||
return
|
||||
await callback.answer()
|
||||
messages = db.list_ticket_messages(tid)
|
||||
text = format_ticket_card(ticket, messages)
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
text,
|
||||
reply_markup=ticket_view_inline(tid, status=str(ticket["status"])),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data == "tix:new")
|
||||
async def cb_ticket_new(callback: CallbackQuery, state: FSMContext, db: Database) -> None:
|
||||
uid = callback.from_user.id if callback.from_user else 0
|
||||
if db.count_open_tickets(uid) >= MAX_OPEN_PER_USER:
|
||||
await callback.answer(
|
||||
f"Лимит: не больше {MAX_OPEN_PER_USER} открытых тикетов",
|
||||
show_alert=True,
|
||||
)
|
||||
return
|
||||
await callback.answer()
|
||||
await state.set_state(TicketCreateStates.waiting_subject)
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
f"<b>Новый тикет</b>\n\nНапиши тему (до {SUBJECT_MAX} символов):",
|
||||
reply_markup=ticket_cancel_inline(),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.message(TicketCreateStates.waiting_subject)
|
||||
async def ticket_subject_msg(message: Message, state: FSMContext) -> None:
|
||||
subject = clamp_subject(message.text or "")
|
||||
if len(subject) < 3:
|
||||
await message.answer("Тема слишком короткая. Минимум 3 символа.")
|
||||
return
|
||||
await state.update_data(subject=subject)
|
||||
await state.set_state(TicketCreateStates.waiting_body)
|
||||
await message.answer(
|
||||
f"Тема: <b>{html.escape(subject)}</b>\n\n"
|
||||
f"Опиши проблему (до {BODY_MAX} символов):",
|
||||
reply_markup=ticket_cancel_inline(),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.message(TicketCreateStates.waiting_body)
|
||||
async def ticket_body_msg(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
db: Database,
|
||||
settings: Settings,
|
||||
bot: Bot,
|
||||
) -> None:
|
||||
body = clamp_body(message.text or "")
|
||||
if len(body) < 5:
|
||||
await message.answer("Сообщение слишком короткое.")
|
||||
return
|
||||
data = await state.get_data()
|
||||
subject = clamp_subject(str(data.get("subject") or ""))
|
||||
await state.clear()
|
||||
uid = message.from_user.id if message.from_user else 0
|
||||
if not uid or not subject:
|
||||
await message.answer("Не удалось создать тикет. Попробуй снова.")
|
||||
return
|
||||
if db.count_open_tickets(uid) >= MAX_OPEN_PER_USER:
|
||||
await message.answer(f"Лимит открытых тикетов: {MAX_OPEN_PER_USER}.")
|
||||
return
|
||||
db.ensure_user(
|
||||
uid,
|
||||
username=message.from_user.username if message.from_user else None,
|
||||
full_name=message.from_user.full_name if message.from_user else None,
|
||||
)
|
||||
ticket = db.create_ticket(uid, subject=subject, body=body)
|
||||
tid = int(ticket["id"])
|
||||
ai_result = await try_ai_ticket_reply(
|
||||
db=db,
|
||||
bot=bot,
|
||||
settings=settings,
|
||||
ticket_id=tid,
|
||||
notify_user=False,
|
||||
)
|
||||
ai_answered = ai_result.get("action") == "answer" and ai_result.get("ok")
|
||||
# Если ИИ уже ответил пользователю — админам короткое уведомление.
|
||||
# Если эскалация — ticket_ai уже пинганул. Если skip — обычное уведомление.
|
||||
if ai_result.get("action") in ("skip", "error") or ai_answered:
|
||||
if ai_result.get("action") != "escalate":
|
||||
await notify_admins_new_ticket(
|
||||
bot,
|
||||
settings,
|
||||
ticket_id=tid,
|
||||
telegram_id=uid,
|
||||
subject=subject,
|
||||
body=body,
|
||||
ai_handled=bool(ai_answered),
|
||||
)
|
||||
ticket = db.get_ticket(tid) or ticket
|
||||
status = str(ticket.get("status") or "open")
|
||||
extra = ""
|
||||
if ai_answered:
|
||||
extra = "\n🤖 ИИ уже ответил — смотри переписку ниже."
|
||||
await message.answer(
|
||||
f"✅ Тикет <b>#{tid}</b> создан.{extra}\n"
|
||||
"Мы ответим в этом чате или в Mini App.",
|
||||
reply_markup=ticket_view_inline(tid, status=status),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
if ai_answered:
|
||||
messages = db.list_ticket_messages(tid)
|
||||
await message.answer(
|
||||
format_ticket_card(ticket, messages),
|
||||
reply_markup=ticket_view_inline(tid, status=status),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tix:reply:"))
|
||||
async def cb_ticket_reply(callback: CallbackQuery, state: FSMContext, db: Database) -> None:
|
||||
try:
|
||||
tid = int((callback.data or "").rsplit(":", 1)[-1])
|
||||
except ValueError:
|
||||
await callback.answer("Ошибка", show_alert=True)
|
||||
return
|
||||
ticket = db.get_ticket(tid)
|
||||
uid = callback.from_user.id if callback.from_user else 0
|
||||
if not ticket or int(ticket["telegram_id"]) != uid:
|
||||
await callback.answer("Тикет не найден", show_alert=True)
|
||||
return
|
||||
if str(ticket["status"]) == "closed":
|
||||
await callback.answer("Тикет закрыт", show_alert=True)
|
||||
return
|
||||
await callback.answer()
|
||||
await state.set_state(TicketReplyStates.waiting_body)
|
||||
await state.update_data(ticket_id=tid)
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
f"<b>Ответ в тикет #{tid}</b>\n\nНапиши сообщение:",
|
||||
reply_markup=ticket_cancel_inline(back=f"tix:view:{tid}"),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.message(TicketReplyStates.waiting_body)
|
||||
async def ticket_reply_msg(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
db: Database,
|
||||
settings: Settings,
|
||||
bot: Bot,
|
||||
) -> None:
|
||||
body = clamp_body(message.text or "")
|
||||
if len(body) < 1:
|
||||
await message.answer("Пустое сообщение.")
|
||||
return
|
||||
data = await state.get_data()
|
||||
tid = int(data.get("ticket_id") or 0)
|
||||
await state.clear()
|
||||
uid = message.from_user.id if message.from_user else 0
|
||||
ticket = db.get_ticket(tid)
|
||||
if not ticket or int(ticket["telegram_id"]) != uid:
|
||||
await message.answer("Тикет не найден.")
|
||||
return
|
||||
try:
|
||||
ticket = db.add_ticket_message(
|
||||
tid,
|
||||
author_role="user",
|
||||
author_id=uid,
|
||||
body=body,
|
||||
)
|
||||
except ValueError as exc:
|
||||
await message.answer(str(exc))
|
||||
return
|
||||
ai_result = await try_ai_ticket_reply(
|
||||
db=db,
|
||||
bot=bot,
|
||||
settings=settings,
|
||||
ticket_id=tid,
|
||||
notify_user=False,
|
||||
)
|
||||
if ai_result.get("action") in ("skip", "error"):
|
||||
await notify_admins_ticket_reply(
|
||||
bot,
|
||||
settings,
|
||||
ticket_id=tid,
|
||||
telegram_id=uid,
|
||||
body=body,
|
||||
)
|
||||
ticket = db.get_ticket(tid) or ticket
|
||||
messages = db.list_ticket_messages(tid)
|
||||
await message.answer(
|
||||
format_ticket_card(ticket, messages),
|
||||
reply_markup=ticket_view_inline(tid, status=str(ticket["status"])),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("tix:close:"))
|
||||
async def cb_ticket_close(callback: CallbackQuery, db: Database) -> None:
|
||||
try:
|
||||
tid = int((callback.data or "").rsplit(":", 1)[-1])
|
||||
except ValueError:
|
||||
await callback.answer("Ошибка", show_alert=True)
|
||||
return
|
||||
ticket = db.get_ticket(tid)
|
||||
uid = callback.from_user.id if callback.from_user else 0
|
||||
if not ticket or int(ticket["telegram_id"]) != uid:
|
||||
await callback.answer("Тикет не найден", show_alert=True)
|
||||
return
|
||||
if str(ticket["status"]) == "closed":
|
||||
await callback.answer("Уже закрыт")
|
||||
return
|
||||
db.set_ticket_status(tid, "closed")
|
||||
await callback.answer("Закрыто")
|
||||
ticket = db.get_ticket(tid)
|
||||
messages = db.list_ticket_messages(tid)
|
||||
if callback.message and ticket:
|
||||
await callback.message.edit_text(
|
||||
format_ticket_card(ticket, messages),
|
||||
reply_markup=ticket_view_inline(tid, status="closed"),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
# ─── Admin (same router; callbacks prefixed admin:tix / admin:tickets) ───────
|
||||
|
||||
|
||||
@router.callback_query(F.data == "admin:tickets")
|
||||
@router.callback_query(F.data.startswith("admin:tickets:"))
|
||||
async def cb_admin_tickets(callback: CallbackQuery, state: FSMContext, db: Database, settings: Settings) -> None:
|
||||
if not callback.from_user or callback.from_user.id not in settings.admin_ids:
|
||||
await callback.answer("Нет доступа", show_alert=True)
|
||||
return
|
||||
await state.clear()
|
||||
await callback.answer()
|
||||
data = callback.data or "admin:tickets"
|
||||
if data == "admin:tickets":
|
||||
stats = db.ticket_stats()
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
_admin_tickets_home_text(stats),
|
||||
reply_markup=admin_tickets_root_inline(stats),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
|
||||
status = data.rsplit(":", 1)[-1]
|
||||
if status not in ("open", "answered", "closed", "all"):
|
||||
status = "open"
|
||||
tickets, _total = db.list_tickets_admin(status=status, limit=20)
|
||||
if not tickets:
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
"Тикетов в этом фильтре нет.",
|
||||
reply_markup=admin_tickets_root_inline(db.ticket_stats()),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return
|
||||
title = {
|
||||
"open": "🟢 Открытые",
|
||||
"answered": "💬 С ответом",
|
||||
"closed": "🔒 Закрытые",
|
||||
"all": "📋 Все тикеты",
|
||||
}.get(status, "Тикеты")
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
f"<b>{title}</b>\n\nВыбери тикет:",
|
||||
reply_markup=tickets_list_inline(
|
||||
tickets,
|
||||
prefix="admin:tix:view",
|
||||
back="admin:tickets",
|
||||
),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin:tix:view:"))
|
||||
async def cb_admin_ticket_view(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
db: Database,
|
||||
settings: Settings,
|
||||
) -> None:
|
||||
if not callback.from_user or callback.from_user.id not in settings.admin_ids:
|
||||
await callback.answer("Нет доступа", show_alert=True)
|
||||
return
|
||||
await state.clear()
|
||||
try:
|
||||
tid = int((callback.data or "").rsplit(":", 1)[-1])
|
||||
except ValueError:
|
||||
await callback.answer("Ошибка", show_alert=True)
|
||||
return
|
||||
ticket = db.get_ticket(tid)
|
||||
if not ticket:
|
||||
await callback.answer("Не найден", show_alert=True)
|
||||
return
|
||||
await callback.answer()
|
||||
messages = db.list_ticket_messages(tid)
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
format_admin_ticket_card(ticket, messages),
|
||||
reply_markup=ticket_view_inline(
|
||||
tid,
|
||||
status=str(ticket["status"]),
|
||||
is_admin=True,
|
||||
),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin:tix:reply:"))
|
||||
async def cb_admin_ticket_reply(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
db: Database,
|
||||
settings: Settings,
|
||||
) -> None:
|
||||
if not callback.from_user or callback.from_user.id not in settings.admin_ids:
|
||||
await callback.answer("Нет доступа", show_alert=True)
|
||||
return
|
||||
try:
|
||||
tid = int((callback.data or "").rsplit(":", 1)[-1])
|
||||
except ValueError:
|
||||
await callback.answer("Ошибка", show_alert=True)
|
||||
return
|
||||
ticket = db.get_ticket(tid)
|
||||
if not ticket:
|
||||
await callback.answer("Не найден", show_alert=True)
|
||||
return
|
||||
if str(ticket["status"]) == "closed":
|
||||
await callback.answer("Тикет закрыт", show_alert=True)
|
||||
return
|
||||
await callback.answer()
|
||||
await state.set_state(AdminTicketReplyStates.waiting_body)
|
||||
await state.update_data(ticket_id=tid)
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
f"<b>Ответ в тикет #{tid}</b>\n\n"
|
||||
f"Пользователь: <code>{ticket['telegram_id']}</code>\n"
|
||||
f"Статус: {status_label(str(ticket['status']))}\n\n"
|
||||
"Напиши ответ:",
|
||||
reply_markup=ticket_cancel_inline(back=f"admin:tix:view:{tid}"),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.message(AdminTicketReplyStates.waiting_body)
|
||||
async def admin_ticket_reply_msg(
|
||||
message: Message,
|
||||
state: FSMContext,
|
||||
db: Database,
|
||||
settings: Settings,
|
||||
bot: Bot,
|
||||
) -> None:
|
||||
if not message.from_user or message.from_user.id not in settings.admin_ids:
|
||||
await state.clear()
|
||||
return
|
||||
body = clamp_body(message.text or "")
|
||||
if len(body) < 1:
|
||||
await message.answer("Пустое сообщение.")
|
||||
return
|
||||
data = await state.get_data()
|
||||
tid = int(data.get("ticket_id") or 0)
|
||||
await state.clear()
|
||||
ticket = db.get_ticket(tid)
|
||||
if not ticket:
|
||||
await message.answer("Тикет не найден.")
|
||||
return
|
||||
try:
|
||||
ticket = db.add_ticket_message(
|
||||
tid,
|
||||
author_role="admin",
|
||||
author_id=message.from_user.id,
|
||||
body=body,
|
||||
)
|
||||
except ValueError as exc:
|
||||
await message.answer(str(exc))
|
||||
return
|
||||
await notify_user_ticket_reply(
|
||||
bot,
|
||||
telegram_id=int(ticket["telegram_id"]),
|
||||
ticket_id=tid,
|
||||
subject=str(ticket.get("subject") or ""),
|
||||
body=body,
|
||||
)
|
||||
messages = db.list_ticket_messages(tid)
|
||||
await message.answer(
|
||||
format_admin_ticket_card(ticket, messages),
|
||||
reply_markup=ticket_view_inline(
|
||||
tid,
|
||||
status=str(ticket["status"]),
|
||||
is_admin=True,
|
||||
),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin:tix:close:"))
|
||||
async def cb_admin_ticket_close(
|
||||
callback: CallbackQuery,
|
||||
db: Database,
|
||||
settings: Settings,
|
||||
) -> None:
|
||||
if not callback.from_user or callback.from_user.id not in settings.admin_ids:
|
||||
await callback.answer("Нет доступа", show_alert=True)
|
||||
return
|
||||
try:
|
||||
tid = int((callback.data or "").rsplit(":", 1)[-1])
|
||||
except ValueError:
|
||||
await callback.answer("Ошибка", show_alert=True)
|
||||
return
|
||||
ticket = db.get_ticket(tid)
|
||||
if not ticket:
|
||||
await callback.answer("Не найден", show_alert=True)
|
||||
return
|
||||
db.set_ticket_status(tid, "closed")
|
||||
await callback.answer("Закрыто")
|
||||
ticket = db.get_ticket(tid)
|
||||
messages = db.list_ticket_messages(tid)
|
||||
if callback.message and ticket:
|
||||
await callback.message.edit_text(
|
||||
format_admin_ticket_card(ticket, messages),
|
||||
reply_markup=ticket_view_inline(tid, status="closed", is_admin=True),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(F.data.startswith("admin:tix:reopen:"))
|
||||
async def cb_admin_ticket_reopen(
|
||||
callback: CallbackQuery,
|
||||
db: Database,
|
||||
settings: Settings,
|
||||
) -> None:
|
||||
if not callback.from_user or callback.from_user.id not in settings.admin_ids:
|
||||
await callback.answer("Нет доступа", show_alert=True)
|
||||
return
|
||||
try:
|
||||
tid = int((callback.data or "").rsplit(":", 1)[-1])
|
||||
except ValueError:
|
||||
await callback.answer("Ошибка", show_alert=True)
|
||||
return
|
||||
ticket = db.set_ticket_status(tid, "open")
|
||||
if not ticket:
|
||||
await callback.answer("Не найден", show_alert=True)
|
||||
return
|
||||
await callback.answer("Открыт снова")
|
||||
messages = db.list_ticket_messages(tid)
|
||||
if callback.message:
|
||||
await callback.message.edit_text(
|
||||
format_admin_ticket_card(ticket, messages),
|
||||
reply_markup=ticket_view_inline(tid, status="open", is_admin=True),
|
||||
parse_mode="HTML",
|
||||
)
|
||||
Reference in New Issue
Block a user