""" Repository для работы с банвордами через SQLAlchemy ORM. """ from typing import Set, List, Optional from datetime import datetime, timedelta from sqlalchemy import select, delete, func, and_ from middleware.loggers import logger from .database import Database from .models import ( BanWord, TempBanWord, WhitelistWord, Admin, Setting, SpamStat, BanWordType ) __all__ = ("BanWordsRepository",) class BanWordsRepository: """ Repository для CRUD операций с банвордами. Все методы работают через SQLAlchemy ORM. """ def __init__(self, db: Database): """ Args: db: Экземпляр Database """ self.db = db # === BANWORDS === async def add_banword( self, word: str, word_type: BanWordType, added_by: Optional[int] = None, reason: Optional[str] = None ) -> bool: """ Добавляет постоянный банворд. Args: word: Слово для блокировки word_type: Тип банворда added_by: ID админа, который добавил reason: Причина добавления Returns: bool: True если добавлен, False если уже существует """ try: async with self.db.get_session() as session: # Проверяем, существует ли уже existing = await session.execute( select(BanWord).where( and_( BanWord.word == word.lower(), BanWord.type == word_type ) ) ) if existing.scalar_one_or_none(): return False # Добавляем новый banword = BanWord( word=word.lower(), type=word_type, added_by=added_by, reason=reason ) session.add(banword) await session.commit() logger.info( f"Добавлен банворд: '{word}' ({word_type.value})", log_type="DATABASE" ) return True except Exception as e: logger.error( f"Ошибка добавления банворда: {e}", log_type="DATABASE" ) return False async def remove_banword(self, word: str, word_type: BanWordType) -> bool: """ Удаляет банворд. Args: word: Слово word_type: Тип Returns: bool: True если удалён """ try: async with self.db.get_session() as session: result = await session.execute( delete(BanWord).where( and_( BanWord.word == word.lower(), BanWord.type == word_type ) ) ) await session.commit() deleted = result.rowcount > 0 if deleted: logger.info( f"Удалён банворд: '{word}' ({word_type.value})", log_type="DATABASE" ) return deleted except Exception as e: logger.error( f"Ошибка удаления банворда: {e}", log_type="DATABASE" ) return False async def get_banwords(self, word_type: BanWordType) -> Set[str]: """ Получает все банворды определённого типа. Args: word_type: Тип банвордов Returns: Set[str]: Набор слов """ try: async with self.db.get_session() as session: result = await session.execute( select(BanWord.word).where(BanWord.type == word_type) ) return set(result.scalars().all()) except Exception as e: logger.error( f"Ошибка получения банвордов: {e}", log_type="DATABASE" ) return set() async def get_all_banwords(self) -> dict[BanWordType, Set[str]]: """ Получает все банворды, сгруппированные по типам. Returns: dict: {BanWordType: Set[str]} """ result = { BanWordType.SUBSTRING: set(), BanWordType.LEMMA: set(), BanWordType.PART: set(), BanWordType.CONFLICT_SUBSTRING: set(), BanWordType.CONFLICT_LEMMA: set(), } try: async with self.db.get_session() as session: banwords = await session.execute(select(BanWord)) for banword in banwords.scalars(): result[banword.type].add(banword.word) except Exception as e: logger.error( f"Ошибка получения всех банвордов: {e}", log_type="DATABASE" ) return result async def search_banwords(self, query: str, limit: int = 50) -> List[BanWord]: """ Поиск банвордов по частичному совпадению. Args: query: Поисковый запрос limit: Максимум результатов Returns: List[BanWord]: Найденные банворды """ try: async with self.db.get_session() as session: result = await session.execute( select(BanWord) .where(BanWord.word.contains(query.lower())) .limit(limit) ) return list(result.scalars().all()) except Exception as e: logger.error( f"Ошибка поиска банвордов: {e}", log_type="DATABASE" ) return [] # === TEMPORARY BANWORDS === async def add_temp_banword( self, word: str, word_type: BanWordType, minutes: int, added_by: Optional[int] = None ) -> bool: """ Добавляет временный банворд. Args: word: Слово word_type: Тип minutes: Длительность в минутах added_by: ID админа Returns: bool: True если добавлен """ try: async with self.db.get_session() as session: # Проверяем существование existing = await session.execute( select(TempBanWord).where( and_( TempBanWord.word == word.lower(), TempBanWord.type == word_type ) ) ) if existing.scalar_one_or_none(): return False # Добавляем expires_at = datetime.now() + timedelta(minutes=minutes) temp_banword = TempBanWord( word=word.lower(), type=word_type, added_by=added_by, expires_at=expires_at ) session.add(temp_banword) await session.commit() logger.info( f"Добавлен временный банворд: '{word}' на {minutes} мин", log_type="DATABASE" ) return True except Exception as e: logger.error( f"Ошибка добавления временного банворда: {e}", log_type="DATABASE" ) return False async def remove_temp_banword(self, word: str, word_type: BanWordType) -> bool: """Удаляет временный банворд досрочно""" try: async with self.db.get_session() as session: result = await session.execute( delete(TempBanWord).where( and_( TempBanWord.word == word.lower(), TempBanWord.type == word_type ) ) ) await session.commit() deleted = result.rowcount > 0 if deleted: logger.info( f"Удалён временный банворд: '{word}'", log_type="DATABASE" ) return deleted except Exception as e: logger.error( f"Ошибка удаления временного банворда: {e}", log_type="DATABASE" ) return False async def get_temp_banwords(self, word_type: BanWordType) -> Set[str]: """ Получает активные (не истёкшие) временные банворды. Args: word_type: Тип банвордов Returns: Set[str]: Набор активных временных слов """ try: async with self.db.get_session() as session: result = await session.execute( select(TempBanWord.word).where( and_( TempBanWord.type == word_type, TempBanWord.expires_at > datetime.now() ) ) ) return set(result.scalars().all()) except Exception as e: logger.error( f"Ошибка получения временных банвордов: {e}", log_type="DATABASE" ) return set() async def get_all_temp_banwords(self) -> dict[BanWordType, Set[str]]: """Получает все активные временные банворды по типам""" result = { BanWordType.SUBSTRING: set(), BanWordType.LEMMA: set(), } try: async with self.db.get_session() as session: temp_banwords = await session.execute( select(TempBanWord).where( TempBanWord.expires_at > datetime.now() ) ) for temp_banword in temp_banwords.scalars(): if temp_banword.type in result: result[temp_banword.type].add(temp_banword.word) except Exception as e: logger.error( f"Ошибка получения всех временных банвордов: {e}", log_type="DATABASE" ) return result async def cleanup_expired_temp_banwords(self) -> int: """ Удаляет истёкшие временные банворды. Returns: int: Количество удалённых записей """ try: async with self.db.get_session() as session: result = await session.execute( delete(TempBanWord).where( TempBanWord.expires_at <= datetime.now() ) ) await session.commit() deleted = result.rowcount if deleted > 0: logger.info( f"Удалено {deleted} истёкших временных банвордов", log_type="DATABASE" ) return deleted except Exception as e: logger.error( f"Ошибка очистки временных банвордов: {e}", log_type="DATABASE" ) return 0 # === WHITELIST === async def add_whitelist( self, word: str, added_by: Optional[int] = None, reason: Optional[str] = None ) -> bool: """Добавляет слово в белый список (исключение)""" try: async with self.db.get_session() as session: # Проверяем существование existing = await session.execute( select(WhitelistWord).where( WhitelistWord.word == word.lower() ) ) if existing.scalar_one_or_none(): return False # Добавляем whitelist_word = WhitelistWord( word=word.lower(), added_by=added_by, reason=reason ) session.add(whitelist_word) await session.commit() logger.info( f"Добавлено исключение: '{word}'", log_type="DATABASE" ) return True except Exception as e: logger.error( f"Ошибка добавления исключения: {e}", log_type="DATABASE" ) return False async def remove_whitelist(self, word: str) -> bool: """Удаляет слово из белого списка""" try: async with self.db.get_session() as session: result = await session.execute( delete(WhitelistWord).where( WhitelistWord.word == word.lower() ) ) await session.commit() deleted = result.rowcount > 0 if deleted: logger.info( f"Удалено исключение: '{word}'", log_type="DATABASE" ) return deleted except Exception as e: logger.error( f"Ошибка удаления исключения: {e}", log_type="DATABASE" ) return False async def get_whitelist(self) -> Set[str]: """Получает все слова из белого списка""" try: async with self.db.get_session() as session: result = await session.execute(select(WhitelistWord.word)) return set(result.scalars().all()) except Exception as e: logger.error( f"Ошибка получения whitelist: {e}", log_type="DATABASE" ) return set() # === ADMINS === async def add_admin( self, user_id: int, added_by: Optional[int] = None ) -> bool: """Добавляет администратора""" try: async with self.db.get_session() as session: # Проверяем существование existing = await session.execute( select(Admin).where(Admin.user_id == user_id) ) if existing.scalar_one_or_none(): return False # Добавляем admin = Admin(user_id=user_id, added_by=added_by) session.add(admin) await session.commit() logger.info( f"Добавлен админ: {user_id}", log_type="DATABASE" ) return True except Exception as e: logger.error( f"Ошибка добавления админа: {e}", log_type="DATABASE" ) return False async def remove_admin(self, user_id: int) -> bool: """Удаляет администратора""" try: async with self.db.get_session() as session: result = await session.execute( delete(Admin).where(Admin.user_id == user_id) ) await session.commit() deleted = result.rowcount > 0 if deleted: logger.info( f"Удалён админ: {user_id}", log_type="DATABASE" ) return deleted except Exception as e: logger.error( f"Ошибка удаления админа: {e}", log_type="DATABASE" ) return False async def get_admins(self) -> Set[int]: """Получает всех администраторов""" try: async with self.db.get_session() as session: result = await session.execute(select(Admin.user_id)) return set(result.scalars().all()) except Exception as e: logger.error( f"Ошибка получения админов: {e}", log_type="DATABASE" ) return set() async def is_admin(self, user_id: int) -> bool: """Проверяет, является ли пользователь админом""" try: async with self.db.get_session() as session: result = await session.execute( select(Admin).where(Admin.user_id == user_id) ) return result.scalar_one_or_none() is not None except Exception as e: logger.error( f"Ошибка проверки админа: {e}", log_type="DATABASE" ) return False # === SETTINGS === async def set_setting(self, key: str, value: str) -> None: """ Сохраняет настройку (или обновляет существующую). Args: key: Ключ настройки value: Значение (строка или JSON) """ try: async with self.db.get_session() as session: # Проверяем существование existing = await session.execute( select(Setting).where(Setting.key == key) ) setting = existing.scalar_one_or_none() if setting: # Обновляем существующую setting.value = value setting.updated_at = datetime.now() else: # Создаём новую setting = Setting(key=key, value=value) session.add(setting) await session.commit() except Exception as e: logger.error( f"Ошибка сохранения настройки: {e}", log_type="DATABASE" ) async def get_setting( self, key: str, default: Optional[str] = None ) -> Optional[str]: """ Получает значение настройки. Args: key: Ключ настройки default: Значение по умолчанию Returns: Optional[str]: Значение или default """ try: async with self.db.get_session() as session: result = await session.execute( select(Setting.value).where(Setting.key == key) ) value = result.scalar_one_or_none() return value if value is not None else default except Exception as e: logger.error( f"Ошибка получения настройки: {e}", log_type="DATABASE" ) return default async def delete_setting(self, key: str) -> bool: """Удаляет настройку""" try: async with self.db.get_session() as session: result = await session.execute( delete(Setting).where(Setting.key == key) ) await session.commit() return result.rowcount > 0 except Exception as e: logger.error( f"Ошибка удаления настройки: {e}", log_type="DATABASE" ) return False # === STATISTICS === async def log_spam_deletion( self, user_id: int, username: str, chat_id: int, message_text: str, matched_word: str, match_type: str ) -> None: """ Записывает статистику удалённого спам-сообщения. Args: user_id: Telegram ID отправителя username: Username отправителя chat_id: ID чата message_text: Текст сообщения (обрезается до 500 символов) matched_word: Слово, по которому сработал фильтр match_type: Тип проверки (substring/lemma/part/silence/conflict) """ try: async with self.db.get_session() as session: spam_stat = SpamStat( user_id=user_id, username=username, chat_id=chat_id, message_text=message_text[:500], matched_word=matched_word, match_type=match_type ) session.add(spam_stat) await session.commit() except Exception as e: logger.error( f"Ошибка логирования статистики: {e}", log_type="DATABASE" ) async def get_spam_stats( self, limit: int = 100, user_id: Optional[int] = None ) -> List[SpamStat]: """ Получает последнюю статистику удалений. Args: limit: Максимум записей user_id: Фильтр по пользователю (опционально) Returns: List[SpamStat]: Список записей статистики """ try: async with self.db.get_session() as session: query = select(SpamStat).order_by(SpamStat.deleted_at.desc()) if user_id: query = query.where(SpamStat.user_id == user_id) query = query.limit(limit) result = await session.execute(query) return list(result.scalars().all()) except Exception as e: logger.error( f"Ошибка получения статистики: {e}", log_type="DATABASE" ) return [] async def get_user_spam_count(self, user_id: int) -> int: """Получает количество удалённых сообщений пользователя""" try: async with self.db.get_session() as session: result = await session.execute( select(func.count(SpamStat.id)).where( SpamStat.user_id == user_id ) ) return result.scalar_one() except Exception as e: logger.error( f"Ошибка подсчёта спама: {e}", log_type="DATABASE" ) return 0 async def get_top_spammers(self, limit: int = 10) -> List[tuple[int, int]]: """ Получает топ спамеров. Args: limit: Количество записей Returns: List[tuple[int, int]]: [(user_id, count), ...] """ try: async with self.db.get_session() as session: result = await session.execute( select( SpamStat.user_id, func.count(SpamStat.id).label('count') ) .group_by(SpamStat.user_id) .order_by(func.count(SpamStat.id).desc()) .limit(limit) ) return [(row.user_id, row.count) for row in result] except Exception as e: logger.error( f"Ошибка получения топ спамеров: {e}", log_type="DATABASE" ) return [] # === GENERAL === async def get_stats(self) -> dict: """Получает общую статистику БД""" try: async with self.db.get_session() as session: banwords_count = await session.execute( select(func.count(BanWord.id)) ) temp_banwords_count = await session.execute( select(func.count(TempBanWord.id)) ) whitelist_count = await session.execute( select(func.count(WhitelistWord.id)) ) admins_count = await session.execute( select(func.count(Admin.id)) ) spam_stats_count = await session.execute( select(func.count(SpamStat.id)) ) return { 'banwords': banwords_count.scalar_one(), 'temp_banwords': temp_banwords_count.scalar_one(), 'whitelist': whitelist_count.scalar_one(), 'admins': admins_count.scalar_one(), 'spam_deletions': spam_stats_count.scalar_one(), } except Exception as e: logger.error( f"Ошибка получения статистики: {e}", log_type="DATABASE" ) return {}