""" Сервис управления пользователями. Отвечает за бизнес-логику работы с пользователями. """ from typing import Dict, List, Optional, Tuple, Any from dataclasses import dataclass from datetime import datetime, timedelta import logging from core.exceptions import ValidationError from core.permissions import UserRole, permission_manager from utils.validators import InputValidator, Validator @dataclass class UserProfile: """Профиль пользователя""" user_id: int username: Optional[str] first_name: str last_name: Optional[str] reputation: int = 0 rank: str = "Новичок" message_count: int = 0 joined_date: Optional[datetime] = None last_activity: Optional[datetime] = None warnings: int = 0 achievements: Optional[List[str]] = None def __post_init__(self): if self.achievements is None: self.achievements = [] if self.joined_date is None: self.joined_date = datetime.now() if self.last_activity is None: self.last_activity = datetime.now() class UserService: """ Сервис для управления пользователями и их данными. Отвечает за: - Создание и обновление профилей пользователей - Расчет рейтингов и рангов - Управление достижениями - Статистику активности """ def __init__(self, user_repository, score_repository, role_service=None): """ Инициализация сервиса. Args: user_repository: Репозиторий пользователей score_repository: Репозиторий очков и рейтингов role_service: Сервис управления ролями """ self.user_repo = user_repository self.score_repo = score_repository self.role_service = role_service self.logger = logging.getLogger(__name__) # Настройки рангов и очков self.rank_thresholds = { "Новичок": 0, "Ученик": 100, "Активист": 500, "Знаток": 1000, "Эксперт": 2500, "Мастер": 5000, "Гуру": 10000, "Легенда": 25000, "Капитан": 50000, "Генерал": 100000, "Император": 250000 } self.rank_names = list(self.rank_thresholds.keys()) async def get_or_create_user(self, user_id: int, username: str = None, first_name: str = None, last_name: str = None) -> UserProfile: """ Получение пользователя или создание нового профиля. Args: user_id: ID пользователя username: Имя пользователя first_name: Имя last_name: Фамилия Returns: Профиль пользователя """ try: # Валидация входных данных if not isinstance(user_id, int) or user_id <= 0 or user_id > 2147483647: raise ValidationError("Неверный ID пользователя") # Получаем данные пользователя из репозитория user_data = await self.user_repo.get_by_id_async(user_id) if user_data and isinstance(user_data, dict): user_data = user_data else: user_data = None self.logger.debug(f"Результат get_by_id для {user_id}: {user_data is not None}") if user_data: self.logger.debug(f"Найден существующий пользователь {user_id}: id={user_data.get('id')}, telegram_id={user_data.get('telegram_id')}, first_name={user_data.get('first_name')}") # Проверяем, что все необходимые поля присутствуют и не None if not user_data.get('id'): self.logger.error(f"Пользователь {user_id} найден, но поле id отсутствует или None") return None # Пользователь существует, обновляем данные self.logger.debug(f"Обновление данных существующего пользователя {user_id}") updated_data = await self._update_user_data(user_data, username, first_name, last_name) self.logger.debug(f"Результат обновления данных для {user_id}: {updated_data is not None}") if updated_data: profile = self._map_to_profile(updated_data) self.logger.debug(f"Создан профиль из обновленных данных для {user_id}: {profile is not None}") return profile else: self.logger.error(f"Ошибка обновления данных пользователя {user_id}") return None else: # Создаем нового пользователя self.logger.debug(f"Создание нового пользователя {user_id}") new_user_data = await self._create_new_user(user_id, username, first_name, last_name) self.logger.debug(f"Результат создания нового пользователя {user_id}: {new_user_data is not None}") if new_user_data: profile = self._map_to_profile(new_user_data) self.logger.debug(f"Создан профиль из новых данных для {user_id}: {profile is not None}") return profile else: self.logger.error(f"Ошибка создания нового пользователя {user_id}") return None except Exception as e: # Handle potential sqlite3.IntegrityError or other database errors if "FOREIGN KEY constraint failed" in str(e) or "IntegrityError" in str(type(e).__name__): self.logger.warning(f"Нарушение внешнего ключа для пользователя {user_id}, пропускаем создание ролей") # Попытка создания без ролей или с ролью по умолчанию try: # Используем роль 1 (user) по умолчанию user_data['role_id'] = 1 new_user_data = await self._create_new_user(user_id, username, first_name, last_name) if new_user_data: profile = self._map_to_profile(new_user_data) return profile except Exception as retry_error: self.logger.error(f"Повторная попытка создания пользователя {user_id} не удалась: {retry_error}") return None else: self.logger.error(f"Ошибка базы данных при работе с пользователем {user_id}: {e}") return None except Exception as e: if "FOREIGN KEY constraint failed" in str(e): self.logger.warning(f"Нарушение внешнего ключа для пользователя {user_id}, пропускаем создание ролей") # Попытка создания без ролей или с ролью по умолчанию try: # Используем роль 1 (user) по умолчанию user_data['role_id'] = 1 new_user_data = await self._create_new_user(user_id, username, first_name, last_name) if new_user_data: profile = self._map_to_profile(new_user_data) return profile except Exception as retry_error: self.logger.error(f"Повторная попытка создания пользователя {user_id} не удалась: {retry_error}") return None else: self.logger.error(f"Ошибка базы данных при работе с пользователем {user_id}: {e}") return None except Exception as e: self.logger.error(f"Ошибка в get_or_create_user для пользователя {user_id}: {e}", exc_info=True) return None async def update_user_activity(self, user_id: int, chat_id: int) -> Dict[str, Any]: """ Обновление активности пользователя. Args: user_id: ID пользователя chat_id: ID чата Returns: Результат обновления (включая повышение ранга) """ try: # Обновляем статистику активности await self.user_repo.update_activity(user_id, chat_id) # Обновляем очки print(f"[DEBUG] UserService.update_user_activity: calling score_repo.update_score({user_id}, 1)") await self.score_repo.update_score(user_id, 1) # Проверяем повышение ранга rank_update = await self._check_rank_promotion(user_id) return { 'activity_updated': True, 'rank_promoted': rank_update is not None, 'rank_update': rank_update } except Exception as e: self.logger.error(f"Ошибка в update_user_activity для пользователя {user_id}: {e}", exc_info=True) # Возвращаем результат с ошибкой, но не прерываем выполнение return { 'activity_updated': False, 'rank_promoted': False, 'rank_update': None, 'error': str(e) } async def add_warning(self, user_id: int, reason: str, admin_id: int) -> bool: """ Добавление предупреждения пользователю. Args: user_id: ID пользователя reason: Причина предупреждения admin_id: ID администратора Returns: True если предупреждение добавлено """ if not InputValidator.validate_text_content(reason, max_length=500): raise ValidationError("Неверная причина предупреждения") print(f"[DEBUG] UserService.add_warning called: user_id={user_id}, reason='{reason}', admin_id={admin_id}") result = await self.user_repo.add_warning(user_id, reason, admin_id) print(f"[DEBUG] UserService.add_warning result: {result}") # Проверяем обновление счетчика предупреждений if result: warnings_count = await self.user_repo.get_warnings_count(user_id) print(f"[DEBUG] Warnings count after adding warning: {warnings_count}") return result async def get_user_achievements(self, user_id: int) -> List[Tuple[str, datetime]]: """ Получение достижений пользователя. Args: user_id: ID пользователя Returns: Список достижений с датами разблокировки """ return await self.user_repo.get_user_achievements(user_id) async def check_and_unlock_achievements(self, user_id: int) -> List[str]: """ Проверка и разблокировка новых достижений. Args: user_id: ID пользователя Returns: Список новых достижений """ self.logger.debug(f"Starting achievement check for user {user_id}") user_profile = await self.get_or_create_user(user_id) user_stats = await self._get_user_statistics(user_id) self.logger.debug(f"User stats for {user_id}: {user_stats}") new_achievements = [] all_achievements = await self.user_repo.get_all_achievements() self.logger.debug(f"Total achievements to check: {len(all_achievements)}") for achievement in all_achievements: achievement_id, name, description, condition_type, condition_value, badge = achievement self.logger.debug(f"Checking achievement: {name}, type: {condition_type}, value: {condition_value}") if await self._check_achievement_condition(user_profile, user_stats, condition_type, condition_value): self.logger.debug(f"Achievement condition met for: {name}") if not await self.user_repo.has_achievement(user_id, achievement_id): self.logger.debug(f"Unlocking achievement: {name}") await self.user_repo.unlock_achievement(user_id, achievement_id) new_achievements.append(name) else: self.logger.debug(f"Achievement already unlocked: {name}") else: self.logger.debug(f"Achievement condition NOT met for: {name}") self.logger.debug(f"New achievements unlocked: {new_achievements}") return new_achievements async def get_top_users(self, limit: int = 10) -> List[Tuple[int, str, str, int]]: """ Получение топ пользователей по очкам. Args: limit: Количество пользователей Returns: Список пользователей (id, username, first_name, score) """ return await self.user_repo.get_top_users_async(limit) async def search_users(self, query: str, limit: int = 10) -> List[Dict[str, Any]]: """ Поиск пользователей по имени или username. Args: query: Поисковый запрос limit: Максимальное количество результатов Returns: Список найденных пользователей """ if not InputValidator.validate_text_content(query, max_length=50): raise ValidationError("Неверный поисковый запрос") return await self.user_repo.search_users(query, limit) def calculate_rank(self, score: int) -> str: """ Расчет ранга пользователя по количеству очков. Args: score: Количество очков Returns: Название ранга """ for rank, threshold in reversed(self.rank_thresholds.items()): if score >= threshold: return rank return "Новичок" def get_user_role(self, user_id: int) -> str: """ Получение роли пользователя. Args: user_id: ID пользователя Returns: Название роли """ if self.role_service: return self.role_service.get_user_role_name(user_id) return UserRole.USER.value # fallback def is_admin(self, user_id: int) -> bool: """ Проверка, является ли пользователь администратором. Args: user_id: ID пользователя Returns: True если администратор """ if self.role_service: return self.role_service.is_admin(user_id) # Используем permission_manager как fallback user_role = self.get_user_role(user_id) try: role_enum = UserRole(user_role) return role_enum in [UserRole.ADMIN, UserRole.SUPER_ADMIN] except ValueError: return False def is_moderator(self, user_id: int) -> bool: """ Проверка, является ли пользователь модератором или администратором. Args: user_id: ID пользователя Returns: True если модератор или администратор """ if self.role_service: return self.role_service.is_moderator(user_id) # Используем permission_manager как fallback user_role = self.get_user_role(user_id) try: role_enum = UserRole(user_role) return role_enum in [UserRole.MODERATOR, UserRole.ADMIN, UserRole.SUPER_ADMIN] except ValueError: return False async def get_user_role_enum_async(self, user_id: int) -> UserRole: """ Получение роли пользователя как enum UserRole (асинхронная версия). Args: user_id: ID пользователя Returns: Роль пользователя как enum """ try: # Используем permission_manager для получения роли from core.permissions import permission_manager # Получаем config из Application если возможно, иначе создаем минимальный try: from core.application import app_instance if app_instance and hasattr(app_instance, 'config'): config = app_instance.config else: config = self._get_minimal_config_for_permissions() except ImportError: config = self._get_minimal_config_for_permissions() role = await permission_manager.get_effective_role(None, user_id, config) print(f"[DEBUG] get_user_role_enum_async: permission_manager returned role: {role.value if hasattr(role, 'value') else role}") # ОТЛАДКА return role except Exception as e: # Fallback к старому методу print(f"[DEBUG] get_user_role_enum_async: permission_manager failed ({e}), using fallback") # ОТЛАДКА role_str = self.get_user_role(user_id) try: role_enum = UserRole(role_str) print(f"[DEBUG] get_user_role_enum_async: fallback returned role: {role_enum.value}") # ОТЛАДКА return role_enum except ValueError: print(f"[DEBUG] get_user_role_enum_async: fallback failed, returning USER") # ОТЛАДКА return UserRole.USER # fallback def get_user_role_enum(self, user_id: int) -> UserRole: """ Получение роли пользователя как enum UserRole (синхронная версия). Args: user_id: ID пользователя Returns: Роль пользователя как enum """ # Создаем event loop для вызова асинхронного метода import asyncio try: loop = asyncio.get_event_loop() if loop.is_running(): # Если loop уже запущен, создаем новую задачу import concurrent.futures with concurrent.futures.ThreadPoolExecutor() as executor: future = executor.submit(asyncio.run, self.get_user_role_enum_async(user_id)) return future.result() else: return loop.run_until_complete(self.get_user_role_enum_async(user_id)) except Exception as e: print(f"[ERROR] get_user_role_enum: failed to get async role ({e}), using fallback") # ОТЛАДКА role_str = self.get_user_role(user_id) try: return UserRole(role_str) except ValueError: return UserRole.USER # fallback def _get_minimal_config_for_permissions(self): """Создание минимального конфига для permission_manager""" class MinimalConfig: def __init__(self): self.bot_config = MinimalBotConfig() class MinimalBotConfig: def __init__(self): self.super_admin_ids = [] self.admin_ids = [] self.moderator_ids = [] config = MinimalConfig() return config def get_rank_progress(self, current_score: int) -> Dict[str, Any]: """ Получение прогресса до следующего ранга. Args: current_score: Текущие очки пользователя Returns: Информация о прогрессе """ current_rank = self.calculate_rank(current_score) # Находим следующий ранг current_index = self.rank_names.index(current_rank) next_index = min(current_index + 1, len(self.rank_names) - 1) current_threshold = self.rank_thresholds[current_rank] next_threshold = self.rank_thresholds[self.rank_names[next_index]] progress = current_score - current_threshold needed = next_threshold - current_threshold return { 'current_rank': current_rank, 'next_rank': self.rank_names[next_index] if current_index < len(self.rank_names) - 1 else current_rank, 'current_score': current_score, 'progress': progress, 'needed': needed, 'percentage': (progress / needed * 100) if needed > 0 else 100 } async def _update_user_data(self, user_data: Dict, username: str, first_name: str, last_name: str) -> Dict: """Обновление данных существующего пользователя""" try: update_data = {} if username and user_data.get('username') != username: update_data['username'] = username if first_name and user_data.get('first_name') != first_name: update_data['first_name'] = first_name if last_name and user_data.get('last_name') != last_name: update_data['last_name'] = last_name if update_data: self.logger.debug(f"Обновление данных пользователя {user_data['id']}: {update_data}") await self.user_repo.update_user(user_data['id'], update_data) self.logger.debug(f"Данные пользователя {user_data['id']} успешно обновлены") else: self.logger.debug(f"Для пользователя {user_data['id']} нет данных для обновления") # Возвращаем актуальные данные пользователя - используем telegram_id вместо id telegram_id = user_data.get('telegram_id') if telegram_id: current_data = await self.user_repo.get_by_id_async(telegram_id) if current_data: return current_data else: self.logger.error(f"Не удалось получить обновленные данные пользователя telegram_id={telegram_id}") return None else: self.logger.error(f"telegram_id отсутствует в данных пользователя: {user_data}") return None except Exception as e: self.logger.error(f"Ошибка при обновлении пользователя {user_data.get('id', 'unknown')}: {e}", exc_info=True) return None async def _create_new_user(self, user_id: int, username: str, first_name: str, last_name: str) -> Dict: """Создание нового пользователя""" try: user_data = { 'telegram_id': user_id, 'username': username, 'first_name': first_name or '', 'last_name': last_name, 'joined_date': datetime.now(), 'last_activity': datetime.now() } result = await self.user_repo._execute_query_async( "INSERT INTO users (telegram_id, username, first_name, last_name, joined_date, last_activity) VALUES (?, ?, ?, ?, ?, ?)", (user_data['telegram_id'], user_data.get('username'), user_data.get('first_name'), user_data.get('last_name'), user_data.get('joined_date'), user_data.get('last_activity')) ) result = True # Проверяем результат создания пользователя if result: # Получаем созданного пользователя для возврата полных данных return await self.user_repo.get_by_id(user_id) else: print(f"Ошибка: репозиторий вернул None при создании пользователя {user_id}") return None except Exception as e: print(f"Ошибка при создании пользователя {user_id}: {e}") return None async def _check_rank_promotion(self, user_id: int) -> Optional[Dict[str, Any]]: """Проверка повышения ранга пользователя""" user_data = await self.user_repo.get_by_id_async(user_id) if not user_data: return None current_rank = user_data.get('rank', 'Новичок') new_rank = self.calculate_rank(user_data.get('reputation', 0)) if new_rank != current_rank: # Обновляем ранг в базе данных await self.user_repo.update_rank(user_id, new_rank) return { 'old_rank': current_rank, 'new_rank': new_rank, 'user_name': user_data.get('first_name', 'Пользователь'), 'promoted': True } return None async def _get_user_statistics(self, user_id: int) -> Dict[str, Any]: """Получение статистики пользователя""" message_count = await self.score_repo.get_message_count(user_id) total_score = await self.score_repo.get_total_score(user_id) days_active = await self.user_repo.get_days_active(user_id) warnings_count = await self.user_repo.get_warnings_count(user_id) return { 'message_count': message_count, 'total_score': total_score, 'days_active': days_active, 'warnings_count': warnings_count } async def _check_achievement_condition(self, profile: UserProfile, stats: Dict[str, Any], condition_type: str, condition_value: Any) -> bool: """Проверка условия достижения""" if condition_type == 'score' and stats.get('total_score', 0) >= condition_value: return True elif condition_type == 'messages' and stats.get('message_count', 0) >= condition_value: return True elif condition_type == 'days_active' and stats.get('days_active', 0) >= condition_value: return True elif condition_type == 'warnings' and stats.get('warnings_count', 0) <= condition_value: return True elif condition_type == 'donations': # Для достижений за донаты проверяем общую сумму донатов total_donations = await self.get_total_donations(profile.user_id) self.logger.debug(f"Checking donation achievement: total_donations={total_donations}, condition_value={condition_value}") result = total_donations >= float(condition_value) self.logger.debug(f"Donation achievement condition result: {result}") return result return False def _map_to_profile(self, user_data: Dict) -> UserProfile: """Преобразование данных из БД в профиль пользователя""" if not user_data: raise ValueError("Данные пользователя не могут быть None") # Дополнительная проверка на наличие обязательных полей if 'id' not in user_data or user_data['id'] is None: raise ValueError(f"Обязательное поле 'id' отсутствует в данных пользователя: {user_data}") # Преобразование строковых дат в datetime объекты joined_date = user_data.get('joined_date') last_activity = user_data.get('last_activity') if isinstance(joined_date, str): try: joined_date = datetime.fromisoformat(joined_date.replace('Z', '+00:00')) except (ValueError, AttributeError): joined_date = None if isinstance(last_activity, str): try: last_activity = datetime.fromisoformat(last_activity.replace('Z', '+00:00')) except (ValueError, AttributeError): last_activity = None return UserProfile( user_id=user_data['id'], username=user_data.get('username'), first_name=user_data.get('first_name', ''), last_name=user_data.get('last_name'), reputation=user_data.get('reputation', 0), rank=user_data.get('rank', 'Новичок'), message_count=user_data.get('message_count', 0), joined_date=joined_date, last_activity=last_activity, warnings=user_data.get('warnings', 0) ) async def add_donation(self, user_id: int, amount: float) -> bool: """ Добавление доната пользователя. Args: user_id: ID пользователя amount: Сумма доната Returns: True если донат добавлен успешно """ self.logger.info(f"Начало добавления доната: user_id={user_id}, amount={amount}") try: # Валидация данных self.logger.debug(f"Валидация входных данных для пользователя {user_id}") if not Validator.validate_user_id(user_id): self.logger.error(f"Неверный ID пользователя: {user_id}") raise ValidationError("Неверный ID пользователя") if amount <= 0: self.logger.error(f"Неверная сумма доната: {amount} (должна быть положительной)") raise ValidationError("Сумма доната должна быть положительной") self.logger.debug(f"Валидация прошла успешно для пользователя {user_id}") # Создаем пользователя, если не существует self.logger.debug(f"Получение или создание профиля пользователя {user_id}") try: user_profile = await self.get_or_create_user(user_id) self.logger.debug(f"Результат get_or_create_user для {user_id}: {user_profile is not None}") if user_profile: self.logger.debug(f"Профиль пользователя {user_id}: user_id={user_profile.user_id}, first_name={user_profile.first_name}") else: self.logger.error(f"get_or_create_user вернул None для пользователя {user_id}") except Exception as profile_error: self.logger.error(f"Исключение в get_or_create_user для пользователя {user_id}: {profile_error}", exc_info=True) return False if not user_profile: self.logger.error(f"Не удалось создать или получить профиль пользователя {user_id}") return False self.logger.debug(f"Профиль пользователя {user_id} получен: {user_profile.user_id if user_profile else None}") # Получаем текущий год для статистики current_year = datetime.now().year self.logger.debug(f"Текущий год для статистики: {current_year}") # Начинаем транзакцию self.logger.debug("Начало транзакции для добавления доната") self.logger.debug("Проверка подключения к репозиториям перед транзакцией") if self.user_repo is None: self.logger.error("user_repo is None - невозможно начать транзакцию") return False if self.score_repo is None: self.logger.error("score_repo is None - невозможно начать транзакцию") return False try: await self.user_repo.begin_transaction() await self.score_repo.begin_transaction() except Exception as e: self.logger.error(f"Не удалось начать транзакцию: {e}") return False try: # Добавляем донат в базу данных self.logger.debug(f"Добавление доната в базу данных: user_id={user_id}, amount={amount}, year={current_year}") self.logger.debug(f"Вызов user_repo.add_donation с параметрами: user_id={user_id}, amount={amount}, year={current_year}") success = await self.user_repo.add_donation(user_id, amount, current_year) self.logger.debug(f"Результат user_repo.add_donation: {success}") if success: self.logger.info(f"Донат успешно добавлен в базу данных для пользователя {user_id}") # Начисляем очки за донат (1 очко за каждые 100 рублей) points = int(amount // 100) self.logger.debug(f"Расчет очков за донат: amount={amount}, points={points}") if points > 0: self.logger.debug(f"Начисление {points} очков пользователю {user_id}") print(f"[DEBUG] UserService.add_donation: calling score_repo.update_score({user_id}, {points})") score_success = await self.score_repo.update_score(user_id, points) self.logger.debug(f"Результат update_score: {score_success}") if score_success: self.logger.info(f"Начислено {points} очков пользователю {user_id}") else: self.logger.warning(f"Не удалось начислить очки пользователю {user_id}") # Проверяем достижения self.logger.debug(f"Проверка достижений для пользователя {user_id}") await self.check_and_unlock_achievements(user_id) self.logger.debug(f"Проверка достижений завершена для пользователя {user_id}") # Подтверждаем транзакцию await self.user_repo.commit_transaction() await self.score_repo.commit_transaction() self.logger.info(f"Донат успешно обработан для пользователя {user_id}") else: self.logger.error(f"Не удалось добавить донат в базу данных для пользователя {user_id}") # Откатываем транзакцию await self.user_repo.rollback_transaction() await self.score_repo.rollback_transaction() return False return success except Exception as inner_e: # Откатываем транзакцию при ошибке self.logger.error(f"Ошибка во время транзакции, выполняем откат: {inner_e}") try: await self.user_repo.rollback_transaction() await self.score_repo.rollback_transaction() except Exception as rollback_e: self.logger.error(f"Ошибка при откате транзакции: {rollback_e}") raise inner_e except ValidationError as e: self.logger.warning(f"Ошибка валидации при добавлении доната для пользователя {user_id}: {e}") raise except Exception as e: self.logger.error(f"Неожиданная ошибка при добавлении доната для пользователя {user_id}: {e}", exc_info=True) return False async def get_total_donations(self, user_id: int, year: int = None) -> float: """ Получение общей суммы донатов пользователя. Args: user_id: ID пользователя year: Год для фильтрации (если None, то все года) Returns: Общая сумма донатов """ result = await self.user_repo._fetch_one_async( "SELECT COALESCE(SUM(amount), 0) as total FROM donations WHERE user_id = (SELECT id FROM users WHERE telegram_id = ?)" + (" AND year = ?" if year else ""), (user_id, year) if year else (user_id,) ) return result['total'] if result else 0.0