diff --git a/bot.py b/bot.py index cf98129..c591740 100644 --- a/bot.py +++ b/bot.py @@ -24,6 +24,8 @@ except ImportError: vk_api = None from database import DatabaseManager +from reminders import parse_reminder_datetime, compute_next_occurrence +from datetime import datetime import config import mailer @@ -47,6 +49,16 @@ BAN_WINDOW_SEC = 60 BAN_SEC = 600 # Сколько секунд кэшировать список рантайм-админов из БД ADMINS_CACHE_TTL = 30 +# Как часто фоновый поток проверяет, не пора ли отправить напоминание +REMINDER_CHECK_INTERVAL_SEC = 60 + +REMINDER_PERIOD_LABELS = { + "once": "разово", + "weekly": "каждую неделю", + "monthly": "каждый месяц", + "yearly": "каждый год", +} +REMINDER_PERIOD_BY_LABEL = {v: k for k, v in REMINDER_PERIOD_LABELS.items()} from keyboards import * # все get_*_keyboard @@ -460,6 +472,52 @@ class VKBotApplication: "(админов из .env и супер-админов отсюда снять нельзя).") self._send_admins_panel(user_id) + def _all_admin_ids(self) -> list: + """Полный список текущих админов: супер-админы + .env + добавленные в рантайме, + без дублей, отсортирован (используется для «все админы» и нумерации в мастере).""" + ids = set(getattr(config, "SUPER_ADMIN_IDS", [])) + ids |= set(getattr(config, "ADMIN_IDS", [])) + ids |= self._runtime_admins() + return sorted(ids) + + def _send_reminders_admin(self, user_id: int): + """Раздел «Напоминалки»: список активных + инструкции.""" + rows = db.get_active_reminders() + listing = "" + for r in rows: + dt = datetime.strptime(r["next_at"], "%Y-%m-%d %H:%M:%S") + who = "всем админам" if r["recipients"] == "all" else \ + ", ".join(f"id{uid}" for uid in r["recipients"]) + period = REMINDER_PERIOD_LABELS.get(r["recurrence"], r["recurrence"]) + short = (r["text"][:50] + "…") if len(r["text"]) > 50 else r["text"] + listing += (f"🔹 [{r['id']}] {dt.strftime('%d.%m.%Y %H:%M')} ({period}) → {who}\n" + f" «{short}»\n") + if not listing: + listing = "Активных напоминаний нет.\n" + kb = VkKeyboard(inline=True) + kb.add_button("➕ Добавить", color=VkKeyboardColor.PRIMARY) + kb.add_line() + kb.add_button("В админку ↩️", color=VkKeyboardColor.SECONDARY) + self.send_message( + user_id, + "⏰ Напоминалки\n\n" + f"{listing}\n" + "Чтобы удалить: напоминалка удалить \n" + "Чтобы добавить: нажмите «➕ Добавить»", + keyboard=kb.get_keyboard() + ) + + def _handle_reminder_delete(self, user_id: int, text: str): + arg = text[len("напоминалка удалить"):].strip() + if not arg.isdigit(): + self.send_message(user_id, "⚠️ Укажите номер: напоминалка удалить 3") + return + if db.delete_reminder(int(arg)): + self.send_message(user_id, f"✅ Напоминание №{arg} удалено.") + else: + self.send_message(user_id, f"⚠️ Напоминание №{arg} не найдено.") + self._send_reminders_admin(user_id) + def _cleanup_antiflood(self): """Периодически выметает протухшие записи анти-флуда, чтобы словари не росли бесконечно.""" now = time.time() @@ -590,6 +648,21 @@ class VKBotApplication: elif text_lower in ["ссылки 🔗", "ссылки", "ссылка"]: self._send_links_admin(user_id) return + elif text_lower in ["⏰ напоминалки", "напоминалки"]: + self._send_reminders_admin(user_id) + return + elif text_lower == "➕ добавить": + db.update_user_state(user_id, "ADMIN_REMINDER_DATE") + self.send_message( + user_id, + "⏰ Новое напоминание.\nВведите дату и время: ДД.ММ.ГГГГ ЧЧ:ММ\n" + "Например: 15.03.2027 09:00", + keyboard=get_cancel_admin_keyboard() + ) + return + elif text_lower.startswith("напоминалка удалить"): + self._handle_reminder_delete(user_id, text_clean) + return elif text_lower in ["уведомления: вкл 🔔", "уведомления: выкл 🔕", "уведомления", "уведомления 🔔", "уведомления 🔕"]: self._toggle_admin_notifications(user_id) return @@ -1623,6 +1696,84 @@ class VKBotApplication: db.update_user_state(user_id, "FREE_CHAT") return + # ---------- Напоминалки ---------- + if state == "ADMIN_REMINDER_DATE": + dt = parse_reminder_datetime(text) + if not dt: + self.send_message(user_id, "⚠️ Формат: ДД.ММ.ГГГГ ЧЧ:ММ (например, 15.03.2027 09:00)", + keyboard=get_cancel_admin_keyboard()) + return + if dt <= datetime.now(): + self.send_message(user_id, "⚠️ Дата и время должны быть в будущем.", + keyboard=get_cancel_admin_keyboard()) + return + self._admin_ctx[user_id] = {"remind_at": dt.strftime("%Y-%m-%d %H:%M:%S")} + db.update_user_state(user_id, "ADMIN_REMINDER_TEXT") + self.send_message(user_id, "✏️ Введите текст напоминания:", keyboard=get_cancel_admin_keyboard()) + return + + if state == "ADMIN_REMINDER_TEXT": + if not text.strip(): + self.send_message(user_id, "⚠️ Текст не может быть пустым.", keyboard=get_cancel_admin_keyboard()) + return + self._admin_ctx.setdefault(user_id, {})["text"] = text.strip() + admins = self._all_admin_ids() + self._admin_ctx[user_id]["admins_list"] = admins + listing = "\n".join(f"{i + 1}. vk.com/id{a}" for i, a in enumerate(admins)) + db.update_user_state(user_id, "ADMIN_REMINDER_RECIPIENTS") + self.send_message( + user_id, + "👥 Кому отправлять напоминание?\n" + f"{listing}\n\n" + "Введите номер (например, 2), несколько через запятую (1,3) или 0 — все админы.", + keyboard=get_cancel_admin_keyboard() + ) + return + + if state == "ADMIN_REMINDER_RECIPIENTS": + ctx = self._admin_ctx.get(user_id, {}) + admins = ctx.get("admins_list", []) + raw = text.strip() + if raw == "0": + recipients = "all" + else: + picks = [p.strip() for p in raw.split(",") if p.strip()] + if not picks or not all(p.isdigit() for p in picks): + self.send_message(user_id, "⚠️ Введите номера через запятую или 0 для всех админов.", + keyboard=get_cancel_admin_keyboard()) + return + indices = [int(p) for p in picks] + if any(i < 1 or i > len(admins) for i in indices): + self.send_message(user_id, "⚠️ Есть номер вне списка. Проверьте и введите снова.", + keyboard=get_cancel_admin_keyboard()) + return + recipients = sorted({admins[i - 1] for i in indices}) + ctx["recipients"] = recipients + db.update_user_state(user_id, "ADMIN_REMINDER_PERIOD") + self.send_message(user_id, "🔁 Периодичность напоминания:", keyboard=get_reminder_period_keyboard()) + return + + if state == "ADMIN_REMINDER_PERIOD": + recurrence = REMINDER_PERIOD_BY_LABEL.get(text_lower) + if not recurrence: + self.send_message(user_id, "⚠️ Выберите период кнопкой ниже.", + keyboard=get_reminder_period_keyboard()) + return + ctx = self._admin_ctx.pop(user_id, {}) + remind_at = ctx.get("remind_at") + rtext = ctx.get("text") + recipients = ctx.get("recipients") + if not (remind_at and rtext and recipients is not None): + db.update_user_state(user_id, "FREE_CHAT") + self.send_message(user_id, "⚠️ Что-то пошло не так, начните заново кнопкой «➕ Добавить».") + self._send_reminders_admin(user_id) + return + db.add_reminder(rtext, remind_at, recurrence, recipients, user_id) + db.update_user_state(user_id, "FREE_CHAT") + self.send_message(user_id, "✅ Напоминание создано.") + self._send_reminders_admin(user_id) + return + # Неизвестное ADMIN_-состояние — сброс self._admin_ctx.pop(user_id, None) db.update_user_state(user_id, "FREE_CHAT") @@ -1829,7 +1980,6 @@ class VKBotApplication: logger.info("Бот успешно запущен в режиме прослушивания LongPoll...") # Старт-пинг администратору — лёгкий сигнal «бот жив» try: - from datetime import datetime ts = datetime.now().strftime("%d.%m.%Y %H:%M") for admin_id in getattr(config, "ADMIN_IDS", []): self.send_message(admin_id, f"✅ Бот Кибершколы БАЗА запущен и на связи ({ts}).")