Files
PrimoGuardBot-/database/repository.py
2026-02-17 11:24:55 +07:00

799 lines
26 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.
"""
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 {}