From e78f9bf291f1bfb1173a6e6ef98fc3dced9bcbe2 Mon Sep 17 00:00:00 2001 From: ProtoPes Date: Tue, 26 Aug 2025 20:28:50 +0300 Subject: [PATCH 01/13] Added check for referral user, small refactoring With regular expressions we can be sure that the data is already valid --- bot/handlers/user/start.py | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/bot/handlers/user/start.py b/bot/handlers/user/start.py index fe0d90b..815934b 100644 --- a/bot/handlers/user/start.py +++ b/bot/handlers/user/start.py @@ -1,4 +1,5 @@ import logging +import re from aiogram import Router, F, types, Bot from aiogram.utils.text_decorations import html_decoration as hd from aiogram.filters import CommandStart, Command @@ -112,13 +113,16 @@ async def send_main_menu(target_event: Union[types.Message, @router.message(CommandStart()) +@router.message(CommandStart(magic=F.args.regexp(r"^ref_(\d+)$").as_("ref_match"))) +@router.message(CommandStart(magic=F.args.regexp(r"^promo_(\w+)$").as_("promo_match"))) async def start_command_handler(message: types.Message, state: FSMContext, settings: Settings, i18n_data: dict, subscription_service: SubscriptionService, session: AsyncSession, - command: Optional[CommandStart] = None): + ref_match: Optional[re.Match] = None, + promo_match: Optional[re.Match] = None): await state.clear() current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") @@ -130,28 +134,14 @@ async def start_command_handler(message: types.Message, referred_by_user_id: Optional[int] = None promo_code_to_apply: Optional[str] = None - - if command and command.args: - arg_payload = command.args - if arg_payload.startswith("ref_"): - try: - potential_referrer_id_str = arg_payload.split("_")[1] - if potential_referrer_id_str.isdigit(): - potential_referrer_id = int(potential_referrer_id_str) - if potential_referrer_id != user_id: - referred_by_user_id = potential_referrer_id - except (IndexError, ValueError) as e: - logging.warning( - f"Could not parse referral from /start args '{arg_payload}': {e}" - ) - elif arg_payload.startswith("promo_"): - try: - promo_code_to_apply = arg_payload.split("_")[1] - logging.info(f"User {user_id} started with promo code: {promo_code_to_apply}") - except (IndexError, ValueError) as e: - logging.warning( - f"Could not parse promo code from /start args '{arg_payload}': {e}" - ) + + if ref_match: + potential_referrer_id = int(ref_match.group(1)) + if await user_dal.get_user_by_id(session, potential_referrer_id): + referred_by_user_id = potential_referrer_id + elif promo_match: + promo_code_to_apply = promo_match.group(1) + logging.info(f"User {user_id} started with promo code: {promo_code_to_apply}") db_user = await user_dal.get_user_by_id(session, user_id) if not db_user: From 1091b7b846b114ba36213fe2e91b820788686543 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Thu, 28 Aug 2025 21:24:31 +0300 Subject: [PATCH 02/13] Update bot functionality and localization improvements - Added new entries to .gitignore for backup JSON files. - Enhanced user statistics in inline mode to include active users today. - Improved message handling in broadcast functionality to differentiate between text and media messages, ensuring proper parameter usage. - Updated payment handling to reflect pagination information in English and Russian. - Refactored promo management to utilize localized button texts for better user experience. --- .gitignore | 2 + bot/handlers/admin/broadcast.py | 55 +++++-- bot/handlers/admin/payments.py | 12 +- bot/handlers/admin/promo/manage.py | 2 +- bot/handlers/inline_mode.py | 1 + locales/en.json | 223 +++-------------------------- locales/ru.json | 209 ++------------------------- 7 files changed, 83 insertions(+), 421 deletions(-) diff --git a/.gitignore b/.gitignore index f2ff5f2..fd0a62f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ __pycache__/ # Игнорировать Docker артефакты (если вдруг) *.log *.pid +locales/ru_backup.json +locales/en_backup.json diff --git a/bot/handlers/admin/broadcast.py b/bot/handlers/admin/broadcast.py index c0e66b3..b2da070 100644 --- a/bot/handlers/admin/broadcast.py +++ b/bot/handlers/admin/broadcast.py @@ -97,14 +97,27 @@ async def process_broadcast_message_handler( # Отправляем превью-копию того, что будет разослано try: - await send_message_by_type( - bot, - chat_id=message.chat.id, - content=content, - parse_mode="HTML", - disable_web_page_preview=True, - disable_notification=True, - ) + # Для медиа-сообщений используем caption_entities, для текста - entities + if content.content_type == "text": + await send_message_by_type( + bot, + chat_id=message.chat.id, + content=content, + parse_mode="HTML", + entities=entities, + disable_web_page_preview=True, + disable_notification=True, + ) + else: + await send_message_by_type( + bot, + chat_id=message.chat.id, + content=content, + parse_mode="HTML", + caption_entities=entities, + disable_web_page_preview=True, + disable_notification=True, + ) except TelegramBadRequest as e: await message.answer( _( @@ -263,13 +276,25 @@ async def confirm_broadcast_callback_handler( # Queue all messages for sending for uid in user_ids: try: - await send_message_via_queue( - queue_manager, - uid, - content, - parse_mode="HTML", - disable_web_page_preview=True, - ) + # Для медиа-сообщений используем caption_entities, для текста - entities + if content.content_type == "text": + await send_message_via_queue( + queue_manager, + uid, + content, + parse_mode="HTML", + entities=entities, + disable_web_page_preview=True, + ) + else: + await send_message_via_queue( + queue_manager, + uid, + content, + parse_mode="HTML", + caption_entities=entities, + disable_web_page_preview=True, + ) sent_count += 1 # Log successful queuing diff --git a/bot/handlers/admin/payments.py b/bot/handlers/admin/payments.py index 2b4a95e..d203e98 100644 --- a/bot/handlers/admin/payments.py +++ b/bot/handlers/admin/payments.py @@ -77,7 +77,7 @@ async def view_payments_handler(callback: types.CallbackQuery, i18n_data: dict, return _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) - page_size = 5 # Показываем по 5 платежей на странице + page_size = 5 # Show 5 payments per page payments, total_count = await get_payments_with_pagination(session, page, page_size) total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 1 @@ -92,7 +92,11 @@ async def view_payments_handler(callback: types.CallbackQuery, i18n_data: dict, # Format payments text text_parts = [_("admin_payments_header", default="💰 Все платежи")] - text_parts.append(f"📊 Показано {len(payments)} из {total_count} платежей (стр. {page + 1}/{total_pages})\n") + text_parts.append(_("admin_payments_pagination_info", + shown=len(payments), + total=total_count, + current_page=page + 1, + total_pages=total_pages) + "\n") for i, payment in enumerate(payments, 1): text_parts.append(f"{page * page_size + i}. {format_payment_text(payment, i18n, current_lang)}") @@ -225,12 +229,12 @@ async def export_payments_csv_handler(callback: types.CallbackQuery, i18n_data: await callback.message.reply_document( document=file, caption=_("admin_payments_export_success", - default="📊 Экспорт платежей завершен!\nВсего записей: {count}", + default="📊 Payments export completed!\nTotal records: {count}", count=len(all_payments)) ) await callback.answer( - _("admin_export_sent", default="Файл отправлен!"), + _("admin_export_sent", default="File sent!"), show_alert=False ) diff --git a/bot/handlers/admin/promo/manage.py b/bot/handlers/admin/promo/manage.py index 25a2270..46198b5 100644 --- a/bot/handlers/admin/promo/manage.py +++ b/bot/handlers/admin/promo/manage.py @@ -126,7 +126,7 @@ async def promo_management_handler(callback: types.CallbackQuery, i18n_data: dic builder.row(*pagination_buttons) # Добавляем кнопки экспорта и возврата - builder.row(InlineKeyboardButton(text="📄 Экспорт CSV", callback_data="promo_export_all")) + builder.row(InlineKeyboardButton(text=_("admin_promo_export_csv_button"), callback_data="promo_export_all")) builder.row(InlineKeyboardButton(text=_("back_to_admin_panel_button"), callback_data="admin_action:main")) # Формируем заголовок с информацией о страницах diff --git a/bot/handlers/inline_mode.py b/bot/handlers/inline_mode.py index 83dd4fb..762da06 100644 --- a/bot/handlers/inline_mode.py +++ b/bot/handlers/inline_mode.py @@ -157,6 +157,7 @@ async def create_user_stats_result(session: AsyncSession, i18n_instance, lang: s "🚫 Заблокированных: {banned}\n" "🎁 Привлечено по реферальной программе: {referral}", total=user_stats['total_users'], + active_today=user_stats['active_today'], paid=user_stats['paid_subscriptions'], trial=user_stats['trial_users'], inactive=user_stats['inactive_users'], diff --git a/locales/en.json b/locales/en.json index bb471e3..1fc4937 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,5 +1,4 @@ { - "welcome": "Welcome, {user_name}!", "main_menu_greeting": "Hi, {user_name}! 👋\nWhat would you like to do?", "menu_activate_trial_button": "🆓 Free Trial", @@ -11,18 +10,19 @@ "menu_server_status_button": "📊 Status", "menu_support_button": "💬 Support", "menu_terms_button": "📄 Terms of Service", + "back_to_main_menu_button": "⬅️ Back", "choose_language": "Choose language / Выберите язык:", "language_set_alert": "Language changed!", + "error_occurred_try_again": "An error occurred, please try again.", - "error_try_again": "Please try again.", "error_displaying_menu": "Error displaying menu.", "main_menu_unknown_action": "Unknown action.", "select_subscription_period": "Select subscription period:", - "no_subscription_options_available": "No subscription options available at the moment.", "subscribe_for_months_button": "{months} mo. - {price} {currency_symbol}", + "choose_payment_method": "Choose payment method:", "pay_button": "💳 Pay", "pay_with_yookassa_button": "💳 YooKassa", @@ -32,63 +32,31 @@ "connect_button": "🔗 Connect", "cancel_button": "❌ Cancel", "payment_description_subscription": "Subscription payment for {months} mo.", - "payment_service_unavailable": "Payment service temporarily unavailable. Please try again later.", - "payment_service_unavailable_alert": "Payment service unavailable", - "error_creating_payment_record": "Error creating payment record. Please try again later.", - "error_payment_gateway_link_failed": "Failed to get payment link. Please contact support.", "payment_link_message": "To pay for {months} mo. subscription, click the button below:", - "error_payment_gateway": "Payment gateway error. Please try again later or contact support.", "payment_successful_error_details": "✅ Payment succeeded, but an error occurred displaying details. Your subscription is active. Contact support if anything is wrong.", "payment_successful_full": "✅ Payment successful!\nYour {months}-month subscription is active until {end_date}.\n\nConnection key:\n{config_link}\n\nTo connect, open the link and follow the instructions 👇", - "payment_successful_with_promo_full": "✅ Payment successful!\nYour {months}-month subscription (with promo bonus +{bonus_days} days) is active until {end_date}.\n\nConnection key:\n{config_link}\n\nTo connect, open the link and follow the instructions 👇", "payment_successful_with_referral_bonus_full": "✅ Payment successful!\nYour {months}-month subscription (base end date: {base_end_date}) has been extended by {bonus_days} bonus days for referral from {inviter_name} and is now active until {final_end_date}.\n\nConnection key:\n{config_link}\n\nTo connect, open the link and follow the instructions 👇", "payment_failed": "❌ Payment failed or was cancelled. Please try again or contact support.", - "error_processing_your_payment": "An error occurred processing your payment after success. Please contact support with the details.", - - "my_subscription_details": "🔐 My Subscription:\n\nStatus: {status}\nActive until: {end_date} (days left: {days_left})\n\nTraffic: {traffic_used} of {traffic_limit}\n\nConfiguration link:\n{config_link}", "config_link_not_available": "not available, contact support", - "subscription_not_active": "You have no active subscription.\nWould you like to purchase one?", - "status_active": "Active", - "status_expired": "Expired", - "status_disabled": "Disabled", "traffic_unlimited": "Unlimited", - "traffic_na": "n/a", - "error_service_unavailable": "Service temporarily unavailable. Please try again later.", "promo_code_prompt": "Please enter your promo code:", "promo_code_not_found": "Promo code {code} not found, expired, or already used the maximum number of times.", "promo_code_already_used_by_user": "You have already used promo code {code}.", - "promo_code_no_active_subscription": "You must have an active subscription to apply this promo code.", "promo_code_applied_success_full": "✅ Promo code applied successfully!\nSubscription active until {end_date}.\n\nConnection key:\n{config_link}\n\nTo connect, open the link and follow the instructions 👇", "error_applying_promo_bonus": "Failed to apply promo bonus. Please try again later or contact support.", "promo_input_cancelled_short": "Promo code entry cancelled.", - "trial_feature_disabled": "Free trial is currently unavailable.", "trial_already_had_subscription_or_trial": "You have already used a free trial or had a paid subscription. Free trial is available only once for new users.", - "trial_confirm_prompt": "Would you like to activate a free trial for {days} days with {traffic_gb} GB traffic limit?", "trial_confirm_activate_button": "✅ Activate!", "trial_activated_alert": "Trial activated!", - "trial_activated_details_message": "🚀 Your {days}-day free trial is activated!\n\nValid until: {end_date}\nTraffic: {traffic_gb} GB\n\nYour configuration link:\n{config_link}", - "trial_activation_failed": "Failed to activate free trial. Please try again later.", - "trial_activation_failed_panel_link": "Failed to link your account to the server for trial activation. Please try again later.", - "trial_activation_failed_db": "Database error during trial activation. Please try again later.", - "trial_activation_failed_panel_update": "Failed to update trial details on the server. Please try again later.", - "user_not_found_for_trial": "Your account was not found in the system. Please run /start first.", "yes_button": "Yes", "no_button": "No", - "trial_cancelled_short": "Activation cancelled.", - "referral_program_info_new": "🎁 Referral Program\n\n📊 Your stats:\n👥 Friends invited: {invited_count}\n💳 Purchased subscription: {purchased_count}\n\n🔗 Your link:\n{referral_link}\n\n💰 Invitation bonuses:\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!", "referral_bonus_per_period": "\n\n🎁 For a friend's {months}-month subscription:\n ➢ You: {inviter_bonus_days} days\n ➢ Friend: {referee_bonus_days} days", - "no_bonus_days": "0", - "referral_no_bonuses_configured": "\nNo referral bonuses configured.", - "referral_link_for_copying_reminder": "The link is above. Press and hold to copy.", "referral_share_message_button": "📩 Message for friend", "referral_friend_message": "🚀 Hey! Try this VPN - it's fast, reliable and affordable!\n\n🎁 Use my link to get bonus days with your subscription!\n\n{referral_link}", "friend_placeholder": "friend", - "referral_bonus_inviter_notification_extended": "🎉 Congrats! Your friend {referee_name} paid for a subscription. You received {days} bonus days! Your subscription is now active until {new_end_date}.", - "referral_bonus_inviter_notification_new_sub": "🎉 Congrats! Your friend {referee_name} paid for a subscription. You received a {days}-day bonus subscription! It is active until {new_end_date}.", - "user_is_banned": "🚫 Your account is banned. Please contact support.", "admin_panel_title": "Admin Panel", @@ -96,15 +64,12 @@ "admin_broadcast_button": "📢 Broadcast", "admin_create_promo_button": "🎁 Create Promo", "admin_create_bulk_promo_button": "📦 Bulk Create", - "admin_manage_promos_button": "🛠 Manage", - "admin_view_promos_button": "👀 List", "admin_ban_user_button": "🚫 Ban", "admin_unban_user_button": "✅ Unban", "admin_view_banned_users_button": "📜 Ban List", "admin_view_logs_menu_button": "📄 Logs", "admin_sync_panel_button": "🔄 Sync", "admin_unknown_action": "Unknown admin action.", - "admin_stats_and_monitoring_section": "📊 Statistics", "admin_user_management_section": "👥 Users", "admin_promo_marketing_section": "🎁 Promos", @@ -112,18 +77,13 @@ "admin_ban_management_section": "🚫 Bans", "admin_users_management_button": "👤 Management", "back_to_user_management_button": "⬅️ To Users", - "admin_action_cancelled_default": "Action cancelled. Returning to menu.", - "admin_action_cancelled_default_alert": "Action cancelled", "back_to_admin_panel_button": "⬅️ To Admin", - "admin_stats_header": "📊 Bot Statistics", "admin_enhanced_users_stats_header": "Users", "admin_financial_stats_header": "Financial Statistics", - "admin_stats_users": "👥 Users: Total - {total_users}, Banned - {banned_users}, Active Subs - {active_subs}", "admin_stats_recent_payments_header": "Recent Payments:", "admin_stats_payment_item": "{status_emoji} {amount} {currency} from {user_info} ({p_status}) [{p_date}]", "admin_stats_no_payments_found": "No payments found yet.", - "admin_view_payments_button": "💰 Payments", "admin_payments_header": "💰 All Payments", "admin_no_payments_found": "No payments found.", @@ -132,7 +92,6 @@ "admin_no_payments_to_export": "No payments to export.", "admin_payments_export_success": "📊 Payments export completed!\nTotal records: {count}", "admin_export_sent": "File sent!", - "admin_csv_payment_id": "ID", "admin_csv_user_id": "User ID", "admin_csv_username": "Username", @@ -152,9 +111,7 @@ "admin_stats_sync_subs_synced": "Subscriptions Synced", "admin_stats_sync_details_label": "Details", "admin_sync_status_never_run": "Panel sync never run.", - "admin_broadcast_enter_message": "Enter the broadcast message (HTML supported):", - "admin_broadcast_confirm_prompt": "You are about to send the following message:\n\n{message_preview}\n\nConfirm sending?", "admin_broadcast_confirm_prompt_short": "The message above will be sent. Confirm?", "broadcast_target_all_button": "👥 All", "broadcast_target_active_button": "✅ Active", @@ -164,35 +121,29 @@ "admin_broadcast_sending_started": "Starting broadcast...", "admin_broadcast_error_no_message": "Error: no message to broadcast.", "admin_broadcast_error_no_message_alert": "Broadcast message is empty!", - "admin_broadcast_finished_stats": "🏁 Broadcast finished!\nSent: {sent_count}\nFailed: {failed_count}", "admin_broadcast_cancelled": "Broadcast cancelled.", "admin_broadcast_cancelled_alert": "Broadcast cancelled!", "admin_broadcast_cancelled_nav_back": "Broadcast cancelled. You are returned to the admin panel.", - - "admin_broadcast_invalid_html": "❌ Invalid HTML in message. Please send valid HTML (Telegram-supported tags) or remove tags.", - - "admin_promo_create_prompt": "Enter promo details in the format: CODE BONUS_DAYS MAX_USES [VALIDITY_DAYS]\nExample: {example_format}\n(Validity is optional; default is indefinite)", - "admin_promo_invalid_format": "Invalid format. Please use: CODE BONUS_DAYS MAX_USES [VALIDITY_DAYS]", "admin_promo_invalid_code_format": "Code must be 3–30 alphanumeric characters.", "admin_promo_invalid_bonus_days": "Bonus days must be a positive number.", "admin_promo_invalid_max_activations": "Max activations must be a positive number.", - "admin_promo_invalid_bonus_or_activations": "Bonus days and max uses must be positive numbers.", "admin_promo_invalid_validity_days": "Validity period (in days) must be a positive number.", - "admin_promo_invalid_values": "Invalid values. {error}", - "admin_promo_invalid_format_general": "Promo parsing error. Check the format.", "admin_promo_created_success": "✅ Promo code {code} created successfully!\nBonus: {bonus_days} days\nMax uses: {max_activations}\nValid until: {valid_until_str}.", - "admin_promo_creation_failed_duplicate": "❌ Error: Promo code {code} already exists.", - "admin_promo_creation_failed": "❌ Failed to create promo code. Please try again later.", + "admin_promo_set_validity_days": "⏰ Установить срок (дни)", + "admin_back_to_panel": "⬅️ В панель", + "admin_promo_unlimited": "♾️ Неограниченно", + "admin_bulk_promo_created_title": "📦 Массовое создание завершено", + "admin_bulk_promo_created_stats": "📊 Создано: {created} из {total}", + "admin_bulk_promo_settings": "🎁 Бонусные дни: {bonus_days}\n📊 Макс. активаций: {max_activations}\n⏰ Срок действия: {validity}", + "admin_promo_list_page_info": "Page {current}/{total} ({count} promo codes)", + "admin_queue_status_button": "📊 Статус очередей", + "admin_queue_status_title": "📊 Message Queue Status", + "admin_queue_status_info": "📤 Message Queues:\n\n👥 Users (25 msg/sec):\n 📋 In queue: {user_queue_size}\n 🔄 Processing: {user_processing}\n 📈 Sent per minute: {user_recent}\n\n📢 Groups/channels (15 msg/min):\n 📋 In queue: {group_queue_size}\n 🔄 Processing: {group_processing}\n 📈 Sent per minute: {group_recent}", "admin_active_promos_list_header": "Active Promo Codes:", "admin_no_active_promos": "No active promo codes.", - "admin_promo_list_item": "{code}: +{bonus}d, {current}/{max} used, valid until {valid_until}", "admin_promo_valid_indefinitely": "indefinite", - "admin_promo_valid_until_display": "until {date}", - "admin_manage_promos_title": "Manage Promo Codes", "admin_promo_edit_button": "✏️ Edit", "admin_promo_delete_button": "🗑 Delete", - "admin_promo_edit_prompt": "Send new details for {code} in format: CODE BONUS_DAYS MAX_USES [VALIDITY_DAYS]", - "admin_promo_updated_success": "Promo {code} updated.", "admin_promo_deleted_success": "Promo {code} deleted.", "admin_promo_not_found": "Promo not found.", "admin_promo_export_csv_button": "📄 Export to CSV", @@ -206,85 +157,36 @@ "admin_promo_edit_bonus_days": "🎁 Bonus Days", "admin_promo_edit_max_activations": "🔢 Max Activations", "admin_promo_edit_validity": "⏰ Validity", - "admin_ban_user_prompt": "Enter user ID or @username to ban:", - "admin_user_not_found_in_bot_db": "User {user_id} not found in bot database.", - "admin_cannot_ban_self_or_admin": "You cannot ban yourself or another admin.", "admin_user_already_banned": "User {user_id_or_username} is already banned.", - "admin_user_banned_success_combined": "✅ User {user_id_or_username} banned in bot {panel_status_part}.", - "admin_panel_ban_success_part": "and on panel", - "admin_panel_ban_fail_part": ", but failed to ban on panel", - "admin_user_ban_failed_local_db_error": "❌ Failed to ban user in local database.", "admin_unban_user_prompt": "Enter user ID or @username to unban:", - "admin_user_not_banned": "User {user_id_or_username} is not banned.", - "admin_user_unbanned_success_combined": "✅ User {user_id_or_username} unbanned in bot {panel_status_part}.", - "admin_panel_unban_success_part": "and on panel", - "admin_panel_unban_fail_part": ", but failed to unban on panel", - "admin_user_unban_failed_local_db_error": "❌ Failed to unban user in local database.", - "admin_no_banned_users": "There are no banned users at the moment.", - "admin_banned_list_title": "Banned Users (page {current_page}/{total_pages}):", + "admin_user_not_banned": "⚠️ User is not banned", "admin_banned_user_button_text": "{user_display} (ID: {user_id})", "prev_page_button": "⬅️ Prev.", "next_page_button": "Next ➡️", - "admin_user_card_title": "User Profile: {user_display}", - "user_card_info": "ID: {user_id}\nUsername: @{username}\nName: {first_name} {last_name}\nLanguage: {language_code}\nPanel UUID: {panel_user_uuid}\nStatus: {ban_status}\nRegistered: {reg_date}\nSubscription until: {sub_end_date}", - "user_card_banned": "BANNED", - "user_card_active": "Active", - "user_card_sub_na": "n/a", + "admin_user_card_title": "User Card", "user_card_ban_button": "🚫 Ban", "user_card_unban_button": "✅ Unban", "user_card_back_to_banned_list_button": "⬅️ Back to Ban List", - "admin_confirm_action_title": "Confirm: {action_text}", - "ban_verb_l": "ban", - "unban_verb_l": "unban", - "admin_confirm_ban_prompt": "Are you sure you want to ban {user_display} (ID: {user_id})?", - "admin_confirm_unban_prompt": "Are you sure you want to unban {user_display} (ID: {user_id})?", - "admin_user_banned_from_card_alert": "User {user_display} (ID: {user_id}) has been banned.", - "admin_user_unbanned_from_card_alert": "User {user_display} (ID: {user_id}) has been unbanned.", - "admin_user_ban_failed_db_error": "Error banning user in database.", - "admin_user_unban_failed_db_error": "Error unbanning user in database.", - "admin_panel_status_update_fail_part": ", but panel status not updated", - "admin_logs_menu_title": "Logs Menu:", "admin_view_all_logs_button": "📜 All Message Logs", "admin_view_user_logs_prompt_button": "👤 User Logs", "admin_export_logs_csv_button": "📄 Export to CSV", - "admin_all_logs_title": "All Logs (page {current_page}/{total_pages}):", "admin_no_logs_found": "No logs found.", "admin_log_entry_format": "{timestamp_str} - {user_display} (ID: {user_id})\n {event_type}: {content_preview}", - "system_or_unknown_user": "System/Unknown", "admin_prompt_for_user_id_or_username_logs": "Enter user ID or @username to view logs:", "admin_log_user_not_found": "User \"{input}\" not found in bot database.", - "admin_user_logs_title": "Logs for {user_display} (page {current_page}/{total_pages}):", - "sync_started_simple": "🔄 Starting synchronization...", "sync_success_simple": "✅ Synchronization completed successfully", "sync_failed_simple": "❌ Synchronization failed", "sync_errors_simple": "⚠️ Synchronization completed with errors ({errors_count} errors)", "sync_critical_error": "❌ Critical synchronization error", - "no_errors_placeholder": "none", "admin_sync_initiated_from_panel": "Sync initiated...", - "admin_panel_user_creation_failed": "❌ Failed to create panel user for TG ID {user_id}. Panel unreachable?", + "admin_broadcast_invalid_html": "❌ Invalid HTML in message. Please send valid HTML (Telegram-supported tags) or remove tags.", "error_displaying_logs_too_long": "Error: logs too long to display in one message. Try viewing logs for a specific user.", "error_displaying_statistics": "Error displaying statistics.", - "stub_page_display": "Page", - - "subscription_72h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 3 days — {end_date}.\n\nPlease renew it using the button below.", - "subscription_48h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 2 days — {end_date}.\n\nPlease renew it using the button below.", - "subscription_24h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 1 day — {end_date}.\n\nPlease renew it using the button below.", - "subscription_expired_notification": "👋 Hi, {user_name}!\n\n⛔ Your VPN subscription expired on {end_date}.\n\nPlease renew it using the button below.", - "subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.", - "tribute_subscription_cancelled": "🚨 Subscription Cancelled\n\nYour Tribute subscription has been cancelled. You have 24 hours to restore access, after which the subscription will be blocked.\n\nTo renew your subscription, press the button below.", "tribute_auto_renewal": "🔄 Subscription Auto-Renewed\n\nYour Tribute subscription has been automatically renewed for {months} months.\nNew expiration date: {end_date}", - - "admin_new_trial_notification": "\ud83c\udf21 User {user_id} activated a free trial until {end_date}.", - - "admin_promo_activation_notification": "\ud83c\udf81 Promo code {code} activated by user {user_id} (+{bonus_days}d).", - - "error_unknown": "An unknown error occurred.", - "admin_user_management_prompt": "👤 User Management\n\nEnter user ID or @username to search:", - "admin_user_card_title": "User Card", "admin_user_subscription_info": "Subscription Information:", "admin_user_reset_trial_button": "🔄 Reset Trial", "admin_user_add_subscription_button": "➕ Add Days", @@ -295,7 +197,6 @@ "admin_user_search_new_button": "🔍 Find Another", "admin_user_view_all_logs_button": "📋 All Actions", "admin_user_back_to_card_button": "🔙 Back to Card", - "admin_user_not_found": "❌ User not found: {input}", "admin_user_not_found_action": "User not found", "admin_user_card_error": "❌ Error displaying user card", @@ -311,7 +212,6 @@ "admin_user_unban_success": "✅ User {input} has been unbanned", "admin_user_ban_error": "❌ Error banning user", "admin_user_unban_error": "❌ Error unbanning user", - "admin_user_not_banned": "⚠️ User is not banned", "admin_user_send_message_prompt": "✉️ Sending message to user {user_id}\n\nEnter message text:", "admin_user_message_too_long": "❌ Message too long (maximum 4000 characters)", "admin_user_message_sent_success": "✅ Message sent to user {user_id}", @@ -319,27 +219,13 @@ "admin_user_no_logs": "📜 User has no actions", "admin_user_logs_error": "❌ Error loading user actions", "admin_direct_message_signature": "\n\n---\n💬 Message from administrator", - "inline_user_stats_message": "👥 User Statistics\n\n📊 Total: {total}\n📈 Active today: {active_today}\n💳 With paid subscription: {paid}\n🆓 On trial period: {trial}\n😴 Inactive: {inactive}\n🚫 Banned: {banned}\n🎁 Via referral program: {referral}", + "inline_user_stats_description": "Всего: {total}, Платных: {active}", "inline_admin_user_stats_title": "👥 User Statistics", - "inline_admin_user_stats_desc": "Total: {total}, Active: {paid}", - "inline_financial_stats_message": "💰 Financial Statistics\n\n📅 Today: {today:.2f} RUB\n ({today_count} payments)\n📅 Week: {week:.2f} RUB\n📅 Month: {month:.2f} RUB\n🏆 All time: {all_time:.2f} RUB", "inline_admin_financial_stats_title": "💰 Financial Statistics", - "inline_admin_financial_stats_desc": "Today: {today:.2f} RUB", - "inline_system_stats_message": "🖥 Panel Statistics\n\n🟢 Online: {online}\n📊 Active: {active}\n🔴 Disabled: {disabled}\n⏰ Expired: {expired}\n⚠️ Limited: {limited}\n👥 Total users: {total}\n💾 RAM Usage: {memory:.1f}%\n📊 Week traffic: {week_traffic}\n📊 Month traffic: {month_traffic}\n🔗 Active nodes: {active_nodes}/{total_nodes}", "inline_admin_system_stats_title": "🖥 System Statistics", - "inline_admin_system_stats_desc": "Online: {online}, Offline: {offline}", - - "inline_admin_help_message": "🤖 Bot Inline Mode\n\n📱 Available commands:\n\n🎁 ref - share referral link\n👥 stat - user statistics\n💰 financial - financial statistics\n🖥 system - system statistics\n\n💡 Just type @{bot_username} and start typing a command in any chat!", - "inline_admin_help_title": "🤖 Inline Help (Admin)", - "inline_admin_help_desc": "Available commands: ref, stat, financial, system", - - "inline_user_help_message": "🤖 Bot Inline Mode\n\n📱 Available commands:\n\n🎁 ref - share referral link\n\n💡 Just type @{bot_username} and start typing 'ref' in any chat!", - "inline_user_help_title": "🤖 Inline Help", - "inline_user_help_desc": "Available command: ref (referral link)", - "log_referral_suffix": " (referral from {referrer_id})", "log_new_user_registration": "👤 New User\n\n🆔 ID: {user_id}\n👤 Name: {user_display}{referral_text}\n📅 Time: {timestamp}", "log_payment_received": "{provider_emoji} Payment Received\n\n👤 User: {user_display}\n💰 Amount: {amount} {currency}\n📅 Period: {months} mo.\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}", @@ -347,28 +233,8 @@ "log_trial_activation": "🆓 Trial Activated\n\n👤 User: {user_display}\n⏰ Valid until: {end_date}\n🕐 Time: {timestamp}", "log_panel_sync": "{status_emoji} Panel Synchronization\n\n📊 Status: {status}\n👥 Users processed: {users_processed}\n📋 Subscriptions synced: {subs_synced}\n🕐 Time: {timestamp}\n\n📝 Details:\n{details}", "log_suspicious_promo": "⚠️ Suspicious Promo Code Attempt\n\n👤 User: {user_display}\n🆔 ID: {user_id}\n📝 Input:
{suspicious_input}
\n🕐 Time: {timestamp}", - - "admin_general_cancel_operation": "Operation cancelled ❌", - "admin_promo_codes_menu_bulk_create": "📦 Bulk Creation", - "admin_bulk_promo_create_prompt": "📦 Bulk Promo Code Creation\n\nEnter data in format:\nquantity|days|description\n\nExample:\n10|7|Weekly promo", - "admin_bulk_promo_invalid_format": "❌ Invalid format. Use: quantity|days|description", - "admin_bulk_promo_created_success": "✅ Created {count} promo codes for {days} days!", - "admin_bulk_promo_codes_file": "📄 All created promo codes in file ({count} pcs.)", - - "admin_promo_edit_step1_code": "✏️ Editing Promo Code\n\nStep 1 of 4: Promo Code\n\nCurrent code: {current_code}\n\nEnter new promo code (3-30 characters, letters and numbers only) or send current one to keep:", - "admin_promo_edit_step2_bonus_days": "✏️ Editing Promo Code\n\nStep 2 of 4: Bonus Days\n\nCode: {code}\nCurrent days: {current_days}\n\nEnter number of bonus days (1-365):", - "admin_promo_edit_step3_max_activations": "✏️ Editing Promo Code\n\nStep 3 of 4: Max Activations\n\nCode: {code}\nDays: {days}\nCurrent activations: {current_max}\n\nEnter maximum number of activations (1-10000):", - "admin_promo_edit_step4_validity": "✏️ Editing Promo Code\n\nStep 4 of 4: Validity Period\n\nCode: {code}\nDays: {days}\nActivations: {max_act}\nCurrent validity: {current_validity}\n\nChoose the validity period for the promo code:", - "admin_promo_edit_unlimited_validity": "♾️ Unlimited", - "admin_promo_edit_set_validity": "⏰ Set Period", - "admin_promo_edit_enter_validity_days": "⏰ Enter the number of validity days for the promo code (1-365):", - "admin_promo_updated_success": "✅ Promo code updated!\n\n🎟 Code: {code}\n🎁 Bonus days: {days}\n🔢 Max activations: {max_act}\n⏰ Valid until: {validity}", - "admin_promo_update_failed": "❌ Error updating promo code", - - "admin_logs_export_csv": "📄 Export to CSV", "admin_logs_csv_export_started": "📄 Starting log export to CSV...", "admin_logs_csv_export_success": "✅ Logs exported! File attached above.", - "admin_csv_header_log_id": "Log ID", "admin_csv_header_timestamp": "Timestamp", "admin_csv_header_user_id": "User ID", @@ -379,44 +245,23 @@ "admin_csv_header_is_admin_event": "Admin Event", "admin_csv_header_target_user_id": "Target User ID", "admin_csv_header_raw_update_preview": "Update Preview", - "admin_banned_users_empty": "📋 Banned Users\n\nList is empty", "admin_banned_users_list": "📋 Banned Users ({count}):\n\n{users}", - "admin_panel_stats_header": "Panel Statistics", - "admin_bulk_promo_step1_quantity": "📦 Bulk Promo Code Creation\n\nStep 1 of 4: Quantity\n\nEnter the number of promo codes to create (1-1000):", "admin_bulk_promo_step2_bonus_days": "📦 Bulk Promo Code Creation\n\nStep 2 of 4: Bonus Days\n\nQuantity: {quantity}\n\nEnter the number of bonus days (1-365):", "admin_bulk_promo_step3_max_activations": "📦 Bulk Promo Code Creation\n\nStep 3 of 4: Max Activations\n\nQuantity: {quantity}\nBonus days: {bonus_days}\n\nEnter the maximum number of activations for each promo code (1-10000):", "admin_bulk_promo_step4_validity": "📦 Bulk Promo Code Creation\n\nStep 4 of 4: Validity Period\n\nQuantity: {quantity}\nBonus days: {bonus_days}\nMax activations: {max_activations}\n\nChoose the validity period for promo codes:", - "admin_bulk_promo_invalid_quantity": "❌ Quantity must be between 1 and 1000", - "admin_bulk_promo_invalid_bonus_days": "❌ Bonus days must be between 1 and 365", - "admin_bulk_promo_invalid_max_activations": "❌ Max activations must be between 1 and 10000", - - "admin_bulk_promo_unlimited_validity": "♾️ Unlimited", - "admin_bulk_promo_set_validity": "⏰ Set Period", "admin_bulk_promo_enter_validity_days": "⏰ Enter the number of validity days for promo codes (1-365):", - "admin_bulk_promo_creating": "⏳ Creating {quantity} promo codes...", - "admin_promo_step1_code": "🎟 Create Promo Code\n\nStep 1 of 4: Promo Code\n\nEnter promo code (3-30 characters, letters and numbers only):", "admin_promo_step2_bonus_days": "🎟 Create Promo Code\n\nStep 2 of 4: Bonus Days\n\nCode: {code}\n\nEnter the number of bonus days (1-365):", "admin_promo_step3_max_activations": "🎟 Create Promo Code\n\nStep 3 of 4: Max Activations\n\nCode: {code}\nBonus days: {bonus_days}\n\nEnter the maximum number of activations (1-10000):", "admin_promo_step4_validity": "🎟 Create Promo Code\n\nStep 4 of 4: Validity Period\n\nCode: {code}\nBonus days: {bonus_days}\nMax activations: {max_activations}\n\nChoose the validity period for the promo code:", - "admin_promo_code_already_exists": "❌ A promo code with this code already exists", - "admin_promo_processing_error": "❌ Error processing promo code", "admin_promo_unlimited_validity": "♾️ Unlimited", - "admin_promo_set_validity": "⏰ Set Period", "admin_promo_enter_validity_days": "⏰ Enter the number of validity days for the promo code (1-365):", - "admin_promo_skip_description": "⏭️ Skip", - "admin_promo_description_too_long": "❌ Description must not exceed 200 characters", - "admin_promo_creating": "⏳ Creating promo code...", - - "admin_panel_back_button": "⬅️ Back", - "admin_bulk_promo_creation_failed": "❌ Error creating promo codes: {error}", - "admin_user_id_label": "🆔 ID:", "admin_user_name_label": "👤 Name:", "admin_user_username_label": "📱 Username:", @@ -429,7 +274,6 @@ "admin_user_traffic_label": "📊 Traffic:", "admin_user_subscription_label": "💼 Subscription:", "admin_user_trial_label": "🏡 Trial:", - "admin_user_status_banned": "🚫 Banned", "admin_user_status_active": "✅ Active", "admin_user_trial_used": "Used", @@ -437,21 +281,12 @@ "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_count": "Total entries: {count}", - "admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}", - "admin_user_actions_count_label": "📜 Total actions:", "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_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}", @@ -471,43 +306,28 @@ "admin_promo_view_activations_button": "📋 Activations", "admin_promo_back_to_list_button": "⬅️ Back to list", "admin_promo_toggle_success": "✅ Promo code {code} {status}", - "admin_promo_toggle_failed": "❌ Error changing promo code status", "admin_promo_no_activations": "📋 Activations of promo code: {code}\n\n❌ No activations found", "admin_promo_activations_header": "📋 Activations of promo code: {code}\n\n", "admin_promo_activation_item": "👤 User ID: {user_id}\n📅 Date: {date}\n", "admin_promo_back_to_detail_button": "⬅️ Back to promo", - "admin_panel_online_label": "Online", "admin_panel_active_label": "Active", "admin_panel_disabled_label": "Disabled", - "admin_panel_offline_label": "Offline", - "admin_panel_expired_label": "Expired", + "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": "🚀 Hey! Try this VPN - it's fast, reliable and affordable!\n\n🎁 Use my link to get bonus days with your subscription!\n\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_referral_description": "Share referral link to get bonuses", "inline_financial_description": "Today: {today} RUB", - "inline_system_title": "System statistics", "inline_system_description": "🟢 Online: {online}, 📊 Active: {active}", - "inline_admin_help_title": "Commands help", - "inline_admin_help_description": "Available commands: ref, stat, financial, system", - "admin_user_stats_total_label": "Total", "admin_user_stats_paid_subs_label": "With paid subscription", "admin_user_stats_trial_label": "On trial period", @@ -519,9 +339,8 @@ "admin_financial_month_label": "This month", "admin_financial_all_time_label": "All time", "admin_financial_payments_label": "payments", - "admin_sync_details": "📊 Synchronization Statistics:\n🔍 Panel records checked: {panel_records_checked}\n👥 Users found in DB: {users_found_in_db}\n✨ New users created: {users_created}\n🔄 Users updated: {users_updated}\n📋 Subscriptions synced: {subscriptions_synced_count}\n ├── Created new: {subscriptions_created}\n └── Updated existing: {subscriptions_updated}{additional_stats}", "admin_sync_no_telegram_id": "\n⚠️ Records without telegramId: {count}", "admin_sync_not_found_in_db": "\n❌ Not found in DB: {count}", - "admin_sync_errors": "\n🚫 Errors: {count}" + "admin_payments_pagination_info": "📊 Showing {shown} of {total} payments (page {current_page}/{total_pages})" } diff --git a/locales/ru.json b/locales/ru.json index 09c39b3..caf4bc8 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,5 +1,4 @@ { - "welcome": "Добро пожаловать, {user_name}!", "main_menu_greeting": "Привет, {user_name}! 👋\nЧто бы вы хотели сделать?", "menu_activate_trial_button": "🆓 Пробный период", @@ -11,18 +10,19 @@ "menu_server_status_button": "📊 Статус", "menu_support_button": "💬 Поддержка", "menu_terms_button": "📄 Условия сервиса", + "back_to_main_menu_button": "⬅️ Назад", - + "choose_language": "Выберите язык / Select language:", "language_set_alert": "Язык изменен!", + "error_occurred_try_again": "Произошла ошибка, попробуйте снова.", - "error_try_again": "Попробуйте еще раз.", "error_displaying_menu": "Ошибка отображения меню.", "main_menu_unknown_action": "Неизвестное действие.", "select_subscription_period": "Выберите срок подписки:", - "no_subscription_options_available": "В данный момент нет доступных вариантов подписки.", "subscribe_for_months_button": "{months} мес. - {price} {currency_symbol}", + "choose_payment_method": "Выберите способ оплаты:", "pay_button": "💳 Оплатить", "pay_with_yookassa_button": "💳 ЮKassa", @@ -32,63 +32,31 @@ "connect_button": "🔗 Подключиться", "cancel_button": "❌ Отмена", "payment_description_subscription": "Оплата подписки на {months} мес.", - "payment_service_unavailable": "Платежный сервис временно недоступен. Пожалуйста, попробуйте позже.", - "payment_service_unavailable_alert": "Платежный сервис недоступен", - "error_creating_payment_record": "Ошибка при создании записи о платеже. Попробуйте позже.", - "error_payment_gateway_link_failed": "Не удалось получить ссылку на оплату. Пожалуйста, свяжитесь с поддержкой.", "payment_link_message": "Для оплаты подписки на {months} мес., нажмите кнопку ниже:", - "error_payment_gateway": "Ошибка платежного шлюза. Попробуйте позже или свяжитесь с поддержкой.", "payment_successful_error_details": "✅ Оплата прошла успешно, но возникла ошибка при отображении деталей. Ваша подписка активна. Свяжитесь с поддержкой, если что-то не так.", "payment_successful_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. активна до {end_date}.\n\nКлюч подключения:\n{config_link}\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇", - "payment_successful_with_promo_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. (с учетом промокода на +{bonus_days} дней) активна до {end_date}.\n\nКлюч подключения:\n{config_link}\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇", "payment_successful_with_referral_bonus_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. (базовая дата окончания: {base_end_date}) продлена на {bonus_days} бонусных дней за приглашение от {inviter_name} и теперь активна до {final_end_date}.\n\nКлюч подключения:\n{config_link}\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇", "payment_failed": "❌ Оплата не удалась или была отменена. Пожалуйста, попробуйте еще раз или свяжитесь с поддержкой.", - "error_processing_your_payment": "Произошла ошибка при обработке вашего платежа после его успеха. Пожалуйста, свяжитесь с поддержкой, указав детали.", - - "my_subscription_details": "🔐 Моя подписка:\n\nСтатус: {status}\nАктивна до: {end_date} (осталось дней: {days_left})\n\nТрафик: {traffic_used} из {traffic_limit}\n\nСсылка на конфигурацию:\n{config_link}", "config_link_not_available": "недоступна, обратитесь в поддержку", - "subscription_not_active": "У вас нет активной подписки. \nХотите приобрести?", - "status_active": "Активна", - "status_expired": "Истекла", - "status_disabled": "Отключена", "traffic_unlimited": "Безлимитный", - "traffic_na": "н/д", - "error_service_unavailable": "Сервис временно недоступен. Пожалуйста, попробуйте позже.", "promo_code_prompt": "Пожалуйста, введите ваш промокод:", "promo_code_not_found": "Промокод {code} не найден, истек или уже использован максимальное количество раз.", "promo_code_already_used_by_user": "Вы уже активировали промокод {code}.", - "promo_code_no_active_subscription": "Для активации этого промокода у вас должна быть активная подписка.", "promo_code_applied_success_full": "✅ Промокод успешно применен!\nПодписка активна до {end_date}.\n\nКлюч подключения:\n{config_link}\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇", "error_applying_promo_bonus": "Не удалось применить бонус по промокоду. Пожалуйста, попробуйте позже или свяжитесь с поддержкой.", "promo_input_cancelled_short": "Ввод промокода отменен.", - "trial_feature_disabled": "Пробный период в данный момент недоступен.", "trial_already_had_subscription_or_trial": "Вы уже использовали пробный период или у вас была платная подписка. Пробный период доступен только один раз для новых пользователей.", - "trial_confirm_prompt": "Хотите активировать бесплатный пробный период на {days} дней с лимитом трафика {traffic_gb}?", "trial_confirm_activate_button": "✅ Активировать!", "trial_activated_alert": "Пробный период активирован!", - "trial_activated_details_message": "🚀 Ваш пробный период на {days} дней активирован!\n\nДействует до: {end_date}\nТрафик: {traffic_gb}\n\nВаша ссылка на конфигурацию:\n{config_link}", - "trial_activation_failed": "Не удалось активировать пробный период. Пожалуйста, попробуйте позже.", - "trial_activation_failed_panel_link": "Не удалось связать ваш аккаунт с сервером для активации пробного периода. Попробуйте позже.", - "trial_activation_failed_db": "Ошибка базы данных при активации пробного периода. Попробуйте позже.", - "trial_activation_failed_panel_update": "Не удалось обновить детали на сервере для пробного периода. Попробуйте позже.", - "user_not_found_for_trial": "Ваш аккаунт не найден в системе. Пожалуйста, сначала выполните команду /start.", "yes_button": "Да", "no_button": "Нет", - "trial_cancelled_short": "Активация отменена.", - "referral_program_info_new": "🎁 Реферальная программа\n\n📊 Твоя статистика:\n👥 Приглашено друзей: {invited_count}\n💳 Купили подписку: {purchased_count}\n\n🔗 Твоя ссылка:\n{referral_link}\n\n💰 Бонусы за приглашения:\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!", "referral_bonus_per_period": "\n\n🎁 За {months}-мес. подписку друга:\n ➢ Вы: {inviter_bonus_days} дн.\n ➢ Друг: {referee_bonus_days} дн.", - "no_bonus_days": "0", - "referral_no_bonuses_configured": "\nРеферальные бонусы не настроены.", - "referral_link_for_copying_reminder": "Ссылка выше. Нажмите и удерживайте для копирования.", "referral_share_message_button": "📩 Сообщение для друга", "referral_friend_message": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}", "friend_placeholder": "друг", - "referral_bonus_inviter_notification_extended": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислено {days} бонусных дней! Ваша подписка теперь активна до {new_end_date}.", - "referral_bonus_inviter_notification_new_sub": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислена бонусная подписка на {days} дней! Она активна до {new_end_date}.", - "user_is_banned": "🚫 Ваш аккаунт заблокирован. Пожалуйста, свяжитесь со службой поддержки.", "admin_panel_title": "Панель администратора", @@ -96,15 +64,12 @@ "admin_broadcast_button": "📢 Рассылка", "admin_create_promo_button": "🎁 Создать промо", "admin_create_bulk_promo_button": "📦 Массовое создание", - "admin_manage_promos_button": "🛠 Управление", - "admin_view_promos_button": "👀 Список", "admin_ban_user_button": "🚫 Забанить", "admin_unban_user_button": "✅ Разбанить", "admin_view_banned_users_button": "📜 Бан-лист", "admin_view_logs_menu_button": "📄 Логи", "admin_sync_panel_button": "🔄 Синхронизация", "admin_unknown_action": "Неизвестное действие администратора.", - "admin_stats_and_monitoring_section": "📊 Статистика", "admin_user_management_section": "👥 Пользователи", "admin_promo_marketing_section": "🎁 Промокоды", @@ -112,18 +77,13 @@ "admin_ban_management_section": "🚫 Блокировки", "admin_users_management_button": "👤 Управление", "back_to_user_management_button": "⬅️ К пользователям", - "admin_action_cancelled_default": "Действие отменено. Возврат в меню.", - "admin_action_cancelled_default_alert": "Действие отменено", "back_to_admin_panel_button": "⬅️ В админку", - "admin_stats_header": "📊 Статистика Бота", "admin_enhanced_users_stats_header": "Пользователи", "admin_financial_stats_header": "Финансовая статистика", - "admin_stats_users": "👥 Пользователи: Всего - {total_users}, Забанено - {banned_users}, С активной подпиской - {active_subs}", "admin_stats_recent_payments_header": "Последние платежи:", "admin_stats_payment_item": "{status_emoji} {amount} {currency} от {user_info} ({p_status}) [{p_date}]", "admin_stats_no_payments_found": "Платежей пока нет.", - "admin_view_payments_button": "💰 Платежи", "admin_payments_header": "💰 Все платежи", "admin_no_payments_found": "Платежи не найдены.", @@ -132,7 +92,6 @@ "admin_no_payments_to_export": "Нет платежей для экспорта.", "admin_payments_export_success": "📊 Экспорт платежей завершен!\nВсего записей: {count}", "admin_export_sent": "Файл отправлен!", - "admin_csv_payment_id": "ID", "admin_csv_user_id": "User ID", "admin_csv_username": "Логин", @@ -152,9 +111,7 @@ "admin_stats_sync_subs_synced": "Синхронизировано подписок", "admin_stats_sync_details_label": "Детали", "admin_sync_status_never_run": "Синхронизация с панелью еще не проводилась.", - "admin_broadcast_enter_message": "Введите сообщение для рассылки (HTML поддерживается):", - "admin_broadcast_confirm_prompt": "Вы собираетесь отправить следующее сообщение:\n\n{message_preview}\n\nПодтверждаете отправку?", "admin_broadcast_confirm_prompt_short": "Сообщение выше будет отправлено. Подтвердить отправку?", "broadcast_target_all_button": "👥 Все", "broadcast_target_active_button": "✅ Активные", @@ -164,20 +121,13 @@ "admin_broadcast_sending_started": "Начинаю рассылку...", "admin_broadcast_error_no_message": "Ошибка: сообщение для рассылки не найдено.", "admin_broadcast_error_no_message_alert": "Сообщение для рассылки пустое!", - "admin_broadcast_finished_stats": "🏁 Рассылка завершена!\nОтправлено: {sent_count}\nНе удалось отправить: {failed_count}", "admin_broadcast_cancelled": "Рассылка отменена.", "admin_broadcast_cancelled_alert": "Рассылка отменена!", "admin_broadcast_cancelled_nav_back": "Рассылка отменена. Вы возвращены в админ-панель.", - - "admin_promo_create_prompt": "Введите детали промокода в формате: КОД ДНИ_БОНУСА МАКС_АКТИВАЦИЙ [СРОК_ДЕЙСТВИЯ_В_ДНЯХ_ОТ_СЕЙЧАС]\nПример: {example_format}\n(Срок действия необязателен, по умолчанию - бессрочный)", - "admin_promo_invalid_format": "Неверный формат ввода. Пожалуйста, используйте: КОД ДНИ_БОНУСА МАКС_АКТИВАЦИЙ [ДНИ_ДЕЙСТВИЯ]", "admin_promo_invalid_code_format": "Код должен быть от 3 до 30 символов и содержать только буквы и цифры.", "admin_promo_invalid_bonus_days": "Количество бонусных дней должно быть положительным числом.", "admin_promo_invalid_max_activations": "Максимальное количество активаций должно быть положительным числом.", - "admin_promo_invalid_bonus_or_activations": "Количество бонусных дней и максимальных активаций должны быть положительными числами.", "admin_promo_invalid_validity_days": "Срок действия промокода (в днях) должен быть положительным числом.", - "admin_promo_invalid_values": "Неверные значения. {error}", - "admin_promo_invalid_format_general": "Ошибка парсинга деталей промокода. Проверьте формат.", "admin_promo_created_success": "✅ Промокод {code} успешно создан!\nБонус: {bonus_days} дней\nМакс. активаций: {max_activations}\nДействителен: {valid_until_str}", "admin_promo_set_validity_days": "⏰ Установить срок (дни)", "admin_back_to_panel": "⬅️ В панель", @@ -189,18 +139,11 @@ "admin_queue_status_button": "📊 Статус очередей", "admin_queue_status_title": "📊 Статус очередей сообщений", "admin_queue_status_info": "📤 Очереди сообщений:\n\n👥 Пользователи (25 сообщ/сек):\n 📋 В очереди: {user_queue_size}\n 🔄 Обрабатывается: {user_processing}\n 📈 Отправлено за минуту: {user_recent}\n\n📢 Группы/каналы (15 сообщ/мин):\n 📋 В очереди: {group_queue_size}\n 🔄 Обрабатывается: {group_processing}\n 📈 Отправлено за минуту: {group_recent}", - "admin_promo_creation_failed_duplicate": "❌ Ошибка: Промокод {code} уже существует.", - "admin_promo_creation_failed": "❌ Не удалось создать промокод. Пожалуйста, попробуйте позже.", "admin_active_promos_list_header": "Активные промокоды:", "admin_no_active_promos": "Нет активных промокодов.", - "admin_promo_list_item": "{code}: +{bonus}дн, {current}/{max} акт., до {valid_until}", "admin_promo_valid_indefinitely": "бессрочно", - "admin_promo_valid_until_display": "до {date}", - "admin_manage_promos_title": "Управление промокодами", "admin_promo_edit_button": "✏️ Изменить", "admin_promo_delete_button": "🗑 Удалить", - "admin_promo_edit_prompt": "Отправьте новые данные для {code} в формате: КОД ДНИ_БОНУСА МАКС_АКТИВАЦИЙ [СРОК]", - "admin_promo_updated_success": "Промокод {code} обновлен.", "admin_promo_deleted_success": "Промокод {code} удален.", "admin_promo_not_found": "Промокод не найден.", "admin_promo_export_csv_button": "📄 Экспорт в CSV", @@ -214,86 +157,36 @@ "admin_promo_edit_bonus_days": "🎁 Бонусные дни", "admin_promo_edit_max_activations": "🔢 Макс. активации", "admin_promo_edit_validity": "⏰ Срок действия", - "admin_ban_user_prompt": "Введите ID или @username пользователя для блокировки:", - "admin_user_not_found_in_bot_db": "Пользователь {user_id} не найден в базе данных бота.", - "admin_cannot_ban_self_or_admin": "Вы не можете заблокировать себя или другого администратора.", "admin_user_already_banned": "Пользователь {user_id_or_username} уже заблокирован.", - "admin_user_banned_success_combined": "✅ Пользователь {user_id_or_username} успешно заблокирован в боте {panel_status_part}.", - "admin_panel_ban_success_part": "и на панели", - "admin_panel_ban_fail_part": ", но не удалось заблокировать на панели", - "admin_user_ban_failed_local_db_error": "❌ Не удалось заблокировать пользователя в локальной БД.", "admin_unban_user_prompt": "Введите ID или @username пользователя для разблокировки:", - "admin_user_not_banned": "Пользователь {user_id_or_username} не заблокирован.", - "admin_user_unbanned_success_combined": "✅ Пользователь {user_id_or_username} успешно разблокирован в боте {panel_status_part}.", - "admin_panel_unban_success_part": "и на панели", - "admin_panel_unban_fail_part": ", но не удалось разблокировать на панели", - "admin_user_unban_failed_local_db_error": "❌ Не удалось разблокировать пользователя в локальной БД.", - "admin_no_banned_users": "В данный момент нет заблокированных пользователей.", - "admin_banned_list_title": "Список забаненных (стр. {current_page}/{total_pages}):", + "admin_user_not_banned": "⚠️ Пользователь не заблокирован", "admin_banned_user_button_text": "{user_display} (ID: {user_id})", "prev_page_button": "⬅️ Пред.", "next_page_button": "След. ➡️", - "admin_user_card_title": "Карточка пользователя: {user_display}", - "user_card_info": "ID: {user_id}\nUsername: @{username}\nИмя: {first_name} {last_name}\nЯзык: {language_code}\nPanel UUID: {panel_user_uuid}\nСтатус: {ban_status}\nРегистрация: {reg_date}\nПодписка до: {sub_end_date}", - "user_card_banned": "ЗАБЛОКИРОВАН", - "user_card_active": "Активен", - "user_card_sub_na": "н/д", + "admin_user_card_title": "Карточка пользователя", "user_card_ban_button": "🚫 Заблокировать", "user_card_unban_button": "✅ Разблокировать", "user_card_back_to_banned_list_button": "⬅️ К списку забаненных", - "admin_confirm_action_title": "Подтверждение: {action_text}", - "ban_verb_l": "блокировка", - "unban_verb_l": "разблокировка", - "admin_confirm_ban_prompt": "Вы уверены, что хотите заблокировать пользователя {user_display} (ID: {user_id})?", - "admin_confirm_unban_prompt": "Вы уверены, что хотите разблокировать пользователя {user_display} (ID: {user_id})?", - "admin_user_banned_from_card_alert": "Пользователь {user_display} (ID: {user_id}) заблокирован.", - "admin_user_unbanned_from_card_alert": "Пользователь {user_display} (ID: {user_id}) разблокирован.", - "admin_user_ban_failed_db_error": "Ошибка блокировки пользователя в БД.", - "admin_user_unban_failed_db_error": "Ошибка разблокировки пользователя в БД.", - "admin_panel_status_update_fail_part": ", но статус на панели не обновлен", - "admin_logs_menu_title": "Меню логов:", "admin_view_all_logs_button": "📜 Все логи сообщений", "admin_view_user_logs_prompt_button": "👤 Логи пользователя", "admin_export_logs_csv_button": "📄 Экспорт в CSV", - "admin_all_logs_title": "Все логи (стр. {current_page}/{total_pages}):", "admin_no_logs_found": "Логи не найдены.", "admin_log_entry_format": "{timestamp_str} - {user_display} (ID: {user_id})\n {event_type}: {content_preview}", - "system_or_unknown_user": "Система/Неизв.", "admin_prompt_for_user_id_or_username_logs": "Введите ID или @username пользователя для просмотра его логов:", "admin_log_user_not_found": "Пользователь по запросу \"{input}\" не найден в базе данных бота.", - "admin_user_logs_title": "Логи пользователя {user_display} (стр. {current_page}/{total_pages}):", - "sync_started_simple": "🔄 Начинаю синхронизацию...", "sync_success_simple": "✅ Синхронизация успешно завершена", "sync_failed_simple": "❌ Синхронизация завершилась с ошибкой", "sync_errors_simple": "⚠️ Синхронизация завершена с ошибками ({errors_count} ошибок)", "sync_critical_error": "❌ Критическая ошибка синхронизации", - "no_errors_placeholder": "нет", "admin_sync_initiated_from_panel": "Синхронизация запущена...", "admin_broadcast_invalid_html": "❌ Некорректный HTML в сообщении. Пожалуйста, отправьте корректный HTML (поддерживаются теги Telegram) или уберите теги.", - "admin_panel_user_creation_failed": "❌ Не удалось создать пользователя на панели для TG ID {user_id}. Панель недоступна?", "error_displaying_logs_too_long": "Ошибка: логи слишком длинные для отображения одним сообщением. Попробуйте найти логи по конкретному пользователю.", "error_displaying_statistics": "Ошибка отображения статистики.", - "stub_page_display": "Страница", - - "subscription_72h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 3 дня — {end_date}.\n\nПродлите её по кнопке ниже.", - "subscription_48h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 2 дня — {end_date}.\n\nПродлите её по кнопке ниже.", - "subscription_24h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 1 день — {end_date}.\n\nПродлите её по кнопке ниже.", - "subscription_expired_notification": "👋 Привет, {user_name}!\n\n⛔ Срок вашей подписки на VPN истек ({end_date}).\n\nПродлите её по кнопке ниже.", - "subscription_expired_yesterday_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекла сутки назад ({end_date}).\n\nПродлите её по кнопке ниже.", - "tribute_subscription_cancelled": "🚨 Подписка отменена\n\nВаша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, после чего подписка будет заблокирована.\n\nДля продления подписки нажмите кнопку ниже.", "tribute_auto_renewal": "🔄 Подписка автоматически продлена\n\nВаша подписка Tribute была автоматически продлена на {months} мес.\nНовая дата окончания: {end_date}", - - "admin_new_trial_notification": "\ud83c\udf21 Пользователь {user_id} активировал пробный период до {end_date}.", - - "admin_promo_activation_notification": "\ud83c\udf81 Пользователь {user_id} активировал промокод {code} (+{bonus_days} дн.)", - - "error_unknown": "Произошла неизвестная ошибка.", - "admin_user_management_prompt": "👤 Управление пользователями\n\nВведите ID пользователя или @username для поиска:", - "admin_user_card_title": "Карточка пользователя", "admin_user_subscription_info": "Информация о подписке:", "admin_user_reset_trial_button": "🔄 Сбросить триал", "admin_user_add_subscription_button": "➕ Добавить дни", @@ -304,7 +197,6 @@ "admin_user_search_new_button": "🔍 Новый поиск", "admin_user_view_all_logs_button": "📋 Все логи", "admin_user_back_to_card_button": "🔙 К карточке", - "admin_user_not_found": "❌ Пользователь не найден: {input}", "admin_user_not_found_action": "Пользователь не найден", "admin_user_card_error": "❌ Ошибка отображения карточки пользователя", @@ -320,7 +212,6 @@ "admin_user_unban_success": "✅ Пользователь {input} разблокирован", "admin_user_ban_error": "❌ Ошибка блокировки пользователя", "admin_user_unban_error": "❌ Ошибка разблокировки пользователя", - "admin_user_not_banned": "⚠️ Пользователь не заблокирован", "admin_user_send_message_prompt": "✉️ Отправка сообщения пользователю {user_id}\n\nВведите текст сообщения:", "admin_user_message_too_long": "❌ Сообщение слишком длинное (максимум 4000 символов)", "admin_user_message_sent_success": "✅ Сообщение отправлено пользователю {user_id}", @@ -328,26 +219,13 @@ "admin_user_no_logs": "📜 У пользователя нет действий", "admin_user_logs_error": "❌ Ошибка загрузки действий пользователя", "admin_direct_message_signature": "\n\n---\n💬 Сообщение от администратора", - - "inline_user_stats_message": "📊 Статистика Бота\n👥 Пользователи\n\n📊 Всего: {total}\n💳 С платной подпиской: {paid}\n🆓 На пробном периоде: {trial}\n😴 Неактивных: {inactive}\n🚫 Заблокированных: {banned}\n🎁 Привлечено по реферальной программе: {referral}", + "inline_user_stats_message": "📊 Статистика Бота\n👥 Пользователи\n\n📊 Всего: {total}\n📈 Активных сегодня: {active_today}\n💳 С платной подпиской: {paid}\n🆓 На пробном периоде: {trial}\n😴 Неактивных: {inactive}\n🚫 Заблокированных: {banned}\n🎁 Привлечено по реферальной программе: {referral}", "inline_user_stats_description": "Всего: {total}, Платных: {active}", "inline_admin_user_stats_title": "👥 Статистика пользователей", - "inline_admin_user_stats_desc": "Всего: {total}, Активных: {paid}", - "inline_financial_stats_message": "💰 Финансовая статистика\n\n📅 За сегодня: {today:.2f} RUB\n ({today_count} платежей)\n📅 За неделю: {week:.2f} RUB\n📅 За месяц: {month:.2f} RUB\n🏆 За все время: {all_time:.2f} RUB", - "inline_admin_financial_stats_title": "💰 Финансовая статистика", - "inline_admin_financial_stats_desc": "Сегодня: {today:.2f} RUB", - + "inline_admin_financial_stats_title": "💰 Финансовая статистика", "inline_system_stats_message": "🖥 Статистика панели\n\n🟢 Онлайн: {online}\n📊 Активных: {active}\n🔴 Отключенных: {disabled}\n⏰ Истекшие: {expired}\n⚠️ Ограниченные: {limited}\n👥 Всего пользователей: {total}\n💾 Использование RAM: {memory:.1f}%\n📊 Трафик за неделю: {week_traffic}\n📊 Трафик за месяц: {month_traffic}\n🔗 Активных нод: {active_nodes}/{total_nodes}", "inline_admin_system_stats_title": "🖥 Системная статистика", - "inline_admin_system_stats_desc": "Онлайн: {online}, Офлайн: {offline}", - - "inline_admin_help_message": "🤖 Inline режим бота\n\n📱 Доступные команды:\n\n🎁 реф/ref - поделиться реферальной ссылкой\n👥 стат/stat - статистика пользователей\n💰 финансы - финансовая статистика\n🖥 система - системная статистика\n\n💡 Просто напишите @{bot_username} и начните вводить команду в любом чате!", - - "inline_user_help_message": "🤖 Inline режим бота\n\n📱 Доступные команды:\n\n🎁 реф/ref - поделиться реферальной ссылкой\n\n💡 Просто напишите @{bot_username} и начните вводить 'реф' в любом чате!", - "inline_user_help_title": "🤖 Inline помощь", - "inline_user_help_desc": "Доступна команда: реф (реферальная ссылка)", - "log_referral_suffix": " (реферал от {referrer_id})", "log_new_user_registration": "👤 Новый пользователь\n\n🆔 ID: {user_id}\n👤 Имя: {user_display}{referral_text}\n📅 Время: {timestamp}", "log_payment_received": "{provider_emoji} Получен платеж\n\n👤 Пользователь: {user_display}\n💰 Сумма: {amount} {currency}\n📅 Период: {months} мес.\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}", @@ -355,28 +233,8 @@ "log_trial_activation": "🆓 Активирован триал\n\n👤 Пользователь: {user_display}\n⏰ Действует до: {end_date}\n🕐 Время: {timestamp}", "log_panel_sync": "{status_emoji} Синхронизация с панелью\n\n📊 Статус: {status}\n👥 Обработано пользователей: {users_processed}\n📋 Синхронизировано подписок: {subs_synced}\n🕐 Время: {timestamp}\n\n📝 Детали:\n{details}", "log_suspicious_promo": "⚠️ Подозрительная попытка ввода промокода\n\n👤 Пользователь: {user_display}\n🆔 ID: {user_id}\n📝 Ввод:
{suspicious_input}
\n🕐 Время: {timestamp}", - - "admin_general_cancel_operation": "Операция отменена ❌", - "admin_promo_codes_menu_bulk_create": "📦 Массовое создание", - "admin_bulk_promo_create_prompt": "📦 Массовое создание промокодов\n\nВведите данные в формате:\nколичество|дни|описание\n\nПример:\n10|7|Промо на неделю", - "admin_bulk_promo_invalid_format": "❌ Неверный формат. Используйте: количество|дни|описание", - "admin_bulk_promo_created_success": "✅ Создано {count} промокодов на {days} дней!", - "admin_bulk_promo_codes_file": "📄 Все созданные промокоды в файле ({count} шт.)", - - "admin_promo_edit_step1_code": "✏️ Редактирование промокода\n\nШаг 1 из 4: Код промокода\n\nТекущий код: {current_code}\n\nВведите новый код промокода (3-30 символов, только буквы и цифры) или отправьте текущий для сохранения:", - "admin_promo_edit_step2_bonus_days": "✏️ Редактирование промокода\n\nШаг 2 из 4: Бонусные дни\n\nКод: {code}\nТекущие дни: {current_days}\n\nВведите количество бонусных дней (1-365):", - "admin_promo_edit_step3_max_activations": "✏️ Редактирование промокода\n\nШаг 3 из 4: Максимальные активации\n\nКод: {code}\nДни: {days}\nТекущие активации: {current_max}\n\nВведите максимальное количество активаций (1-10000):", - "admin_promo_edit_step4_validity": "✏️ Редактирование промокода\n\nШаг 4 из 4: Срок действия\n\nКод: {code}\nДни: {days}\nАктивации: {max_act}\nТекущий срок: {current_validity}\n\nВыберите срок действия промокода:", - "admin_promo_edit_unlimited_validity": "♾️ Неограниченно", - "admin_promo_edit_set_validity": "⏰ Установить срок", - "admin_promo_edit_enter_validity_days": "⏰ Введите количество дней действия промокода (1-365):", - "admin_promo_updated_success": "✅ Промокод обновлен!\n\n🎟 Код: {code}\n🎁 Бонусные дни: {days}\n🔢 Максимальные активации: {max_act}\n⏰ Действует до: {validity}", - "admin_promo_update_failed": "❌ Ошибка обновления промокода", - - "admin_logs_export_csv": "📄 Экспорт в CSV", "admin_logs_csv_export_started": "📄 Начинаю экспорт логов в CSV...", "admin_logs_csv_export_success": "✅ Логи экспортированы! Файл прикреплен выше.", - "admin_csv_header_log_id": "ID Лога", "admin_csv_header_timestamp": "Время", "admin_csv_header_user_id": "ID Пользователя", @@ -387,44 +245,23 @@ "admin_csv_header_is_admin_event": "Админ событие", "admin_csv_header_target_user_id": "ID Целевого юзера", "admin_csv_header_raw_update_preview": "Превью обновления", - "admin_banned_users_empty": "📋 Заблокированные пользователи\n\nСписок пуст", "admin_banned_users_list": "📋 Заблокированные пользователи ({count}):\n\n{users}", - "admin_panel_stats_header": "Статистика панели", - "admin_bulk_promo_step1_quantity": "📦 Массовое создание промокодов\n\nШаг 1 из 4: Количество промокодов\n\nВведите количество промокодов для создания (1-1000):", "admin_bulk_promo_step2_bonus_days": "📦 Массовое создание промокодов\n\nШаг 2 из 4: Бонусные дни\n\nКоличество: {quantity}\n\nВведите количество бонусных дней (1-365):", "admin_bulk_promo_step3_max_activations": "📦 Массовое создание промокодов\n\nШаг 3 из 4: Максимальные активации\n\nКоличество: {quantity}\nБонусные дни: {bonus_days}\n\nВведите максимальное количество активаций для каждого промокода (1-10000):", "admin_bulk_promo_step4_validity": "📦 Массовое создание промокодов\n\nШаг 4 из 4: Срок действия\n\nКоличество: {quantity}\nБонусные дни: {bonus_days}\nМакс. активации: {max_activations}\n\nВыберите срок действия промокодов:", - "admin_bulk_promo_invalid_quantity": "❌ Количество должно быть от 1 до 1000", - "admin_bulk_promo_invalid_bonus_days": "❌ Бонусные дни должны быть от 1 до 365", - "admin_bulk_promo_invalid_max_activations": "❌ Максимальные активации должны быть от 1 до 10000", - - "admin_bulk_promo_unlimited_validity": "♾️ Неограниченно", - "admin_bulk_promo_set_validity": "⏰ Установить срок", "admin_bulk_promo_enter_validity_days": "⏰ Введите количество дней действия промокодов (1-365):", - "admin_bulk_promo_creating": "⏳ Создаю {quantity} промокодов...", - "admin_promo_step1_code": "🎟 Создание промокода\n\nШаг 1 из 4: Код промокода\n\nВведите код промокода (3-30 символов, только буквы и цифры):", "admin_promo_step2_bonus_days": "🎟 Создание промокода\n\nШаг 2 из 4: Бонусные дни\n\nКод: {code}\n\nВведите количество бонусных дней (1-365):", "admin_promo_step3_max_activations": "🎟 Создание промокода\n\nШаг 3 из 4: Максимальные активации\n\nКод: {code}\nБонусные дни: {bonus_days}\n\nВведите максимальное количество активаций (1-10000):", "admin_promo_step4_validity": "🎟 Создание промокода\n\nШаг 4 из 4: Срок действия\n\nКод: {code}\nБонусные дни: {bonus_days}\nМакс. активации: {max_activations}\n\nВыберите срок действия промокода:", - "admin_promo_code_already_exists": "❌ Промокод с таким кодом уже существует", - "admin_promo_processing_error": "❌ Ошибка обработки промокода", "admin_promo_unlimited_validity": "♾️ Неограниченно", - "admin_promo_set_validity": "⏰ Установить срок", "admin_promo_enter_validity_days": "⏰ Введите количество дней действия промокода (1-365):", - "admin_promo_skip_description": "⏭️ Пропустить", - "admin_promo_description_too_long": "❌ Описание не должно превышать 200 символов", - "admin_promo_creating": "⏳ Создаю промокод...", - - "admin_panel_back_button": "⬅️ Назад", - "admin_bulk_promo_creation_failed": "❌ Ошибка создания промокодов: {error}", - "admin_user_id_label": "🆔 ID:", "admin_user_name_label": "👤 Имя:", "admin_user_username_label": "📱 Username:", @@ -437,7 +274,6 @@ "admin_user_traffic_label": "📊 Трафик:", "admin_user_subscription_label": "💼 Подписка:", "admin_user_trial_label": "🏡 Триал:", - "admin_user_status_banned": "🚫 Заблокирован", "admin_user_status_active": "✅ Активен", "admin_user_trial_used": "Использовал", @@ -445,21 +281,12 @@ "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_count": "Всего записей: {count}", - "admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}", - "admin_user_actions_count_label": "📜 Всего действий:", "admin_user_subscription_active_until": "⏰ Действует до:", "admin_user_subscription_error": "Ошибка загрузки", - - "admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод", - "admin_promo_management_button": "🎟 Управление промокодами", + "admin_promo_management_title": "🎟 Управление промокодами\n\nВыберите промокод для детального просмотра:", "admin_promo_management_empty": "📭 Промокоды отсутствуют", "admin_promo_card_title": "🎟 Промокод: {code}", @@ -479,43 +306,28 @@ "admin_promo_view_activations_button": "📋 Активации", "admin_promo_back_to_list_button": "⬅️ К списку", "admin_promo_toggle_success": "✅ Промокод {code} {status}", - "admin_promo_toggle_failed": "❌ Ошибка изменения статуса промокода", "admin_promo_no_activations": "📋 Активации промокода: {code}\n\n❌ Активаций не найдено", "admin_promo_activations_header": "📋 Активации промокода: {code}\n\n", "admin_promo_activation_item": "👤 User ID: {user_id}\n📅 Дата: {date}\n", "admin_promo_back_to_detail_button": "⬅️ К промокоду", - "admin_panel_online_label": "Онлайн", "admin_panel_active_label": "Активных", "admin_panel_disabled_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": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\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}, 📊 Активных: {active}", - "inline_admin_help_title": "Помощь по командам", - "inline_admin_help_description": "Доступны команды: реф, стат, финансы, система", - "admin_user_stats_total_label": "Всего", "admin_user_stats_paid_subs_label": "С платной подпиской", "admin_user_stats_trial_label": "На пробном периоде", @@ -527,9 +339,8 @@ "admin_financial_month_label": "За месяц", "admin_financial_all_time_label": "За все время", "admin_financial_payments_label": "платежей", - "admin_sync_details": "📊 Статистика синхронизации:\n🔍 Проверено записей панели: {panel_records_checked}\n👥 Найдено пользователей в БД: {users_found_in_db}\n✨ Создано новых пользователей: {users_created}\n🔄 Пользователей обновлено: {users_updated}\n📋 Подписок синхронизировано: {subscriptions_synced_count}\n ├── Создано новых: {subscriptions_created}\n └── Обновлено существующих: {subscriptions_updated}{additional_stats}", "admin_sync_no_telegram_id": "\n⚠️ Записей без telegramId: {count}", "admin_sync_not_found_in_db": "\n❌ Не найдено в БД: {count}", - "admin_sync_errors": "\n🚫 Ошибок: {count}" + "admin_payments_pagination_info": "📊 Показано {shown} из {total} платежей (стр. {current_page}/{total_pages})" } From 97268b990619076eb2651892c8430e5c723dc6e7 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Thu, 28 Aug 2025 21:41:40 +0300 Subject: [PATCH 03/13] Add subscription expiration notifications in English and Russian - Introduced new localized messages for notifying users about upcoming subscription expirations at 72, 48, and 24 hours. - Added notifications for expired subscriptions and a message for cancelled Tribute subscriptions, enhancing user communication regarding their subscription status. --- locales/en.json | 6 ++++++ locales/ru.json | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/locales/en.json b/locales/en.json index 1fc4937..febae54 100644 --- a/locales/en.json +++ b/locales/en.json @@ -185,6 +185,12 @@ "admin_broadcast_invalid_html": "❌ Invalid HTML in message. Please send valid HTML (Telegram-supported tags) or remove tags.", "error_displaying_logs_too_long": "Error: logs too long to display in one message. Try viewing logs for a specific user.", "error_displaying_statistics": "Error displaying statistics.", + "subscription_72h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 3 days — {end_date}.\n\nPlease renew it using the button below.", + "subscription_48h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 2 days — {end_date}.\n\nPlease renew it using the button below.", + "subscription_24h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 1 day — {end_date}.\n\nPlease renew it using the button below.", + "subscription_expired_notification": "👋 Hi, {user_name}!\n\n⛔ Your VPN subscription expired on {end_date}.\n\nPlease renew it using the button below.", + "subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.", + "tribute_subscription_cancelled": "🚨 Subscription Cancelled\n\nYour Tribute subscription has been cancelled. You have 24 hours to restore access, after which the subscription will be blocked.\n\nTo renew your subscription, press the button below.", "tribute_auto_renewal": "🔄 Subscription Auto-Renewed\n\nYour Tribute subscription has been automatically renewed for {months} months.\nNew expiration date: {end_date}", "admin_user_management_prompt": "👤 User Management\n\nEnter user ID or @username to search:", "admin_user_subscription_info": "Subscription Information:", diff --git a/locales/ru.json b/locales/ru.json index caf4bc8..51d3d6a 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -129,6 +129,12 @@ "admin_promo_invalid_max_activations": "Максимальное количество активаций должно быть положительным числом.", "admin_promo_invalid_validity_days": "Срок действия промокода (в днях) должен быть положительным числом.", "admin_promo_created_success": "✅ Промокод {code} успешно создан!\nБонус: {bonus_days} дней\nМакс. активаций: {max_activations}\nДействителен: {valid_until_str}", + "subscription_72h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 3 дня — {end_date}.\n\nПродлите её по кнопке ниже.", + "subscription_48h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 2 дня — {end_date}.\n\nПродлите её по кнопке ниже.", + "subscription_24h_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекает через 1 день — {end_date}.\n\nПродлите её по кнопке ниже.", + "subscription_expired_notification": "👋 Привет, {user_name}!\n\n⛔ Срок вашей подписки на VPN истек ({end_date}).\n\nПродлите её по кнопке ниже.", + "subscription_expired_yesterday_notification": "👋 Привет, {user_name}!\n\n⏳ Ваша подписка на VPN истекла сутки назад ({end_date}).\n\nПродлите её по кнопке ниже.", + "tribute_subscription_cancelled": "🚨 Подписка отменена\n\nВаша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, после чего подписка будет заблокирована.\n\nДля продления подписки нажмите кнопку ниже.", "admin_promo_set_validity_days": "⏰ Установить срок (дни)", "admin_back_to_panel": "⬅️ В панель", "admin_promo_unlimited": "♾️ Неограниченно", From b576d04501f5749948938e9c26a478bb6461d2ef Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Thu, 28 Aug 2025 21:46:59 +0300 Subject: [PATCH 04/13] Add welcome messages in English and Russian localization - Introduced a new "welcome" message for users in both English and Russian, enhancing the user experience with personalized greetings. --- locales/en.json | 1 + locales/ru.json | 1 + 2 files changed, 2 insertions(+) diff --git a/locales/en.json b/locales/en.json index febae54..79aed63 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,4 +1,5 @@ { + "welcome": "Welcome, {user_name}!", "main_menu_greeting": "Hi, {user_name}! 👋\nWhat would you like to do?", "menu_activate_trial_button": "🆓 Free Trial", diff --git a/locales/ru.json b/locales/ru.json index 51d3d6a..f8dd0b4 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,4 +1,5 @@ { + "welcome": "Добро пожаловать, {user_name}!", "main_menu_greeting": "Привет, {user_name}! 👋\nЧто бы вы хотели сделать?", "menu_activate_trial_button": "🆓 Пробный период", From 85bf0fb6fdc45c617890bbcc676b2b353801efb4 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Thu, 28 Aug 2025 21:52:36 +0300 Subject: [PATCH 05/13] Enhance promo export functionality with English localization - Added support for exporting promo activations and all promo codes with captions and CSV headers in English. - Updated the promo export handlers to ensure consistent English messaging for user notifications and CSV content. - Introduced new localization entries for English in the locales files to support the changes. --- bot/handlers/admin/promo/manage.py | 33 +++++++++++++++++++++--------- locales/en.json | 13 ++++++++++++ locales/ru.json | 13 ++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/bot/handlers/admin/promo/manage.py b/bot/handlers/admin/promo/manage.py index 46198b5..9b60921 100644 --- a/bot/handlers/admin/promo/manage.py +++ b/bot/handlers/admin/promo/manage.py @@ -248,6 +248,7 @@ async def promo_export_activations_handler(callback: types.CallbackQuery, i18n_d if not i18n or not callback.message or not current_lang: return await callback.answer("Error processing request.", show_alert=True) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + export_lang = "en" try: promo_id = int(callback.data.split(":")[1]) @@ -267,7 +268,11 @@ async def promo_export_activations_handler(callback: types.CallbackQuery, i18n_d output.seek(0) file = types.BufferedInputFile(output.getvalue().encode('utf-8'), filename=f"promo_{promo.code}_activations.csv") - await callback.message.answer_document(file, caption=_("admin_promo_export_caption", code=promo.code)) + # Force English caption for exports + await callback.message.answer_document( + file, + caption=i18n.gettext(export_lang, "admin_promo_export_caption", code=promo.code) + ) except (ValueError, IndexError): await callback.answer(_("admin_promo_not_found"), show_alert=True) @@ -281,9 +286,10 @@ async def promo_export_all_handler(callback: types.CallbackQuery, i18n_data: dic if not i18n or not callback.message or not current_lang: return await callback.answer("Error processing request.", show_alert=True) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) + export_lang = "en" try: - await callback.answer("📄 Создаю CSV файл...", show_alert=True) + await callback.answer(i18n.gettext(export_lang, "admin_promo_export_all_generating"), show_alert=True) # Получаем все промокоды all_promos = await promo_code_dal.get_all_promo_codes_with_details(session, limit=10000, offset=0) @@ -291,15 +297,22 @@ async def promo_export_all_handler(callback: types.CallbackQuery, i18n_data: dic output = io.StringIO() writer = csv.writer(output) - # Заголовки CSV + # CSV headers (forced to English) writer.writerow([ - "Код", "Бонусные дни", "Максимальные активации", "Текущие активации", - "Статус", "Активен", "Действителен до", "Создан", "Создал (Admin ID)" + i18n.gettext(export_lang, "admin_promo_csv_code"), + i18n.gettext(export_lang, "admin_promo_csv_bonus_days"), + i18n.gettext(export_lang, "admin_promo_csv_max_activations"), + i18n.gettext(export_lang, "admin_promo_csv_current_activations"), + i18n.gettext(export_lang, "admin_promo_csv_status"), + i18n.gettext(export_lang, "admin_promo_csv_is_active"), + i18n.gettext(export_lang, "admin_promo_csv_valid_until"), + i18n.gettext(export_lang, "admin_promo_csv_created_at"), + i18n.gettext(export_lang, "admin_promo_csv_created_by_admin_id"), ]) for promo in all_promos: # Определяем статус - status_emoji, status_text = get_promo_status_emoji_and_text(promo, i18n, current_lang) + status_emoji, status_text = get_promo_status_emoji_and_text(promo, i18n, export_lang) # Формируем данные для CSV row = [ @@ -308,8 +321,8 @@ async def promo_export_all_handler(callback: types.CallbackQuery, i18n_data: dic promo.max_activations, promo.current_activations, status_text, - "Да" if promo.is_active else "Нет", - promo.valid_until.strftime("%Y-%m-%d %H:%M:%S") if promo.valid_until else "Без ограничений", + i18n.gettext(export_lang, "csv_yes") if promo.is_active else i18n.gettext(export_lang, "csv_no"), + promo.valid_until.strftime("%Y-%m-%d %H:%M:%S") if promo.valid_until else i18n.gettext(export_lang, "admin_promo_valid_indefinitely"), promo.created_at.strftime("%Y-%m-%d %H:%M:%S") if promo.created_at else "N/A", promo.created_by_admin_id or "N/A" ] @@ -324,11 +337,11 @@ async def promo_export_all_handler(callback: types.CallbackQuery, i18n_data: dic filename=filename ) - caption = f"📄 Экспорт всех промокодов\n📊 Всего: {len(all_promos)} промокодов" + caption = i18n.gettext(export_lang, "admin_promo_export_all_caption", count=len(all_promos)) await callback.message.answer_document(file, caption=caption) except Exception as e: - await callback.answer(f"❌ Ошибка экспорта: {str(e)}", show_alert=True) + await callback.answer(f"❌ Export error: {str(e)}", show_alert=True) @router.callback_query(F.data.startswith("promo_delete:")) diff --git a/locales/en.json b/locales/en.json index 79aed63..41e5b5b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -149,6 +149,19 @@ "admin_promo_not_found": "Promo not found.", "admin_promo_export_csv_button": "📄 Export to CSV", "admin_promo_export_caption": "📄 Activations for promo code {code}", + "admin_promo_export_all_generating": "📄 Generating CSV...", + "admin_promo_export_all_caption": "📄 Export of all promo codes\n📊 Total: {count} promo codes", + "admin_promo_csv_code": "Code", + "admin_promo_csv_bonus_days": "Bonus Days", + "admin_promo_csv_max_activations": "Max Activations", + "admin_promo_csv_current_activations": "Current Activations", + "admin_promo_csv_status": "Status", + "admin_promo_csv_is_active": "Active", + "admin_promo_csv_valid_until": "Valid Until", + "admin_promo_csv_created_at": "Created", + "admin_promo_csv_created_by_admin_id": "Created By (Admin ID)", + "csv_yes": "Yes", + "csv_no": "No", "admin_promo_edit_select_field": "Select a field to edit:", "admin_promo_prompt_bonus_days": "Enter the new number of bonus days:", "admin_promo_prompt_max_activations": "Enter the new maximum number of activations:", diff --git a/locales/ru.json b/locales/ru.json index f8dd0b4..e790e7e 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -155,6 +155,19 @@ "admin_promo_not_found": "Промокод не найден.", "admin_promo_export_csv_button": "📄 Экспорт в CSV", "admin_promo_export_caption": "📄 Активации промокода {code}", + "admin_promo_export_all_generating": "📄 Создаю CSV файл...", + "admin_promo_export_all_caption": "📄 Экспорт всех промокодов\n📊 Всего: {count} промокодов", + "admin_promo_csv_code": "Код", + "admin_promo_csv_bonus_days": "Бонусные дни", + "admin_promo_csv_max_activations": "Максимальные активации", + "admin_promo_csv_current_activations": "Текущие активации", + "admin_promo_csv_status": "Статус", + "admin_promo_csv_is_active": "Активен", + "admin_promo_csv_valid_until": "Действителен до", + "admin_promo_csv_created_at": "Создан", + "admin_promo_csv_created_by_admin_id": "Создал (Admin ID)", + "csv_yes": "Да", + "csv_no": "Нет", "admin_promo_edit_select_field": "Выберите поле для редактирования:", "admin_promo_prompt_bonus_days": "Введите новое количество бонусных дней:", "admin_promo_prompt_max_activations": "Введите новое максимальное количество активаций:", From 24cf193f3cfe6928a75c8c7727dbf5596910f7ab Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Thu, 28 Aug 2025 22:03:48 +0300 Subject: [PATCH 06/13] Refactor broadcast and user logs messages for localization support - Updated the broadcast confirmation message to utilize localization for dynamic content, enhancing multilingual support. - Modified user logs display to incorporate localization for the title, improving consistency across languages. - Added new localization entries in both English and Russian for better user experience and clarity. --- bot/handlers/admin/broadcast.py | 27 ++++++++++++++++++--------- bot/handlers/admin/user_management.py | 4 +++- locales/en.json | 23 ++++++++++++++--------- locales/ru.json | 4 ++++ 4 files changed, 39 insertions(+), 19 deletions(-) diff --git a/bot/handlers/admin/broadcast.py b/bot/handlers/admin/broadcast.py index b2da070..ce3821d 100644 --- a/bot/handlers/admin/broadcast.py +++ b/bot/handlers/admin/broadcast.py @@ -337,15 +337,24 @@ async def confirm_broadcast_callback_handler( # Get queue stats for detailed report queue_stats = queue_manager.get_queue_stats() - result_message = f"""🚀 Рассылка поставлена в очередь! -📤 В очередь добавлено: {sent_count} -❌ Ошибок: {failed_count} - -📊 Статус очередей: -👥 Очередь пользователей: {queue_stats['user_queue_size']} сообщений -📢 Очередь групп: {queue_stats['group_queue_size']} сообщений - -ℹ️ Сообщения будут отправлены автоматически с соблюдением лимитов Telegram.""" + result_message = ( + _( + "broadcast_queue_result", + default=( + "🚀 Рассылка поставлена в очередь!\n" + "📤 В очередь добавлено: {sent_count}\n" + "❌ Ошибок: {failed_count}\n\n" + "📊 Статус очередей:\n" + "👥 Очередь пользователей: {user_queue_size} сообщений\n" + "📢 Очередь групп: {group_queue_size} сообщений\n\n" + "ℹ️ Сообщения будут отправлены автоматически с соблюдением лимитов Telegram." + ), + sent_count=sent_count, + failed_count=failed_count, + user_queue_size=queue_stats["user_queue_size"], + group_queue_size=queue_stats["group_queue_size"], + ) + ) await callback.message.answer( result_message, reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), diff --git a/bot/handlers/admin/user_management.py b/bot/handlers/admin/user_management.py index 24de068..2cbcadb 100644 --- a/bot/handlers/admin/user_management.py +++ b/bot/handlers/admin/user_management.py @@ -420,7 +420,9 @@ async def handle_view_user_logs(callback: types.CallbackQuery, user: User, ), show_alert=True) return - logs_text_parts = [f"📜 Последние действия пользователя {user.user_id}:\n"] + logs_text_parts = [ + f"{_('admin_user_recent_actions_title', default='📜 Последние действия пользователя {user_id}:', user_id=user.user_id)}\n" + ] for log in logs: timestamp = log.timestamp.strftime('%Y-%m-%d %H:%M') if log.timestamp else 'N/A' diff --git a/locales/en.json b/locales/en.json index 41e5b5b..f29f418 100644 --- a/locales/en.json +++ b/locales/en.json @@ -14,7 +14,7 @@ "back_to_main_menu_button": "⬅️ Back", - "choose_language": "Choose language / Выберите язык:", + "choose_language": "Choose language:", "language_set_alert": "Language changed!", "error_occurred_try_again": "An error occurred, please try again.", @@ -58,6 +58,8 @@ "referral_share_message_button": "📩 Message for friend", "referral_friend_message": "🚀 Hey! Try this VPN - it's fast, reliable and affordable!\n\n🎁 Use my link to get bonus days with your subscription!\n\n{referral_link}", "friend_placeholder": "friend", + "referral_bonus_inviter_notification_extended": "🎉 Congrats! Your friend {referee_name} paid for a subscription. You received {days} bonus days! Your subscription is now active until {new_end_date}.", + "referral_bonus_inviter_notification_new_sub": "🎉 Congrats! Your friend {referee_name} paid for a subscription. You received a {days}-day bonus subscription! It is active until {new_end_date}.", "user_is_banned": "🚫 Your account is banned. Please contact support.", "admin_panel_title": "Admin Panel", @@ -125,19 +127,20 @@ "admin_broadcast_cancelled": "Broadcast cancelled.", "admin_broadcast_cancelled_alert": "Broadcast cancelled!", "admin_broadcast_cancelled_nav_back": "Broadcast cancelled. You are returned to the admin panel.", + "broadcast_queue_result": "🚀 Broadcast queued!\n📤 Enqueued: {sent_count}\n❌ Errors: {failed_count}\n\n📊 Queue Status:\n👥 User queue: {user_queue_size} messages\n📢 Group queue: {group_queue_size} messages\n\nℹ️ Messages will be sent automatically within Telegram limits.", "admin_promo_invalid_code_format": "Code must be 3–30 alphanumeric characters.", "admin_promo_invalid_bonus_days": "Bonus days must be a positive number.", "admin_promo_invalid_max_activations": "Max activations must be a positive number.", "admin_promo_invalid_validity_days": "Validity period (in days) must be a positive number.", "admin_promo_created_success": "✅ Promo code {code} created successfully!\nBonus: {bonus_days} days\nMax uses: {max_activations}\nValid until: {valid_until_str}.", - "admin_promo_set_validity_days": "⏰ Установить срок (дни)", - "admin_back_to_panel": "⬅️ В панель", - "admin_promo_unlimited": "♾️ Неограниченно", - "admin_bulk_promo_created_title": "📦 Массовое создание завершено", - "admin_bulk_promo_created_stats": "📊 Создано: {created} из {total}", - "admin_bulk_promo_settings": "🎁 Бонусные дни: {bonus_days}\n📊 Макс. активаций: {max_activations}\n⏰ Срок действия: {validity}", + "admin_promo_set_validity_days": "⏰ Set validity (days)", + "admin_back_to_panel": "⬅️ Back to panel", + "admin_promo_unlimited": "♾️ Unlimited", + "admin_bulk_promo_created_title": "📦 Bulk creation completed", + "admin_bulk_promo_created_stats": "📊 Created: {created} of {total}", + "admin_bulk_promo_settings": "🎁 Bonus days: {bonus_days}\n📊 Max activations: {max_activations}\n⏰ Validity: {validity}", "admin_promo_list_page_info": "Page {current}/{total} ({count} promo codes)", - "admin_queue_status_button": "📊 Статус очередей", + "admin_queue_status_button": "📊 Queue Status", "admin_queue_status_title": "📊 Message Queue Status", "admin_queue_status_info": "📤 Message Queues:\n\n👥 Users (25 msg/sec):\n 📋 In queue: {user_queue_size}\n 🔄 Processing: {user_processing}\n 📈 Sent per minute: {user_recent}\n\n📢 Groups/channels (15 msg/min):\n 📋 In queue: {group_queue_size}\n 🔄 Processing: {group_processing}\n 📈 Sent per minute: {group_recent}", "admin_active_promos_list_header": "Active Promo Codes:", @@ -240,7 +243,7 @@ "admin_user_logs_error": "❌ Error loading user actions", "admin_direct_message_signature": "\n\n---\n💬 Message from administrator", "inline_user_stats_message": "👥 User Statistics\n\n📊 Total: {total}\n📈 Active today: {active_today}\n💳 With paid subscription: {paid}\n🆓 On trial period: {trial}\n😴 Inactive: {inactive}\n🚫 Banned: {banned}\n🎁 Via referral program: {referral}", - "inline_user_stats_description": "Всего: {total}, Платных: {active}", + "inline_user_stats_description": "Total: {total}, Paid: {active}", "inline_admin_user_stats_title": "👥 User Statistics", "inline_financial_stats_message": "💰 Financial Statistics\n\n📅 Today: {today:.2f} RUB\n ({today_count} payments)\n📅 Week: {week:.2f} RUB\n📅 Month: {month:.2f} RUB\n🏆 All time: {all_time:.2f} RUB", "inline_admin_financial_stats_title": "💰 Financial Statistics", @@ -255,6 +258,8 @@ "log_suspicious_promo": "⚠️ Suspicious Promo Code Attempt\n\n👤 User: {user_display}\n🆔 ID: {user_id}\n📝 Input:
{suspicious_input}
\n🕐 Time: {timestamp}", "admin_logs_csv_export_started": "📄 Starting log export to CSV...", "admin_logs_csv_export_success": "✅ Logs exported! File attached above.", + "admin_user_logs_title": "Logs for {user_display} (page {current_page}/{total_pages}):", + "admin_all_logs_title": "All Logs (page {current_page}/{total_pages}):", "admin_csv_header_log_id": "Log ID", "admin_csv_header_timestamp": "Timestamp", "admin_csv_header_user_id": "User ID", diff --git a/locales/ru.json b/locales/ru.json index e790e7e..a43cc03 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -58,6 +58,8 @@ "referral_share_message_button": "📩 Сообщение для друга", "referral_friend_message": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}", "friend_placeholder": "друг", + "referral_bonus_inviter_notification_extended": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислено {days} бонусных дней! Ваша подписка теперь активна до {new_end_date}.", + "referral_bonus_inviter_notification_new_sub": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислена бонусная подписка на {days} дней! Она активна до {new_end_date}.", "user_is_banned": "🚫 Ваш аккаунт заблокирован. Пожалуйста, свяжитесь со службой поддержки.", "admin_panel_title": "Панель администратора", @@ -255,6 +257,8 @@ "log_suspicious_promo": "⚠️ Подозрительная попытка ввода промокода\n\n👤 Пользователь: {user_display}\n🆔 ID: {user_id}\n📝 Ввод:
{suspicious_input}
\n🕐 Время: {timestamp}", "admin_logs_csv_export_started": "📄 Начинаю экспорт логов в CSV...", "admin_logs_csv_export_success": "✅ Логи экспортированы! Файл прикреплен выше.", + "admin_user_logs_title": "Логи пользователя {user_display} (стр. {current_page}/{total_pages}):", + "admin_all_logs_title": "Все логи (стр. {current_page}/{total_pages}):", "admin_csv_header_log_id": "ID Лога", "admin_csv_header_timestamp": "Время", "admin_csv_header_user_id": "ID Пользователя", From e6b3a7c66faff70067a80cb5f9e813c72ec10333 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Fri, 29 Aug 2025 10:51:40 +0300 Subject: [PATCH 07/13] Add trial activation details in English and Russian localization - Introduced a new message for trial activation details, providing users with information about the duration, validity, and configuration link. - Enhanced user experience by ensuring clarity and consistency in both English and Russian locales. --- locales/en.json | 1 + locales/ru.json | 1 + 2 files changed, 2 insertions(+) diff --git a/locales/en.json b/locales/en.json index f29f418..57690fd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -51,6 +51,7 @@ "trial_already_had_subscription_or_trial": "You have already used a free trial or had a paid subscription. Free trial is available only once for new users.", "trial_confirm_activate_button": "✅ Activate!", "trial_activated_alert": "Trial activated!", + "trial_activated_details_message": "🚀 Your {days}-day free trial is activated!\n\nValid until: {end_date}\nTraffic: {traffic_gb} GB\n\nYour configuration link:\n{config_link}", "yes_button": "Yes", "no_button": "No", "referral_program_info_new": "🎁 Referral Program\n\n📊 Your stats:\n👥 Friends invited: {invited_count}\n💳 Purchased subscription: {purchased_count}\n\n🔗 Your link:\n{referral_link}\n\n💰 Invitation bonuses:\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!", diff --git a/locales/ru.json b/locales/ru.json index a43cc03..c1049e9 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -51,6 +51,7 @@ "trial_already_had_subscription_or_trial": "Вы уже использовали пробный период или у вас была платная подписка. Пробный период доступен только один раз для новых пользователей.", "trial_confirm_activate_button": "✅ Активировать!", "trial_activated_alert": "Пробный период активирован!", + "trial_activated_details_message": "🚀 Ваш пробный период на {days} дней активирован!\n\nДействует до: {end_date}\nТрафик: {traffic_gb} GB\n\nВаша ссылка на конфигурацию:\n{config_link}", "yes_button": "Да", "no_button": "Нет", "referral_program_info_new": "🎁 Реферальная программа\n\n📊 Твоя статистика:\n👥 Приглашено друзей: {invited_count}\n💳 Купили подписку: {purchased_count}\n\n🔗 Твоя ссылка:\n{referral_link}\n\n💰 Бонусы за приглашения:\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!", From f4730c3cebedd503bd5ec06c479a73bb8085fab4 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Fri, 29 Aug 2025 11:05:21 +0300 Subject: [PATCH 08/13] Enhance user description synchronization with Telegram profile - Added functionality to update user descriptions on the panel during synchronization, incorporating Telegram fields such as username, first name, and last name. - Implemented error handling and logging for failed description updates in both admin sync and profile sync processes. - Updated user creation and details update methods in the subscription service to include user descriptions, improving consistency across user data management. --- bot/handlers/admin/sync_admin.py | 17 ++++++++++++ bot/middlewares/profile_sync.py | 18 ++++++++++++ bot/services/subscription_service.py | 41 +++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/bot/handlers/admin/sync_admin.py b/bot/handlers/admin/sync_admin.py index 5525146..1428fe2 100644 --- a/bot/handlers/admin/sync_admin.py +++ b/bot/handlers/admin/sync_admin.py @@ -136,6 +136,23 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession, users_uuid_updated += 1 logging.info(f"Updated panel UUID for user {actual_user_id}: {panel_uuid}") + # Ensure panel description contains Telegram fields + try: + if panel_uuid and existing_user: + description_text = "\n".join([ + existing_user.username or "", + existing_user.first_name or "", + existing_user.last_name or "", + ]) + if description_text.strip(): + await panel_service.update_user_details_on_panel( + panel_uuid, {"description": description_text} + ) + except Exception as e_desc: + logging.warning( + f"Sync: Failed to update description for panel user {panel_uuid} (tg {actual_user_id}): {e_desc}" + ) + # Sync subscription data panel_expire_at_iso = panel_user_dict.get("expireAt") panel_status = panel_user_dict.get("status", "UNKNOWN") diff --git a/bot/middlewares/profile_sync.py b/bot/middlewares/profile_sync.py index d15cb9c..24d075d 100644 --- a/bot/middlewares/profile_sync.py +++ b/bot/middlewares/profile_sync.py @@ -36,6 +36,24 @@ class ProfileSyncMiddleware(BaseMiddleware): logging.info( f"ProfileSyncMiddleware: Updated user {tg_user.id} profile fields: {list(update_payload.keys())}" ) + + # Also update description on panel if linked + try: + panel_service = data.get("panel_service") + if panel_service and db_user.panel_user_uuid: + description_text = "\n".join([ + tg_user.username or "", + tg_user.first_name or "", + tg_user.last_name or "", + ]) + await panel_service.update_user_details_on_panel( + db_user.panel_user_uuid, + {"description": description_text}, + ) + except Exception as e_upd_desc: + logging.warning( + f"ProfileSyncMiddleware: Failed to update panel description for user {tg_user.id}: {e_upd_desc}" + ) except Exception as e: logging.error( f"ProfileSyncMiddleware: Failed to sync profile for user {getattr(tg_user, 'id', 'N/A')}: {e}", diff --git a/bot/services/subscription_service.py b/bot/services/subscription_service.py index b349e39..78cf15a 100644 --- a/bot/services/subscription_service.py +++ b/bot/services/subscription_service.py @@ -119,6 +119,11 @@ class SubscriptionService: creation_response = await self.panel_service.create_panel_user( username_on_panel=panel_username_on_panel_standard, telegram_id=user_id, + description="\n".join([ + (db_user.username or "") if db_user else "", + (db_user.first_name or "") if db_user else "", + (db_user.last_name or "") if db_user else "", + ]), specific_squad_uuids=self.settings.parsed_user_squad_uuids, default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes, default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY, @@ -142,6 +147,11 @@ class SubscriptionService: creation_response = await self.panel_service.create_panel_user( username_on_panel=panel_username_on_panel_standard, telegram_id=user_id, + description="\n".join([ + (db_user.username or "") if db_user else "", + (db_user.first_name or "") if db_user else "", + (db_user.last_name or "") if db_user else "", + ]), specific_squad_uuids=self.settings.parsed_user_squad_uuids, default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes, default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY, @@ -258,8 +268,19 @@ class SubscriptionService: logging.info( f"Panel user {current_local_panel_uuid} has telegramId '{panel_telegram_id_from_api}'. Updating on panel to '{user_id}'." ) + # Also set readable description with Telegram fields await self.panel_service.update_user_details_on_panel( - current_local_panel_uuid, {"telegramId": user_id} + current_local_panel_uuid, + { + "telegramId": user_id, + "description": "\n".join( + [ + (db_user.username or "") if db_user else "", + (db_user.first_name or "") if db_user else "", + (db_user.last_name or "") if db_user else "", + ] + ), + }, ) panel_sub_link_id = panel_user_obj_from_api.get( @@ -356,6 +377,15 @@ class SubscriptionService: traffic_limit_bytes=self.settings.trial_traffic_limit_bytes, ) + # Add user description based on Telegram profile + panel_update_payload["description"] = "\n".join( + [ + (db_user.username or "") if db_user else "", + (db_user.first_name or "") if db_user else "", + (db_user.last_name or "") if db_user else "", + ] + ) + updated_panel_user = await self.panel_service.update_user_details_on_panel( panel_user_uuid, panel_update_payload ) @@ -498,6 +528,15 @@ class SubscriptionService: traffic_limit_bytes=self.settings.user_traffic_limit_bytes, ) + # Add user description based on Telegram profile + panel_update_payload["description"] = "\n".join( + [ + (db_user.username or "") if db_user else "", + (db_user.first_name or "") if db_user else "", + (db_user.last_name or "") if db_user else "", + ] + ) + updated_panel_user = await self.panel_service.update_user_details_on_panel( panel_user_uuid, panel_update_payload ) From fb2672732de0283327e6722d477b48197aeb0e30 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Fri, 29 Aug 2025 12:10:00 +0300 Subject: [PATCH 09/13] Improve error handling in user interaction responses - Refactored the handling of callback answers and message responses to include try-except blocks, ensuring that exceptions are caught and logged without disrupting the user experience. - Updated message sending methods to utilize the `answer` method consistently, enhancing the reliability of user notifications in various scenarios. --- bot/handlers/user/start.py | 23 +++++----- bot/handlers/user/trial_handler.py | 70 ++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/bot/handlers/user/start.py b/bot/handlers/user/start.py index 815934b..0c25f17 100644 --- a/bot/handlers/user/start.py +++ b/bot/handlers/user/start.py @@ -43,10 +43,9 @@ async def send_main_menu(target_event: Union[types.Message, await target_event.answer(err_msg_fallback, show_alert=True) except Exception: pass - elif isinstance(target_event, types.Message) and hasattr( - target_event, 'chat') and target_event.chat: + elif isinstance(target_event, types.Message): try: - await target_event.chat.send_message(err_msg_fallback) + await target_event.answer(err_msg_fallback) except Exception: pass return @@ -93,23 +92,27 @@ async def send_main_menu(target_event: Union[types.Message, await target_message_obj.answer(text, reply_markup=reply_markup) if isinstance(target_event, types.CallbackQuery): - await target_event.answer() + try: + await target_event.answer() + except Exception: + pass except Exception as e_send_edit: logging.warning( f"Failed to send/edit main menu (user: {user_id}, is_edit: {is_edit}): {type(e_send_edit).__name__} - {e_send_edit}." ) - if is_edit and target_message_obj and hasattr( - target_message_obj, 'chat') and target_message_obj.chat: + if is_edit and target_message_obj: try: - await target_message_obj.chat.send_message( - text, reply_markup=reply_markup) + await target_message_obj.answer(text, reply_markup=reply_markup) except Exception as e_send_new: logging.error( f"Also failed to send new main menu message for user {user_id}: {e_send_new}" ) if isinstance(target_event, types.CallbackQuery): - await target_event.answer( - _("error_occurred_try_again") if is_edit else None) + try: + await target_event.answer( + _("error_occurred_try_again") if is_edit else None) + except Exception: + pass @router.message(CommandStart()) diff --git a/bot/handlers/user/trial_handler.py b/bot/handlers/user/trial_handler.py index 043fb18..34d2e4f 100644 --- a/bot/handlers/user/trial_handler.py +++ b/bot/handlers/user/trial_handler.py @@ -31,7 +31,10 @@ async def request_trial_confirmation_handler( _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key if not i18n or not callback.message: - await callback.answer(_("error_occurred_try_again"), show_alert=True) + try: + await callback.answer(_("error_occurred_try_again"), show_alert=True) + except Exception: + pass return show_trial_btn_in_menu_if_fail = False @@ -46,7 +49,10 @@ async def request_trial_confirmation_handler( current_lang, i18n, settings, False ), ) - await callback.answer() + try: + await callback.answer() + except Exception: + pass return if await subscription_service.has_had_any_subscription(session, user_id): @@ -56,7 +62,10 @@ async def request_trial_confirmation_handler( current_lang, i18n, settings, False ), ) - await callback.answer() + try: + await callback.answer() + except Exception: + pass return # Directly activate trial without confirmation @@ -68,7 +77,10 @@ async def request_trial_confirmation_handler( show_trial_button_after_action = False if activation_result and activation_result.get("activated"): - await callback.answer(_("trial_activated_alert"), show_alert=True) + try: + await callback.answer(_("trial_activated_alert"), show_alert=True) + except Exception: + pass end_date_obj = activation_result.get("end_date") config_link_for_trial = activation_result.get("subscription_url") or _( @@ -106,7 +118,10 @@ async def request_trial_confirmation_handler( else "trial_activation_failed" ) final_message_text_in_chat = _(message_key_from_service) - await callback.answer(final_message_text_in_chat, show_alert=True) + try: + await callback.answer(final_message_text_in_chat, show_alert=True) + except Exception: + pass if ( settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription( @@ -129,12 +144,8 @@ async def request_trial_confirmation_handler( f"Could not edit trial result message: {e_edit}. Sending new one." ) - if ( - callback.message - and hasattr(callback.message, "chat") - and callback.message.chat - ): - await callback.message.chat.send_message( + if callback.message: + await callback.message.answer( final_message_text_in_chat, parse_mode="HTML", reply_markup=get_main_menu_inline_keyboard( @@ -160,20 +171,29 @@ async def confirm_activate_trial_handler( _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key if not i18n or not callback.message: - await callback.answer(_("error_occurred_try_again"), show_alert=True) + try: + await callback.answer(_("error_occurred_try_again"), show_alert=True) + except Exception: + pass return if not settings.TRIAL_ENABLED: - await callback.answer(_("trial_feature_disabled"), show_alert=True) + try: + await callback.answer(_("trial_feature_disabled"), show_alert=True) + except Exception: + pass await send_main_menu( callback, settings, i18n_data, subscription_service, session, is_edit=True ) return if await subscription_service.has_had_any_subscription(session, user_id): - await callback.answer( - _("trial_already_had_subscription_or_trial"), show_alert=True - ) + try: + await callback.answer( + _("trial_already_had_subscription_or_trial"), show_alert=True + ) + except Exception: + pass await send_main_menu( callback, settings, i18n_data, subscription_service, session, is_edit=True ) @@ -187,7 +207,10 @@ async def confirm_activate_trial_handler( show_trial_button_after_action = False if activation_result and activation_result.get("activated"): - await callback.answer(_("trial_activated_alert"), show_alert=True) + try: + await callback.answer(_("trial_activated_alert"), show_alert=True) + except Exception: + pass end_date_obj = activation_result.get("end_date") config_link_for_trial = activation_result.get("subscription_url") or _( @@ -221,7 +244,10 @@ async def confirm_activate_trial_handler( else "trial_activation_failed" ) final_message_text_in_chat = _(message_key_from_service) - await callback.answer(final_message_text_in_chat, show_alert=True) + try: + await callback.answer(final_message_text_in_chat, show_alert=True) + except Exception: + pass if ( settings.TRIAL_ENABLED and not await subscription_service.has_had_any_subscription( @@ -244,12 +270,8 @@ async def confirm_activate_trial_handler( f"Could not edit trial result message: {e_edit}. Sending new one." ) - if ( - callback.message - and hasattr(callback.message, "chat") - and callback.message.chat - ): - await callback.message.chat.send_message( + if callback.message: + await callback.message.answer( final_message_text_in_chat, parse_mode="HTML", reply_markup=get_main_menu_inline_keyboard( From acf5f060a6a6632f2a975ab130717df1f78d2be5 Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Fri, 29 Aug 2025 12:27:13 +0300 Subject: [PATCH 10/13] Enhance error handling in subscription response methods - Implemented try-except blocks around callback answer methods to prevent exceptions from disrupting user interactions. - Ensured consistent use of the `answer` method across various subscription-related handlers, improving reliability in user notifications. --- bot/handlers/user/subscription.py | 129 +++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 29 deletions(-) diff --git a/bot/handlers/user/subscription.py b/bot/handlers/user/subscription.py index d4be011..433169c 100644 --- a/bot/handlers/user/subscription.py +++ b/bot/handlers/user/subscription.py @@ -35,7 +35,10 @@ async def display_subscription_options(event: Union[types.Message, if not i18n: err_msg = "Language service error." if isinstance(event, types.CallbackQuery): - await event.answer(err_msg, show_alert=True) + try: + await event.answer(err_msg, show_alert=True) + except Exception: + pass elif isinstance(event, types.Message): await event.answer(err_msg) return @@ -54,8 +57,11 @@ async def display_subscription_options(event: Union[types.Message, event, types.CallbackQuery) else event if not target_message_obj: if isinstance(event, types.CallbackQuery): - await event.answer(get_text("error_occurred_try_again"), - show_alert=True) + try: + await event.answer(get_text("error_occurred_try_again"), + show_alert=True) + except Exception: + pass return if isinstance(event, types.CallbackQuery): @@ -65,7 +71,10 @@ async def display_subscription_options(event: Union[types.Message, except Exception: await target_message_obj.answer(text_content, reply_markup=reply_markup) - await event.answer() + try: + await event.answer() + except Exception: + pass else: await target_message_obj.answer(text_content, reply_markup=reply_markup) @@ -81,8 +90,11 @@ async def select_subscription_period_callback_handler( ) if i18n else key if not i18n or not callback.message: - await callback.answer(get_text("error_occurred_try_again"), - show_alert=True) + try: + await callback.answer(get_text("error_occurred_try_again"), + show_alert=True) + except Exception: + pass return try: @@ -90,7 +102,10 @@ async def select_subscription_period_callback_handler( except (ValueError, IndexError): logging.error( f"Invalid subscription period in callback_data: {callback.data}") - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return price_rub = settings.subscription_options.get(months) @@ -98,7 +113,10 @@ async def select_subscription_period_callback_handler( logging.error( f"Price not found for {months} months subscription period in settings.subscription_options." ) - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL @@ -125,7 +143,10 @@ async def select_subscription_period_callback_handler( ) await callback.message.answer(text_content, reply_markup=reply_markup) - await callback.answer() + try: + await callback.answer() + except Exception: + pass @router.callback_query(F.data.startswith("pay_stars:")) @@ -138,7 +159,10 @@ async def pay_stars_callback_handler( get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key if not i18n or not callback.message: - await callback.answer(get_text("error_occurred_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_occurred_try_again"), show_alert=True) + except Exception: + pass return try: @@ -148,7 +172,10 @@ async def pay_stars_callback_handler( stars_price = int(price_str) except (ValueError, IndexError): logging.error(f"Invalid pay_stars data in callback: {callback.data}") - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return user_id = callback.from_user.id @@ -158,10 +185,16 @@ async def pay_stars_callback_handler( session, user_id, months, stars_price, payment_description) if payment_id is None: await callback.message.edit_text(get_text("error_payment_gateway")) - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return - await callback.answer() + try: + await callback.answer() + except Exception: + pass @router.callback_query(F.data.startswith("pay_yk:")) @@ -175,9 +208,11 @@ async def pay_yk_callback_handler( ) if i18n else key if not i18n or not callback.message: - - await callback.answer(get_text("error_occurred_try_again"), - show_alert=True) + try: + await callback.answer(get_text("error_occurred_try_again"), + show_alert=True) + except Exception: + pass return if not yookassa_service or not yookassa_service.configured: @@ -185,8 +220,11 @@ async def pay_yk_callback_handler( target_msg_edit = callback.message await target_msg_edit.edit_text(get_text("payment_service_unavailable") ) - await callback.answer(get_text("payment_service_unavailable_alert"), - show_alert=True) + try: + await callback.answer(get_text("payment_service_unavailable_alert"), + show_alert=True) + except Exception: + pass return try: @@ -197,7 +235,10 @@ async def pay_yk_callback_handler( except (ValueError, IndexError): logging.error( f"Invalid pay_yk data in callback: {callback.data}") - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return user_id = callback.from_user.id @@ -229,13 +270,19 @@ async def pay_yk_callback_handler( exc_info=True) await callback.message.edit_text( get_text("error_creating_payment_record")) - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return if not db_payment_record: await callback.message.edit_text( get_text("error_creating_payment_record")) - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return yookassa_metadata = { @@ -267,7 +314,10 @@ async def pay_yk_callback_handler( exc_info=True) await callback.message.edit_text( get_text("error_payment_gateway_link_failed")) - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return await callback.message.edit_text( @@ -291,7 +341,10 @@ async def pay_yk_callback_handler( ) await callback.message.edit_text(get_text("error_payment_gateway")) - await callback.answer() + try: + await callback.answer() + except Exception: + pass @router.callback_query(F.data.startswith("pay_crypto:")) @@ -303,12 +356,18 @@ async def pay_crypto_callback_handler( get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key if not i18n or not callback.message: - await callback.answer(get_text("error_occurred_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_occurred_try_again"), show_alert=True) + except Exception: + pass return if not cryptopay_service or not cryptopay_service.configured: await callback.message.edit_text(get_text("payment_service_unavailable")) - await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True) + try: + await callback.answer(get_text("payment_service_unavailable_alert"), show_alert=True) + except Exception: + pass return try: @@ -318,7 +377,10 @@ async def pay_crypto_callback_handler( amount_val = float(amount_str) except (ValueError, IndexError): logging.error(f"Invalid pay_crypto data in callback: {callback.data}") - await callback.answer(get_text("error_try_again"), show_alert=True) + try: + await callback.answer(get_text("error_try_again"), show_alert=True) + except Exception: + pass return user_id = callback.from_user.id @@ -334,7 +396,10 @@ async def pay_crypto_callback_handler( ) else: await callback.message.edit_text(get_text("error_payment_gateway")) - await callback.answer() + try: + await callback.answer() + except Exception: + pass @router.callback_query(F.data == "main_action:subscribe") @@ -387,7 +452,10 @@ async def my_subscription_command_handler( ) if isinstance(event, types.CallbackQuery): - await event.answer() + try: + await event.answer() + except Exception: + pass try: await event.message.edit_text(text, reply_markup=kb) except: @@ -421,7 +489,10 @@ async def my_subscription_command_handler( markup = get_back_to_main_menu_markup(current_lang, i18n) if isinstance(event, types.CallbackQuery): - await event.answer() + try: + await event.answer() + except Exception: + pass try: await event.message.edit_text(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True) except: From e74f801aecf9eca5764deb73c33450ecb3c3809d Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Fri, 29 Aug 2025 13:09:01 +0300 Subject: [PATCH 11/13] Add new error messages and subscription details in English and Russian localization - Introduced additional error messages for improved user feedback, including service unavailability and payment gateway errors. - Added subscription details localization to enhance user experience, providing information on subscription status and traffic usage. - Ensured consistency in messaging across both English and Russian locales. --- locales/en.json | 14 +++++++++++++- locales/ru.json | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index 57690fd..c62ee3b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -18,6 +18,7 @@ "language_set_alert": "Language changed!", "error_occurred_try_again": "An error occurred, please try again.", + "error_try_again": "Please try again.", "error_displaying_menu": "Error displaying menu.", "main_menu_unknown_action": "Unknown action.", @@ -368,5 +369,16 @@ "admin_sync_details": "📊 Synchronization Statistics:\n🔍 Panel records checked: {panel_records_checked}\n👥 Users found in DB: {users_found_in_db}\n✨ New users created: {users_created}\n🔄 Users updated: {users_updated}\n📋 Subscriptions synced: {subscriptions_synced_count}\n ├── Created new: {subscriptions_created}\n └── Updated existing: {subscriptions_updated}{additional_stats}", "admin_sync_no_telegram_id": "\n⚠️ Records without telegramId: {count}", "admin_sync_not_found_in_db": "\n❌ Not found in DB: {count}", - "admin_payments_pagination_info": "📊 Showing {shown} of {total} payments (page {current_page}/{total_pages})" + "admin_payments_pagination_info": "📊 Showing {shown} of {total} payments (page {current_page}/{total_pages})", + "my_subscription_details": "🔐 My Subscription\n\n⏰ Status: {status}\n📅 Active until: {end_date}\n📆 Days left: {days_left}\n\n🔗 Configuration link:\n{config_link}\n\n📊 Traffic:\nLimit: {traffic_limit}\nUsed: {traffic_used}", + "subscription_not_active": "You don't have an active subscription.", + "error_service_unavailable": "Service unavailable. Please try again later.", + "error_payment_gateway": "Payment service error. Please try again later.", + "traffic_na": "N/A", + "payment_service_unavailable": "Payment service is unavailable.", + "payment_service_unavailable_alert": "Payment service is unavailable. Please try again later.", + "error_creating_payment_record": "Error creating payment record. Please try again later.", + "error_payment_gateway_link_failed": "Error creating payment link. Please try again later.", + "status_active": "Active", + "status_inactive": "Inactive" } diff --git a/locales/ru.json b/locales/ru.json index c1049e9..2ab89f3 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -18,6 +18,7 @@ "language_set_alert": "Язык изменен!", "error_occurred_try_again": "Произошла ошибка, попробуйте снова.", + "error_try_again": "Попробуйте еще раз.", "error_displaying_menu": "Ошибка отображения меню.", "main_menu_unknown_action": "Неизвестное действие.", @@ -367,5 +368,16 @@ "admin_sync_details": "📊 Статистика синхронизации:\n🔍 Проверено записей панели: {panel_records_checked}\n👥 Найдено пользователей в БД: {users_found_in_db}\n✨ Создано новых пользователей: {users_created}\n🔄 Пользователей обновлено: {users_updated}\n📋 Подписок синхронизировано: {subscriptions_synced_count}\n ├── Создано новых: {subscriptions_created}\n └── Обновлено существующих: {subscriptions_updated}{additional_stats}", "admin_sync_no_telegram_id": "\n⚠️ Записей без telegramId: {count}", "admin_sync_not_found_in_db": "\n❌ Не найдено в БД: {count}", - "admin_payments_pagination_info": "📊 Показано {shown} из {total} платежей (стр. {current_page}/{total_pages})" + "admin_payments_pagination_info": "📊 Показано {shown} из {total} платежей (стр. {current_page}/{total_pages})", + "my_subscription_details": "🔐 Моя подписка\n\n⏰ Статус: {status}\n📅 Действует до: {end_date}\n📆 Осталось дней: {days_left}\n\n🔗 Ссылка на конфигурацию:\n{config_link}\n\n📊 Трафик:\nЛимит: {traffic_limit}\nИспользовано: {traffic_used}", + "subscription_not_active": "У вас нет активной подписки.", + "error_service_unavailable": "Сервис недоступен. Попробуйте позже.", + "error_payment_gateway": "Ошибка платежного сервиса. Попробуйте позже.", + "traffic_na": "N/A", + "payment_service_unavailable": "Платежный сервис недоступен.", + "payment_service_unavailable_alert": "Платежный сервис недоступен. Попробуйте позже.", + "error_creating_payment_record": "Ошибка создания записи платежа. Попробуйте позже.", + "error_payment_gateway_link_failed": "Ошибка создания платежной ссылки. Попробуйте позже.", + "status_active": "Активна", + "status_inactive": "Неактивна" } From 56fc88c10ec46f992557480d15ca535c56a1fddc Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Sat, 30 Aug 2025 13:29:42 +0300 Subject: [PATCH 12/13] Refactor trial confirmation and activation handlers to utilize new keyboard options - Updated the trial confirmation and activation handlers to use a dynamic reply markup based on activation status, improving user interaction. - Enhanced localization for trial activation details in both English and Russian, providing clearer messaging about trial status and connection instructions. - Ensured consistency in the user experience across different scenarios by integrating the new keyboard options. --- bot/handlers/user/trial_handler.py | 39 +++++++++++++++++++++--------- locales/en.json | 2 +- locales/ru.json | 2 +- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/bot/handlers/user/trial_handler.py b/bot/handlers/user/trial_handler.py index 34d2e4f..b03966c 100644 --- a/bot/handlers/user/trial_handler.py +++ b/bot/handlers/user/trial_handler.py @@ -11,6 +11,7 @@ from bot.services.notification_service import NotificationService from bot.keyboards.inline.user_keyboards import ( get_trial_confirmation_keyboard, get_main_menu_inline_keyboard, + get_connect_and_main_keyboard, ) from bot.middlewares.i18n import JsonI18n from .start import send_main_menu @@ -75,6 +76,7 @@ async def request_trial_confirmation_handler( final_message_text_in_chat = "" show_trial_button_after_action = False + config_link_for_trial = None if activation_result and activation_result.get("activated"): try: @@ -130,13 +132,21 @@ async def request_trial_confirmation_handler( ): show_trial_button_after_action = True + reply_markup = ( + get_connect_and_main_keyboard( + current_lang, i18n, settings, config_link_for_trial + ) + if activation_result and activation_result.get("activated") + else get_main_menu_inline_keyboard( + current_lang, i18n, settings, show_trial_button_after_action + ) + ) + try: await callback.message.edit_text( final_message_text_in_chat, parse_mode="HTML", - reply_markup=get_main_menu_inline_keyboard( - current_lang, i18n, settings, show_trial_button_after_action - ), + reply_markup=reply_markup, disable_web_page_preview=True, ) except Exception as e_edit: @@ -148,9 +158,7 @@ async def request_trial_confirmation_handler( await callback.message.answer( final_message_text_in_chat, parse_mode="HTML", - reply_markup=get_main_menu_inline_keyboard( - current_lang, i18n, settings, show_trial_button_after_action - ), + reply_markup=reply_markup, disable_web_page_preview=True, ) @@ -205,6 +213,7 @@ async def confirm_activate_trial_handler( final_message_text_in_chat = "" show_trial_button_after_action = False + config_link_for_trial = None if activation_result and activation_result.get("activated"): try: @@ -256,13 +265,21 @@ async def confirm_activate_trial_handler( ): show_trial_button_after_action = True + reply_markup = ( + get_connect_and_main_keyboard( + current_lang, i18n, settings, config_link_for_trial + ) + if activation_result and activation_result.get("activated") + else get_main_menu_inline_keyboard( + current_lang, i18n, settings, show_trial_button_after_action + ) + ) + try: await callback.message.edit_text( final_message_text_in_chat, parse_mode="HTML", - reply_markup=get_main_menu_inline_keyboard( - current_lang, i18n, settings, show_trial_button_after_action - ), + reply_markup=reply_markup, disable_web_page_preview=True, ) except Exception as e_edit: @@ -274,9 +291,7 @@ async def confirm_activate_trial_handler( await callback.message.answer( final_message_text_in_chat, parse_mode="HTML", - reply_markup=get_main_menu_inline_keyboard( - current_lang, i18n, settings, show_trial_button_after_action - ), + reply_markup=reply_markup, disable_web_page_preview=True, ) diff --git a/locales/en.json b/locales/en.json index c62ee3b..9e511fe 100644 --- a/locales/en.json +++ b/locales/en.json @@ -52,7 +52,7 @@ "trial_already_had_subscription_or_trial": "You have already used a free trial or had a paid subscription. Free trial is available only once for new users.", "trial_confirm_activate_button": "✅ Activate!", "trial_activated_alert": "Trial activated!", - "trial_activated_details_message": "🚀 Your {days}-day free trial is activated!\n\nValid until: {end_date}\nTraffic: {traffic_gb} GB\n\nYour configuration link:\n{config_link}", + "trial_activated_details_message": "✅ Trial activated!\nYour {days}-day trial is active until {end_date}.\n\nConnection key:\n{config_link}\n\nTo connect, open the link and follow the instructions 👇", "yes_button": "Yes", "no_button": "No", "referral_program_info_new": "🎁 Referral Program\n\n📊 Your stats:\n👥 Friends invited: {invited_count}\n💳 Purchased subscription: {purchased_count}\n\n🔗 Your link:\n{referral_link}\n\n💰 Invitation bonuses:\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!", diff --git a/locales/ru.json b/locales/ru.json index 2ab89f3..b58b394 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -52,7 +52,7 @@ "trial_already_had_subscription_or_trial": "Вы уже использовали пробный период или у вас была платная подписка. Пробный период доступен только один раз для новых пользователей.", "trial_confirm_activate_button": "✅ Активировать!", "trial_activated_alert": "Пробный период активирован!", - "trial_activated_details_message": "🚀 Ваш пробный период на {days} дней активирован!\n\nДействует до: {end_date}\nТрафик: {traffic_gb} GB\n\nВаша ссылка на конфигурацию:\n{config_link}", + "trial_activated_details_message": "✅ Пробный доступ активирован!\nВаш триал на {days} дн. действует до {end_date}.\n\nКлюч подключения:\n{config_link}\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇", "yes_button": "Да", "no_button": "Нет", "referral_program_info_new": "🎁 Реферальная программа\n\n📊 Твоя статистика:\n👥 Приглашено друзей: {invited_count}\n💳 Купили подписку: {purchased_count}\n\n🔗 Твоя ссылка:\n{referral_link}\n\n💰 Бонусы за приглашения:\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!", From b69c2ab18d4a01ee7be38f3dd3e21226ad4c3eaa Mon Sep 17 00:00:00 2001 From: machka-pasla Date: Mon, 1 Sep 2025 21:23:55 +0300 Subject: [PATCH 13/13] Enhance PanelWebhookService to support panel expiry updates and auto-renew payments - Updated the PanelWebhookService to accept a new PanelApiService dependency for managing panel user details. - Implemented functionality to update panel expiry upon subscription renewal, ensuring users maintain access to services. - Added error handling and logging for both panel expiry updates and auto-renew payment record creation, improving reliability and user feedback. --- bot/app/factories/build_services.py | 2 +- bot/services/panel_webhook_service.py | 55 ++++++++++++++++++++++++++- db/dal/payment_dal.py | 11 ++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/bot/app/factories/build_services.py b/bot/app/factories/build_services.py index f108b9c..8e84968 100644 --- a/bot/app/factories/build_services.py +++ b/bot/app/factories/build_services.py @@ -45,7 +45,7 @@ def build_core_services( subscription_service, referral_service, ) - panel_webhook_service = PanelWebhookService(bot, settings, i18n, async_session_factory) + panel_webhook_service = PanelWebhookService(bot, settings, i18n, async_session_factory, panel_service) yookassa_service = YooKassaService( shop_id=settings.YOOKASSA_SHOP_ID, secret_key=settings.YOOKASSA_SECRET_KEY, diff --git a/bot/services/panel_webhook_service.py b/bot/services/panel_webhook_service.py index b94f4c8..cee7ada 100644 --- a/bot/services/panel_webhook_service.py +++ b/bot/services/panel_webhook_service.py @@ -8,6 +8,7 @@ from aiogram.types import InlineKeyboardMarkup from sqlalchemy.orm import sessionmaker from typing import Optional from config.settings import Settings +from .panel_api_service import PanelApiService from bot.middlewares.i18n import JsonI18n from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup from db.dal import user_dal @@ -20,11 +21,12 @@ EVENT_MAP = { } class PanelWebhookService: - def __init__(self, bot: Bot, settings: Settings, i18n: JsonI18n, async_session_factory: sessionmaker): + def __init__(self, bot: Bot, settings: Settings, i18n: JsonI18n, async_session_factory: sessionmaker, panel_service: PanelApiService): self.bot = bot self.settings = settings self.i18n = i18n self.async_session_factory = async_session_factory + self.panel_service = panel_service async def _send_message( self, @@ -72,6 +74,7 @@ class PanelWebhookService: # Extend subscription by the last payment duration (calendar months) new_end_date = add_months(datetime.now(timezone.utc), last_tribute_duration) + # Update local DB subscription await subscription_dal.update_subscription( session, sub.subscription_id, @@ -81,6 +84,56 @@ class PanelWebhookService: 'is_active': True } ) + # Update panel expiry to ensure actual service access is extended + try: + panel_payload = { + "uuid": sub.panel_user_uuid, + "expireAt": new_end_date.isoformat(timespec='milliseconds').replace('+00:00', 'Z'), + "status": "ACTIVE", + } + panel_update_resp = await self.panel_service.update_user_details_on_panel( + sub.panel_user_uuid, + panel_payload, + log_response=True, + ) + if panel_update_resp: + logging.info( + f"Panel expiry updated for user {user_id} (panel_uuid {sub.panel_user_uuid}) to {new_end_date}" + ) + except Exception as e_panel: + logging.error( + f"Failed to update panel expiry for user {user_id} (panel_uuid {sub.panel_user_uuid}): {e_panel}") + + # Create a succeeded payment record in DB with the same amount/currency as last tribute payment + try: + last_payment = await payment_dal.get_last_tribute_payment(session, user_id) + if last_payment and last_payment.amount and last_payment.currency: + provider_payment_id = ( + f"tribute_auto_{user_id}_{sub.subscription_id}_" + f"{new_end_date.strftime('%Y%m%d')}" + ) + created_payment = await payment_dal.ensure_payment_with_provider_id( + session, + user_id=user_id, + amount=float(last_payment.amount), + currency=last_payment.currency, + months=last_tribute_duration, + description="Auto-renewal (panel webhook)", + provider="tribute", + provider_payment_id=provider_payment_id, + ) + if created_payment: + logging.info( + f"Auto-renew payment recorded (id={created_payment.payment_id}) for user {user_id} amount={created_payment.amount} {created_payment.currency} months={last_tribute_duration}" + ) + else: + logging.warning( + f"Could not create auto-renew payment for user {user_id}: previous tribute payment not found or missing amount/currency") + except Exception as e_pay: + logging.error( + f"Failed to create auto-renew payment record for user {user_id}: {e_pay}", + exc_info=True, + ) # Send auto-renewal notification _ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k diff --git a/db/dal/payment_dal.py b/db/dal/payment_dal.py index 7da4cbb..939ff2b 100644 --- a/db/dal/payment_dal.py +++ b/db/dal/payment_dal.py @@ -251,3 +251,14 @@ async def get_last_tribute_payment_duration(session: AsyncSession, user_id: int) result = await session.execute(stmt) return result.scalar_one_or_none() + + +async def get_last_tribute_payment( + session: AsyncSession, user_id: int) -> Optional[Payment]: + """Return the most recent succeeded Tribute payment for the user.""" + stmt = (select(Payment).where( + and_(Payment.user_id == user_id, Payment.provider == 'tribute', + Payment.status == 'succeeded')).order_by( + Payment.created_at.desc()).limit(1)) + result = await session.execute(stmt) + return result.scalar_one_or_none()