Release Notes: dev-12 #14
@@ -617,6 +617,20 @@ class BanService:
|
||||
ban_author=ban_author_id,
|
||||
)
|
||||
|
||||
# Обновляем статус поста на declined
|
||||
if call.message.text == CONTENT_TYPE_MEDIA_GROUP:
|
||||
# Для медиагруппы обновляем статус по helper_message_id
|
||||
updated_rows = await self.db.update_status_for_media_group_by_helper_id(
|
||||
call.message.message_id, "declined"
|
||||
)
|
||||
if updated_rows == 0:
|
||||
logger.warning(f"Не удалось обновить статус медиагруппы helper_message_id={call.message.message_id} на 'declined'")
|
||||
else:
|
||||
# Для одиночного поста обновляем статус по message_id
|
||||
updated_rows = await self.db.update_status_by_message_id(call.message.message_id, "declined")
|
||||
if updated_rows == 0:
|
||||
logger.warning(f"Не удалось обновить статус поста message_id={call.message.message_id} на 'declined'")
|
||||
|
||||
await self._get_bot(call.message).delete_message(chat_id=self.group_for_posts, message_id=call.message.message_id)
|
||||
|
||||
date_str = (current_date + timedelta(days=7)).strftime("%d.%m.%Y %H:%M")
|
||||
|
||||
@@ -147,43 +147,39 @@ class PrivateHandlers:
|
||||
@track_errors("private_handlers", "suggest_router")
|
||||
@track_time("suggest_router", "private_handlers")
|
||||
async def suggest_router(self, message: types.Message, state: FSMContext, album: list = None, **kwargs):
|
||||
"""Handle post submission in suggest state"""
|
||||
"""Handle post submission in suggest state - сразу отвечает пользователю, обработка в фоне"""
|
||||
# Сразу отвечаем пользователю
|
||||
markup_for_user = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE')
|
||||
await message.answer(success_send_message, reply_markup=markup_for_user)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
|
||||
# Проверяем, есть ли механизм для получения полной медиагруппы (для медиагрупп)
|
||||
album_getter = kwargs.get("album_getter")
|
||||
|
||||
if album_getter and message.media_group_id:
|
||||
# Это медиагруппа - сразу отвечаем пользователю, обработку делаем в фоне
|
||||
markup_for_user = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE')
|
||||
await message.answer(success_send_message, reply_markup=markup_for_user)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
# В фоне обрабатываем пост
|
||||
async def process_post_background():
|
||||
try:
|
||||
# Обновляем активность пользователя
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
|
||||
# В фоне ждем полную медиагруппу и обрабатываем пост
|
||||
async def process_media_group_background():
|
||||
try:
|
||||
# Ждем полную медиагруппу
|
||||
# Логируем сообщение (только для одиночных сообщений, не медиагрупп)
|
||||
if message.media_group_id is None:
|
||||
await self.user_service.log_user_message(message)
|
||||
|
||||
# Для медиагрупп ждем полную медиагруппу
|
||||
if album_getter and message.media_group_id:
|
||||
full_album = await album_getter.get_album(timeout=10.0)
|
||||
if not full_album:
|
||||
return
|
||||
if full_album:
|
||||
await self.post_service.process_post(message, full_album)
|
||||
else:
|
||||
# Обычное сообщение или медиагруппа уже собрана
|
||||
await self.post_service.process_post(message, album)
|
||||
except Exception as e:
|
||||
from logs.custom_logger import logger
|
||||
logger.error(f"Ошибка при фоновой обработке поста: {e}")
|
||||
|
||||
# Обрабатываем пост с полной медиагруппой
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
await self.post_service.process_post(message, full_album)
|
||||
except Exception as e:
|
||||
from logs.custom_logger import logger
|
||||
logger.error(f"Ошибка при фоновой обработке медиагруппы: {e}")
|
||||
|
||||
asyncio.create_task(process_media_group_background())
|
||||
else:
|
||||
# Обычное сообщение или медиагруппа уже собрана - обрабатываем синхронно
|
||||
await self.user_service.update_user_activity(message.from_user.id)
|
||||
if message.media_group_id is None:
|
||||
await self.user_service.log_user_message(message)
|
||||
await self.post_service.process_post(message, album)
|
||||
markup_for_user = await get_reply_keyboard(self.db, message.from_user.id)
|
||||
success_send_message = messages.get_message(get_first_name(message), 'SUCCESS_SEND_MESSAGE')
|
||||
await message.answer(success_send_message, reply_markup=markup_for_user)
|
||||
await state.set_state(FSM_STATES["START"])
|
||||
asyncio.create_task(process_post_background())
|
||||
|
||||
@error_handler
|
||||
@track_errors("private_handlers", "stickers")
|
||||
|
||||
@@ -74,7 +74,8 @@ class UserService:
|
||||
"""Ensure user exists in database, create if needed with metrics tracking"""
|
||||
user_id = message.from_user.id
|
||||
full_name = message.from_user.full_name
|
||||
username = message.from_user.username or "private_username"
|
||||
# Сохраняем только реальный username, если его нет - сохраняем None/пустую строку
|
||||
username = message.from_user.username
|
||||
first_name = get_first_name(message)
|
||||
is_bot = message.from_user.is_bot
|
||||
language_code = message.from_user.language_code
|
||||
@@ -85,7 +86,7 @@ class UserService:
|
||||
user_id=user_id,
|
||||
first_name=first_name,
|
||||
full_name=full_name,
|
||||
username=username,
|
||||
username=username, # Может быть None - это нормально
|
||||
is_bot=is_bot,
|
||||
language_code=language_code,
|
||||
emoji="",
|
||||
@@ -104,6 +105,7 @@ class UserService:
|
||||
if is_need_update:
|
||||
await self.db.update_user_info(user_id, username, full_name)
|
||||
safe_full_name = html.escape(full_name) if full_name else "Неизвестный пользователь"
|
||||
# Для отображения используем подстановочное значение, но в БД сохраняем только реальный username
|
||||
safe_username = html.escape(username) if username else "Без никнейма"
|
||||
|
||||
await message.answer(
|
||||
@@ -177,6 +179,223 @@ class PostService:
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Ошибка сохранения скоров для {message_id}: {e}")
|
||||
|
||||
async def _get_scores_with_error_handling(self, text: str) -> tuple:
|
||||
"""
|
||||
Получает скоры для текста поста с обработкой ошибок.
|
||||
|
||||
Returns:
|
||||
Tuple (deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, error_message)
|
||||
error_message будет None если все ок, или строка с описанием ошибки
|
||||
"""
|
||||
if not self.scoring_manager:
|
||||
# Скоры выключены в .env - это нормально
|
||||
return None, None, None, None, None, None
|
||||
|
||||
if not text or not text.strip():
|
||||
return None, None, None, None, None, None
|
||||
|
||||
try:
|
||||
scores = await self.scoring_manager.score_post(text)
|
||||
|
||||
# Формируем JSON для сохранения в БД
|
||||
import json
|
||||
ml_scores_json = json.dumps(scores.to_json_dict()) if scores.has_any_score() else None
|
||||
|
||||
# Получаем данные от RAG
|
||||
rag_confidence = scores.rag.confidence if scores.rag else None
|
||||
rag_score_pos_only = scores.rag.metadata.get("rag_score_pos_only") if scores.rag else None
|
||||
|
||||
return scores.deepseek_score, scores.rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, None
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Ошибка получения скоров: {e}")
|
||||
# Возвращаем частичные скоры если есть, или сообщение об ошибке
|
||||
error_message = "Не удалось рассчитать скоры"
|
||||
return None, None, None, None, None, error_message
|
||||
|
||||
@track_time("_process_post_background", "post_service")
|
||||
@track_errors("post_service", "_process_post_background")
|
||||
async def _process_post_background(
|
||||
self,
|
||||
message: types.Message,
|
||||
first_name: str,
|
||||
content_type: str,
|
||||
album: Union[list, None] = None
|
||||
) -> None:
|
||||
"""
|
||||
Обрабатывает пост в фоне: получает скоры, отправляет в группу модерации, сохраняет в БД.
|
||||
|
||||
Args:
|
||||
message: Сообщение от пользователя
|
||||
first_name: Имя пользователя
|
||||
content_type: Тип контента ('text', 'photo', 'video', 'audio', 'voice', 'video_note', 'media_group')
|
||||
album: Список сообщений медиагруппы (только для media_group)
|
||||
"""
|
||||
try:
|
||||
# Определяем исходный текст для скоринга и определения анонимности
|
||||
original_raw_text = ""
|
||||
if content_type == "text":
|
||||
original_raw_text = message.text or ""
|
||||
elif content_type == "media_group":
|
||||
original_raw_text = album[0].caption or "" if album and album[0].caption else ""
|
||||
else:
|
||||
original_raw_text = message.caption or ""
|
||||
|
||||
# Получаем скоры с обработкой ошибок
|
||||
deepseek_score, rag_score, rag_confidence, rag_score_pos_only, ml_scores_json, error_message = \
|
||||
await self._get_scores_with_error_handling(original_raw_text)
|
||||
|
||||
# Формируем текст для поста (с сообщением об ошибке если есть)
|
||||
text_for_post = original_raw_text
|
||||
if error_message:
|
||||
# Для текстовых постов добавляем в конец текста
|
||||
if content_type == "text":
|
||||
text_for_post = f"{original_raw_text}\n\n⚠️ {error_message}"
|
||||
# Для медиа добавляем в caption
|
||||
elif content_type in ("photo", "video", "audio") and original_raw_text:
|
||||
text_for_post = f"{original_raw_text}\n\n⚠️ {error_message}"
|
||||
|
||||
# Формируем текст/caption с учетом скоров
|
||||
post_text = ""
|
||||
if text_for_post or content_type == "text":
|
||||
post_text = get_text_message(
|
||||
text_for_post.lower() if text_for_post else "",
|
||||
first_name,
|
||||
message.from_user.username,
|
||||
deepseek_score=deepseek_score,
|
||||
rag_score=rag_score,
|
||||
rag_confidence=rag_confidence,
|
||||
rag_score_pos_only=rag_score_pos_only,
|
||||
)
|
||||
|
||||
# Определяем анонимность по исходному тексту (без сообщения об ошибке)
|
||||
is_anonymous = determine_anonymity(original_raw_text)
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
sent_message = None
|
||||
|
||||
# Отправляем пост в группу модерации в зависимости от типа
|
||||
if content_type == "text":
|
||||
sent_message = await send_text_message(
|
||||
self.settings.group_for_posts, message, post_text, markup
|
||||
)
|
||||
elif content_type == "photo":
|
||||
sent_message = await send_photo_message(
|
||||
self.settings.group_for_posts, message, message.photo[-1].file_id, post_text, markup
|
||||
)
|
||||
elif content_type == "video":
|
||||
sent_message = await send_video_message(
|
||||
self.settings.group_for_posts, message, message.video.file_id, post_text, markup
|
||||
)
|
||||
elif content_type == "audio":
|
||||
sent_message = await send_audio_message(
|
||||
self.settings.group_for_posts, message, message.audio.file_id, post_text, markup
|
||||
)
|
||||
elif content_type == "voice":
|
||||
sent_message = await send_voice_message(
|
||||
self.settings.group_for_posts, message, message.voice.file_id, markup
|
||||
)
|
||||
elif content_type == "video_note":
|
||||
sent_message = await send_video_note_message(
|
||||
self.settings.group_for_posts, message, message.video_note.file_id, markup
|
||||
)
|
||||
elif content_type == "media_group":
|
||||
# Для медиагруппы используем специальную обработку
|
||||
# Передаем ml_scores_json для сохранения в БД
|
||||
await self._process_media_group_background(
|
||||
message, album, first_name, post_text, is_anonymous, original_raw_text, ml_scores_json
|
||||
)
|
||||
return
|
||||
else:
|
||||
logger.error(f"PostService: Неподдерживаемый тип контента: {content_type}")
|
||||
return
|
||||
|
||||
if not sent_message:
|
||||
logger.error(f"PostService: Не удалось отправить пост типа {content_type}")
|
||||
return
|
||||
|
||||
# Сохраняем пост в БД (сохраняем исходный текст, без сообщения об ошибке)
|
||||
post = TelegramPost(
|
||||
message_id=sent_message.message_id,
|
||||
text=original_raw_text,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
)
|
||||
await self.db.add_post(post)
|
||||
|
||||
# Сохраняем медиа и скоры в фоне
|
||||
if content_type in ("photo", "video", "audio", "voice", "video_note"):
|
||||
asyncio.create_task(self._save_media_background(sent_message, self.db, self.s3_storage))
|
||||
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(sent_message.message_id, ml_scores_json))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Критическая ошибка в _process_post_background для {content_type}: {e}")
|
||||
|
||||
async def _process_media_group_background(
|
||||
self,
|
||||
message: types.Message,
|
||||
album: list,
|
||||
first_name: str,
|
||||
post_caption: str,
|
||||
is_anonymous: bool,
|
||||
original_raw_text: str,
|
||||
ml_scores_json: str = None
|
||||
) -> None:
|
||||
"""Обрабатывает медиагруппу в фоне"""
|
||||
try:
|
||||
media_group = await prepare_media_group_from_middlewares(album, post_caption)
|
||||
|
||||
media_group_message_ids = await send_media_group_message_to_private_chat(
|
||||
self.settings.group_for_posts, message, media_group, self.db, None, self.s3_storage
|
||||
)
|
||||
|
||||
main_post_id = media_group_message_ids[-1]
|
||||
|
||||
main_post = TelegramPost(
|
||||
message_id=main_post_id,
|
||||
text=original_raw_text,
|
||||
author_id=message.from_user.id,
|
||||
created_at=int(datetime.now().timestamp()),
|
||||
is_anonymous=is_anonymous
|
||||
)
|
||||
await self.db.add_post(main_post)
|
||||
|
||||
# Сохраняем скоры в фоне (если они были получены)
|
||||
if ml_scores_json:
|
||||
asyncio.create_task(self._save_scores_background(main_post_id, ml_scores_json))
|
||||
|
||||
for msg_id in media_group_message_ids:
|
||||
await self.db.add_message_link(main_post_id, msg_id)
|
||||
|
||||
await asyncio.sleep(0.2)
|
||||
|
||||
markup = get_reply_keyboard_for_post()
|
||||
helper_message = await send_text_message(
|
||||
self.settings.group_for_posts,
|
||||
message,
|
||||
"^",
|
||||
markup
|
||||
)
|
||||
helper_message_id = helper_message.message_id
|
||||
|
||||
helper_post = TelegramPost(
|
||||
message_id=helper_message_id,
|
||||
text="^",
|
||||
author_id=message.from_user.id,
|
||||
helper_text_message_id=main_post_id,
|
||||
created_at=int(datetime.now().timestamp())
|
||||
)
|
||||
await self.db.add_post(helper_post)
|
||||
|
||||
await self.db.update_helper_message(
|
||||
message_id=main_post_id,
|
||||
helper_message_id=helper_message_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"PostService: Ошибка в _process_media_group_background: {e}")
|
||||
|
||||
@track_time("handle_text_post", "post_service")
|
||||
@track_errors("post_service", "handle_text_post")
|
||||
@db_query_time("handle_text_post", "posts", "insert")
|
||||
@@ -479,30 +698,19 @@ class PostService:
|
||||
@track_errors("post_service", "process_post")
|
||||
@track_media_processing("media_group")
|
||||
async def process_post(self, message: types.Message, album: Union[list, None] = None) -> None:
|
||||
"""Process post based on content type"""
|
||||
"""
|
||||
Запускает обработку поста в фоне.
|
||||
Не блокирует выполнение - сразу возвращает управление.
|
||||
"""
|
||||
first_name = get_first_name(message)
|
||||
|
||||
if message.media_group_id is not None:
|
||||
await self.handle_media_group_post(message, album, first_name)
|
||||
return
|
||||
# Определяем тип контента
|
||||
content_type = "media_group" if message.media_group_id is not None else message.content_type
|
||||
|
||||
content_handlers: Dict[str, Callable] = {
|
||||
'text': lambda: self.handle_text_post(message, first_name),
|
||||
'photo': lambda: self.handle_photo_post(message, first_name),
|
||||
'video': lambda: self.handle_video_post(message, first_name),
|
||||
'video_note': lambda: self.handle_video_note_post(message),
|
||||
'audio': lambda: self.handle_audio_post(message, first_name),
|
||||
'voice': lambda: self.handle_voice_post(message)
|
||||
}
|
||||
|
||||
handler = content_handlers.get(message.content_type)
|
||||
if handler:
|
||||
await handler()
|
||||
else:
|
||||
from .constants import ERROR_MESSAGES
|
||||
await message.bot.send_message(
|
||||
message.chat.id, ERROR_MESSAGES["UNSUPPORTED_CONTENT"]
|
||||
)
|
||||
# Запускаем фоновую обработку
|
||||
asyncio.create_task(
|
||||
self._process_post_background(message, first_name, content_type, album)
|
||||
)
|
||||
|
||||
|
||||
class StickerService:
|
||||
|
||||
Reference in New Issue
Block a user