Refactor notification service and enhance user management features

- Replaced legacy payment notification functions with a new NotificationService for improved code organization and maintainability.
- Updated user management handlers to utilize the new notification system for payment confirmations.
- Enhanced user card formatting with localized labels for better clarity and user experience.
- Introduced context management for the PanelApiService to ensure proper session handling.
- Updated localization files to include new labels and messages for user management and notifications.
This commit is contained in:
machka-pasla
2025-08-03 18:58:50 +03:00
parent 973e97c54e
commit aa97618165
12 changed files with 215 additions and 145 deletions
+2 -1
View File
@@ -266,7 +266,8 @@ async def process_promo_set_validity(callback: types.CallbackQuery,
async def process_promo_validity_days_handler(message: types.Message,
state: FSMContext,
i18n_data: dict,
settings: Settings):
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:
+46 -49
View File
@@ -142,56 +142,53 @@ async def show_statistics_handler(callback: types.CallbackQuery,
stats_text_parts.append(f"\n<b>🖥 {_('admin_panel_stats_header', default='Статистика панели')}</b>")
try:
panel_service = PanelApiService(settings)
# Get panel system statistics
logging.info("Fetching panel 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()
logging.info(f"Panel stats response: panel_stats={panel_stats}, nodes_stats={nodes_stats}, online_count={online_count}, users_activity={users_activity}")
# Online users
if online_count is not None:
stats_text_parts.append(f"🟢 Онлайн сейчас: <b>{online_count}</b>")
else:
stats_text_parts.append(f"🟢 Онлайн сейчас: <b>N/A</b>")
# Users activity
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"📅 Подключались сегодня: <b>{today_connected}</b>")
stats_text_parts.append(f"📅 Подключались за неделю: <b>{week_connected}</b>")
stats_text_parts.append(f"❌ Никогда не подключались: <b>{never_connected}</b>")
else:
stats_text_parts.append(f"📅 Активность пользователей: <b>N/A</b>")
# Nodes statistics
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"🔗 Активных нод: <b>{active_nodes}/{total_nodes}</b>")
else:
stats_text_parts.append(f"🔗 Ноды: <b>N/A</b>")
# System statistics
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: <b>{memory_usage}%</b>")
stats_text_parts.append(f"🔄 Загрузка CPU: <b>{cpu_usage}%</b>")
async with PanelApiService(settings) as panel_service:
# Get panel system statistics
logging.info("Fetching panel 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()
logging.info(f"Panel stats response: panel_stats={panel_stats}, nodes_stats={nodes_stats}, online_count={online_count}, users_activity={users_activity}")
# Online users
if online_count is not None:
stats_text_parts.append(f"🟢 Онлайн сейчас: <b>{online_count}</b>")
else:
stats_text_parts.append(f"💾 Системная информация: <b>N/A</b>")
else:
stats_text_parts.append(f"💾 Системная статистика: <b>N/A</b>")
await panel_service.close()
stats_text_parts.append(f"🟢 Онлайн сейчас: <b>N/A</b>")
# Users activity
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"📅 Подключались сегодня: <b>{today_connected}</b>")
stats_text_parts.append(f"📅 Подключались за неделю: <b>{week_connected}</b>")
stats_text_parts.append(f"❌ Никогда не подключались: <b>{never_connected}</b>")
else:
stats_text_parts.append(f"📅 Активность пользователей: <b>N/A</b>")
# Nodes statistics
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"🔗 Активных нод: <b>{active_nodes}/{total_nodes}</b>")
else:
stats_text_parts.append(f"🔗 Ноды: <b>N/A</b>")
# System statistics
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: <b>{memory_usage}%</b>")
stats_text_parts.append(f"🔄 Загрузка CPU: <b>{cpu_usage}%</b>")
else:
stats_text_parts.append(f"💾 Системная информация: <b>N/A</b>")
else:
stats_text_parts.append(f"💾 Системная статистика: <b>N/A</b>")
except Exception as e:
logging.error(f"Failed to fetch panel statistics: {e}", exc_info=True)
+35 -34
View File
@@ -113,27 +113,28 @@ async def format_user_card(user: User, session: AsyncSession,
card_parts.append(f"👤 <b>{_('admin_user_card_title', default='Карточка пользователя')}</b>\n")
# User details
user_name = user.first_name or "N/A"
username_display = f"@{user.username}" if user.username else "N/A"
registration_date = user.registration_date.strftime('%Y-%m-%d %H:%M') if user.registration_date else "N/A"
na_value = _("admin_user_na_value", default="N/A")
user_name = user.first_name or na_value
username_display = f"@{user.username}" if user.username else na_value
registration_date = user.registration_date.strftime('%Y-%m-%d %H:%M') if user.registration_date else na_value
card_parts.append(f"🆔 <b>ID:</b> {hcode(str(user.user_id))}")
card_parts.append(f"👤 <b>Имя:</b> {hcode(user_name)}")
card_parts.append(f"📱 <b>Username:</b> {hcode(username_display)}")
card_parts.append(f"🌍 <b>Язык:</b> {hcode(user.language_code or 'N/A')}")
card_parts.append(f"📅 <b>Регистрация:</b> {hcode(registration_date)}")
card_parts.append(f"{_('admin_user_id_label', default='🆔 <b>ID:</b>')} {hcode(str(user.user_id))}")
card_parts.append(f"{_('admin_user_name_label', default='👤 <b>Имя:</b>')} {hcode(user_name)}")
card_parts.append(f"{_('admin_user_username_label', default='📱 <b>Username:</b>')} {hcode(username_display)}")
card_parts.append(f"{_('admin_user_language_label', default='🌍 <b>Язык:</b>')} {hcode(user.language_code or na_value)}")
card_parts.append(f"{_('admin_user_registration_label', default='📅 <b>Регистрация:</b>')} {hcode(registration_date)}")
# Ban status
ban_status = "🚫 Заблокирован" if user.is_banned else "✅ Активен"
card_parts.append(f"🛡 <b>Статус:</b> {ban_status}")
ban_status = _("admin_user_status_banned", default="🚫 Заблокирован") if user.is_banned else _("admin_user_status_active", default="✅ Активен")
card_parts.append(f"{_('admin_user_status_label', default='🛡 <b>Статус:</b>')} {ban_status}")
# Referral info
if user.referred_by_id:
card_parts.append(f"🎁 <b>Привлечен по реферальной программе от:</b> {hcode(str(user.referred_by_id))}")
card_parts.append(f"{_('admin_user_referral_label', default='🎁 <b>Привлечен по реферальной программе от:</b>')} {hcode(str(user.referred_by_id))}")
# Panel info
if user.panel_user_uuid:
card_parts.append(f"🔗 <b>Panel UUID:</b> {hcode(user.panel_user_uuid[:8] + '...' if len(user.panel_user_uuid) > 8 else user.panel_user_uuid)}")
card_parts.append(f"{_('admin_user_panel_uuid_label', default='🔗 <b>Panel UUID:</b>')} {hcode(user.panel_user_uuid[:8] + '...' if len(user.panel_user_uuid) > 8 else user.panel_user_uuid)}")
card_parts.append("") # Empty line
@@ -146,33 +147,33 @@ async def format_user_card(user: User, session: AsyncSession,
end_date = subscription_details.get('end_date')
if end_date:
end_date_str = end_date.strftime('%Y-%m-%d %H:%M') if isinstance(end_date, datetime) else str(end_date)
card_parts.append(f"⏰ <b>Действует до:</b> {hcode(end_date_str)}")
card_parts.append(f"{_('admin_user_subscription_active_until', default='⏰ <b>Действует до:</b>')} {hcode(end_date_str)}")
status = subscription_details.get('status_from_panel', 'UNKNOWN')
card_parts.append(f"📊 <b>Статус на панели:</b> {hcode(status)}")
card_parts.append(f"{_('admin_user_panel_status_label', default='📊 <b>Статус на панели:</b>')} {hcode(status)}")
traffic_limit = subscription_details.get('traffic_limit_bytes')
traffic_used = subscription_details.get('traffic_used_bytes')
if traffic_limit and traffic_used is not None:
traffic_limit_gb = traffic_limit / (1024**3)
traffic_used_gb = traffic_used / (1024**3)
card_parts.append(f"📊 <b>Трафик:</b> {hcode(f'{traffic_used_gb:.2f}GB / {traffic_limit_gb:.2f}GB')}")
card_parts.append(f"{_('admin_user_traffic_label', default='📊 <b>Трафик:</b>')} {hcode(f'{traffic_used_gb:.2f}GB / {traffic_limit_gb:.2f}GB')}")
else:
card_parts.append(f"💳 <b>Подписка:</b> {hcode('Отсутствует')}")
card_parts.append(f"{_('admin_user_subscription_label', default='💼 <b>Подписка:</b>')} {hcode(_('admin_user_subscription_none', default='Нет активной подписки'))}")
except Exception as e:
logging.error(f"Error getting subscription details for user {user.user_id}: {e}")
card_parts.append(f"💳 <b>Подписка:</b> {hcode('Ошибка загрузки')}")
card_parts.append(f"{_('admin_user_subscription_label', default='💼 <b>Подписка:</b>')} {hcode(_('admin_user_subscription_error', default='Ошибка загрузки'))}")
# Statistics
try:
# Count user logs
logs_count = await message_log_dal.count_user_message_logs(session, user.user_id)
card_parts.append(f"📜 <b>Всего действий:</b> {hcode(str(logs_count))}")
card_parts.append(f"{_('admin_user_actions_count_label', default='📜 <b>Всего действий:</b>')} {hcode(str(logs_count))}")
# Check if user had any subscriptions
had_subscriptions = await subscription_service.has_had_any_subscription(session, user.user_id)
trial_status = "Использовал" if had_subscriptions else "Не использовал"
card_parts.append(f"🆓 <b>Триал:</b> {hcode(trial_status)}")
trial_status = _("admin_user_trial_used", default="Использовал") if had_subscriptions else _("admin_user_trial_not_used", default="Не использовал")
card_parts.append(f"{_('admin_user_trial_label', default='🏡 <b>Триал:</b>')} {hcode(trial_status)}")
except Exception as e:
logging.error(f"Error getting user statistics for {user.user_id}: {e}")
@@ -353,7 +354,7 @@ async def handle_toggle_ban(callback: types.CallbackQuery, user: User,
await session.commit()
status_text = "заблокирован" if new_ban_status else "разблокирован"
status_text = _("admin_user_ban_action_banned", default="заблокирован") if new_ban_status else _("admin_user_ban_action_unbanned", default="разблокирован")
await callback.answer(_(
"admin_user_ban_toggle_success",
default="✅ Пользователь {status}",
@@ -365,9 +366,9 @@ async def handle_toggle_ban(callback: types.CallbackQuery, user: User,
from config.settings import Settings
from bot.services.panel_api_service import PanelApiService
settings = Settings()
panel_service = PanelApiService(settings)
subscription_service = SubscriptionService(settings, panel_service)
await handle_refresh_user_card(callback, user, subscription_service, session, i18n_instance, lang)
async with PanelApiService(settings) as panel_service:
subscription_service = SubscriptionService(settings, panel_service)
await handle_refresh_user_card(callback, user, subscription_service, session, i18n_instance, lang)
except Exception as e:
logging.error(f"Error toggling ban for user {user.user_id}: {e}")
@@ -631,16 +632,16 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
# Show user card again
from bot.services.panel_api_service import PanelApiService
panel_service = PanelApiService(settings)
subscription_service = SubscriptionService(settings, panel_service)
user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang)
keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang)
await message.answer(
user_card_text,
reply_markup=keyboard.as_markup(),
parse_mode="HTML"
)
async with PanelApiService(settings) as panel_service:
subscription_service = SubscriptionService(settings, panel_service)
user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang)
keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang)
await message.answer(
user_card_text,
reply_markup=keyboard.as_markup(),
parse_mode="HTML"
)
except Exception as e:
logging.error(f"Error sending direct message to user {target_user_id}: {e}")
+1 -11
View File
@@ -20,7 +20,7 @@ from bot.services.panel_api_service import PanelApiService
from bot.services.yookassa_service import YooKassaService
from bot.middlewares.i18n import JsonI18n
from config.settings import Settings
from bot.services.notification_service import notify_admin_new_payment, NotificationService
from bot.services.notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
payment_processing_lock = asyncio.Lock()
@@ -209,16 +209,6 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
)
except Exception as e:
logging.error(f"Failed to send payment notification: {e}")
# Legacy notification for backwards compatibility
await notify_admin_new_payment(
bot,
settings,
i18n,
user_id,
subscription_months,
payment_value,
)
except Exception as e_process:
logging.error(
-1
View File
@@ -237,7 +237,6 @@ async def start_command_handler(message: types.Message,
config_link = active.get("config_link") if active else None
config_link = config_link or _("config_link_not_available")
from datetime import datetime
new_end_date = result if isinstance(result, datetime) else None
promo_success_text = _(
+15 -10
View File
@@ -14,7 +14,7 @@ from bot.middlewares.i18n import JsonI18n
from bot.services.subscription_service import SubscriptionService
from bot.services.referral_service import ReferralService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from bot.services.notification_service import notify_admin_new_payment
from bot.services.notification_service import NotificationService
from db.dal import payment_dal, user_dal
@@ -194,15 +194,20 @@ class CryptoPayService:
except Exception as e:
logging.error(f"Failed to send CryptoPay success message: {e}")
await notify_admin_new_payment(
bot,
settings,
i18n,
user_id,
months,
float(invoice.amount),
currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL,
)
# Send notification about payment
try:
notification_service = NotificationService(bot, settings, i18n)
user = await user_dal.get_user_by_id(session, user_id)
await notification_service.notify_payment_received(
user_id=user_id,
amount=float(invoice.amount),
currency=invoice.asset or settings.DEFAULT_CURRENCY_SYMBOL,
months=months,
payment_provider="crypto_pay",
username=user.username if user else None
)
except Exception as e:
logging.error(f"Failed to send crypto_pay payment notification: {e}")
async def webhook_route(self, request: web.Request) -> web.Response:
if not self.configured or not self.client:
+1 -14
View File
@@ -220,20 +220,7 @@ async def notify_admin_new_trial(bot: Bot, settings: Settings, i18n: JsonI18n,
await notification_service.notify_trial_activation(user_id, end_date)
async def notify_admin_new_payment(bot: Bot, settings: Settings, i18n: JsonI18n,
user_id: int, months: int, amount: float,
currency: str | None = None) -> None:
currency_symbol = currency or settings.DEFAULT_CURRENCY_SYMBOL
await notify_admins(
bot,
settings,
i18n,
"admin_new_payment_notification",
user_id=user_id,
months=months,
amount=f"{amount:.2f}",
currency=currency_symbol,
)
async def notify_admin_promo_activation(bot: Bot, settings: Settings,
+9 -1
View File
@@ -21,6 +21,14 @@ class PanelApiService:
self.api_key = settings.PANEL_API_KEY
self._session: Optional[aiohttp.ClientSession] = None
self.default_client_ip = "127.0.0.1"
async def __aenter__(self):
"""Context manager entry"""
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit - automatically close session"""
await self.close_session()
async def _get_session(self) -> aiohttp.ClientSession:
if self._session is None or self._session.closed:
@@ -32,7 +40,7 @@ class PanelApiService:
if self._session and not self._session.closed:
await self._session.close()
self._session = None
logging.info("Panel API service HTTP session closed.")
logging.debug("Panel API service HTTP session closed.")
async def close(self):
"""Alias for close_session for API consistency."""
+15 -10
View File
@@ -10,7 +10,7 @@ from db.dal import payment_dal, user_dal
from .subscription_service import SubscriptionService
from .referral_service import ReferralService
from bot.middlewares.i18n import JsonI18n
from .notification_service import notify_admin_new_payment
from .notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
@@ -153,13 +153,18 @@ class StarsService:
logging.error(
f"Failed to send stars payment success message: {e_send}")
await notify_admin_new_payment(
self.bot,
self.settings,
self.i18n,
message.from_user.id,
months,
float(stars_amount),
currency="XTR",
)
# Send notification about payment
try:
notification_service = NotificationService(self.bot, self.settings, self.i18n)
user = await user_dal.get_user_by_id(session, message.from_user.id)
await notification_service.notify_payment_received(
user_id=message.from_user.id,
amount=float(stars_amount),
currency="XTR",
months=months,
payment_provider="stars",
username=user.username if user else None
)
except Exception as e:
logging.error(f"Failed to send stars payment notification: {e}")
+15 -10
View File
@@ -13,7 +13,7 @@ from bot.middlewares.i18n import JsonI18n
from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService
from bot.services.referral_service import ReferralService
from .notification_service import notify_admin_new_payment
from .notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import get_connect_and_main_keyboard
from db.dal import payment_dal, user_dal, subscription_dal
@@ -187,15 +187,20 @@ class TributeService:
logging.error(
f"Failed to send Tribute payment success message to user {user_id}: {e}")
await notify_admin_new_payment(
bot,
settings,
i18n,
user_id,
months,
float(price_rub),
currency="RUB",
)
# Send notification about payment
try:
notification_service = NotificationService(bot, settings, i18n)
user = await user_dal.get_user_by_id(session, user_id)
await notification_service.notify_payment_received(
user_id=user_id,
amount=float(price_rub),
currency="RUB",
months=months,
payment_provider="tribute",
username=user.username if user else None
)
except Exception as e:
logging.error(f"Failed to send tribute payment notification: {e}")
else:
await session.commit()
return web.Response(status=200, text="ok")
+38 -2
View File
@@ -234,7 +234,7 @@
"subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.",
"admin_new_trial_notification": "\ud83c\udf21 User {user_id} activated a free trial until {end_date}.",
"admin_new_payment_notification": "\ud83d\udcb3 Payment received from user {user_id}: {months} mo. for {amount} {currency}.",
"admin_promo_activation_notification": "\ud83c\udf81 Promo code {code} activated by user {user_id} (+{bonus_days}d).",
"error_unknown": "An unknown error occurred.",
@@ -362,5 +362,41 @@
"admin_promo_creation_failed": "❌ Error creating promo code: {error}",
"admin_panel_back_button": "⬅️ Back",
"admin_bulk_promo_creation_failed": "❌ Error creating promo codes: {error}"
"admin_bulk_promo_creation_failed": "❌ Error creating promo codes: {error}",
"admin_user_id_label": "🆔 <b>ID:</b>",
"admin_user_name_label": "👤 <b>Name:</b>",
"admin_user_username_label": "📱 <b>Username:</b>",
"admin_user_language_label": "🌍 <b>Language:</b>",
"admin_user_registration_label": "📅 <b>Registration:</b>",
"admin_user_status_label": "🛡 <b>Status:</b>",
"admin_user_referral_label": "🎁 <b>Referred by:</b>",
"admin_user_panel_uuid_label": "🔗 <b>Panel UUID:</b>",
"admin_user_panel_status_label": "📊 <b>Panel Status:</b>",
"admin_user_traffic_label": "📊 <b>Traffic:</b>",
"admin_user_subscription_label": "💼 <b>Subscription:</b>",
"admin_user_trial_label": "🏡 <b>Trial:</b>",
"admin_user_status_banned": "🚫 Banned",
"admin_user_status_active": "✅ Active",
"admin_user_trial_used": "Used",
"admin_user_trial_not_used": "Not used",
"admin_user_ban_action_banned": "banned",
"admin_user_ban_action_unbanned": "unbanned",
"admin_user_na_value": "N/A",
"admin_user_subscription_active": "Active until {end_date}",
"admin_user_subscription_expired": "Expired {end_date}",
"admin_user_subscription_none": "No active subscription",
"admin_user_logs_title": "📋 <b>User logs {user_id}</b>",
"admin_user_logs_empty": "No logs available",
"admin_user_logs_count": "Total entries: {count}",
"admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}",
"admin_user_actions_count_label": "📜 <b>Total actions:</b>",
"admin_user_subscription_active_until": "⏰ <b>Active until:</b>",
"admin_user_subscription_error": "Loading error",
"admin_bulk_promo_unique_generation_failed": "Failed to create unique promo code"
}
+38 -2
View File
@@ -234,7 +234,7 @@
"subscription_expired_yesterday_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекла сутки назад ({end_date}).\n\nПродлите её по кнопке ниже.",
"admin_new_trial_notification": "\ud83c\udf21 Пользователь {user_id} активировал пробный период до {end_date}.",
"admin_new_payment_notification": "\ud83d\udcb3 Получен платеж от пользователя {user_id}: {months} мес. за {amount} {currency}.",
"admin_promo_activation_notification": "\ud83c\udf81 Пользователь {user_id} активировал промокод {code} (+{bonus_days} дн.)",
"error_unknown": "Произошла неизвестная ошибка.",
@@ -362,5 +362,41 @@
"admin_promo_creation_failed": "❌ Ошибка создания промокода: {error}",
"admin_panel_back_button": "⬅️ Назад",
"admin_bulk_promo_creation_failed": "❌ Ошибка создания промокодов: {error}"
"admin_bulk_promo_creation_failed": "❌ Ошибка создания промокодов: {error}",
"admin_user_id_label": "🆔 <b>ID:</b>",
"admin_user_name_label": "👤 <b>Имя:</b>",
"admin_user_username_label": "📱 <b>Username:</b>",
"admin_user_language_label": "🌍 <b>Язык:</b>",
"admin_user_registration_label": "📅 <b>Регистрация:</b>",
"admin_user_status_label": "🛡 <b>Статус:</b>",
"admin_user_referral_label": "🎁 <b>Привлечен по реферальной программе от:</b>",
"admin_user_panel_uuid_label": "🔗 <b>Panel UUID:</b>",
"admin_user_panel_status_label": "📊 <b>Статус на панели:</b>",
"admin_user_traffic_label": "📊 <b>Трафик:</b>",
"admin_user_subscription_label": "💼 <b>Подписка:</b>",
"admin_user_trial_label": "🏡 <b>Триал:</b>",
"admin_user_status_banned": "🚫 Заблокирован",
"admin_user_status_active": "✅ Активен",
"admin_user_trial_used": "Использовал",
"admin_user_trial_not_used": "Не использовал",
"admin_user_ban_action_banned": "заблокирован",
"admin_user_ban_action_unbanned": "разблокирован",
"admin_user_na_value": "N/A",
"admin_user_subscription_active": "Активна до {end_date}",
"admin_user_subscription_expired": "Истекла {end_date}",
"admin_user_subscription_none": "Нет активной подписки",
"admin_user_logs_title": "📋 <b>Логи пользователя {user_id}</b>",
"admin_user_logs_empty": "Логи отсутствуют",
"admin_user_logs_count": "Всего записей: {count}",
"admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}",
"admin_user_actions_count_label": "📜 <b>Всего действий:</b>",
"admin_user_subscription_active_until": "⏰ <b>Действует до:</b>",
"admin_user_subscription_error": "Ошибка загрузки",
"admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод"
}