Подготовка к релизу

This commit is contained in:
2025-10-30 18:28:26 +03:00
parent 69c0162c6b
commit 4981db90a0
191 changed files with 45557 additions and 3651 deletions
+13
View File
@@ -0,0 +1,13 @@
"""
Сервисный слой телеграм-бота.
Содержит бизнес-логику и правила работы приложения.
"""
from .user_service import UserService
from .game_service import GameService
from .moderation_service import ModerationService
from .donation_service import DonationService
from .notification_service import NotificationService
from .trigger_service import TriggerService
__all__ = ['UserService', 'GameService', 'ModerationService', 'DonationService', 'NotificationService', 'TriggerService']
+461
View File
@@ -0,0 +1,461 @@
"""
Сервис для обработки донатов через различные платежные системы.
"""
import asyncio
import logging
from typing import Dict, List, Optional, Any, Tuple
from datetime import datetime, timedelta
from dataclasses import dataclass
from core.payment_models import (
PaymentIntent, PaymentEvent, PaymentStatus, TransactionType,
DonationResponse, PaymentValidationResult
)
from core.payment_exceptions import (
PaymentError, PaymentProviderError, PaymentValidationError,
PaymentSecurityError, PaymentDuplicateError, PaymentNotFoundError
)
from services.payment_provider import (
PaymentProvider, StripePaymentProvider, YooKassaPaymentProvider, SBPaymentProvider
)
from database.payment_repository import PaymentRepository, TransactionRepository
from services.user_service import UserService
from services.notification_service import NotificationService
@dataclass
class PaymentConfig:
"""Конфигурация платежного провайдера"""
enabled: bool
api_key: Optional[str] = None
webhook_secret: Optional[str] = None
shop_id: Optional[str] = None
secret_key: Optional[str] = None
bank_id: Optional[str] = None
class DonationService:
"""
Основной сервис для обработки донатов.
Отвечает за:
- Создание платежей через различные провайдеры
- Валидацию платежных данных
- Обработку webhook уведомлений
- Управление статусами платежей
- Интеграцию с пользователями и уведомлениями
"""
def __init__(self, payment_repo: PaymentRepository,
transaction_repo: TransactionRepository,
user_service: UserService,
notification_service: NotificationService,
payment_configs: Dict[str, PaymentConfig]):
"""
Инициализация сервиса донатов.
Args:
payment_repo: Репозиторий платежей
transaction_repo: Репозиторий транзакций
user_service: Сервис пользователей
notification_service: Сервис уведомлений
payment_configs: Конфигурации платежных провайдеров
"""
self.payment_repo = payment_repo
self.transaction_repo = transaction_repo
self.user_service = user_service
self.notification_service = notification_service
self.payment_configs = payment_configs
self.logger = logging.getLogger(__name__)
# Инициализация провайдеров
self.providers: Dict[str, PaymentProvider] = {}
self._initialize_providers()
# Инициализация rate limiter для защиты от злоупотреблений
from core.payment_security import PaymentRateLimiter
self.rate_limiter = PaymentRateLimiter(
max_payments_per_hour=10, # Максимум 10 платежей в час на пользователя
max_amount_per_hour=50000.0 # Максимум 50000 руб в час
)
# Настройки валидации
self.min_amount = 10.0
self.max_amount = 10000.0
self.allowed_currencies = ['RUB', 'USD', 'EUR']
# Настройки безопасности
self.duplicate_check_window_minutes = 30
def _initialize_providers(self):
"""Инициализация платежных провайдеров"""
# Stripe
stripe_config = self.payment_configs.get('stripe')
if stripe_config and stripe_config.enabled:
self.providers['stripe'] = StripePaymentProvider(
api_key=stripe_config.api_key,
webhook_secret=stripe_config.webhook_secret
)
# YooKassa
yookassa_config = self.payment_configs.get('yookassa')
if yookassa_config and yookassa_config.enabled:
self.providers['yookassa'] = YooKassaPaymentProvider(
shop_id=yookassa_config.shop_id,
secret_key=yookassa_config.secret_key
)
# СБП (заглушка)
sbp_config = self.payment_configs.get('sbp')
if sbp_config and sbp_config.enabled:
self.providers['sbp'] = SBPaymentProvider(
api_key=sbp_config.api_key,
bank_id=sbp_config.bank_id,
webhook_secret=sbp_config.webhook_secret
)
async def create_donation(self, user_id: int, amount: float,
provider_name: str = 'stripe',
metadata: Optional[Dict[str, Any]] = None) -> DonationResponse:
"""
Создание доната.
Args:
user_id: ID пользователя
amount: Сумма доната
provider_name: Название провайдера
metadata: Дополнительные данные
Returns:
DonationResponse: Ответ с URL оплаты и ID платежа
Raises:
PaymentValidationError: Ошибка валидации
PaymentProviderError: Ошибка провайдера
"""
try:
self.logger.info(f"Creating donation: user_id={user_id}, amount={amount}, provider={provider_name}")
# Валидация входных данных
validation_result = self._validate_donation_data(user_id, amount, provider_name)
if not validation_result.is_valid:
raise PaymentValidationError(f"Validation failed: {', '.join(validation_result.errors)}")
# Проверка rate limit
rate_limit_result = self.rate_limiter.check_rate_limit(user_id, amount)
if not rate_limit_result['allowed']:
raise PaymentValidationError(
f"Rate limit exceeded. Try again after {rate_limit_result['reset_time']}"
)
# Проверка на дубликаты
if self._check_duplicate_donation(user_id, amount):
raise PaymentDuplicateError("Duplicate donation detected")
# Получение провайдера
provider = self._get_provider(provider_name)
# Подготовка метаданных
if metadata is None:
metadata = {}
metadata.update({
'user_id': str(user_id),
'created_at': datetime.now().isoformat(),
'service': 'telegram_bot_donation'
})
# Создание платежа через провайдера
payment_intent = provider.create_payment(amount, 'RUB', user_id, metadata)
# Сохранение платежа в БД
payment_record = self.payment_repo.create_payment({
'user_id': user_id,
'amount': amount,
'currency': 'RUB',
'provider': provider_name,
'external_id': payment_intent.id,
'status': 'pending',
'metadata': metadata,
'created_at': datetime.now(),
'updated_at': datetime.now()
})
self.logger.info(f"Donation created: payment_id={payment_record['id']}, external_id={payment_intent.id}")
# Создание транзакции
self.transaction_repo.create_transaction({
'payment_id': payment_record['id'],
'type': 'payment',
'amount': amount,
'status': 'pending',
'created_at': datetime.now()
})
# Регистрация платежа в rate limiter для учета лимитов
self.rate_limiter.record_payment(user_id, amount)
return DonationResponse(
payment_url=payment_intent.url,
payment_id=payment_record['id'],
provider=provider_name,
amount=amount,
currency='RUB'
)
except (PaymentValidationError, PaymentDuplicateError, PaymentProviderError):
raise
except Exception as e:
self.logger.error(f"Unexpected error creating donation: {e}", exc_info=True)
raise PaymentError(f"Failed to create donation: {e}")
async def process_payment_webhook(self, provider_name: str, webhook_data: Dict[str, Any],
signature: str) -> bool:
"""
Обработка webhook уведомления от платежного провайдера.
Args:
provider_name: Название провайдера
webhook_data: Данные webhook
signature: Подпись для валидации
Returns:
bool: Успешно ли обработано
Raises:
PaymentSecurityError: Ошибка безопасности
PaymentProviderError: Ошибка провайдера
"""
try:
self.logger.info(f"Processing webhook: provider={provider_name}")
# Получение провайдера
provider = self._get_provider(provider_name)
# Валидация подписи
if not provider.validate_webhook(webhook_data, signature):
raise PaymentSecurityError("Invalid webhook signature")
# Обработка события платежа
payment_event = provider.process_webhook(webhook_data)
# Обработка события
await self._handle_payment_event(payment_event)
return True
except (PaymentSecurityError, PaymentProviderError):
raise
except Exception as e:
self.logger.error(f"Error processing webhook: {e}", exc_info=True)
raise PaymentProviderError(f"Webhook processing failed: {e}")
async def _handle_payment_event(self, event: PaymentEvent):
"""
Обработка события платежа.
Args:
event: Событие платежа
"""
try:
self.logger.info(f"Handling payment event: {event.type} for payment {event.payment_id}")
# Определение типа события
if event.type == 'payment.succeeded':
await self._process_successful_payment(event)
elif event.type == 'payment.failed':
await self._process_failed_payment(event)
elif event.type == 'payment.cancelled':
await self._process_cancelled_payment(event)
elif event.type == 'payment.pending':
await self._process_pending_payment(event)
else:
self.logger.warning(f"Unknown payment event type: {event.type}")
except Exception as e:
self.logger.error(f"Error handling payment event: {e}", exc_info=True)
# Не прерываем обработку, логируем ошибку
async def _process_successful_payment(self, event: PaymentEvent):
"""Обработка успешного платежа"""
try:
# Обновление статуса платежа
success = self.payment_repo.update_payment_status_by_external_id(
event.payment_id, 'succeeded',
processed_at=datetime.now()
)
if not success:
self.logger.error(f"Failed to update payment status for {event.payment_id}")
return
# Получение данных платежа
payment = self.payment_repo.get_payment_by_external_id(event.payment_id)
if not payment:
self.logger.error(f"Payment not found: {event.payment_id}")
return
# Обновление баланса пользователя
await self._update_user_balance(payment['user_id'], event.amount)
# Обновление транзакции
transactions = self.transaction_repo.get_transactions_by_payment(payment['id'])
if transactions:
self.transaction_repo.update_transaction_status(
transactions[0]['id'], 'succeeded'
)
# Уведомление пользователя
if self.notification_service:
await self.notification_service.notify_payment_success(
payment['user_id'], event.amount
)
self.logger.info(f"Successfully processed payment: {event.payment_id}, amount: {event.amount}")
except Exception as e:
self.logger.error(f"Error processing successful payment: {e}", exc_info=True)
async def _process_failed_payment(self, event: PaymentEvent):
"""Обработка неудачного платежа"""
try:
# Обновление статуса платежа
self.payment_repo.update_payment_status_by_external_id(
event.payment_id, 'failed',
processed_at=datetime.now(),
error_message=event.metadata.get('error', 'Payment failed')
)
# Получение данных платежа
payment = self.payment_repo.get_payment_by_external_id(event.payment_id)
if payment and self.notification_service:
await self.notification_service.notify_payment_failed(
payment['user_id'],
event.metadata.get('error', 'Unknown error')
)
self.logger.info(f"Processed failed payment: {event.payment_id}")
except Exception as e:
self.logger.error(f"Error processing failed payment: {e}", exc_info=True)
async def _process_cancelled_payment(self, event: PaymentEvent):
"""Обработка отмененного платежа"""
try:
# Обновление статуса платежа
self.payment_repo.update_payment_status_by_external_id(
event.payment_id, 'cancelled',
processed_at=datetime.now()
)
self.logger.info(f"Processed cancelled payment: {event.payment_id}")
except Exception as e:
self.logger.error(f"Error processing cancelled payment: {e}", exc_info=True)
async def _process_pending_payment(self, event: PaymentEvent):
"""Обработка платежа в ожидании"""
try:
# Обновление статуса платежа
self.payment_repo.update_payment_status_by_external_id(
event.payment_id, 'processing'
)
self.logger.info(f"Processed pending payment: {event.payment_id}")
except Exception as e:
self.logger.error(f"Error processing pending payment: {e}", exc_info=True)
async def _update_user_balance(self, user_id: int, amount: float):
"""Обновление баланса пользователя после успешного платежа"""
try:
# Добавление доната в систему пользователей
await self.user_service.add_donation(user_id, amount)
self.logger.info(f"Updated user balance: user_id={user_id}, amount={amount}")
except Exception as e:
self.logger.error(f"Error updating user balance: {e}", exc_info=True)
def _validate_donation_data(self, user_id: int, amount: float, provider_name: str) -> PaymentValidationResult:
"""Валидация данных доната"""
errors = []
warnings = []
# Проверка user_id
if not isinstance(user_id, int) or user_id <= 0:
errors.append("Invalid user ID")
# Проверка суммы
if not isinstance(amount, (int, float)) or amount <= 0:
errors.append("Amount must be positive number")
elif amount < self.min_amount:
errors.append(f"Minimum amount is {self.min_amount}")
elif amount > self.max_amount:
errors.append(f"Maximum amount is {self.max_amount}")
# Проверка провайдера
if provider_name not in self.providers:
errors.append(f"Unsupported provider: {provider_name}")
elif not self.payment_configs.get(provider_name, PaymentConfig(False)).enabled:
errors.append(f"Provider {provider_name} is disabled")
return PaymentValidationResult(
is_valid=len(errors) == 0,
errors=errors,
warnings=warnings
)
def _check_duplicate_donation(self, user_id: int, amount: float) -> bool:
"""Проверка на дубликат доната"""
return self.payment_repo.check_duplicate_payment(
"", amount, user_id, self.duplicate_check_window_minutes
)
def _get_provider(self, provider_name: str) -> PaymentProvider:
"""Получение провайдера по имени"""
if provider_name not in self.providers:
raise PaymentProviderError(f"Provider {provider_name} not found")
return self.providers[provider_name]
def get_payment_status(self, payment_id: int) -> Optional[Dict[str, Any]]:
"""Получение статуса платежа"""
return self.payment_repo.get_payment_by_id(payment_id)
def get_user_payments(self, user_id: int, limit: int = 10) -> List[Dict[str, Any]]:
"""Получение платежей пользователя"""
return self.payment_repo.get_payments_by_user(user_id, limit)
def get_payment_statistics(self, date_from: Optional[datetime] = None,
date_to: Optional[datetime] = None) -> Dict[str, Any]:
"""Получение статистики платежей"""
return self.payment_repo.get_payment_statistics(date_from, date_to)
def cancel_payment(self, payment_id: int, user_id: int) -> bool:
"""Отмена платежа"""
try:
# Получение данных платежа
payment = self.payment_repo.get_payment_by_id(payment_id)
if not payment:
raise PaymentNotFoundError(f"Payment {payment_id} not found")
if payment['user_id'] != user_id:
raise PaymentSecurityError("Unauthorized payment cancellation")
if payment['status'] not in ['pending', 'processing']:
return False # Нельзя отменить завершенный платеж
# Получение провайдера и отмена платежа
provider = self._get_provider(payment['provider'])
success = provider.cancel_payment(payment['external_id'])
if success:
self.payment_repo.update_payment_status(payment_id, 'cancelled')
return True
return False
except Exception as e:
self.logger.error(f"Error cancelling payment: {e}", exc_info=True)
return False
+874
View File
@@ -0,0 +1,874 @@
"""
Сервис управления играми.
Отвечает за бизнес-логику игровых механик.
"""
import logging
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass
from datetime import datetime, timedelta
from core.exceptions import ValidationError
from utils.validators import InputValidator
import random
@dataclass
class GameSession:
"""Игровая сессия"""
game_id: str
game_type: str
player_id: int
chat_id: int
status: str = "active"
created_at: datetime = None
data: Dict[str, Any] = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now()
if self.data is None:
self.data = {}
class GameService:
"""
Сервис для управления игровыми механиками.
Отвечает за:
- Управление игровыми сессиями
- Логику игр (камень-ножницы-бумага, викторина, etc.)
- Расчет очков за игры
- Статистику игровых достижений
"""
def __init__(self, user_repository, score_repository):
"""
Инициализация сервиса.
Args:
user_repository: Репозиторий пользователей
score_repository: Репозиторий очков и рейтингов
"""
self.user_repo = user_repository
self.score_repo = score_repository
self.logger = logging.getLogger(__name__)
# Настройки игр
self.game_settings = {
'rock_paper_scissors': {
'points_win': 5,
'points_draw': 1
},
'tic_tac_toe': {
'points_win': 15,
'points_draw': 3
},
'quiz': {
'points_correct': 5,
'max_questions': 10
},
'battleship': {
'points_win': 20,
'board_size': 5,
'ships': [3, 2, 1] # размеры кораблей
},
'game_2048': {
'points_win': 20,
'board_size': 4,
'win_value': 2048
},
'tetris': {
'points_per_line': 10,
'board_width': 10,
'board_height': 20
},
'snake': {
'points_per_food': 10,
'board_size': 10,
'initial_length': 3
}
}
# Вопросы для викторины
self.quiz_questions = [
{
'question': 'Столица Франции?',
'answers': ['Париж', 'Лондон', 'Берлин', 'Мадрид'],
'correct': 0
},
{
'question': 'Сколько континентов на Земле?',
'answers': ['5', '6', '7', '8'],
'correct': 2
},
{
'question': 'Какой газ составляет большую часть атмосферы?',
'answers': ['Кислород', 'Азот', 'Углекислый газ', 'Водород'],
'correct': 1
},
{
'question': 'В каком году был запущен первый искусственный спутник?',
'answers': ['1955', '1957', '1959', '1961'],
'correct': 1
}
]
# Активные игровые сессии
self.active_sessions = {}
def create_game_session(self, game_type: str, player_id: int, chat_id: int) -> GameSession:
"""
Создание новой игровой сессии.
Args:
game_type: Тип игры
player_id: ID игрока
chat_id: ID чата
Returns:
Игровая сессия
"""
if game_type not in self.game_settings:
raise ValidationError(f"Неизвестный тип игры: {game_type}")
import random
game_id = f"{game_type[:3]}_{random.randint(100000, 999999)}"
# Проверяем, что game_id не пустой
if not game_id:
raise ValidationError(f"Не удалось сгенерировать game_id для игры {game_type}")
self.logger.info(f"Создан game_id: {game_id} для игры {game_type}")
session = GameSession(
game_id=game_id,
game_type=game_type,
player_id=player_id,
chat_id=chat_id,
status="active"
)
# Инициализируем данные игры в зависимости от типа
if game_type == 'rock_paper_scissors':
session.data = {
'player_choice': None,
'bot_choice': None,
'result': None
}
elif game_type == 'tic_tac_toe':
session.data = {
'board': [' '] * 9,
'current_player': 'X',
'moves': 0,
'winner': None
}
elif game_type == 'quiz':
session.data = {
'current_question': 0,
'score': 0,
'questions': random.sample(self.quiz_questions, 3)
}
elif game_type == 'battleship':
session.data = {
'player_board': [['~'] * 5 for _ in range(5)],
'bot_board': [['~'] * 5 for _ in range(5)],
'bot_ships': self._place_battleship_ships(),
'player_shots': 0,
'player_hits': 0,
'game_status': 'active'
}
elif game_type == 'game_2048':
session.data = {
'board': [[0] * 4 for _ in range(4)],
'score': 0,
'moves': 0,
'game_status': 'active'
}
# Добавляем две начальные плитки
self._add_random_2048_tile(session.data['board'])
self._add_random_2048_tile(session.data['board'])
elif game_type == 'tetris':
session.data = {
'board': [[0] * 10 for _ in range(20)],
'current_piece': self._get_random_tetris_piece(),
'score': 0,
'lines_cleared': 0,
'level': 1,
'game_status': 'active'
}
elif game_type == 'snake':
session.data = {
'board': [[0] * 10 for _ in range(10)],
'snake': [(5, 5)],
'food': None,
'direction': 'right',
'score': 0,
'game_status': 'active'
}
# Размещаем первую еду
session.data['food'] = self._place_snake_food(session.data['board'], session.data['snake'])
self.active_sessions[game_id] = session
self.logger.info(f"Сессия сохранена в active_sessions: {repr(game_id)}, активные сессии: {list(self.active_sessions.keys())}")
return session
def get_game_session(self, game_id: str) -> Optional[GameSession]:
"""Получение игровой сессии по ID"""
self.logger.info(f"Поиск сессии для game_id: {repr(game_id)}, активные сессии: {list(self.active_sessions.keys())}")
session = self.active_sessions.get(game_id)
if not session:
# self.logger.error(f"Сессия не найдена для game_id: {repr(game_id)}, активные сессии: {list(self.active_sessions.keys())}")
return None
if session.status != "active":
self.logger.warning(f"Сессия {repr(game_id)} не активна (status: {session.status})")
return None
return session
def end_game_session(self, game_id: str) -> bool:
"""Завершение игровой сессии"""
self.logger.info(f"Завершение сессии для game_id: {repr(game_id)}, активные сессии: {list(self.active_sessions.keys())}")
if game_id in self.active_sessions:
self.active_sessions[game_id].status = "completed"
del self.active_sessions[game_id]
self.logger.info(f"Сессия {repr(game_id)} завершена и удалена")
return True
self.logger.warning(f"Сессия {repr(game_id)} не найдена для завершения")
return False
def play_rock_paper_scissors(self, game_id: str, player_choice: str) -> Dict[str, Any]:
"""
Игра в камень-ножницы-бумага.
Args:
game_id: ID игровой сессии
player_choice: Выбор игрока (rock, paper, scissors, или на русском: камень, ножницы, бумага)
Returns:
Результат игры
"""
session = self.get_game_session(game_id)
if not session or session.game_type != 'rock_paper_scissors':
raise ValidationError("Игра не найдена или неверный тип игры")
# Mapping для поддержки русского ввода
choice_mapping = {
'камень': 'rock',
'ножницы': 'scissors',
'бумага': 'paper',
'rock': 'rock',
'paper': 'paper',
'scissors': 'scissors'
}
normalized_choice = choice_mapping.get(player_choice.lower(), player_choice)
valid_choices = ['rock', 'paper', 'scissors']
if normalized_choice not in valid_choices:
raise ValidationError(f"Неверный выбор: {player_choice}. Допустимые: камень, ножницы, бумага или rock, paper, scissors")
# Определяем выбор бота
bot_choice = random.choice(valid_choices)
# Определяем результат
if normalized_choice == bot_choice:
result = 'draw'
points = self.game_settings['rock_paper_scissors']['points_draw']
elif (normalized_choice == 'rock' and bot_choice == 'scissors') or \
(normalized_choice == 'paper' and bot_choice == 'rock') or \
(normalized_choice == 'scissors' and bot_choice == 'paper'):
result = 'win'
points = self.game_settings['rock_paper_scissors']['points_win']
else:
result = 'lose'
points = 0
# Обновляем данные сессии
session.data.update({
'player_choice': normalized_choice,
'bot_choice': bot_choice,
'result': result
})
# Начисляем очки игроку
if points > 0:
# Здесь будет вызов score_repository для начисления очков
pass
return {
'result': result,
'player_choice': normalized_choice,
'bot_choice': bot_choice,
'points': points
}
def make_tic_tac_toe_move(self, game_id: str, position: int) -> Dict[str, Any]:
"""
Ход в крестики-нолики.
Args:
game_id: ID игровой сессии
position: Позиция хода (0-8)
Returns:
Результат хода
"""
session = self.get_game_session(game_id)
if not session or session.game_type != 'tic_tac_toe':
raise ValidationError("Игра не найдена или неверный тип игры")
if not (0 <= position <= 8):
raise ValidationError("Неверная позиция хода")
board = session.data['board']
if board[position] != ' ':
# Клетка уже занята, возвращаем текущее состояние
return {
'status': 'already_occupied',
'board': board,
'points': 0
}
# Ход игрока
board[position] = 'X'
session.data['moves'] += 1
session.data['board'] = board
# Проверяем победу игрока
winner = self._check_tic_tac_toe_winner(board)
if winner:
session.data['winner'] = 'player'
points = self.game_settings['tic_tac_toe']['points_win']
return {
'status': 'player_win',
'board': board,
'winner': 'player',
'points': points
}
# Проверяем ничью
if session.data['moves'] >= 9:
session.data['winner'] = 'draw'
points = self.game_settings['tic_tac_toe']['points_draw']
return {
'status': 'draw',
'board': board,
'winner': 'draw',
'points': points
}
# Ход бота (простая логика - случайный ход)
available_moves = [i for i, cell in enumerate(board) if cell == ' ']
if available_moves:
bot_position = random.choice(available_moves)
board[bot_position] = 'O'
session.data['moves'] += 1
session.data['board'] = board
# Проверяем победу бота
winner = self._check_tic_tac_toe_winner(board)
if winner:
session.data['winner'] = 'bot'
return {
'status': 'bot_win',
'board': board,
'winner': 'bot',
'points': 0
}
return {
'status': 'continue',
'board': board,
'points': 0
}
def answer_quiz_question(self, game_id: str, answer_index: int) -> Dict[str, Any]:
"""
Ответ на вопрос викторины.
Args:
game_id: ID игровой сессии
answer_index: Индекс ответа
Returns:
Результат ответа
"""
session = self.get_game_session(game_id)
if not session or session.game_type != 'quiz':
raise ValidationError("Игра не найдена или неверный тип игры")
current_q = session.data['current_question']
questions = session.data['questions']
if current_q >= len(questions):
raise ValidationError("Все вопросы отвечены")
question = questions[current_q]
if not (0 <= answer_index < len(question['answers'])):
raise ValidationError("Неверный индекс ответа")
# Проверяем ответ
is_correct = (answer_index == question['correct'])
if is_correct:
session.data['score'] += self.game_settings['quiz']['points_correct']
# Переходим к следующему вопросу
session.data['current_question'] += 1
current_q += 1
# Проверяем завершение игры
if current_q >= len(questions):
final_score = session.data['score']
return {
'status': 'game_over',
'correct': is_correct,
'final_score': final_score,
'points': final_score
}
return {
'status': 'continue',
'correct': is_correct,
'current_score': session.data['score']
}
def _check_tic_tac_toe_winner(self, board: List[str]) -> Optional[str]:
"""Проверка победителя в крестики-нолики"""
# Проверяем строки
for i in range(0, 9, 3):
if board[i] == board[i+1] == board[i+2] != ' ':
return board[i]
# Проверяем столбцы
for i in range(3):
if board[i] == board[i+3] == board[i+6] != ' ':
return board[i]
# Проверяем диагонали
if board[0] == board[4] == board[8] != ' ':
return board[0]
if board[2] == board[4] == board[6] != ' ':
return board[2]
return None
def get_game_statistics(self, user_id: int) -> Dict[str, Any]:
"""Получение игровой статистики пользователя"""
# Здесь будет получение статистики из базы данных
return {
'games_played': 0,
'games_won': 0,
'games_lost': 0,
'total_points': 0,
'favorite_game': None
}
# ===== МОРСКОЙ БОЙ =====
def make_battleship_shot(self, game_id: str, row: int, col: int) -> Dict[str, Any]:
"""Ход в морском бое"""
session = self.get_game_session(game_id)
if not session or session.game_type != 'battleship':
raise ValidationError("Игра не найдена или неверный тип игры")
data = session.data
bot_board = data['bot_board']
bot_ships = data['bot_ships']
if not (0 <= row < 5 and 0 <= col < 5):
raise ValidationError("Неверные координаты")
if bot_board[row][col] != '~':
# Уже стреляли, возвращаем текущее состояние
return {
'status': 'already_shot',
'board': bot_board,
'hit': False,
'shots': data['player_shots'],
'points': 0
}
# Определяем попадание
hit = False
for ship in bot_ships:
if (row, col) in ship:
hit = True
bot_board[row][col] = '💥'
data['player_hits'] += 1
break
if not hit:
bot_board[row][col] = '💧'
data['player_shots'] += 1
session.data = data
# Проверяем победу
total_ship_cells = sum(len(ship) for ship in bot_ships)
if data['player_hits'] >= total_ship_cells:
data['game_status'] = 'won'
return {
'status': 'win',
'board': bot_board,
'hit': hit,
'shots': data['player_shots'],
'points': self.game_settings['battleship']['points_win']
}
return {
'status': 'continue',
'board': bot_board,
'hit': hit,
'shots': data['player_shots'],
'points': 0
}
def _place_battleship_ships(self) -> List[List[Tuple[int, int]]]:
"""Размещение кораблей бота"""
ships = []
ship_sizes = self.game_settings['battleship']['ships'].copy()
for size in ship_sizes:
placed = False
attempts = 0
while not placed and attempts < 100:
direction = random.randint(0, 1) # 0 - горизонтально, 1 - вертикально
if direction == 0:
row = random.randint(0, 4)
col = random.randint(0, 4 - size)
ship_coords = [(row, col + i) for i in range(size)]
else:
row = random.randint(0, 4 - size)
col = random.randint(0, 4)
ship_coords = [(row + i, col) for i in range(size)]
# Проверяем пересечения
if not any(coord in [c for ship in ships for c in ship] for coord in ship_coords):
ships.append(ship_coords)
placed = True
attempts += 1
return ships
# ===== ИГРА 2048 =====
def make_2048_move(self, game_id: str, direction: str) -> Dict[str, Any]:
"""Ход в игре 2048"""
session = self.get_game_session(game_id)
if not session or session.game_type != 'game_2048':
raise ValidationError("Игра не найдена или неверный тип игры")
data = session.data
board = data['board']
valid_directions = ['left', 'right', 'up', 'down']
if direction not in valid_directions:
raise ValidationError(f"Неверное направление: {direction}")
# Сохраняем доску для сравнения
old_board = [row[:] for row in board]
# Делаем ход
moved, score_gained = self._move_2048_board(board, direction)
data['score'] += score_gained
data['moves'] += 1
if moved:
self._add_random_2048_tile(board)
# Проверяем победу
if self._check_2048_win(board):
data['game_status'] = 'won'
return {
'status': 'win',
'board': board,
'score': data['score'],
'moved': moved,
'points': self.game_settings['game_2048']['points_win']
}
# Проверяем поражение
if not self._can_move_2048(board):
data['game_status'] = 'lost'
return {
'status': 'lose',
'board': board,
'score': data['score'],
'moved': moved,
'points': 0
}
return {
'status': 'continue',
'board': board,
'score': data['score'],
'moved': moved,
'moves': data['moves'],
'points': 0
}
def _move_2048_board(self, board: List[List[int]], direction: str) -> Tuple[bool, int]:
"""Движение доски 2048"""
def move_row_left(row):
new_row = [num for num in row if num != 0]
score = 0
for i in range(len(new_row) - 1):
if new_row[i] == new_row[i + 1]:
new_row[i] *= 2
score += new_row[i]
new_row[i + 1] = 0
new_row = [num for num in new_row if num != 0]
return new_row + [0] * (4 - len(new_row)), score
moved = False
total_score = 0
if direction == 'left':
for i in range(4):
old_row = board[i][:]
board[i], score = move_row_left(board[i])
if board[i] != old_row:
moved = True
total_score += score
elif direction == 'right':
for i in range(4):
old_row = board[i][::-1]
new_row, score = move_row_left(old_row)
board[i] = new_row[::-1]
if board[i] != old_row[::-1]:
moved = True
total_score += score
elif direction == 'up':
for j in range(4):
col = [board[i][j] for i in range(4)]
old_col = col[:]
new_col, score = move_row_left(col)
for i in range(4):
board[i][j] = new_col[i]
if new_col != old_col:
moved = True
total_score += score
elif direction == 'down':
for j in range(4):
col = [board[i][j] for i in range(3, -1, -1)]
old_col = col[:]
new_col, score = move_row_left(col)
for i in range(3, -1, -1):
board[3 - i][j] = new_col[i]
if new_col != old_col:
moved = True
total_score += score
return moved, total_score
def _add_random_2048_tile(self, board: List[List[int]]):
"""Добавление случайной плитки"""
empty_cells = [(i, j) for i in range(4) for j in range(4) if board[i][j] == 0]
if empty_cells:
i, j = random.choice(empty_cells)
board[i][j] = 2 if random.random() < 0.9 else 4
def _check_2048_win(self, board: List[List[int]]) -> bool:
"""Проверка победы в 2048"""
return any(2048 in row for row in board)
def _can_move_2048(self, board: List[List[int]]) -> bool:
"""Проверка возможности хода"""
for i in range(4):
for j in range(4):
if board[i][j] == 0:
return True
if i > 0 and board[i][j] == board[i-1][j]:
return True
if j > 0 and board[i][j] == board[i][j-1]:
return True
return False
# ===== ТЕТРИС =====
def make_tetris_move(self, game_id: str, action: str) -> Dict[str, Any]:
"""Ход в тетрисе"""
session = self.get_game_session(game_id)
if not session or session.game_type != 'tetris':
raise ValidationError("Игра не найдена или неверный тип игры")
data = session.data
board = data['board']
piece = data['current_piece']
if action == 'left' and piece['x'] > 0:
piece['x'] -= 1
if not self._is_valid_tetris_position(board, piece):
piece['x'] += 1
elif action == 'right' and piece['x'] < 10 - len(piece['shape'][0]):
piece['x'] += 1
if not self._is_valid_tetris_position(board, piece):
piece['x'] -= 1
elif action == 'down':
piece['y'] += 1
if not self._is_valid_tetris_position(board, piece):
piece['y'] -= 1
# Фиксируем фигуру
self._place_tetris_piece(board, piece)
lines_cleared = self._clear_tetris_lines(board)
data['score'] += lines_cleared * self.game_settings['tetris']['points_per_line']
data['lines_cleared'] += lines_cleared
# Создаем новую фигуру
piece = self._get_random_tetris_piece()
if not self._is_valid_tetris_position(board, piece):
data['game_status'] = 'game_over'
return {
'status': 'game_over',
'board': board,
'score': data['score'],
'lines_cleared': data['lines_cleared'],
'points': 0
}
data['current_piece'] = piece
elif action == 'rotate':
old_shape = piece['shape'][:]
piece['shape'] = list(zip(*piece['shape'][::-1]))
if not self._is_valid_tetris_position(board, piece):
piece['shape'] = old_shape
return {
'status': 'continue',
'board': board,
'piece': piece,
'score': data['score'],
'lines_cleared': data['lines_cleared'],
'level': data['level'],
'points': 0
}
def _is_valid_tetris_position(self, board: List[List[int]], piece: Dict) -> bool:
"""Проверка валидности позиции тетрис-фигуры"""
for i, row in enumerate(piece['shape']):
for j, cell in enumerate(row):
if cell:
x, y = piece['x'] + j, piece['y'] + i
if x < 0 or x >= 10 or y >= 20 or (y >= 0 and board[y][x] != 0):
return False
return True
def _place_tetris_piece(self, board: List[List[int]], piece: Dict):
"""Размещение тетрис-фигуры на доске"""
for i, row in enumerate(piece['shape']):
for j, cell in enumerate(row):
if cell:
x, y = piece['x'] + j, piece['y'] + i
if 0 <= y < 20 and 0 <= x < 10:
board[y][x] = 1
def _clear_tetris_lines(self, board: List[List[int]]) -> int:
"""Очистка заполненных линий"""
lines_cleared = 0
y = 19
while y >= 0:
if all(board[y]):
del board[y]
board.insert(0, [0] * 10)
lines_cleared += 1
else:
y -= 1
return lines_cleared
def _get_random_tetris_piece(self) -> Dict:
"""Получение случайной тетрис-фигуры"""
pieces = [
{'shape': [[1, 1, 1, 1]], 'x': 3, 'y': 0}, # I
{'shape': [[1, 1], [1, 1]], 'x': 4, 'y': 0}, # O
{'shape': [[1, 0, 0], [1, 1, 1]], 'x': 3, 'y': 0}, # J
{'shape': [[0, 0, 1], [1, 1, 1]], 'x': 3, 'y': 0}, # L
{'shape': [[0, 1, 1], [1, 1, 0]], 'x': 3, 'y': 0}, # S
{'shape': [[1, 1, 0], [0, 1, 1]], 'x': 3, 'y': 0}, # Z
{'shape': [[0, 1, 0], [1, 1, 1]], 'x': 3, 'y': 0}, # T
]
return random.choice(pieces)
# ===== ЗМЕЙКА =====
def make_snake_move(self, game_id: str, direction: str) -> Dict[str, Any]:
"""Ход в змейке"""
session = self.get_game_session(game_id)
if not session or session.game_type != 'snake':
raise ValidationError("Игра не найдена или неверный тип игры")
data = session.data
snake = data['snake']
food = data['food']
current_direction = data['direction']
# Проверяем допустимость направления
opposites = {'up': 'down', 'down': 'up', 'left': 'right', 'right': 'left'}
if direction == opposites.get(current_direction):
raise ValidationError("Нельзя двигаться в противоположном направлении")
# Вычисляем новую голову
head = snake[0]
if direction == 'up':
new_head = (head[0] - 1, head[1])
elif direction == 'down':
new_head = (head[0] + 1, head[1])
elif direction == 'left':
new_head = (head[0], head[1] - 1)
elif direction == 'right':
new_head = (head[0], head[1] + 1)
else:
raise ValidationError(f"Неверное направление: {direction}")
# Проверяем столкновения
if (new_head[0] < 0 or new_head[0] >= 10 or
new_head[1] < 0 or new_head[1] >= 10 or
new_head in snake):
data['game_status'] = 'game_over'
return {
'status': 'game_over',
'snake': snake,
'food': food,
'score': data['score'],
'length': len(snake),
'points': 0
}
# Добавляем новую голову
snake.insert(0, new_head)
data['direction'] = direction
# Проверяем еду
if new_head == food:
data['score'] += self.game_settings['snake']['points_per_food']
food = self._place_snake_food(data['board'], snake)
data['food'] = food
else:
# Удаляем хвост если не съели еду
snake.pop()
return {
'status': 'continue',
'snake': snake,
'food': food,
'score': data['score'],
'length': len(snake),
'points': 0
}
def _place_snake_food(self, board: List[List[int]], snake: List[Tuple[int, int]]) -> Tuple[int, int]:
"""Размещение еды для змейки"""
empty_cells = [(i, j) for i in range(10) for j in range(10) if (i, j) not in snake]
if empty_cells:
return random.choice(empty_cells)
return None
def cleanup_old_sessions(self, max_age_minutes: int = 30):
"""Очистка старых игровых сессий"""
current_time = datetime.now()
cutoff_time = current_time - timedelta(minutes=max_age_minutes)
to_remove = []
for game_id, session in self.active_sessions.items():
if session.created_at < cutoff_time:
to_remove.append(game_id)
for game_id in to_remove:
del self.active_sessions[game_id]
+434
View File
@@ -0,0 +1,434 @@
"""
Сервис модерации пользователей.
Отвечает за бизнес-логику модерации и администрирования.
"""
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass
from datetime import datetime, timedelta
from core.exceptions import ValidationError, PermissionError
from utils.validators import InputValidator
import re
@dataclass
class ModerationAction:
"""Действие модерации"""
action_id: str
user_id: int
admin_id: int
action_type: str
reason: str
duration: Optional[int] = None # в секундах для временных наказаний
created_at: datetime = None
expires_at: Optional[datetime] = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now()
if self.duration and self.expires_at is None:
self.expires_at = self.created_at + timedelta(seconds=self.duration)
class ModerationService:
"""
Сервис для модерации пользователей и администрирования.
Отвечает за:
- Управление предупреждениями
- Блокировку и разблокировку пользователей
- Модерацию контента
- Проверку прав администраторов
- Логирование действий модерации
"""
def __init__(self, user_repository, score_repository):
"""
Инициализация сервиса.
Args:
user_repository: Репозиторий пользователей
score_repository: Репозиторий очков и рейтингов
"""
self.user_repo = user_repository
self.score_repo = score_repository
# Настройки модерации
self.moderation_settings = {
'max_warnings': 3,
'ban_threshold': 5,
'mute_duration': 300, # 5 минут по умолчанию
'profanity_filter_enabled': True,
'auto_moderate_media': False
}
# Список запрещенных слов
self.profanity_words = [
'блядь', 'бля', 'блять', 'хуй', 'хуйня', 'пизда', 'пиздец',
'ебать', 'ебаный', 'сука', 'сукин', 'мудак', 'говно',
'fuck', 'shit', 'damn', 'asshole', 'bastard', 'cunt', 'dick', 'bitch'
]
# Активные модерационные действия
self.active_actions = {}
async def warn_user(self, user_id: int, reason: str, admin_id: int) -> Dict[str, Any]:
"""
Выдача предупреждения пользователю.
Args:
user_id: ID пользователя
reason: Причина предупреждения
admin_id: ID администратора
Returns:
Результат действия
"""
if not InputValidator.validate_text_content(reason, max_length=500):
raise ValidationError("Неверная причина предупреждения")
# Добавляем предупреждение в базу данных
print(f"[DEBUG] ModerationService.warn_user: calling user_repo.add_warning({user_id}, '{reason}', {admin_id})")
success = self.user_repo.add_warning(user_id, reason, admin_id)
if not success:
raise ValidationError("Не удалось выдать предупреждение")
# Получаем обновленное количество предупреждений
print(f"[DEBUG] ModerationService.warn_user: calling user_repo.get_user_warnings({user_id})")
current_warnings = self.user_repo.get_user_warnings(user_id)
# Проверяем необходимость автоматических действий
action_taken = None
if current_warnings >= self.moderation_settings['ban_threshold']:
action_taken = 'auto_ban'
await self.ban_user(user_id, "Автоматический бан за превышение лимита предупреждений", admin_id, permanent=True)
elif current_warnings >= self.moderation_settings['max_warnings']:
action_taken = 'auto_mute'
await self.mute_user(user_id, "Автоматическая заглушка за превышение лимита предупреждений", admin_id)
return {
'success': True,
'warnings_count': current_warnings,
'action_taken': action_taken,
'reason': reason
}
async def mute_user(self, user_id: int, reason: str, admin_id: int, duration: int = None) -> Dict[str, Any]:
"""
Заглушка пользователя.
Args:
user_id: ID пользователя
reason: Причина заглушки
admin_id: ID администратора
duration: Длительность в секундах
Returns:
Результат действия
"""
if duration is None:
duration = self.moderation_settings['mute_duration']
if not InputValidator.validate_number(duration, min_value=1, max_value=86400): # макс 24 часа
raise ValidationError("Неверная длительность заглушки")
# Создаем действие модерации
action_id = f"mute_{user_id}_{admin_id}_{datetime.now().strftime('%H%M%S')}"
action = ModerationAction(
action_id=action_id,
user_id=user_id,
admin_id=admin_id,
action_type='mute',
reason=reason,
duration=duration
)
self.active_actions[action_id] = action
# Логируем действие в базу данных
await self._log_moderation_action(user_id, admin_id, 'mute', reason, duration)
return {
'success': True,
'action_id': action_id,
'duration': duration,
'expires_at': action.expires_at
}
async def ban_user(self, user_id: int, reason: str, admin_id: int, permanent: bool = False) -> Dict[str, Any]:
"""
Блокировка пользователя.
Args:
user_id: ID пользователя
reason: Причина блокировки
admin_id: ID администратора
permanent: Постоянная блокировка
Returns:
Результат действия
"""
duration = None if permanent else (24 * 3600) # 24 часа по умолчанию
# Создаем действие модерации
action_id = f"ban_{user_id}_{admin_id}_{datetime.now().strftime('%H%M%S')}"
action = ModerationAction(
action_id=action_id,
user_id=user_id,
admin_id=admin_id,
action_type='ban',
reason=reason,
duration=duration
)
self.active_actions[action_id] = action
# Логируем действие в базу данных
await self._log_moderation_action(user_id, admin_id, 'ban', reason, duration)
return {
'success': True,
'action_id': action_id,
'permanent': permanent,
'duration': duration,
'expires_at': action.expires_at if not permanent else None
}
async def unban_user(self, user_id: int, admin_id: int) -> bool:
"""
Разблокировка пользователя.
Args:
user_id: ID пользователя
admin_id: ID администратора
Returns:
True если разблокировка успешна
"""
# Удаляем все активные действия бана для пользователя
to_remove = []
for action_id, action in self.active_actions.items():
if action.user_id == user_id and action.action_type == 'ban':
to_remove.append(action_id)
for action_id in to_remove:
del self.active_actions[action_id]
# Логируем действие в базу данных
await self._log_moderation_action(user_id, admin_id, 'unban', "Разблокировка пользователя")
return len(to_remove) > 0
async def unmute_user(self, user_id: int, admin_id: int) -> bool:
"""
Снятие заглушки с пользователя.
Args:
user_id: ID пользователя
admin_id: ID администратора
Returns:
True если снятие заглушки успешно
"""
# Удаляем все активные действия mute для пользователя
to_remove = []
for action_id, action in self.active_actions.items():
if action.user_id == user_id and action.action_type == 'mute':
to_remove.append(action_id)
for action_id in to_remove:
del self.active_actions[action_id]
# Логируем действие в базу данных
await self._log_moderation_action(user_id, admin_id, 'unmute', "Снятие заглушки")
return len(to_remove) > 0
def check_profanity(self, text: str) -> List[str]:
"""
Проверка текста на нецензурную лексику.
Args:
text: Проверяемый текст
Returns:
Список найденных запрещенных слов
"""
if not self.moderation_settings['profanity_filter_enabled']:
return []
found_words = []
text_lower = text.lower()
for word in self.profanity_words:
if word in text_lower:
found_words.append(word)
return found_words
async def moderate_message(self, user_id: int, text: str, chat_id: int) -> Dict[str, Any]:
"""
Модерация сообщения пользователя.
Args:
user_id: ID пользователя
text: Текст сообщения
chat_id: ID чата
Returns:
Результат модерации
"""
# Проверяем на нецензурную лексику
profanity_words = self.check_profanity(text)
if profanity_words:
# Автоматически выдаем предупреждение за нецензурную лексику
admin_id = 0 # Системный администратор для авто-модерации
await self.warn_user(user_id, f"Нецензурная лексика: {', '.join(profanity_words)}", admin_id)
return {
'action': 'warning',
'reason': 'Нецензурная лексика',
'profanity_words': profanity_words
}
return {
'action': 'none',
'clean': True
}
def is_user_muted(self, user_id: int) -> bool:
"""
Проверка, заглушен ли пользователь.
Args:
user_id: ID пользователя
Returns:
True если пользователь заглушен
"""
current_time = datetime.now()
for action in self.active_actions.values():
if (action.user_id == user_id and
action.action_type == 'mute' and
action.expires_at and
current_time < action.expires_at):
return True
return False
def is_user_banned(self, user_id: int) -> bool:
"""
Проверка, забанен ли пользователь.
Args:
user_id: ID пользователя
Returns:
True если пользователь забанен
"""
current_time = datetime.now()
for action in self.active_actions.values():
if (action.user_id == user_id and
action.action_type == 'ban' and
(action.expires_at is None or current_time < action.expires_at)):
return True
return False
async def get_moderation_stats(self) -> Dict[str, Any]:
"""
Получение статистики модерации.
Returns:
Статистика модерации
"""
# Подсчитываем активные действия
mutes = sum(1 for action in self.active_actions.values() if action.action_type == 'mute')
bans = sum(1 for action in self.active_actions.values() if action.action_type == 'ban')
return {
'active_mutes': mutes,
'active_bans': bans,
'total_actions': len(self.active_actions),
'profanity_filter_enabled': self.moderation_settings['profanity_filter_enabled']
}
async def get_user_moderation_history(self, user_id: int, limit: int = 10) -> List[Dict[str, Any]]:
"""
Получение истории модерации пользователя.
Args:
user_id: ID пользователя
limit: Максимальное количество записей
Returns:
История модерации
"""
# Здесь будет получение истории из базы данных
return []
async def _log_moderation_action(self, user_id: int, admin_id: int, action_type: str,
reason: str, duration: int = None):
"""
Логирование действия модерации в базу данных.
Args:
user_id: ID пользователя
admin_id: ID администратора
action_type: Тип действия
reason: Причина
duration: Длительность (для временных действий)
"""
# Здесь будет сохранение в базу данных через репозиторий
pass
def cleanup_expired_actions(self):
"""Очистка истекших модерационных действий"""
current_time = datetime.now()
to_remove = []
for action_id, action in self.active_actions.items():
if (action.expires_at and current_time > action.expires_at):
to_remove.append(action_id)
for action_id in to_remove:
del self.active_actions[action_id]
def add_profanity_word(self, word: str) -> bool:
"""
Добавление слова в фильтр нецензурной лексики.
Args:
word: Слово для добавления
Returns:
True если слово добавлено
"""
if not InputValidator.validate_text_content(word, max_length=50):
return False
if word.lower() not in self.profanity_words:
self.profanity_words.append(word.lower())
return True
return False
def remove_profanity_word(self, word: str) -> bool:
"""
Удаление слова из фильтра нецензурной лексики.
Args:
word: Слово для удаления
Returns:
True если слово удалено
"""
if word.lower() in self.profanity_words:
self.profanity_words.remove(word.lower())
return True
return False
+402
View File
@@ -0,0 +1,402 @@
"""
Сервис для отправки уведомлений пользователям о платежах.
"""
import logging
import asyncio
import time
from typing import Optional, Dict, Any
from telegram import Bot
from telegram.error import TelegramError, NetworkError, TimedOut, RetryAfter
class NotificationService:
"""
Сервис для отправки уведомлений пользователям.
Отвечает за:
- Уведомления об успешных платежах
- Уведомления о неудачных платежах
- Уведомления администраторам о проблемах
"""
def __init__(self, bot_token: str, admin_chat_ids: Optional[list] = None):
"""
Инициализация сервиса уведомлений.
Args:
bot_token: Токен Telegram бота
admin_chat_ids: Список ID чатов администраторов
"""
self.bot = Bot(token=bot_token)
self.admin_chat_ids = admin_chat_ids or []
self.logger = logging.getLogger(__name__)
# Статистика отправки уведомлений
self.stats = {
'successful_sends': 0,
'failed_sends': 0,
'network_errors': 0,
'timeout_errors': 0,
'rate_limit_errors': 0,
'other_errors': 0
}
# Настройки повторных попыток
self.max_retries = 3
self.base_retry_delay = 1.0 # секунды
self.max_retry_delay = 30.0 # секунды
# Rate limiting
self.last_request_time = 0
self.min_request_interval = 0.1 # минимум 100ms между запросами
async def notify_payment_success(self, user_id: int, amount: float, currency: str = "RUB"):
"""
Уведомление об успешном платеже.
Args:
user_id: ID пользователя
amount: Сумма платежа
currency: Валюта
"""
try:
message = (
f"✅ <b>Спасибо за донат!</b>\n\n"
f"💰 Сумма: <code>{amount:.2f} {currency}</code>\n"
f"🎯 Средства успешно зачислены на ваш счет!\n\n"
f"Вы помогаете развитию проекта и получаете бонусные очки за поддержку. "
f"Посмотреть свою статистику можно командой /rank"
)
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode='HTML'
)
self.logger.info(f"Sent success notification to user {user_id} for amount {amount}")
except TelegramError as e:
self.logger.error(f"Failed to send success notification to user {user_id}: {e}")
except Exception as e:
self.logger.error(f"Unexpected error sending success notification: {e}")
async def notify_payment_failed(self, user_id: int, reason: str = "Неизвестная ошибка"):
"""
Уведомление о неудачном платеже.
Args:
user_id: ID пользователя
reason: Причина неудачи
"""
try:
message = (
f"❌ <b>Платеж не прошел</b>\n\n"
f"💡 Причина: <code>{reason}</code>\n\n"
f"Попробуйте еще раз или обратитесь в поддержку, "
f"если проблема сохраняется."
)
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode='HTML'
)
self.logger.info(f"Sent failure notification to user {user_id}: {reason}")
except TelegramError as e:
self.logger.error(f"Failed to send failure notification to user {user_id}: {e}")
except Exception as e:
self.logger.error(f"Unexpected error sending failure notification: {e}")
async def notify_payment_cancelled(self, user_id: int):
"""
Уведомление об отмене платежа.
Args:
user_id: ID пользователя
"""
try:
message = (
f"🚫 <b>Платеж отменен</b>\n\n"
f"Если это было сделано по ошибке, попробуйте создать новый платеж."
)
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode='HTML'
)
self.logger.info(f"Sent cancellation notification to user {user_id}")
except TelegramError as e:
self.logger.error(f"Failed to send cancellation notification to user {user_id}: {e}")
except Exception as e:
self.logger.error(f"Unexpected error sending cancellation notification: {e}")
async def notify_payment_pending(self, user_id: int, payment_url: str, amount: float):
"""
Уведомление о том, что платеж находится в обработке.
Args:
user_id: ID пользователя
payment_url: URL для оплаты
amount: Сумма платежа
"""
try:
message = (
f"⏳ <b>Платеж в обработке</b>\n\n"
f"💰 Сумма: <code>{amount:.2f} RUB</code>\n"
f"🔗 <a href=\"{payment_url}\">Завершить оплату</a>\n\n"
f"Пожалуйста, завершите оплату по ссылке выше."
)
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode='HTML',
disable_web_page_preview=True
)
self.logger.info(f"Sent pending notification to user {user_id}")
except TelegramError as e:
self.logger.error(f"Failed to send pending notification to user {user_id}: {e}")
except Exception as e:
self.logger.error(f"Unexpected error sending pending notification: {e}")
async def notify_admin_payment_issue(self, payment_id: str, error: str, user_id: Optional[int] = None):
"""
Уведомление администраторов о проблеме с платежом.
Args:
payment_id: ID платежа
error: Описание проблемы
user_id: ID пользователя (опционально)
"""
try:
user_info = f" (Пользователь: {user_id})" if user_id else ""
message = (
f"🚨 <b>Проблема с платежом</b>\n\n"
f"🆔 ID платежа: <code>{payment_id}</code>{user_info}\n"
f"❌ Ошибка: <code>{error}</code>\n\n"
f"Требуется ручная проверка."
)
for admin_chat_id in self.admin_chat_ids:
try:
await self.bot.send_message(
chat_id=admin_chat_id,
text=message,
parse_mode='HTML'
)
except TelegramError as e:
self.logger.error(f"Failed to send admin notification to {admin_chat_id}: {e}")
self.logger.warning(f"Sent admin notification for payment {payment_id}: {error}")
except Exception as e:
self.logger.error(f"Unexpected error sending admin notification: {e}")
async def notify_admin_system_error(self, error_type: str, error_message: str, details: Optional[str] = None):
"""
Уведомление администраторов о системной ошибке.
Args:
error_type: Тип ошибки
error_message: Сообщение об ошибке
details: Дополнительные детали
"""
try:
message = (
f"🔥 <b>Системная ошибка платежей</b>\n\n"
f"⚠️ Тип: <code>{error_type}</code>\n"
f"📝 Ошибка: <code>{error_message}</code>\n"
)
if details:
message += f"📋 Детали: <code>{details[:500]}</code>\n\n"
message += "Требуется немедленное вмешательство!"
for admin_chat_id in self.admin_chat_ids:
try:
await self.bot.send_message(
chat_id=admin_chat_id,
text=message,
parse_mode='HTML'
)
except TelegramError as e:
self.logger.error(f"Failed to send system error notification to {admin_chat_id}: {e}")
self.logger.critical(f"Sent system error notification: {error_type} - {error_message}")
except Exception as e:
self.logger.error(f"Unexpected error sending system error notification: {e}")
async def notify_admin_payment_stats(self, stats: dict):
"""
Уведомление администраторов о статистике платежей.
Args:
stats: Статистика платежей
"""
try:
message = (
f"📊 <b>Статистика платежей</b>\n\n"
f"💰 Всего платежей: <code>{stats.get('total_payments', 0)}</code>\n"
f"✅ Успешных: <code>{stats.get('successful_payments', 0)}</code>\n"
f"❌ Неудачных: <code>{stats.get('failed_payments', 0)}</code>\n"
f"⏳ Ожидающих: <code>{stats.get('pending_payments', 0)}</code>\n"
f"📈 Общая сумма: <code>{stats.get('total_amount', 0):.2f} RUB</code>\n"
)
for admin_chat_id in self.admin_chat_ids:
try:
await self.bot.send_message(
chat_id=admin_chat_id,
text=message,
parse_mode='HTML'
)
except TelegramError as e:
self.logger.error(f"Failed to send stats notification to {admin_chat_id}: {e}")
self.logger.info("Sent payment statistics notification to admins")
except Exception as e:
self.logger.error(f"Unexpected error sending stats notification: {e}")
async def send_custom_notification(self, user_id: int, message: str, parse_mode: str = 'HTML'):
"""
Отправка произвольного уведомления пользователю.
Args:
user_id: ID пользователя
message: Текст сообщения
parse_mode: Режим парсинга ('HTML', 'Markdown', None)
"""
await self._send_with_retry(user_id, message, parse_mode)
async def _send_with_retry(self, user_id: int, message: str, parse_mode: str = 'HTML', attempt: int = 1):
"""
Отправка сообщения с механизмом повторных попыток и rate limiting.
Args:
user_id: ID пользователя
message: Текст сообщения
parse_mode: Режим парсинга
attempt: Номер текущей попытки
"""
# Rate limiting
await self._apply_rate_limit()
try:
await self.bot.send_message(
chat_id=user_id,
text=message,
parse_mode=parse_mode if parse_mode != 'HTML' else None,
disable_web_page_preview=True
)
self.stats['successful_sends'] += 1
self.logger.info(f"Sent notification to user {user_id} (attempt {attempt})")
except RetryAfter as e:
# Rate limit от Telegram API
self.stats['rate_limit_errors'] += 1
self.logger.warning(f"Rate limit hit for user {user_id}, retry after {e.retry_after}s")
await asyncio.sleep(e.retry_after)
await self._send_with_retry(user_id, message, parse_mode, attempt)
except TimedOut as e:
# Таймаут сети
self.stats['timeout_errors'] += 1
self.logger.warning(f"Timeout sending notification to user {user_id} (attempt {attempt})")
await self._handle_retry(user_id, message, parse_mode, attempt, "timeout")
except NetworkError as e:
# Сетевая ошибка
self.stats['network_errors'] += 1
self.logger.warning(f"Network error sending notification to user {user_id} (attempt {attempt}): {e}")
await self._handle_retry(user_id, message, parse_mode, attempt, "network")
except TelegramError as e:
# Другие ошибки Telegram
self.stats['other_errors'] += 1
self.logger.error(f"Telegram error sending notification to user {user_id} (attempt {attempt}): {e}")
self.stats['failed_sends'] += 1
except Exception as e:
# Неожиданные ошибки
self.stats['other_errors'] += 1
self.logger.error(f"Unexpected error sending notification to user {user_id} (attempt {attempt}): {e}")
self.stats['failed_sends'] += 1
async def _apply_rate_limit(self):
"""Применение rate limiting между запросами."""
current_time = time.time()
time_since_last_request = current_time - self.last_request_time
if time_since_last_request < self.min_request_interval:
await asyncio.sleep(self.min_request_interval - time_since_last_request)
self.last_request_time = time.time()
async def _handle_retry(self, user_id: int, message: str, parse_mode: str, attempt: int, error_type: str):
"""
Обработка повторных попыток отправки.
Args:
user_id: ID пользователя
message: Текст сообщения
parse_mode: Режим парсинга
attempt: Текущий номер попытки
error_type: Тип ошибки
"""
if attempt >= self.max_retries:
self.stats['failed_sends'] += 1
self.logger.error(f"Max retries ({self.max_retries}) exceeded for user {user_id}, giving up")
return
# Экспоненциальная задержка
delay = min(self.base_retry_delay * (2 ** (attempt - 1)), self.max_retry_delay)
self.logger.info(f"Retrying notification to user {user_id} in {delay}s (attempt {attempt + 1}/{self.max_retries})")
await asyncio.sleep(delay)
await self._send_with_retry(user_id, message, parse_mode, attempt + 1)
def get_notification_stats(self) -> Dict[str, Any]:
"""
Получение статистики отправки уведомлений.
Returns:
Dict с статистикой отправки
"""
total_attempts = (
self.stats['successful_sends'] +
self.stats['failed_sends'] +
self.stats['network_errors'] +
self.stats['timeout_errors'] +
self.stats['rate_limit_errors'] +
self.stats['other_errors']
)
return {
**self.stats,
'total_attempts': total_attempts,
'success_rate': (self.stats['successful_sends'] / total_attempts * 100) if total_attempts > 0 else 0.0
}
def reset_stats(self):
"""Сброс статистики отправки уведомлений."""
self.stats = {
'successful_sends': 0,
'failed_sends': 0,
'network_errors': 0,
'timeout_errors': 0,
'rate_limit_errors': 0,
'other_errors': 0
}
+523
View File
@@ -0,0 +1,523 @@
"""
Интерфейсы и реализации платежных провайдеров.
"""
import logging
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
from datetime import datetime
from core.payment_models import PaymentIntent, PaymentEvent, PaymentProvider as ProviderEnum
from core.payment_exceptions import PaymentProviderError, PaymentSecurityError
class PaymentProvider(ABC):
"""Абстрактный интерфейс для платежных провайдеров"""
def __init__(self, name: str):
self.name = name
self.logger = logging.getLogger(f"{__name__}.{self.__class__.__name__}")
@abstractmethod
def create_payment(self, amount: float, currency: str, user_id: int, metadata: Dict[str, Any]) -> PaymentIntent:
"""
Создание платежа
Args:
amount: Сумма платежа
currency: Валюта
user_id: ID пользователя
metadata: Дополнительные данные
Returns:
PaymentIntent: Интент платежа
"""
pass
@abstractmethod
def confirm_payment(self, payment_id: str) -> bool:
"""
Подтверждение платежа
Args:
payment_id: ID платежа у провайдера
Returns:
bool: Успешно ли подтвержден
"""
pass
@abstractmethod
def cancel_payment(self, payment_id: str) -> bool:
"""
Отмена платежа
Args:
payment_id: ID платежа у провайдера
Returns:
bool: Успешно ли отменен
"""
pass
@abstractmethod
def get_payment_status(self, payment_id: str) -> str:
"""
Получение статуса платежа
Args:
payment_id: ID платежа у провайдера
Returns:
str: Статус платежа
"""
pass
@abstractmethod
def validate_webhook(self, request_data: Dict, signature: str) -> bool:
"""
Валидация webhook подписи
Args:
request_data: Данные запроса
signature: Подпись
Returns:
bool: Валидна ли подпись
"""
pass
@abstractmethod
def process_webhook(self, webhook_data: Dict) -> PaymentEvent:
"""
Обработка webhook события
Args:
webhook_data: Данные webhook
Returns:
PaymentEvent: Событие платежа
"""
pass
class StripePaymentProvider(PaymentProvider):
"""Интеграция с Stripe"""
def __init__(self, api_key: str, webhook_secret: str):
super().__init__("stripe")
try:
import stripe
stripe.api_key = api_key
self.stripe = stripe
except ImportError:
raise PaymentProviderError("Stripe library not installed")
self.webhook_secret = webhook_secret
def create_payment(self, amount: float, currency: str, user_id: int, metadata: Dict[str, Any]) -> PaymentIntent:
"""Создание платежа в Stripe"""
try:
self.logger.info(f"Creating Stripe payment: amount={amount}, currency={currency}, user_id={user_id}")
# Создаем PaymentIntent
intent = self.stripe.PaymentIntent.create(
amount=int(amount * 100), # Stripe работает с копейками
currency=currency.lower(),
metadata={
'user_id': str(user_id),
'created_at': datetime.now().isoformat(),
**metadata
},
payment_method_types=['card'],
confirmation_method='automatic',
capture_method='automatic'
)
self.logger.info(f"Stripe payment created: {intent.id}")
return PaymentIntent(
id=intent.id,
url=f"https://checkout.stripe.com/pay/{intent.client_secret}",
client_secret=intent.client_secret,
status=intent.status
)
except self.stripe.error.StripeError as e:
self.logger.error(f"Stripe API error: {e}")
raise PaymentProviderError(f"Stripe error: {e}")
def confirm_payment(self, payment_id: str) -> bool:
"""Подтверждение платежа в Stripe"""
try:
intent = self.stripe.PaymentIntent.retrieve(payment_id)
if intent.status == 'requires_confirmation':
intent.confirm()
return intent.status in ['succeeded', 'processing']
except self.stripe.error.StripeError as e:
raise PaymentProviderError(f"Stripe confirm error: {e}")
def cancel_payment(self, payment_id: str) -> bool:
"""Отмена платежа в Stripe"""
try:
intent = self.stripe.PaymentIntent.retrieve(payment_id)
intent.cancel()
return True
except self.stripe.error.StripeError as e:
raise PaymentProviderError(f"Stripe cancel error: {e}")
def get_payment_status(self, payment_id: str) -> str:
"""Получение статуса платежа из Stripe"""
try:
intent = self.stripe.PaymentIntent.retrieve(payment_id)
return intent.status
except self.stripe.error.StripeError as e:
raise PaymentProviderError(f"Stripe status error: {e}")
def validate_webhook(self, request_data: Dict, signature: str) -> bool:
"""Валидация Stripe webhook подписи"""
try:
self.stripe.Webhook.construct_event(
request_data,
signature,
self.webhook_secret
)
return True
except ValueError:
return False
def process_webhook(self, webhook_data: Dict) -> PaymentEvent:
"""Обработка Stripe webhook"""
event = webhook_data
event_type = event.get('type', '')
data = event.get('data', {}).get('object', {})
# Извлекаем payment_intent
payment_intent = data
if 'payment_intent' in data:
payment_intent = data['payment_intent']
amount = payment_intent.get('amount', 0) / 100 # Конвертируем из копеек
metadata = payment_intent.get('metadata', {})
return PaymentEvent(
type=f"payment.{event_type.split('.')[1]}",
payment_id=payment_intent.get('id'),
amount=amount,
currency=payment_intent.get('currency', 'usd').upper(),
user_id=int(metadata.get('user_id')) if metadata.get('user_id') else None,
metadata=metadata
)
class YooKassaPaymentProvider(PaymentProvider):
"""Интеграция с YooKassa"""
def __init__(self, shop_id: str, secret_key: str):
super().__init__("yookassa")
try:
from yookassa import Client, Payment
self.client = Client(shop_id=shop_id, secret_key=secret_key)
self.Payment = Payment
except ImportError:
raise PaymentProviderError("YooKassa library not installed")
self.secret_key = secret_key
def create_payment(self, amount: float, currency: str, user_id: int, metadata: Dict[str, Any]) -> PaymentIntent:
"""Создание платежа в YooKassa"""
try:
self.logger.info(f"Creating YooKassa payment: amount={amount}, currency={currency}, user_id={user_id}")
payment = self.Payment.create({
"amount": {
"value": f"{amount:.2f}",
"currency": currency.upper()
},
"metadata": {
'user_id': str(user_id),
'created_at': datetime.now().isoformat(),
**metadata
},
"confirmation": {
"type": "redirect",
"return_url": "https://t.me/bot" # URL возврата в бот
},
"capture": True,
"description": f"Донат пользователю {user_id}"
}, self.client.id)
self.logger.info(f"YooKassa payment created: {payment.id}")
return PaymentIntent(
id=payment.id,
url=payment.confirmation.confirmation_url,
status=payment.status
)
except Exception as e:
self.logger.error(f"YooKassa API error: {e}")
raise PaymentProviderError(f"YooKassa error: {e}")
def confirm_payment(self, payment_id: str) -> bool:
"""Подтверждение платежа в YooKassa"""
try:
payment = self.Payment.find_one(payment_id, self.client.id)
return payment.status == 'succeeded'
except Exception as e:
raise PaymentProviderError(f"YooKassa confirm error: {e}")
def cancel_payment(self, payment_id: str) -> bool:
"""Отмена платежа в YooKassa"""
try:
payment = self.Payment.find_one(payment_id, self.client.id)
if payment.status == 'pending':
payment.cancel()
return True
return False
except Exception as e:
raise PaymentProviderError(f"YooKassa cancel error: {e}")
def get_payment_status(self, payment_id: str) -> str:
"""Получение статуса платежа из YooKassa"""
try:
payment = self.Payment.find_one(payment_id, self.client.id)
return payment.status
except Exception as e:
raise PaymentProviderError(f"YooKassa status error: {e}")
def validate_webhook(self, request_data: Dict, signature: str) -> bool:
"""Валидация YooKassa webhook подписи"""
try:
# Реализация проверки подписи YooKassa
import hashlib
import hmac
body = request_data
if isinstance(request_data, dict):
import json
body = json.dumps(request_data, separators=(',', ':'))
expected_signature = hmac.new(
self.secret_key.encode('utf-8'),
body.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
except Exception as e:
self.logger.error(f"YooKassa webhook validation error: {e}")
return False
def process_webhook(self, webhook_data: Dict) -> PaymentEvent:
"""Обработка YooKassa webhook"""
event = webhook_data
payment = event.get('object', {})
amount_value = float(payment.get('amount', {}).get('value', 0))
currency = payment.get('amount', {}).get('currency', 'RUB')
metadata = payment.get('metadata', {})
return PaymentEvent(
type=f"payment.{payment.get('status', 'unknown')}",
payment_id=payment.get('id'),
amount=amount_value,
currency=currency,
user_id=int(metadata.get('user_id')) if metadata.get('user_id') else None,
metadata=metadata
)
class SBPaymentProvider(PaymentProvider):
"""Интеграция с Системой Быстрых Платежей (СБП)"""
def __init__(self, api_key: str, bank_id: str, webhook_secret: str, base_url: str = "https://api.sbp.ru"):
super().__init__("sbp")
self.api_key = api_key
self.bank_id = bank_id
self.webhook_secret = webhook_secret
self.base_url = base_url
self.session = None
# Импорт aiohttp для асинхронных HTTP запросов
try:
import aiohttp
self.aiohttp = aiohttp
except ImportError:
raise PaymentProviderError("aiohttp library not installed (required for SBP)")
async def _ensure_session(self):
"""Инициализация HTTP сессии"""
if self.session is None:
self.session = self.aiohttp.ClientSession()
async def close(self):
"""Закрытие HTTP сессии"""
if self.session:
await self.session.close()
self.session = None
def create_payment(self, amount: float, currency: str, user_id: int, metadata: Dict[str, Any]) -> PaymentIntent:
"""Создание платежа через СБП"""
# Для СБП создание платежа обычно происходит асинхронно
# Возвращаем intent с URL для перенаправления на платежную форму
try:
payment_id = f"sbp_{user_id}_{int(datetime.now().timestamp())}"
# СБП обычно использует QR-код или перенаправление на банковское приложение
payment_url = f"{self.base_url}/pay/{payment_id}"
return PaymentIntent(
id=payment_id,
url=payment_url,
status="pending"
)
except Exception as e:
self.logger.error(f"SBP payment creation error: {e}")
raise PaymentProviderError(f"SBP payment creation failed: {e}")
async def create_payment_async(self, amount: float, currency: str, user_id: int, metadata: Dict[str, Any]) -> PaymentIntent:
"""Асинхронное создание платежа через СБП"""
await self._ensure_session()
try:
payment_data = {
"amount": amount,
"currency": currency,
"user_id": user_id,
"bank_id": self.bank_id,
"metadata": metadata
}
async with self.session.post(
f"{self.base_url}/payments",
json=payment_data,
headers={"Authorization": f"Bearer {self.api_key}"}
) as response:
if response.status == 201:
data = await response.json()
return PaymentIntent(
id=data["payment_id"],
url=data["payment_url"],
status="pending"
)
else:
error_data = await response.text()
raise PaymentProviderError(f"SBP API error: {response.status} - {error_data}")
except self.aiohttp.ClientError as e:
raise PaymentProviderError(f"SBP network error: {e}")
def confirm_payment(self, payment_id: str) -> bool:
"""Подтверждение платежа через СБП"""
# Для СБП подтверждение обычно происходит через webhook
# Этот метод можно использовать для ручного подтверждения
self.logger.warning(f"SBP manual confirmation not supported for {payment_id}")
return False
async def confirm_payment_async(self, payment_id: str) -> bool:
"""Асинхронное подтверждение платежа через СБП"""
await self._ensure_session()
try:
async with self.session.post(
f"{self.base_url}/payments/{payment_id}/confirm",
headers={"Authorization": f"Bearer {self.api_key}"}
) as response:
return response.status == 200
except self.aiohttp.ClientError as e:
self.logger.error(f"SBP confirm error: {e}")
return False
def cancel_payment(self, payment_id: str) -> bool:
"""Отмена платежа через СБП"""
self.logger.warning(f"SBP cancel not supported for {payment_id}")
return False
async def cancel_payment_async(self, payment_id: str) -> bool:
"""Асинхронная отмена платежа через СБП"""
await self._ensure_session()
try:
async with self.session.post(
f"{self.base_url}/payments/{payment_id}/cancel",
headers={"Authorization": f"Bearer {self.api_key}"}
) as response:
return response.status == 200
except self.aiohttp.ClientError as e:
self.logger.error(f"SBP cancel error: {e}")
return False
def get_payment_status(self, payment_id: str) -> str:
"""Получение статуса платежа через СБП"""
# Синхронная версия для совместимости
import asyncio
try:
return asyncio.run(self.get_payment_status_async(payment_id))
except:
return "unknown"
async def get_payment_status_async(self, payment_id: str) -> str:
"""Асинхронное получение статуса платежа через СБП"""
await self._ensure_session()
try:
async with self.session.get(
f"{self.base_url}/payments/{payment_id}/status",
headers={"Authorization": f"Bearer {self.api_key}"}
) as response:
if response.status == 200:
data = await response.json()
return data.get("status", "unknown")
else:
return "unknown"
except self.aiohttp.ClientError as e:
self.logger.error(f"SBP status error: {e}")
return "unknown"
def validate_webhook(self, request_data: Dict, signature: str) -> bool:
"""Валидация webhook подписи для СБП"""
try:
# СБП webhook валидация основана на HMAC-SHA256
import base64
import json
# Сериализуем данные в JSON
body = json.dumps(request_data, separators=(',', ':'))
# Вычисляем ожидаемую подпись
expected_hmac = hmac.new(
self.webhook_secret.encode('utf-8'),
body.encode('utf-8'),
hashlib.sha256
)
expected_signature = base64.b64encode(expected_hmac.digest()).decode('utf-8')
return hmac.compare_digest(signature, expected_signature)
except Exception as e:
self.logger.error(f"SBP webhook validation error: {e}")
return False
def process_webhook(self, webhook_data: Dict) -> PaymentEvent:
"""Обработка webhook события от СБП"""
try:
event_type = webhook_data.get("event_type", "payment.unknown")
payment_data = webhook_data.get("payment", {})
amount = float(payment_data.get("amount", 0))
metadata = payment_data.get("metadata", {})
return PaymentEvent(
type=f"payment.{event_type.split('.')[-1]}",
payment_id=payment_data.get("id"),
amount=amount,
currency=payment_data.get("currency", "RUB"),
user_id=int(metadata.get("user_id")) if metadata.get("user_id") else None,
metadata=metadata
)
except Exception as e:
self.logger.error(f"SBP webhook processing error: {e}")
raise PaymentProviderError(f"SBP webhook processing failed: {e}")
+346
View File
@@ -0,0 +1,346 @@
"""
Сервис управления ролями пользователей.
Отвечает за назначение, проверку и получение ролей пользователей.
"""
import json
from typing import Dict, Optional, List
from database.models import Role
from database.repository import BaseRepository
class RoleService:
"""
Сервис для управления ролями пользователей.
Предоставляет методы для:
- Получения ролей пользователей
- Назначения ролей
- Проверки прав доступа
- Валидации ролей
"""
# Стандартные роли с их ID
DEFAULT_ROLES = {
'user': 1,
'moderator': 2,
'admin': 3
}
def __init__(self, db_path: str):
"""
Инициализация сервиса ролей.
Args:
db_path: Путь к файлу базы данных
"""
self.db_repo = BaseRepository(db_path)
def get_user_role_name(self, user_id: int) -> str:
"""
Получить имя роли пользователя.
Args:
user_id: ID пользователя в Telegram
Returns:
str: Имя роли ('user', 'moderator', 'admin')
"""
try:
# Получаем role_id пользователя
query = """
SELECT r.name
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE u.telegram_id = ?
"""
result = self.db_repo._fetch_one(query, (user_id,))
if result:
return result[0]
# Если пользователь не найден или роль не указана, возвращаем 'user'
return 'user'
except Exception as e:
print(f"Error getting user role: {e}")
return 'user' # fallback
def get_user_role_id(self, user_id: int) -> int:
"""
Получить ID роли пользователя.
Args:
user_id: ID пользователя в Telegram
Returns:
int: ID роли
"""
try:
query = "SELECT role_id FROM users WHERE telegram_id = ?"
result = self.db_repo._fetch_one(query, (user_id,))
return result[0] if result else 1 # default user role
except Exception as e:
print(f"Error getting user role ID: {e}")
return 1
def assign_role(self, user_id: int, role_name: str) -> bool:
"""
Назначить роль пользователю.
Args:
user_id: ID пользователя в Telegram
role_name: Имя роли ('user', 'moderator', 'admin')
Returns:
bool: True если роль назначена успешно
"""
try:
# Проверяем валидность роли
if not self._validate_role_name(role_name):
return False
# Получаем ID роли
role_id = self._get_role_id_by_name(role_name)
if not role_id:
return False
# Обновляем роль пользователя
query = "UPDATE users SET role_id = ? WHERE telegram_id = ?"
self.db_repo._execute_query(query, (role_id, user_id))
return True
except Exception as e:
print(f"Error assigning role: {e}")
return False
def is_admin(self, user_id: int) -> bool:
"""
Проверить, является ли пользователь администратором.
Args:
user_id: ID пользователя в Telegram
Returns:
bool: True если пользователь администратор
"""
role_name = self.get_user_role_name(user_id)
return role_name == 'admin'
def is_moderator(self, user_id: int) -> bool:
"""
Проверить, является ли пользователь модератором или администратором.
Args:
user_id: ID пользователя в Telegram
Returns:
bool: True если пользователь модератор или администратор
"""
role_name = self.get_user_role_name(user_id)
return role_name in ['moderator', 'admin']
def get_role_permissions(self, role_name: str) -> Dict:
"""
Получить разрешения для роли.
Args:
role_name: Имя роли
Returns:
Dict: Словарь с разрешениями
"""
try:
query = "SELECT permissions FROM roles WHERE name = ?"
result = self.db_repo._fetch_one(query, (role_name,))
if result and result[0]:
return json.loads(result[0])
# Возвращаем разрешения по умолчанию
return self._get_default_permissions(role_name)
except Exception as e:
print(f"Error getting role permissions: {e}")
return {}
def get_all_roles(self) -> List[Role]:
"""
Получить все активные роли.
Returns:
List[Role]: Список ролей
"""
try:
query = "SELECT id, name, display_name, description, permissions, is_active FROM roles WHERE is_active = 1"
rows = self.db_repo._fetch_all(query)
roles = []
for row in rows:
role = Role(
id=row[0],
name=row[1],
display_name=row[2],
description=row[3],
permissions=row[4],
is_active=bool(row[5])
)
roles.append(role)
return roles
except Exception as e:
print(f"Error getting all roles: {e}")
return []
def _validate_role_name(self, role_name: str) -> bool:
"""
Проверить валидность имени роли.
Args:
role_name: Имя роли
Returns:
bool: True если роль валидна
"""
return role_name in self.DEFAULT_ROLES.keys()
def _get_role_id_by_name(self, role_name: str) -> Optional[int]:
"""
Получить ID роли по имени.
Args:
role_name: Имя роли
Returns:
Optional[int]: ID роли или None
"""
try:
query = "SELECT id FROM roles WHERE name = ? AND is_active = 1"
result = self.db_repo._fetch_one(query, (role_name,))
return result[0] if result else None
except Exception as e:
print(f"Error getting role ID by name: {e}")
return None
def _get_default_permissions(self, role_name: str) -> Dict:
"""
Получить разрешения по умолчанию для роли.
Args:
role_name: Имя роли
Returns:
Dict: Словарь с разрешениями
"""
defaults = {
'user': {
"can_use_basic_commands": True,
"can_play_games": True
},
'moderator': {
"can_use_basic_commands": True,
"can_play_games": True,
"can_moderate": True,
"can_warn_users": True
},
'admin': {
"can_use_basic_commands": True,
"can_play_games": True,
"can_moderate": True,
"can_warn_users": True,
"can_manage_users": True,
"can_schedule_posts": True,
"can_export_data": True
}
}
return defaults.get(role_name, {})
def initialize_roles(self) -> bool:
"""
Инициализировать стандартные роли в базе данных.
Returns:
bool: True если инициализация успешна
"""
try:
roles_data = [
(1, 'user', 'Пользователь', 'Обычный пользователь чата', json.dumps({
"can_use_basic_commands": True,
"can_play_games": True
})),
(2, 'moderator', 'Модератор', 'Модератор чата с правами управления', json.dumps({
"can_use_basic_commands": True,
"can_play_games": True,
"can_moderate": True,
"can_warn_users": True
})),
(3, 'admin', 'Администратор', 'Администратор бота с полными правами', json.dumps({
"can_use_basic_commands": True,
"can_play_games": True,
"can_moderate": True,
"can_warn_users": True,
"can_manage_users": True,
"can_schedule_posts": True,
"can_export_data": True
}))
]
for role_data in roles_data:
query = """
INSERT OR IGNORE INTO roles (id, name, display_name, description, permissions)
VALUES (?, ?, ?, ?, ?)
"""
self.db_repo._execute_query(query, role_data)
return True
except Exception as e:
print(f"Error initializing roles: {e}")
return False
def initialize_admin_users(self, admin_ids: List[int]) -> bool:
"""
Инициализировать пользователей-администраторов из конфигурации.
Args:
admin_ids: Список ID администраторов из конфигурации
Returns:
bool: True если инициализация успешна
"""
try:
for admin_id in admin_ids:
try:
# Проверяем, существует ли пользователь
existing_user = self.db_repo._fetch_one(
"SELECT id FROM users WHERE telegram_id = ?", (admin_id,)
)
if existing_user:
# Обновляем роль существующего пользователя на admin
query = "UPDATE users SET role_id = ? WHERE telegram_id = ?"
self.db_repo._execute_query(query, (3, admin_id)) # 3 = admin role
print(f"Updated existing user {admin_id} to admin role")
else:
# Создаем нового пользователя с ролью admin
# Для этого нам нужны дополнительные данные, которые мы не имеем
# Поэтому просто пропускаем - пользователь будет создан при первом взаимодействии
print(f"Admin user {admin_id} not found in database, will be created on first interaction")
continue
except Exception as e:
print(f"Error processing admin user {admin_id}: {e}")
continue
return True
except Exception as e:
print(f"Error initializing admin users: {e}")
return False
+437
View File
@@ -0,0 +1,437 @@
"""
Сервис управления триггерами сообщений.
Отвечает за проверку триггеров, их активацию и обновление статистики.
"""
import re
import logging
from typing import List, Dict, Optional, Tuple, Any
from datetime import datetime
from database.repository import TriggerRepository
from core.exceptions import ValidationError, DatabaseError
class TriggerService:
"""
Сервис для управления триггерами сообщений.
Отвечает за:
- Проверку текста сообщений на совпадение с триггерами
- Выполнение действий при срабатывании триггеров
- Обновление статистики использования триггеров
- Управление активными триггерами
"""
def __init__(self, database_url: str):
"""
Инициализация сервиса триггеров.
Args:
database_url: URL базы данных
"""
self.logger = logging.getLogger(__name__)
self.repository = TriggerRepository(database_url)
self._triggers_cache: Optional[List[Dict]] = None
self._last_cache_update: Optional[datetime] = None
self._cache_ttl_seconds = 300 # 5 минут
async def check_triggers(self, message_text: str, chat_type: str = "group") -> List[Dict]:
"""
Проверка текста сообщения на совпадение с активными триггерами.
Args:
message_text: Текст сообщения для проверки
chat_type: Тип чата ("group" или "private")
Returns:
Список сработавших триггеров с их данными
"""
if not message_text or not isinstance(message_text, str):
return []
try:
# Получаем активные триггеры
active_triggers = await self.get_active_triggers(chat_type)
if not active_triggers:
return []
matched_triggers = []
for trigger in active_triggers:
try:
# Компилируем регулярное выражение
pattern = re.compile(trigger['pattern'], re.IGNORECASE | re.MULTILINE)
# Проверяем совпадение
if pattern.search(message_text.strip()):
matched_triggers.append(trigger)
# Обновляем статистику асинхронно (не ждем завершения)
self._update_trigger_stats_async(trigger['id'])
except re.error as e:
self.logger.error(f"Invalid regex pattern in trigger {trigger['id']}: {e}")
continue
except Exception as e:
self.logger.error(f"Error checking trigger {trigger['id']}: {e}")
continue
return matched_triggers
except Exception as e:
self.logger.error(f"Error in check_triggers: {e}")
return []
async def get_active_triggers(self, chat_type: str = "group") -> List[Dict]:
"""
Получение активных триггеров для указанного типа чата.
Args:
chat_type: Тип чата ("group" или "private")
Returns:
Список активных триггеров
"""
try:
# Проверяем кеш
if self._should_use_cache():
return self._triggers_cache or []
# Получаем из базы данных
triggers = await self.repository.get_active_triggers_async(chat_type)
# Обновляем кеш
self._triggers_cache = triggers
self._last_cache_update = datetime.now()
return triggers
except DatabaseError as e:
self.logger.error(f"Database error getting active triggers: {e}")
return self._triggers_cache or []
except Exception as e:
self.logger.error(f"Error getting active triggers: {e}")
return []
async def execute_trigger_actions(self, matched_triggers: List[Dict],
chat_id: int, message_text: str, message_id: int = None) -> List[Dict]:
"""
Выполнение действий для сработавших триггеров.
Args:
matched_triggers: Список сработавших триггеров
chat_id: ID чата
message_text: Оригинальный текст сообщения
message_id: ID сообщения (для реакций)
Returns:
Список действий для выполнения
"""
actions = []
for trigger in matched_triggers:
try:
trigger_actions = self._build_trigger_actions(trigger, chat_id, message_text, message_id)
actions.extend(trigger_actions)
# Обновляем статистику
await self.update_trigger_stats(trigger['id'])
except Exception as e:
self.logger.error(f"Error executing actions for trigger {trigger['id']}: {e}")
continue
return actions
def _build_trigger_actions(self, trigger: Dict, chat_id: int, message_text: str, message_id: int = None) -> List[Dict]:
"""
Построение списка действий для триггера.
Args:
trigger: Данные триггера
chat_id: ID чата
message_text: Текст сообщения
message_id: ID сообщения (для реакций)
Returns:
Список действий
"""
actions = []
# Текстовый ответ
if trigger.get('response_text'):
actions.append({
'type': 'text',
'content': trigger['response_text'],
'trigger_id': trigger['id'],
'trigger_name': trigger['name']
})
# Стикер
if trigger.get('response_sticker'):
actions.append({
'type': 'sticker',
'content': trigger['response_sticker'],
'trigger_id': trigger['id'],
'trigger_name': trigger['name']
})
# GIF
if trigger.get('response_gif'):
actions.append({
'type': 'gif',
'content': trigger['response_gif'],
'trigger_id': trigger['id'],
'trigger_name': trigger['name']
})
# Реакция
if trigger.get('reaction_type') and trigger.get('action_data') and message_id is not None:
actions.append({
'type': 'reaction',
'reaction_type': trigger['reaction_type'],
'content': trigger['action_data'], # emoji
'message_id': message_id,
'trigger_id': trigger['id'],
'trigger_name': trigger['name']
})
return actions
async def update_trigger_stats(self, trigger_id: int) -> bool:
"""
Обновление статистики срабатывания триггера.
Args:
trigger_id: ID триггера
Returns:
True если обновление успешно
"""
try:
success = self.repository.update_trigger_stats(trigger_id)
# Инвалидируем кеш
self._invalidate_cache()
return success
except Exception as e:
self.logger.error(f"Error updating trigger stats for {trigger_id}: {e}")
return False
def _update_trigger_stats_async(self, trigger_id: int):
"""Асинхронное обновление статистики триггера (не ждет завершения)"""
import asyncio
asyncio.create_task(self.update_trigger_stats(trigger_id))
async def add_trigger(self, trigger_data: Dict) -> Optional[int]:
"""
Добавление нового триггера.
Args:
trigger_data: Данные триггера
Returns:
ID созданного триггера или None при ошибке
"""
try:
# Валидация данных
self._validate_trigger_data(trigger_data)
# Проверяем регулярное выражение
self._validate_regex_pattern(trigger_data['pattern'])
trigger_id = self.repository.add_trigger(trigger_data)
# Инвалидируем кеш
self._invalidate_cache()
self.logger.info(f"Created new trigger: {trigger_data['name']} (ID: {trigger_id})")
return trigger_id
except ValidationError as e:
self.logger.warning(f"Validation error creating trigger: {e}")
raise
except Exception as e:
self.logger.error(f"Error creating trigger: {e}")
return None
async def update_trigger(self, trigger_id: int, update_data: Dict) -> bool:
"""
Обновление триггера.
Args:
trigger_id: ID триггера
update_data: Данные для обновления
Returns:
True если обновление успешно
"""
try:
# Валидация данных
if 'pattern' in update_data:
self._validate_regex_pattern(update_data['pattern'])
success = self.repository.update_trigger(trigger_id, update_data)
# Инвалидируем кеш
self._invalidate_cache()
if success:
self.logger.info(f"Updated trigger {trigger_id}")
else:
self.logger.warning(f"Trigger {trigger_id} not found for update")
return success
except ValidationError as e:
self.logger.warning(f"Validation error updating trigger {trigger_id}: {e}")
raise
except Exception as e:
self.logger.error(f"Error updating trigger {trigger_id}: {e}")
return False
async def delete_trigger(self, trigger_id: int) -> bool:
"""
Удаление триггера.
Args:
trigger_id: ID триггера
Returns:
True если удаление успешно
"""
try:
success = self.repository.delete_trigger(trigger_id)
# Инвалидируем кеш
self._invalidate_cache()
if success:
self.logger.info(f"Deleted trigger {trigger_id}")
else:
self.logger.warning(f"Trigger {trigger_id} not found for deletion")
return success
except Exception as e:
self.logger.error(f"Error deleting trigger {trigger_id}: {e}")
return False
async def toggle_trigger(self, trigger_id: int, is_active: bool) -> bool:
"""
Включение/выключение триггера.
Args:
trigger_id: ID триггера
is_active: Статус активности
Returns:
True если обновление успешно
"""
try:
success = self.repository.toggle_trigger(trigger_id, is_active)
# Инвалидируем кеш
self._invalidate_cache()
if success:
status = "activated" if is_active else "deactivated"
self.logger.info(f"Trigger {trigger_id} {status}")
else:
self.logger.warning(f"Trigger {trigger_id} not found for toggle")
return success
except Exception as e:
self.logger.error(f"Error toggling trigger {trigger_id}: {e}")
return False
async def get_all_triggers(self) -> List[Dict]:
"""
Получение всех триггеров (для администрирования).
Returns:
Список всех триггеров
"""
try:
return self.repository.get_all_triggers()
except Exception as e:
self.logger.error(f"Error getting all triggers: {e}")
return []
async def get_trigger_by_id(self, trigger_id: int) -> Optional[Dict]:
"""
Получение триггера по ID.
Args:
trigger_id: ID триггера
Returns:
Данные триггера или None
"""
try:
return self.repository.get_trigger_by_id(trigger_id)
except Exception as e:
self.logger.error(f"Error getting trigger {trigger_id}: {e}")
return None
def _validate_trigger_data(self, data: Dict):
"""Валидация данных триггера"""
if not data.get('name') or not isinstance(data['name'], str):
raise ValidationError("Trigger name is required and must be a string")
if not data.get('pattern') or not isinstance(data['pattern'], str):
raise ValidationError("Trigger pattern is required and must be a string")
if len(data['name'].strip()) < 1 or len(data['name']) > 100:
raise ValidationError("Trigger name must be between 1 and 100 characters")
if len(data['pattern'].strip()) < 1 or len(data['pattern']) > 500:
raise ValidationError("Trigger pattern must be between 1 and 500 characters")
chat_type = data.get('chat_type', 'group')
if chat_type not in ['group', 'private']:
raise ValidationError("Chat type must be 'group' or 'private'")
# Валидация полей реакции
reaction_type = data.get('reaction_type')
if reaction_type is not None:
if not isinstance(reaction_type, str):
raise ValidationError("Reaction type must be a string")
if reaction_type not in ['emoji']:
raise ValidationError("Reaction type must be 'emoji'")
# action_data обязательно при наличии reaction_type
if not data.get('action_data') or not isinstance(data['action_data'], str):
raise ValidationError("Action data is required when reaction type is specified")
if len(data['action_data'].strip()) < 1 or len(data['action_data']) > 100:
raise ValidationError("Action data must be between 1 and 100 characters")
def _validate_regex_pattern(self, pattern: str):
"""Валидация регулярного выражения"""
try:
re.compile(pattern, re.IGNORECASE | re.MULTILINE)
except re.error as e:
raise ValidationError(f"Invalid regex pattern: {e}")
def _should_use_cache(self) -> bool:
"""Проверка необходимости использования кеша"""
if self._triggers_cache is None or self._last_cache_update is None:
return False
time_diff = (datetime.now() - self._last_cache_update).total_seconds()
return time_diff < self._cache_ttl_seconds
def _invalidate_cache(self):
"""Инвалидация кеша"""
self._triggers_cache = None
self._last_cache_update = None
async def clear_cache(self):
"""Очистка кеша"""
self._invalidate_cache()
self.logger.debug("Trigger cache cleared")
+805
View File
@@ -0,0 +1,805 @@
"""
Сервис управления пользователями.
Отвечает за бизнес-логику работы с пользователями.
"""
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
+200
View File
@@ -0,0 +1,200 @@
"""
Сервис управления приветственными сообщениями.
Отвечает за генерацию приветствий в зависимости от роли пользователя.
"""
from typing import Dict, Optional
from core.permissions import UserRole
from core.exceptions import ValidationError
class WelcomeService:
"""
Сервис для генерации приветственных сообщений на основе роли пользователя.
Отвечает за:
- Генерацию приветствий для разных ролей
- Форматирование сообщений с персональными данными
- Валидацию входных данных
"""
def __init__(self):
"""Инициализация сервиса приветствий"""
self.welcome_messages = self._initialize_welcome_messages()
def _initialize_welcome_messages(self) -> Dict[str, str]:
"""
Инициализация словаря приветственных сообщений для разных ролей.
На основе требований из welcome_messages_proposals.md
"""
return {
UserRole.USER.value: """👋 Добро пожаловать в наш бот!
Здесь вы можете:
• 📊 Проверить свой ранг: /rank
• 🏆 Посмотреть таблицу лидеров: /leaderboard
• 🎯 Играть в игры: /play_game
• 🌦️ Узнать погоду: /weather
• 📰 Прочитать новости: /news
• 💸 Поддержать проект: /donate
Для справки используйте /help""",
UserRole.MODERATOR.value: """🛡️ Добро пожаловать, модератор!
Ваши права:
• ⚠️ Предупреждения: /warn
• 🔇 Заглушка: /mute /unmute
• 📊 Статистика модерации
Все базовые команды также доступны: /rank, /play_game, /weather и др.""",
UserRole.ADMIN.value: """👑 Добро пожаловать, администратор!
У вас полный доступ к управлению ботом:
• 👥 Модерация: /warn, /mute, /ban, /kick
• 📊 Статистика: /admin_stats, /export_stats
• 📝 Планирование: /schedule_post, /list_posts
• 🛠️ Система: /admin_errors, /report_error
Все пользовательские команды также доступны.
Используйте /help для полного списка.""",
UserRole.SUPER_ADMIN.value: """🔧 Супер-администратор активирован!
Ваши возможности:
• ⚠️ Модерация пользователей: /warn /mute /ban
• 📈 Просмотр статистики: /admin_stats
• 📅 Управление публикациями: /schedule_post
• 🔍 Работа с ошибками: /admin_errors
• 📤 Экспорт данных: /export_stats
Обычные команды: /rank /leaderboard /play_game и др."""
}
def get_welcome_message(self, user_role: UserRole, user_name: Optional[str] = None) -> str:
"""
Получение приветственного сообщения для указанной роли.
Args:
user_role: Роль пользователя
user_name: Имя пользователя (опционально)
Returns:
Отформатированное приветственное сообщение
Raises:
ValidationError: Если роль не поддерживается
"""
if not isinstance(user_role, UserRole):
raise ValidationError(f"Неверная роль пользователя: {user_role}")
# Получаем базовое сообщение для роли
base_message = self.welcome_messages.get(user_role.value)
if not base_message:
# Fallback на сообщение для обычного пользователя
base_message = self.welcome_messages[UserRole.USER.value]
# Форматируем с именем пользователя, если указано
if user_name:
# Добавляем персональное приветствие в начало
personal_greeting = f"👋 Привет, {user_name}!\n\n"
return personal_greeting + base_message
else:
# Безымянное приветствие
return "👋 Привет!\n\n" + base_message
def get_available_commands_for_role(self, user_role: UserRole) -> Dict[str, list]:
"""
Получение доступных команд для указанной роли.
Args:
user_role: Роль пользователя
Returns:
Словарь с доступными командами по категориям
"""
if not isinstance(user_role, UserRole):
raise ValidationError(f"Неверная роль пользователя: {user_role}")
# Базовые команды для всех пользователей
base_commands = {
'user_commands': [
'/start - Приветствие и начало работы',
'/help - Справка по командам',
'/rank - Просмотр личного ранга и прогресса',
'/leaderboard - Таблица лидеров по очкам',
'/info - Информация о боте',
'/weather - Погода',
'/news - Новости',
'/translate - Перевод текста',
'/donate - Поддержать проект'
],
'games': [
'/play_game - Выбор игры',
'/rock_paper_scissors - Камень-ножницы-бумага',
'/tic_tac_toe - Крестики-нолики',
'/quiz - Викторина',
'/battleship - Морской бой',
'/game_2048 - Игра 2048',
'/tetris - Тетрис',
'/snake - Змейка'
]
}
# Добавляем команды в зависимости от роли
if user_role in [UserRole.MODERATOR, UserRole.ADMIN, UserRole.SUPER_ADMIN]:
base_commands['moderation'] = [
'/warn - Выдать предупреждение',
'/mute - Заглушить пользователя',
'/unmute - Снять заглушку'
]
if user_role in [UserRole.ADMIN, UserRole.SUPER_ADMIN]:
base_commands['admin'] = [
'/ban - Заблокировать пользователя',
'/unban - Разблокировать пользователя',
'/kick - Выгнать из чата',
'/admin_stats - Статистика админа',
'/schedule_post - Запланировать публикацию',
'/list_posts - Список запланированных постов',
'/delete_post - Удалить пост',
'/publish_now - Опубликовать немедленно',
'/report_error - Сообщить об ошибке',
'/admin_errors - Управление ошибками',
'/export_stats - Экспорт статистики'
]
if user_role == UserRole.SUPER_ADMIN:
base_commands['super_admin'] = [
'/admin_system - Системные настройки',
'/admin_logs - Системные логи',
'/admin_restart - Управление перезапуском'
]
return base_commands
def validate_user_name(self, name: str) -> bool:
"""
Валидация имени пользователя.
Args:
name: Имя для проверки
Returns:
True если имя корректное
"""
if not name or not isinstance(name, str):
return False
# Проверяем длину
if len(name.strip()) < 1 or len(name) > 100:
return False
# Проверяем на запрещенные символы (простая проверка)
forbidden_chars = ['<', '>', '&', '\n', '\r']
if any(char in name for char in forbidden_chars):
return False
return True