Добавлены новые методы для получения статистики постов пользователей, информации о последних постах и количестве банов. Обновлены запросы в репозиториях для сортировки пользователей по дате бана. Исправлены вызовы функций форматирования сообщений для администраторов. Обновлены тесты для проверки новых функциональностей.
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user