Добавлены новые методы для получения статистики постов пользователей, информации о последних постах и количестве банов. Обновлены запросы в репозиториях для сортировки пользователей по дате бана. Исправлены вызовы функций форматирования сообщений для администраторов. Обновлены тесты для проверки новых функциональностей.

This commit is contained in:
2026-02-28 21:30:08 +03:00
parent e2a6944ed8
commit 694cf1c106
18 changed files with 1296 additions and 144 deletions

View File

@@ -291,12 +291,33 @@ class PrivateHandlers:
"""Handle messages in admin chat states"""
# User service operations with metrics
await self.user_service.update_user_activity(message.from_user.id)
await message.forward(chat_id=self.settings.group_for_message)
# Формируем обогащённое сообщение для админов
user_id = message.from_user.id
full_name = message.from_user.full_name
username = message.from_user.username
message_text = message.text or ""
enriched_message = await self.user_service.format_user_message_for_admins(
user_id=user_id,
full_name=full_name,
username=username,
message_text=message_text,
)
# Отправляем обогащённое сообщение вместо forward
sent_message = await message.bot.send_message(
chat_id=self.settings.group_for_message,
text=enriched_message,
parse_mode="HTML",
)
current_date = datetime.now()
date = int(current_date.timestamp())
# Сохраняем message_id из результата send_message
await self.db.add_message(
message.text, message.from_user.id, message.message_id + 1, date
message.text, message.from_user.id, sent_message.message_id, date
)
question = messages.get_message(get_first_name(message), "QUESTION")

View File

@@ -156,6 +156,92 @@ class UserService:
username = message.from_user.username or "Без никнейма"
return html.escape(full_name), html.escape(username)
async def format_user_message_for_admins(
self, user_id: int, full_name: str, username: str, message_text: str
) -> str:
"""
Форматирует сообщение пользователя для отправки админам с обогащёнными данными.
Args:
user_id: ID пользователя
full_name: Полное имя пользователя
username: Username пользователя (может быть None)
message_text: Текст сообщения пользователя
Returns:
Отформатированное сообщение для админов
"""
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
safe_username = html.escape(username) if username else None
safe_message_text = html.escape(message_text) if message_text else ""
# Формируем строку с информацией об авторе
if safe_username:
author_info = f"{safe_full_name} (@{safe_username})"
else:
author_info = f"{safe_full_name} (Ник не указан)"
# Получаем статистику постов
approved, declined, suggest = await self.db.get_user_posts_stats(user_id)
total_posts = approved + declined + suggest
# Получаем последний пост
last_post = await self.db.get_last_post_by_author(user_id)
if last_post:
if len(last_post) > 80:
last_post_display = f'"{html.escape(last_post[:80])}..."'
else:
last_post_display = f'"{html.escape(last_post)}"'
else:
last_post_display = "Нет постов"
# Получаем дату регистрации
user_info = await self.db.get_user_by_id(user_id)
if user_info and user_info.date_added:
date_added = datetime.fromtimestamp(user_info.date_added).strftime("%d.%m.%Y")
else:
date_added = "Неизвестно"
# Получаем информацию о банах
ban_count = await self.db.get_user_ban_count(user_id)
ban_section = ""
if ban_count > 0:
last_ban = await self.db.get_last_ban_info(user_id)
if last_ban:
date_ban, reason, date_unban = last_ban
ban_date_str = datetime.fromtimestamp(date_ban).strftime("%d.%m.%Y")
reason_display = html.escape(reason) if reason else "Не указана"
if date_unban:
unban_date_str = datetime.fromtimestamp(date_unban).strftime(
"%d.%m.%Y %H:%M"
)
last_ban_info = (
f" Последний: {ban_date_str}, причина «{reason_display}», "
f"истёк {unban_date_str}"
)
else:
last_ban_info = (
f" Последний: {ban_date_str}, причина «{reason_display}», "
f"активен"
)
ban_section = f"\n\n🚫 Банов: {ban_count}\n{last_ban_info}"
# Формируем итоговое сообщение
formatted_message = (
f"👤 От: {author_info} | ID: {user_id}\n\n"
f"📊 Постов в базе: {total_posts}\n"
f"📝 Последний пост: {last_post_display}\n"
f"📅 В боте с: {date_added}"
f"{ban_section}\n\n"
f"---\n"
f"<b>Сообщение пользователя:</b>\n\n"
f"<b>{safe_message_text}</b>"
)
return formatted_message
class PostService:
"""Service for post-related operations"""
@@ -236,6 +322,18 @@ class PostService:
f"PostService: Ошибка сохранения скоров для {message_id}: {e}"
)
async def _add_submitted_post_background(
self, text: str, post_id: int, rag_score: float = None
) -> None:
"""Индексирует пост в RAG submitted collection в фоне."""
try:
if self.scoring_manager:
await self.scoring_manager.add_submitted_post(text, post_id, rag_score)
except Exception as e:
logger.warning(
f"PostService: Ошибка добавления поста в submitted: {e}"
)
async def _get_scores_with_error_handling(self, text: str) -> tuple:
"""
Получает скоры для текста поста с обработкой ошибок.
@@ -321,6 +419,37 @@ class PostService:
error_message,
) = await self._get_scores_with_error_handling(original_raw_text)
# Проверяем похожие посты (до добавления текущего в submitted)
similar_warning = ""
if self.scoring_manager and original_raw_text and original_raw_text.strip():
try:
similar_result = await self.scoring_manager.find_similar_posts(
original_raw_text, threshold=0.9, hours=24
)
if similar_result and similar_result.similar_count > 0:
# Формируем предупреждение с текстом похожего поста
similar_text = ""
if similar_result.similar_posts:
first_similar = similar_result.similar_posts[0]
if first_similar.text:
truncated_text = first_similar.text[:150]
if len(first_similar.text) > 150:
truncated_text += "..."
similar_text = f'\n<b>Текст поста:</b>\n"{html.escape(truncated_text)}"'
similar_warning = (
f"\n\n⚠️ <b>Похожий пост за последние 24ч</b> "
f"(совпадение {similar_result.max_similarity:.0%})"
f"{similar_text}"
)
logger.info(
f"PostService: Найден похожий пост для message_id={message.message_id}, "
f"similar_count={similar_result.similar_count}, "
f"max_similarity={similar_result.max_similarity:.2%}"
)
except Exception as e:
logger.warning(f"PostService: Ошибка поиска похожих постов: {e}")
# Формируем текст для поста (с сообщением об ошибке если есть)
text_for_post = original_raw_text
if error_message:
@@ -347,9 +476,11 @@ class PostService:
message.from_user.username,
deepseek_score=deepseek_score,
rag_score=rag_score,
rag_confidence=rag_confidence,
rag_score_pos_only=rag_score_pos_only,
user_id=message.from_user.id,
)
# Добавляем предупреждение о похожем посте
if similar_warning:
post_text += similar_warning
# Определяем анонимность по исходному тексту (без сообщения об ошибке)
is_anonymous = determine_anonymity(original_raw_text)
@@ -401,8 +532,11 @@ class PostService:
markup,
)
elif content_type == "media_group":
# Добавляем предупреждение о похожем посте в caption медиагруппы
if similar_warning:
post_text += similar_warning
# Для медиагруппы используем специальную обработку
# Передаем ml_scores_json для сохранения в БД
# Передаем ml_scores_json и rag_score для сохранения в БД
await self._process_media_group_background(
message,
album,
@@ -411,6 +545,7 @@ class PostService:
is_anonymous,
original_raw_text,
ml_scores_json,
rag_score,
)
return
else:
@@ -448,6 +583,14 @@ class PostService:
)
)
# Индексируем пост в RAG submitted collection (после успешной отправки)
if self.scoring_manager and original_raw_text and original_raw_text.strip():
asyncio.create_task(
self._add_submitted_post_background(
original_raw_text, sent_message.message_id, rag_score
)
)
except Exception as e:
logger.error(
f"PostService: Критическая ошибка в _process_post_background для {content_type}: {e}"
@@ -462,6 +605,7 @@ class PostService:
is_anonymous: bool,
original_raw_text: str,
ml_scores_json: str = None,
rag_score: float = None,
) -> None:
"""Обрабатывает медиагруппу в фоне"""
try:
@@ -495,6 +639,14 @@ class PostService:
self._save_scores_background(main_post_id, ml_scores_json)
)
# Индексируем пост в RAG submitted collection
if self.scoring_manager and original_raw_text and original_raw_text.strip():
asyncio.create_task(
self._add_submitted_post_background(
original_raw_text, main_post_id, rag_score
)
)
for msg_id in media_group_message_ids:
await self.db.add_message_link(main_post_id, msg_id)
@@ -552,8 +704,7 @@ class PostService:
message.from_user.username,
deepseek_score=deepseek_score,
rag_score=rag_score,
rag_confidence=rag_confidence,
rag_score_pos_only=rag_score_pos_only,
user_id=message.from_user.id,
)
markup = get_reply_keyboard_for_post()
@@ -611,8 +762,7 @@ class PostService:
message.from_user.username,
deepseek_score=deepseek_score,
rag_score=rag_score,
rag_confidence=rag_confidence,
rag_score_pos_only=rag_score_pos_only,
user_id=message.from_user.id,
)
markup = get_reply_keyboard_for_post()
@@ -677,8 +827,7 @@ class PostService:
message.from_user.username,
deepseek_score=deepseek_score,
rag_score=rag_score,
rag_confidence=rag_confidence,
rag_score_pos_only=rag_score_pos_only,
user_id=message.from_user.id,
)
markup = get_reply_keyboard_for_post()
@@ -770,8 +919,7 @@ class PostService:
message.from_user.username,
deepseek_score=deepseek_score,
rag_score=rag_score,
rag_confidence=rag_confidence,
rag_score_pos_only=rag_score_pos_only,
user_id=message.from_user.id,
)
markup = get_reply_keyboard_for_post()
@@ -869,8 +1017,7 @@ class PostService:
message.from_user.username,
deepseek_score=deepseek_score,
rag_score=rag_score,
rag_confidence=rag_confidence,
rag_score_pos_only=rag_score_pos_only,
user_id=message.from_user.id,
)
is_anonymous = determine_anonymity(raw_caption)