Напоминалки: гейт часового пояса, конвертация времени, состояние паузы

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Kenik
2026-09-05 14:44:35 +03:00
parent 5ce69fb66c
commit 3ff411b554
+111 -8
View File
@@ -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