From 409339b20310d85a1d96b0a7439be2b075cbe404 Mon Sep 17 00:00:00 2001 From: Verum Date: Mon, 23 Feb 2026 14:20:23 +0700 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=B1=D0=B0=D0=B7=D1=8B=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database/models.py | 362 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 database/models.py diff --git a/database/models.py b/database/models.py new file mode 100644 index 0000000..c56bab3 --- /dev/null +++ b/database/models.py @@ -0,0 +1,362 @@ +""" +SQLAlchemy модели для банвордов. +""" +from datetime import datetime, timezone +from enum import Enum as PyEnum +from typing import Optional + +from sqlalchemy import String, Integer, DateTime, Text, Enum, BigInteger +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + +__all__ = ( + "Base", + "BanWordType", + "SpamMode", + "BanWord", + "TempBanWord", + "WhitelistWord", + "Admin", + "Setting", + "SpamStat", + "SpamLog", + "AutoComment", + "Report", +) + + +class Base(DeclarativeBase): + """Базовый класс для всех моделей""" + pass + + +class BanWordType(str, PyEnum): + """Типы банвордов""" + SUBSTRING = "substring" + LEMMA = "lemma" + PART = "part" + CONFLICT_SUBSTRING = "conflict_substring" + CONFLICT_LEMMA = "conflict_lemma" + + +class SpamMode(str, PyEnum): + """Режимы работы спам-фильтра""" + NORMAL = "normal" + SILENCE = "silence" + CONFLICT = "conflict" + + +class BanWord(Base): + """ + Постоянные банворды. + + Attributes: + id: Уникальный ID + word: Само слово (lowercase) + type: Тип банворда + added_by: Telegram ID добавившего админа + added_at: Дата добавления + reason: Причина добавления + """ + __tablename__ = "banwords" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + word: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + type: Mapped[BanWordType] = mapped_column( + Enum(BanWordType, native_enum=False), + nullable=False, + index=True + ) + added_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + added_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.now, + nullable=False + ) + reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + def __repr__(self) -> str: + return f"" + + +class TempBanWord(Base): + """ + Временные банворды (с автоудалением). + + Attributes: + id: Уникальный ID + word: Само слово + type: Тип банворда + added_by: ID админа + added_at: Дата добавления + expires_at: Дата истечения + """ + __tablename__ = "temp_banwords" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + word: Mapped[str] = mapped_column(String(255), nullable=False, index=True) + type: Mapped[BanWordType] = mapped_column( + Enum(BanWordType, native_enum=False), + nullable=False + ) + added_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + added_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.now, + nullable=False + ) + expires_at: Mapped[datetime] = mapped_column( + DateTime, + nullable=False, + index=True + ) + + def is_expired(self) -> bool: + """Проверяет, истёк ли срок""" + return datetime.now() >= self.expires_at + + def __repr__(self) -> str: + return f"" + + +class WhitelistWord(Base): + """ + Белый список (исключения из проверки). + + Attributes: + id: Уникальный ID + word: Слово-исключение + added_by: ID админа + added_at: Дата добавления + reason: Причина (например, "ложное срабатывание") + """ + __tablename__ = "whitelist" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + word: Mapped[str] = mapped_column(String(255), nullable=False, unique=True, index=True) + added_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + added_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.now, + nullable=False + ) + reason: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + def __repr__(self) -> str: + return f"" + + +class Admin(Base): + """ + Дополнительные администраторы бота. + + Attributes: + id: Уникальный ID записи + user_id: Telegram ID пользователя (уникальный) + added_by: ID суперадмина, который добавил + added_at: Дата добавления + permissions: JSON со списком прав (для будущего) + """ + __tablename__ = "admins" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column(Integer, nullable=False, unique=True, index=True) + added_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + added_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.now, + nullable=False + ) + permissions: Mapped[Optional[str]] = mapped_column( + Text, + default="[]", + nullable=True + ) + + def __repr__(self) -> str: + return f"" + + +class Setting(Base): + """ + Настройки и состояния бота. + + Attributes: + key: Ключ настройки (primary key) + value: Значение (JSON string) + updated_at: Дата обновления + + Examples: + - silence_until: datetime ISO string + - conflict_until: datetime ISO string + - spam_mode: "normal"/"silence"/"conflict" + """ + __tablename__ = "settings" + + key: Mapped[str] = mapped_column(String(100), primary_key=True) + value: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + updated_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.now, + onupdate=datetime.now, + nullable=False + ) + + def __repr__(self) -> str: + return f"" + + +class SpamStat(Base): + """ + Статистика удалённых спам-сообщений. + + Attributes: + id: Уникальный ID + user_id: Telegram ID отправителя + username: Username отправителя + chat_id: ID чата + message_text: Текст сообщения (до 500 символов) + matched_word: Слово, по которому сработал фильтр + match_type: Тип проверки (substring/lemma/part) + deleted_at: Дата удаления + """ + __tablename__ = "spam_stats" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + username: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + chat_id: Mapped[int] = mapped_column(Integer, nullable=False) + message_text: Mapped[str] = mapped_column(Text, nullable=False) + matched_word: Mapped[str] = mapped_column(String(255), nullable=False) + match_type: Mapped[str] = mapped_column(String(50), nullable=False) + deleted_at: Mapped[datetime] = mapped_column( + DateTime, + default=datetime.now, + nullable=False, + index=True + ) + + def __repr__(self) -> str: + return f"" + + +class SpamLog(Base): + """Модель для логирования срабатываний спам-фильтра""" + __tablename__ = "spam_logs" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + user_id: Mapped[int] = mapped_column(BigInteger, nullable=False) + username: Mapped[str] = mapped_column(String(255), nullable=True) + chat_id: Mapped[int] = mapped_column(BigInteger, nullable=False) + message_text: Mapped[str] = mapped_column(Text, nullable=True) + matched_word: Mapped[str] = mapped_column(String(255), nullable=True) # <-- Должно быть! + match_type: Mapped[str] = mapped_column(String(50), nullable=True) # <-- Должно быть! + timestamp: Mapped[datetime] = mapped_column( + DateTime, + default=lambda: datetime.now(timezone.utc) + ) + +class AutoComment(Base): + """ + Настройки автокомментариев для каналов. + + Attributes: + id: Уникальный ID + channel_id: ID канала (-100...) + text: Текст комментария (HTML) + button_text: Текст кнопки + button_url: URL кнопки + photo_url: URL фото для preview + is_enabled: Включены ли автокомментарии для этого канала + created_at: Дата создания + updated_at: Дата последнего обновления + updated_by: ID админа, который последним изменил + """ + __tablename__ = "auto_comments" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + channel_id: Mapped[int] = mapped_column(BigInteger, nullable=False, unique=True, index=True) + text: Mapped[str] = mapped_column(Text, nullable=False) + button_text: Mapped[str] = mapped_column(String(100), nullable=False) + button_url: Mapped[str] = mapped_column(String(500), nullable=False) + photo_url: Mapped[str] = mapped_column(String(500), nullable=False) + is_enabled: Mapped[bool] = mapped_column(Integer, default=1, nullable=False) + created_at: Mapped[datetime] = mapped_column( + DateTime, + default=lambda: datetime.now(timezone.utc), + nullable=False + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + nullable=False + ) + updated_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + + def __repr__(self) -> str: + return f"" + + +class Report(Base): + """ + Модель для хранения статистики репортов. + + Attributes: + id: Уникальный ID репорта + report_id: Строковый ID репорта (timestamp) + reporter_id: ID пользователя, который пожаловался + reporter_username: Username жалобщика + reported_user_id: ID пользователя, на которого пожаловались + reported_username: Username нарушителя + chat_id: ID чата, где произошло нарушение + chat_title: Название чата + message_id: ID сообщения-нарушения + message_thread_id: ID топика (если есть) + message_text: Текст сообщения (до 500 символов) + reason: Причина жалобы + status: Статус репорта (pending, closed, banned, deleted) + processed_by: ID админа, который обработал + created_at: Дата создания репорта + processed_at: Дата обработки + """ + __tablename__ = "reports" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + report_id: Mapped[str] = mapped_column(String(50), nullable=False, unique=True, index=True) + + # Информация о жалобщике + reporter_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) + reporter_username: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + + # Информация о нарушителе + reported_user_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True) + reported_username: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + + # Информация о чате и сообщении + chat_id: Mapped[int] = mapped_column(BigInteger, nullable=False) + chat_title: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + message_id: Mapped[int] = mapped_column(Integer, nullable=False) + message_thread_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + message_text: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + # Причина и статус + reason: Mapped[str] = mapped_column(Text, nullable=False) + status: Mapped[str] = mapped_column( + String(20), + default="pending", + nullable=False, + index=True + ) # pending, closed, banned, deleted + + # Обработка + processed_by: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + created_at: Mapped[datetime] = mapped_column( + DateTime, + default=lambda: datetime.now(timezone.utc), + nullable=False, + index=True + ) + processed_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + + def __repr__(self) -> str: + return f""