Merge remote-tracking branch 'origin/master' into fix-1

This commit is contained in:
2026-02-02 00:41:51 +03:00
116 changed files with 9801 additions and 6456 deletions

View File

@@ -4,28 +4,25 @@
# Local imports - constants and utilities
from .constants import BUTTON_TEXTS, ERROR_MESSAGES, FSM_STATES
from .decorators import error_handler
from .private_handlers import (PrivateHandlers, create_private_handlers,
private_router)
from .private_handlers import PrivateHandlers, create_private_handlers, private_router
# Local imports - services
from .services import BotSettings, PostService, StickerService, UserService
__all__ = [
# Main components
'private_router',
'create_private_handlers',
'PrivateHandlers',
"private_router",
"create_private_handlers",
"PrivateHandlers",
# Services
'BotSettings',
'UserService',
'PostService',
'StickerService',
"BotSettings",
"UserService",
"PostService",
"StickerService",
# Constants
'FSM_STATES',
'BUTTON_TEXTS',
'ERROR_MESSAGES',
"FSM_STATES",
"BUTTON_TEXTS",
"ERROR_MESSAGES",
# Utilities
'error_handler'
"error_handler",
]

View File

@@ -7,7 +7,7 @@ FSM_STATES: Final[Dict[str, str]] = {
"START": "START",
"SUGGEST": "SUGGEST",
"PRE_CHAT": "PRE_CHAT",
"CHAT": "CHAT"
"CHAT": "CHAT",
}
# Button texts
@@ -18,7 +18,7 @@ BUTTON_TEXTS: Final[Dict[str, str]] = {
"RETURN_TO_BOT": "Вернуться в бота",
"WANT_STICKERS": "🤪Хочу стикеры",
"CONNECT_ADMIN": "📩Связаться с админами",
"VOICE_BOT": "🎤Голосовой бот"
"VOICE_BOT": "🎤Голосовой бот",
}
# Button to command mapping for metrics
@@ -29,15 +29,15 @@ BUTTON_COMMAND_MAPPING: Final[Dict[str, str]] = {
"Вернуться в бота": "return_to_bot",
"🤪Хочу стикеры": "want_stickers",
"📩Связаться с админами": "connect_admin",
"🎤Голосовой бот": "voice_bot"
"🎤Голосовой бот": "voice_bot",
}
# Error messages
ERROR_MESSAGES: Final[Dict[str, str]] = {
"UNSUPPORTED_CONTENT": (
'Я пока не умею работать с таким сообщением. '
'Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n'
'Мы добавим его к обработке если необходимо'
"Я пока не умею работать с таким сообщением. "
"Пришли текст и фото/фоты(ы). А лучше перешли это сообщение админу @kerrad1\n"
"Мы добавим его к обработке если необходимо"
),
"STICKERS_LINK": "Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk"
"STICKERS_LINK": "Хорошо, лови, добавить можно отсюда: https://t.me/addstickers/love_biysk",
}

View File

@@ -6,12 +6,14 @@ from typing import Any, Callable
# Third-party imports
from aiogram import types
# Local imports
from logs.custom_logger import logger
def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
"""Decorator for centralized error handling"""
async def wrapper(*args: Any, **kwargs: Any) -> Any:
try:
return await func(*args, **kwargs)
@@ -19,18 +21,23 @@ def error_handler(func: Callable[..., Any]) -> Callable[..., Any]:
logger.error(f"Error in {func.__name__}: {str(e)}")
# Try to send error to logs if possible
try:
message = next((arg for arg in args if isinstance(arg, types.Message)), None)
if message and hasattr(message, 'bot'):
from helper_bot.utils.base_dependency_factory import \
get_global_instance
message = next(
(arg for arg in args if isinstance(arg, types.Message)), None
)
if message and hasattr(message, "bot"):
from helper_bot.utils.base_dependency_factory import (
get_global_instance,
)
bdf = get_global_instance()
important_logs = bdf.settings['Telegram']['important_logs']
important_logs = bdf.settings["Telegram"]["important_logs"]
await message.bot.send_message(
chat_id=important_logs,
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}"
text=f"Произошла ошибка в {func.__name__}: {str(e)}\n\nTraceback:\n{traceback.format_exc()}",
)
except Exception:
# If we can't log the error, at least it was logged to logger
pass
raise
return wrapper

View File

@@ -8,18 +8,23 @@ from datetime import datetime
from aiogram import F, Router, types
from aiogram.filters import Command, StateFilter
from aiogram.fsm.context import FSMContext
# Local imports - filters and middlewares
from database.async_db import AsyncBotDB
from helper_bot.filters.main import ChatTypeFilter
# Local imports - utilities
from helper_bot.keyboards import (get_reply_keyboard,
get_reply_keyboard_for_post)
from helper_bot.keyboards import get_reply_keyboard, get_reply_keyboard_for_post
from helper_bot.keyboards.keyboards import get_reply_keyboard_leave_chat
from helper_bot.middlewares.album_middleware import AlbumMiddleware
from helper_bot.middlewares.blacklist_middleware import BlacklistMiddleware
from helper_bot.utils import messages
from helper_bot.utils.helper_func import (check_user_emoji, get_first_name,
update_user_info)
from helper_bot.utils.helper_func import (
check_user_emoji,
get_first_name,
update_user_info,
)
# Local imports - metrics
from helper_bot.utils.metrics import db_query_time, track_errors, track_time
@@ -34,83 +39,144 @@ sleep = asyncio.sleep
class PrivateHandlers:
"""Main handler class for private messages"""
def __init__(self, db: AsyncBotDB, settings: BotSettings, s3_storage=None, scoring_manager=None):
def __init__(
self,
db: AsyncBotDB,
settings: BotSettings,
s3_storage=None,
scoring_manager=None,
):
self.db = db
self.settings = settings
self.user_service = UserService(db, settings)
self.post_service = PostService(db, settings, s3_storage, scoring_manager)
self.sticker_service = StickerService(settings)
self.router = Router()
self.router.message.middleware(AlbumMiddleware(latency=5.0))
self.router.message.middleware(BlacklistMiddleware())
# Register handlers
self._register_handlers()
def _register_handlers(self):
"""Register all message handlers"""
# Command handlers
self.router.message.register(self.handle_emoji_message, ChatTypeFilter(chat_type=["private"]), Command("emoji"))
self.router.message.register(self.handle_restart_message, ChatTypeFilter(chat_type=["private"]), Command("restart"))
self.router.message.register(self.handle_start_message, ChatTypeFilter(chat_type=["private"]), Command("start"))
self.router.message.register(self.handle_start_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["RETURN_TO_BOT"])
# Button handlers
self.router.message.register(self.suggest_post, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["SUGGEST_POST"])
self.router.message.register(self.end_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["SAY_GOODBYE"])
self.router.message.register(self.end_message, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["LEAVE_CHAT"])
self.router.message.register(self.stickers, ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["WANT_STICKERS"])
self.router.message.register(self.connect_with_admin, StateFilter(FSM_STATES["START"]), ChatTypeFilter(chat_type=["private"]), F.text == BUTTON_TEXTS["CONNECT_ADMIN"])
self.router.message.register(
self.handle_emoji_message,
ChatTypeFilter(chat_type=["private"]),
Command("emoji"),
)
self.router.message.register(
self.handle_restart_message,
ChatTypeFilter(chat_type=["private"]),
Command("restart"),
)
self.router.message.register(
self.handle_start_message,
ChatTypeFilter(chat_type=["private"]),
Command("start"),
)
self.router.message.register(
self.handle_start_message,
ChatTypeFilter(chat_type=["private"]),
F.text == BUTTON_TEXTS["RETURN_TO_BOT"],
)
# Button handlers
self.router.message.register(
self.suggest_post,
StateFilter(FSM_STATES["START"]),
ChatTypeFilter(chat_type=["private"]),
F.text == BUTTON_TEXTS["SUGGEST_POST"],
)
self.router.message.register(
self.end_message,
ChatTypeFilter(chat_type=["private"]),
F.text == BUTTON_TEXTS["SAY_GOODBYE"],
)
self.router.message.register(
self.end_message,
ChatTypeFilter(chat_type=["private"]),
F.text == BUTTON_TEXTS["LEAVE_CHAT"],
)
self.router.message.register(
self.stickers,
ChatTypeFilter(chat_type=["private"]),
F.text == BUTTON_TEXTS["WANT_STICKERS"],
)
self.router.message.register(
self.connect_with_admin,
StateFilter(FSM_STATES["START"]),
ChatTypeFilter(chat_type=["private"]),
F.text == BUTTON_TEXTS["CONNECT_ADMIN"],
)
# State handlers
self.router.message.register(self.suggest_router, StateFilter(FSM_STATES["SUGGEST"]), ChatTypeFilter(chat_type=["private"]))
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["PRE_CHAT"]), ChatTypeFilter(chat_type=["private"]))
self.router.message.register(self.resend_message_in_group_for_message, StateFilter(FSM_STATES["CHAT"]), ChatTypeFilter(chat_type=["private"]))
self.router.message.register(
self.suggest_router,
StateFilter(FSM_STATES["SUGGEST"]),
ChatTypeFilter(chat_type=["private"]),
)
self.router.message.register(
self.resend_message_in_group_for_message,
StateFilter(FSM_STATES["PRE_CHAT"]),
ChatTypeFilter(chat_type=["private"]),
)
self.router.message.register(
self.resend_message_in_group_for_message,
StateFilter(FSM_STATES["CHAT"]),
ChatTypeFilter(chat_type=["private"]),
)
@error_handler
@track_errors("private_handlers", "handle_emoji_message")
@track_time("handle_emoji_message", "private_handlers")
async def handle_emoji_message(self, message: types.Message, state: FSMContext, **kwargs):
async def handle_emoji_message(
self, message: types.Message, state: FSMContext, **kwargs
):
"""Handle emoji command"""
await self.user_service.log_user_message(message)
user_emoji = await check_user_emoji(message)
await state.set_state(FSM_STATES["START"])
if user_emoji is not None:
await message.answer(f'Твоя эмодзя - {user_emoji}', parse_mode='HTML')
await message.answer(f"Твоя эмодзя - {user_emoji}", parse_mode="HTML")
@error_handler
@track_errors("private_handlers", "handle_restart_message")
@track_time("handle_restart_message", "private_handlers")
async def handle_restart_message(self, message: types.Message, state: FSMContext, **kwargs):
async def handle_restart_message(
self, message: types.Message, state: FSMContext, **kwargs
):
"""Handle restart command"""
markup = await get_reply_keyboard(self.db, message.from_user.id)
await self.user_service.log_user_message(message)
await state.set_state(FSM_STATES["START"])
await update_user_info('love', message)
await update_user_info("love", message)
await check_user_emoji(message)
await message.answer('Я перезапущен!', reply_markup=markup, parse_mode='HTML')
await message.answer("Я перезапущен!", reply_markup=markup, parse_mode="HTML")
@error_handler
@track_errors("private_handlers", "handle_start_message")
@track_time("handle_start_message", "private_handlers")
async def handle_start_message(self, message: types.Message, state: FSMContext, **kwargs):
async def handle_start_message(
self, message: types.Message, state: FSMContext, **kwargs
):
"""Handle start command and return to bot button with metrics tracking"""
# User service operations with metrics
await self.user_service.log_user_message(message)
await self.user_service.ensure_user_exists(message)
await state.set_state(FSM_STATES["START"])
# Send sticker with metrics
await self.sticker_service.send_random_hello_sticker(message)
# Send welcome message with metrics
markup = await get_reply_keyboard(self.db, message.from_user.id)
hello_message = messages.get_message(get_first_name(message), 'HELLO_MESSAGE')
await message.answer(hello_message, reply_markup=markup, parse_mode='HTML')
hello_message = messages.get_message(get_first_name(message), "HELLO_MESSAGE")
await message.answer(hello_message, reply_markup=markup, parse_mode="HTML")
@error_handler
@track_errors("private_handlers", "suggest_post")
@track_time("suggest_post", "private_handlers")
@@ -120,11 +186,11 @@ class PrivateHandlers:
await self.user_service.update_user_activity(message.from_user.id)
await self.user_service.log_user_message(message)
await state.set_state(FSM_STATES["SUGGEST"])
markup = types.ReplyKeyboardRemove()
suggest_news = messages.get_message(get_first_name(message), 'SUGGEST_NEWS')
suggest_news = messages.get_message(get_first_name(message), "SUGGEST_NEWS")
await message.answer(suggest_news, reply_markup=markup)
@error_handler
@track_errors("private_handlers", "end_message")
@track_time("end_message", "private_handlers")
@@ -133,40 +199,44 @@ class PrivateHandlers:
# User service operations with metrics
await self.user_service.update_user_activity(message.from_user.id)
await self.user_service.log_user_message(message)
# Send sticker
await self.sticker_service.send_random_goodbye_sticker(message)
# Send goodbye message
markup = types.ReplyKeyboardRemove()
bye_message = messages.get_message(get_first_name(message), 'BYE_MESSAGE')
bye_message = messages.get_message(get_first_name(message), "BYE_MESSAGE")
await message.answer(bye_message, reply_markup=markup)
await state.set_state(FSM_STATES["START"])
@error_handler
@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):
async def suggest_router(
self, message: types.Message, state: FSMContext, album: list = None, **kwargs
):
"""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')
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")
# В фоне обрабатываем пост
async def process_post_background():
try:
# Обновляем активность пользователя
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)
# Для медиагрупп ждем полную медиагруппу
if album_getter and message.media_group_id:
full_album = await album_getter.get_album(timeout=10.0)
@@ -177,10 +247,11 @@ class PrivateHandlers:
await self.post_service.process_post(message, album)
except Exception as e:
from logs.custom_logger import logger
logger.error(f"Ошибка при фоновой обработке поста: {e}")
asyncio.create_task(process_post_background())
@error_handler
@track_errors("private_handlers", "stickers")
@track_time("stickers", "private_handlers")
@@ -191,41 +262,46 @@ class PrivateHandlers:
markup = await get_reply_keyboard(self.db, message.from_user.id)
await self.db.update_stickers_info(message.from_user.id)
await self.user_service.log_user_message(message)
await message.answer(
text=ERROR_MESSAGES["STICKERS_LINK"],
reply_markup=markup
)
await message.answer(text=ERROR_MESSAGES["STICKERS_LINK"], reply_markup=markup)
await state.set_state(FSM_STATES["START"])
@error_handler
@track_errors("private_handlers", "connect_with_admin")
@track_time("connect_with_admin", "private_handlers")
async def connect_with_admin(self, message: types.Message, state: FSMContext, **kwargs):
async def connect_with_admin(
self, message: types.Message, state: FSMContext, **kwargs
):
"""Handle connect with admin button"""
# User service operations with metrics
await self.user_service.update_user_activity(message.from_user.id)
admin_message = messages.get_message(get_first_name(message), 'CONNECT_WITH_ADMIN')
admin_message = messages.get_message(
get_first_name(message), "CONNECT_WITH_ADMIN"
)
await message.answer(admin_message, parse_mode="html")
await self.user_service.log_user_message(message)
await state.set_state(FSM_STATES["PRE_CHAT"])
@error_handler
@track_errors("private_handlers", "resend_message_in_group_for_message")
@track_time("resend_message_in_group_for_message", "private_handlers")
@db_query_time("resend_message_in_group_for_message", "messages", "insert")
async def resend_message_in_group_for_message(self, message: types.Message, state: FSMContext, **kwargs):
async def resend_message_in_group_for_message(
self, message: types.Message, state: FSMContext, **kwargs
):
"""Handle messages in admin chat states"""
# User service operations with metrics
await self.user_service.update_user_activity(message.from_user.id)
await message.forward(chat_id=self.settings.group_for_message)
current_date = datetime.now()
date = int(current_date.timestamp())
await self.db.add_message(message.text, message.from_user.id, message.message_id + 1, date)
question = messages.get_message(get_first_name(message), 'QUESTION')
await self.db.add_message(
message.text, message.from_user.id, message.message_id + 1, date
)
question = messages.get_message(get_first_name(message), "QUESTION")
user_state = await state.get_state()
if user_state == FSM_STATES["PRE_CHAT"]:
markup = await get_reply_keyboard(self.db, message.from_user.id)
await message.answer(question, reply_markup=markup)
@@ -236,7 +312,9 @@ class PrivateHandlers:
# Factory function to create handlers with dependencies
def create_private_handlers(db: AsyncBotDB, settings: BotSettings, s3_storage=None, scoring_manager=None) -> PrivateHandlers:
def create_private_handlers(
db: AsyncBotDB, settings: BotSettings, s3_storage=None, scoring_manager=None
) -> PrivateHandlers:
"""Create private handlers instance with dependencies"""
return PrivateHandlers(db, settings, s3_storage, scoring_manager)
@@ -247,37 +325,39 @@ private_router = Router()
# Флаг инициализации для защиты от повторного вызова
_legacy_router_initialized = False
# Initialize with global dependencies (for backward compatibility)
def init_legacy_router():
"""Initialize legacy router with global dependencies"""
global private_router, _legacy_router_initialized
if _legacy_router_initialized:
return
from helper_bot.utils.base_dependency_factory import get_global_instance
bdf = get_global_instance()
settings = BotSettings(
group_for_posts=bdf.settings['Telegram']['group_for_posts'],
group_for_message=bdf.settings['Telegram']['group_for_message'],
main_public=bdf.settings['Telegram']['main_public'],
group_for_logs=bdf.settings['Telegram']['group_for_logs'],
important_logs=bdf.settings['Telegram']['important_logs'],
preview_link=bdf.settings['Telegram']['preview_link'],
logs=bdf.settings['Settings']['logs'],
test=bdf.settings['Settings']['test']
group_for_posts=bdf.settings["Telegram"]["group_for_posts"],
group_for_message=bdf.settings["Telegram"]["group_for_message"],
main_public=bdf.settings["Telegram"]["main_public"],
group_for_logs=bdf.settings["Telegram"]["group_for_logs"],
important_logs=bdf.settings["Telegram"]["important_logs"],
preview_link=bdf.settings["Telegram"]["preview_link"],
logs=bdf.settings["Settings"]["logs"],
test=bdf.settings["Settings"]["test"],
)
db = bdf.get_db()
s3_storage = bdf.get_s3_storage()
scoring_manager = bdf.get_scoring_manager()
handlers = create_private_handlers(db, settings, s3_storage, scoring_manager)
# Instead of trying to copy handlers, we'll use the new router directly
# This maintains backward compatibility while using the new architecture
private_router = handlers.router
_legacy_router_initialized = True
# Initialize legacy router
init_legacy_router()

File diff suppressed because it is too large Load Diff