Первый коммит

This commit is contained in:
2026-02-17 11:24:55 +07:00
commit a06448ca4b
109 changed files with 21165 additions and 0 deletions

798
database/repository.py Normal file
View File

@@ -0,0 +1,798 @@
"""
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 {}