diff --git a/bot.py b/bot.py index fb7075a..493b018 100644 --- a/bot.py +++ b/bot.py @@ -24,7 +24,8 @@ except ImportError: vk_api = None from database import DatabaseManager -from reminders import parse_reminder_datetime, compute_next_occurrence +from reminders import parse_reminder_datetime, compute_next_occurrence, \ + server_utc_offset_hours, convert_time from datetime import datetime import config import mailer @@ -486,17 +487,60 @@ class VKBotApplication: name = self._vk_name(uid) return f"{name} (vk.com/id{uid})" if name else f"vk.com/id{uid}" + def _parse_tz_offset(self, raw: str) -> Optional[int]: + """Парсит смещение от UTC вида '+3', '-5', '0'. None, если не число + или вне диапазона реальных часовых поясов (-12..+14).""" + raw = raw.strip() + if not re.match(r"^[+-]?\d{1,2}$", raw): + return None + val = int(raw) + return val if -12 <= val <= 14 else None + + def _require_reminder_tz(self, user_id: int) -> bool: + """Гейт часового пояса: если у админа он ещё не задан — переводит его + в мастер ввода пояса и возвращает False (вызывающий должен просто + return). Если пояс уже задан — возвращает True, можно продолжать.""" + if db.get_admin_tz_offset(user_id) is not None: + return True + db.update_user_state(user_id, "ADMIN_REMINDER_TZ_SETUP") + self.send_message( + user_id, + "🌐 Прежде чем пользоваться напоминалками, укажите свой часовой пояс " + "относительно UTC.\nНапример: +3 (Москва), +5 (Екатеринбург), 0 (UTC).", + keyboard=get_cancel_admin_keyboard() + ) + return False + + def _handle_set_timezone(self, user_id: int, text: str): + """Команда 'часовой пояс <±N>' — сменить уже заданный пояс в любой момент.""" + raw = text[len("часовой пояс"):].strip() + offset = self._parse_tz_offset(raw) + if offset is None: + self.send_message(user_id, "⚠️ Формат: часовой пояс +3 (число от -12 до +14).") + return + db.set_admin_tz_offset(user_id, offset) + self.send_message(user_id, f"✅ Часовой пояс сохранён: UTC{offset:+d}.") + def _send_reminders_admin(self, user_id: int): - """Раздел «Напоминалки»: список активных + инструкции.""" + """Раздел «Напоминалки»: список активных + инструкции. + Вызывается только после прохождения гейта часового пояса + (см. _require_reminder_tz) — тут он гарантированно уже задан.""" rows = db.get_active_reminders() + viewer_offset = db.get_admin_tz_offset(user_id) + if viewer_offset is None: + viewer_offset = 0 + server_offset = server_utc_offset_hours() listing = "" for r in rows: - dt = datetime.strptime(r["next_at"], "%Y-%m-%d %H:%M:%S") + server_dt = datetime.strptime(r["next_at"], "%Y-%m-%d %H:%M:%S") + viewer_dt = convert_time(server_dt, server_offset, viewer_offset) who = "всем админам" if r["recipients"] == "all" else \ ", ".join(self._admin_display(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" + pause_mark = "⏸ (на паузе) " if r["paused"] else "" + listing += (f"🔹 [{r['id']}] {pause_mark}{viewer_dt.strftime('%d.%m.%Y %H:%M')} " + f"(UTC{viewer_offset:+d}, {period}) → {who}\n" f" «{short}»\n") if not listing: listing = "Активных напоминаний нет.\n" @@ -504,7 +548,9 @@ class VKBotApplication: user_id, "⏰ Напоминалки\n\n" f"{listing}\n" - "Управляйте кнопками ниже: ➕ добавить • 🗑 удалить", + "ℹ️ Чтобы время в списке отображалось верно у всех, каждому нужно один раз " + "зайти сюда и указать свой часовой пояс.\n\n" + "Управляйте кнопками ниже: ➕ добавить • 🗑 удалить • ⏸/▶ пауза", keyboard=get_reminders_admin_keyboard() ) @@ -650,9 +696,12 @@ class VKBotApplication: self._send_links_admin(user_id) return elif text_lower in ["⏰ напоминалки", "напоминалки"]: - self._send_reminders_admin(user_id) + if self._require_reminder_tz(user_id): + self._send_reminders_admin(user_id) return elif text_lower == "➕ добавить": + if not self._require_reminder_tz(user_id): + return db.update_user_state(user_id, "ADMIN_REMINDER_DATE") self.send_message( user_id, @@ -665,6 +714,8 @@ class VKBotApplication: self._handle_reminder_delete(user_id, text_clean) return elif text_lower in ["🗑 удалить напоминание", "удалить напоминание"]: + if not self._require_reminder_tz(user_id): + return rows = db.get_active_reminders() if not rows: self.send_message(user_id, "Активных напоминаний нет.") @@ -675,6 +726,25 @@ class VKBotApplication: self.send_message(user_id, "🗑 Какое напоминание удалить? Выберите:", keyboard=get_pick_keyboard(items)) return + elif text_lower in ["⏸/▶ пауза", "пауза"]: + if not self._require_reminder_tz(user_id): + return + rows = db.get_active_reminders() + if not rows: + self.send_message(user_id, "Активных напоминаний нет.") + self._send_reminders_admin(user_id) + return + items = [ + (r["id"], ("▶ Возобновить: " if r["paused"] else "⏸ На паузу: ") + r["text"]) + for r in rows + ] + db.update_user_state(user_id, "ADMIN_REMINDER_PAUSE_PICK") + self.send_message(user_id, "⏸/▶ Какое напоминание переключить? Выберите:", + keyboard=get_pick_keyboard(items)) + return + elif text_lower.startswith("часовой пояс"): + self._handle_set_timezone(user_id, text_clean) + return elif text_lower in ["уведомления: вкл 🔔", "уведомления: выкл 🔕", "уведомления", "уведомления 🔔", "уведомления 🔕"]: self._toggle_admin_notifications(user_id) return @@ -1709,6 +1779,18 @@ class VKBotApplication: return # ---------- Напоминалки ---------- + if state == "ADMIN_REMINDER_TZ_SETUP": + offset = self._parse_tz_offset(text) + if offset is None: + self.send_message(user_id, "⚠️ Формат: число от -12 до +14, например +3 или -5.", + keyboard=get_cancel_admin_keyboard()) + return + db.set_admin_tz_offset(user_id, offset) + db.update_user_state(user_id, "FREE_CHAT") + self.send_message(user_id, f"✅ Часовой пояс сохранён: UTC{offset:+d}.") + self._send_reminders_admin(user_id) + return + if state == "ADMIN_REMINDER_DEL_PICK": rid = pick_id(text) if not rid.isdigit() or not db.delete_reminder(int(rid)): @@ -1719,17 +1801,38 @@ class VKBotApplication: self._send_reminders_admin(user_id) return + if state == "ADMIN_REMINDER_PAUSE_PICK": + rid = pick_id(text) + rows_by_id = {r["id"]: r for r in db.get_active_reminders()} + if not rid.isdigit() or int(rid) not in rows_by_id: + self.send_message(user_id, "⚠️ Выберите напоминание кнопкой ниже.") + return + new_paused = not rows_by_id[int(rid)]["paused"] + db.set_reminder_paused(int(rid), new_paused) + db.update_user_state(user_id, "FREE_CHAT") + self.send_message( + user_id, + f"✅ Напоминание №{rid} " + + ("поставлено на паузу." if new_paused else "снято с паузы.") + ) + self._send_reminders_admin(user_id) + 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(): + admin_offset = db.get_admin_tz_offset(user_id) + if admin_offset is None: + admin_offset = 0 + server_dt = convert_time(dt, admin_offset, server_utc_offset_hours()) + if server_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")} + self._admin_ctx[user_id] = {"remind_at": server_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