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

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

@@ -138,7 +138,7 @@ async def get_banned_users(
keyboard = create_keyboard_with_pagination(
1, len(buttons_list), buttons_list, "unlock"
)
await message.answer(text=message_text, reply_markup=keyboard)
await message.answer(text=message_text, reply_markup=keyboard, parse_mode="HTML")
else:
await message.answer(
text="В списке заблокированных пользователей никого нет"
@@ -216,9 +216,15 @@ async def get_ml_stats(message: types.Message, state: FSMContext, **kwargs):
# Fallback на синхронные данные (если API недоступен)
lines.append(f" • API URL: {rag.get('api_url', 'N/A')}")
if "enabled" in rag:
lines.append(
f" • Статус: {'✅ Включен' if rag.get('enabled') else '❌ Отключен'}"
)
if rag.get("enabled"):
lines.append(
f" • Статус: ⚠️ Включен, но API не отвечает"
)
lines.append(
f" • Проверьте доступность сервиса и API ключ"
)
else:
lines.append(f" • Статус: ❌ Отключен")
lines.append("")

View File

@@ -255,6 +255,8 @@ async def change_page(call: CallbackQuery, bot_db: MagicData("bot_db"), **kwargs
logger.info(f"Переход на страницу {page_number}")
items_per_page = 9
if call.message.text == "Список пользователей которые последними обращались к боту":
list_users = await bot_db.get_last_users(30)
keyboard = create_keyboard_with_pagination(
@@ -266,11 +268,13 @@ async def change_page(call: CallbackQuery, bot_db: MagicData("bot_db"), **kwargs
reply_markup=keyboard,
)
else:
message_user = await get_banned_users_list(int(page_number) * 7 - 7, bot_db)
offset = (page_number - 1) * items_per_page
message_user = await get_banned_users_list(offset, bot_db)
await call.bot.edit_message_text(
chat_id=call.message.chat.id,
message_id=call.message.message_id,
text=message_user,
parse_mode="HTML",
)
buttons = await get_banned_users_buttons(bot_db)

View File

@@ -8,7 +8,7 @@ from aiogram.types import CallbackQuery
from helper_bot.keyboards.keyboards import create_keyboard_for_ban_reason
from helper_bot.utils.helper_func import (
delete_user_blacklist,
get_text_message,
get_publish_text,
send_audio_message,
send_media_group_to_channel,
send_photo_message,
@@ -137,7 +137,7 @@ class PostPublishService:
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
# Формируем финальный текст с учетом is_anonymous
formatted_text = get_text_message(
formatted_text = get_publish_text(
raw_text, user.first_name, user.username, is_anonymous
)
@@ -188,7 +188,7 @@ class PostPublishService:
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
# Формируем финальный текст с учетом is_anonymous
formatted_text = get_text_message(
formatted_text = get_publish_text(
raw_text, user.first_name, user.username, is_anonymous
)
@@ -247,7 +247,7 @@ class PostPublishService:
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
# Формируем финальный текст с учетом is_anonymous
formatted_text = get_text_message(
formatted_text = get_publish_text(
raw_text, user.first_name, user.username, is_anonymous
)
@@ -340,7 +340,7 @@ class PostPublishService:
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
# Формируем финальный текст с учетом is_anonymous
formatted_text = get_text_message(
formatted_text = get_publish_text(
raw_text, user.first_name, user.username, is_anonymous
)
@@ -452,7 +452,7 @@ class PostPublishService:
f"Пользователь {author_id} не найден в базе данных"
)
formatted_text = get_text_message(
formatted_text = get_publish_text(
raw_text, user.first_name, user.username, is_anonymous
)
@@ -838,7 +838,7 @@ class BanService:
await self.db.set_user_blacklist(
user_id=author_id,
user_name=None,
message_for_user="Спам",
message_for_user="Последний пост",
date_to_unban=date_to_unban,
ban_author=ban_author_id,
)

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)

View File

@@ -4,7 +4,8 @@ HTTP клиент для взаимодействия с внешним RAG се
Использует REST API для получения скоров и отправки примеров.
"""
from typing import Any, Dict, Optional
from dataclasses import dataclass
from typing import Any, Dict, List, Optional
import httpx
@@ -15,6 +16,30 @@ from .base import ScoringResult
from .exceptions import InsufficientExamplesError, ScoringError, TextTooShortError
@dataclass
class SimilarPost:
"""Данные о похожем посте."""
similarity: float
created_at: int
post_id: Optional[int]
text: str
rag_score: Optional[float]
@dataclass
class SimilarPostsResult:
"""Результат поиска похожих постов."""
similar_count: int
similar_posts: List[SimilarPost]
max_similarity: float = 0.0
def __post_init__(self):
if self.similar_posts:
self.max_similarity = max(p.similarity for p in self.similar_posts)
class RagApiClient:
"""
HTTP клиент для взаимодействия с внешним RAG сервисом.
@@ -329,21 +354,39 @@ class RagApiClient:
Словарь со статистикой или пустой словарь при ошибке
"""
if not self._enabled:
logger.debug("RagApiClient: get_stats пропущен - клиент отключен")
return {}
try:
logger.debug(f"RagApiClient: Запрос статистики от {self.api_url}/stats")
response = await self._client.get(f"{self.api_url}/stats")
if response.status_code == 200:
return response.json()
data = response.json()
logger.info(
f"RagApiClient: Статистика получена успешно: "
f"model_loaded={data.get('model_loaded')}, "
f"model_name={data.get('model_name')}, "
f"vector_store={data.get('vector_store', {}).get('total_count', 'N/A')} примеров"
)
return data
elif response.status_code == 401 or response.status_code == 403:
logger.warning(
f"RagApiClient: Ошибка авторизации при получении статистики: "
f"status={response.status_code}, body={response.text[:200]}"
)
return {}
else:
logger.warning(
f"RagApiClient: Неожиданный статус при получении статистики: {response.status_code}"
f"RagApiClient: Неожиданный статус при получении статистики: "
f"status={response.status_code}, body={response.text[:200]}"
)
return {}
except httpx.TimeoutException:
logger.warning(f"RagApiClient: Таймаут при получении статистики")
logger.warning(
f"RagApiClient: Таймаут при получении статистики (timeout={self.timeout}s)"
)
return {}
except httpx.RequestError as e:
logger.warning(
@@ -365,3 +408,135 @@ class RagApiClient:
"api_url": self.api_url,
"timeout": self.timeout,
}
@track_time("find_similar_posts", "rag_client")
async def find_similar_posts(
self, text: str, threshold: float = 0.9, hours: int = 24
) -> Optional[SimilarPostsResult]:
"""
Ищет похожие посты за последние N часов.
Args:
text: Текст поста для поиска похожих
threshold: Порог схожести (0.0-1.0), по умолчанию 0.9
hours: За сколько часов искать (1-168), по умолчанию 24
Returns:
SimilarPostsResult с информацией о похожих постах или None при ошибке
"""
if not self._enabled:
return None
if not text or not text.strip():
return None
try:
response = await self._client.post(
f"{self.api_url}/similar",
json={"text": text.strip(), "threshold": threshold, "hours": hours},
)
if response.status_code == 200:
data = response.json()
similar_posts = []
for post_data in data.get("similar_posts", []):
similar_posts.append(
SimilarPost(
similarity=float(post_data.get("similarity", 0.0)),
created_at=int(post_data.get("created_at", 0)),
post_id=post_data.get("post_id"),
text=post_data.get("text", ""),
rag_score=post_data.get("rag_score"),
)
)
result = SimilarPostsResult(
similar_count=data.get("similar_count", 0),
similar_posts=similar_posts,
)
if result.similar_count > 0:
logger.info(
f"RagApiClient: Найдено {result.similar_count} похожих постов "
f"(max_similarity={result.max_similarity:.2%})"
)
return result
else:
logger.warning(
f"RagApiClient: Неожиданный статус при поиске похожих постов: "
f"{response.status_code}, body: {response.text}"
)
return None
except httpx.TimeoutException:
logger.warning("RagApiClient: Таймаут при поиске похожих постов")
return None
except httpx.RequestError as e:
logger.warning(
f"RagApiClient: Ошибка подключения при поиске похожих постов: {e}"
)
return None
except Exception as e:
logger.error(f"RagApiClient: Ошибка поиска похожих постов: {e}")
return None
@track_time("add_submitted_post", "rag_client")
async def add_submitted_post(
self, text: str, post_id: Optional[int] = None, rag_score: Optional[float] = None
) -> bool:
"""
Добавляет пост в коллекцию submitted для поиска похожих.
Args:
text: Текст поста
post_id: ID поста (опционально)
rag_score: RAG скор на момент добавления (опционально)
Returns:
True если пост успешно добавлен
"""
if not self._enabled:
return False
if not text or not text.strip():
return False
try:
payload = {"text": text.strip()}
if post_id is not None:
payload["post_id"] = post_id
if rag_score is not None:
payload["rag_score"] = rag_score
response = await self._client.post(
f"{self.api_url}/submitted",
json=payload,
)
if response.status_code in (200, 201):
data = response.json()
logger.debug(
f"RagApiClient: Пост добавлен в submitted "
f"(post_id={post_id}, submitted_count={data.get('submitted_count', 'N/A')})"
)
return True
else:
logger.warning(
f"RagApiClient: Неожиданный статус при добавлении в submitted: "
f"{response.status_code}"
)
return False
except httpx.TimeoutException:
logger.warning("RagApiClient: Таймаут при добавлении в submitted")
return False
except httpx.RequestError as e:
logger.warning(
f"RagApiClient: Ошибка подключения при добавлении в submitted: {e}"
)
return False
except Exception as e:
logger.error(f"RagApiClient: Ошибка добавления в submitted: {e}")
return False

View File

@@ -221,3 +221,43 @@ class ScoringManager:
stats["deepseek"] = self.deepseek_service.get_stats()
return stats
@track_time("find_similar_posts", "scoring_manager")
async def find_similar_posts(
self, text: str, threshold: float = 0.9, hours: int = 24
):
"""
Ищет похожие посты через RAG API.
Args:
text: Текст для поиска похожих
threshold: Порог схожести (0.0-1.0)
hours: За сколько часов искать
Returns:
SimilarPostsResult или None
"""
if not self.rag_client or not self.rag_client.is_enabled:
return None
return await self.rag_client.find_similar_posts(text, threshold, hours)
@track_time("add_submitted_post", "scoring_manager")
async def add_submitted_post(
self, text: str, post_id: Optional[int] = None, rag_score: Optional[float] = None
) -> bool:
"""
Добавляет пост в коллекцию submitted для поиска похожих.
Args:
text: Текст поста
post_id: ID поста (опционально)
rag_score: RAG скор на момент добавления (опционально)
Returns:
True если успешно добавлен
"""
if not self.rag_client or not self.rag_client.is_enabled:
return False
return await self.rag_client.add_submitted_post(text, post_id, rag_score)

View File

@@ -138,6 +138,52 @@ def determine_anonymity(post_text: str) -> bool:
return False
def get_publish_text(
post_text: str,
first_name: str,
username: str = None,
is_anonymous: Optional[bool] = None,
) -> str:
"""
Форматирует текст для финальной публикации в канал.
Только текст поста + подпись автора или анон.
Args:
post_text: Текст сообщения
first_name: Имя автора поста
username: Юзернейм автора поста (может быть None)
is_anonymous: Флаг анонимности (True - анонимно, False - не анонимно, None - legacy)
Returns:
str: Текст для публикации в канал
"""
safe_post_text = post_text or ""
safe_first_name = first_name or "Пользователь"
# Формируем строку с информацией об авторе
if username:
author_info = f"{safe_first_name} @{username}"
else:
author_info = f"{safe_first_name}"
# Определяем анонимность и формируем финальный текст
if is_anonymous is not None:
if is_anonymous:
final_text = f"{safe_post_text}\n\nПост опубликован анонимно"
else:
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
else:
# Legacy: определяем по тексту
if "неанон" in post_text.lower() or "не анон" in post_text.lower():
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
elif "анон" in post_text.lower():
final_text = f"{safe_post_text}\n\nПост опубликован анонимно"
else:
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
return final_text
def get_text_message(
post_text: str,
first_name: str,
@@ -147,10 +193,10 @@ def get_text_message(
rag_score: Optional[float] = None,
rag_confidence: Optional[float] = None,
rag_score_pos_only: Optional[float] = None,
user_id: Optional[int] = None,
):
"""
Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон"
или переданного параметра is_anonymous.
Форматирует текст сообщения для модерации (с полной информацией об авторе и скорами).
Args:
post_text: Текст сообщения
@@ -161,64 +207,69 @@ def get_text_message(
rag_score: Скор от RAG/ruBERT neg/pos (0.0-1.0, опционально)
rag_confidence: Уверенность RAG модели (0.0-1.0, зависит от количества примеров)
rag_score_pos_only: Скор RAG только по положительным примерам (0.0-1.0, опционально)
user_id: ID пользователя Telegram (опционально)
Returns:
str: - Сформированный текст сообщения.
str: - Сформированный текст сообщения для модерации.
"""
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
# Экранируем username для безопасного использования в HTML
safe_username = html.escape(username) if username else None
safe_first_name = html.escape(first_name) if first_name else "Пользователь"
# Формируем строку с информацией об авторе
# Формируем шапку с информацией об авторе
if safe_username:
author_info = f"{first_name} @{safe_username}"
header = f"👤 От: {safe_first_name} (@{safe_username})"
else:
author_info = f"{first_name} (Ник не указан)"
header = f"👤 От: {safe_first_name} (Ник не указан)"
# Формируем базовый текст
# Если передан is_anonymous, используем его, иначе определяем по тексту (legacy)
if user_id:
header += f" | ID: {user_id}"
# Формируем строку с информацией об авторе для подвала
if safe_username:
author_info = f"{safe_first_name} @{safe_username}"
else:
author_info = f"{safe_first_name} (Ник не указан)"
# Формируем блок с текстом поста
separator = "=" * 32
post_block = f"{header}\n<b>Текст поста:</b>\n{separator}\n{safe_post_text}"
# Определяем анонимность и формируем подвал
if is_anonymous is not None:
if is_anonymous:
final_text = f"{safe_post_text}\n\nПост опубликован анонимно"
post_block += f"\n\nПост опубликован анонимно"
else:
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
post_block += f"\n\n<b>Автор поста:</b> {author_info}"
else:
# Legacy: определяем по тексту
if "неанон" in post_text or "не анон" in post_text:
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
post_block += f"\n\n<b>Автор поста:</b> {author_info}"
elif "анон" in post_text:
final_text = f"{safe_post_text}\n\nПост опубликован анонимно"
post_block += f"\n\nПост опубликован анонимно"
else:
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
post_block += f"\n\n<b>Автор поста:</b> {author_info}"
# Добавляем блок со скорами если есть
if (
deepseek_score is not None
or rag_score is not None
or rag_score_pos_only is not None
):
scores_lines = ["\n📊 Уверенность в одобрении:"]
post_block += f"\n{separator}"
# Добавляем блок со скорами если есть (без RAG pos only и уверенности)
if deepseek_score is not None or rag_score is not None:
scores_lines = ["📊 <b>Уверенность в одобрении:</b>"]
if deepseek_score is not None:
scores_lines.append(f"DeepSeek: {deepseek_score:.2f}")
if rag_score is not None:
logger.debug(
f"get_text_message: Форматирование rag_score - "
f"rag_score={rag_score} (type: {type(rag_score).__name__}), "
f"rag_score_pos_only={rag_score_pos_only} (type: {type(rag_score_pos_only).__name__ if rag_score_pos_only is not None else 'None'}), "
f"rag_confidence={rag_confidence} (type: {type(rag_confidence).__name__ if rag_confidence is not None else 'None'}), "
f"formatted_value={rag_score:.2f}"
)
rag_line = f"RAG neg/pos: {rag_score:.2f}"
if rag_confidence is not None:
rag_line += f" (уверенность: {rag_confidence:.0%})"
scores_lines.append(rag_line)
if rag_score_pos_only is not None:
scores_lines.append(f"RAG pos only: {rag_score_pos_only:.2f}")
final_text += "\n" + "\n".join(scores_lines)
scores_lines.append(f"RAG neg/pos: {rag_score:.2f}")
post_block += "\n" + "\n".join(scores_lines)
return final_text
return post_block
@track_time("download_file", "helper_func")
@@ -854,15 +905,14 @@ async def send_text_message(
):
from .rate_limiter import send_with_rate_limit
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
async def _send_message():
if markup is None:
return await message.bot.send_message(chat_id=chat_id, text=safe_post_text)
return await message.bot.send_message(
chat_id=chat_id, text=post_text, parse_mode="HTML"
)
else:
return await message.bot.send_message(
chat_id=chat_id, text=safe_post_text, reply_markup=markup
chat_id=chat_id, text=post_text, reply_markup=markup, parse_mode="HTML"
)
sent_message = await send_with_rate_limit(_send_message, chat_id)
@@ -878,16 +928,17 @@ async def send_photo_message(
post_text: str,
markup: types.ReplyKeyboardMarkup = None,
):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None:
sent_message = await message.bot.send_photo(
chat_id=chat_id, caption=safe_post_text, photo=photo
chat_id=chat_id, caption=post_text, photo=photo, parse_mode="HTML"
)
else:
sent_message = await message.bot.send_photo(
chat_id=chat_id, caption=safe_post_text, photo=photo, reply_markup=markup
chat_id=chat_id,
caption=post_text,
photo=photo,
reply_markup=markup,
parse_mode="HTML",
)
return sent_message
@@ -901,16 +952,17 @@ async def send_video_message(
post_text: str = "",
markup: types.ReplyKeyboardMarkup = None,
):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None:
sent_message = await message.bot.send_video(
chat_id=chat_id, caption=safe_post_text, video=video
chat_id=chat_id, caption=post_text, video=video, parse_mode="HTML"
)
else:
sent_message = await message.bot.send_video(
chat_id=chat_id, caption=safe_post_text, video=video, reply_markup=markup
chat_id=chat_id,
caption=post_text,
video=video,
reply_markup=markup,
parse_mode="HTML",
)
return sent_message
@@ -943,16 +995,17 @@ async def send_audio_message(
post_text: str,
markup: types.ReplyKeyboardMarkup = None,
):
# Экранируем post_text для безопасного использования в HTML
safe_post_text = html.escape(str(post_text)) if post_text else ""
if markup is None:
sent_message = await message.bot.send_audio(
chat_id=chat_id, caption=safe_post_text, audio=audio
chat_id=chat_id, caption=post_text, audio=audio, parse_mode="HTML"
)
else:
sent_message = await message.bot.send_audio(
chat_id=chat_id, caption=safe_post_text, audio=audio, reply_markup=markup
chat_id=chat_id,
caption=post_text,
audio=audio,
reply_markup=markup,
parse_mode="HTML",
)
return sent_message
@@ -1012,11 +1065,12 @@ async def get_banned_users_list(offset: int, bot_db):
message - текст сообщения
user_ids - лист кортежей [(user_name: user_id)]
"""
users = await bot_db.get_banned_users_from_db_with_limits(limit=7, offset=offset)
items_per_page = 9
users = await bot_db.get_banned_users_from_db_with_limits(limit=items_per_page, offset=offset)
message = "Список заблокированных пользователей:\n"
for user in users:
user_id, ban_reason, unban_date = user
user_id, ban_reason, unban_date, ban_date = user
# Получаем имя пользователя из таблицы users
username = await bot_db.get_username(user_id)
full_name = await bot_db.get_full_name_by_id(user_id)
@@ -1028,41 +1082,42 @@ async def get_banned_users_list(offset: int, bot_db):
html.escape(str(ban_reason)) if ban_reason else "Причина не указана"
)
# Форматируем дату разбана в человекочитаемый формат
if unban_date:
try:
# Предполагаем, что unban_date это UNIX timestamp
if isinstance(unban_date, (int, float)):
unban_datetime = datetime.fromtimestamp(unban_date)
safe_unban_date = unban_datetime.strftime("%d-%m-%Y %H:%M")
elif isinstance(unban_date, str):
# Если это строка, попытаемся её обработать
try:
# Попробуем преобразовать строку в timestamp
timestamp = int(unban_date)
unban_datetime = datetime.fromtimestamp(timestamp)
safe_unban_date = unban_datetime.strftime("%d-%m-%Y %H:%M")
except (ValueError, TypeError):
# Если не удалось, показываем как есть
safe_unban_date = html.escape(str(unban_date))
elif hasattr(unban_date, "strftime"):
# Если это datetime объект
safe_unban_date = unban_date.strftime("%d-%m-%Y %H:%M")
else:
# Для всех остальных случаев
safe_unban_date = html.escape(str(unban_date))
except (ValueError, TypeError, OSError):
# В случае ошибки показываем исходное значение
safe_unban_date = html.escape(str(unban_date))
else:
safe_unban_date = "Дата не указана"
# Форматируем дату бана в человекочитаемый формат
safe_ban_date = _format_timestamp_to_date(ban_date)
message += f"**Пользователь:** {safe_user_name}\n"
message += f"**Причина бана:** {safe_ban_reason}\n"
message += f"**Дата разбана:** {safe_unban_date}\n\n"
# Форматируем дату разбана в человекочитаемый формат
safe_unban_date = _format_timestamp_to_date(unban_date, default="Навсегда")
message += f"<b>Пользователь:</b> {safe_user_name}\n"
message += f"<b>Причина бана:</b> {safe_ban_reason}\n"
message += f"<b>Дата бана:</b> {safe_ban_date}\n"
message += f"<b>Дата разбана:</b> {safe_unban_date}\n\n"
return message
def _format_timestamp_to_date(timestamp, default: str = "Дата не указана") -> str:
"""Форматирует timestamp в читаемую дату."""
if not timestamp:
return default
try:
if isinstance(timestamp, (int, float)):
dt = datetime.fromtimestamp(timestamp)
return dt.strftime("%d.%m.%Y %H:%M")
elif isinstance(timestamp, str):
try:
ts = int(timestamp)
dt = datetime.fromtimestamp(ts)
return dt.strftime("%d.%m.%Y %H:%M")
except (ValueError, TypeError):
return html.escape(str(timestamp))
elif hasattr(timestamp, "strftime"):
return timestamp.strftime("%d.%m.%Y %H:%M")
else:
return html.escape(str(timestamp))
except (ValueError, TypeError, OSError):
return html.escape(str(timestamp))
@track_time("get_banned_users_buttons", "helper_func")
@track_errors("helper_func", "get_banned_users_buttons")
@db_query_time("get_banned_users_buttons", "users", "select")