diff --git a/bot/handlers/admin/promo_codes.py b/bot/handlers/admin/promo_codes.py index a22cc7d..e8e9403 100644 --- a/bot/handlers/admin/promo_codes.py +++ b/bot/handlers/admin/promo_codes.py @@ -465,10 +465,12 @@ async def view_promo_codes_handler(callback: types.CallbackQuery, await callback.answer() -@router.callback_query(F.data == "admin_action:manage_promos") -async def manage_promo_codes_handler(callback: types.CallbackQuery, - i18n_data: dict, settings: Settings, - session: AsyncSession): +# New unified promo management system +@router.callback_query(F.data == "admin_action:promo_management") +async def promo_management_handler(callback: types.CallbackQuery, + i18n_data: dict, settings: Settings, + session: AsyncSession): + """Show list of all promo codes for management""" current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") if not i18n or not callback.message: @@ -476,36 +478,168 @@ async def manage_promo_codes_handler(callback: types.CallbackQuery, return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) - promo_models = await promo_code_dal.get_all_active_promo_codes(session, - limit=20, - offset=0) + # Get ALL promo codes (including inactive) + promo_models = await promo_code_dal.get_all_promo_codes_with_details(session, limit=50, offset=0) if not promo_models: await callback.message.edit_text( - _("admin_no_active_promos"), + _("admin_promo_management_empty", default="📭 Промокоды отсутствуют"), reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n)) await callback.answer() return kb = InlineKeyboardBuilder() for promo in promo_models: + # Show promo code with status indicator + status_emoji = "✅" if promo.is_active else "🚫" + if promo.valid_until and promo.valid_until < datetime.now(timezone.utc): + status_emoji = "⏰" # Expired + elif promo.current_activations >= promo.max_activations: + status_emoji = "🔄" # Used up + + button_text = f"{status_emoji} {promo.code} ({promo.current_activations}/{promo.max_activations})" kb.row( InlineKeyboardButton( - text=promo.code, - callback_data=f"promo_edit:{promo.promo_code_id}"), - InlineKeyboardButton( - text=_("admin_promo_delete_button"), - callback_data=f"promo_delete:{promo.promo_code_id}"), + text=button_text, + callback_data=f"promo_detail:{promo.promo_code_id}") ) + kb.row( - InlineKeyboardButton(text=_("back_to_admin_panel_button"), + InlineKeyboardButton(text=_("back_to_admin_panel_button", default="⬅️ Назад"), callback_data="admin_action:main")) await callback.message.edit_text( - _("admin_manage_promos_title"), - reply_markup=kb.as_markup()) + _("admin_promo_management_title", default="🎟 Управление промокодами\n\nВыберите промокод для детального просмотра:"), + reply_markup=kb.as_markup(), + parse_mode="HTML") await callback.answer() +@router.callback_query(F.data.startswith("promo_detail:")) +async def promo_detail_handler(callback: types.CallbackQuery, + i18n_data: dict, settings: Settings, + session: AsyncSession): + """Show detailed promo code information with management options""" + current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) + i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") + if not i18n or not callback.message: + await callback.answer("Error displaying promo details.", show_alert=True) + return + _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + + promo_id = int(callback.data.split(":")[1]) + promo = await promo_code_dal.get_promo_code_by_id(session, promo_id) + if not promo: + await callback.answer(_("admin_promo_not_found", default="Промокод не найден"), show_alert=True) + return + + # Determine status + status = _("admin_promo_status_active", default="✅ Активен") + if not promo.is_active: + status = _("admin_promo_status_inactive", default="🚫 Неактивен") + elif promo.valid_until and promo.valid_until < datetime.now(timezone.utc): + status = _("admin_promo_status_expired", default="⏰ Истек") + elif promo.current_activations >= promo.max_activations: + status = _("admin_promo_status_used_up", default="🔄 Исчерпан") + + # Format validity + validity = _("admin_promo_valid_indefinitely", default="Неограниченно") + if promo.valid_until: + validity = promo.valid_until.strftime('%Y-%m-%d %H:%M') + + # Format created date + created_date = promo.created_at.strftime('%Y-%m-%d %H:%M') if promo.created_at else "N/A" + creator = f"Admin {promo.created_by_admin_id}" if promo.created_by_admin_id else "N/A" + + # Build card text + card_text = _( + "admin_promo_card_title", + default="🎟 Промокод: {code}", + code=promo.code + ) + "\n\n" + + card_text += _( + "admin_promo_card_bonus_days", + default="🎁 Бонусные дни: {days}", + days=promo.bonus_days + ) + "\n" + + card_text += _( + "admin_promo_card_activations", + default="🔢 Активации: {current}/{max}", + current=promo.current_activations, + max=promo.max_activations + ) + "\n" + + card_text += _( + "admin_promo_card_validity", + default="⏰ Действует до: {validity}", + validity=validity + ) + "\n" + + card_text += _( + "admin_promo_card_status", + default="📊 Статус: {status}", + status=status + ) + "\n" + + card_text += _( + "admin_promo_card_created", + default="📅 Создан: {created}", + created=created_date + ) + "\n" + + card_text += _( + "admin_promo_card_created_by", + default="👤 Создал: {creator}", + creator=creator + ) + + # Build keyboard + kb = InlineKeyboardBuilder() + + # Row 1: Edit and Toggle status + kb.row( + InlineKeyboardButton( + text=_("admin_promo_edit_button", default="✏️ Редактировать"), + callback_data=f"promo_edit:{promo_id}"), + InlineKeyboardButton( + text=_("admin_promo_toggle_status_button", default="🔄 Вкл/Выкл"), + callback_data=f"promo_toggle:{promo_id}") + ) + + # Row 2: View activations and Delete + kb.row( + InlineKeyboardButton( + text=_("admin_promo_view_activations_button", default="📋 Активации"), + callback_data=f"promo_activations:{promo_id}"), + InlineKeyboardButton( + text=_("admin_promo_delete_button", default="🗑 Удалить"), + callback_data=f"promo_delete:{promo_id}") + ) + + # Row 3: Back to list + kb.row( + InlineKeyboardButton( + text=_("admin_promo_back_to_list_button", default="⬅️ К списку"), + callback_data="admin_action:promo_management") + ) + + await callback.message.edit_text( + card_text, + reply_markup=kb.as_markup(), + parse_mode="HTML") + await callback.answer() + + +# Legacy manage_promo_codes_handler - keeping for compatibility +@router.callback_query(F.data == "admin_action:manage_promos") +async def manage_promo_codes_handler(callback: types.CallbackQuery, + i18n_data: dict, settings: Settings, + session: AsyncSession): + # Redirect to new unified handler + await promo_management_handler(callback, i18n_data, settings, session) + + @router.callback_query(F.data.startswith("promo_edit:")) async def promo_edit_select_handler(callback: types.CallbackQuery, state: FSMContext, i18n_data: dict, settings: Settings, diff --git a/bot/handlers/admin/statistics.py b/bot/handlers/admin/statistics.py index cfe3279..485dda1 100644 --- a/bot/handlers/admin/statistics.py +++ b/bot/handlers/admin/statistics.py @@ -37,27 +37,97 @@ async def show_statistics_handler(callback: types.CallbackQuery, f"\n👥 {_('admin_enhanced_users_stats_header', default='Пользователи')}" ) stats_text_parts.append( - f"📊 Всего: {user_stats['total_users']}" + f"📊 {_('admin_user_stats_total_label', default='Всего')}: {user_stats['total_users']}" + ) + # Removed: Active today moved to panel stats + stats_text_parts.append( + f"💳 {_('admin_user_stats_paid_subs_label', default='С платной подпиской')}: {user_stats['paid_subscriptions']}" ) stats_text_parts.append( - f"📈 Активных сегодня: {user_stats['active_today']}" + f"🆓 {_('admin_user_stats_trial_label', default='На пробном периоде')}: {user_stats['trial_users']}" ) stats_text_parts.append( - f"💳 С платной подпиской: {user_stats['paid_subscriptions']}" + f"😴 {_('admin_user_stats_inactive_label', default='Неактивных')}: {user_stats['inactive_users']}" ) stats_text_parts.append( - f"🆓 На пробном периоде: {user_stats['trial_users']}" + f"🚫 {_('admin_user_stats_banned_label', default='Заблокированных')}: {user_stats['banned_users']}" ) stats_text_parts.append( - f"😴 Неактивных: {user_stats['inactive_users']}" - ) - stats_text_parts.append( - f"🚫 Заблокированных: {user_stats['banned_users']}" - ) - stats_text_parts.append( - f"🎁 Привлечено по реферальной программе: {user_stats['referral_users']}" + f"🎁 {_('admin_user_stats_referral_label', default='Привлечено по реферальной программе')}: {user_stats['referral_users']}" ) + # Panel Statistics - moved above financial + stats_text_parts.append(f"\n🖥 {_('admin_panel_stats_header', default='Статистика панели')}") + + try: + async with PanelApiService(settings) as panel_service: + # Get system stats + system_stats = await panel_service.get_system_stats() + bandwidth_stats = await panel_service.get_bandwidth_stats() + nodes_stats = await panel_service.get_nodes_statistics() + + logging.info(f"Panel stats response: system={system_stats}, bandwidth={bandwidth_stats}, nodes={nodes_stats}") + + if system_stats: + users = system_stats.get('users', {}) + active_users = users.get('active', 0) + disabled_users = users.get('disabled', 0) + expired_users = users.get('expired', 0) + limited_users = users.get('limited', 0) + total_users = users.get('total', 0) + + stats_text_parts.append(f"🟢 {_('admin_panel_online_label', default='Онлайн')}: {active_users}") + stats_text_parts.append(f"🔴 {_('admin_panel_offline_label', default='Офлайн')}: {disabled_users}") + stats_text_parts.append(f"⏰ {_('admin_panel_expired_label', default='Истекшие')}: {expired_users}") + stats_text_parts.append(f"⚠️ {_('admin_panel_limited_label', default='Ограниченные')}: {limited_users}") + stats_text_parts.append(f"👥 {_('admin_panel_total_users_label', default='Всего пользователей')}: {total_users}") + + # System resources + cpu = system_stats.get('cpu', {}) + memory = system_stats.get('memory', {}) + if cpu: + cpu_usage = cpu.get('usage', 0) + stats_text_parts.append(f"🔄 {_('admin_panel_cpu_usage_label', default='Загрузка CPU')}: {cpu_usage:.1f}%") + if memory: + memory_usage = memory.get('usage', 0) + stats_text_parts.append(f"💾 {_('admin_panel_memory_usage_label', default='Использование RAM')}: {memory_usage:.1f}%") + else: + stats_text_parts.append(f"⚠️ {_('admin_panel_system_stats_error', default='Ошибка получения системной статистики')}") + + # Bandwidth stats + if bandwidth_stats: + today_traffic = bandwidth_stats.get('bandwidthToday', {}) + week_traffic = bandwidth_stats.get('bandwidthLastSevenDays', {}) + month_traffic = bandwidth_stats.get('bandwidthLastThirtyDays', {}) + + if today_traffic: + today_total = today_traffic.get('total', '0 B') + stats_text_parts.append(f"📊 {_('admin_panel_traffic_today_label', default='Трафик сегодня')}: {today_total}") + + if week_traffic: + week_total = week_traffic.get('total', '0 B') + stats_text_parts.append(f"📊 {_('admin_panel_traffic_week_label', default='Трафик за неделю')}: {week_total}") + + if month_traffic: + month_total = month_traffic.get('total', '0 B') + stats_text_parts.append(f"📊 {_('admin_panel_traffic_month_label', default='Трафик за месяц')}: {month_total}") + else: + stats_text_parts.append(f"⚠️ {_('admin_panel_bandwidth_stats_error', default='Ошибка получения статистики трафика')}") + + # Nodes stats + if nodes_stats: + last_seven_days = nodes_stats.get('lastSevenDays', []) + active_nodes_count = len([node for node in last_seven_days if node.get('status') == 'active']) + total_nodes_count = len(last_seven_days) + stats_text_parts.append(f"🔗 {_('admin_panel_nodes_label', default='Активных нод')}: {active_nodes_count}/{total_nodes_count}") + else: + stats_text_parts.append(f"⚠️ {_('admin_panel_nodes_stats_error', default='Ошибка получения статистики нод')}") + + except Exception as e: + logging.error(f"Failed to fetch panel statistics: {e}", exc_info=True) + stats_text_parts.append(f"❌ {_('admin_panel_stats_fetch_error', default='Ошибка получения данных с панели')}") + stats_text_parts.append(f"⚠️ {_('admin_panel_stats_error_details', default='Детали')}: {str(e)}") + # Financial statistics financial_stats = await payment_dal.get_financial_statistics(session) @@ -65,16 +135,16 @@ async def show_statistics_handler(callback: types.CallbackQuery, f"\n💰 {_('admin_financial_stats_header', default='Финансовая статистика')}" ) stats_text_parts.append( - f"📅 За сегодня: {financial_stats['today_revenue']:.2f} RUB ({financial_stats['today_payments_count']} платежей)" + f"📅 {_('admin_financial_today_label', default='За сегодня')}: {financial_stats['today_revenue']:.2f} RUB ({financial_stats['today_payments_count']} {_('admin_financial_payments_label', default='платежей')})" ) stats_text_parts.append( - f"📅 За неделю: {financial_stats['week_revenue']:.2f} RUB" + f"📅 {_('admin_financial_week_label', default='За неделю')}: {financial_stats['week_revenue']:.2f} RUB" ) stats_text_parts.append( - f"📅 За месяц: {financial_stats['month_revenue']:.2f} RUB" + f"📅 {_('admin_financial_month_label', default='За месяц')}: {financial_stats['month_revenue']:.2f} RUB" ) stats_text_parts.append( - f"🏆 За все время: {financial_stats['all_time_revenue']:.2f} RUB" + f"🏆 {_('admin_financial_all_time_label', default='За все время')}: {financial_stats['all_time_revenue']:.2f} RUB" ) last_payments_models: List[ diff --git a/bot/handlers/inline_mode.py b/bot/handlers/inline_mode.py index 693ea7a..7634414 100644 --- a/bot/handlers/inline_mode.py +++ b/bot/handlers/inline_mode.py @@ -92,8 +92,7 @@ async def create_referral_result(inline_query: InlineQuery, bot: Bot, "✨ Быстрый и надежный\n" "🔒 Полная анонимность\n" "🌍 Серверы по всему миру\n" - "💎 Бесплатный пробный период\n\n" - "Переходи по ссылке: {referral_link}", + "💎 Бесплатный пробный период\n\n{referral_link}", referral_link=referral_link ) @@ -180,8 +179,10 @@ async def create_user_stats_result(session: AsyncSession, i18n_instance, lang: s default="👥 Статистика пользователей" ), description=_( - "inline_admin_user_stats_desc", - default=f"Всего: {user_stats['total_users']}, Активных: {user_stats['paid_subscriptions']}" + "inline_stats_description", + default="Всего: {total}, Активных: {active}", + total=user_stats['total_users'], + active=user_stats['paid_subscriptions'] ), input_message_content=InputTextMessageContent( message_text=stats_text, @@ -225,8 +226,9 @@ async def create_financial_stats_result(session: AsyncSession, i18n_instance, la default="💰 Финансовая статистика" ), description=_( - "inline_admin_financial_stats_desc", - default=f"Сегодня: {financial_stats['today_revenue']:.2f} RUB" + "inline_financial_description", + default="Сегодня: {today} RUB", + today=f"{financial_stats['today_revenue']:.2f}" ), input_message_content=InputTextMessageContent( message_text=stats_text, @@ -296,8 +298,10 @@ async def create_system_stats_result(session: AsyncSession, i18n_instance, lang: default="🖥 Системная статистика" ), description=_( - "inline_admin_system_stats_desc", - default=f"Онлайн: {active_subs}, Офлайн: {max(0, offline_users)}" + "inline_system_description", + default="Онлайн: {online}, Офлайн: {offline}", + online=active_subs, + offline=max(0, offline_users) ), input_message_content=InputTextMessageContent( message_text=stats_text, @@ -327,7 +331,7 @@ async def create_help_result(i18n_instance, lang: str, is_admin: bool) -> Inline "💡 Просто напишите @{bot_username} и начните вводить команду в любом чате!" ) title = _("inline_admin_help_title", default="🤖 Inline помощь (Админ)") - description = _("inline_admin_help_desc", default="Доступны команды: реф, стат, финансы, система") + description = _("inline_admin_help_description", default="Доступны команды: реф, стат, финансы, система") else: help_text = _( "inline_user_help_message", @@ -337,7 +341,7 @@ async def create_help_result(i18n_instance, lang: str, is_admin: bool) -> Inline "💡 Просто напишите @{bot_username} и начните вводить 'реф' в любом чате!" ) title = _("inline_user_help_title", default="🤖 Inline помощь") - description = _("inline_user_help_desc", default="Доступна команда: реф (реферальная ссылка)") + description = _("inline_user_help_description", default="Доступна команда: реф (реферальная ссылка)") return InlineQueryResultArticle( id="help", diff --git a/bot/handlers/user/promo_user.py b/bot/handlers/user/promo_user.py index f720324..18fa120 100644 --- a/bot/handlers/user/promo_user.py +++ b/bot/handlers/user/promo_user.py @@ -105,30 +105,23 @@ async def process_promo_code_input(message: types.Message, state: FSMContext, response_to_user_text = "" if is_suspicious: - - if settings.ADMIN_IDS: - admin_notify_key = "admin_suspicious_promo_attempt_notification" if user.username else "admin_suspicious_promo_attempt_notification_no_username" - - 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)) - for admin_id in settings.ADMIN_IDS: - try: - await bot.send_message(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 {admin_id}: {e_admin_notify}" - ) + # Send notification through NotificationService if enabled + if settings.LOG_SUSPICIOUS_ACTIVITY: + try: + from bot.services.notification_service import NotificationService + notification_service = NotificationService(bot, settings, i18n) + await notification_service.notify_suspicious_promo_attempt( + user_id=user.id, + username=user.username, + first_name=user.first_name, + suspicious_input=code_input + ) + except Exception as e: + logging.error(f"Failed to send suspicious promo notification: {e}") response_to_user_text = _("promo_code_not_found", code=hcode(code_input.upper())) + reply_markup = get_back_to_main_menu_markup(current_lang, i18n) else: success, result = await promo_code_service.apply_promo_code( diff --git a/bot/keyboards/inline/admin_keyboards.py b/bot/keyboards/inline/admin_keyboards.py index 15e7c32..99eb072 100644 --- a/bot/keyboards/inline/admin_keyboards.py +++ b/bot/keyboards/inline/admin_keyboards.py @@ -88,14 +88,12 @@ def get_promo_marketing_keyboard(i18n_instance, lang: str) -> InlineKeyboardMark callback_data="admin_action:create_promo") builder.button(text=_(key="admin_create_bulk_promo_button"), callback_data="admin_action:create_bulk_promo") - builder.button(text=_(key="admin_manage_promos_button"), - callback_data="admin_action:manage_promos") - builder.button(text=_(key="admin_view_promos_button"), - callback_data="admin_action:view_promos") + builder.button(text=_(key="admin_promo_management_button"), + callback_data="admin_action:promo_management") builder.button(text=_(key="back_to_admin_panel_button"), callback_data="admin_action:main") - builder.adjust(2, 2, 1) + builder.adjust(2, 1, 1) return builder.as_markup() diff --git a/bot/services/panel_api_service.py b/bot/services/panel_api_service.py index 582ad55..2f036b1 100644 --- a/bot/services/panel_api_service.py +++ b/bot/services/panel_api_service.py @@ -470,41 +470,23 @@ class PanelApiService: 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) + async def get_system_stats(self) -> Optional[Dict[str, Any]]: + """Get system statistics (CPU, memory, users counts)""" + response_data = await self._request("GET", "/system/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_bandwidth_stats(self) -> Optional[Dict[str, Any]]: + """Get bandwidth statistics""" + response_data = await self._request("GET", "/system/stats/bandwidth", 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]]]: + async def get_nodes_statistics(self) -> Optional[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) + response_data = await self._request("GET", "/system/stats/nodes", 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/services/promo_code_service.py b/bot/services/promo_code_service.py index 4338d81..6aaaca8 100644 --- a/bot/services/promo_code_service.py +++ b/bot/services/promo_code_service.py @@ -11,7 +11,7 @@ from db.models import PromoCode, User from .subscription_service import SubscriptionService from bot.middlewares.i18n import JsonI18n -from .notification_service import notify_admin_promo_activation, NotificationService +from .notification_service import NotificationService class PromoCodeService: @@ -75,15 +75,6 @@ class PromoCodeService: except Exception as e: logging.error(f"Failed to send promo activation notification: {e}") - # Legacy notification for backwards compatibility - await notify_admin_promo_activation( - self.bot, - self.settings, - self.i18n, - user_id, - code_input_upper, - bonus_days, - ) return True, new_end_date else: diff --git a/config/settings.py b/config/settings.py index 37df041..208a4aa 100644 --- a/config/settings.py +++ b/config/settings.py @@ -306,6 +306,7 @@ class Settings(BaseSettings): LOG_PAYMENTS: bool = Field(default=True, description="Send notifications for successful payments") LOG_PROMO_ACTIVATIONS: bool = Field(default=True, description="Send notifications for promo code activations") LOG_TRIAL_ACTIVATIONS: bool = Field(default=True, description="Send notifications for trial activations") + LOG_SUSPICIOUS_ACTIVITY: bool = Field(default=True, description="Send notifications for suspicious promo attempts") model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', diff --git a/db/dal/promo_code_dal.py b/db/dal/promo_code_dal.py index 591be40..f937b4d 100644 --- a/db/dal/promo_code_dal.py +++ b/db/dal/promo_code_dal.py @@ -56,6 +56,33 @@ async def get_all_active_promo_codes(session: AsyncSession, return result.scalars().all() +async def get_all_promo_codes_with_details(session: AsyncSession, limit: int = 50, + offset: int = 0) -> List[PromoCode]: + """Get all promo codes (active and inactive) with pagination for management""" + stmt = (select(PromoCode).order_by( + PromoCode.created_at.desc()).limit(limit).offset(offset)) + result = await session.execute(stmt) + return result.scalars().all() + + +async def get_promo_code_by_id(session: AsyncSession, promo_id: int) -> Optional[PromoCode]: + """Get promo code by ID""" + stmt = select(PromoCode).where(PromoCode.promo_code_id == promo_id) + result = await session.execute(stmt) + return result.scalar_one_or_none() + + +async def get_promo_activations_by_code_id(session: AsyncSession, promo_code_id: int) -> List: + """Get activation history for a specific promo code""" + from db.models import PromoCodeActivation + stmt = (select(PromoCodeActivation) + .where(PromoCodeActivation.promo_code_id == promo_code_id) + .order_by(PromoCodeActivation.created_at.desc()) + .limit(50)) # Limit to last 50 activations + result = await session.execute(stmt) + return result.scalars().all() + + async def update_promo_code(session: AsyncSession, promo_id: int, update_data: Dict[str, Any]) -> Optional[PromoCode]: promo = await get_promo_code_by_id(session, promo_id) diff --git a/locales/en.json b/locales/en.json index 612a39c..50d6ab8 100644 --- a/locales/en.json +++ b/locales/en.json @@ -398,5 +398,72 @@ "admin_user_subscription_active_until": "⏰ Active until:", "admin_user_subscription_error": "Loading error", - "admin_bulk_promo_unique_generation_failed": "Failed to create unique promo code" + "admin_bulk_promo_unique_generation_failed": "Failed to create unique promo code", + + "admin_suspicious_promo_attempt_notification": "⚠️ Suspicious promo code attempt\n\n👤 User: @{user_username} (ID: {user_id})\n📝 Name: {user_first_name}\n💬 Input text: {promo_code_input}", + "admin_suspicious_promo_attempt_notification_no_username": "⚠️ Suspicious promo code attempt\n\n👤 User ID: {user_id}\n📝 Name: {user_first_name}\n💬 Input text: {promo_code_input}", + + "admin_promo_management_button": "🎟 Promo Management", + "admin_promo_management_title": "🎟 Promo Code Management\n\nSelect a promo code for detailed view:", + "admin_promo_management_empty": "📭 No promo codes available", + "admin_promo_card_title": "🎟 Promo Code: {code}", + "admin_promo_card_bonus_days": "🎁 Bonus days: {days}", + "admin_promo_card_activations": "🔢 Activations: {current}/{max}", + "admin_promo_card_validity": "⏰ Valid until: {validity}", + "admin_promo_card_status": "📊 Status: {status}", + "admin_promo_card_created": "📅 Created: {created}", + "admin_promo_card_created_by": "👤 Created by: {creator}", + "admin_promo_status_active": "✅ Active", + "admin_promo_status_inactive": "🚫 Inactive", + "admin_promo_status_expired": "⏰ Expired", + "admin_promo_status_used_up": "🔄 Used up", + "admin_promo_edit_button": "✏️ Edit", + "admin_promo_toggle_status_button": "🔄 On/Off", + "admin_promo_delete_button": "🗑 Delete", + "admin_promo_view_activations_button": "📋 Activations", + "admin_promo_back_to_list_button": "⬅️ Back to list", + + "admin_panel_stats_header": "Panel Statistics", + "admin_panel_online_label": "Online", + "admin_panel_offline_label": "Offline", + "admin_panel_expired_label": "Expired", + "admin_panel_limited_label": "Limited", + "admin_panel_total_users_label": "Total users", + "admin_panel_cpu_usage_label": "CPU Usage", + "admin_panel_memory_usage_label": "RAM Usage", + "admin_panel_traffic_today_label": "Traffic today", + "admin_panel_traffic_week_label": "Traffic this week", + "admin_panel_traffic_month_label": "Traffic this month", + "admin_panel_nodes_label": "Active nodes", + "admin_panel_system_stats_error": "Error getting system statistics", + "admin_panel_bandwidth_stats_error": "Error getting bandwidth statistics", + "admin_panel_nodes_stats_error": "Error getting nodes statistics", + "admin_panel_stats_fetch_error": "Error fetching panel data", + "admin_panel_stats_error_details": "Details", + + "inline_referral_message": "🎁 Get bonuses for friends!\n\nConnect friends to fast VPN and get bonus days for each new user.\n\n💰 Your referral link:\n{referral_link}", + "inline_referral_title": "Referral link", + "inline_referral_description": "Share referral link to get bonuses", + "inline_stats_title": "Bot statistics", + "inline_stats_description": "Total: {total}, Active: {active}", + "inline_financial_title": "Financial statistics", + "inline_financial_description": "Today: {today} RUB", + "inline_system_title": "System statistics", + "inline_system_description": "Online: {online}, Offline: {offline}", + "inline_admin_help_title": "Commands help", + "inline_admin_help_description": "Available commands: ref, stat, financial, system", + "inline_user_help_title": "Commands help", + "inline_user_help_description": "Available command: ref (referral link)", + + "admin_user_stats_total_label": "Total", + "admin_user_stats_paid_subs_label": "With paid subscription", + "admin_user_stats_trial_label": "On trial period", + "admin_user_stats_inactive_label": "Inactive", + "admin_user_stats_banned_label": "Banned", + "admin_user_stats_referral_label": "Attracted via referral program", + "admin_financial_today_label": "Today", + "admin_financial_week_label": "This week", + "admin_financial_month_label": "This month", + "admin_financial_all_time_label": "All time", + "admin_financial_payments_label": "payments" } diff --git a/locales/ru.json b/locales/ru.json index 70e4bc8..86f83ff 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -398,5 +398,72 @@ "admin_user_subscription_active_until": "⏰ Действует до:", "admin_user_subscription_error": "Ошибка загрузки", - "admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод" + "admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод", + + "admin_suspicious_promo_attempt_notification": "⚠️ Подозрительная попытка ввода промокода\n\n👤 Пользователь: @{user_username} (ID: {user_id})\n📝 Имя: {user_first_name}\n💬 Введенный текст: {promo_code_input}", + "admin_suspicious_promo_attempt_notification_no_username": "⚠️ Подозрительная попытка ввода промокода\n\n👤 Пользователь ID: {user_id}\n📝 Имя: {user_first_name}\n💬 Введенный текст: {promo_code_input}", + + "admin_promo_management_button": "🎟 Управление промокодами", + "admin_promo_management_title": "🎟 Управление промокодами\n\nВыберите промокод для детального просмотра:", + "admin_promo_management_empty": "📭 Промокоды отсутствуют", + "admin_promo_card_title": "🎟 Промокод: {code}", + "admin_promo_card_bonus_days": "🎁 Бонусные дни: {days}", + "admin_promo_card_activations": "🔢 Активации: {current}/{max}", + "admin_promo_card_validity": "⏰ Действует до: {validity}", + "admin_promo_card_status": "📊 Статус: {status}", + "admin_promo_card_created": "📅 Создан: {created}", + "admin_promo_card_created_by": "👤 Создал: {creator}", + "admin_promo_status_active": "✅ Активен", + "admin_promo_status_inactive": "🚫 Неактивен", + "admin_promo_status_expired": "⏰ Истек", + "admin_promo_status_used_up": "🔄 Исчерпан", + "admin_promo_edit_button": "✏️ Редактировать", + "admin_promo_toggle_status_button": "🔄 Вкл/Выкл", + "admin_promo_delete_button": "🗑 Удалить", + "admin_promo_view_activations_button": "📋 Активации", + "admin_promo_back_to_list_button": "⬅️ К списку", + + "admin_panel_stats_header": "Статистика панели", + "admin_panel_online_label": "Онлайн", + "admin_panel_offline_label": "Офлайн", + "admin_panel_expired_label": "Истекшие", + "admin_panel_limited_label": "Ограниченные", + "admin_panel_total_users_label": "Всего пользователей", + "admin_panel_cpu_usage_label": "Загрузка CPU", + "admin_panel_memory_usage_label": "Использование RAM", + "admin_panel_traffic_today_label": "Трафик сегодня", + "admin_panel_traffic_week_label": "Трафик за неделю", + "admin_panel_traffic_month_label": "Трафик за месяц", + "admin_panel_nodes_label": "Активных нод", + "admin_panel_system_stats_error": "Ошибка получения системной статистики", + "admin_panel_bandwidth_stats_error": "Ошибка получения статистики трафика", + "admin_panel_nodes_stats_error": "Ошибка получения статистики нод", + "admin_panel_stats_fetch_error": "Ошибка получения данных с панели", + "admin_panel_stats_error_details": "Детали", + + "inline_referral_message": "🎁 Получи бонусы за друзей!\n\nПодключай друзей к быстрому VPN и получай бонусные дни за каждого нового пользователя.\n\n💰 Твоя реферальная ссылка:\n{referral_link}", + "inline_referral_title": "Реферальная ссылка", + "inline_referral_description": "Поделиться реферальной ссылкой для получения бонусов", + "inline_stats_title": "Статистика бота", + "inline_stats_description": "Всего: {total}, Активных: {active}", + "inline_financial_title": "Финансовая статистика", + "inline_financial_description": "Сегодня: {today} RUB", + "inline_system_title": "Системная статистика", + "inline_system_description": "Онлайн: {online}, Офлайн: {offline}", + "inline_admin_help_title": "Помощь по командам", + "inline_admin_help_description": "Доступны команды: реф, стат, финансы, система", + "inline_user_help_title": "Помощь по командам", + "inline_user_help_description": "Доступна команда: реф (реферальная ссылка)", + + "admin_user_stats_total_label": "Всего", + "admin_user_stats_paid_subs_label": "С платной подпиской", + "admin_user_stats_trial_label": "На пробном периоде", + "admin_user_stats_inactive_label": "Неактивных", + "admin_user_stats_banned_label": "Заблокированных", + "admin_user_stats_referral_label": "Привлечено по реферальной программе", + "admin_financial_today_label": "За сегодня", + "admin_financial_week_label": "За неделю", + "admin_financial_month_label": "За месяц", + "admin_financial_all_time_label": "За все время", + "admin_financial_payments_label": "платежей" }