diff --git a/bot.py b/bot.py index 5c17905..61c64a4 100644 --- a/bot.py +++ b/bot.py @@ -112,6 +112,7 @@ def get_admin_keyboard(is_super: bool = False) -> str: keyboard.add_button("Кандидаты 👥", color=VkKeyboardColor.SECONDARY) keyboard.add_line() keyboard.add_button("Управление услугами ⚙️", color=VkKeyboardColor.SECONDARY) + keyboard.add_button("Вакансии 💼", color=VkKeyboardColor.SECONDARY) keyboard.add_line() 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 += "\n➕ Добавленные из бота:\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( user_id, "👑 Управление администраторами\n\n" f"{lines}\n" - "Команды:\n" - "• Добавить админа: админ доб [VK_ID]\n" - "• Удалить админа: админ удал [VK_ID]\n" - " (снять можно только добавленных из бота)\n\n" + "Управляйте кнопками ниже 👇\n\n" "ℹ️ Сменить/добавить СУПЕР-АДМИНА можно только в коде: откройте config.py, " "впишите 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): @@ -634,6 +636,10 @@ class VKBotApplication: if state.startswith("ADMIN_"): self._handle_admin_wizard(user_id, text_clean, text_lower, state, attachments) 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"]: self._send_admin_panel(user_id) return @@ -692,6 +698,29 @@ class VKBotApplication: elif text_lower.startswith("рассылка"): self._handle_admin_broadcast(user_id, text_clean) 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 ["быстрые ответы 💬", "быстрые ответы", "фак"]: self._send_faq_admin(user_id) return @@ -710,6 +739,28 @@ class VKBotApplication: elif text_lower in ["администраторы 👑", "администраторы", "админы"]: self._send_admins_panel(user_id) 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.", + 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("админ доб"): self._handle_admin_add_admin(user_id, text_clean) return @@ -750,7 +801,7 @@ class VKBotApplication: self.send_message( user_id, "Заполнение заявки отменено. Мы вернулись в главное меню. Чем я могу помочь?", - keyboard=get_main_keyboard() + keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)) ) return @@ -761,7 +812,7 @@ class VKBotApplication: self.send_message( user_id, "Отклик отменён. Вы вернулись в главное меню.", - keyboard=get_main_keyboard() + keyboard=get_main_keyboard(is_admin=self._is_admin(user_id)) ) return @@ -830,7 +881,7 @@ class VKBotApplication: self.send_message( 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() ) + # Дополнительно — быстрые ответы из админки инлайн-кнопками + 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 # 3. Возврат в главное меню @@ -1707,7 +1771,7 @@ class VKBotApplication: ) 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: @@ -1757,26 +1821,37 @@ class VKBotApplication: """Базовая проверка свободного ответа в отклике (не пусто, разумной длины).""" 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): """Выбор ветки: игрок в состав или сотрудник.""" if text_lower in ["игроком в состав 🎮", "игроком", "игрок", "в состав", "состав"]: db.update_user_state(user_id, "VAC_P_DISCIPLINE") - self.send_message( - user_id, - "🎮 Набор в состав\n\nШаг 1 из 5: по какой дисциплине вы играете?", - keyboard=get_vacancy_discipline_keyboard() - ) + self._send_vac_options(user_id, "player", + "🎮 Набор в состав\n\nШаг 1 из 5: по какой дисциплине вы играете?") elif text_lower in ["сотрудником 💼", "сотрудником", "сотрудник", "работником", "в команду"]: db.update_user_state(user_id, "VAC_W_POSITION") - self.send_message( - user_id, - "💼 Набор в команду\n\nШаг 1 из 3: на какую должность вы претендуете?", - keyboard=get_vacancy_position_keyboard() - ) + self._send_vac_options(user_id, "worker", + "💼 Набор в команду\n\nШаг 1 из 3: на какую должность вы претендуете?", + include_other=True) elif text_lower in ["назад в меню ↩️", "назад", "меню", "в меню"]: db.clear_vac_data(user_id) 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: self.send_message( user_id, @@ -1788,21 +1863,25 @@ class VKBotApplication: def _handle_vac_p_discipline(self, user_id: int, text: str, text_lower: str): discipline = None - syn = { - "Counter-Strike 2": ["кс", "cs", "cs2", "csgo", "ксго", "контра", "counter"], - "Dota 2": ["дота", "dota", "дотка", "доту"], - "World of Tanks": ["танк", "wot", "tanks", "ворлд оф танкс", "вот"], - } - for name, words in syn.items(): - if any(w in text_lower for w in words): - discipline = name + # 1) Сопоставление с опциями из БД (нажата инлайн-кнопка или введено название) + for _id, label in db.get_vacancy_options("player"): + ll = label.lower() + if text.strip() == label or text_lower == ll or ll in text_lower or text_lower in ll: + discipline = label break + # 2) Фолбэк-синонимы для свободного ввода if not discipline: - self.send_message( - user_id, - "Пожалуйста, выберите дисциплину на клавиатуре ниже.", - keyboard=get_vacancy_discipline_keyboard() - ) + syn = { + "Counter-Strike 2": ["кс", "cs", "cs2", "csgo", "ксго", "контра", "counter"], + "Dota 2": ["дота", "dota", "дотка", "доту"], + "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 db.set_vac_field(user_id, "discipline", discipline) 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): - known = { - "smm / контент": "SMM / контент", - "монтажёр / дизайнер": "Монтажёр / дизайнер", - "менеджер": "Менеджер", - "разработчик": "Разработчик", - } if text_lower in ["другое ✍️", "другое", "иное"]: db.update_user_state(user_id, "VAC_W_POSITION_OTHER") self.send_message( @@ -1874,15 +1947,17 @@ class VKBotApplication: keyboard=get_vacancy_cancel_keyboard() ) 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 self._valid_free_text(text): - self.send_message( - user_id, - "Выберите должность на клавиатуре или нажмите «Другое ✍️».", - keyboard=get_vacancy_position_keyboard() - ) + self._send_vac_options(user_id, "worker", + "Выберите должность на кнопках ниже или нажмите «Другое ✍️»:", + include_other=True) return position = text.strip() db.set_vac_field(user_id, "position", position) @@ -1940,7 +2015,7 @@ class VKBotApplication: "✅ Спасибо за отклик!\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): @@ -2070,6 +2145,25 @@ class VKBotApplication: # ЗАПУСК И ОБРАБОТКА 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): """Мастера админки: добавление/изменение/удаление услуг и быстрых ответов, смена картинок.""" if text_lower in ["отмена ❌", "отмена", "в админку ↩️", "в админку", "назад"]: @@ -2223,6 +2317,57 @@ class VKBotApplication: back_to_faq(f"✅ Ответ №{fid} удалён.") 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_-состояние — сброс self._admin_ctx.pop(user_id, None) db.update_user_state(user_id, "FREE_CHAT") @@ -2497,6 +2642,13 @@ class VKBotApplication: content = text[len("рассылка"):].strip() 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: подтверждение и запуск if content_lower in ["подтвердить", "да", "отправить", "!"]: pending = self._pending_broadcasts.get(user_id) @@ -2544,10 +2696,12 @@ class VKBotApplication: "— — —\n" f"{content}\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): """Фоновая отправка рассылки с троттлингом и подсказкой об отписке.""" diff --git a/database.py b/database.py index fb43186..c7d2fb8 100644 --- a/database.py +++ b/database.py @@ -123,6 +123,24 @@ class DatabaseManager: except sqlite3.OperationalError: 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 (добавляются супер-админом из бота) cursor.execute(""" CREATE TABLE IF NOT EXISTS admins ( @@ -434,6 +452,31 @@ class DatabaseManager: ) 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: with self._get_connection() as conn: diff --git a/help_img/cs2.jpg b/help_img/cs2.jpg deleted file mode 100644 index b743883..0000000 Binary files a/help_img/cs2.jpg and /dev/null differ diff --git a/help_img/dota2.jpg b/help_img/dota2.jpg deleted file mode 100644 index ddcde54..0000000 Binary files a/help_img/dota2.jpg and /dev/null differ diff --git a/help_img/service_1.jpg b/help_img/service_1.jpg new file mode 100644 index 0000000..091f194 Binary files /dev/null and b/help_img/service_1.jpg differ diff --git a/help_img/service_2.jpg b/help_img/service_2.jpg new file mode 100644 index 0000000..6f5e1c4 Binary files /dev/null and b/help_img/service_2.jpg differ diff --git a/help_img/service_3.jpg b/help_img/service_3.jpg new file mode 100644 index 0000000..f8a2c7e Binary files /dev/null and b/help_img/service_3.jpg differ diff --git a/help_img/wot.jpg b/help_img/wot.jpg deleted file mode 100644 index c69d788..0000000 Binary files a/help_img/wot.jpg and /dev/null differ