Enhance promo code management and user statistics features
- Introduced a unified promo code management system, allowing admins to view and manage both active and inactive promo codes with detailed information. - Updated the promo code detail view to include status indicators and management options for editing, toggling status, and viewing activation history. - Enhanced user statistics display by restructuring the information layout and adding new labels for clarity. - Improved localization files to support new messages and labels related to promo code management and user statistics. - Implemented error handling for suspicious promo code attempts, integrating a notification system for better admin awareness.
This commit is contained in:
@@ -465,10 +465,12 @@ async def view_promo_codes_handler(callback: types.CallbackQuery,
|
|||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data == "admin_action:manage_promos")
|
# New unified promo management system
|
||||||
async def manage_promo_codes_handler(callback: types.CallbackQuery,
|
@router.callback_query(F.data == "admin_action:promo_management")
|
||||||
i18n_data: dict, settings: Settings,
|
async def promo_management_handler(callback: types.CallbackQuery,
|
||||||
session: AsyncSession):
|
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)
|
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
|
||||||
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
|
||||||
if not i18n or not callback.message:
|
if not i18n or not callback.message:
|
||||||
@@ -476,36 +478,168 @@ async def manage_promo_codes_handler(callback: types.CallbackQuery,
|
|||||||
return
|
return
|
||||||
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
|
||||||
|
|
||||||
promo_models = await promo_code_dal.get_all_active_promo_codes(session,
|
# Get ALL promo codes (including inactive)
|
||||||
limit=20,
|
promo_models = await promo_code_dal.get_all_promo_codes_with_details(session, limit=50, offset=0)
|
||||||
offset=0)
|
|
||||||
if not promo_models:
|
if not promo_models:
|
||||||
await callback.message.edit_text(
|
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))
|
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n))
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
return
|
return
|
||||||
|
|
||||||
kb = InlineKeyboardBuilder()
|
kb = InlineKeyboardBuilder()
|
||||||
for promo in promo_models:
|
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(
|
kb.row(
|
||||||
InlineKeyboardButton(
|
InlineKeyboardButton(
|
||||||
text=promo.code,
|
text=button_text,
|
||||||
callback_data=f"promo_edit:{promo.promo_code_id}"),
|
callback_data=f"promo_detail:{promo.promo_code_id}")
|
||||||
InlineKeyboardButton(
|
|
||||||
text=_("admin_promo_delete_button"),
|
|
||||||
callback_data=f"promo_delete:{promo.promo_code_id}"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
kb.row(
|
kb.row(
|
||||||
InlineKeyboardButton(text=_("back_to_admin_panel_button"),
|
InlineKeyboardButton(text=_("back_to_admin_panel_button", default="⬅️ Назад"),
|
||||||
callback_data="admin_action:main"))
|
callback_data="admin_action:main"))
|
||||||
|
|
||||||
await callback.message.edit_text(
|
await callback.message.edit_text(
|
||||||
_("admin_manage_promos_title"),
|
_("admin_promo_management_title", default="🎟 <b>Управление промокодами</b>\n\nВыберите промокод для детального просмотра:"),
|
||||||
reply_markup=kb.as_markup())
|
reply_markup=kb.as_markup(),
|
||||||
|
parse_mode="HTML")
|
||||||
await callback.answer()
|
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="🎟 <b>Промокод: {code}</b>",
|
||||||
|
code=promo.code
|
||||||
|
) + "\n\n"
|
||||||
|
|
||||||
|
card_text += _(
|
||||||
|
"admin_promo_card_bonus_days",
|
||||||
|
default="🎁 Бонусные дни: <b>{days}</b>",
|
||||||
|
days=promo.bonus_days
|
||||||
|
) + "\n"
|
||||||
|
|
||||||
|
card_text += _(
|
||||||
|
"admin_promo_card_activations",
|
||||||
|
default="🔢 Активации: <b>{current}/{max}</b>",
|
||||||
|
current=promo.current_activations,
|
||||||
|
max=promo.max_activations
|
||||||
|
) + "\n"
|
||||||
|
|
||||||
|
card_text += _(
|
||||||
|
"admin_promo_card_validity",
|
||||||
|
default="⏰ Действует до: <b>{validity}</b>",
|
||||||
|
validity=validity
|
||||||
|
) + "\n"
|
||||||
|
|
||||||
|
card_text += _(
|
||||||
|
"admin_promo_card_status",
|
||||||
|
default="📊 Статус: <b>{status}</b>",
|
||||||
|
status=status
|
||||||
|
) + "\n"
|
||||||
|
|
||||||
|
card_text += _(
|
||||||
|
"admin_promo_card_created",
|
||||||
|
default="📅 Создан: <b>{created}</b>",
|
||||||
|
created=created_date
|
||||||
|
) + "\n"
|
||||||
|
|
||||||
|
card_text += _(
|
||||||
|
"admin_promo_card_created_by",
|
||||||
|
default="👤 Создал: <b>{creator}</b>",
|
||||||
|
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:"))
|
@router.callback_query(F.data.startswith("promo_edit:"))
|
||||||
async def promo_edit_select_handler(callback: types.CallbackQuery, state: FSMContext,
|
async def promo_edit_select_handler(callback: types.CallbackQuery, state: FSMContext,
|
||||||
i18n_data: dict, settings: Settings,
|
i18n_data: dict, settings: Settings,
|
||||||
|
|||||||
@@ -37,27 +37,97 @@ async def show_statistics_handler(callback: types.CallbackQuery,
|
|||||||
f"\n<b>👥 {_('admin_enhanced_users_stats_header', default='Пользователи')}</b>"
|
f"\n<b>👥 {_('admin_enhanced_users_stats_header', default='Пользователи')}</b>"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"📊 Всего: <b>{user_stats['total_users']}</b>"
|
f"📊 {_('admin_user_stats_total_label', default='Всего')}: <b>{user_stats['total_users']}</b>"
|
||||||
|
)
|
||||||
|
# Removed: Active today moved to panel stats
|
||||||
|
stats_text_parts.append(
|
||||||
|
f"💳 {_('admin_user_stats_paid_subs_label', default='С платной подпиской')}: <b>{user_stats['paid_subscriptions']}</b>"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"📈 Активных сегодня: <b>{user_stats['active_today']}</b>"
|
f"🆓 {_('admin_user_stats_trial_label', default='На пробном периоде')}: <b>{user_stats['trial_users']}</b>"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"💳 С платной подпиской: <b>{user_stats['paid_subscriptions']}</b>"
|
f"😴 {_('admin_user_stats_inactive_label', default='Неактивных')}: <b>{user_stats['inactive_users']}</b>"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"🆓 На пробном периоде: <b>{user_stats['trial_users']}</b>"
|
f"🚫 {_('admin_user_stats_banned_label', default='Заблокированных')}: <b>{user_stats['banned_users']}</b>"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"😴 Неактивных: <b>{user_stats['inactive_users']}</b>"
|
f"🎁 {_('admin_user_stats_referral_label', default='Привлечено по реферальной программе')}: <b>{user_stats['referral_users']}</b>"
|
||||||
)
|
|
||||||
stats_text_parts.append(
|
|
||||||
f"🚫 Заблокированных: <b>{user_stats['banned_users']}</b>"
|
|
||||||
)
|
|
||||||
stats_text_parts.append(
|
|
||||||
f"🎁 Привлечено по реферальной программе: <b>{user_stats['referral_users']}</b>"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Panel Statistics - moved above financial
|
||||||
|
stats_text_parts.append(f"\n<b>🖥 {_('admin_panel_stats_header', default='Статистика панели')}</b>")
|
||||||
|
|
||||||
|
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='Онлайн')}: <b>{active_users}</b>")
|
||||||
|
stats_text_parts.append(f"🔴 {_('admin_panel_offline_label', default='Офлайн')}: <b>{disabled_users}</b>")
|
||||||
|
stats_text_parts.append(f"⏰ {_('admin_panel_expired_label', default='Истекшие')}: <b>{expired_users}</b>")
|
||||||
|
stats_text_parts.append(f"⚠️ {_('admin_panel_limited_label', default='Ограниченные')}: <b>{limited_users}</b>")
|
||||||
|
stats_text_parts.append(f"👥 {_('admin_panel_total_users_label', default='Всего пользователей')}: <b>{total_users}</b>")
|
||||||
|
|
||||||
|
# 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')}: <b>{cpu_usage:.1f}%</b>")
|
||||||
|
if memory:
|
||||||
|
memory_usage = memory.get('usage', 0)
|
||||||
|
stats_text_parts.append(f"💾 {_('admin_panel_memory_usage_label', default='Использование RAM')}: <b>{memory_usage:.1f}%</b>")
|
||||||
|
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='Трафик сегодня')}: <b>{today_total}</b>")
|
||||||
|
|
||||||
|
if week_traffic:
|
||||||
|
week_total = week_traffic.get('total', '0 B')
|
||||||
|
stats_text_parts.append(f"📊 {_('admin_panel_traffic_week_label', default='Трафик за неделю')}: <b>{week_total}</b>")
|
||||||
|
|
||||||
|
if month_traffic:
|
||||||
|
month_total = month_traffic.get('total', '0 B')
|
||||||
|
stats_text_parts.append(f"📊 {_('admin_panel_traffic_month_label', default='Трафик за месяц')}: <b>{month_total}</b>")
|
||||||
|
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='Активных нод')}: <b>{active_nodes_count}/{total_nodes_count}</b>")
|
||||||
|
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 statistics
|
||||||
financial_stats = await payment_dal.get_financial_statistics(session)
|
financial_stats = await payment_dal.get_financial_statistics(session)
|
||||||
|
|
||||||
@@ -65,16 +135,16 @@ async def show_statistics_handler(callback: types.CallbackQuery,
|
|||||||
f"\n<b>💰 {_('admin_financial_stats_header', default='Финансовая статистика')}</b>"
|
f"\n<b>💰 {_('admin_financial_stats_header', default='Финансовая статистика')}</b>"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"📅 За сегодня: <b>{financial_stats['today_revenue']:.2f} RUB</b> ({financial_stats['today_payments_count']} платежей)"
|
f"📅 {_('admin_financial_today_label', default='За сегодня')}: <b>{financial_stats['today_revenue']:.2f} RUB</b> ({financial_stats['today_payments_count']} {_('admin_financial_payments_label', default='платежей')})"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"📅 За неделю: <b>{financial_stats['week_revenue']:.2f} RUB</b>"
|
f"📅 {_('admin_financial_week_label', default='За неделю')}: <b>{financial_stats['week_revenue']:.2f} RUB</b>"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"📅 За месяц: <b>{financial_stats['month_revenue']:.2f} RUB</b>"
|
f"📅 {_('admin_financial_month_label', default='За месяц')}: <b>{financial_stats['month_revenue']:.2f} RUB</b>"
|
||||||
)
|
)
|
||||||
stats_text_parts.append(
|
stats_text_parts.append(
|
||||||
f"🏆 За все время: <b>{financial_stats['all_time_revenue']:.2f} RUB</b>"
|
f"🏆 {_('admin_financial_all_time_label', default='За все время')}: <b>{financial_stats['all_time_revenue']:.2f} RUB</b>"
|
||||||
)
|
)
|
||||||
|
|
||||||
last_payments_models: List[
|
last_payments_models: List[
|
||||||
|
|||||||
+14
-10
@@ -92,8 +92,7 @@ async def create_referral_result(inline_query: InlineQuery, bot: Bot,
|
|||||||
"✨ Быстрый и надежный\n"
|
"✨ Быстрый и надежный\n"
|
||||||
"🔒 Полная анонимность\n"
|
"🔒 Полная анонимность\n"
|
||||||
"🌍 Серверы по всему миру\n"
|
"🌍 Серверы по всему миру\n"
|
||||||
"💎 Бесплатный пробный период\n\n"
|
"💎 Бесплатный пробный период\n\n{referral_link}",
|
||||||
"Переходи по ссылке: {referral_link}",
|
|
||||||
referral_link=referral_link
|
referral_link=referral_link
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -180,8 +179,10 @@ async def create_user_stats_result(session: AsyncSession, i18n_instance, lang: s
|
|||||||
default="👥 Статистика пользователей"
|
default="👥 Статистика пользователей"
|
||||||
),
|
),
|
||||||
description=_(
|
description=_(
|
||||||
"inline_admin_user_stats_desc",
|
"inline_stats_description",
|
||||||
default=f"Всего: {user_stats['total_users']}, Активных: {user_stats['paid_subscriptions']}"
|
default="Всего: {total}, Активных: {active}",
|
||||||
|
total=user_stats['total_users'],
|
||||||
|
active=user_stats['paid_subscriptions']
|
||||||
),
|
),
|
||||||
input_message_content=InputTextMessageContent(
|
input_message_content=InputTextMessageContent(
|
||||||
message_text=stats_text,
|
message_text=stats_text,
|
||||||
@@ -225,8 +226,9 @@ async def create_financial_stats_result(session: AsyncSession, i18n_instance, la
|
|||||||
default="💰 Финансовая статистика"
|
default="💰 Финансовая статистика"
|
||||||
),
|
),
|
||||||
description=_(
|
description=_(
|
||||||
"inline_admin_financial_stats_desc",
|
"inline_financial_description",
|
||||||
default=f"Сегодня: {financial_stats['today_revenue']:.2f} RUB"
|
default="Сегодня: {today} RUB",
|
||||||
|
today=f"{financial_stats['today_revenue']:.2f}"
|
||||||
),
|
),
|
||||||
input_message_content=InputTextMessageContent(
|
input_message_content=InputTextMessageContent(
|
||||||
message_text=stats_text,
|
message_text=stats_text,
|
||||||
@@ -296,8 +298,10 @@ async def create_system_stats_result(session: AsyncSession, i18n_instance, lang:
|
|||||||
default="🖥 Системная статистика"
|
default="🖥 Системная статистика"
|
||||||
),
|
),
|
||||||
description=_(
|
description=_(
|
||||||
"inline_admin_system_stats_desc",
|
"inline_system_description",
|
||||||
default=f"Онлайн: {active_subs}, Офлайн: {max(0, offline_users)}"
|
default="Онлайн: {online}, Офлайн: {offline}",
|
||||||
|
online=active_subs,
|
||||||
|
offline=max(0, offline_users)
|
||||||
),
|
),
|
||||||
input_message_content=InputTextMessageContent(
|
input_message_content=InputTextMessageContent(
|
||||||
message_text=stats_text,
|
message_text=stats_text,
|
||||||
@@ -327,7 +331,7 @@ async def create_help_result(i18n_instance, lang: str, is_admin: bool) -> Inline
|
|||||||
"💡 Просто напишите @{bot_username} и начните вводить команду в любом чате!"
|
"💡 Просто напишите @{bot_username} и начните вводить команду в любом чате!"
|
||||||
)
|
)
|
||||||
title = _("inline_admin_help_title", default="🤖 Inline помощь (Админ)")
|
title = _("inline_admin_help_title", default="🤖 Inline помощь (Админ)")
|
||||||
description = _("inline_admin_help_desc", default="Доступны команды: реф, стат, финансы, система")
|
description = _("inline_admin_help_description", default="Доступны команды: реф, стат, финансы, система")
|
||||||
else:
|
else:
|
||||||
help_text = _(
|
help_text = _(
|
||||||
"inline_user_help_message",
|
"inline_user_help_message",
|
||||||
@@ -337,7 +341,7 @@ async def create_help_result(i18n_instance, lang: str, is_admin: bool) -> Inline
|
|||||||
"💡 Просто напишите @{bot_username} и начните вводить 'реф' в любом чате!"
|
"💡 Просто напишите @{bot_username} и начните вводить 'реф' в любом чате!"
|
||||||
)
|
)
|
||||||
title = _("inline_user_help_title", default="🤖 Inline помощь")
|
title = _("inline_user_help_title", default="🤖 Inline помощь")
|
||||||
description = _("inline_user_help_desc", default="Доступна команда: реф (реферальная ссылка)")
|
description = _("inline_user_help_description", default="Доступна команда: реф (реферальная ссылка)")
|
||||||
|
|
||||||
return InlineQueryResultArticle(
|
return InlineQueryResultArticle(
|
||||||
id="help",
|
id="help",
|
||||||
|
|||||||
@@ -105,30 +105,23 @@ async def process_promo_code_input(message: types.Message, state: FSMContext,
|
|||||||
|
|
||||||
response_to_user_text = ""
|
response_to_user_text = ""
|
||||||
if is_suspicious:
|
if is_suspicious:
|
||||||
|
# Send notification through NotificationService if enabled
|
||||||
if settings.ADMIN_IDS:
|
if settings.LOG_SUSPICIOUS_ACTIVITY:
|
||||||
admin_notify_key = "admin_suspicious_promo_attempt_notification" if user.username else "admin_suspicious_promo_attempt_notification_no_username"
|
try:
|
||||||
|
from bot.services.notification_service import NotificationService
|
||||||
admin_lang = settings.DEFAULT_LANGUAGE
|
notification_service = NotificationService(bot, settings, i18n)
|
||||||
_admin = lambda k, **kw: i18n.gettext(admin_lang, k, **kw)
|
await notification_service.notify_suspicious_promo_attempt(
|
||||||
admin_notification_text = _admin(
|
user_id=user.id,
|
||||||
admin_notify_key,
|
username=user.username,
|
||||||
user_id=user.id,
|
first_name=user.first_name,
|
||||||
user_username=user.username or "N/A",
|
suspicious_input=code_input
|
||||||
user_first_name=user.first_name or "N/A",
|
)
|
||||||
promo_code_input=hcode(code_input))
|
except Exception as e:
|
||||||
for admin_id in settings.ADMIN_IDS:
|
logging.error(f"Failed to send suspicious promo notification: {e}")
|
||||||
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}"
|
|
||||||
)
|
|
||||||
|
|
||||||
response_to_user_text = _("promo_code_not_found",
|
response_to_user_text = _("promo_code_not_found",
|
||||||
code=hcode(code_input.upper()))
|
code=hcode(code_input.upper()))
|
||||||
|
reply_markup = get_back_to_main_menu_markup(current_lang, i18n)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
success, result = await promo_code_service.apply_promo_code(
|
success, result = await promo_code_service.apply_promo_code(
|
||||||
|
|||||||
@@ -88,14 +88,12 @@ def get_promo_marketing_keyboard(i18n_instance, lang: str) -> InlineKeyboardMark
|
|||||||
callback_data="admin_action:create_promo")
|
callback_data="admin_action:create_promo")
|
||||||
builder.button(text=_(key="admin_create_bulk_promo_button"),
|
builder.button(text=_(key="admin_create_bulk_promo_button"),
|
||||||
callback_data="admin_action:create_bulk_promo")
|
callback_data="admin_action:create_bulk_promo")
|
||||||
builder.button(text=_(key="admin_manage_promos_button"),
|
builder.button(text=_(key="admin_promo_management_button"),
|
||||||
callback_data="admin_action:manage_promos")
|
callback_data="admin_action:promo_management")
|
||||||
builder.button(text=_(key="admin_view_promos_button"),
|
|
||||||
callback_data="admin_action:view_promos")
|
|
||||||
|
|
||||||
builder.button(text=_(key="back_to_admin_panel_button"),
|
builder.button(text=_(key="back_to_admin_panel_button"),
|
||||||
callback_data="admin_action:main")
|
callback_data="admin_action:main")
|
||||||
builder.adjust(2, 2, 1)
|
builder.adjust(2, 1, 1)
|
||||||
return builder.as_markup()
|
return builder.as_markup()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -470,41 +470,23 @@ class PanelApiService:
|
|||||||
return await panel_sync_dal.get_panel_sync_status(session)
|
return await panel_sync_dal.get_panel_sync_status(session)
|
||||||
|
|
||||||
|
|
||||||
async def get_panel_statistics(self) -> Optional[Dict[str, Any]]:
|
async def get_system_stats(self) -> Optional[Dict[str, Any]]:
|
||||||
"""Get general panel statistics"""
|
"""Get system statistics (CPU, memory, users counts)"""
|
||||||
response_data = await self._request("GET", "/admin/stats", log_full_response=False)
|
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:
|
if response_data and not response_data.get("error") and "response" in response_data:
|
||||||
return response_data.get("response")
|
return response_data.get("response")
|
||||||
return None
|
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"""
|
"""Get nodes statistics"""
|
||||||
response_data = await self._request("GET", "/admin/nodes/stats", 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", {}).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:
|
if response_data and not response_data.get("error") and "response" in response_data:
|
||||||
return response_data.get("response")
|
return response_data.get("response")
|
||||||
return None
|
return None
|
||||||
@@ -11,7 +11,7 @@ from db.models import PromoCode, User
|
|||||||
|
|
||||||
from .subscription_service import SubscriptionService
|
from .subscription_service import SubscriptionService
|
||||||
from bot.middlewares.i18n import JsonI18n
|
from bot.middlewares.i18n import JsonI18n
|
||||||
from .notification_service import notify_admin_promo_activation, NotificationService
|
from .notification_service import NotificationService
|
||||||
|
|
||||||
|
|
||||||
class PromoCodeService:
|
class PromoCodeService:
|
||||||
@@ -75,15 +75,6 @@ class PromoCodeService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to send promo activation notification: {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
|
return True, new_end_date
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
|||||||
@@ -306,6 +306,7 @@ class Settings(BaseSettings):
|
|||||||
LOG_PAYMENTS: bool = Field(default=True, description="Send notifications for successful payments")
|
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_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_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',
|
model_config = SettingsConfigDict(env_file='.env',
|
||||||
env_file_encoding='utf-8',
|
env_file_encoding='utf-8',
|
||||||
|
|||||||
@@ -56,6 +56,33 @@ async def get_all_active_promo_codes(session: AsyncSession,
|
|||||||
return result.scalars().all()
|
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,
|
async def update_promo_code(session: AsyncSession, promo_id: int,
|
||||||
update_data: Dict[str, Any]) -> Optional[PromoCode]:
|
update_data: Dict[str, Any]) -> Optional[PromoCode]:
|
||||||
promo = await get_promo_code_by_id(session, promo_id)
|
promo = await get_promo_code_by_id(session, promo_id)
|
||||||
|
|||||||
+68
-1
@@ -398,5 +398,72 @@
|
|||||||
"admin_user_subscription_active_until": "⏰ <b>Active until:</b>",
|
"admin_user_subscription_active_until": "⏰ <b>Active until:</b>",
|
||||||
"admin_user_subscription_error": "Loading error",
|
"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": "⚠️ <b>Suspicious promo code attempt</b>\n\n👤 User: <b>@{user_username}</b> (ID: {user_id})\n📝 Name: <b>{user_first_name}</b>\n💬 Input text: <b>{promo_code_input}</b>",
|
||||||
|
"admin_suspicious_promo_attempt_notification_no_username": "⚠️ <b>Suspicious promo code attempt</b>\n\n👤 User ID: <b>{user_id}</b>\n📝 Name: <b>{user_first_name}</b>\n💬 Input text: <b>{promo_code_input}</b>",
|
||||||
|
|
||||||
|
"admin_promo_management_button": "🎟 Promo Management",
|
||||||
|
"admin_promo_management_title": "🎟 <b>Promo Code Management</b>\n\nSelect a promo code for detailed view:",
|
||||||
|
"admin_promo_management_empty": "📭 No promo codes available",
|
||||||
|
"admin_promo_card_title": "🎟 <b>Promo Code: {code}</b>",
|
||||||
|
"admin_promo_card_bonus_days": "🎁 Bonus days: <b>{days}</b>",
|
||||||
|
"admin_promo_card_activations": "🔢 Activations: <b>{current}/{max}</b>",
|
||||||
|
"admin_promo_card_validity": "⏰ Valid until: <b>{validity}</b>",
|
||||||
|
"admin_promo_card_status": "📊 Status: <b>{status}</b>",
|
||||||
|
"admin_promo_card_created": "📅 Created: <b>{created}</b>",
|
||||||
|
"admin_promo_card_created_by": "👤 Created by: <b>{creator}</b>",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-1
@@ -398,5 +398,72 @@
|
|||||||
"admin_user_subscription_active_until": "⏰ <b>Действует до:</b>",
|
"admin_user_subscription_active_until": "⏰ <b>Действует до:</b>",
|
||||||
"admin_user_subscription_error": "Ошибка загрузки",
|
"admin_user_subscription_error": "Ошибка загрузки",
|
||||||
|
|
||||||
"admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод"
|
"admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод",
|
||||||
|
|
||||||
|
"admin_suspicious_promo_attempt_notification": "⚠️ <b>Подозрительная попытка ввода промокода</b>\n\n👤 Пользователь: <b>@{user_username}</b> (ID: {user_id})\n📝 Имя: <b>{user_first_name}</b>\n💬 Введенный текст: <b>{promo_code_input}</b>",
|
||||||
|
"admin_suspicious_promo_attempt_notification_no_username": "⚠️ <b>Подозрительная попытка ввода промокода</b>\n\n👤 Пользователь ID: <b>{user_id}</b>\n📝 Имя: <b>{user_first_name}</b>\n💬 Введенный текст: <b>{promo_code_input}</b>",
|
||||||
|
|
||||||
|
"admin_promo_management_button": "🎟 Управление промокодами",
|
||||||
|
"admin_promo_management_title": "🎟 <b>Управление промокодами</b>\n\nВыберите промокод для детального просмотра:",
|
||||||
|
"admin_promo_management_empty": "📭 Промокоды отсутствуют",
|
||||||
|
"admin_promo_card_title": "🎟 <b>Промокод: {code}</b>",
|
||||||
|
"admin_promo_card_bonus_days": "🎁 Бонусные дни: <b>{days}</b>",
|
||||||
|
"admin_promo_card_activations": "🔢 Активации: <b>{current}/{max}</b>",
|
||||||
|
"admin_promo_card_validity": "⏰ Действует до: <b>{validity}</b>",
|
||||||
|
"admin_promo_card_status": "📊 Статус: <b>{status}</b>",
|
||||||
|
"admin_promo_card_created": "📅 Создан: <b>{created}</b>",
|
||||||
|
"admin_promo_card_created_by": "👤 Создал: <b>{creator}</b>",
|
||||||
|
"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": "платежей"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user