Спека: настраиваемые напоминалки для админов
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