Напоминалки: гейт часового пояса, конвертация времени, состояние паузы
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,8 @@ except ImportError:
|
|||||||
vk_api = None
|
vk_api = None
|
||||||
|
|
||||||
from database import DatabaseManager
|
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
|
from datetime import datetime
|
||||||
import config
|
import config
|
||||||
import mailer
|
import mailer
|
||||||
@@ -486,17 +487,60 @@ class VKBotApplication:
|
|||||||
name = self._vk_name(uid)
|
name = self._vk_name(uid)
|
||||||
return f"{name} (vk.com/id{uid})" if name else f"vk.com/id{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):
|
def _send_reminders_admin(self, user_id: int):
|
||||||
"""Раздел «Напоминалки»: список активных + инструкции."""
|
"""Раздел «Напоминалки»: список активных + инструкции.
|
||||||
|
Вызывается только после прохождения гейта часового пояса
|
||||||
|
(см. _require_reminder_tz) — тут он гарантированно уже задан."""
|
||||||
rows = db.get_active_reminders()
|
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 = ""
|
listing = ""
|
||||||
for r in rows:
|
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 \
|
who = "всем админам" if r["recipients"] == "all" else \
|
||||||
", ".join(self._admin_display(uid) for uid in r["recipients"])
|
", ".join(self._admin_display(uid) for uid in r["recipients"])
|
||||||
period = REMINDER_PERIOD_LABELS.get(r["recurrence"], r["recurrence"])
|
period = REMINDER_PERIOD_LABELS.get(r["recurrence"], r["recurrence"])
|
||||||
short = (r["text"][:50] + "…") if len(r["text"]) > 50 else r["text"]
|
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")
|
f" «{short}»\n")
|
||||||
if not listing:
|
if not listing:
|
||||||
listing = "Активных напоминаний нет.\n"
|
listing = "Активных напоминаний нет.\n"
|
||||||
@@ -504,7 +548,9 @@ class VKBotApplication:
|
|||||||
user_id,
|
user_id,
|
||||||
"⏰ Напоминалки\n\n"
|
"⏰ Напоминалки\n\n"
|
||||||
f"{listing}\n"
|
f"{listing}\n"
|
||||||
"Управляйте кнопками ниже: ➕ добавить • 🗑 удалить",
|
"ℹ️ Чтобы время в списке отображалось верно у всех, каждому нужно один раз "
|
||||||
|
"зайти сюда и указать свой часовой пояс.\n\n"
|
||||||
|
"Управляйте кнопками ниже: ➕ добавить • 🗑 удалить • ⏸/▶ пауза",
|
||||||
keyboard=get_reminders_admin_keyboard()
|
keyboard=get_reminders_admin_keyboard()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -650,9 +696,12 @@ class VKBotApplication:
|
|||||||
self._send_links_admin(user_id)
|
self._send_links_admin(user_id)
|
||||||
return
|
return
|
||||||
elif text_lower in ["⏰ напоминалки", "напоминалки"]:
|
elif text_lower in ["⏰ напоминалки", "напоминалки"]:
|
||||||
self._send_reminders_admin(user_id)
|
if self._require_reminder_tz(user_id):
|
||||||
|
self._send_reminders_admin(user_id)
|
||||||
return
|
return
|
||||||
elif text_lower == "➕ добавить":
|
elif text_lower == "➕ добавить":
|
||||||
|
if not self._require_reminder_tz(user_id):
|
||||||
|
return
|
||||||
db.update_user_state(user_id, "ADMIN_REMINDER_DATE")
|
db.update_user_state(user_id, "ADMIN_REMINDER_DATE")
|
||||||
self.send_message(
|
self.send_message(
|
||||||
user_id,
|
user_id,
|
||||||
@@ -665,6 +714,8 @@ class VKBotApplication:
|
|||||||
self._handle_reminder_delete(user_id, text_clean)
|
self._handle_reminder_delete(user_id, text_clean)
|
||||||
return
|
return
|
||||||
elif text_lower in ["🗑 удалить напоминание", "удалить напоминание"]:
|
elif text_lower in ["🗑 удалить напоминание", "удалить напоминание"]:
|
||||||
|
if not self._require_reminder_tz(user_id):
|
||||||
|
return
|
||||||
rows = db.get_active_reminders()
|
rows = db.get_active_reminders()
|
||||||
if not rows:
|
if not rows:
|
||||||
self.send_message(user_id, "Активных напоминаний нет.")
|
self.send_message(user_id, "Активных напоминаний нет.")
|
||||||
@@ -675,6 +726,25 @@ class VKBotApplication:
|
|||||||
self.send_message(user_id, "🗑 Какое напоминание удалить? Выберите:",
|
self.send_message(user_id, "🗑 Какое напоминание удалить? Выберите:",
|
||||||
keyboard=get_pick_keyboard(items))
|
keyboard=get_pick_keyboard(items))
|
||||||
return
|
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 ["уведомления: вкл 🔔", "уведомления: выкл 🔕", "уведомления", "уведомления 🔔", "уведомления 🔕"]:
|
elif text_lower in ["уведомления: вкл 🔔", "уведомления: выкл 🔕", "уведомления", "уведомления 🔔", "уведомления 🔕"]:
|
||||||
self._toggle_admin_notifications(user_id)
|
self._toggle_admin_notifications(user_id)
|
||||||
return
|
return
|
||||||
@@ -1709,6 +1779,18 @@ class VKBotApplication:
|
|||||||
return
|
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":
|
if state == "ADMIN_REMINDER_DEL_PICK":
|
||||||
rid = pick_id(text)
|
rid = pick_id(text)
|
||||||
if not rid.isdigit() or not db.delete_reminder(int(rid)):
|
if not rid.isdigit() or not db.delete_reminder(int(rid)):
|
||||||
@@ -1719,17 +1801,38 @@ class VKBotApplication:
|
|||||||
self._send_reminders_admin(user_id)
|
self._send_reminders_admin(user_id)
|
||||||
return
|
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":
|
if state == "ADMIN_REMINDER_DATE":
|
||||||
dt = parse_reminder_datetime(text)
|
dt = parse_reminder_datetime(text)
|
||||||
if not dt:
|
if not dt:
|
||||||
self.send_message(user_id, "⚠️ Формат: ДД.ММ.ГГГГ ЧЧ:ММ (например, 15.03.2027 09:00)",
|
self.send_message(user_id, "⚠️ Формат: ДД.ММ.ГГГГ ЧЧ:ММ (например, 15.03.2027 09:00)",
|
||||||
keyboard=get_cancel_admin_keyboard())
|
keyboard=get_cancel_admin_keyboard())
|
||||||
return
|
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, "⚠️ Дата и время должны быть в будущем.",
|
self.send_message(user_id, "⚠️ Дата и время должны быть в будущем.",
|
||||||
keyboard=get_cancel_admin_keyboard())
|
keyboard=get_cancel_admin_keyboard())
|
||||||
return
|
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")
|
db.update_user_state(user_id, "ADMIN_REMINDER_TEXT")
|
||||||
self.send_message(user_id, "✏️ Введите текст напоминания:", keyboard=get_cancel_admin_keyboard())
|
self.send_message(user_id, "✏️ Введите текст напоминания:", keyboard=get_cancel_admin_keyboard())
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user