Files
bot_vk_ikp_prodagi/docs/superpowers/specs/2026-09-02-admin-reminders-design.md
T

7.6 KiB
Raw Blame History

Настраиваемые напоминалки для админов — дизайн

Дата: 2026-09-02

Проблема

Админам нужно, чтобы бот сам напоминал о разовых или периодических событиях (например, об окончании аренды домена/сервера): дата и время, текст, кому из админов отправить, периодичность (разово / каждую неделю / каждый месяц / каждый год).

Права доступа

Создавать, просматривать и удалять напоминания может любой админ (из ADMIN_IDS или добавленный в рантайме, см. _is_admin в bot.py). Список напоминаний — общий для всех админов (как общая доска задач), отдельного разграничения «моё / чужое» нет.

Хранение данных

Новая таблица SQLite reminders (создаётся в Database._init_db() в database.py, по образцу существующих admins/custom_faq):

CREATE TABLE IF NOT EXISTS reminders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    text TEXT NOT NULL,
    remind_at TEXT NOT NULL,       -- "YYYY-MM-DD HH:MM:SS", исходное (якорное) время
    next_at TEXT NOT NULL,         -- "YYYY-MM-DD HH:MM:SS", следующее срабатывание
    recurrence TEXT NOT NULL,      -- 'once' | 'weekly' | 'monthly' | 'yearly'
    recipients TEXT NOT NULL,      -- JSON: список vk_id, либо строка "all"
    created_by INTEGER NOT NULL,
    created_at TEXT NOT NULL,
    active INTEGER NOT NULL DEFAULT 1
);

remind_at — неизменное якорное время первого срабатывания (день месяца/год для пересчёта периодов), next_at — то, что реально проверяет планировщик и что двигается вперёд после каждой отправки.

CRUD-методы в Database (по аналогии с add_admin/remove_admin/get_admins): add_reminder(...), get_active_reminders(), get_due_reminders(now_str), update_reminder_next_at(id, next_at), deactivate_reminder(id), delete_reminder(id).

Планировщик

Отдельный фоновый поток threading.Thread(target=self._reminders_loop, daemon=True), запускается в run() рядом с уже существующими фоновыми потоками (прогрев медиа, рассылка). Цикл:

while True:
    now = datetime.now()  # локальное серверное время, как везде в проекте (без TZ-логики)
    for r in db.get_due_reminders(now):
        отправить текст всем recipients (self.send_message)
        if r.recurrence == 'once':
            db.deactivate_reminder(r.id)
        else:
            next_at = compute_next(r.remind_at, r.recurrence, after=now)
            db.update_reminder_next_at(r.id, next_at)
    time.sleep(60)

compute_next считает следующее срабатывание строго после now, отталкиваясь от якорной даты remind_at (а не от предыдущего next_at) — так периодичность не дрейфует из-за клампинга коротких месяцев:

  • weekly: remind_at + 7*N дней, ближайшее значение > now;
  • monthly: тот же день месяца, что и в remind_at; если в целевом месяце такого дня нет — последний день этого месяца (31.01 → 28/29.02 → 31.03, а не 28.03);
  • yearly: тот же день/месяц; 29 февраля в невисокосный год → 28 февраля.

Пропуск при простое бота: если бот был выключен и время срабатывания уже прошло — при первом же тике после старта напоминание досылается один раз, после чего сразу пересчитывается следующее будущее срабатывание (не досылается «пачка» пропущенных периодов).

UI — мастер в админ-панели

Новая кнопка « Напоминалки» в get_admin_keyboard (keyboards.py).

Список (_send_reminders_admin): для каждого активного напоминания — id, дата следующего срабатывания, периодичность, текст (обрезанный превью), получатели. Плюс инструкция: «напоминалка добавить» / «напоминалка удалить ».

Добавление — пошаговый мастер состояний (по образцу ADMIN_FAQ_ADD в bot.py):

  1. ADMIN_REMINDER_DATE — ввод ДД.ММ.ГГГГ ЧЧ:ММ, валидация формата и что дата не в прошлом;
  2. ADMIN_REMINDER_TEXT — текст напоминания;
  3. ADMIN_REMINDER_RECIPIENTS — нумерованный список текущих админов + пункт «0 — все админы»; можно ввести несколько номеров через запятую (например «1,3»);
  4. ADMIN_REMINDER_PERIOD — кнопки «Разово / Каждую неделю / Каждый месяц / Каждый год»;
  5. подтверждение и запись в БД (add_reminder), возврат к списку напоминаний.

Отмена мастера на любом шаге — существующим паттерном («отмена»/кнопка отмены → get_cancel_admin_keyboard).

Удаление — по id, текстовой командой «напоминалка удалить », как удаление админа (db.delete_reminder).

Сознательное упрощение (YAGNI): редактирования существующего напоминания нет — только добавить/удалить, как сейчас устроено управление списком админов. Чтобы поправить напоминание — удалить и создать заново.

Изменяемые файлы

  • database.py — таблица reminders + CRUD-методы.
  • bot.py — мастер ADMIN_REMINDER_*, обработчики команд, фоновый поток-планировщик, запуск потока в run().
  • keyboards.py — кнопка « Напоминалки» в админ-клавиатуре, клавиатура выбора периодичности.

Тестирование

Юнит-тесты в tests/ (по образцу tests/test_database.py):

  • CRUD напоминаний в БД;
  • расчёт compute_next для всех 3 периодичностей, включая крайние случаи (31 число, 29 февраля);
  • логика «пропущенного» напоминания при простое (одна отправка + пересчёт в будущее).