DispelEvilBot/common/repositories/schedules_repo.py

205 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Репозиторий расписаний рассылок (Блок 4.5).
Редактирует админка, исполняет бот: он читает активные строки раз в минуту.
Каждое изменение пишет снимок строки в ``schedules_history`` (аудит).
"""
from __future__ import annotations
from typing import Any
from psycopg.types.json import Jsonb
from common.db.pool import get_pool
#: Поля, которые попадают в историю и форму (без служебных отметок времени).
SNAPSHOT_FIELDS = ("template_key", "hour", "minute", "weekdays", "timezone", "enabled", "description")
def _snapshot(row: dict[str, Any]) -> dict[str, Any]:
return {field: row.get(field) for field in SNAPSHOT_FIELDS}
async def list_schedules() -> list[dict[str, Any]]:
"""Все расписания с описанием шаблона (для таблицы в админке)."""
async with get_pool().connection() as conn:
cur = await conn.execute(
"""
SELECT s.id, s.template_key, t.description AS template_description,
s.hour, s.minute, s.weekdays, s.timezone, s.enabled,
s.description, s.updated_at, s.updated_by
FROM bot.schedules AS s
JOIN bot.message_templates AS t ON t.key = s.template_key
ORDER BY s.hour, s.minute, s.id
"""
)
return await cur.fetchall()
async def list_active() -> list[dict[str, Any]]:
"""Активные расписания — для планировщика бота."""
async with get_pool().connection() as conn:
cur = await conn.execute(
"""
SELECT id, template_key, hour, minute, weekdays, timezone
FROM bot.schedules
WHERE enabled
ORDER BY hour, minute, id
"""
)
return await cur.fetchall()
async def claim_run(schedule_id: int, run_at: Any) -> bool:
"""Атомарно отметить минуту как отработанную.
``True`` — эту минуту ещё никто не отправлял; ``False`` — уже была отправка
(например, до перезапуска бота).
"""
async with get_pool().connection() as conn:
cur = await conn.execute(
"""
INSERT INTO bot.schedule_runs (schedule_id, run_at)
VALUES (%s, %s)
ON CONFLICT DO NOTHING
""",
(schedule_id, run_at),
)
claimed = bool(cur.rowcount)
if claimed:
await conn.execute("DELETE FROM bot.schedule_runs WHERE run_at < now() - interval '7 days'")
return claimed
async def get_schedule(schedule_id: int) -> dict[str, Any] | None:
async with get_pool().connection() as conn:
cur = await conn.execute(
"""
SELECT id, template_key, hour, minute, weekdays, timezone, enabled,
description, updated_at, updated_by
FROM bot.schedules
WHERE id = %s
""",
(schedule_id,),
)
return await cur.fetchone()
async def create_schedule(data: dict[str, Any], *, editor_id: int | None = None) -> int:
"""Создать расписание и записать его в историю. Возвращает id."""
async with get_pool().connection() as conn:
cur = await conn.execute(
"""
INSERT INTO bot.schedules
(template_key, hour, minute, weekdays, timezone, enabled, description, updated_by)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""",
(
data["template_key"],
data["hour"],
data["minute"],
data["weekdays"],
data["timezone"],
data["enabled"],
data["description"],
editor_id,
),
)
row = await cur.fetchone()
schedule_id = int(row["id"])
await conn.execute(
"""
INSERT INTO bot.schedules_history (schedule_id, old_data, new_data, editor_id)
VALUES (%s, NULL, %s, %s)
""",
(schedule_id, Jsonb(data), editor_id),
)
return schedule_id
async def update_schedule(schedule_id: int, data: dict[str, Any], *, editor_id: int | None = None) -> bool:
"""Обновить расписание и записать старое/новое значение. False — если записи нет."""
async with get_pool().connection() as conn:
cur = await conn.execute(
"SELECT * FROM bot.schedules WHERE id = %s FOR UPDATE",
(schedule_id,),
)
row = await cur.fetchone()
if row is None:
return False
old = _snapshot(row)
await conn.execute(
"""
UPDATE bot.schedules
SET template_key = %s, hour = %s, minute = %s, weekdays = %s,
timezone = %s, enabled = %s, description = %s,
updated_at = now(), updated_by = %s
WHERE id = %s
""",
(
data["template_key"],
data["hour"],
data["minute"],
data["weekdays"],
data["timezone"],
data["enabled"],
data["description"],
editor_id,
schedule_id,
),
)
await conn.execute(
"""
INSERT INTO bot.schedules_history (schedule_id, old_data, new_data, editor_id)
VALUES (%s, %s, %s, %s)
""",
(schedule_id, Jsonb(old), Jsonb(data), editor_id),
)
return True
async def set_enabled(schedule_id: int, enabled: bool, *, editor_id: int | None = None) -> bool:
"""Включить/выключить расписание (тоже с записью в историю)."""
schedule = await get_schedule(schedule_id)
if schedule is None:
return False
data = _snapshot(schedule)
data["enabled"] = enabled
return await update_schedule(schedule_id, data, editor_id=editor_id)
async def delete_schedule(schedule_id: int, *, editor_id: int | None = None) -> bool:
"""Удалить расписание, сохранив последний снимок в истории."""
async with get_pool().connection() as conn:
cur = await conn.execute("SELECT * FROM bot.schedules WHERE id = %s FOR UPDATE", (schedule_id,))
row = await cur.fetchone()
if row is None:
return False
await conn.execute(
"""
INSERT INTO bot.schedules_history (schedule_id, old_data, new_data, editor_id)
VALUES (%s, %s, NULL, %s)
""",
(schedule_id, Jsonb(_snapshot(row)), editor_id),
)
await conn.execute("DELETE FROM bot.schedules WHERE id = %s", (schedule_id,))
return True
async def schedule_history(schedule_id: int, *, limit: int = 10) -> list[dict[str, Any]]:
async with get_pool().connection() as conn:
cur = await conn.execute(
"""
SELECT id, old_data, new_data, editor_id, changed_at
FROM bot.schedules_history
WHERE schedule_id = %s
ORDER BY changed_at DESC
LIMIT %s
""",
(schedule_id, limit),
)
return await cur.fetchall()