Супер-админ, выбор даты/времени звонка, инлайн-документы, маска телефона

- Супер-админ (config.SUPER_ADMIN_IDS, только через код): добавляет/удаляет
  обычных админов из бота (раздел «Администраторы 👑»), таблица admins.
  _is_admin теперь учитывает супер-админов, .env и добавленных из бота.
- «Когда удобно созвониться» → выбор конкретного дня (кнопки на ближайшие даты)
  и времени (слоты), сохраняется как «День, ЧЧ:ММ».
- Шаг согласия: инлайн-кнопки со ссылками на оферту и политику ПД (PDF), затем
  подтверждение. Старое вложение .docx убрано.
- Маска отображения телефона: +7 (999) 123-45-67 в карточке и уведомлениях.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kenik
2026-06-24 18:39:19 +03:00
parent ab7df0644e
commit 29097a18c3
3 changed files with 236 additions and 53 deletions
+191 -51
View File
@@ -66,6 +66,17 @@ def normalize_phone(text: str) -> Optional[str]:
return None return None
def format_phone_display(phone: str) -> str:
"""Красивое отображение номера: +7 (999) 123-45-67 для РФ, иначе как есть."""
if not phone:
return phone
digits = re.sub(r"\D", "", phone)
if len(digits) == 11 and digits[0] in ("7", "8"):
d = digits
return f"+7 ({d[1:4]}) {d[4:7]}-{d[7:9]}-{d[9:11]}"
return phone
def is_valid_name(text: str) -> bool: def is_valid_name(text: str) -> bool:
"""Имя должно состоять только из букв, пробела, дефиса или апострофа (без цифр и спецсимволов).""" """Имя должно состоять только из букв, пробела, дефиса или апострофа (без цифр и спецсимволов)."""
return bool(NAME_REGEX.match(text.strip())) return bool(NAME_REGEX.match(text.strip()))
@@ -94,8 +105,8 @@ def get_main_keyboard(is_admin: bool = False) -> str:
return keyboard.get_keyboard() return keyboard.get_keyboard()
def get_admin_keyboard() -> str: def get_admin_keyboard(is_super: bool = False) -> str:
"""Клавиатура админ-панели с разделами.""" """Клавиатура админ-панели с разделами. Супер-админу — раздел «Администраторы»."""
keyboard = VkKeyboard(one_time=False) keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Статистика 📊", color=VkKeyboardColor.SECONDARY) keyboard.add_button("Статистика 📊", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("Кандидаты 👥", color=VkKeyboardColor.SECONDARY) keyboard.add_button("Кандидаты 👥", color=VkKeyboardColor.SECONDARY)
@@ -105,6 +116,9 @@ def get_admin_keyboard() -> str:
keyboard.add_button("Быстрые ответы 💬", color=VkKeyboardColor.SECONDARY) keyboard.add_button("Быстрые ответы 💬", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("Рассылка 📣", color=VkKeyboardColor.SECONDARY) keyboard.add_button("Рассылка 📣", color=VkKeyboardColor.SECONDARY)
keyboard.add_line() keyboard.add_line()
if is_super:
keyboard.add_button("Администраторы 👑", color=VkKeyboardColor.NEGATIVE)
keyboard.add_line()
keyboard.add_button("В главное меню ↩️", color=VkKeyboardColor.PRIMARY) keyboard.add_button("В главное меню ↩️", color=VkKeyboardColor.PRIMARY)
return keyboard.get_keyboard() return keyboard.get_keyboard()
@@ -175,6 +189,15 @@ def get_faq_keyboard() -> str:
return keyboard.get_keyboard() return keyboard.get_keyboard()
def get_consent_docs_keyboard() -> str:
"""Инлайн-кнопки со ссылками на оферту и политику обработки ПД."""
keyboard = VkKeyboard(inline=True)
keyboard.add_openlink_button("📄 Оферта", getattr(config, "OFFER_URL", "https://infocyber.pro/"))
keyboard.add_line()
keyboard.add_openlink_button("🔐 Политика обработки ПД", getattr(config, "POLICY_URL", "https://infocyber.pro/"))
return keyboard.get_keyboard()
def get_socials_keyboard() -> str: def get_socials_keyboard() -> str:
"""Клавиатура с кнопками-ссылками на площадки ИКП (открываются в один тап).""" """Клавиатура с кнопками-ссылками на площадки ИКП (открываются в один тап)."""
keyboard = VkKeyboard(one_time=False) keyboard = VkKeyboard(one_time=False)
@@ -229,15 +252,35 @@ def get_returning_keyboard() -> str:
return keyboard.get_keyboard() return keyboard.get_keyboard()
def get_calltime_keyboard() -> str: def get_calldate_keyboard() -> str:
"""Клавиатура выбора удобного времени для звонка.""" """Клавиатура выбора ДНЯ для звонка (сегодня/завтра + ближайшие даты)."""
from datetime import datetime, timedelta
wd = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
today = datetime.now()
keyboard = VkKeyboard(one_time=False) keyboard = VkKeyboard(one_time=False)
keyboard.add_button("Как можно скорее ⏰", color=VkKeyboardColor.PRIMARY) keyboard.add_button("Сегодня", color=VkKeyboardColor.PRIMARY)
keyboard.add_button("Завтра", color=VkKeyboardColor.PRIMARY)
keyboard.add_line() keyboard.add_line()
keyboard.add_button("Сегодня вечером 🌆", color=VkKeyboardColor.SECONDARY) for i in (2, 3, 4):
keyboard.add_button("Завтра ☀️", color=VkKeyboardColor.SECONDARY) d = today + timedelta(days=i)
keyboard.add_button(f"{wd[d.weekday()]} {d.strftime('%d.%m')}", color=VkKeyboardColor.SECONDARY)
keyboard.add_line() keyboard.add_line()
keyboard.add_button("В выходные 🎮", color=VkKeyboardColor.SECONDARY) keyboard.add_button("Отменить заполнение ", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard()
def get_calltime_keyboard() -> str:
"""Клавиатура выбора ВРЕМЕНИ (слоты в рабочие часы)."""
keyboard = VkKeyboard(one_time=False)
keyboard.add_button("10:00", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("12:00", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("14:00", color=VkKeyboardColor.SECONDARY)
keyboard.add_line()
keyboard.add_button("16:00", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("18:00", color=VkKeyboardColor.SECONDARY)
keyboard.add_button("20:00", color=VkKeyboardColor.SECONDARY)
keyboard.add_line()
keyboard.add_button("Любое время", color=VkKeyboardColor.PRIMARY)
keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE) keyboard.add_button("Отменить заполнение ❌", color=VkKeyboardColor.NEGATIVE)
return keyboard.get_keyboard() return keyboard.get_keyboard()
@@ -368,9 +411,20 @@ class VKBotApplication:
self._msg_times[user_id] = times self._msg_times[user_id] = times
return len(times) > FLOOD_MAX_MSGS return len(times) > FLOOD_MAX_MSGS
def _is_super_admin(self, user_id: int) -> bool:
"""Супер-админ (определён только в config.SUPER_ADMIN_IDS, меняется в коде)."""
return user_id in getattr(config, "SUPER_ADMIN_IDS", [])
def _is_admin(self, user_id: int) -> bool: def _is_admin(self, user_id: int) -> bool:
"""Является ли пользователь администратором (по config.ADMIN_IDS).""" """Админ: супер-админ, либо из config.ADMIN_IDS, либо добавленный в рантайме."""
return hasattr(config, "ADMIN_IDS") and user_id in config.ADMIN_IDS if self._is_super_admin(user_id):
return True
if hasattr(config, "ADMIN_IDS") and user_id in config.ADMIN_IDS:
return True
try:
return user_id in db.get_admins()
except Exception:
return False
def _operator_ids(self) -> list: def _operator_ids(self) -> list:
"""Список ID операторов (несколько). Фолбэк — один OPERATOR_PEER_ID.""" """Список ID операторов (несколько). Фолбэк — один OPERATOR_PEER_ID."""
@@ -395,16 +449,84 @@ class VKBotApplication:
def _send_admin_panel(self, user_id: int): def _send_admin_panel(self, user_id: int):
"""Главный экран админ-панели с кнопками разделов.""" """Главный экран админ-панели с кнопками разделов."""
is_super = self._is_super_admin(user_id)
extra = "\n• 👑 Администраторы — добавить/удалить (только супер-админ)" if is_super else ""
self.send_message( self.send_message(
user_id, user_id,
"⚙️ Админ-панель ИКП\n\n" "⚙️ Админ-панель ИКП\n\n"
"Выберите раздел на кнопках ниже:\n" "Выберите раздел на кнопках ниже:\n"
"• 📊 Статистика — заявки, отклики, диалоги\n" "• 📊 Статистика — заявки, отклики, диалоги\n"
"• 👥 Кандидаты — отклики и их статусы\n"
"• ⚙️ Управление услугами — добавить/изменить/удалить\n" "• ⚙️ Управление услугами — добавить/изменить/удалить\n"
"📣 Рассылка — сообщение подписчикам", "💬 Быстрые ответы — авто-ответы по ключевым словам\n"
keyboard=get_admin_keyboard() "• 📣 Рассылка — сообщение подписчикам"
+ extra,
keyboard=get_admin_keyboard(is_super=is_super)
) )
def _send_admins_panel(self, user_id: int):
"""Раздел управления администраторами (только супер-админ)."""
if not self._is_super_admin(user_id):
self.send_message(user_id, "⛔ Раздел доступен только супер-админу.")
return
runtime = db.get_admins()
supers = getattr(config, "SUPER_ADMIN_IDS", [])
cfg = [a for a in getattr(config, "ADMIN_IDS", []) if a not in supers]
lines = "👑 Супер-админы (меняются только в коде):\n"
lines += ("".join(f" • vk.com/id{a}\n" for a in supers) or "\n")
lines += "\n🛡 Админы из .env (ADMIN_IDS):\n"
lines += ("".join(f" • vk.com/id{a}\n" for a in cfg) or "\n")
lines += "\n➕ Добавленные из бота:\n"
lines += ("".join(f" • vk.com/id{a}\n" for a in runtime) or "\n")
self.send_message(
user_id,
"👑 Управление администраторами\n\n"
f"{lines}\n"
"Команды:\n"
"• Добавить админа: админ доб [VK_ID]\n"
"• Удалить админа: админ удал [VK_ID]\n"
" (снять можно только добавленных из бота)\n\n"
"ℹ️ Сменить/добавить СУПЕР-АДМИНА можно только в коде: откройте config.py, "
"впишите VK ID в список SUPER_ADMIN_IDS и перезапустите бота.",
keyboard=get_admin_keyboard(is_super=True)
)
def _handle_admin_add_admin(self, user_id: int, text: str):
if not self._is_super_admin(user_id):
self.send_message(user_id, "⛔ Добавлять админов может только супер-админ.")
return
arg = text.split(" ", 2)[-1].strip()
if not arg.lstrip("-").isdigit():
self.send_message(user_id, "⚠️ Формат: админ доб [VK_ID числом]")
return
new_id = int(arg)
if db.add_admin(new_id):
self.send_message(user_id, f"✅ Пользователь vk.com/id{new_id} теперь администратор.")
try:
self.send_message(new_id, "🛡 Вам выданы права администратора бота ИКП. "
"Откройте меню — появится кнопка «Админка ⚙️».")
except Exception:
pass
else:
self.send_message(user_id, f"ℹ️ Пользователь vk.com/id{new_id} уже администратор.")
self._send_admins_panel(user_id)
def _handle_admin_remove_admin(self, user_id: int, text: str):
if not self._is_super_admin(user_id):
self.send_message(user_id, "⛔ Удалять админов может только супер-админ.")
return
arg = text.split(" ", 2)[-1].strip()
if not arg.lstrip("-").isdigit():
self.send_message(user_id, "⚠️ Формат: админ удал [VK_ID числом]")
return
rem_id = int(arg)
if db.remove_admin(rem_id):
self.send_message(user_id, f"✅ Пользователь vk.com/id{rem_id} больше не администратор.")
else:
self.send_message(user_id, f"⚠️ vk.com/id{rem_id} нет среди добавленных из бота "
"(админов из .env и супер-админов отсюда снять нельзя).")
self._send_admins_panel(user_id)
def _notify_allowed(self, user_id: int) -> bool: def _notify_allowed(self, user_id: int) -> bool:
"""Антиспам: не чаще раза в NOTIFY_COOLDOWN_SEC уведомляем оператора об одном пользователе.""" """Антиспам: не чаще раза в NOTIFY_COOLDOWN_SEC уведомляем оператора об одном пользователе."""
now = time.time() now = time.time()
@@ -488,6 +610,15 @@ class VKBotApplication:
elif text_lower.startswith("статус "): elif text_lower.startswith("статус "):
self._handle_candidate_status(user_id, text_clean) self._handle_candidate_status(user_id, text_clean)
return return
elif text_lower in ["администраторы 👑", "администраторы", "админы"]:
self._send_admins_panel(user_id)
return
elif text_lower.startswith("админ доб"):
self._handle_admin_add_admin(user_id, text_clean)
return
elif text_lower.startswith("админ удал"):
self._handle_admin_remove_admin(user_id, text_clean)
return
elif text_lower.startswith("ред "): elif text_lower.startswith("ред "):
self._handle_admin_edit(user_id, text_clean) self._handle_admin_edit(user_id, text_clean)
return return
@@ -552,10 +683,10 @@ class VKBotApplication:
self._handle_form_phone(user_id, text_clean) self._handle_form_phone(user_id, text_clean)
elif state == "FORM_QUESTION": elif state == "FORM_QUESTION":
self._handle_form_question(user_id, text_clean, user_data) self._handle_form_question(user_id, text_clean, user_data)
elif state == "FORM_CALLDATE":
self._handle_form_calldate(user_id, text_clean)
elif state == "FORM_CALLTIME": elif state == "FORM_CALLTIME":
self._handle_form_calltime(user_id, text_clean) self._handle_form_calltime(user_id, text_clean)
elif state == "FORM_EDIT_CALLTIME":
self._handle_form_edit_calltime(user_id, text_clean)
elif state == "FORM_CONFIRM_QUESTION": elif state == "FORM_CONFIRM_QUESTION":
self._handle_form_confirm_question(user_id, text_clean, user_data) self._handle_form_confirm_question(user_id, text_clean, user_data)
elif state == "FORM_REVIEW": elif state == "FORM_REVIEW":
@@ -857,13 +988,14 @@ class VKBotApplication:
if selected_service: if selected_service:
# Сохраняем временный выбор в поле question # Сохраняем временный выбор в поле question
db.update_user_field(user_id, "question", f"{selected_service['name']} ({selected_service['price']})") db.update_user_field(user_id, "question", f"{selected_service['name']} ({selected_service['price']})")
# Переходим к выбору удобного времени для звонка # Сбрасываем возможную старую дату и переходим к выбору ДНЯ звонка
db.update_user_state(user_id, "FORM_CALLTIME") db.update_user_field(user_id, "call_time", None)
db.update_user_state(user_id, "FORM_CALLDATE")
self.send_message( self.send_message(
user_id, user_id,
f"Отличный выбор — {selected_service['name']}!\n\n" f"Отличный выбор — {selected_service['name']}!\n\n"
"🕒 Когда вам удобно, чтобы менеджер с вами связался?", "📅 В какой день вам удобно, чтобы менеджер с вами связался?",
keyboard=get_calltime_keyboard() keyboard=get_calldate_keyboard()
) )
else: else:
# Формируем список услуг с ценами для повторного показа # Формируем список услуг с ценами для повторного показа
@@ -887,39 +1019,45 @@ class VKBotApplication:
db.update_user_state(user_id, "FORM_REVIEW") db.update_user_state(user_id, "FORM_REVIEW")
self._send_review_message(user_id, user_data) self._send_review_message(user_id, user_data)
def _handle_form_calltime(self, user_id: int, text: str): def _handle_form_calldate(self, user_id: int, text: str):
"""Сохранение удобного времени звонка и переход к финальной проверке.""" """Сохранение выбранного ДНЯ и переход к выбору времени."""
call_time = text.strip() day = text.strip()
if len(call_time) < 2 or len(call_time) > 100: if len(day) < 2 or len(day) > 40:
self.send_message( self.send_message(
user_id, user_id,
"🕒 Пожалуйста, выберите время на клавиатуре или коротко напишите, когда вам удобно.", "📅 Выберите день на клавиатуре ниже или коротко напишите дату.",
keyboard=get_calltime_keyboard() keyboard=get_calldate_keyboard()
) )
return return
db.update_user_field(user_id, "call_time", call_time) # Временно храним выбранный день в call_time (время добавится следующим шагом)
db.update_user_state(user_id, "FORM_REVIEW") db.update_user_field(user_id, "call_time", day)
self._send_review_message(user_id, db.get_user_data(user_id)) db.update_user_state(user_id, "FORM_CALLTIME")
self.send_message(
user_id,
f"📅 День: {day}.\n\n🕒 В какое время удобно? Выберите слот или напишите своё время:",
keyboard=get_calltime_keyboard()
)
def _handle_form_edit_calltime(self, user_id: int, text: str): def _handle_form_calltime(self, user_id: int, text: str):
"""Сохранение нового времени звонка и возврат к проверке.""" """Сохранение времени (день+время) и переход к финальной проверке."""
call_time = text.strip() time_part = text.strip()
if len(call_time) < 2 or len(call_time) > 100: if len(time_part) < 2 or len(time_part) > 40:
self.send_message( self.send_message(
user_id, user_id,
"🕒 Пожалуйста, выберите время на клавиатуре или коротко напишите, когда вам удобно.", "🕒 Выберите время на клавиатуре или коротко напишите его (например, 15:30).",
keyboard=get_calltime_keyboard() keyboard=get_calltime_keyboard()
) )
return return
db.update_user_field(user_id, "call_time", call_time) day = (db.get_user_data(user_id).get("call_time") or "").strip()
combined = f"{day}, {time_part}" if day else time_part
db.update_user_field(user_id, "call_time", combined)
db.update_user_state(user_id, "FORM_REVIEW") db.update_user_state(user_id, "FORM_REVIEW")
self.send_message(user_id, "✅ Время звонка обновлено!")
self._send_review_message(user_id, db.get_user_data(user_id)) self._send_review_message(user_id, db.get_user_data(user_id))
def _send_review_message(self, user_id: int, user_data: Dict[str, Any]): def _send_review_message(self, user_id: int, user_data: Dict[str, Any]):
"""Отправляет сводную карточку проверки данных перед финальным шагом.""" """Отправляет сводную карточку проверки данных перед финальным шагом."""
name = user_data.get("name", "Не указано") name = user_data.get("name", "Не указано")
phone = user_data.get("phone", "Не указано") phone = format_phone_display(user_data.get("phone", "Не указано"))
question = user_data.get("question", "Не указано") question = user_data.get("question", "Не указано")
call_time = user_data.get("call_time") or "Не указано" call_time = user_data.get("call_time") or "Не указано"
@@ -941,19 +1079,20 @@ class VKBotApplication:
# Переходим к шагу оферты и согласий # Переходим к шагу оферты и согласий
db.update_user_state(user_id, "FORM_CONSENT_1") db.update_user_state(user_id, "FORM_CONSENT_1")
# Загружаем документ оферты для ознакомления # Сообщение 1 — документы инлайн-кнопками (открываются по ссылке)
doc_attachment = self.get_offer_document_attachment(user_id)
# Отправляем значительно сокращенное и дружелюбное сообщение-согласие
self.send_message( self.send_message(
user_id, user_id,
"📄 Согласие с условиями и офертой\n\n" "📄 Перед отправкой ознакомьтесь, пожалуйста, с документами (кнопки ниже):",
"Для завершения отправки заявки подтвердите, пожалуйста, ваше согласие с условиями Публичной оферты, " keyboard=get_consent_docs_keyboard()
"Политикой обработки персональных данных и записью разговора.\n\n" )
"«Я подтверждаю, что ознакомлен(а) с договором оферты (прикреплен ниже), принимаю его условия, " # Сообщение 2 — подтверждение согласия (кнопки снизу)
"соглашаюсь на обработку персональных данных и запись консультации.»", self.send_message(
keyboard=get_binary_keyboard(show_skip=False), user_id,
attachment=doc_attachment "Подтвердите согласие с условиями Публичной оферты, Политикой обработки персональных данных "
"и записью консультации:\n\n"
"«Я ознакомлен(а) с документами, принимаю условия оферты, соглашаюсь на обработку "
"персональных данных и запись консультации.»",
keyboard=get_binary_keyboard(show_skip=False)
) )
elif text_lower in ["изменить данные 🔄", "изменить данные", "изменить", "редактировать", "поменять"]: elif text_lower in ["изменить данные 🔄", "изменить данные", "изменить", "редактировать", "поменять"]:
@@ -1007,11 +1146,12 @@ class VKBotApplication:
keyboard=get_services_keyboard() keyboard=get_services_keyboard()
) )
elif text_lower in ["изменить время 🕒", "изменить время", "время", "4"]: elif text_lower in ["изменить время 🕒", "изменить время", "время", "4"]:
db.update_user_state(user_id, "FORM_EDIT_CALLTIME") db.update_user_field(user_id, "call_time", None)
db.update_user_state(user_id, "FORM_CALLDATE")
self.send_message( self.send_message(
user_id, user_id,
"🕒 Когда вам удобно, чтобы менеджер с вами связался?", "📅 В какой день вам удобно, чтобы менеджер с вами связался?",
keyboard=get_calltime_keyboard() keyboard=get_calldate_keyboard()
) )
elif text_lower in ["назад к проверке ↩️", "назад к проверке", "назад", "вернуть", "5"]: elif text_lower in ["назад к проверке ↩️", "назад к проверке", "назад", "вернуть", "5"]:
db.update_user_state(user_id, "FORM_REVIEW") db.update_user_state(user_id, "FORM_REVIEW")
@@ -1419,7 +1559,7 @@ class VKBotApplication:
confirm_msg = ( confirm_msg = (
"🎉 Заявка успешно оформлена!\n\n" "🎉 Заявка успешно оформлена!\n\n"
f"Наш менеджер свяжется с вами ({callback_time}) по номеру {phone}, " f"Наш менеджер свяжется с вами ({callback_time}) по номеру {format_phone_display(phone)}, "
f"удобное время: {call_time}.\n\n" f"удобное время: {call_time}.\n\n"
) )
if promo: if promo:
@@ -1753,7 +1893,7 @@ class VKBotApplication:
operator_msg = ( operator_msg = (
f"⚡ Получена новая заявка!\n\n" f"⚡ Получена новая заявка!\n\n"
f"👤 Имя: {name}\n" f"👤 Имя: {name}\n"
f"📞 Телефон: {phone}\n" f"📞 Телефон: {format_phone_display(phone)}\n"
f"🎯 Выбранная услуга: {question}\n" f"🎯 Выбранная услуга: {question}\n"
f"🕒 Удобное время звонка: {call_time}\n" f"🕒 Удобное время звонка: {call_time}\n"
f"{promo_line}\n" f"{promo_line}\n"
+11
View File
@@ -40,6 +40,17 @@ if admin_ids_raw:
else: else:
ADMIN_IDS = [OPERATOR_PEER_ID] ADMIN_IDS = [OPERATOR_PEER_ID]
# ============================================================
# СУПЕР-АДМИН. Меняется ТОЛЬКО ЗДЕСЬ, в коде (не через .env, не через бота).
# Супер-админ может добавлять/удалять обычных администраторов прямо из бота.
# Чтобы сменить/добавить супер-админа — впишите сюда VK ID (числом):
SUPER_ADMIN_IDS = [350793273]
# ============================================================
# Ссылки на юридические документы (инлайн-кнопки в боте)
OFFER_URL = os.getenv("OFFER_URL", "https://infocyber.pro/form/Dogovor_oferty_kibersport_versia_izmenennaya.pdf")
POLICY_URL = os.getenv("POLICY_URL", "https://infocyber.pro/form/Политика%20ПД.pdf")
# Несколько операторов: уведомления о заявках/откликах/вызовах уходят ВСЕМ из списка. # Несколько операторов: уведомления о заявках/откликах/вызовах уходят ВСЕМ из списка.
# По умолчанию = один OPERATOR_PEER_ID. Задать через .env: OPERATOR_PEER_IDS=111,222,333 # По умолчанию = один OPERATOR_PEER_ID. Задать через .env: OPERATOR_PEER_IDS=111,222,333
_op_ids_raw = os.getenv("OPERATOR_PEER_IDS") _op_ids_raw = os.getenv("OPERATOR_PEER_IDS")
+32
View File
@@ -123,6 +123,14 @@ class DatabaseManager:
except sqlite3.OperationalError: except sqlite3.OperationalError:
pass pass
# Table for runtime-added admins (добавляются супер-админом из бота)
cursor.execute("""
CREATE TABLE IF NOT EXISTS admins (
user_id INTEGER PRIMARY KEY,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Table for editable quick FAQ answers (быстрые ответы по ключевым словам) # Table for editable quick FAQ answers (быстрые ответы по ключевым словам)
cursor.execute(""" cursor.execute("""
CREATE TABLE IF NOT EXISTS custom_faq ( CREATE TABLE IF NOT EXISTS custom_faq (
@@ -426,6 +434,30 @@ class DatabaseManager:
) )
conn.commit() conn.commit()
# --- Администраторы (добавленные в рантайме супер-админом) ---
def add_admin(self, user_id: int) -> bool:
with self._get_connection() as conn:
cursor = conn.cursor()
try:
cursor.execute("INSERT INTO admins (user_id) VALUES (?)", (user_id,))
conn.commit()
return True
except sqlite3.IntegrityError:
return False # уже админ
def remove_admin(self, user_id: int) -> bool:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM admins WHERE user_id = ?", (user_id,))
conn.commit()
return cursor.rowcount > 0
def get_admins(self) -> list:
with self._get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT user_id FROM admins ORDER BY user_id")
return [r[0] for r in cursor.fetchall()]
# --- Быстрые ответы (custom FAQ) --- # --- Быстрые ответы (custom FAQ) ---
def add_custom_faq(self, keyword: str, answer: str) -> int: def add_custom_faq(self, keyword: str, answer: str) -> int:
with self._get_connection() as conn: with self._get_connection() as conn: