diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py index 227d239..9775ecf 100644 --- a/bot/handlers/admin/promo_codes.py +++ b/bot/handlers/admin/promo_codes.py @@ -32,29 +32,35 @@ async def create_promo_prompt_handler(callback: types.CallbackQuery, return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) - prompt_text = _("admin_promo_create_prompt", - example_format="MYPROMO20 7 100 30") + # Step 1: Ask for promo code + prompt_text = _( + "admin_promo_step1_code", + default="🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 5: Код ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠΎΠ΄ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 символов, Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π±ΡƒΠΊΠ²Ρ‹ ΠΈ Ρ†ΠΈΡ„Ρ€Ρ‹):" + ) try: await callback.message.edit_text( prompt_text, - reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)) + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML") except Exception as e: logging.warning( f"Could not edit message for promo prompt: {e}. Sending new.") await callback.message.answer( prompt_text, - reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)) + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML") await callback.answer() - await state.set_state(AdminStates.waiting_for_promo_details) + await state.set_state(AdminStates.waiting_for_promo_code) -@router.message(AdminStates.waiting_for_promo_details, F.text) -async def process_promo_code_details_handler(message: types.Message, - state: FSMContext, - i18n_data: dict, - settings: Settings, - session: AsyncSession): +# Step 1: Process promo code +@router.message(AdminStates.waiting_for_promo_code, F.text) +async def process_promo_code_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings, + session: AsyncSession): current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") if not i18n: @@ -62,87 +68,457 @@ async def process_promo_code_details_handler(message: types.Message, return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) - if not message.text: - await message.answer(_("admin_promo_invalid_format")) - return - - parts = message.text.strip().split() - if not (3 <= len(parts) <= 4): - await message.answer(_("admin_promo_invalid_format")) - return - try: - code_str = parts[0].upper() + code_str = message.text.strip().upper() if not (3 <= len(code_str) <= 30 and code_str.isalnum()): - raise ValueError(_("admin_promo_invalid_code_format")) + await message.answer(_( + "admin_promo_invalid_code_format", + default="❌ Код ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ 3-30 символов (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π±ΡƒΠΊΠ²Ρ‹ ΠΈ Ρ†ΠΈΡ„Ρ€Ρ‹)" + )) + return + + # Check if code already exists + existing_promo = await promo_code_dal.get_promo_code_by_code(session, code_str) + if existing_promo: + await message.answer(_( + "admin_promo_code_already_exists", + default="❌ ΠŸΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ с Ρ‚Π°ΠΊΠΈΠΌ ΠΊΠΎΠ΄ΠΎΠΌ ΡƒΠΆΠ΅ сущСствуСт" + )) + return + + await state.update_data(promo_code=code_str) + + # Step 2: Ask for bonus days + prompt_text = _( + "admin_promo_step2_bonus_days", + default="🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 5: БонусныС Π΄Π½ΠΈ\n\nКод: {code}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство бонусных Π΄Π½Π΅ΠΉ (1-365):", + code=code_str + ) + + await message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_promo_bonus_days) + + except Exception as e: + logging.error(f"Error processing promo code: {e}") + await message.answer(_( + "admin_promo_processing_error", + default="❌ Ошибка ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°" + )) - bonus_days = int(parts[1]) - max_activations = int(parts[2]) - - valid_until_date: Optional[datetime] = None - valid_until_str_display = _("admin_promo_valid_indefinitely") - - if len(parts) == 4: - valid_days_from_now = int(parts[3]) - if valid_days_from_now <= 0: - raise ValueError(_("admin_promo_invalid_validity_days")) - valid_until_date = datetime.now( - timezone.utc) + timedelta(days=valid_days_from_now) - valid_until_str_display = _( - "admin_promo_valid_until_display", - date=valid_until_date.strftime('%Y-%m-%d')) - - if bonus_days <= 0 or max_activations <= 0: - raise ValueError(_("admin_promo_invalid_bonus_or_activations")) - - except ValueError as e: - await message.answer(_("admin_promo_invalid_values", error=str(e))) +# Step 2: Process bonus days +@router.message(AdminStates.waiting_for_promo_bonus_days, F.text) +async def process_promo_bonus_days_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await message.reply("Language service error.") return - except Exception as e_parse: - logging.error( - f"Error parsing promo details '{message.text}': {e_parse}") - await message.answer(_("admin_promo_invalid_format_general")) - return - - admin_id = message.from_user.id if message.from_user else 0 - - promo_data_to_create = { - "code": code_str, - "bonus_days": bonus_days, - "max_activations": max_activations, - "created_by_admin_id": admin_id, - "valid_until": valid_until_date, - "is_active": True, - "current_activations": 0 - } + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) try: - created_promo = await promo_code_dal.create_promo_code( - session, promo_data_to_create) - await session.commit() + bonus_days = int(message.text.strip()) + if bonus_days <= 0 or bonus_days > 365: + await message.answer(_( + "admin_promo_invalid_bonus_days", + default="❌ БонусныС Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365" + )) + return + + data = await state.get_data() + await state.update_data(bonus_days=bonus_days) + + # Step 3: Ask for max activations + prompt_text = _( + "admin_promo_step3_max_activations", + default="🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 5: ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ\n\nКод: {code}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ максимальноС количСство Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΉ (1-10000):", + code=data['promo_code'], + bonus_days=bonus_days + ) + + await message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_promo_max_activations) + + except ValueError: + await message.answer(_( + "admin_promo_invalid_bonus_days", + default="❌ БонусныС Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365" + )) - if created_promo: - success_text = _("admin_promo_created_success", - code=created_promo.code, - bonus_days=created_promo.bonus_days, - max_activations=created_promo.max_activations, - valid_until_str=valid_until_str_display) - await message.answer(success_text, - reply_markup=get_back_to_admin_panel_keyboard( - current_lang, i18n)) +# Step 3: Process max activations +@router.message(AdminStates.waiting_for_promo_max_activations, F.text) +async def process_promo_max_activations_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + try: + max_activations = int(message.text.strip()) + if max_activations <= 0 or max_activations > 10000: + await message.answer(_( + "admin_promo_invalid_max_activations", + default="❌ ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 10000" + )) + return + + data = await state.get_data() + await state.update_data(max_activations=max_activations) + + # Step 4: Ask for validity days (with buttons) + keyboard = InlineKeyboardBuilder() + keyboard.row( + InlineKeyboardButton( + text=_("admin_promo_unlimited_validity", default="♾️ НСограничСнно"), + callback_data="promo_unlimited_validity" + ) + ) + keyboard.row( + InlineKeyboardButton( + text=_("admin_promo_set_validity", default="⏰ Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ срок"), + callback_data="promo_set_validity" + ) + ) + keyboard.row( + InlineKeyboardButton( + text=_("admin_panel_back_button", default="⬅️ Назад"), + callback_data="admin_panel_back" + ) + ) + + prompt_text = _( + "admin_promo_step4_validity", + default="🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 5: Π‘Ρ€ΠΎΠΊ дСйствия\n\nКод: {code}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\nМакс. Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_activations}\n\nΠ’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ срок дСйствия ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°:", + code=data['promo_code'], + bonus_days=data['bonus_days'], + max_activations=max_activations + ) + + await message.answer( + prompt_text, + reply_markup=keyboard.as_markup(), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_promo_validity_days) + + except ValueError: + await message.answer(_( + "admin_promo_invalid_max_activations", + default="❌ ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 10000" + )) + +# Handle validity choice buttons +@router.callback_query(F.data == "promo_unlimited_validity", StateFilter(AdminStates.waiting_for_promo_validity_days)) +async def process_promo_unlimited_validity(callback: types.CallbackQuery, + state: FSMContext, + i18n_data: dict, + settings: Settings): + await state.update_data(validity_days=None) + await ask_for_promo_description(callback, state, i18n_data, settings) + +@router.callback_query(F.data == "promo_set_validity", StateFilter(AdminStates.waiting_for_promo_validity_days)) +async def process_promo_set_validity(callback: types.CallbackQuery, + state: FSMContext, + i18n_data: dict, + settings: Settings): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await callback.answer("Language service error.", show_alert=True) + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + prompt_text = _( + "admin_promo_enter_validity_days", + default="⏰ Π’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство Π΄Π½Π΅ΠΉ дСйствия ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-365):" + ) + + try: + await callback.message.edit_text( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n) + ) + except Exception as e: + await callback.message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n) + ) + await callback.answer() + +# Step 4: Process validity days (when user chooses to set) +@router.message(AdminStates.waiting_for_promo_validity_days, F.text) +async def process_promo_validity_days_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + try: + validity_days = int(message.text.strip()) + if validity_days <= 0 or validity_days > 365: + await message.answer(_( + "admin_promo_invalid_validity_days", + default="❌ Π”Π½ΠΈ дСйствия Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365" + )) + return + + await state.update_data(validity_days=validity_days) + + # Create a fake callback for consistency + fake_callback = types.CallbackQuery( + id="fake", + from_user=message.from_user, + chat_instance="fake", + message=message, + data="fake" + ) + + await ask_for_promo_description(fake_callback, state, i18n_data, settings) + + except ValueError: + await message.answer(_( + "admin_promo_invalid_validity_days", + default="❌ Π”Π½ΠΈ дСйствия Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365" + )) + +async def ask_for_promo_description(callback_or_message, + state: FSMContext, + i18n_data: dict, + settings: Settings): + """Ask for promo description (final step)""" + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + if hasattr(callback_or_message, 'answer'): + await callback_or_message.answer("Language service error.", show_alert=True) else: - await message.answer( - _("admin_promo_creation_failed_duplicate", code=code_str)) + await callback_or_message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) - except Exception as e_db_create: + data = await state.get_data() + validity_days = data.get('validity_days') + validity_text = "НСограничСнно" if validity_days is None else f"{validity_days} Π΄Π½." + + # Step 5: Ask for description (optional) + keyboard = InlineKeyboardBuilder() + keyboard.row( + InlineKeyboardButton( + text=_("admin_promo_skip_description", default="⏭️ ΠŸΡ€ΠΎΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ"), + callback_data="promo_skip_description" + ) + ) + keyboard.row( + InlineKeyboardButton( + text=_("admin_panel_back_button", default="⬅️ Назад"), + callback_data="admin_panel_back" + ) + ) + + prompt_text = _( + "admin_promo_step5_description", + default="🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 5 ΠΈΠ· 5: ОписаниС (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ)\n\nКод: {code}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\nМакс. Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_activations}\nДСйствуСт: {validity}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ описаниС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° ΠΈΠ»ΠΈ Π½Π°ΠΆΠΌΠΈΡ‚Π΅ \"ΠŸΡ€ΠΎΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ\":", + code=data['promo_code'], + bonus_days=data['bonus_days'], + max_activations=data['max_activations'], + validity=validity_text + ) + + if hasattr(callback_or_message, 'message'): + try: + await callback_or_message.message.edit_text( + prompt_text, + reply_markup=keyboard.as_markup(), + parse_mode="HTML" + ) + except: + await callback_or_message.message.answer( + prompt_text, + reply_markup=keyboard.as_markup(), + parse_mode="HTML" + ) + if hasattr(callback_or_message, 'answer'): + await callback_or_message.answer() + else: + await callback_or_message.answer( + prompt_text, + reply_markup=keyboard.as_markup(), + parse_mode="HTML" + ) + + await state.set_state(AdminStates.waiting_for_promo_description) + +# Handle skip description button +@router.callback_query(F.data == "promo_skip_description", StateFilter(AdminStates.waiting_for_promo_description)) +async def process_promo_skip_description(callback: types.CallbackQuery, + state: FSMContext, + i18n_data: dict, + settings: Settings, + session: AsyncSession): + await state.update_data(description=None) + await create_promo_code_final(callback, state, i18n_data, settings, session) + +# Step 5: Process description (optional) +@router.message(AdminStates.waiting_for_promo_description, F.text) +async def process_promo_description_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings, + session: AsyncSession): + description = message.text.strip() + if len(description) > 200: + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if i18n: + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + await message.answer(_( + "admin_promo_description_too_long", + default="❌ ОписаниС Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ°Ρ‚ΡŒ 200 символов" + )) + else: + await message.answer("Description too long") + return + + await state.update_data(description=description) + + # Create a fake callback for consistency + fake_callback = types.CallbackQuery( + id="fake", + from_user=message.from_user, + chat_instance="fake", + message=message, + data="fake" + ) + + await create_promo_code_final(fake_callback, state, i18n_data, settings, session) + +async def create_promo_code_final(callback_or_message, + state: FSMContext, + i18n_data: dict, + settings: Settings, + session: AsyncSession): + """Final step to create the promo code""" + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + if hasattr(callback_or_message, 'answer'): + await callback_or_message.answer("Language service error.", show_alert=True) + else: + await callback_or_message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + data = await state.get_data() + promo_code = data['promo_code'] + bonus_days = data['bonus_days'] + max_activations = data['max_activations'] + validity_days = data.get('validity_days') + description = data.get('description') + + # Create validity date + valid_until_date: Optional[datetime] = None + valid_until_str_display = _("admin_promo_valid_indefinitely", default="НСограничСнно") + + if validity_days: + valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days) + valid_until_str_display = valid_until_date.strftime('%Y-%m-%d') + + # Show creating message + creating_text = _( + "admin_promo_creating", + default="⏳ Боздаю ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄..." + ) + + if hasattr(callback_or_message, 'message'): + try: + await callback_or_message.message.edit_text(creating_text) + except: + await callback_or_message.message.answer(creating_text) + if hasattr(callback_or_message, 'answer'): + await callback_or_message.answer() + else: + await callback_or_message.answer(creating_text) + + # Create promo code + try: + promo_data = { + "code": promo_code, + "bonus_days": bonus_days, + "max_activations": max_activations, + "current_activations": 0, + "is_active": True, + "valid_until": valid_until_date, + "description": description + } + + await promo_code_dal.create_promo_code(session, promo_data) + await session.commit() + + # Send success message + success_text = _( + "admin_promo_created_success", + default="βœ… ΠŸΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ создан!\n\n🎟 Код: {code}\n🎁 БонусныС Π΄Π½ΠΈ: {days}\nπŸ”’ ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_act}\n⏰ ДСйствуСт Π΄ΠΎ: {validity}\nπŸ“ ОписаниС: {description}", + code=promo_code, + days=bonus_days, + max_act=max_activations, + validity=valid_until_str_display, + description=description or "НС ΡƒΠΊΠ°Π·Π°Π½ΠΎ" + ) + + if hasattr(callback_or_message, 'message'): + target_message = callback_or_message.message + else: + target_message = callback_or_message + + await target_message.answer( + success_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + + except Exception as e: + logging.error(f"Error creating promo code: {e}") await session.rollback() - logging.error( - f"Failed to create promo code '{code_str}' in DB: {e_db_create}", - exc_info=True) - await message.answer(_("admin_promo_creation_failed")) - + error_text = _( + "admin_promo_creation_failed", + default="❌ Ошибка создания ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°: {error}", + error=str(e) + ) + + if hasattr(callback_or_message, 'message'): + target_message = callback_or_message.message + else: + target_message = callback_or_message + + await target_message.answer( + error_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n) + ) + await state.clear() +# Legacy handler removed - using step-by-step creation now + async def view_promo_codes_handler(callback: types.CallbackQuery, i18n_data: dict, settings: Settings, @@ -341,8 +717,17 @@ async def promo_delete_handler(callback: types.CallbackQuery, i18n_data: dict, F.data == "admin_action:main", StateFilter( AdminStates.waiting_for_promo_details, + AdminStates.waiting_for_promo_code, + AdminStates.waiting_for_promo_bonus_days, + AdminStates.waiting_for_promo_max_activations, + AdminStates.waiting_for_promo_validity_days, + AdminStates.waiting_for_promo_description, AdminStates.waiting_for_promo_edit_details, AdminStates.waiting_for_bulk_promo_details, + AdminStates.waiting_for_bulk_promo_quantity, + AdminStates.waiting_for_bulk_promo_bonus_days, + AdminStates.waiting_for_bulk_promo_max_activations, + AdminStates.waiting_for_bulk_promo_validity_days, ), ) async def cancel_promo_creation_state_to_menu(callback: types.CallbackQuery, @@ -382,24 +767,26 @@ async def create_bulk_promo_prompt_handler(callback: types.CallbackQuery, return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + # Step 1: Ask for quantity prompt_text = _( - "admin_bulk_promo_create_prompt", - default="πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ Π΄Π°Π½Π½Ρ‹Π΅ Π² Ρ„ΠΎΡ€ΠΌΠ°Ρ‚Π΅:\n<количСство> <бонусныС_Π΄Π½ΠΈ> <максимальноС_использованиС> [Π΄Π½ΠΈ_дСйствия]\n\nΠŸΡ€ΠΈΠΌΠ΅Ρ€: 50 7 100 30\n(создаст 50 ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π½Π° 7 Π΄Π½Π΅ΠΉ с Π»ΠΈΠΌΠΈΡ‚ΠΎΠΌ 100 Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΉ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ, Π΄Π΅ΠΉΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ… 30 Π΄Π½Π΅ΠΉ)", - example_format="50 7 100 30" + "admin_bulk_promo_step1_quantity", + default="πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² для создания (1-1000):" ) try: await callback.message.edit_text( prompt_text, - reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)) + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML") except Exception as e: logging.warning( f"Could not edit message for bulk promo prompt: {e}. Sending new.") await callback.message.answer( prompt_text, - reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)) + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML") await callback.answer() - await state.set_state(AdminStates.waiting_for_bulk_promo_details) + await state.set_state(AdminStates.waiting_for_bulk_promo_quantity) def generate_unique_promo_code(length: int = 8) -> str: @@ -410,6 +797,386 @@ def generate_unique_promo_code(length: int = 8) -> str: return ''.join(random.choice(characters) for _ in range(length)) +# Step 1: Process quantity +@router.message(AdminStates.waiting_for_bulk_promo_quantity, F.text) +async def process_bulk_promo_quantity_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + try: + quantity = int(message.text.strip()) + if quantity <= 0 or quantity > 1000: + await message.answer(_( + "admin_bulk_promo_invalid_quantity", + default="❌ ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 1000" + )) + return + + await state.update_data(quantity=quantity) + + # Step 2: Ask for bonus days + prompt_text = _( + "admin_bulk_promo_step2_bonus_days", + default="πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: БонусныС Π΄Π½ΠΈ\n\nΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ: {quantity}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство бонусных Π΄Π½Π΅ΠΉ (1-365):", + quantity=quantity + ) + + await message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_bulk_promo_bonus_days) + + except ValueError: + await message.answer(_( + "admin_bulk_promo_invalid_quantity", + default="❌ ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 1000" + )) + +# Step 2: Process bonus days +@router.message(AdminStates.waiting_for_bulk_promo_bonus_days, F.text) +async def process_bulk_promo_bonus_days_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + try: + bonus_days = int(message.text.strip()) + if bonus_days <= 0 or bonus_days > 365: + await message.answer(_( + "admin_bulk_promo_invalid_bonus_days", + default="❌ БонусныС Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365" + )) + return + + data = await state.get_data() + await state.update_data(bonus_days=bonus_days) + + # Step 3: Ask for max activations + prompt_text = _( + "admin_bulk_promo_step3_max_activations", + default="πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ\n\nΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ: {quantity}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ максимальноС количСство Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΉ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-10000):", + quantity=data['quantity'], + bonus_days=bonus_days + ) + + await message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_bulk_promo_max_activations) + + except ValueError: + await message.answer(_( + "admin_bulk_promo_invalid_bonus_days", + default="❌ БонусныС Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365" + )) + +# Step 3: Process max activations +@router.message(AdminStates.waiting_for_bulk_promo_max_activations, F.text) +async def process_bulk_promo_max_activations_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + try: + max_activations = int(message.text.strip()) + if max_activations <= 0 or max_activations > 10000: + await message.answer(_( + "admin_bulk_promo_invalid_max_activations", + default="❌ ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 10000" + )) + return + + data = await state.get_data() + await state.update_data(max_activations=max_activations) + + # Step 4: Ask for validity days (optional) + keyboard = InlineKeyboardBuilder() + keyboard.row( + InlineKeyboardButton( + text=_("admin_bulk_promo_unlimited_validity", default="♾️ НСограничСнно"), + callback_data="bulk_promo_unlimited_validity" + ) + ) + keyboard.row( + InlineKeyboardButton( + text=_("admin_bulk_promo_set_validity", default="⏰ Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ срок"), + callback_data="bulk_promo_set_validity" + ) + ) + keyboard.row( + InlineKeyboardButton( + text=_("admin_panel_back_button", default="⬅️ Назад"), + callback_data="admin_panel_back" + ) + ) + + prompt_text = _( + "admin_bulk_promo_step4_validity", + default="πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘Ρ€ΠΎΠΊ дСйствия\n\nΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ: {quantity}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\nМакс. Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_activations}\n\nΠ’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ срок дСйствия ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²:", + quantity=data['quantity'], + bonus_days=data['bonus_days'], + max_activations=max_activations + ) + + await message.answer( + prompt_text, + reply_markup=keyboard.as_markup(), + parse_mode="HTML" + ) + await state.set_state(AdminStates.waiting_for_bulk_promo_validity_days) + + except ValueError: + await message.answer(_( + "admin_bulk_promo_invalid_max_activations", + default="❌ ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 10000" + )) + +# Handle validity choice buttons +@router.callback_query(F.data == "bulk_promo_unlimited_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days)) +async def process_bulk_promo_unlimited_validity(callback: types.CallbackQuery, + state: FSMContext, + i18n_data: dict, + settings: Settings, + session: AsyncSession): + await state.update_data(validity_days=None) + await create_bulk_promo_codes_final(callback, state, i18n_data, settings, session) + +@router.callback_query(F.data == "bulk_promo_set_validity", StateFilter(AdminStates.waiting_for_bulk_promo_validity_days)) +async def process_bulk_promo_set_validity(callback: types.CallbackQuery, + state: FSMContext, + i18n_data: dict, + settings: Settings): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await callback.answer("Language service error.", show_alert=True) + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + prompt_text = _( + "admin_bulk_promo_enter_validity_days", + default="⏰ Π’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство Π΄Π½Π΅ΠΉ дСйствия ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² (1-365):" + ) + + try: + await callback.message.edit_text( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n) + ) + except Exception as e: + await callback.message.answer( + prompt_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n) + ) + await callback.answer() + +# Step 4: Process validity days (when user chooses to set) +@router.message(AdminStates.waiting_for_bulk_promo_validity_days, F.text) +async def process_bulk_promo_validity_days_handler(message: types.Message, + state: FSMContext, + i18n_data: dict, + settings: Settings, + session: AsyncSession): + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + await message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + try: + validity_days = int(message.text.strip()) + if validity_days <= 0 or validity_days > 365: + await message.answer(_( + "admin_bulk_promo_invalid_validity_days", + default="❌ Π”Π½ΠΈ дСйствия Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365" + )) + return + + await state.update_data(validity_days=validity_days) + + # Create a fake callback for consistency + fake_callback = types.CallbackQuery( + id="fake", + from_user=message.from_user, + chat_instance="fake", + message=message, + data="fake" + ) + + await create_bulk_promo_codes_final(fake_callback, state, i18n_data, settings, session) + + except ValueError: + await message.answer(_( + "admin_bulk_promo_invalid_validity_days", + default="❌ Π”Π½ΠΈ дСйствия Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365" + )) + +async def create_bulk_promo_codes_final(callback_or_message, + state: FSMContext, + i18n_data: dict, + settings: Settings, + session: AsyncSession): + """Final step to create all promo codes""" + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n: + if hasattr(callback_or_message, 'answer'): + await callback_or_message.answer("Language service error.", show_alert=True) + else: + await callback_or_message.reply("Language service error.") + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + data = await state.get_data() + quantity = data['quantity'] + bonus_days = data['bonus_days'] + max_activations = data['max_activations'] + validity_days = data.get('validity_days') + + # Create validity date + valid_until_date: Optional[datetime] = None + valid_until_str_display = _("admin_promo_valid_indefinitely", default="НСограничСнно") + + if validity_days: + valid_until_date = datetime.now(timezone.utc) + timedelta(days=validity_days) + valid_until_str_display = valid_until_date.strftime('%Y-%m-%d') + + # Show creating message + creating_text = _( + "admin_bulk_promo_creating", + default="⏳ Боздаю {quantity} ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²...", + quantity=quantity + ) + + if hasattr(callback_or_message, 'message'): + try: + await callback_or_message.message.edit_text(creating_text) + except: + await callback_or_message.message.answer(creating_text) + if hasattr(callback_or_message, 'answer'): + await callback_or_message.answer() + else: + await callback_or_message.answer(creating_text) + + # Generate and create promo codes + created_codes = [] + try: + for i in range(quantity): + # Generate unique code + promo_code = generate_unique_promo_code() + + # Ensure uniqueness + existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code) + retries = 0 + while existing_promo and retries < 10: + promo_code = generate_unique_promo_code() + existing_promo = await promo_code_dal.get_promo_code_by_code(session, promo_code) + retries += 1 + + if retries >= 10: + raise Exception("НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄") + + # Create promo code + promo_data = { + "code": promo_code, + "bonus_days": bonus_days, + "max_activations": max_activations, + "current_activations": 0, + "is_active": True, + "valid_until": valid_until_date, + "description": f"Bulk created #{i+1}/{quantity}" + } + + await promo_code_dal.create_promo_code(session, promo_data) + created_codes.append(promo_code) + + await session.commit() + + # Send success message + codes_text = " ".join(created_codes) + success_text = _( + "admin_bulk_promo_created_success", + default="βœ… Π‘ΠΎΠ·Π΄Π°Π½ΠΎ {count} ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² Π½Π° {days} Π΄Π½Π΅ΠΉ!\n\nΠ”Π΅ΠΉΡΡ‚Π²ΡƒΡŽΡ‚ Π΄ΠΎ: {validity}\nМакс. Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_act}\n\nΠŸΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Ρ‹:\n{codes}", + count=quantity, + days=bonus_days, + validity=valid_until_str_display, + max_act=max_activations, + codes=codes_text[:3500] + "..." if len(codes_text) > 3500 else codes_text + ) + + if hasattr(callback_or_message, 'message'): + target_message = callback_or_message.message + else: + target_message = callback_or_message + + await target_message.answer( + success_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), + parse_mode="HTML" + ) + + # Send codes as file if too many + if len(codes_text) > 3500: + codes_file_content = "\n".join(created_codes) + codes_file = types.BufferedInputFile( + codes_file_content.encode('utf-8'), + filename=f"bulk_promo_codes_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + ) + + await target_message.answer_document( + codes_file, + caption=_( + "admin_bulk_promo_codes_file", + default="πŸ“„ ВсС созданныС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Ρ‹ Π² Ρ„Π°ΠΉΠ»Π΅", + count=quantity + ) + ) + + except Exception as e: + logging.error(f"Error creating bulk promo codes: {e}") + await session.rollback() + error_text = _( + "admin_bulk_promo_creation_failed", + default="❌ Ошибка создания ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²: {error}", + error=str(e) + ) + + if hasattr(callback_or_message, 'message'): + target_message = callback_or_message.message + else: + target_message = callback_or_message + + await target_message.answer( + error_text, + reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n) + ) + + await state.clear() + @router.message(AdminStates.waiting_for_bulk_promo_details, F.text) async def process_bulk_promo_details_handler(message: types.Message, state: FSMContext, diff --git a/bot/handlers/admin/statistics.py b/bot/handlers/admin/statistics.py index 93d7b32..bcc7dce 100644 --- a/bot/handlers/admin/statistics.py +++ b/bot/handlers/admin/statistics.py @@ -8,6 +8,7 @@ from config.settings import Settings from db.dal import user_dal, payment_dal, panel_sync_dal from db.models import Payment, PanelSyncStatus +from bot.services.panel_api_service import PanelApiService from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboard from bot.middlewares.i18n import JsonI18n @@ -137,6 +138,49 @@ async def show_statistics_handler(callback: types.CallbackQuery, else: stats_text_parts.append(f"\n{_('admin_sync_status_never_run')}") + # Panel Statistics + try: + panel_service = PanelApiService(settings) + + # Get panel system statistics + panel_stats = await panel_service.get_panel_statistics() + nodes_stats = await panel_service.get_nodes_statistics() + online_count = await panel_service.get_online_users_count() + users_activity = await panel_service.get_users_activity_stats() + + stats_text_parts.append(f"\nπŸ–₯ {_('admin_panel_stats_header', default='Бтатистика ΠΏΠ°Π½Π΅Π»ΠΈ')}") + + if online_count is not None: + stats_text_parts.append(f"🟒 Онлайн сСйчас: {online_count}") + + if users_activity: + today_connected = users_activity.get('today_connected', 'N/A') + week_connected = users_activity.get('week_connected', 'N/A') + never_connected = users_activity.get('never_connected', 'N/A') + stats_text_parts.append(f"πŸ“… ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π»ΠΈΡΡŒ сСгодня: {today_connected}") + stats_text_parts.append(f"πŸ“… ΠŸΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π»ΠΈΡΡŒ Π·Π° нСдСлю: {week_connected}") + stats_text_parts.append(f"❌ Никогда Π½Π΅ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π»ΠΈΡΡŒ: {never_connected}") + + if nodes_stats: + active_nodes = len([node for node in nodes_stats if node.get('status') == 'active']) + total_nodes = len(nodes_stats) + stats_text_parts.append(f"πŸ”— Активных Π½ΠΎΠ΄: {active_nodes}/{total_nodes}") + + if panel_stats: + system_info = panel_stats.get('system', {}) + if system_info: + memory_usage = system_info.get('memory_usage_percent', 'N/A') + cpu_usage = system_info.get('cpu_usage_percent', 'N/A') + stats_text_parts.append(f"πŸ’Ύ ИспользованиС RAM: {memory_usage}%") + stats_text_parts.append(f"πŸ”„ Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ° CPU: {cpu_usage}%") + + await panel_service.close() + + except Exception as e: + logging.warning(f"Failed to fetch panel statistics: {e}") + stats_text_parts.append(f"\nπŸ–₯ Бтатистика ΠΏΠ°Π½Π΅Π»ΠΈ") + stats_text_parts.append("❌ НС ΡƒΠ΄Π°Π»ΠΎΡΡŒ ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ с ΠΏΠ°Π½Π΅Π»ΠΈ") + final_text = "\n".join(stats_text_parts) try: diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py index 9ffb6d7..a09f499 100644 --- a/bot/services/panel_api_service.py +++ b/bot/services/panel_api_service.py @@ -460,3 +460,43 @@ class PanelApiService: async def get_bot_db_last_sync_status( self, session: AsyncSession) -> Optional[PanelSyncStatus]: return await panel_sync_dal.get_panel_sync_status(session) + + + async def get_panel_statistics(self) -> Optional[Dict[str, Any]]: + """Get general panel statistics""" + response_data = await self._request("GET", "/admin/stats", log_full_response=False) + if response_data and not response_data.get("error") and "response" in response_data: + return response_data.get("response") + return None + + + async def get_nodes_statistics(self) -> Optional[List[Dict[str, Any]]]: + """Get nodes statistics""" + response_data = await self._request("GET", "/admin/nodes/stats", log_full_response=False) + if response_data and not response_data.get("error") and "response" in response_data: + return response_data.get("response", {}).get("nodes", []) + return None + + + async def get_system_info(self) -> Optional[Dict[str, Any]]: + """Get system information""" + response_data = await self._request("GET", "/admin/system/info", log_full_response=False) + if response_data and not response_data.get("error") and "response" in response_data: + return response_data.get("response") + return None + + + async def get_online_users_count(self) -> Optional[int]: + """Get count of currently online users""" + response_data = await self._request("GET", "/admin/stats/online-users", log_full_response=False) + if response_data and not response_data.get("error") and "response" in response_data: + return response_data.get("response", {}).get("count", 0) + return None + + + async def get_users_activity_stats(self) -> Optional[Dict[str, Any]]: + """Get users activity statistics (today, week, never connected)""" + response_data = await self._request("GET", "/admin/stats/users-activity", log_full_response=False) + if response_data and not response_data.get("error") and "response" in response_data: + return response_data.get("response") + return None \ No newline at end of file diff --git a/bot/states/admin_states.py b/bot/states/admin_states.py index e7b4c63..21ee390 100644 --- a/bot/states/admin_states.py +++ b/bot/states/admin_states.py @@ -6,8 +6,17 @@ class AdminStates(StatesGroup): waiting_for_broadcast_message = State() confirming_broadcast = State() waiting_for_promo_details = State() + waiting_for_promo_code = State() + waiting_for_promo_bonus_days = State() + waiting_for_promo_max_activations = State() + waiting_for_promo_validity_days = State() + waiting_for_promo_description = State() waiting_for_promo_edit_details = State() waiting_for_bulk_promo_details = State() + waiting_for_bulk_promo_quantity = State() + waiting_for_bulk_promo_bonus_days = State() + waiting_for_bulk_promo_max_activations = State() + waiting_for_bulk_promo_validity_days = State() waiting_for_user_id_to_ban = State() waiting_for_user_id_to_unban = State() diff --git a/locales/en.json b/locales/en.json index 9290905..f02392b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -323,5 +323,42 @@ "admin_csv_header_raw_update_preview": "Update Preview", "admin_banned_users_empty": "πŸ“‹ Banned Users\n\nList is empty", - "admin_banned_users_list": "πŸ“‹ Banned Users ({count}):\n\n{users}" + "admin_banned_users_list": "πŸ“‹ Banned Users ({count}):\n\n{users}", + + "admin_panel_stats_header": "Panel Statistics", + "admin_bulk_promo_invalid_values": "❌ Invalid values: {error}", + + "admin_bulk_promo_step1_quantity": "πŸ“¦ Bulk Promo Code Creation\n\nStep 1 of 4: Quantity\n\nEnter the number of promo codes to create (1-1000):", + "admin_bulk_promo_step2_bonus_days": "πŸ“¦ Bulk Promo Code Creation\n\nStep 2 of 4: Bonus Days\n\nQuantity: {quantity}\n\nEnter the number of bonus days (1-365):", + "admin_bulk_promo_step3_max_activations": "πŸ“¦ Bulk Promo Code Creation\n\nStep 3 of 4: Max Activations\n\nQuantity: {quantity}\nBonus days: {bonus_days}\n\nEnter the maximum number of activations for each promo code (1-10000):", + "admin_bulk_promo_step4_validity": "πŸ“¦ Bulk Promo Code Creation\n\nStep 4 of 4: Validity Period\n\nQuantity: {quantity}\nBonus days: {bonus_days}\nMax activations: {max_activations}\n\nChoose the validity period for promo codes:", + + "admin_bulk_promo_invalid_quantity": "❌ Quantity must be between 1 and 1000", + "admin_bulk_promo_invalid_bonus_days": "❌ Bonus days must be between 1 and 365", + "admin_bulk_promo_invalid_max_activations": "❌ Max activations must be between 1 and 10000", + "admin_bulk_promo_invalid_validity_days": "❌ Validity days must be between 1 and 365", + + "admin_bulk_promo_unlimited_validity": "♾️ Unlimited", + "admin_bulk_promo_set_validity": "⏰ Set Period", + "admin_bulk_promo_enter_validity_days": "⏰ Enter the number of validity days for promo codes (1-365):", + + "admin_bulk_promo_creating": "⏳ Creating {quantity} promo codes...", + + "admin_promo_step1_code": "🎟 Create Promo Code\n\nStep 1 of 5: Promo Code\n\nEnter promo code (3-30 characters, letters and numbers only):", + "admin_promo_step2_bonus_days": "🎟 Create Promo Code\n\nStep 2 of 5: Bonus Days\n\nCode: {code}\n\nEnter the number of bonus days (1-365):", + "admin_promo_step3_max_activations": "🎟 Create Promo Code\n\nStep 3 of 5: Max Activations\n\nCode: {code}\nBonus days: {bonus_days}\n\nEnter the maximum number of activations (1-10000):", + "admin_promo_step4_validity": "🎟 Create Promo Code\n\nStep 4 of 5: Validity Period\n\nCode: {code}\nBonus days: {bonus_days}\nMax activations: {max_activations}\n\nChoose the validity period for the promo code:", + "admin_promo_step5_description": "🎟 Create Promo Code\n\nStep 5 of 5: Description (optional)\n\nCode: {code}\nBonus days: {bonus_days}\nMax activations: {max_activations}\nValid until: {validity}\n\nEnter promo code description or click \"Skip\":", + + "admin_promo_invalid_code_format": "❌ Promo code must contain 3-30 characters (letters and numbers only)", + "admin_promo_code_already_exists": "❌ A promo code with this code already exists", + "admin_promo_processing_error": "❌ Error processing promo code", + "admin_promo_unlimited_validity": "♾️ Unlimited", + "admin_promo_set_validity": "⏰ Set Period", + "admin_promo_enter_validity_days": "⏰ Enter the number of validity days for the promo code (1-365):", + "admin_promo_skip_description": "⏭️ Skip", + "admin_promo_description_too_long": "❌ Description must not exceed 200 characters", + "admin_promo_creating": "⏳ Creating promo code...", + "admin_promo_created_success": "βœ… Promo code created!\n\n🎟 Code: {code}\n🎁 Bonus days: {days}\nπŸ”’ Max activations: {max_act}\n⏰ Valid until: {validity}\nπŸ“ Description: {description}", + "admin_promo_creation_failed": "❌ Error creating promo code: {error}" } diff --git a/locales/ru.json b/locales/ru.json index 220cf96..a0c6bbf 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -323,5 +323,42 @@ "admin_csv_header_raw_update_preview": "ΠŸΡ€Π΅Π²ΡŒΡŽ обновлСния", "admin_banned_users_empty": "πŸ“‹ Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΠΈ\n\nБписок пуст", - "admin_banned_users_list": "πŸ“‹ Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΠΈ ({count}):\n\n{users}" + "admin_banned_users_list": "πŸ“‹ Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΠΈ ({count}):\n\n{users}", + + "admin_panel_stats_header": "Бтатистика ΠΏΠ°Π½Π΅Π»ΠΈ", + "admin_bulk_promo_invalid_values": "❌ НСвСрныС значСния: {error}", + + "admin_bulk_promo_step1_quantity": "πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 1 ΠΈΠ· 4: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² для создания (1-1000):", + "admin_bulk_promo_step2_bonus_days": "πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 2 ΠΈΠ· 4: БонусныС Π΄Π½ΠΈ\n\nΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ: {quantity}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство бонусных Π΄Π½Π΅ΠΉ (1-365):", + "admin_bulk_promo_step3_max_activations": "πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 3 ΠΈΠ· 4: ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ\n\nΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ: {quantity}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ максимальноС количСство Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΉ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-10000):", + "admin_bulk_promo_step4_validity": "πŸ“¦ МассовоС созданиС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²\n\nΠ¨Π°Π³ 4 ΠΈΠ· 4: Π‘Ρ€ΠΎΠΊ дСйствия\n\nΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ: {quantity}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\nМакс. Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_activations}\n\nΠ’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ срок дСйствия ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²:", + + "admin_bulk_promo_invalid_quantity": "❌ ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 1000", + "admin_bulk_promo_invalid_bonus_days": "❌ БонусныС Π΄Π½ΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365", + "admin_bulk_promo_invalid_max_activations": "❌ ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 10000", + "admin_bulk_promo_invalid_validity_days": "❌ Π”Π½ΠΈ дСйствия Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ ΠΎΡ‚ 1 Π΄ΠΎ 365", + + "admin_bulk_promo_unlimited_validity": "♾️ НСограничСнно", + "admin_bulk_promo_set_validity": "⏰ Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ срок", + "admin_bulk_promo_enter_validity_days": "⏰ Π’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство Π΄Π½Π΅ΠΉ дСйствия ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ² (1-365):", + + "admin_bulk_promo_creating": "⏳ Боздаю {quantity} ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ΠΎΠ²...", + + "admin_promo_step1_code": "🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 1 ΠΈΠ· 5: Код ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ ΠΊΠΎΠ΄ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° (3-30 символов, Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π±ΡƒΠΊΠ²Ρ‹ ΠΈ Ρ†ΠΈΡ„Ρ€Ρ‹):", + "admin_promo_step2_bonus_days": "🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 2 ΠΈΠ· 5: БонусныС Π΄Π½ΠΈ\n\nКод: {code}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство бонусных Π΄Π½Π΅ΠΉ (1-365):", + "admin_promo_step3_max_activations": "🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 3 ΠΈΠ· 5: ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ\n\nКод: {code}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ максимальноС количСство Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΉ (1-10000):", + "admin_promo_step4_validity": "🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 4 ΠΈΠ· 5: Π‘Ρ€ΠΎΠΊ дСйствия\n\nКод: {code}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\nМакс. Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_activations}\n\nΠ’Ρ‹Π±Π΅Ρ€ΠΈΡ‚Π΅ срок дСйствия ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°:", + "admin_promo_step5_description": "🎟 Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°\n\nΠ¨Π°Π³ 5 ΠΈΠ· 5: ОписаниС (ΠΎΠΏΡ†ΠΈΠΎΠ½Π°Π»ΡŒΠ½ΠΎ)\n\nКод: {code}\nБонусныС Π΄Π½ΠΈ: {bonus_days}\nМакс. Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_activations}\nДСйствуСт: {validity}\n\nΠ’Π²Π΅Π΄ΠΈΡ‚Π΅ описаниС ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° ΠΈΠ»ΠΈ Π½Π°ΠΆΠΌΠΈΡ‚Π΅ \"ΠŸΡ€ΠΎΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ\":", + + "admin_promo_invalid_code_format": "❌ Код ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° Π΄ΠΎΠ»ΠΆΠ΅Π½ ΡΠΎΠ΄Π΅Ρ€ΠΆΠ°Ρ‚ΡŒ 3-30 символов (Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π±ΡƒΠΊΠ²Ρ‹ ΠΈ Ρ†ΠΈΡ„Ρ€Ρ‹)", + "admin_promo_code_already_exists": "❌ ΠŸΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ с Ρ‚Π°ΠΊΠΈΠΌ ΠΊΠΎΠ΄ΠΎΠΌ ΡƒΠΆΠ΅ сущСствуСт", + "admin_promo_processing_error": "❌ Ошибка ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°", + "admin_promo_unlimited_validity": "♾️ НСограничСнно", + "admin_promo_set_validity": "⏰ Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ срок", + "admin_promo_enter_validity_days": "⏰ Π’Π²Π΅Π΄ΠΈΡ‚Π΅ количСство Π΄Π½Π΅ΠΉ дСйствия ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π° (1-365):", + "admin_promo_skip_description": "⏭️ ΠŸΡ€ΠΎΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ", + "admin_promo_description_too_long": "❌ ОписаниС Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ°Ρ‚ΡŒ 200 символов", + "admin_promo_creating": "⏳ Боздаю ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄...", + "admin_promo_created_success": "βœ… ΠŸΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄ создан!\n\n🎟 Код: {code}\n🎁 БонусныС Π΄Π½ΠΈ: {days}\nπŸ”’ ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹Π΅ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠΈ: {max_act}\n⏰ ДСйствуСт Π΄ΠΎ: {validity}\nπŸ“ ОписаниС: {description}", + "admin_promo_creation_failed": "❌ Ошибка создания ΠΏΡ€ΠΎΠΌΠΎΠΊΠΎΠ΄Π°: {error}" }