Правки админки: инлайн-кнопки, отмена рассылки, FAQ в меню, вакансии в БД
- Отмена в «Заявке»/«Карьере» возвращает меню с кнопкой «Админка» для админов. - Рассылка: инлайн-кнопки «Отправить»/«Отмена» в предпросмотре. - Управление админами — инлайн-кнопки «Добавить»/«Удалить» (без текстовых команд), выбор удаляемого админа кнопкой. - Быстрые ответы из админки показываются в «Частые вопросы» инлайн-кнопками. - Вакансии «Карьеры» перенесены в БД (vacancy_options): направления игроков и должности сотрудников редактируются из админки (раздел «Вакансии»), а в опросе показываются инлайн-кнопками из БД. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,7 @@ def get_admin_keyboard(is_super: bool = False) -> str:
|
|||||||
keyboard.add_button("Кандидаты 👥", color=VkKeyboardColor.SECONDARY)
|
keyboard.add_button("Кандидаты 👥", color=VkKeyboardColor.SECONDARY)
|
||||||
keyboard.add_line()
|
keyboard.add_line()
|
||||||
keyboard.add_button("Управление услугами ⚙️", color=VkKeyboardColor.SECONDARY)
|
keyboard.add_button("Управление услугами ⚙️", color=VkKeyboardColor.SECONDARY)
|
||||||
|
keyboard.add_button("Вакансии 💼", color=VkKeyboardColor.SECONDARY)
|
||||||
keyboard.add_line()
|
keyboard.add_line()
|
||||||
keyboard.add_button("Быстрые ответы 💬", color=VkKeyboardColor.SECONDARY)
|
keyboard.add_button("Быстрые ответы 💬", color=VkKeyboardColor.SECONDARY)
|
||||||
keyboard.add_button("Рассылка 📣", color=VkKeyboardColor.SECONDARY)
|
keyboard.add_button("Рассылка 📣", color=VkKeyboardColor.SECONDARY)
|
||||||
@@ -528,17 +529,18 @@ class VKBotApplication:
|
|||||||
lines += ("".join(f" • vk.com/id{a}\n" for a in cfg) or " —\n")
|
lines += ("".join(f" • vk.com/id{a}\n" for a in cfg) or " —\n")
|
||||||
lines += "\n➕ Добавленные из бота:\n"
|
lines += "\n➕ Добавленные из бота:\n"
|
||||||
lines += ("".join(f" • vk.com/id{a}\n" for a in runtime) or " —\n")
|
lines += ("".join(f" • vk.com/id{a}\n" for a in runtime) or " —\n")
|
||||||
|
kb = VkKeyboard(inline=True)
|
||||||
|
kb.add_button("➕ Добавить админа", color=VkKeyboardColor.POSITIVE)
|
||||||
|
kb.add_line()
|
||||||
|
kb.add_button("🗑 Удалить админа", color=VkKeyboardColor.NEGATIVE)
|
||||||
self.send_message(
|
self.send_message(
|
||||||
user_id,
|
user_id,
|
||||||
"👑 Управление администраторами\n\n"
|
"👑 Управление администраторами\n\n"
|
||||||
f"{lines}\n"
|
f"{lines}\n"
|
||||||
"Команды:\n"
|
"Управляйте кнопками ниже 👇\n\n"
|
||||||
"• Добавить админа: админ доб [VK_ID]\n"
|
|
||||||
"• Удалить админа: админ удал [VK_ID]\n"
|
|
||||||
" (снять можно только добавленных из бота)\n\n"
|
|
||||||
"ℹ️ Сменить/добавить СУПЕР-АДМИНА можно только в коде: откройте config.py, "
|
"ℹ️ Сменить/добавить СУПЕР-АДМИНА можно только в коде: откройте config.py, "
|
||||||
"впишите VK ID в список SUPER_ADMIN_IDS и перезапустите бота.",
|
"впишите VK ID в список SUPER_ADMIN_IDS и перезапустите бота.",
|
||||||
keyboard=get_admin_keyboard(is_super=True)
|
keyboard=kb.get_keyboard()
|
||||||
)
|
)
|
||||||
|
|
||||||
def _handle_admin_add_admin(self, user_id: int, text: str):
|
def _handle_admin_add_admin(self, user_id: int, text: str):
|
||||||
@@ -634,6 +636,10 @@ class VKBotApplication:
|
|||||||
if state.startswith("ADMIN_"):
|
if state.startswith("ADMIN_"):
|
||||||
self._handle_admin_wizard(user_id, text_clean, text_lower, state, attachments)
|
self._handle_admin_wizard(user_id, text_clean, text_lower, state, attachments)
|
||||||
return
|
return
|
||||||
|
# Инлайн-кнопки предпросмотра рассылки
|
||||||
|
if user_id in self._pending_broadcasts and text_lower in ["отправить ✅", "отправить", "отмена ❌", "отмена"]:
|
||||||
|
self._handle_admin_broadcast(user_id, "рассылка отмена" if "отмен" in text_lower else "рассылка подтвердить")
|
||||||
|
return
|
||||||
if text_lower in ["админка", "админка ⚙️", "/admin"]:
|
if text_lower in ["админка", "админка ⚙️", "/admin"]:
|
||||||
self._send_admin_panel(user_id)
|
self._send_admin_panel(user_id)
|
||||||
return
|
return
|
||||||
@@ -692,6 +698,29 @@ class VKBotApplication:
|
|||||||
elif text_lower.startswith("рассылка"):
|
elif text_lower.startswith("рассылка"):
|
||||||
self._handle_admin_broadcast(user_id, text_clean)
|
self._handle_admin_broadcast(user_id, text_clean)
|
||||||
return
|
return
|
||||||
|
elif text_lower in ["вакансии 💼", "вакансии"]:
|
||||||
|
self._send_vacancy_admin(user_id)
|
||||||
|
return
|
||||||
|
elif text_lower in ["➕ вакансия игрок", "вакансия игрок"]:
|
||||||
|
db.update_user_state(user_id, "ADMIN_VAC_ADD_PLAYER")
|
||||||
|
self.send_message(user_id, "➕ Название нового направления для ИГРОКОВ (например, Valorant 🎯):",
|
||||||
|
keyboard=get_cancel_admin_keyboard())
|
||||||
|
return
|
||||||
|
elif text_lower in ["➕ вакансия сотрудник", "вакансия сотрудник"]:
|
||||||
|
db.update_user_state(user_id, "ADMIN_VAC_ADD_WORKER")
|
||||||
|
self.send_message(user_id, "➕ Название новой должности для СОТРУДНИКОВ (например, Аналитик):",
|
||||||
|
keyboard=get_cancel_admin_keyboard())
|
||||||
|
return
|
||||||
|
elif text_lower in ["🗑 удалить вакансию", "удалить вакансию"]:
|
||||||
|
items = [(i, f"🎮 {l}") for i, l in db.get_vacancy_options("player")]
|
||||||
|
items += [(i, f"💼 {l}") for i, l in db.get_vacancy_options("worker")]
|
||||||
|
if not items:
|
||||||
|
self.send_message(user_id, "Список вакансий пуст.")
|
||||||
|
self._send_vacancy_admin(user_id)
|
||||||
|
return
|
||||||
|
db.update_user_state(user_id, "ADMIN_VAC_DEL_PICK")
|
||||||
|
self.send_message(user_id, "🗑 Какую вакансию удалить? Выберите:", keyboard=get_pick_keyboard(items))
|
||||||
|
return
|
||||||
elif text_lower in ["быстрые ответы 💬", "быстрые ответы", "фак"]:
|
elif text_lower in ["быстрые ответы 💬", "быстрые ответы", "фак"]:
|
||||||
self._send_faq_admin(user_id)
|
self._send_faq_admin(user_id)
|
||||||
return
|
return
|
||||||
@@ -710,6 +739,28 @@ class VKBotApplication:
|
|||||||
elif text_lower in ["администраторы 👑", "администраторы", "админы"]:
|
elif text_lower in ["администраторы 👑", "администраторы", "админы"]:
|
||||||
self._send_admins_panel(user_id)
|
self._send_admins_panel(user_id)
|
||||||
return
|
return
|
||||||
|
elif text_lower in ["➕ добавить админа", "добавить админа"]:
|
||||||
|
if not self._is_super_admin(user_id):
|
||||||
|
self.send_message(user_id, "⛔ Только супер-админ.")
|
||||||
|
return
|
||||||
|
db.update_user_state(user_id, "ADMIN_ADMIN_ADD")
|
||||||
|
self.send_message(user_id, "➕ Пришлите VK ID нового администратора (числом).\n"
|
||||||
|
"ID можно узнать на vk.com/<id> или через сервисы определения id.",
|
||||||
|
keyboard=get_cancel_admin_keyboard())
|
||||||
|
return
|
||||||
|
elif text_lower in ["🗑 удалить админа", "удалить админа"]:
|
||||||
|
if not self._is_super_admin(user_id):
|
||||||
|
self.send_message(user_id, "⛔ Только супер-админ.")
|
||||||
|
return
|
||||||
|
runtime = db.get_admins()
|
||||||
|
if not runtime:
|
||||||
|
self.send_message(user_id, "Список добавленных из бота админов пуст.")
|
||||||
|
self._send_admins_panel(user_id)
|
||||||
|
return
|
||||||
|
db.update_user_state(user_id, "ADMIN_ADMIN_DEL_PICK")
|
||||||
|
self.send_message(user_id, "🗑 Кого удалить из админов? Выберите:",
|
||||||
|
keyboard=get_pick_keyboard([(a, f"vk.com/id{a}") for a in runtime]))
|
||||||
|
return
|
||||||
elif text_lower.startswith("админ доб"):
|
elif text_lower.startswith("админ доб"):
|
||||||
self._handle_admin_add_admin(user_id, text_clean)
|
self._handle_admin_add_admin(user_id, text_clean)
|
||||||
return
|
return
|
||||||
@@ -750,7 +801,7 @@ class VKBotApplication:
|
|||||||
self.send_message(
|
self.send_message(
|
||||||
user_id,
|
user_id,
|
||||||
"Заполнение заявки отменено. Мы вернулись в главное меню. Чем я могу помочь?",
|
"Заполнение заявки отменено. Мы вернулись в главное меню. Чем я могу помочь?",
|
||||||
keyboard=get_main_keyboard()
|
keyboard=get_main_keyboard(is_admin=self._is_admin(user_id))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -761,7 +812,7 @@ class VKBotApplication:
|
|||||||
self.send_message(
|
self.send_message(
|
||||||
user_id,
|
user_id,
|
||||||
"Отклик отменён. Вы вернулись в главное меню.",
|
"Отклик отменён. Вы вернулись в главное меню.",
|
||||||
keyboard=get_main_keyboard()
|
keyboard=get_main_keyboard(is_admin=self._is_admin(user_id))
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -830,7 +881,7 @@ class VKBotApplication:
|
|||||||
self.send_message(
|
self.send_message(
|
||||||
user_id,
|
user_id,
|
||||||
"Произошла непредвиденная ошибка в системе. Возвращаю вас в главное меню.",
|
"Произошла непредвиденная ошибка в системе. Возвращаю вас в главное меню.",
|
||||||
keyboard=get_main_keyboard()
|
keyboard=get_main_keyboard(is_admin=self._is_admin(user_id))
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
@@ -873,6 +924,19 @@ class VKBotApplication:
|
|||||||
"❓ Выберите тему, которая вас интересует, на клавиатуре ниже:",
|
"❓ Выберите тему, которая вас интересует, на клавиатуре ниже:",
|
||||||
keyboard=get_faq_keyboard()
|
keyboard=get_faq_keyboard()
|
||||||
)
|
)
|
||||||
|
# Дополнительно — быстрые ответы из админки инлайн-кнопками
|
||||||
|
custom = db.get_custom_faq()
|
||||||
|
if custom:
|
||||||
|
kb = VkKeyboard(inline=True)
|
||||||
|
for n, (_id, kw, _ans) in enumerate(custom[:6]):
|
||||||
|
if n and n % 2 == 0:
|
||||||
|
kb.add_line()
|
||||||
|
kb.add_button(kw[:40], color=VkKeyboardColor.SECONDARY)
|
||||||
|
self.send_message(
|
||||||
|
user_id,
|
||||||
|
"💬 Популярные вопросы — нажмите, чтобы получить ответ:",
|
||||||
|
keyboard=kb.get_keyboard()
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# 3. Возврат в главное меню
|
# 3. Возврат в главное меню
|
||||||
@@ -1707,7 +1771,7 @@ class VKBotApplication:
|
|||||||
)
|
)
|
||||||
confirm_msg += "Спасибо за обращение!\n\nВы вернулись в главное меню бота."
|
confirm_msg += "Спасибо за обращение!\n\nВы вернулись в главное меню бота."
|
||||||
|
|
||||||
self.send_message(user_id, confirm_msg, keyboard=get_main_keyboard())
|
self.send_message(user_id, confirm_msg, keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)))
|
||||||
|
|
||||||
# Журналируем заявку для статистики и истории
|
# Журналируем заявку для статистики и истории
|
||||||
try:
|
try:
|
||||||
@@ -1757,26 +1821,37 @@ class VKBotApplication:
|
|||||||
"""Базовая проверка свободного ответа в отклике (не пусто, разумной длины)."""
|
"""Базовая проверка свободного ответа в отклике (не пусто, разумной длины)."""
|
||||||
return 1 <= len(text.strip()) <= 300
|
return 1 <= len(text.strip()) <= 300
|
||||||
|
|
||||||
|
def _send_vac_options(self, user_id: int, track: str, question: str, include_other: bool = False):
|
||||||
|
"""Шлёт вопрос (с кнопкой отмены) и инлайн-кнопки доступных опций вакансии из БД."""
|
||||||
|
self.send_message(user_id, question, keyboard=get_vacancy_cancel_keyboard())
|
||||||
|
kb = VkKeyboard(inline=True)
|
||||||
|
opts = db.get_vacancy_options(track)
|
||||||
|
n = 0
|
||||||
|
for _id, label in opts[:5]:
|
||||||
|
if n and n % 2 == 0:
|
||||||
|
kb.add_line()
|
||||||
|
kb.add_button(label[:40], color=VkKeyboardColor.SECONDARY)
|
||||||
|
n += 1
|
||||||
|
if include_other:
|
||||||
|
kb.add_line()
|
||||||
|
kb.add_button("Другое ✍️", color=VkKeyboardColor.SECONDARY)
|
||||||
|
self.send_message(user_id, "Выберите вариант 👇", keyboard=kb.get_keyboard())
|
||||||
|
|
||||||
def _handle_vac_choose(self, user_id: int, text: str, text_lower: str):
|
def _handle_vac_choose(self, user_id: int, text: str, text_lower: str):
|
||||||
"""Выбор ветки: игрок в состав или сотрудник."""
|
"""Выбор ветки: игрок в состав или сотрудник."""
|
||||||
if text_lower in ["игроком в состав 🎮", "игроком", "игрок", "в состав", "состав"]:
|
if text_lower in ["игроком в состав 🎮", "игроком", "игрок", "в состав", "состав"]:
|
||||||
db.update_user_state(user_id, "VAC_P_DISCIPLINE")
|
db.update_user_state(user_id, "VAC_P_DISCIPLINE")
|
||||||
self.send_message(
|
self._send_vac_options(user_id, "player",
|
||||||
user_id,
|
"🎮 Набор в состав\n\nШаг 1 из 5: по какой дисциплине вы играете?")
|
||||||
"🎮 Набор в состав\n\nШаг 1 из 5: по какой дисциплине вы играете?",
|
|
||||||
keyboard=get_vacancy_discipline_keyboard()
|
|
||||||
)
|
|
||||||
elif text_lower in ["сотрудником 💼", "сотрудником", "сотрудник", "работником", "в команду"]:
|
elif text_lower in ["сотрудником 💼", "сотрудником", "сотрудник", "работником", "в команду"]:
|
||||||
db.update_user_state(user_id, "VAC_W_POSITION")
|
db.update_user_state(user_id, "VAC_W_POSITION")
|
||||||
self.send_message(
|
self._send_vac_options(user_id, "worker",
|
||||||
user_id,
|
"💼 Набор в команду\n\nШаг 1 из 3: на какую должность вы претендуете?",
|
||||||
"💼 Набор в команду\n\nШаг 1 из 3: на какую должность вы претендуете?",
|
include_other=True)
|
||||||
keyboard=get_vacancy_position_keyboard()
|
|
||||||
)
|
|
||||||
elif text_lower in ["назад в меню ↩️", "назад", "меню", "в меню"]:
|
elif text_lower in ["назад в меню ↩️", "назад", "меню", "в меню"]:
|
||||||
db.clear_vac_data(user_id)
|
db.clear_vac_data(user_id)
|
||||||
db.update_user_state(user_id, "FREE_CHAT")
|
db.update_user_state(user_id, "FREE_CHAT")
|
||||||
self.send_message(user_id, "🏠 Вы вернулись в главное меню.", keyboard=get_main_keyboard())
|
self.send_message(user_id, "🏠 Вы вернулись в главное меню.", keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)))
|
||||||
else:
|
else:
|
||||||
self.send_message(
|
self.send_message(
|
||||||
user_id,
|
user_id,
|
||||||
@@ -1788,21 +1863,25 @@ class VKBotApplication:
|
|||||||
|
|
||||||
def _handle_vac_p_discipline(self, user_id: int, text: str, text_lower: str):
|
def _handle_vac_p_discipline(self, user_id: int, text: str, text_lower: str):
|
||||||
discipline = None
|
discipline = None
|
||||||
syn = {
|
# 1) Сопоставление с опциями из БД (нажата инлайн-кнопка или введено название)
|
||||||
"Counter-Strike 2": ["кс", "cs", "cs2", "csgo", "ксго", "контра", "counter"],
|
for _id, label in db.get_vacancy_options("player"):
|
||||||
"Dota 2": ["дота", "dota", "дотка", "доту"],
|
ll = label.lower()
|
||||||
"World of Tanks": ["танк", "wot", "tanks", "ворлд оф танкс", "вот"],
|
if text.strip() == label or text_lower == ll or ll in text_lower or text_lower in ll:
|
||||||
}
|
discipline = label
|
||||||
for name, words in syn.items():
|
|
||||||
if any(w in text_lower for w in words):
|
|
||||||
discipline = name
|
|
||||||
break
|
break
|
||||||
|
# 2) Фолбэк-синонимы для свободного ввода
|
||||||
if not discipline:
|
if not discipline:
|
||||||
self.send_message(
|
syn = {
|
||||||
user_id,
|
"Counter-Strike 2": ["кс", "cs", "cs2", "csgo", "ксго", "контра", "counter"],
|
||||||
"Пожалуйста, выберите дисциплину на клавиатуре ниже.",
|
"Dota 2": ["дота", "dota", "дотка", "доту"],
|
||||||
keyboard=get_vacancy_discipline_keyboard()
|
"World of Tanks": ["танк", "wot", "tanks", "ворлд оф танкс", "вот"],
|
||||||
)
|
}
|
||||||
|
for name, words in syn.items():
|
||||||
|
if any(w in text_lower for w in words):
|
||||||
|
discipline = name
|
||||||
|
break
|
||||||
|
if not discipline:
|
||||||
|
self._send_vac_options(user_id, "player", "Пожалуйста, выберите дисциплину на кнопках ниже:")
|
||||||
return
|
return
|
||||||
db.set_vac_field(user_id, "discipline", discipline)
|
db.set_vac_field(user_id, "discipline", discipline)
|
||||||
db.update_user_state(user_id, "VAC_P_RANK")
|
db.update_user_state(user_id, "VAC_P_RANK")
|
||||||
@@ -1860,12 +1939,6 @@ class VKBotApplication:
|
|||||||
# ----- Ветка сотрудника -----
|
# ----- Ветка сотрудника -----
|
||||||
|
|
||||||
def _handle_vac_w_position(self, user_id: int, text: str, text_lower: str):
|
def _handle_vac_w_position(self, user_id: int, text: str, text_lower: str):
|
||||||
known = {
|
|
||||||
"smm / контент": "SMM / контент",
|
|
||||||
"монтажёр / дизайнер": "Монтажёр / дизайнер",
|
|
||||||
"менеджер": "Менеджер",
|
|
||||||
"разработчик": "Разработчик",
|
|
||||||
}
|
|
||||||
if text_lower in ["другое ✍️", "другое", "иное"]:
|
if text_lower in ["другое ✍️", "другое", "иное"]:
|
||||||
db.update_user_state(user_id, "VAC_W_POSITION_OTHER")
|
db.update_user_state(user_id, "VAC_W_POSITION_OTHER")
|
||||||
self.send_message(
|
self.send_message(
|
||||||
@@ -1874,15 +1947,17 @@ class VKBotApplication:
|
|||||||
keyboard=get_vacancy_cancel_keyboard()
|
keyboard=get_vacancy_cancel_keyboard()
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
# Известная кнопка или произвольный непустой текст принимаем как должность
|
# Сопоставление с должностями из БД (инлайн-кнопка), иначе — произвольный текст
|
||||||
position = known.get(text_lower)
|
position = None
|
||||||
|
for _id, label in db.get_vacancy_options("worker"):
|
||||||
|
if text.strip() == label or text_lower == label.lower():
|
||||||
|
position = label
|
||||||
|
break
|
||||||
if not position:
|
if not position:
|
||||||
if not self._valid_free_text(text):
|
if not self._valid_free_text(text):
|
||||||
self.send_message(
|
self._send_vac_options(user_id, "worker",
|
||||||
user_id,
|
"Выберите должность на кнопках ниже или нажмите «Другое ✍️»:",
|
||||||
"Выберите должность на клавиатуре или нажмите «Другое ✍️».",
|
include_other=True)
|
||||||
keyboard=get_vacancy_position_keyboard()
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
position = text.strip()
|
position = text.strip()
|
||||||
db.set_vac_field(user_id, "position", position)
|
db.set_vac_field(user_id, "position", position)
|
||||||
@@ -1940,7 +2015,7 @@ class VKBotApplication:
|
|||||||
"✅ Спасибо за отклик!\n\n"
|
"✅ Спасибо за отклик!\n\n"
|
||||||
"Мы изучим вашу анкету и свяжемся, если кандидатура подойдёт. Удачи! 🎮\n\n"
|
"Мы изучим вашу анкету и свяжемся, если кандидатура подойдёт. Удачи! 🎮\n\n"
|
||||||
"Вы вернулись в главное меню.",
|
"Вы вернулись в главное меню.",
|
||||||
keyboard=get_main_keyboard()
|
keyboard=get_main_keyboard(is_admin=self._is_admin(user_id))
|
||||||
)
|
)
|
||||||
# Антиспам: ограничиваем частоту уведомлений об откликах от одного пользователя
|
# Антиспам: ограничиваем частоту уведомлений об откликах от одного пользователя
|
||||||
if self._notify_allowed(user_id):
|
if self._notify_allowed(user_id):
|
||||||
@@ -2070,6 +2145,25 @@ class VKBotApplication:
|
|||||||
# ЗАПУСК И ОБРАБОТКА LONGPOLL
|
# ЗАПУСК И ОБРАБОТКА LONGPOLL
|
||||||
# ------------------------------------------
|
# ------------------------------------------
|
||||||
|
|
||||||
|
def _send_vacancy_admin(self, user_id: int):
|
||||||
|
"""Раздел управления вакансиями (направления игроков / должности сотрудников)."""
|
||||||
|
players = db.get_vacancy_options("player")
|
||||||
|
workers = db.get_vacancy_options("worker")
|
||||||
|
lines = "🎮 Направления (игроки):\n"
|
||||||
|
lines += ("".join(f" • [{i}] {l}\n" for i, l in players) or " —\n")
|
||||||
|
lines += "\n💼 Должности (сотрудники):\n"
|
||||||
|
lines += ("".join(f" • [{i}] {l}\n" for i, l in workers) or " —\n")
|
||||||
|
kb = VkKeyboard(inline=True)
|
||||||
|
kb.add_button("➕ Вакансия игрок", color=VkKeyboardColor.POSITIVE)
|
||||||
|
kb.add_button("➕ Вакансия сотрудник", color=VkKeyboardColor.POSITIVE)
|
||||||
|
kb.add_line()
|
||||||
|
kb.add_button("🗑 Удалить вакансию", color=VkKeyboardColor.NEGATIVE)
|
||||||
|
self.send_message(
|
||||||
|
user_id,
|
||||||
|
"💼 Управление вакансиями\n\n" + lines + "\nУправляйте кнопками ниже 👇",
|
||||||
|
keyboard=kb.get_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
def _handle_admin_wizard(self, user_id: int, text: str, text_lower: str, state: str, attachments: list):
|
def _handle_admin_wizard(self, user_id: int, text: str, text_lower: str, state: str, attachments: list):
|
||||||
"""Мастера админки: добавление/изменение/удаление услуг и быстрых ответов, смена картинок."""
|
"""Мастера админки: добавление/изменение/удаление услуг и быстрых ответов, смена картинок."""
|
||||||
if text_lower in ["отмена ❌", "отмена", "в админку ↩️", "в админку", "назад"]:
|
if text_lower in ["отмена ❌", "отмена", "в админку ↩️", "в админку", "назад"]:
|
||||||
@@ -2223,6 +2317,57 @@ class VKBotApplication:
|
|||||||
back_to_faq(f"✅ Ответ №{fid} удалён.")
|
back_to_faq(f"✅ Ответ №{fid} удалён.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# ---------- Вакансии ----------
|
||||||
|
def back_to_vacancies(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_vacancy_admin(user_id)
|
||||||
|
|
||||||
|
if state == "ADMIN_VAC_ADD_PLAYER":
|
||||||
|
if not self._valid_free_text(text):
|
||||||
|
self.send_message(user_id, "⚠️ Введите название направления.", keyboard=get_cancel_admin_keyboard())
|
||||||
|
return
|
||||||
|
db.add_vacancy_option("player", text.strip())
|
||||||
|
back_to_vacancies("✅ Направление для игроков добавлено.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if state == "ADMIN_VAC_ADD_WORKER":
|
||||||
|
if not self._valid_free_text(text):
|
||||||
|
self.send_message(user_id, "⚠️ Введите название должности.", keyboard=get_cancel_admin_keyboard())
|
||||||
|
return
|
||||||
|
db.add_vacancy_option("worker", text.strip())
|
||||||
|
back_to_vacancies("✅ Должность для сотрудников добавлена.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if state == "ADMIN_VAC_DEL_PICK":
|
||||||
|
vid = pick_id(text)
|
||||||
|
if not vid.isdigit() or not db.delete_vacancy_option(int(vid)):
|
||||||
|
self.send_message(user_id, "⚠️ Выберите вакансию кнопкой ниже.")
|
||||||
|
return
|
||||||
|
back_to_vacancies(f"✅ Вакансия №{vid} удалена.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ---------- Администраторы (супер-админ) ----------
|
||||||
|
if state == "ADMIN_ADMIN_ADD":
|
||||||
|
if not self._is_super_admin(user_id):
|
||||||
|
db.update_user_state(user_id, "FREE_CHAT")
|
||||||
|
self._send_admin_panel(user_id)
|
||||||
|
return
|
||||||
|
self._handle_admin_add_admin(user_id, "админ доб " + pick_id(text))
|
||||||
|
db.update_user_state(user_id, "FREE_CHAT")
|
||||||
|
return
|
||||||
|
|
||||||
|
if state == "ADMIN_ADMIN_DEL_PICK":
|
||||||
|
if not self._is_super_admin(user_id):
|
||||||
|
db.update_user_state(user_id, "FREE_CHAT")
|
||||||
|
self._send_admin_panel(user_id)
|
||||||
|
return
|
||||||
|
self._handle_admin_remove_admin(user_id, "админ удал " + pick_id(text))
|
||||||
|
db.update_user_state(user_id, "FREE_CHAT")
|
||||||
|
return
|
||||||
|
|
||||||
# Неизвестное ADMIN_-состояние — сброс
|
# Неизвестное ADMIN_-состояние — сброс
|
||||||
self._admin_ctx.pop(user_id, None)
|
self._admin_ctx.pop(user_id, None)
|
||||||
db.update_user_state(user_id, "FREE_CHAT")
|
db.update_user_state(user_id, "FREE_CHAT")
|
||||||
@@ -2497,6 +2642,13 @@ class VKBotApplication:
|
|||||||
content = text[len("рассылка"):].strip()
|
content = text[len("рассылка"):].strip()
|
||||||
content_lower = content.lower()
|
content_lower = content.lower()
|
||||||
|
|
||||||
|
# Отмена рассылки
|
||||||
|
if content_lower in ["отмена", "отменить", "отмена ❌"]:
|
||||||
|
self._pending_broadcasts.pop(user_id, None)
|
||||||
|
self.send_message(user_id, "❌ Рассылка отменена.")
|
||||||
|
self._send_admin_panel(user_id)
|
||||||
|
return
|
||||||
|
|
||||||
# Шаг 2: подтверждение и запуск
|
# Шаг 2: подтверждение и запуск
|
||||||
if content_lower in ["подтвердить", "да", "отправить", "!"]:
|
if content_lower in ["подтвердить", "да", "отправить", "!"]:
|
||||||
pending = self._pending_broadcasts.get(user_id)
|
pending = self._pending_broadcasts.get(user_id)
|
||||||
@@ -2544,10 +2696,12 @@ class VKBotApplication:
|
|||||||
"— — —\n"
|
"— — —\n"
|
||||||
f"{content}\n"
|
f"{content}\n"
|
||||||
"— — —\n\n"
|
"— — —\n\n"
|
||||||
"Чтобы отправить — напишите: рассылка подтвердить\n"
|
"Отправить или отменить?"
|
||||||
"Чтобы отменить — просто проигнорируйте."
|
|
||||||
)
|
)
|
||||||
self.send_message(user_id, preview)
|
kb = VkKeyboard(inline=True)
|
||||||
|
kb.add_button("Отправить ✅", color=VkKeyboardColor.POSITIVE, payload={"cmd": "bc_send"})
|
||||||
|
kb.add_button("Отмена ❌", color=VkKeyboardColor.NEGATIVE, payload={"cmd": "bc_cancel"})
|
||||||
|
self.send_message(user_id, preview, keyboard=kb.get_keyboard())
|
||||||
|
|
||||||
def _run_broadcast(self, admin_id: int, text: str, recipients: list):
|
def _run_broadcast(self, admin_id: int, text: str, recipients: list):
|
||||||
"""Фоновая отправка рассылки с троттлингом и подсказкой об отписке."""
|
"""Фоновая отправка рассылки с троттлингом и подсказкой об отписке."""
|
||||||
|
|||||||
+43
@@ -123,6 +123,24 @@ class DatabaseManager:
|
|||||||
except sqlite3.OperationalError:
|
except sqlite3.OperationalError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Table for vacancy options (направления игроков и должности сотрудников)
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS vacancy_options (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
track TEXT NOT NULL, -- 'player' | 'worker'
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
is_active INTEGER DEFAULT 1
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cursor.execute("SELECT COUNT(*) FROM vacancy_options")
|
||||||
|
if cursor.fetchone()[0] == 0:
|
||||||
|
seed = [
|
||||||
|
("player", "Counter-Strike 2 🔫"), ("player", "Dota 2 🎮"), ("player", "World of Tanks 🛡️"),
|
||||||
|
("worker", "SMM / контент"), ("worker", "Монтажёр / дизайнер"),
|
||||||
|
("worker", "Менеджер"), ("worker", "Разработчик"),
|
||||||
|
]
|
||||||
|
cursor.executemany("INSERT INTO vacancy_options (track, label) VALUES (?, ?)", seed)
|
||||||
|
|
||||||
# Table for runtime-added admins (добавляются супер-админом из бота)
|
# Table for runtime-added admins (добавляются супер-админом из бота)
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS admins (
|
CREATE TABLE IF NOT EXISTS admins (
|
||||||
@@ -434,6 +452,31 @@ class DatabaseManager:
|
|||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
|
# --- Вакансии (направления игроков / должности сотрудников) ---
|
||||||
|
def get_vacancy_options(self, track: str) -> list:
|
||||||
|
"""Активные опции для трека ('player'|'worker') как список (id, label)."""
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id, label FROM vacancy_options WHERE track = ? AND is_active = 1 ORDER BY id",
|
||||||
|
(track,)
|
||||||
|
)
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
def add_vacancy_option(self, track: str, label: str) -> int:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("INSERT INTO vacancy_options (track, label) VALUES (?, ?)", (track, label.strip()))
|
||||||
|
conn.commit()
|
||||||
|
return cursor.lastrowid
|
||||||
|
|
||||||
|
def delete_vacancy_option(self, option_id: int) -> bool:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE vacancy_options SET is_active = 0 WHERE id = ?", (option_id,))
|
||||||
|
conn.commit()
|
||||||
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
# --- Администраторы (добавленные в рантайме супер-админом) ---
|
# --- Администраторы (добавленные в рантайме супер-админом) ---
|
||||||
def add_admin(self, user_id: int) -> bool:
|
def add_admin(self, user_id: int) -> bool:
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 75 KiB |
Reference in New Issue
Block a user