Спека: настраиваемые напоминалки для админов
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# Настраиваемые напоминалки для админов — дизайн
|
||||
|
||||
Дата: 2026-09-02
|
||||
|
||||
## Проблема
|
||||
|
||||
Админам нужно, чтобы бот сам напоминал о разовых или периодических событиях (например,
|
||||
об окончании аренды домена/сервера): дата и время, текст, кому из админов отправить,
|
||||
периодичность (разово / каждую неделю / каждый месяц / каждый год).
|
||||
|
||||
## Права доступа
|
||||
|
||||
Создавать, просматривать и удалять напоминания может **любой админ** (из `ADMIN_IDS`
|
||||
или добавленный в рантайме, см. `_is_admin` в [bot.py](../../../bot.py)). Список
|
||||
напоминаний — общий для всех админов (как общая доска задач), отдельного разграничения
|
||||
«моё / чужое» нет.
|
||||
|
||||
## Хранение данных
|
||||
|
||||
Новая таблица SQLite `reminders` (создаётся в `Database._init_db()` в
|
||||
[database.py](../../../database.py), по образцу существующих `admins`/`custom_faq`):
|
||||
|
||||
```sql
|
||||
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](../../../keyboards.py)).
|
||||
|
||||
**Список** (`_send_reminders_admin`): для каждого активного напоминания — id, дата
|
||||
следующего срабатывания, периодичность, текст (обрезанный превью), получатели.
|
||||
Плюс инструкция: «напоминалка добавить» / «напоминалка удалить <id>».
|
||||
|
||||
**Добавление** — пошаговый мастер состояний (по образцу `ADMIN_FAQ_ADD` в
|
||||
[bot.py](../../../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, текстовой командой «напоминалка удалить <id>», как удаление
|
||||
админа (`db.delete_reminder`).
|
||||
|
||||
**Сознательное упрощение (YAGNI)**: редактирования существующего напоминания нет —
|
||||
только добавить/удалить, как сейчас устроено управление списком админов. Чтобы
|
||||
поправить напоминание — удалить и создать заново.
|
||||
|
||||
## Изменяемые файлы
|
||||
|
||||
- [database.py](../../../database.py) — таблица `reminders` + CRUD-методы.
|
||||
- [bot.py](../../../bot.py) — мастер `ADMIN_REMINDER_*`, обработчики команд, фоновый
|
||||
поток-планировщик, запуск потока в `run()`.
|
||||
- [keyboards.py](../../../keyboards.py) — кнопка «⏰ Напоминалки» в админ-клавиатуре,
|
||||
клавиатура выбора периодичности.
|
||||
|
||||
## Тестирование
|
||||
|
||||
Юнит-тесты в `tests/` (по образцу `tests/test_database.py`):
|
||||
- CRUD напоминаний в БД;
|
||||
- расчёт `compute_next` для всех 3 периодичностей, включая крайние случаи (31 число,
|
||||
29 февраля);
|
||||
- логика «пропущенного» напоминания при простое (одна отправка + пересчёт в будущее).
|
||||
Reference in New Issue
Block a user