Кнопочное управление услугами (+смена картинок) и быстрыми ответами
- Приём фото-вложений от админа (handle_message получает attachments). - Раздел «Услуги»: кнопки Добавить / Изменить / Удалить / Сменить картинку. Картинка скачивается, обрезается под 13:8 (650×400) и сохраняется как service_<id>.jpg; карусель берёт её приоритетно. Мастера на состояниях ADMIN_*. - Раздел «Быстрые ответы»: кнопки Добавить / Изменить / Удалить с выбором. - Контекст мастеров в памяти (_admin_ctx), отмена из любого шага. Заметка: CRUD «Карьеры» (направления игроков/должности) — отдельным заходом, т.к. опции там пока захардкожены и требуют перевода на БД. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,54 @@ def get_admin_keyboard(is_super: bool = False) -> str:
|
||||
return keyboard.get_keyboard()
|
||||
|
||||
|
||||
def get_svc_admin_keyboard() -> str:
|
||||
"""Клавиатура раздела управления услугами (направлениями)."""
|
||||
keyboard = VkKeyboard(one_time=False)
|
||||
keyboard.add_button("➕ Добавить услугу", color=VkKeyboardColor.POSITIVE)
|
||||
keyboard.add_line()
|
||||
keyboard.add_button("✏️ Изменить услугу", color=VkKeyboardColor.SECONDARY)
|
||||
keyboard.add_button("🖼 Сменить картинку", color=VkKeyboardColor.SECONDARY)
|
||||
keyboard.add_line()
|
||||
keyboard.add_button("🗑 Удалить услугу", color=VkKeyboardColor.NEGATIVE)
|
||||
keyboard.add_line()
|
||||
keyboard.add_button("В админку ↩️", color=VkKeyboardColor.PRIMARY)
|
||||
return keyboard.get_keyboard()
|
||||
|
||||
|
||||
def get_faq_admin_keyboard() -> str:
|
||||
"""Клавиатура раздела управления быстрыми ответами."""
|
||||
keyboard = VkKeyboard(one_time=False)
|
||||
keyboard.add_button("➕ Добавить ответ", color=VkKeyboardColor.POSITIVE)
|
||||
keyboard.add_line()
|
||||
keyboard.add_button("✏️ Изменить ответ", color=VkKeyboardColor.SECONDARY)
|
||||
keyboard.add_button("🗑 Удалить ответ", color=VkKeyboardColor.NEGATIVE)
|
||||
keyboard.add_line()
|
||||
keyboard.add_button("В админку ↩️", color=VkKeyboardColor.PRIMARY)
|
||||
return keyboard.get_keyboard()
|
||||
|
||||
|
||||
def get_pick_keyboard(items: list) -> str:
|
||||
"""Клавиатура выбора из списка пар (id, label). Кнопка отправляет '<id>. <label>'."""
|
||||
keyboard = VkKeyboard(one_time=False)
|
||||
count = 0
|
||||
for _id, label in items[:10]:
|
||||
if count and count % 2 == 0:
|
||||
keyboard.add_line()
|
||||
short = label if len(label) <= 30 else label[:29] + "…"
|
||||
keyboard.add_button(f"{_id}. {short}", color=VkKeyboardColor.SECONDARY)
|
||||
count += 1
|
||||
keyboard.add_line()
|
||||
keyboard.add_button("Отмена ❌", color=VkKeyboardColor.NEGATIVE)
|
||||
return keyboard.get_keyboard()
|
||||
|
||||
|
||||
def get_cancel_admin_keyboard() -> str:
|
||||
"""Кнопка отмены для админских мастеров ввода."""
|
||||
keyboard = VkKeyboard(one_time=False)
|
||||
keyboard.add_button("Отмена ❌", color=VkKeyboardColor.NEGATIVE)
|
||||
return keyboard.get_keyboard()
|
||||
|
||||
|
||||
def get_vacancy_choice_keyboard() -> str:
|
||||
"""Выбор направления в разделе 'Карьера в ИКП'."""
|
||||
keyboard = VkKeyboard(one_time=False)
|
||||
@@ -354,6 +402,8 @@ class VKBotApplication:
|
||||
self.longpoll = None
|
||||
# Тексты рассылок, ожидающих подтверждения админом: {admin_id: text}
|
||||
self._pending_broadcasts: Dict[int, str] = {}
|
||||
# Контекст админских мастеров (что редактируем): {admin_id: {...}}
|
||||
self._admin_ctx: Dict[int, dict] = {}
|
||||
# Кэш загруженных в VK фото: {путь_к_файлу: {"attachment": "...", "raw": "..."}}
|
||||
self._photo_cache: Dict[str, Dict[str, str]] = {}
|
||||
# Анти-флуд и анти-спам (in-memory, сбрасываются при перезапуске)
|
||||
@@ -535,10 +585,11 @@ class VKBotApplication:
|
||||
self._last_notify[user_id] = now
|
||||
return True
|
||||
|
||||
def handle_message(self, user_id: int, text: str):
|
||||
def handle_message(self, user_id: int, text: str, attachments: Optional[list] = None):
|
||||
"""Маршрутизатор входящих сообщений на основе состояний пользователя."""
|
||||
text_clean = text.strip()
|
||||
text_lower = text_clean.lower()
|
||||
attachments = attachments or []
|
||||
|
||||
# Анти-флуд + временный бан (админы — исключение)
|
||||
if not self._is_admin(user_id):
|
||||
@@ -579,9 +630,55 @@ class VKBotApplication:
|
||||
|
||||
# Глобальная обработка админ-команд и кнопок (только для ADMIN_IDS)
|
||||
if self._is_admin(user_id):
|
||||
# Мастера админки (ожидают ввод текста/выбор/фото) — ловим по состоянию
|
||||
if state.startswith("ADMIN_"):
|
||||
self._handle_admin_wizard(user_id, text_clean, text_lower, state, attachments)
|
||||
return
|
||||
if text_lower in ["админка", "админка ⚙️", "/admin"]:
|
||||
self._send_admin_panel(user_id)
|
||||
return
|
||||
# Кнопки-действия раздела «Услуги»
|
||||
elif text_lower in ["➕ добавить услугу", "добавить услугу"]:
|
||||
db.update_user_state(user_id, "ADMIN_SVC_ADD")
|
||||
self.send_message(user_id, "➕ Новая услуга. Отправьте: Название | Цена | Описание\n"
|
||||
"Пример: Тренировка по Valorant 🎯 | 1 500 ₽/час | Разбор стрельбы.",
|
||||
keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
elif text_lower in ["✏️ изменить услугу", "изменить услугу"]:
|
||||
items = [(k, s["name"]) for k, s in db.get_services().items()]
|
||||
db.update_user_state(user_id, "ADMIN_SVC_EDIT_PICK")
|
||||
self.send_message(user_id, "✏️ Какую услугу изменить? Выберите:", keyboard=get_pick_keyboard(items))
|
||||
return
|
||||
elif text_lower in ["🖼 сменить картинку", "сменить картинку"]:
|
||||
items = [(k, s["name"]) for k, s in db.get_services().items()]
|
||||
db.update_user_state(user_id, "ADMIN_SVC_IMG_PICK")
|
||||
self.send_message(user_id, "🖼 Для какой услуги сменить картинку? Выберите:", keyboard=get_pick_keyboard(items))
|
||||
return
|
||||
elif text_lower in ["🗑 удалить услугу", "удалить услугу"]:
|
||||
items = [(k, s["name"]) for k, s in db.get_services().items()]
|
||||
db.update_user_state(user_id, "ADMIN_SVC_DEL_PICK")
|
||||
self.send_message(user_id, "🗑 Какую услугу удалить? Выберите:", keyboard=get_pick_keyboard(items))
|
||||
return
|
||||
# Кнопки-действия раздела «Быстрые ответы»
|
||||
elif text_lower in ["➕ добавить ответ", "добавить ответ"]:
|
||||
db.update_user_state(user_id, "ADMIN_FAQ_ADD")
|
||||
self.send_message(user_id, "➕ Новый быстрый ответ. Отправьте: ключевое слово | текст ответа\n"
|
||||
"Пример: расписание | Тренировки ежедневно с 10:00 до 22:00.",
|
||||
keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
elif text_lower in ["✏️ изменить ответ", "изменить ответ"]:
|
||||
items = [(i, kw) for i, kw, _ in db.get_custom_faq()]
|
||||
db.update_user_state(user_id, "ADMIN_FAQ_EDIT_PICK")
|
||||
self.send_message(user_id, "✏️ Какой ответ изменить? Выберите:", keyboard=get_pick_keyboard(items))
|
||||
return
|
||||
elif text_lower in ["🗑 удалить ответ", "удалить ответ"]:
|
||||
items = [(i, kw) for i, kw, _ in db.get_custom_faq()]
|
||||
db.update_user_state(user_id, "ADMIN_FAQ_DEL_PICK")
|
||||
self.send_message(user_id, "🗑 Какой ответ удалить? Выберите:", keyboard=get_pick_keyboard(items))
|
||||
return
|
||||
elif text_lower in ["в админку ↩️", "в админку"]:
|
||||
self._send_admin_panel(user_id)
|
||||
return
|
||||
elif text_lower in ["статистика 📊", "стата", "статистика", "/stats"]:
|
||||
self._handle_admin_stats(user_id)
|
||||
return
|
||||
@@ -1326,6 +1423,43 @@ class VKBotApplication:
|
||||
logger.error(f"Ошибка загрузки фото {image_path} в VK: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _first_photo(attachments: list) -> Optional[dict]:
|
||||
"""Возвращает первое фото-вложение из сообщения (или None)."""
|
||||
for att in attachments or []:
|
||||
if isinstance(att, dict) and att.get("type") == "photo":
|
||||
return att.get("photo")
|
||||
return None
|
||||
|
||||
def _save_service_image(self, service_id: str, photo: dict) -> bool:
|
||||
"""Скачивает присланное фото, обрезает под 13:8 (650×400) и сохраняет как service_<id>.jpg."""
|
||||
import os
|
||||
try:
|
||||
sizes = photo.get("sizes", [])
|
||||
if not sizes:
|
||||
return False
|
||||
url = max(sizes, key=lambda s: s.get("width", 0)).get("url")
|
||||
import requests
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
data = requests.get(url, timeout=15).content
|
||||
im = Image.open(BytesIO(data)).convert("RGB")
|
||||
TARGET = 13 / 8
|
||||
w, h = im.size
|
||||
if w / h > TARGET:
|
||||
nw = int(h * TARGET); x = (w - nw) // 2; box = (x, 0, x + nw, h)
|
||||
else:
|
||||
nh = int(w / TARGET); y = (h - nh) // 2; box = (0, y, w, y + nh)
|
||||
im = im.crop(box).resize((650, 400), Image.LANCZOS)
|
||||
path = os.path.join(os.path.dirname(__file__), "help_img", f"service_{service_id}.jpg")
|
||||
im.save(path, "JPEG", quality=88)
|
||||
self._photo_cache.pop(path, None) # сбросить кэш, чтобы перезалить в VK
|
||||
logger.info(f"Картинка услуги {service_id} обновлена: {path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка сохранения картинки услуги {service_id}: {e}")
|
||||
return False
|
||||
|
||||
def get_logo_attachment(self, peer_id: int) -> Optional[str]:
|
||||
"""Attachment-строка с логотипом ИКП для прикрепления к приветствию."""
|
||||
import os
|
||||
@@ -1369,7 +1503,10 @@ class VKBotApplication:
|
||||
img_by_id = {"1": "cs2.jpg", "2": "dota2.jpg", "3": "wot.jpg"}
|
||||
elements = []
|
||||
for key, svc in config.SERVICES.items():
|
||||
disc_img = os.path.join(base, img_by_id.get(key, ""))
|
||||
# Сначала ищем загруженную админом картинку service_<id>.jpg, затем легаси-имя
|
||||
uploaded = os.path.join(base, f"service_{key}.jpg")
|
||||
legacy = os.path.join(base, img_by_id.get(key, ""))
|
||||
disc_img = uploaded if os.path.exists(uploaded) else legacy
|
||||
info = self._upload_photo(peer_id, disc_img) if os.path.exists(disc_img) else None
|
||||
element = {
|
||||
"title": svc["name"][:80],
|
||||
@@ -1933,6 +2070,164 @@ class VKBotApplication:
|
||||
# ЗАПУСК И ОБРАБОТКА LONGPOLL
|
||||
# ------------------------------------------
|
||||
|
||||
def _handle_admin_wizard(self, user_id: int, text: str, text_lower: str, state: str, attachments: list):
|
||||
"""Мастера админки: добавление/изменение/удаление услуг и быстрых ответов, смена картинок."""
|
||||
if text_lower in ["отмена ❌", "отмена", "в админку ↩️", "в админку", "назад"]:
|
||||
self._admin_ctx.pop(user_id, None)
|
||||
db.update_user_state(user_id, "FREE_CHAT")
|
||||
self._send_admin_panel(user_id)
|
||||
return
|
||||
|
||||
def back_to_services(msg=None):
|
||||
self._admin_ctx.pop(user_id, None)
|
||||
db.update_user_state(user_id, "FREE_CHAT")
|
||||
if msg:
|
||||
self.send_message(user_id, msg)
|
||||
self._send_admin_menu(user_id)
|
||||
|
||||
def back_to_faq(msg=None):
|
||||
self._admin_ctx.pop(user_id, None)
|
||||
db.update_user_state(user_id, "FREE_CHAT")
|
||||
if msg:
|
||||
self.send_message(user_id, msg)
|
||||
self._send_faq_admin(user_id)
|
||||
|
||||
def pick_id(t):
|
||||
t = t.strip()
|
||||
return t.split(".")[0].split()[0] if t else ""
|
||||
|
||||
# ---------- Услуги ----------
|
||||
if state == "ADMIN_SVC_ADD":
|
||||
parts = [p.strip() for p in text.split("|")]
|
||||
if len(parts) < 3 or not all(parts[:3]):
|
||||
self.send_message(user_id, "⚠️ Формат: Название | Цена | Описание", keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
new_id = db.add_service(parts[0], parts[1], parts[2])
|
||||
config.SERVICES = db.get_services()
|
||||
back_to_services(f"✅ Услуга добавлена (№{new_id}). Картинку можно задать через «🖼 Сменить картинку».")
|
||||
return
|
||||
|
||||
if state == "ADMIN_SVC_EDIT_PICK":
|
||||
sid = pick_id(text)
|
||||
services = db.get_services()
|
||||
if sid not in services:
|
||||
self.send_message(user_id, "⚠️ Выберите услугу кнопкой ниже.")
|
||||
return
|
||||
self._admin_ctx[user_id] = {"svc_id": sid}
|
||||
db.update_user_state(user_id, "ADMIN_SVC_EDIT_VAL")
|
||||
self.send_message(user_id,
|
||||
f"✏️ Услуга «{services[sid]['name']}». Отправьте: поле | значение\n"
|
||||
"Поля: имя, цена, описание\nПример: цена | 1 900 ₽/час",
|
||||
keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
|
||||
if state == "ADMIN_SVC_EDIT_VAL":
|
||||
sid = self._admin_ctx.get(user_id, {}).get("svc_id")
|
||||
if not sid or sid not in db.get_services():
|
||||
back_to_services("⚠️ Услуга не найдена.")
|
||||
return
|
||||
if "|" not in text:
|
||||
self.send_message(user_id, "⚠️ Формат: поле | значение", keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
field, value = [p.strip() for p in text.split("|", 1)]
|
||||
mapping = {"имя": "name", "название": "name", "цена": "price", "стоимость": "price", "описание": "description"}
|
||||
if field.lower() not in mapping or not value:
|
||||
self.send_message(user_id, "⚠️ Поле должно быть: имя / цена / описание.", keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
db.update_service(sid, mapping[field.lower()], value)
|
||||
config.SERVICES = db.get_services()
|
||||
back_to_services(f"✅ Услуга №{sid} обновлена.")
|
||||
return
|
||||
|
||||
if state == "ADMIN_SVC_IMG_PICK":
|
||||
sid = pick_id(text)
|
||||
if sid not in db.get_services():
|
||||
self.send_message(user_id, "⚠️ Выберите услугу кнопкой ниже.")
|
||||
return
|
||||
self._admin_ctx[user_id] = {"svc_id": sid}
|
||||
db.update_user_state(user_id, "ADMIN_SVC_IMG_WAIT")
|
||||
self.send_message(user_id,
|
||||
"🖼 Пришлите картинку (именно фото). Лучше широкую (~13:8) — я сам аккуратно обрежу.",
|
||||
keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
|
||||
if state == "ADMIN_SVC_IMG_WAIT":
|
||||
photo = self._first_photo(attachments)
|
||||
if not photo:
|
||||
self.send_message(user_id, "⚠️ Прикрепите именно фото (картинкой), не файлом.", keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
sid = self._admin_ctx.get(user_id, {}).get("svc_id")
|
||||
ok = self._save_service_image(sid, photo) if sid else False
|
||||
if ok:
|
||||
import os
|
||||
self._upload_photo(config.OPERATOR_PEER_ID,
|
||||
os.path.join(os.path.dirname(__file__), "help_img", f"service_{sid}.jpg"))
|
||||
back_to_services("✅ Картинка обновлена." if ok else "⚠️ Не удалось сохранить картинку.")
|
||||
return
|
||||
|
||||
if state == "ADMIN_SVC_DEL_PICK":
|
||||
sid = pick_id(text)
|
||||
if sid not in db.get_services():
|
||||
self.send_message(user_id, "⚠️ Выберите услугу кнопкой ниже.")
|
||||
return
|
||||
db.delete_service(sid)
|
||||
config.SERVICES = db.get_services()
|
||||
back_to_services(f"✅ Услуга №{sid} удалена.")
|
||||
return
|
||||
|
||||
# ---------- Быстрые ответы ----------
|
||||
if state == "ADMIN_FAQ_ADD":
|
||||
if "|" not in text:
|
||||
self.send_message(user_id, "⚠️ Формат: ключевое слово | текст ответа", keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
kw, ans = [p.strip() for p in text.split("|", 1)]
|
||||
if not kw or not ans:
|
||||
self.send_message(user_id, "⚠️ Нужны и ключевое слово, и текст ответа.", keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
db.add_custom_faq(kw, ans)
|
||||
back_to_faq("✅ Быстрый ответ добавлен.")
|
||||
return
|
||||
|
||||
if state == "ADMIN_FAQ_EDIT_PICK":
|
||||
fid = pick_id(text)
|
||||
ids = [str(i) for i, _, _ in db.get_custom_faq()]
|
||||
if fid not in ids:
|
||||
self.send_message(user_id, "⚠️ Выберите ответ кнопкой ниже.")
|
||||
return
|
||||
self._admin_ctx[user_id] = {"faq_id": int(fid)}
|
||||
db.update_user_state(user_id, "ADMIN_FAQ_EDIT_VAL")
|
||||
self.send_message(user_id, "✏️ Отправьте новый вариант: ключевое слово | текст ответа",
|
||||
keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
|
||||
if state == "ADMIN_FAQ_EDIT_VAL":
|
||||
fid = self._admin_ctx.get(user_id, {}).get("faq_id")
|
||||
if "|" not in text:
|
||||
self.send_message(user_id, "⚠️ Формат: ключевое слово | текст ответа", keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
kw, ans = [p.strip() for p in text.split("|", 1)]
|
||||
if not kw or not ans:
|
||||
self.send_message(user_id, "⚠️ Нужны и ключевое слово, и текст ответа.", keyboard=get_cancel_admin_keyboard())
|
||||
return
|
||||
if fid:
|
||||
db.delete_custom_faq(fid)
|
||||
db.add_custom_faq(kw, ans)
|
||||
back_to_faq("✅ Быстрый ответ обновлён.")
|
||||
return
|
||||
|
||||
if state == "ADMIN_FAQ_DEL_PICK":
|
||||
fid = pick_id(text)
|
||||
if not fid.isdigit() or not db.delete_custom_faq(int(fid)):
|
||||
self.send_message(user_id, "⚠️ Выберите ответ кнопкой ниже.")
|
||||
return
|
||||
back_to_faq(f"✅ Ответ №{fid} удалён.")
|
||||
return
|
||||
|
||||
# Неизвестное ADMIN_-состояние — сброс
|
||||
self._admin_ctx.pop(user_id, None)
|
||||
db.update_user_state(user_id, "FREE_CHAT")
|
||||
self._send_admin_panel(user_id)
|
||||
|
||||
def _send_admin_menu(self, user_id: int):
|
||||
"""Отправляет администратору панель управления услугами с текущим списком."""
|
||||
services = db.get_services()
|
||||
@@ -1944,26 +2239,13 @@ class VKBotApplication:
|
||||
services_list_text += f" 📝 Описание: {svc['description']}\n\n"
|
||||
|
||||
admin_text = (
|
||||
"⚙️ Панель управления услугами (Админ-меню)\n\n"
|
||||
"Здесь вы можете редактировать существующие услуги или добавлять новые.\n\n"
|
||||
"📋 Текущий список активных услуг:\n\n"
|
||||
"⚙️ Управление услугами (направлениями)\n\n"
|
||||
"📋 Текущие направления:\n\n"
|
||||
f"{services_list_text}"
|
||||
"🔧 Доступные команды управления:\n\n"
|
||||
"1. Редактирование услуги:\n"
|
||||
" 👉 Отправьте: ред [номер] [поле] [новое значение]\n"
|
||||
" Доступные поля: имя, цена, описание.\n"
|
||||
" *Пример:* ред 1 цена 1 800 ₽/час\n\n"
|
||||
"2. Добавление услуги:\n"
|
||||
" 👉 Отправьте: доб [Имя] | [Цена] | [Описание]\n"
|
||||
" *Пример:* доб Тренировка по Valorant 🎯 | 1 500 ₽/час | Разбор стрельбы и aim.\n\n"
|
||||
"3. Удаление услуги:\n"
|
||||
" 👉 Отправьте: удал [номер]\n"
|
||||
" *Пример:* удал 4\n\n"
|
||||
"📊 Статистика: отправьте стата\n\n"
|
||||
"📣 Рассылка по подписчикам:\n"
|
||||
" 👉 рассылка [текст] → предпросмотр → рассылка подтвердить"
|
||||
"Управляйте кнопками ниже:\n"
|
||||
"• ➕ Добавить • ✏️ Изменить • 🖼 Сменить картинку • 🗑 Удалить"
|
||||
)
|
||||
self.send_message(user_id, admin_text)
|
||||
self.send_message(user_id, admin_text, keyboard=get_svc_admin_keyboard())
|
||||
|
||||
def _handle_admin_edit(self, user_id: int, text: str):
|
||||
"""Обрабатывает редактирование услуги администратором."""
|
||||
@@ -2149,10 +2431,8 @@ class VKBotApplication:
|
||||
"💬 Быстрые ответы (по ключевым словам)\n\n"
|
||||
"Если в сообщении клиента встречается ключевое слово — бот сразу отвечает заданным текстом.\n\n"
|
||||
f"📋 Текущие:\n{listing}\n"
|
||||
"Команды:\n"
|
||||
"• Добавить: фак доб ключевое слово | текст ответа\n"
|
||||
" Пример: фак доб расписание | Тренировки идут ежедневно с 10:00 до 22:00.\n"
|
||||
"• Удалить: фак удал [номер]"
|
||||
"Управляйте кнопками ниже: ➕ добавить • ✏️ изменить • 🗑 удалить",
|
||||
keyboard=get_faq_admin_keyboard()
|
||||
)
|
||||
|
||||
def _handle_faq_add(self, user_id: int, text: str):
|
||||
@@ -2329,8 +2609,9 @@ class VKBotApplication:
|
||||
message_obj = event.obj.message
|
||||
user_id = message_obj["from_id"]
|
||||
text = message_obj["text"]
|
||||
attachments = message_obj.get("attachments", [])
|
||||
try:
|
||||
self.handle_message(user_id, text)
|
||||
self.handle_message(user_id, text, attachments)
|
||||
except Exception as e:
|
||||
# Ошибка обработки одного сообщения не должна останавливать бота
|
||||
logger.error(f"Ошибка обработки сообщения от {user_id}: {e}")
|
||||
|
||||
Reference in New Issue
Block a user