import logging import re from aiogram import Router, F, types, Bot from aiogram.fsm.context import FSMContext from typing import Optional from config.settings import Settings from bot.states.user_states import UserPromoStates from bot.services.promo_code_service import PromoCodeService from bot.services.subscription_service import SubscriptionService from bot.keyboards.inline.user_keyboards import get_back_to_main_menu_markup from bot.middlewares.i18n import JsonI18n from aiogram.utils.markdown import hcode from .start import send_main_menu router = Router(name="user_promo_router") SUSPICIOUS_SQL_KEYWORDS_REGEX = re.compile( r"\b(DROP\s*TABLE|DELETE\s*FROM|ALTER\s*TABLE|TRUNCATE\s*TABLE|UNION\s*SELECT|;\s*SELECT|;\s*INSERT|;\s*UPDATE|;\s*DELETE|xp_cmdshell|sysdatabases|sysobjects|INFORMATION_SCHEMA)\b", re.IGNORECASE) SUSPICIOUS_CHARS_REGEX = re.compile(r"(--|#\s|;|\*\/|\/\*)") async def prompt_promo_code_input(callback: types.CallbackQuery, state: FSMContext, i18n_data: dict, settings: Settings): current_lang = i18n_data.get("current_language", getattr(settings, 'DEFAULT_LANGUAGE', 'en')) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") if not i18n: await callback.answer("Language error.", show_alert=True) return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if not callback.message: logging.error( "CallbackQuery has no message in prompt_promo_code_input") await callback.answer(_("error_occurred_processing_request"), show_alert=True) return try: await callback.message.edit_text( text=_(key="promo_code_prompt"), reply_markup=get_back_to_main_menu_markup(current_lang, i18n)) except Exception as e: logging.warning(f"Failed to edit message for promo prompt: {e}") await callback.message.answer( text=_(key="promo_code_prompt"), reply_markup=get_back_to_main_menu_markup(current_lang, i18n)) await callback.answer() await state.set_state(UserPromoStates.waiting_for_promo_code) logging.info( f"User {callback.from_user.id} entered state UserPromoStates.waiting_for_promo_code. FSM state: {await state.get_state()}" ) @router.message(UserPromoStates.waiting_for_promo_code, F.text) async def process_promo_code_input(message: types.Message, state: FSMContext, settings: Settings, i18n_data: dict, promo_code_service: PromoCodeService, bot: Bot): logging.info( f"Processing promo code input from user {message.from_user.id} in state {await state.get_state()}: '{message.text}'" ) current_lang = i18n_data.get("current_language", getattr(settings, 'DEFAULT_LANGUAGE', 'en')) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") if not i18n or not promo_code_service: logging.error("Deps missing in process_promo_code_input") await message.reply("Service error. Please try again.") await state.clear() return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) code_input = message.text.strip() user = message.from_user is_suspicious = False if SUSPICIOUS_SQL_KEYWORDS_REGEX.search( code_input) or SUSPICIOUS_CHARS_REGEX.search(code_input) or len( code_input) > 100: is_suspicious = True logging.warning( f"Suspicious input for promo by user {user.id} (len: {len(code_input)}): '{code_input}'" ) response_to_user_text = "" if is_suspicious: admin_notify_key = "admin_suspicious_promo_attempt_notification_no_username" if not user.username else "admin_suspicious_promo_attempt_notification" admin_lang = settings.DEFAULT_LANGUAGE _admin = lambda k, **kw: i18n.gettext(admin_lang, k, **kw) admin_notification_text = _admin(admin_notify_key, user_id=user.id, user_username=user.username or "N/A", user_first_name=user.first_name or "N/A", promo_code_input=hcode(code_input)) try: await bot.send_message(settings.ADMIN_ID, admin_notification_text, parse_mode="HTML") except Exception as e_admin_notify: logging.error( f"Failed to send suspicious promo notification to admin: {e_admin_notify}" ) response_to_user_text = _("promo_code_not_found", code=code_input.upper()) else: success, response_text_from_service = await promo_code_service.apply_promo_code( user.id, code_input, current_lang) response_to_user_text = response_text_from_service await message.answer(response_to_user_text, reply_markup=get_back_to_main_menu_markup( current_lang, i18n)) await state.clear() logging.info( f"Promo code '{code_input}' processing finished for user {message.from_user.id}. State cleared." ) @router.callback_query(F.data == "main_action:back_to_main", UserPromoStates.waiting_for_promo_code) async def cancel_promo_input_via_button( callback: types.CallbackQuery, state: FSMContext, settings: Settings, i18n_data: dict, subscription_service: SubscriptionService): current_lang = i18n_data.get("current_language", getattr(settings, 'DEFAULT_LANGUAGE', 'en')) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") if not i18n: logging.error("i18n missing in cancel_promo_input_via_button") await callback.answer("Language error", show_alert=True) return logging.info( f"User {callback.from_user.id} cancelled promo code input via button from state {await state.get_state()}. Clearing state." ) await state.clear() logging.info( f"State after clear for user {callback.from_user.id}: {await state.get_state()}" ) if callback.message: show_trial_button_on_back = False if settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription( callback.from_user.id): show_trial_button_on_back = True await send_main_menu(callback, settings, i18n_data, show_trial_button_flag=show_trial_button_on_back, is_edit=True) else: await callback.answer("Promo code input cancelled.", show_alert=False)