122 lines
7.6 KiB
Markdown
122 lines
7.6 KiB
Markdown
# Настраиваемые напоминалки для админов — дизайн
|
||
|
||
Дата: 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 февраля);
|
||
- логика «пропущенного» напоминания при простое (одна отправка + пересчёт в будущее).
|