+
{% for msg in messages|reverse %}
- {% if msg.text %}
{{ msg.text }}
{% endif %}
+ {% if msg.direction == 'out' %}
+
Поддержка
+ {% endif %}
+ {% if msg.text %}
{{ msg.text }}
{% endif %}
{% if msg.attachment_json and msg.attachment_json != 'null' %}
📎 Вложение
{% endif %}
-
+
{{ msg.created_at[:16] }}
- {% if msg.direction == 'in' %} · входящее{% else %} · исходящее{% endif %}
{% else %}
-
История пуста
+
История пуста
{% endfor %}
+
+{# Форма отправки #}
+{% if bot_user and bot_user.max_chat_id %}
+
+
+
Ctrl+Enter для отправки
+
+{% else %}
+
+ ℹ️ Отправка недоступна: нет chat_id (пользователь ещё не начал диалог с ботом).
+
+{% endif %}
+
+
+
{% endblock %}
diff --git a/bot/bot_instance.py b/bot/bot_instance.py
new file mode 100644
index 0000000..ec94d58
--- /dev/null
+++ b/bot/bot_instance.py
@@ -0,0 +1,20 @@
+"""Глобальное хранилище экземпляра бота для доступа из admin-панели."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from maxapi.bot import Bot
+
+_bot: "Bot | None" = None
+
+
+def set_bot(bot: "Bot") -> None:
+ global _bot
+ _bot = bot
+
+
+def get_bot() -> "Bot":
+ if _bot is None:
+ raise RuntimeError("Bot not initialized — call set_bot() first")
+ return _bot
diff --git a/bot/handlers/create_ticket.py b/bot/handlers/create_ticket.py
index f90cc5d..a362cf7 100644
--- a/bot/handlers/create_ticket.py
+++ b/bot/handlers/create_ticket.py
@@ -22,6 +22,7 @@ from maxapi.types.updates.message_created import MessageCreated
from bot.keyboards import done_ticket_kb, main_menu_kb
from bot.logging_setup import get_logger
+from bot.send_helper import bot_send
from bot.states import TicketStates
logger = get_logger("bot.handlers.create_ticket")
@@ -70,8 +71,10 @@ async def cb_create_ticket(
if chat_id is None:
return
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=bot_user["max_user_id"],
text=(
"📝 Опишите вашу проблему.\n\n"
"Вы также можете прикрепить файлы, фото или голосовые сообщения.\n"
@@ -85,7 +88,7 @@ async def cb_create_ticket(
async def on_ticket_description(
event: MessageCreated, context: BaseContext
) -> None:
- chat_id, _ = event.get_ids()
+ chat_id, user_id = event.get_ids()
if chat_id is None:
return
@@ -99,8 +102,10 @@ async def on_ticket_description(
)
await context.set_state(TicketStates.collecting_attachments)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text=(
"✏️ Описание принято. Вы можете добавить ещё файлы "
"или нажмите «Готово» для отправки заявки."
@@ -145,8 +150,10 @@ async def cb_done_ticket(
if not description:
await event.answer(notification="Сначала опишите проблему!")
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=bot_user["max_user_id"],
text="❗ Пожалуйста, сначала напишите описание проблемы.",
attachments=[done_ticket_kb()],
)
@@ -173,8 +180,10 @@ async def cb_done_ticket(
await context.clear()
await event.answer(notification="Заявка создана!")
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=bot_user["max_user_id"],
text=(
f"✅ Заявка №{ticket_id} успешно создана!\n"
"Наши специалисты свяжутся с вами в ближайшее время."
diff --git a/bot/handlers/menu.py b/bot/handlers/menu.py
index fc1e18d..5059aa3 100644
--- a/bot/handlers/menu.py
+++ b/bot/handlers/menu.py
@@ -5,12 +5,15 @@ 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_send
router = Router(router_id="menu")
@router.message_callback(F.callback.payload == "main_menu")
-async def cb_main_menu(event: MessageCallback, context: BaseContext) -> None:
+async def cb_main_menu(
+ event: MessageCallback, context: BaseContext, bot_user: dict
+) -> None:
await context.set_state(None)
await event.answer(notification="")
@@ -18,8 +21,10 @@ async def cb_main_menu(event: MessageCallback, context: BaseContext) -> None:
if chat_id is None:
return
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=bot_user["max_user_id"],
text="Главное меню:",
attachments=[main_menu_kb()],
)
diff --git a/bot/handlers/start.py b/bot/handlers/start.py
index 45099cb..6437fe7 100644
--- a/bot/handlers/start.py
+++ b/bot/handlers/start.py
@@ -18,6 +18,7 @@ from maxapi.filters.command import Command
from bot.keyboards import contact_request_kb, main_menu_kb
from bot.logging_setup import get_logger
+from bot.send_helper import bot_send
from bot.states import AuthStates
logger = get_logger("bot.handlers.start")
@@ -53,8 +54,10 @@ async def on_bot_started(event: BotStarted, context: BaseContext) -> None:
if bot_user["is_blocked"]:
logger.info("🚫 bot_started blocked | user_id=%s", user.user_id)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=event.chat_id,
+ user_id=user.user_id,
text=(
"⛔ Ваш номер не найден в системе. Доступ заблокирован.\n"
"Обратитесь к администратору для подключения."
@@ -65,8 +68,10 @@ 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)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=event.chat_id,
+ user_id=user.user_id,
text="👋 С возвращением! Выберите действие:",
attachments=[main_menu_kb()],
)
@@ -75,8 +80,10 @@ async def on_bot_started(event: BotStarted, context: BaseContext) -> None:
# Новый пользователь — запрашиваем контакт
logger.info("👋 bot_started new user | user_id=%s", user.user_id)
await context.set_state(AuthStates.waiting_contact)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=event.chat_id,
+ user_id=user.user_id,
text=(
"👋 Добро пожаловать в систему технической поддержки!\n\n"
"Для входа нажмите кнопку ниже и поделитесь своим номером телефона."
@@ -101,8 +108,10 @@ async def on_start_command(event: MessageCreated, context: BaseContext) -> None:
first_name=None,
)
await context.set_state(AuthStates.waiting_contact)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text=(
"👋 Добро пожаловать!\n\n"
"Поделитесь номером телефона для доступа к системе."
@@ -112,24 +121,30 @@ async def on_start_command(event: MessageCreated, context: BaseContext) -> None:
return
if bot_user["is_blocked"]:
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text="⛔ Доступ заблокирован. Обратитесь к администратору.",
)
return
if not bot_user["is_authorized"]:
await context.set_state(AuthStates.waiting_contact)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text="Поделитесь номером телефона для доступа:",
attachments=[contact_request_kb()],
)
return
await context.set_state(None)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text="Главное меню:",
attachments=[main_menu_kb()],
)
@@ -148,8 +163,10 @@ async def on_contact_shared(
# Извлекаем телефон из VCF
payload = contact.payload
if not isinstance(payload, ContactAttachmentPayload):
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text="Не удалось прочитать контакт. Попробуйте ещё раз.",
attachments=[contact_request_kb()],
)
@@ -158,8 +175,10 @@ async def on_contact_shared(
vcf = payload.vcf
raw_phone = vcf.phone
if not raw_phone:
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text="В контакте не найден номер телефона. Попробуйте ещё раз.",
attachments=[contact_request_kb()],
)
@@ -183,8 +202,10 @@ async def on_contact_shared(
logger.warning("🚫 phone not found, blocking | user_id=%s phone=%s", user_id, phone)
await block_bot_user(max_user_id=user_id, phone=phone)
await context.set_state(None)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text=(
"⛔ Ваш номер не найден в системе.\n"
"Доступ заблокирован. Обратитесь к администратору для подключения."
@@ -202,8 +223,10 @@ async def on_contact_shared(
)
await context.set_state(None)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=user_id,
text="✅ Вы успешно авторизованы! Выберите действие:",
attachments=[main_menu_kb()],
)
diff --git a/bot/handlers/view_tickets.py b/bot/handlers/view_tickets.py
index 6b7f6c3..8393128 100644
--- a/bot/handlers/view_tickets.py
+++ b/bot/handlers/view_tickets.py
@@ -14,6 +14,7 @@ from maxapi.types.updates.message_callback import MessageCallback
from bot.filters import PayloadStartsWith
from bot.keyboards import ticket_detail_kb, tickets_list_kb
+from bot.send_helper import bot_send
from database.queries.ticket_attachments import get_attachments
from database.queries.tickets import (
PAGE_SIZE,
@@ -43,15 +44,19 @@ async def _send_tickets_page(
return
if not tickets:
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=bot_user["max_user_id"],
text="📭 У вашей организации пока нет заявок.",
attachments=[ticket_detail_kb(page=0)],
)
return
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=bot_user["max_user_id"],
text=f"📋 Заявки организации (стр. {page + 1}):",
attachments=[tickets_list_kb(tickets, page=page, total=total)],
)
@@ -127,8 +132,10 @@ async def cb_ticket_detail(
att_lines.append(line)
text += "\n\n📎 Вложения:\n" + "\n".join(att_lines)
- await event.bot.send_message(
+ await bot_send(
+ event.bot,
chat_id=chat_id,
+ user_id=bot_user["max_user_id"],
text=text,
attachments=[ticket_detail_kb(page=0)],
)
diff --git a/bot/send_helper.py b/bot/send_helper.py
new file mode 100644
index 0000000..64fc51c
--- /dev/null
+++ b/bot/send_helper.py
@@ -0,0 +1,23 @@
+"""Обёртка над bot.send_message с автоматическим логированием исходящих."""
+from database.queries.message_log import log_message
+
+
+async def bot_send(
+ bot,
+ *,
+ chat_id: int,
+ user_id: int | None,
+ text: str,
+ attachments=None,
+) -> None:
+ kw: dict = {"chat_id": chat_id, "text": text}
+ if attachments is not None:
+ kw["attachments"] = attachments
+ await bot.send_message(**kw)
+ await log_message(
+ direction="out",
+ max_user_id=user_id,
+ max_chat_id=chat_id,
+ message_id=None,
+ text=text,
+ )
diff --git a/bot/setup.py b/bot/setup.py
index a7dd21b..eb14788 100644
--- a/bot/setup.py
+++ b/bot/setup.py
@@ -3,12 +3,14 @@ from maxapi.context import MemoryContext
from maxapi.dispatcher import Dispatcher, Router
import config
+from bot.bot_instance import set_bot
from bot.middleware import AuthMiddleware
from bot.handlers import start, menu, create_ticket, view_tickets
def create_bot_and_dispatcher() -> tuple[Bot, Dispatcher]:
bot = Bot(token=config.BOT_TOKEN)
+ set_bot(bot)
dp = Dispatcher(router_id="main", storage=MemoryContext)
dp.outer_middleware(AuthMiddleware())
diff --git a/data/support.db-shm b/data/support.db-shm
index 46ff81f..beaf4b9 100644
Binary files a/data/support.db-shm and b/data/support.db-shm differ
diff --git a/data/support.db-wal b/data/support.db-wal
index c7fb78f..f45ce7a 100644
Binary files a/data/support.db-wal and b/data/support.db-wal differ