"""Репозиторий шаблонов сообщений (Блок 4.1: редактирование в админке). Каждое изменение текста пишет запись в ``message_template_history`` — это аудит и материал для отката. Бот подключится к этим данным в Блоке 4.5. """ from __future__ import annotations from typing import Any from common.db.pool import get_pool async def load_texts() -> dict[str, str]: """Актуальные тексты включённых шаблонов (для кеша бота в ``common.texts.store``).""" async with get_pool().connection() as conn: cur = await conn.execute("SELECT key, text FROM bot.message_templates WHERE enabled ORDER BY key") rows = await cur.fetchall() return {row["key"]: row["text"] for row in rows} async def list_templates() -> list[dict[str, Any]]: """Вернуть список шаблонов без текста (для таблицы в админке).""" async with get_pool().connection() as conn: cur = await conn.execute( """ SELECT key, description, enabled, updated_at, updated_by, length(text) AS text_length FROM bot.message_templates ORDER BY key """ ) return await cur.fetchall() async def get_template(key: str) -> dict[str, Any] | None: """Вернуть шаблон целиком (с текстом) или ``None``.""" async with get_pool().connection() as conn: cur = await conn.execute( """ SELECT key, text, description, enabled, updated_at, updated_by FROM bot.message_templates WHERE key = %s """, (key,), ) return await cur.fetchone() async def create_template( key: str, text: str, description: str, *, enabled: bool = True, editor_id: int | None = None, ) -> bool: """Создать новый шаблон. False — если ключ уже занят.""" async with get_pool().connection() as conn: cur = await conn.execute("SELECT 1 FROM bot.message_templates WHERE key = %s", (key,)) if await cur.fetchone() is not None: return False await conn.execute( """ INSERT INTO bot.message_templates (key, text, description, enabled, updated_by) VALUES (%s, %s, %s, %s, %s) """, (key, text, description, enabled, editor_id), ) await conn.execute( """ INSERT INTO bot.message_template_history (key, old_text, new_text, editor_id) VALUES (%s, NULL, %s, %s) """, (key, text, editor_id), ) return True async def update_template(key: str, text: str, *, editor_id: int | None = None) -> dict[str, Any] | None: """Обновить текст шаблона и записать историю. Возвращает ``None``, если шаблона нет. Старое значение читается под ``FOR UPDATE``, поэтому параллельные правки не потеряют историю. """ async with get_pool().connection() as conn: cur = await conn.execute("SELECT text FROM bot.message_templates WHERE key = %s FOR UPDATE", (key,)) row = await cur.fetchone() if row is None: return None old_text = row["text"] if old_text == text: return {"key": key, "old_text": old_text, "text": text, "changed": False} await conn.execute( """ UPDATE bot.message_templates SET text = %s, updated_at = now(), updated_by = %s WHERE key = %s """, (text, editor_id, key), ) await conn.execute( """ INSERT INTO bot.message_template_history (key, old_text, new_text, editor_id) VALUES (%s, %s, %s, %s) """, (key, old_text, text, editor_id), ) return {"key": key, "old_text": old_text, "text": text, "changed": True} async def template_history(key: str, *, limit: int = 10) -> list[dict[str, Any]]: """Последние изменения шаблона (для блока истории в редакторе).""" async with get_pool().connection() as conn: cur = await conn.execute( """ SELECT id, old_text, new_text, editor_id, changed_at FROM bot.message_template_history WHERE key = %s ORDER BY changed_at DESC LIMIT %s """, (key, limit), ) return await cur.fetchall()