Супер-админ, выбор даты/времени звонка, инлайн-документы, маска телефона
- Супер-админ (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:
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user