v1.2.0
This commit is contained in:
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
|
||||
from middleware.loggers import logger
|
||||
from .database import Database, get_db
|
||||
from .repository import BanWordsRepository
|
||||
from .models import BanWordType, SpamStat, SpamLog, TempBanWord
|
||||
from .models import BanWordType, SpamStat, SpamLog, TempBanWord, AutoComment
|
||||
|
||||
from sqlalchemy import select, delete, func, desc
|
||||
|
||||
@@ -43,6 +43,7 @@ class BanWordsManager:
|
||||
async def init(self) -> None:
|
||||
"""Инициализирует базу данных и загружает кэш"""
|
||||
await self.db.init()
|
||||
await self.init_default_bot_settings() # ← добавлено
|
||||
await self.refresh_cache()
|
||||
logger.info("BanWordsManager инициализирован", log_type="DATABASE")
|
||||
|
||||
@@ -335,7 +336,6 @@ class BanWordsManager:
|
||||
now = datetime.now().timestamp()
|
||||
|
||||
if now >= silence_until:
|
||||
# Время истекло - удаляем настройку
|
||||
await self.disable_silence_mode()
|
||||
return False
|
||||
|
||||
@@ -381,7 +381,6 @@ class BanWordsManager:
|
||||
now = datetime.now().timestamp()
|
||||
|
||||
if now >= conflict_until:
|
||||
# Время истекло
|
||||
await self.disable_conflict_mode()
|
||||
return False
|
||||
|
||||
@@ -433,7 +432,6 @@ class BanWordsManager:
|
||||
"""Получает общую статистику"""
|
||||
db_stats = await self.repo.get_stats()
|
||||
|
||||
# Добавляем информацию о кэше
|
||||
cache_info = {
|
||||
'cache_active': self._cache_banwords is not None,
|
||||
'cache_updated_at': self._cache_updated_at.isoformat() if self._cache_updated_at else None
|
||||
@@ -480,7 +478,6 @@ class BanWordsManager:
|
||||
"""
|
||||
async with self.db.get_session() as session:
|
||||
try:
|
||||
# Группируем по matched_word и считаем количество
|
||||
query = select(
|
||||
SpamLog.matched_word,
|
||||
SpamLog.match_type,
|
||||
@@ -497,7 +494,6 @@ class BanWordsManager:
|
||||
result = await session.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
# Форматируем результат
|
||||
top_words = []
|
||||
for row in rows:
|
||||
top_words.append({
|
||||
@@ -531,7 +527,6 @@ class BanWordsManager:
|
||||
try:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# Ищем истёкшие временные слова
|
||||
query = select(TempBanWord).where(
|
||||
TempBanWord.expires_at < now
|
||||
)
|
||||
@@ -541,23 +536,18 @@ class BanWordsManager:
|
||||
if not expired_words:
|
||||
return 0
|
||||
|
||||
# Собираем информацию для логирования
|
||||
expired_info = []
|
||||
for word in expired_words:
|
||||
expired_info.append({
|
||||
'word': word.word,
|
||||
'type': word.word_type.value,
|
||||
'type': word.type.value, # ← ИСПРАВЛЕНО: было word.word_type.value
|
||||
'expires_at': word.expires_at
|
||||
})
|
||||
await session.delete(word)
|
||||
|
||||
# Сохраняем изменения
|
||||
await session.commit()
|
||||
|
||||
# Обновляем кеш
|
||||
await self.refresh_cache()
|
||||
|
||||
# Логируем подробности
|
||||
logger.info(
|
||||
f"Удалено {len(expired_words)} истёкших временных банвордов",
|
||||
log_type="DATABASE"
|
||||
@@ -608,7 +598,6 @@ class BanWordsManager:
|
||||
"""
|
||||
async with self.db.get_session() as session:
|
||||
try:
|
||||
# Удаляем все записи
|
||||
await session.execute(delete(SpamLog))
|
||||
await session.commit()
|
||||
|
||||
@@ -629,32 +618,30 @@ class BanWordsManager:
|
||||
"""
|
||||
Получает настройки автокомментариев для канала.
|
||||
|
||||
Args:
|
||||
channel_id: ID канала
|
||||
|
||||
Returns:
|
||||
dict: Настройки или значения по умолчанию
|
||||
ВАЖНО: возвращает сохранённые поля даже когда is_enabled=False,
|
||||
чтобы UI/preview показывали реальную конфигурацию.
|
||||
"""
|
||||
from configs import settings
|
||||
|
||||
auto_comment = await self.repo.get_auto_comment(channel_id)
|
||||
|
||||
if auto_comment and auto_comment.is_enabled:
|
||||
return {
|
||||
'text': auto_comment.text,
|
||||
'button_text': auto_comment.button_text,
|
||||
'button_url': auto_comment.button_url,
|
||||
'photo_url': auto_comment.photo_url,
|
||||
'is_enabled': auto_comment.is_enabled,
|
||||
}
|
||||
|
||||
# Возвращаем настройки по умолчанию из .env
|
||||
return {
|
||||
defaults = {
|
||||
'text': settings.AUTO_COMMENT_TEXT,
|
||||
'button_text': settings.AUTO_COMMENT_BUTTON_TEXT,
|
||||
'button_url': settings.AUTO_COMMENT_BUTTON_URL,
|
||||
'photo_url': settings.AUTO_COMMENT_PHOTO_URL,
|
||||
'is_enabled': False, # По умолчанию выключено
|
||||
'is_enabled': False,
|
||||
}
|
||||
|
||||
if not auto_comment:
|
||||
return defaults
|
||||
|
||||
return {
|
||||
'text': auto_comment.text if auto_comment.text is not None else defaults['text'],
|
||||
'button_text': auto_comment.button_text if auto_comment.button_text is not None else defaults['button_text'],
|
||||
'button_url': auto_comment.button_url if auto_comment.button_url is not None else defaults['button_url'],
|
||||
'photo_url': auto_comment.photo_url if auto_comment.photo_url is not None else defaults['photo_url'],
|
||||
'is_enabled': bool(auto_comment.is_enabled),
|
||||
}
|
||||
|
||||
async def save_auto_comment_settings(
|
||||
@@ -715,6 +702,136 @@ class BanWordsManager:
|
||||
channel_id, 'photo_url', photo_url, updated_by
|
||||
)
|
||||
|
||||
async def log_report(
|
||||
self,
|
||||
report_id: str,
|
||||
reporter_id: int,
|
||||
reporter_username: Optional[str],
|
||||
reported_user_id: int,
|
||||
reported_username: Optional[str],
|
||||
chat_id: int,
|
||||
chat_title: Optional[str],
|
||||
message_id: int,
|
||||
message_thread_id: Optional[int],
|
||||
message_text: Optional[str],
|
||||
reason: str
|
||||
) -> bool:
|
||||
"""Логирует репорт в БД"""
|
||||
return await self.repo.log_report(
|
||||
report_id=report_id,
|
||||
reporter_id=reporter_id,
|
||||
reporter_username=reporter_username,
|
||||
reported_user_id=reported_user_id,
|
||||
reported_username=reported_username,
|
||||
chat_id=chat_id,
|
||||
chat_title=chat_title,
|
||||
message_id=message_id,
|
||||
message_thread_id=message_thread_id,
|
||||
message_text=message_text,
|
||||
reason=reason
|
||||
)
|
||||
|
||||
# ==================== ✅ КАНАЛЫ АВТОКОММЕНТАРИЕВ ====================
|
||||
|
||||
async def add_auto_comment_channel(self, channel_id: int, added_by: int) -> bool:
|
||||
"""✅ Добавляет новый канал в БД"""
|
||||
async with self.db.get_session() as session:
|
||||
try:
|
||||
new_channel = AutoComment(
|
||||
channel_id=channel_id,
|
||||
text="",
|
||||
button_text="",
|
||||
button_url="",
|
||||
photo_url="",
|
||||
is_enabled=False,
|
||||
updated_by=added_by,
|
||||
)
|
||||
session.add(new_channel)
|
||||
await session.commit()
|
||||
await session.refresh(new_channel)
|
||||
logger.info(f"✅ Канал добавлен: {channel_id}", log_type="CHANNEL")
|
||||
return True
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.error(f"Ошибка добавления канала {channel_id}: {e}", log_type="CHANNEL")
|
||||
return False
|
||||
|
||||
async def get_auto_comment_channels(self) -> List[int]:
|
||||
"""✅ Возвращает все channel_id из БД"""
|
||||
async with self.db.get_session() as session:
|
||||
result = await session.execute(select(AutoComment.channel_id).distinct())
|
||||
return [row[0] for row in result.fetchall()]
|
||||
|
||||
async def delete_auto_comment(self, channel_id: int) -> bool:
|
||||
"""✅ Удаляет настройки канала"""
|
||||
async with self.db.get_session() as session:
|
||||
try:
|
||||
result = await session.execute(
|
||||
delete(AutoComment).where(AutoComment.channel_id == channel_id)
|
||||
)
|
||||
if result.rowcount > 0:
|
||||
await session.commit()
|
||||
logger.info(f"✅ Канал удален: {channel_id}", log_type="CHANNEL")
|
||||
return True
|
||||
await session.rollback()
|
||||
return False
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.error(f"Ошибка удаления канала {channel_id}: {e}", log_type="CHANNEL")
|
||||
return False
|
||||
|
||||
# ==================== BOT SETTINGS (замена .env) ====================
|
||||
async def get_bot_settings(self) -> dict:
|
||||
"""Получает все настройки бота из БД"""
|
||||
settings = {
|
||||
'admin_chat_id': await self.repo.get_setting("admin_chat_id"),
|
||||
'admin_thread_id': await self.repo.get_setting("admin_thread_id"),
|
||||
'report_chat_id': await self.repo.get_setting("report_chat_id"),
|
||||
'report_thread_id': await self.repo.get_setting("report_thread_id"),
|
||||
}
|
||||
return {k: v for k, v in settings.items() if v is not None}
|
||||
|
||||
async def set_bot_setting(self, key: str, value: Optional[str]) -> bool:
|
||||
"""
|
||||
Сохраняет настройку бота в БД
|
||||
|
||||
Args:
|
||||
key: admin_chat_id, admin_thread_id, report_chat_id, report_thread_id
|
||||
value: str или None/null
|
||||
|
||||
Returns:
|
||||
bool: True если сохранено
|
||||
"""
|
||||
if value is None:
|
||||
return await self.repo.delete_setting(key)
|
||||
else:
|
||||
return await self.repo.set_setting(key, value)
|
||||
|
||||
async def get_bot_setting(self, key: str) -> Optional[str]:
|
||||
"""Получает ОДНУ настройку бота"""
|
||||
settings = await self.get_bot_settings()
|
||||
return settings.get(key)
|
||||
|
||||
async def init_default_bot_settings(self) -> None:
|
||||
"""Инициализирует настройки по умолчанию из .env"""
|
||||
try:
|
||||
from configs import settings
|
||||
|
||||
defaults = {
|
||||
"admin_chat_id": getattr(settings, 'ADMIN_CHAT_ID', None),
|
||||
"admin_thread_id": str(getattr(settings, 'ADMIN_THREAD_ID', None)) if getattr(settings, 'ADMIN_THREAD_ID', None) else None,
|
||||
"report_chat_id": getattr(settings, 'REPORT_CHAT_ID', None),
|
||||
"report_thread_id": str(getattr(settings, 'REPORT_THREAD_ID', None)) if getattr(settings, 'REPORT_THREAD_ID', None) else None,
|
||||
}
|
||||
|
||||
for key, value in defaults.items():
|
||||
if value: # Не null
|
||||
await self.set_bot_setting(key, str(value))
|
||||
|
||||
logger.info("✅ Настройки бота инициализированы из .env", log_type="SETTINGS")
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось инициализировать настройки из .env: {e}", log_type="SETTINGS")
|
||||
|
||||
|
||||
# Глобальный экземпляр менеджера
|
||||
_manager_instance: Optional[BanWordsManager] = None
|
||||
|
||||
@@ -19,7 +19,8 @@ __all__ = (
|
||||
"Setting",
|
||||
"SpamStat",
|
||||
"SpamLog",
|
||||
"AutoComment",
|
||||
"AutoComment",
|
||||
"Report",
|
||||
)
|
||||
|
||||
|
||||
@@ -294,3 +295,68 @@ class AutoComment(Base):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<AutoComment(channel_id={self.channel_id}, enabled={self.is_enabled})>"
|
||||
|
||||
|
||||
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"<Report(id={self.report_id}, reporter={self.reporter_id}, reported={self.reported_user_id})>"
|
||||
|
||||
@@ -157,12 +157,6 @@ class BanWordsRepository:
|
||||
return set()
|
||||
|
||||
async def get_all_banwords(self) -> dict[BanWordType, Set[str]]:
|
||||
"""
|
||||
Получает все банворды, сгруппированные по типам.
|
||||
|
||||
Returns:
|
||||
dict: {BanWordType: Set[str]}
|
||||
"""
|
||||
result = {
|
||||
BanWordType.SUBSTRING: set(),
|
||||
BanWordType.LEMMA: set(),
|
||||
@@ -170,19 +164,28 @@ class BanWordsRepository:
|
||||
BanWordType.CONFLICT_SUBSTRING: set(),
|
||||
BanWordType.CONFLICT_LEMMA: set(),
|
||||
}
|
||||
|
||||
try:
|
||||
async with self.db.get_session() as session:
|
||||
banwords = await session.execute(select(BanWord))
|
||||
loaded = 0
|
||||
for banword in banwords.scalars():
|
||||
result[banword.type].add(banword.word)
|
||||
|
||||
try:
|
||||
word_type = (
|
||||
banword.type
|
||||
if isinstance(banword.type, BanWordType)
|
||||
else BanWordType(banword.type.casefold())
|
||||
)
|
||||
if word_type in result:
|
||||
result[word_type].add(banword.word)
|
||||
loaded += 1
|
||||
except ValueError:
|
||||
logger.warning(
|
||||
f"Неизвестный тип: '{banword.type}' для '{banword.word}'",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
logger.info(f"✅ Кэш загружен: {loaded} банвордов", log_type="DATABASE")
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка получения всех банвордов: {e}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
|
||||
logger.error(f"❌ get_all_banwords: {e}", log_type="DATABASE")
|
||||
return result
|
||||
|
||||
async def search_banwords(self, query: str, limit: int = 50) -> List[BanWord]:
|
||||
@@ -344,8 +347,14 @@ class BanWordsRepository:
|
||||
)
|
||||
)
|
||||
for temp_banword in temp_banwords.scalars():
|
||||
if temp_banword.type in result:
|
||||
result[temp_banword.type].add(temp_banword.word)
|
||||
word_type = (
|
||||
temp_banword.type
|
||||
if isinstance(temp_banword.type, BanWordType)
|
||||
else BanWordType(temp_banword.type.casefold())
|
||||
)
|
||||
if word_type in result:
|
||||
result[word_type].add(temp_banword.word)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
@@ -1046,3 +1055,336 @@ class BanWordsRepository:
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return False
|
||||
|
||||
# === REPORTS ===
|
||||
|
||||
async def log_report(
|
||||
self,
|
||||
report_id: str,
|
||||
reporter_id: int,
|
||||
reporter_username: Optional[str],
|
||||
reported_user_id: int,
|
||||
reported_username: Optional[str],
|
||||
chat_id: int,
|
||||
chat_title: Optional[str],
|
||||
message_id: int,
|
||||
message_thread_id: Optional[int],
|
||||
message_text: Optional[str],
|
||||
reason: str
|
||||
) -> bool:
|
||||
"""
|
||||
Сохраняет репорт в БД.
|
||||
|
||||
Args:
|
||||
report_id: Уникальный ID репорта
|
||||
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: Текст сообщения
|
||||
reason: Причина жалобы
|
||||
|
||||
Returns:
|
||||
bool: True если успешно
|
||||
"""
|
||||
try:
|
||||
from .models import Report # Импорт здесь, чтобы избежать циклических импортов
|
||||
|
||||
async with self.db.get_session() as session:
|
||||
report = Report(
|
||||
report_id=report_id,
|
||||
reporter_id=reporter_id,
|
||||
reporter_username=reporter_username,
|
||||
reported_user_id=reported_user_id,
|
||||
reported_username=reported_username,
|
||||
chat_id=chat_id,
|
||||
chat_title=chat_title,
|
||||
message_id=message_id,
|
||||
message_thread_id=message_thread_id,
|
||||
message_text=message_text[:500] if message_text else None,
|
||||
reason=reason
|
||||
)
|
||||
session.add(report)
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
f"Репорт #{report_id} сохранён в БД",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка сохранения репорта: {e}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return False
|
||||
|
||||
async def update_report_status(
|
||||
self,
|
||||
report_id: str,
|
||||
status: str,
|
||||
processed_by: int
|
||||
) -> bool:
|
||||
"""
|
||||
Обновляет статус репорта.
|
||||
|
||||
Args:
|
||||
report_id: ID репорта
|
||||
status: Новый статус (closed, banned, deleted)
|
||||
processed_by: ID админа
|
||||
|
||||
Returns:
|
||||
bool: True если успешно
|
||||
"""
|
||||
try:
|
||||
from .models import Report
|
||||
|
||||
async with self.db.get_session() as session:
|
||||
result = await session.execute(
|
||||
select(Report).where(Report.report_id == report_id)
|
||||
)
|
||||
report = result.scalar_one_or_none()
|
||||
|
||||
if not report:
|
||||
return False
|
||||
|
||||
report.status = status
|
||||
report.processed_by = processed_by
|
||||
report.processed_at = datetime.now(timezone.utc)
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
f"Репорт #{report_id} обновлён: статус={status}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка обновления статуса репорта: {e}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return False
|
||||
|
||||
async def get_report_stats(self) -> dict:
|
||||
"""
|
||||
Получает общую статистику по репортам.
|
||||
|
||||
Returns:
|
||||
dict: Статистика
|
||||
"""
|
||||
try:
|
||||
from .models import Report
|
||||
|
||||
async with self.db.get_session() as session:
|
||||
# Всего репортов
|
||||
total_reports = await session.execute(
|
||||
select(func.count(Report.id))
|
||||
)
|
||||
|
||||
# По статусам
|
||||
pending_reports = await session.execute(
|
||||
select(func.count(Report.id)).where(Report.status == "pending")
|
||||
)
|
||||
closed_reports = await session.execute(
|
||||
select(func.count(Report.id)).where(Report.status == "closed")
|
||||
)
|
||||
banned_reports = await session.execute(
|
||||
select(func.count(Report.id)).where(Report.status == "banned")
|
||||
)
|
||||
deleted_reports = await session.execute(
|
||||
select(func.count(Report.id)).where(Report.status == "deleted")
|
||||
)
|
||||
|
||||
return {
|
||||
'total': total_reports.scalar_one(),
|
||||
'pending': pending_reports.scalar_one(),
|
||||
'closed': closed_reports.scalar_one(),
|
||||
'banned': banned_reports.scalar_one(),
|
||||
'deleted': deleted_reports.scalar_one(),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка получения статистики репортов: {e}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return {}
|
||||
|
||||
async def get_top_reporters(self, limit: int = 10) -> List[tuple[int, str, int]]:
|
||||
"""
|
||||
Получает топ жалобщиков.
|
||||
|
||||
Args:
|
||||
limit: Количество записей
|
||||
|
||||
Returns:
|
||||
List[tuple[int, str, int]]: [(user_id, username, count), ...]
|
||||
"""
|
||||
try:
|
||||
from .models import Report
|
||||
|
||||
async with self.db.get_session() as session:
|
||||
result = await session.execute(
|
||||
select(
|
||||
Report.reporter_id,
|
||||
Report.reporter_username,
|
||||
func.count(Report.id).label('count')
|
||||
)
|
||||
.group_by(Report.reporter_id, Report.reporter_username)
|
||||
.order_by(func.count(Report.id).desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return [
|
||||
(row.reporter_id, row.reporter_username or f"id{row.reporter_id}", row.count)
|
||||
for row in result
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка получения топ жалобщиков: {e}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return []
|
||||
|
||||
async def get_top_reported_users(self, limit: int = 10) -> List[tuple[int, str, int]]:
|
||||
"""
|
||||
Получает топ нарушителей.
|
||||
|
||||
Args:
|
||||
limit: Количество записей
|
||||
|
||||
Returns:
|
||||
List[tuple[int, str, int]]: [(user_id, username, count), ...]
|
||||
"""
|
||||
try:
|
||||
from .models import Report
|
||||
|
||||
async with self.db.get_session() as session:
|
||||
result = await session.execute(
|
||||
select(
|
||||
Report.reported_user_id,
|
||||
Report.reported_username,
|
||||
func.count(Report.id).label('count')
|
||||
)
|
||||
.group_by(Report.reported_user_id, Report.reported_username)
|
||||
.order_by(func.count(Report.id).desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return [
|
||||
(row.reported_user_id, row.reported_username or f"id{row.reported_user_id}", row.count)
|
||||
for row in result
|
||||
]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка получения топ нарушителей: {e}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return []
|
||||
|
||||
async def get_recent_reports(self, limit: int = 20) -> List:
|
||||
"""
|
||||
Получает последние репорты.
|
||||
|
||||
Args:
|
||||
limit: Количество записей
|
||||
|
||||
Returns:
|
||||
List[Report]: Список репортов
|
||||
"""
|
||||
try:
|
||||
from .models import Report
|
||||
|
||||
async with self.db.get_session() as session:
|
||||
result = await session.execute(
|
||||
select(Report)
|
||||
.order_by(Report.created_at.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка получения последних репортов: {e}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return []
|
||||
|
||||
async def get_user_report_count(self, user_id: int, as_reporter: bool = True) -> int:
|
||||
"""
|
||||
Получает количество репортов пользователя.
|
||||
|
||||
Args:
|
||||
user_id: ID пользователя
|
||||
as_reporter: True - как жалобщик, False - как нарушитель
|
||||
|
||||
Returns:
|
||||
int: Количество репортов
|
||||
"""
|
||||
try:
|
||||
from .models import Report
|
||||
|
||||
async with self.db.get_session() as session:
|
||||
if as_reporter:
|
||||
result = await session.execute(
|
||||
select(func.count(Report.id)).where(
|
||||
Report.reporter_id == user_id
|
||||
)
|
||||
)
|
||||
else:
|
||||
result = await session.execute(
|
||||
select(func.count(Report.id)).where(
|
||||
Report.reported_user_id == user_id
|
||||
)
|
||||
)
|
||||
return result.scalar_one()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Ошибка подсчёта репортов пользователя: {e}",
|
||||
log_type="DATABASE"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
async def get_setting(self, key: str) -> Optional[str]:
|
||||
"""Получает значение настройки"""
|
||||
async with self.db.get_session() as session:
|
||||
result = await session.get(Setting, key)
|
||||
return result.value if result else None
|
||||
|
||||
async def set_setting(self, key: str, value: str) -> bool:
|
||||
"""Устанавливает значение настройки"""
|
||||
async with self.db.get_session() as session:
|
||||
try:
|
||||
setting = await session.get(Setting, key)
|
||||
if setting:
|
||||
setting.value = value
|
||||
setting.updated_at = datetime.now()
|
||||
else:
|
||||
setting = Setting(key=key, value=value)
|
||||
session.add(setting)
|
||||
await session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.error(f"set_setting {key} failed: {e}", log_type="DATABASE")
|
||||
return False
|
||||
|
||||
async def delete_setting(self, key: str) -> bool:
|
||||
"""Удаляет настройку"""
|
||||
async with self.db.get_session() as session:
|
||||
try:
|
||||
result = await session.execute(delete(Setting).where(Setting.key == key))
|
||||
await session.commit()
|
||||
return result.rowcount > 0
|
||||
except Exception as e:
|
||||
await session.rollback()
|
||||
logger.error(f"delete_setting {key} failed: {e}", log_type="DATABASE")
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user