Compare commits
2 Commits
dev-14
...
b3cdadfd8e
| Author | SHA1 | Date | |
|---|---|---|---|
| b3cdadfd8e | |||
| 694cf1c106 |
@@ -279,6 +279,34 @@ class AsyncBotDB:
|
|||||||
"""Получает тексты отклоненных постов для обучения RAG."""
|
"""Получает тексты отклоненных постов для обучения RAG."""
|
||||||
return await self.factory.posts.get_declined_posts_texts(limit)
|
return await self.factory.posts.get_declined_posts_texts(limit)
|
||||||
|
|
||||||
|
async def get_user_posts_stats(self, user_id: int) -> Tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Получает статистику постов пользователя.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (approved_count, declined_count, suggest_count)
|
||||||
|
"""
|
||||||
|
return await self.factory.posts.get_user_posts_stats(user_id)
|
||||||
|
|
||||||
|
async def get_last_post_by_author(self, user_id: int) -> Optional[str]:
|
||||||
|
"""Получает текст последнего поста пользователя."""
|
||||||
|
return await self.factory.posts.get_last_post_by_author(user_id)
|
||||||
|
|
||||||
|
async def get_user_ban_count(self, user_id: int) -> int:
|
||||||
|
"""Получает количество банов пользователя за все время."""
|
||||||
|
return await self.factory.blacklist_history.get_ban_count(user_id)
|
||||||
|
|
||||||
|
async def get_last_ban_info(
|
||||||
|
self, user_id: int
|
||||||
|
) -> Optional[Tuple[int, str, Optional[int]]]:
|
||||||
|
"""
|
||||||
|
Получает информацию о последнем бане пользователя.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (date_ban, reason, date_unban) или None
|
||||||
|
"""
|
||||||
|
return await self.factory.blacklist_history.get_last_ban_info(user_id)
|
||||||
|
|
||||||
# Методы для работы с черным списком
|
# Методы для работы с черным списком
|
||||||
async def set_user_blacklist(
|
async def set_user_blacklist(
|
||||||
self,
|
self,
|
||||||
@@ -361,7 +389,8 @@ class AsyncBotDB:
|
|||||||
"""Возвращает список пользователей в черном списке с учетом смещения и ограничения."""
|
"""Возвращает список пользователей в черном списке с учетом смещения и ограничения."""
|
||||||
users = await self.factory.blacklist.get_all_users(offset, limit)
|
users = await self.factory.blacklist.get_all_users(offset, limit)
|
||||||
return [
|
return [
|
||||||
(user.user_id, user.message_for_user, user.date_to_unban) for user in users
|
(user.user_id, user.message_for_user, user.date_to_unban, user.created_at)
|
||||||
|
for user in users
|
||||||
]
|
]
|
||||||
|
|
||||||
async def get_blacklist_users_by_id(self, user_id: int) -> Optional[tuple]:
|
async def get_blacklist_users_by_id(self, user_id: int) -> Optional[tuple]:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Optional
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
from database.base import DatabaseConnection
|
from database.base import DatabaseConnection
|
||||||
from database.models import BlacklistHistoryRecord
|
from database.models import BlacklistHistoryRecord
|
||||||
@@ -120,3 +120,55 @@ class BlacklistHistoryRepository(DatabaseConnection):
|
|||||||
f"Ошибка обновления даты разбана в истории для user_id={user_id}: {str(e)}"
|
f"Ошибка обновления даты разбана в истории для user_id={user_id}: {str(e)}"
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
async def get_ban_count(self, user_id: int) -> int:
|
||||||
|
"""
|
||||||
|
Получает количество банов пользователя за все время.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Количество банов
|
||||||
|
"""
|
||||||
|
query = "SELECT COUNT(*) FROM blacklist_history WHERE user_id = ?"
|
||||||
|
rows = await self._execute_query_with_result(query, (user_id,))
|
||||||
|
row = rows[0] if rows else None
|
||||||
|
|
||||||
|
count = row[0] if row else 0
|
||||||
|
self.logger.info(f"Количество банов для user_id={user_id}: {count}")
|
||||||
|
return count
|
||||||
|
|
||||||
|
async def get_last_ban_info(
|
||||||
|
self, user_id: int
|
||||||
|
) -> Optional[Tuple[int, str, Optional[int]]]:
|
||||||
|
"""
|
||||||
|
Получает информацию о последнем бане пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (date_ban, reason, date_unban) или None, если банов не было
|
||||||
|
"""
|
||||||
|
query = """
|
||||||
|
SELECT date_ban, reason, date_unban FROM blacklist_history
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY date_ban DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
rows = await self._execute_query_with_result(query, (user_id,))
|
||||||
|
row = rows[0] if rows else None
|
||||||
|
|
||||||
|
if row:
|
||||||
|
date_ban = row[0]
|
||||||
|
reason = row[1]
|
||||||
|
date_unban = row[2]
|
||||||
|
self.logger.info(
|
||||||
|
f"Последний бан для user_id={user_id}: "
|
||||||
|
f"date_ban={date_ban}, reason={reason}, date_unban={date_unban}"
|
||||||
|
)
|
||||||
|
return (date_ban, reason, date_unban)
|
||||||
|
|
||||||
|
self.logger.info(f"Банов для user_id={user_id} не найдено")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -87,13 +87,14 @@ class BlacklistRepository(DatabaseConnection):
|
|||||||
async def get_all_users(
|
async def get_all_users(
|
||||||
self, offset: int = 0, limit: int = 10
|
self, offset: int = 0, limit: int = 10
|
||||||
) -> List[BlacklistUser]:
|
) -> List[BlacklistUser]:
|
||||||
"""Возвращает список пользователей в черном списке."""
|
"""Возвращает список пользователей в черном списке, отсортированных по дате бана (новые первые)."""
|
||||||
query = """
|
query = """
|
||||||
SELECT user_id, message_for_user, date_to_unban, created_at, ban_author
|
SELECT user_id, message_for_user, date_to_unban, created_at, ban_author
|
||||||
FROM blacklist
|
FROM blacklist
|
||||||
LIMIT ?, ?
|
ORDER BY created_at DESC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
"""
|
"""
|
||||||
rows = await self._execute_query_with_result(query, (offset, limit))
|
rows = await self._execute_query_with_result(query, (limit, offset))
|
||||||
|
|
||||||
users = []
|
users = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
@@ -113,10 +114,11 @@ class BlacklistRepository(DatabaseConnection):
|
|||||||
return users
|
return users
|
||||||
|
|
||||||
async def get_all_users_no_limit(self) -> List[BlacklistUser]:
|
async def get_all_users_no_limit(self) -> List[BlacklistUser]:
|
||||||
"""Возвращает список всех пользователей в черном списке без лимитов."""
|
"""Возвращает список всех пользователей в черном списке без лимитов, отсортированных по дате бана (новые первые)."""
|
||||||
query = """
|
query = """
|
||||||
SELECT user_id, message_for_user, date_to_unban, created_at, ban_author
|
SELECT user_id, message_for_user, date_to_unban, created_at, ban_author
|
||||||
FROM blacklist
|
FROM blacklist
|
||||||
|
ORDER BY created_at DESC
|
||||||
"""
|
"""
|
||||||
rows = await self._execute_query_with_result(query)
|
rows = await self._execute_query_with_result(query)
|
||||||
|
|
||||||
|
|||||||
@@ -545,3 +545,67 @@ class PostRepository(DatabaseConnection):
|
|||||||
texts = [row[0] for row in rows if row[0]]
|
texts = [row[0] for row in rows if row[0]]
|
||||||
self.logger.info(f"Получено {len(texts)} отклоненных постов для обучения")
|
self.logger.info(f"Получено {len(texts)} отклоненных постов для обучения")
|
||||||
return texts
|
return texts
|
||||||
|
|
||||||
|
async def get_user_posts_stats(self, user_id: int) -> Tuple[int, int, int]:
|
||||||
|
"""
|
||||||
|
Получает статистику постов пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple (approved_count, declined_count, suggest_count)
|
||||||
|
"""
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END) as approved,
|
||||||
|
SUM(CASE WHEN status = 'declined' THEN 1 ELSE 0 END) as declined,
|
||||||
|
SUM(CASE WHEN status = 'suggest' THEN 1 ELSE 0 END) as suggest
|
||||||
|
FROM post_from_telegram_suggest
|
||||||
|
WHERE author_id = ? AND text != '^'
|
||||||
|
"""
|
||||||
|
rows = await self._execute_query_with_result(query, (user_id,))
|
||||||
|
row = rows[0] if rows else None
|
||||||
|
|
||||||
|
if row:
|
||||||
|
approved = row[0] or 0
|
||||||
|
declined = row[1] or 0
|
||||||
|
suggest = row[2] or 0
|
||||||
|
self.logger.info(
|
||||||
|
f"Статистика постов для user_id={user_id}: "
|
||||||
|
f"approved={approved}, declined={declined}, suggest={suggest}"
|
||||||
|
)
|
||||||
|
return (approved, declined, suggest)
|
||||||
|
|
||||||
|
return (0, 0, 0)
|
||||||
|
|
||||||
|
async def get_last_post_by_author(self, user_id: int) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Получает текст последнего поста пользователя.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: ID пользователя
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Текст последнего поста или None, если постов нет
|
||||||
|
"""
|
||||||
|
query = """
|
||||||
|
SELECT text FROM post_from_telegram_suggest
|
||||||
|
WHERE author_id = ? AND text IS NOT NULL AND text != '' AND text != '^'
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
rows = await self._execute_query_with_result(query, (user_id,))
|
||||||
|
row = rows[0] if rows else None
|
||||||
|
|
||||||
|
if row:
|
||||||
|
text = row[0]
|
||||||
|
self.logger.info(
|
||||||
|
f"Последний пост для user_id={user_id}: '{text[:50]}...'"
|
||||||
|
if len(text) > 50
|
||||||
|
else f"Последний пост для user_id={user_id}: '{text}'"
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
|
||||||
|
self.logger.info(f"Постов для user_id={user_id} не найдено")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ async def get_banned_users(
|
|||||||
keyboard = create_keyboard_with_pagination(
|
keyboard = create_keyboard_with_pagination(
|
||||||
1, len(buttons_list), buttons_list, "unlock"
|
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:
|
else:
|
||||||
await message.answer(
|
await message.answer(
|
||||||
text="В списке заблокированных пользователей никого нет"
|
text="В списке заблокированных пользователей никого нет"
|
||||||
@@ -216,9 +216,15 @@ async def get_ml_stats(message: types.Message, state: FSMContext, **kwargs):
|
|||||||
# Fallback на синхронные данные (если API недоступен)
|
# Fallback на синхронные данные (если API недоступен)
|
||||||
lines.append(f" • API URL: {rag.get('api_url', 'N/A')}")
|
lines.append(f" • API URL: {rag.get('api_url', 'N/A')}")
|
||||||
if "enabled" in rag:
|
if "enabled" in rag:
|
||||||
lines.append(
|
if rag.get("enabled"):
|
||||||
f" • Статус: {'✅ Включен' if rag.get('enabled') else '❌ Отключен'}"
|
lines.append(
|
||||||
)
|
f" • Статус: ⚠️ Включен, но API не отвечает"
|
||||||
|
)
|
||||||
|
lines.append(
|
||||||
|
f" • Проверьте доступность сервиса и API ключ"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(f" • Статус: ❌ Отключен")
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ async def change_page(call: CallbackQuery, bot_db: MagicData("bot_db"), **kwargs
|
|||||||
|
|
||||||
logger.info(f"Переход на страницу {page_number}")
|
logger.info(f"Переход на страницу {page_number}")
|
||||||
|
|
||||||
|
items_per_page = 9
|
||||||
|
|
||||||
if call.message.text == "Список пользователей которые последними обращались к боту":
|
if call.message.text == "Список пользователей которые последними обращались к боту":
|
||||||
list_users = await bot_db.get_last_users(30)
|
list_users = await bot_db.get_last_users(30)
|
||||||
keyboard = create_keyboard_with_pagination(
|
keyboard = create_keyboard_with_pagination(
|
||||||
@@ -266,11 +268,13 @@ async def change_page(call: CallbackQuery, bot_db: MagicData("bot_db"), **kwargs
|
|||||||
reply_markup=keyboard,
|
reply_markup=keyboard,
|
||||||
)
|
)
|
||||||
else:
|
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(
|
await call.bot.edit_message_text(
|
||||||
chat_id=call.message.chat.id,
|
chat_id=call.message.chat.id,
|
||||||
message_id=call.message.message_id,
|
message_id=call.message.message_id,
|
||||||
text=message_user,
|
text=message_user,
|
||||||
|
parse_mode="HTML",
|
||||||
)
|
)
|
||||||
|
|
||||||
buttons = await get_banned_users_buttons(bot_db)
|
buttons = await get_banned_users_buttons(bot_db)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from aiogram.types import CallbackQuery
|
|||||||
from helper_bot.keyboards.keyboards import create_keyboard_for_ban_reason
|
from helper_bot.keyboards.keyboards import create_keyboard_for_ban_reason
|
||||||
from helper_bot.utils.helper_func import (
|
from helper_bot.utils.helper_func import (
|
||||||
delete_user_blacklist,
|
delete_user_blacklist,
|
||||||
get_text_message,
|
get_publish_text,
|
||||||
send_audio_message,
|
send_audio_message,
|
||||||
send_media_group_to_channel,
|
send_media_group_to_channel,
|
||||||
send_photo_message,
|
send_photo_message,
|
||||||
@@ -137,7 +137,7 @@ class PostPublishService:
|
|||||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||||
|
|
||||||
# Формируем финальный текст с учетом is_anonymous
|
# Формируем финальный текст с учетом is_anonymous
|
||||||
formatted_text = get_text_message(
|
formatted_text = get_publish_text(
|
||||||
raw_text, user.first_name, user.username, is_anonymous
|
raw_text, user.first_name, user.username, is_anonymous
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ class PostPublishService:
|
|||||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||||
|
|
||||||
# Формируем финальный текст с учетом is_anonymous
|
# Формируем финальный текст с учетом is_anonymous
|
||||||
formatted_text = get_text_message(
|
formatted_text = get_publish_text(
|
||||||
raw_text, user.first_name, user.username, is_anonymous
|
raw_text, user.first_name, user.username, is_anonymous
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ class PostPublishService:
|
|||||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||||
|
|
||||||
# Формируем финальный текст с учетом is_anonymous
|
# Формируем финальный текст с учетом is_anonymous
|
||||||
formatted_text = get_text_message(
|
formatted_text = get_publish_text(
|
||||||
raw_text, user.first_name, user.username, is_anonymous
|
raw_text, user.first_name, user.username, is_anonymous
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -340,7 +340,7 @@ class PostPublishService:
|
|||||||
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
raise PostNotFoundError(f"Пользователь {author_id} не найден в базе данных")
|
||||||
|
|
||||||
# Формируем финальный текст с учетом is_anonymous
|
# Формируем финальный текст с учетом is_anonymous
|
||||||
formatted_text = get_text_message(
|
formatted_text = get_publish_text(
|
||||||
raw_text, user.first_name, user.username, is_anonymous
|
raw_text, user.first_name, user.username, is_anonymous
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -452,7 +452,7 @@ class PostPublishService:
|
|||||||
f"Пользователь {author_id} не найден в базе данных"
|
f"Пользователь {author_id} не найден в базе данных"
|
||||||
)
|
)
|
||||||
|
|
||||||
formatted_text = get_text_message(
|
formatted_text = get_publish_text(
|
||||||
raw_text, user.first_name, user.username, is_anonymous
|
raw_text, user.first_name, user.username, is_anonymous
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -838,7 +838,7 @@ class BanService:
|
|||||||
await self.db.set_user_blacklist(
|
await self.db.set_user_blacklist(
|
||||||
user_id=author_id,
|
user_id=author_id,
|
||||||
user_name=None,
|
user_name=None,
|
||||||
message_for_user="Спам",
|
message_for_user="Последний пост",
|
||||||
date_to_unban=date_to_unban,
|
date_to_unban=date_to_unban,
|
||||||
ban_author=ban_author_id,
|
ban_author=ban_author_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -291,12 +291,33 @@ class PrivateHandlers:
|
|||||||
"""Handle messages in admin chat states"""
|
"""Handle messages in admin chat states"""
|
||||||
# User service operations with metrics
|
# User service operations with metrics
|
||||||
await self.user_service.update_user_activity(message.from_user.id)
|
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()
|
current_date = datetime.now()
|
||||||
date = int(current_date.timestamp())
|
date = int(current_date.timestamp())
|
||||||
|
|
||||||
|
# Сохраняем message_id из результата send_message
|
||||||
await self.db.add_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")
|
question = messages.get_message(get_first_name(message), "QUESTION")
|
||||||
|
|||||||
@@ -156,6 +156,92 @@ class UserService:
|
|||||||
username = message.from_user.username or "Без никнейма"
|
username = message.from_user.username or "Без никнейма"
|
||||||
return html.escape(full_name), html.escape(username)
|
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:
|
class PostService:
|
||||||
"""Service for post-related operations"""
|
"""Service for post-related operations"""
|
||||||
@@ -236,6 +322,18 @@ class PostService:
|
|||||||
f"PostService: Ошибка сохранения скоров для {message_id}: {e}"
|
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:
|
async def _get_scores_with_error_handling(self, text: str) -> tuple:
|
||||||
"""
|
"""
|
||||||
Получает скоры для текста поста с обработкой ошибок.
|
Получает скоры для текста поста с обработкой ошибок.
|
||||||
@@ -321,6 +419,37 @@ class PostService:
|
|||||||
error_message,
|
error_message,
|
||||||
) = await self._get_scores_with_error_handling(original_raw_text)
|
) = 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
|
text_for_post = original_raw_text
|
||||||
if error_message:
|
if error_message:
|
||||||
@@ -347,9 +476,11 @@ class PostService:
|
|||||||
message.from_user.username,
|
message.from_user.username,
|
||||||
deepseek_score=deepseek_score,
|
deepseek_score=deepseek_score,
|
||||||
rag_score=rag_score,
|
rag_score=rag_score,
|
||||||
rag_confidence=rag_confidence,
|
user_id=message.from_user.id,
|
||||||
rag_score_pos_only=rag_score_pos_only,
|
|
||||||
)
|
)
|
||||||
|
# Добавляем предупреждение о похожем посте
|
||||||
|
if similar_warning:
|
||||||
|
post_text += similar_warning
|
||||||
|
|
||||||
# Определяем анонимность по исходному тексту (без сообщения об ошибке)
|
# Определяем анонимность по исходному тексту (без сообщения об ошибке)
|
||||||
is_anonymous = determine_anonymity(original_raw_text)
|
is_anonymous = determine_anonymity(original_raw_text)
|
||||||
@@ -401,8 +532,11 @@ class PostService:
|
|||||||
markup,
|
markup,
|
||||||
)
|
)
|
||||||
elif content_type == "media_group":
|
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(
|
await self._process_media_group_background(
|
||||||
message,
|
message,
|
||||||
album,
|
album,
|
||||||
@@ -411,6 +545,7 @@ class PostService:
|
|||||||
is_anonymous,
|
is_anonymous,
|
||||||
original_raw_text,
|
original_raw_text,
|
||||||
ml_scores_json,
|
ml_scores_json,
|
||||||
|
rag_score,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
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:
|
except Exception as e:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"PostService: Критическая ошибка в _process_post_background для {content_type}: {e}"
|
f"PostService: Критическая ошибка в _process_post_background для {content_type}: {e}"
|
||||||
@@ -462,6 +605,7 @@ class PostService:
|
|||||||
is_anonymous: bool,
|
is_anonymous: bool,
|
||||||
original_raw_text: str,
|
original_raw_text: str,
|
||||||
ml_scores_json: str = None,
|
ml_scores_json: str = None,
|
||||||
|
rag_score: float = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Обрабатывает медиагруппу в фоне"""
|
"""Обрабатывает медиагруппу в фоне"""
|
||||||
try:
|
try:
|
||||||
@@ -495,6 +639,14 @@ class PostService:
|
|||||||
self._save_scores_background(main_post_id, ml_scores_json)
|
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:
|
for msg_id in media_group_message_ids:
|
||||||
await self.db.add_message_link(main_post_id, msg_id)
|
await self.db.add_message_link(main_post_id, msg_id)
|
||||||
|
|
||||||
@@ -552,8 +704,7 @@ class PostService:
|
|||||||
message.from_user.username,
|
message.from_user.username,
|
||||||
deepseek_score=deepseek_score,
|
deepseek_score=deepseek_score,
|
||||||
rag_score=rag_score,
|
rag_score=rag_score,
|
||||||
rag_confidence=rag_confidence,
|
user_id=message.from_user.id,
|
||||||
rag_score_pos_only=rag_score_pos_only,
|
|
||||||
)
|
)
|
||||||
markup = get_reply_keyboard_for_post()
|
markup = get_reply_keyboard_for_post()
|
||||||
|
|
||||||
@@ -611,8 +762,7 @@ class PostService:
|
|||||||
message.from_user.username,
|
message.from_user.username,
|
||||||
deepseek_score=deepseek_score,
|
deepseek_score=deepseek_score,
|
||||||
rag_score=rag_score,
|
rag_score=rag_score,
|
||||||
rag_confidence=rag_confidence,
|
user_id=message.from_user.id,
|
||||||
rag_score_pos_only=rag_score_pos_only,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
markup = get_reply_keyboard_for_post()
|
markup = get_reply_keyboard_for_post()
|
||||||
@@ -677,8 +827,7 @@ class PostService:
|
|||||||
message.from_user.username,
|
message.from_user.username,
|
||||||
deepseek_score=deepseek_score,
|
deepseek_score=deepseek_score,
|
||||||
rag_score=rag_score,
|
rag_score=rag_score,
|
||||||
rag_confidence=rag_confidence,
|
user_id=message.from_user.id,
|
||||||
rag_score_pos_only=rag_score_pos_only,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
markup = get_reply_keyboard_for_post()
|
markup = get_reply_keyboard_for_post()
|
||||||
@@ -770,8 +919,7 @@ class PostService:
|
|||||||
message.from_user.username,
|
message.from_user.username,
|
||||||
deepseek_score=deepseek_score,
|
deepseek_score=deepseek_score,
|
||||||
rag_score=rag_score,
|
rag_score=rag_score,
|
||||||
rag_confidence=rag_confidence,
|
user_id=message.from_user.id,
|
||||||
rag_score_pos_only=rag_score_pos_only,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
markup = get_reply_keyboard_for_post()
|
markup = get_reply_keyboard_for_post()
|
||||||
@@ -869,8 +1017,7 @@ class PostService:
|
|||||||
message.from_user.username,
|
message.from_user.username,
|
||||||
deepseek_score=deepseek_score,
|
deepseek_score=deepseek_score,
|
||||||
rag_score=rag_score,
|
rag_score=rag_score,
|
||||||
rag_confidence=rag_confidence,
|
user_id=message.from_user.id,
|
||||||
rag_score_pos_only=rag_score_pos_only,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
is_anonymous = determine_anonymity(raw_caption)
|
is_anonymous = determine_anonymity(raw_caption)
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ HTTP клиент для взаимодействия с внешним RAG се
|
|||||||
Использует REST API для получения скоров и отправки примеров.
|
Использует REST API для получения скоров и отправки примеров.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -15,6 +16,30 @@ from .base import ScoringResult
|
|||||||
from .exceptions import InsufficientExamplesError, ScoringError, TextTooShortError
|
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:
|
class RagApiClient:
|
||||||
"""
|
"""
|
||||||
HTTP клиент для взаимодействия с внешним RAG сервисом.
|
HTTP клиент для взаимодействия с внешним RAG сервисом.
|
||||||
@@ -329,21 +354,39 @@ class RagApiClient:
|
|||||||
Словарь со статистикой или пустой словарь при ошибке
|
Словарь со статистикой или пустой словарь при ошибке
|
||||||
"""
|
"""
|
||||||
if not self._enabled:
|
if not self._enabled:
|
||||||
|
logger.debug("RagApiClient: get_stats пропущен - клиент отключен")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
logger.debug(f"RagApiClient: Запрос статистики от {self.api_url}/stats")
|
||||||
response = await self._client.get(f"{self.api_url}/stats")
|
response = await self._client.get(f"{self.api_url}/stats")
|
||||||
|
|
||||||
if response.status_code == 200:
|
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:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"RagApiClient: Неожиданный статус при получении статистики: {response.status_code}"
|
f"RagApiClient: Неожиданный статус при получении статистики: "
|
||||||
|
f"status={response.status_code}, body={response.text[:200]}"
|
||||||
)
|
)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
logger.warning(f"RagApiClient: Таймаут при получении статистики")
|
logger.warning(
|
||||||
|
f"RagApiClient: Таймаут при получении статистики (timeout={self.timeout}s)"
|
||||||
|
)
|
||||||
return {}
|
return {}
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -365,3 +408,135 @@ class RagApiClient:
|
|||||||
"api_url": self.api_url,
|
"api_url": self.api_url,
|
||||||
"timeout": self.timeout,
|
"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
|
||||||
|
|||||||
@@ -221,3 +221,43 @@ class ScoringManager:
|
|||||||
stats["deepseek"] = self.deepseek_service.get_stats()
|
stats["deepseek"] = self.deepseek_service.get_stats()
|
||||||
|
|
||||||
return 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)
|
||||||
|
|||||||
@@ -138,6 +138,52 @@ def determine_anonymity(post_text: str) -> bool:
|
|||||||
return False
|
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(
|
def get_text_message(
|
||||||
post_text: str,
|
post_text: str,
|
||||||
first_name: str,
|
first_name: str,
|
||||||
@@ -147,10 +193,10 @@ def get_text_message(
|
|||||||
rag_score: Optional[float] = None,
|
rag_score: Optional[float] = None,
|
||||||
rag_confidence: Optional[float] = None,
|
rag_confidence: Optional[float] = None,
|
||||||
rag_score_pos_only: Optional[float] = None,
|
rag_score_pos_only: Optional[float] = None,
|
||||||
|
user_id: Optional[int] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Форматирует текст сообщения для публикации в зависимости от наличия ключевых слов "анон" и "неанон"
|
Форматирует текст сообщения для модерации (с полной информацией об авторе и скорами).
|
||||||
или переданного параметра is_anonymous.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
post_text: Текст сообщения
|
post_text: Текст сообщения
|
||||||
@@ -161,64 +207,69 @@ def get_text_message(
|
|||||||
rag_score: Скор от RAG/ruBERT neg/pos (0.0-1.0, опционально)
|
rag_score: Скор от RAG/ruBERT neg/pos (0.0-1.0, опционально)
|
||||||
rag_confidence: Уверенность RAG модели (0.0-1.0, зависит от количества примеров)
|
rag_confidence: Уверенность RAG модели (0.0-1.0, зависит от количества примеров)
|
||||||
rag_score_pos_only: Скор RAG только по положительным примерам (0.0-1.0, опционально)
|
rag_score_pos_only: Скор RAG только по положительным примерам (0.0-1.0, опционально)
|
||||||
|
user_id: ID пользователя Telegram (опционально)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: - Сформированный текст сообщения.
|
str: - Сформированный текст сообщения для модерации.
|
||||||
"""
|
"""
|
||||||
# Экранируем post_text для безопасного использования в HTML
|
# Экранируем post_text для безопасного использования в HTML
|
||||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
||||||
|
|
||||||
# Экранируем username для безопасного использования в HTML
|
# Экранируем username для безопасного использования в HTML
|
||||||
safe_username = html.escape(username) if username else None
|
safe_username = html.escape(username) if username else None
|
||||||
|
safe_first_name = html.escape(first_name) if first_name else "Пользователь"
|
||||||
|
|
||||||
# Формируем строку с информацией об авторе
|
# Формируем шапку с информацией об авторе
|
||||||
if safe_username:
|
if safe_username:
|
||||||
author_info = f"{first_name} @{safe_username}"
|
header = f"👤 От: {safe_first_name} (@{safe_username})"
|
||||||
else:
|
else:
|
||||||
author_info = f"{first_name} (Ник не указан)"
|
header = f"👤 От: {safe_first_name} (Ник не указан)"
|
||||||
|
|
||||||
# Формируем базовый текст
|
if user_id:
|
||||||
# Если передан is_anonymous, используем его, иначе определяем по тексту (legacy)
|
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 is not None:
|
||||||
if is_anonymous:
|
if is_anonymous:
|
||||||
final_text = f"{safe_post_text}\n\nПост опубликован анонимно"
|
post_block += f"\n\nПост опубликован анонимно"
|
||||||
else:
|
else:
|
||||||
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
|
post_block += f"\n\n<b>Автор поста:</b> {author_info}"
|
||||||
else:
|
else:
|
||||||
# Legacy: определяем по тексту
|
# Legacy: определяем по тексту
|
||||||
if "неанон" in post_text or "не анон" in post_text:
|
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:
|
elif "анон" in post_text:
|
||||||
final_text = f"{safe_post_text}\n\nПост опубликован анонимно"
|
post_block += f"\n\nПост опубликован анонимно"
|
||||||
else:
|
else:
|
||||||
final_text = f"{safe_post_text}\n\nАвтор поста: {author_info}"
|
post_block += f"\n\n<b>Автор поста:</b> {author_info}"
|
||||||
|
|
||||||
# Добавляем блок со скорами если есть
|
post_block += f"\n{separator}"
|
||||||
if (
|
|
||||||
deepseek_score is not None
|
# Добавляем блок со скорами если есть (без RAG pos only и уверенности)
|
||||||
or rag_score is not None
|
if deepseek_score is not None or rag_score is not None:
|
||||||
or rag_score_pos_only is not None
|
scores_lines = ["📊 <b>Уверенность в одобрении:</b>"]
|
||||||
):
|
|
||||||
scores_lines = ["\n📊 Уверенность в одобрении:"]
|
|
||||||
if deepseek_score is not None:
|
if deepseek_score is not None:
|
||||||
scores_lines.append(f"DeepSeek: {deepseek_score:.2f}")
|
scores_lines.append(f"DeepSeek: {deepseek_score:.2f}")
|
||||||
if rag_score is not None:
|
if rag_score is not None:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"get_text_message: Форматирование rag_score - "
|
f"get_text_message: Форматирование rag_score - "
|
||||||
f"rag_score={rag_score} (type: {type(rag_score).__name__}), "
|
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}"
|
f"formatted_value={rag_score:.2f}"
|
||||||
)
|
)
|
||||||
rag_line = f"RAG neg/pos: {rag_score:.2f}"
|
scores_lines.append(f"RAG neg/pos: {rag_score:.2f}")
|
||||||
if rag_confidence is not None:
|
post_block += "\n" + "\n".join(scores_lines)
|
||||||
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)
|
|
||||||
|
|
||||||
return final_text
|
return post_block
|
||||||
|
|
||||||
|
|
||||||
@track_time("download_file", "helper_func")
|
@track_time("download_file", "helper_func")
|
||||||
@@ -854,15 +905,14 @@ async def send_text_message(
|
|||||||
):
|
):
|
||||||
from .rate_limiter import send_with_rate_limit
|
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():
|
async def _send_message():
|
||||||
if markup is None:
|
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:
|
else:
|
||||||
return await message.bot.send_message(
|
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)
|
sent_message = await send_with_rate_limit(_send_message, chat_id)
|
||||||
@@ -878,16 +928,17 @@ async def send_photo_message(
|
|||||||
post_text: str,
|
post_text: str,
|
||||||
markup: types.ReplyKeyboardMarkup = None,
|
markup: types.ReplyKeyboardMarkup = None,
|
||||||
):
|
):
|
||||||
# Экранируем post_text для безопасного использования в HTML
|
|
||||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
|
||||||
|
|
||||||
if markup is None:
|
if markup is None:
|
||||||
sent_message = await message.bot.send_photo(
|
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:
|
else:
|
||||||
sent_message = await message.bot.send_photo(
|
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
|
return sent_message
|
||||||
|
|
||||||
@@ -901,16 +952,17 @@ async def send_video_message(
|
|||||||
post_text: str = "",
|
post_text: str = "",
|
||||||
markup: types.ReplyKeyboardMarkup = None,
|
markup: types.ReplyKeyboardMarkup = None,
|
||||||
):
|
):
|
||||||
# Экранируем post_text для безопасного использования в HTML
|
|
||||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
|
||||||
|
|
||||||
if markup is None:
|
if markup is None:
|
||||||
sent_message = await message.bot.send_video(
|
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:
|
else:
|
||||||
sent_message = await message.bot.send_video(
|
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
|
return sent_message
|
||||||
|
|
||||||
@@ -943,16 +995,17 @@ async def send_audio_message(
|
|||||||
post_text: str,
|
post_text: str,
|
||||||
markup: types.ReplyKeyboardMarkup = None,
|
markup: types.ReplyKeyboardMarkup = None,
|
||||||
):
|
):
|
||||||
# Экранируем post_text для безопасного использования в HTML
|
|
||||||
safe_post_text = html.escape(str(post_text)) if post_text else ""
|
|
||||||
|
|
||||||
if markup is None:
|
if markup is None:
|
||||||
sent_message = await message.bot.send_audio(
|
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:
|
else:
|
||||||
sent_message = await message.bot.send_audio(
|
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
|
return sent_message
|
||||||
|
|
||||||
@@ -1012,11 +1065,12 @@ async def get_banned_users_list(offset: int, bot_db):
|
|||||||
message - текст сообщения
|
message - текст сообщения
|
||||||
user_ids - лист кортежей [(user_name: user_id)]
|
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"
|
message = "Список заблокированных пользователей:\n"
|
||||||
|
|
||||||
for user in users:
|
for user in users:
|
||||||
user_id, ban_reason, unban_date = user
|
user_id, ban_reason, unban_date, ban_date = user
|
||||||
# Получаем имя пользователя из таблицы users
|
# Получаем имя пользователя из таблицы users
|
||||||
username = await bot_db.get_username(user_id)
|
username = await bot_db.get_username(user_id)
|
||||||
full_name = await bot_db.get_full_name_by_id(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 "Причина не указана"
|
html.escape(str(ban_reason)) if ban_reason else "Причина не указана"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Форматируем дату разбана в человекочитаемый формат
|
# Форматируем дату бана в человекочитаемый формат
|
||||||
if unban_date:
|
safe_ban_date = _format_timestamp_to_date(ban_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 = "Дата не указана"
|
|
||||||
|
|
||||||
message += f"**Пользователь:** {safe_user_name}\n"
|
# Форматируем дату разбана в человекочитаемый формат
|
||||||
message += f"**Причина бана:** {safe_ban_reason}\n"
|
safe_unban_date = _format_timestamp_to_date(unban_date, default="Навсегда")
|
||||||
message += f"**Дата разбана:** {safe_unban_date}\n\n"
|
|
||||||
|
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
|
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_time("get_banned_users_buttons", "helper_func")
|
||||||
@track_errors("helper_func", "get_banned_users_buttons")
|
@track_errors("helper_func", "get_banned_users_buttons")
|
||||||
@db_query_time("get_banned_users_buttons", "users", "select")
|
@db_query_time("get_banned_users_buttons", "users", "select")
|
||||||
|
|||||||
@@ -274,9 +274,9 @@ class TestBlacklistRepository:
|
|||||||
|
|
||||||
# Нормализуем SQL запрос (убираем лишние пробелы и переносы строк)
|
# Нормализуем SQL запрос (убираем лишние пробелы и переносы строк)
|
||||||
actual_query = " ".join(call_args[0][0].split())
|
actual_query = " ".join(call_args[0][0].split())
|
||||||
expected_query = "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist LIMIT ?, ?"
|
expected_query = "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist ORDER BY created_at DESC LIMIT ? OFFSET ?"
|
||||||
assert actual_query == expected_query
|
assert actual_query == expected_query
|
||||||
assert call_args[0][1] == (0, 10)
|
assert call_args[0][1] == (10, 0)
|
||||||
|
|
||||||
# Проверяем логирование
|
# Проверяем логирование
|
||||||
blacklist_repository.logger.info.assert_called_once_with(
|
blacklist_repository.logger.info.assert_called_once_with(
|
||||||
@@ -310,7 +310,7 @@ class TestBlacklistRepository:
|
|||||||
|
|
||||||
# Нормализуем SQL запрос (убираем лишние пробелы и переносы строк)
|
# Нормализуем SQL запрос (убираем лишние пробелы и переносы строк)
|
||||||
actual_query = " ".join(call_args[0][0].split())
|
actual_query = " ".join(call_args[0][0].split())
|
||||||
expected_query = "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist"
|
expected_query = "SELECT user_id, message_for_user, date_to_unban, created_at, ban_author FROM blacklist ORDER BY created_at DESC"
|
||||||
assert actual_query == expected_query
|
assert actual_query == expected_query
|
||||||
# Проверяем, что параметры пустые (без лимитов)
|
# Проверяем, что параметры пустые (без лимитов)
|
||||||
assert len(call_args[0]) == 1 # Только SQL запрос, без параметров
|
assert len(call_args[0]) == 1 # Только SQL запрос, без параметров
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class TestPostPublishService:
|
|||||||
return call
|
return call
|
||||||
|
|
||||||
@patch("helper_bot.handlers.callback.services.send_text_message")
|
@patch("helper_bot.handlers.callback.services.send_text_message")
|
||||||
@patch("helper_bot.handlers.callback.services.get_text_message")
|
@patch("helper_bot.handlers.callback.services.get_publish_text")
|
||||||
async def test_publish_post_text_success(
|
async def test_publish_post_text_success(
|
||||||
self, mock_get_text, mock_send_text, service, mock_call_text, mock_db
|
self, mock_get_text, mock_send_text, service, mock_call_text, mock_db
|
||||||
):
|
):
|
||||||
@@ -214,7 +214,7 @@ class TestPostPublishService:
|
|||||||
|
|
||||||
@patch("helper_bot.handlers.callback.services.send_photo_message")
|
@patch("helper_bot.handlers.callback.services.send_photo_message")
|
||||||
@patch("helper_bot.handlers.callback.services.send_text_message")
|
@patch("helper_bot.handlers.callback.services.send_text_message")
|
||||||
@patch("helper_bot.handlers.callback.services.get_text_message")
|
@patch("helper_bot.handlers.callback.services.get_publish_text")
|
||||||
async def test_publish_post_photo_success(
|
async def test_publish_post_photo_success(
|
||||||
self, mock_get_text, mock_send_text, mock_send_photo, service, mock_db
|
self, mock_get_text, mock_send_text, mock_send_photo, service, mock_db
|
||||||
):
|
):
|
||||||
@@ -239,7 +239,7 @@ class TestPostPublishService:
|
|||||||
|
|
||||||
@patch("helper_bot.handlers.callback.services.send_video_message")
|
@patch("helper_bot.handlers.callback.services.send_video_message")
|
||||||
@patch("helper_bot.handlers.callback.services.send_text_message")
|
@patch("helper_bot.handlers.callback.services.send_text_message")
|
||||||
@patch("helper_bot.handlers.callback.services.get_text_message")
|
@patch("helper_bot.handlers.callback.services.get_publish_text")
|
||||||
async def test_publish_post_video_success(
|
async def test_publish_post_video_success(
|
||||||
self, mock_get_text, mock_send_text, mock_send_video, service, mock_db
|
self, mock_get_text, mock_send_text, mock_send_video, service, mock_db
|
||||||
):
|
):
|
||||||
@@ -285,7 +285,7 @@ class TestPostPublishService:
|
|||||||
|
|
||||||
@patch("helper_bot.handlers.callback.services.send_audio_message")
|
@patch("helper_bot.handlers.callback.services.send_audio_message")
|
||||||
@patch("helper_bot.handlers.callback.services.send_text_message")
|
@patch("helper_bot.handlers.callback.services.send_text_message")
|
||||||
@patch("helper_bot.handlers.callback.services.get_text_message")
|
@patch("helper_bot.handlers.callback.services.get_publish_text")
|
||||||
async def test_publish_post_audio_success(
|
async def test_publish_post_audio_success(
|
||||||
self, mock_get_text, mock_send_text, mock_send_audio, service, mock_db
|
self, mock_get_text, mock_send_text, mock_send_audio, service, mock_db
|
||||||
):
|
):
|
||||||
@@ -499,7 +499,7 @@ class TestPostPublishService:
|
|||||||
|
|
||||||
@patch("helper_bot.handlers.callback.services.send_media_group_to_channel")
|
@patch("helper_bot.handlers.callback.services.send_media_group_to_channel")
|
||||||
@patch("helper_bot.handlers.callback.services.send_text_message")
|
@patch("helper_bot.handlers.callback.services.send_text_message")
|
||||||
@patch("helper_bot.handlers.callback.services.get_text_message")
|
@patch("helper_bot.handlers.callback.services.get_publish_text")
|
||||||
async def test_publish_media_group_success(
|
async def test_publish_media_group_success(
|
||||||
self, mock_get_text, mock_send_text, mock_send_media, service, mock_db
|
self, mock_get_text, mock_send_text, mock_send_media, service, mock_db
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ class TestPrivateHandlers:
|
|||||||
db.add_message = AsyncMock()
|
db.add_message = AsyncMock()
|
||||||
db.update_helper_message = AsyncMock()
|
db.update_helper_message = AsyncMock()
|
||||||
db.update_user_activity = AsyncMock()
|
db.update_user_activity = AsyncMock()
|
||||||
|
db.get_user_posts_stats = AsyncMock(return_value=(5, 2, 3))
|
||||||
|
db.get_last_post_by_author = AsyncMock(return_value="Last post text")
|
||||||
|
db.get_user_by_id = AsyncMock(return_value=Mock(date_added=1704067200))
|
||||||
|
db.get_user_ban_count = AsyncMock(return_value=0)
|
||||||
|
db.get_last_ban_info = AsyncMock(return_value=None)
|
||||||
return db
|
return db
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -257,6 +262,7 @@ class TestPrivateHandlers:
|
|||||||
"""resend_message_in_group при PRE_CHAT переводит в START и отправляет question."""
|
"""resend_message_in_group при PRE_CHAT переводит в START и отправляет question."""
|
||||||
handlers = create_private_handlers(mock_db, mock_settings)
|
handlers = create_private_handlers(mock_db, mock_settings)
|
||||||
mock_state.get_state = AsyncMock(return_value=FSM_STATES["PRE_CHAT"])
|
mock_state.get_state = AsyncMock(return_value=FSM_STATES["PRE_CHAT"])
|
||||||
|
mock_message.bot.send_message = AsyncMock(return_value=Mock(message_id=100))
|
||||||
with pytest.MonkeyPatch().context() as m:
|
with pytest.MonkeyPatch().context() as m:
|
||||||
m.setattr(
|
m.setattr(
|
||||||
"helper_bot.handlers.private.private_handlers.get_reply_keyboard",
|
"helper_bot.handlers.private.private_handlers.get_reply_keyboard",
|
||||||
@@ -267,9 +273,7 @@ class TestPrivateHandlers:
|
|||||||
lambda x, y: "Question?",
|
lambda x, y: "Question?",
|
||||||
)
|
)
|
||||||
await handlers.resend_message_in_group_for_message(mock_message, mock_state)
|
await handlers.resend_message_in_group_for_message(mock_message, mock_state)
|
||||||
mock_message.forward.assert_called_once_with(
|
mock_message.bot.send_message.assert_called_once()
|
||||||
chat_id=mock_settings.group_for_message
|
|
||||||
)
|
|
||||||
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
mock_state.set_state.assert_called_once_with(FSM_STATES["START"])
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -279,6 +283,7 @@ class TestPrivateHandlers:
|
|||||||
"""resend_message_in_group при CHAT оставляет в CHAT и отправляет question с leave markup."""
|
"""resend_message_in_group при CHAT оставляет в CHAT и отправляет question с leave markup."""
|
||||||
handlers = create_private_handlers(mock_db, mock_settings)
|
handlers = create_private_handlers(mock_db, mock_settings)
|
||||||
mock_state.get_state = AsyncMock(return_value=FSM_STATES["CHAT"])
|
mock_state.get_state = AsyncMock(return_value=FSM_STATES["CHAT"])
|
||||||
|
mock_message.bot.send_message = AsyncMock(return_value=Mock(message_id=100))
|
||||||
with pytest.MonkeyPatch().context() as m:
|
with pytest.MonkeyPatch().context() as m:
|
||||||
m.setattr(
|
m.setattr(
|
||||||
"helper_bot.handlers.private.private_handlers.get_reply_keyboard_leave_chat",
|
"helper_bot.handlers.private.private_handlers.get_reply_keyboard_leave_chat",
|
||||||
@@ -289,9 +294,7 @@ class TestPrivateHandlers:
|
|||||||
lambda x, y: "Question?",
|
lambda x, y: "Question?",
|
||||||
)
|
)
|
||||||
await handlers.resend_message_in_group_for_message(mock_message, mock_state)
|
await handlers.resend_message_in_group_for_message(mock_message, mock_state)
|
||||||
mock_message.forward.assert_called_once_with(
|
mock_message.bot.send_message.assert_called_once()
|
||||||
chat_id=mock_settings.group_for_message
|
|
||||||
)
|
|
||||||
mock_message.answer.assert_called()
|
mock_message.answer.assert_called()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -665,7 +665,7 @@ class TestSendMessageFunctions:
|
|||||||
|
|
||||||
assert result == mock_sent_message
|
assert result == mock_sent_message
|
||||||
mock_message.bot.send_photo.assert_called_once_with(
|
mock_message.bot.send_photo.assert_called_once_with(
|
||||||
chat_id=123, caption="Подпись к фото", photo="photo.jpg"
|
chat_id=123, caption="Подпись к фото", photo="photo.jpg", parse_mode="HTML"
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -684,7 +684,7 @@ class TestSendMessageFunctions:
|
|||||||
|
|
||||||
assert result == mock_sent_message
|
assert result == mock_sent_message
|
||||||
mock_message.bot.send_video.assert_called_once_with(
|
mock_message.bot.send_video.assert_called_once_with(
|
||||||
chat_id=123, caption="Подпись к видео", video="video.mp4"
|
chat_id=123, caption="Подпись к видео", video="video.mp4", parse_mode="HTML"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -722,8 +722,9 @@ class TestUtilityFunctions:
|
|||||||
"""Тест получения списка заблокированных пользователей"""
|
"""Тест получения списка заблокированных пользователей"""
|
||||||
mock_db = AsyncMock()
|
mock_db = AsyncMock()
|
||||||
mock_db.get_banned_users_from_db_with_limits.return_value = [
|
mock_db.get_banned_users_from_db_with_limits.return_value = [
|
||||||
(123, "Spam", 1704067200), # user_id, ban_reason, unban_date (timestamp)
|
# user_id, ban_reason, unban_date (timestamp), ban_date (timestamp)
|
||||||
(456, "Violation", 1704153600),
|
(123, "Spam", 1704067200, 1703980800),
|
||||||
|
(456, "Violation", 1704153600, 1704067200),
|
||||||
]
|
]
|
||||||
mock_db.get_username.return_value = None
|
mock_db.get_username.return_value = None
|
||||||
mock_db.get_full_name_by_id.return_value = "Test User"
|
mock_db.get_full_name_by_id.return_value = "Test User"
|
||||||
@@ -734,18 +735,16 @@ class TestUtilityFunctions:
|
|||||||
assert "Test User" in result
|
assert "Test User" in result
|
||||||
assert "Spam" in result
|
assert "Spam" in result
|
||||||
assert "Violation" in result
|
assert "Violation" in result
|
||||||
|
assert "<b>Дата бана:</b>" in result
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_banned_users_list_with_string_timestamp(self):
|
async def test_get_banned_users_list_with_string_timestamp(self):
|
||||||
"""Тест получения списка заблокированных пользователей со строковым timestamp"""
|
"""Тест получения списка заблокированных пользователей со строковым timestamp"""
|
||||||
mock_db = AsyncMock()
|
mock_db = AsyncMock()
|
||||||
mock_db.get_banned_users_from_db_with_limits.return_value = [
|
mock_db.get_banned_users_from_db_with_limits.return_value = [
|
||||||
(
|
# user_id, ban_reason, unban_date (string timestamp), ban_date (string timestamp)
|
||||||
123,
|
(123, "Spam", "1704067200", "1703980800"),
|
||||||
"Spam",
|
(456, "Violation", "1704153600", "1704067200"),
|
||||||
"1704067200",
|
|
||||||
), # user_id, ban_reason, unban_date (string timestamp)
|
|
||||||
(456, "Violation", "1704153600"),
|
|
||||||
]
|
]
|
||||||
mock_db.get_username.return_value = None
|
mock_db.get_username.return_value = None
|
||||||
mock_db.get_full_name_by_id.return_value = "Test User"
|
mock_db.get_full_name_by_id.return_value = "Test User"
|
||||||
@@ -756,6 +755,7 @@ class TestUtilityFunctions:
|
|||||||
assert "Test User" in result
|
assert "Test User" in result
|
||||||
assert "Spam" in result
|
assert "Spam" in result
|
||||||
assert "Violation" in result
|
assert "Violation" in result
|
||||||
|
assert "<b>Дата бана:</b>" in result
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_banned_users_buttons(self):
|
async def test_get_banned_users_buttons(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user