Compare commits

..
27 Commits
Author SHA1 Message Date
Machka PaslaandGitHub dace4c2938 Merge pull request #82 from machka-pasla/dev
I HATE tribute
2025-09-01 21:26:30 +03:00
machka-pasla b69c2ab18d 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.
2025-09-01 21:23:55 +03:00
machka-pasla 56fc88c10e 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.
2025-08-30 13:29:42 +03:00
machka-pasla e74f801aec 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.
2025-08-29 13:09:01 +03:00
machka-pasla acf5f060a6 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.
2025-08-29 12:27:13 +03:00
machka-pasla fb2672732d 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.
2025-08-29 12:10:00 +03:00
machka-pasla f4730c3ceb 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.
2025-08-29 11:05:21 +03:00
machka-pasla e6b3a7c66f 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.
2025-08-29 10:51:40 +03:00
machka-pasla 24cf193f3c 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.
2025-08-28 22:03:48 +03:00
machka-pasla 85bf0fb6fd 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.
2025-08-28 21:52:36 +03:00
machka-pasla b576d04501 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.
2025-08-28 21:46:59 +03:00
machka-pasla 97268b9906 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.
2025-08-28 21:41:40 +03:00
machka-pasla 1091b7b846 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.
2025-08-28 21:24:31 +03:00
Machka PaslaandGitHub f191c1813e Merge pull request #79 from ProtoPes/add-referral-check
Added check for referral user, small refactoring
2025-08-26 20:38:45 +03:00
ProtoPes e78f9bf291 Added check for referral user, small refactoring
With regular expressions we can be sure that the data is already valid
2025-08-26 20:28:50 +03:00
Machka PaslaandGitHub 5c74a08ed8 Merge pull request #78 from machka-pasla/dev
Implement user creation and synchronization enhancements in admin syn…
2025-08-26 18:14:40 +03:00
machka-pasla 14950fd559 Implement user creation and synchronization enhancements in admin sync handler
- Added functionality to create new users during synchronization if they are not found in the local database and have a valid Telegram ID.
- Introduced logging for newly created users to improve tracking and debugging.
- Enhanced synchronization statistics to include the count of newly created users, with localization support for both English and Russian.
- Updated the details of synchronization status to reflect additional statistics, improving clarity in admin reports.
2025-08-26 18:11:52 +03:00
Machka PaslaandGitHub 711b9a2487 Merge pull request #77 from machka-pasla/dev
Update broadcast and some other small bugs 🪲
2025-08-25 14:13:27 +03:00
machka-pasla 6e7eb6acfd Enhance message sending functionality to filter unsupported parameters
- Introduced a `SUPPORTED_PARAMS` dictionary to define valid parameters for each message type.
- Added a `filter_kwargs` utility function to filter out unsupported parameters based on the content type.
- Updated `send_message_by_type`, `send_message_via_queue`, and `send_direct_message` functions to utilize the new filtering logic, ensuring only valid parameters are passed during message sending.
- Improved handling for unknown content types by sending a default text message.
2025-08-25 13:49:31 +03:00
machka-pasla b42fae8772 Refactor message handling in broadcast and user management to utilize new utility functions
- Introduced `get_message_content` and `send_message_by_type` utility functions to streamline content type handling and message sending for various media types.
- Updated `process_broadcast_message_handler` and `process_direct_message_handler` to leverage these new functions, reducing code duplication and improving maintainability.
- Enhanced error handling for empty messages and improved message formatting with admin signatures.
2025-08-25 13:44:37 +03:00
machka-pasla f707662125 Refactor bot initialization and routing logic to enforce webhook mode requirement
- Updated the bot's startup logic to require a configured WEBHOOK_BASE_URL, exiting if not set, and logging appropriate error messages.
- Simplified the decision-making process for running the AIOHTTP server, ensuring it only runs in webhook mode.
- Enhanced the router configuration to filter updates for private chats, improving message handling security.
2025-08-25 13:24:30 +03:00
machka-pasla 60c6e0e961 Refactor subscription duration calculation in PanelWebhookService and SubscriptionService
- Introduced a utility function `add_months` to handle subscription duration calculations based on calendar months instead of a fixed 30-day period.
- Updated the auto-renewal logic in `PanelWebhookService` to use the new function for extending subscription end dates.
- Adjusted the duration calculation in `SubscriptionService` to derive the end date after a specified number of months, improving accuracy in subscription management.
2025-08-25 12:30:44 +03:00
machka-pasla b23b75b72e Refactor username update logic in SubscriptionService to prevent overwriting Telegram usernames
- Removed the conditional logic that updated the local user's username with the panel username, ensuring that the Telegram username remains unchanged.
- Added a comment to clarify the purpose of the update, focusing on maintaining the linkage to the panel UUID.
2025-08-25 12:24:30 +03:00
machka-pasla c69f02f7c0 Enhance direct message handling in user management to support multiple content types
- Updated the process_direct_message_handler to determine the content type of incoming messages (text, photo, video, etc.) and send them accordingly to the target user.
- Added error handling for empty messages and invalid HTML content, improving user feedback.
- Included admin signature in messages, ensuring consistent formatting across different content types.
2025-08-20 17:12:40 +03:00
machka-pasla f22e359684 Refactor user creation logic in DAL to support race-safe inserts and return creation status
- Updated the create_user function to use PostgreSQL upsert for concurrent user creation, preventing IntegrityError.
- Modified the function to return a tuple containing the user object and a boolean indicating if the user was newly created.
- Adjusted the start_command_handler to log user registration only if a new user was created, improving logging clarity.
2025-08-20 15:49:12 +03:00
machka-pasla d2402fea77 Add preview message functionality for multiple content types in broadcast handler
- Implemented preview message sending for various content types (text, photo, video, animation, document, audio, voice, sticker, video_note) in the process_broadcast_message_handler.
- Added error handling for invalid HTML content in broadcast messages, improving user feedback and experience.
- Enhanced the confirmation prompt to provide a concise message preview without duplicating text.
2025-08-20 15:39:22 +03:00
machka-pasla 9b8ddb39da Enhance broadcast message handling to support multiple content types
- Updated the process_broadcast_message_handler to determine the content type of incoming messages (text, photo, video, etc.) and store relevant data in the state.
- Implemented new methods in MessageQueueManager for queuing various media types, improving the flexibility of the broadcast system.
- Adjusted confirmation prompts to provide a concise message preview, enhancing user experience.
- Added localization for the new confirmation prompt in both English and Russian.
2025-08-20 15:30:06 +03:00
24 changed files with 1094 additions and 660 deletions
+2
View File
@@ -14,3 +14,5 @@ __pycache__/
# Игнорировать Docker артефакты (если вдруг) # Игнорировать Docker артефакты (если вдруг)
*.log *.log
*.pid *.pid
locales/ru_backup.json
locales/en_backup.json
+1 -1
View File
@@ -45,7 +45,7 @@ def build_core_services(
subscription_service, subscription_service,
referral_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( yookassa_service = YooKassaService(
shop_id=settings.YOOKASSA_SHOP_ID, shop_id=settings.YOOKASSA_SHOP_ID,
secret_key=settings.YOOKASSA_SECRET_KEY, secret_key=settings.YOOKASSA_SECRET_KEY,
+86 -47
View File
@@ -19,6 +19,7 @@ from bot.keyboards.inline.admin_keyboards import (
) )
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.utils.message_queue import get_queue_manager from bot.utils.message_queue import get_queue_manager
from bot.utils import get_message_content, send_message_by_type, send_message_via_queue, MessageContent
router = Router(name="admin_broadcast_router") router = Router(name="admin_broadcast_router")
@@ -76,30 +77,47 @@ async def process_broadcast_message_handler(
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
# Сохраняем в state исходный текст и entities # Определяем тип содержимого и сохраняем данные в state
text = (message.text or message.caption or "").strip()
entities = message.entities or message.caption_entities or [] entities = message.entities or message.caption_entities or []
content = get_message_content(message)
# Если текст пустой (например, прислали стикер/фото без подписи) — просим ввести текст # Если нет ни текста, ни медиа — ошибка
if not text: if not content.text and not content.file_id:
await message.answer(_("admin_broadcast_error_no_message")) await message.answer(_("admin_broadcast_error_no_message"))
return return
# Предварительная проверка HTML: попробуем отправить и сразу удалить # Сохраняем данные для рассылки
# Если HTML некорректный, Telegram вернёт ошибку парсинга await state.update_data(
broadcast_text=content.text,
broadcast_entities=entities,
broadcast_content_type=content.content_type,
broadcast_file_id=content.file_id,
broadcast_target="all",
)
# Отправляем превью-копию того, что будет разослано
try: try:
test_msg = await bot.send_message( # Для медиа-сообщений используем caption_entities, для текста - entities
chat_id=message.chat.id, if content.content_type == "text":
text=text, await send_message_by_type(
parse_mode="HTML", bot,
disable_web_page_preview=True, chat_id=message.chat.id,
disable_notification=True, content=content,
) parse_mode="HTML",
# Удалим тестовое сообщение entities=entities,
try: disable_web_page_preview=True,
await bot.delete_message(chat_id=message.chat.id, message_id=test_msg.message_id) disable_notification=True,
except Exception: )
pass 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: except TelegramBadRequest as e:
await message.answer( await message.answer(
_( _(
@@ -110,13 +128,8 @@ async def process_broadcast_message_handler(
) )
return return
await state.update_data( # Показываем короткое подтверждение без дублирования текста — сообщение выше служит превью
broadcast_text=text, confirmation_prompt = _("admin_broadcast_confirm_prompt_short")
broadcast_entities=entities,
broadcast_target="all",
)
confirmation_prompt = _("admin_broadcast_confirm_prompt", message_preview=text)
await message.answer( await message.answer(
confirmation_prompt, confirmation_prompt,
@@ -148,10 +161,9 @@ async def change_broadcast_target_handler(
await state.update_data(broadcast_target=new_target) await state.update_data(broadcast_target=new_target)
user_fsm_data = await state.get_data() user_fsm_data = await state.get_data()
text = user_fsm_data.get("broadcast_text", "")
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
confirmation_prompt = _( confirmation_prompt = _(
"admin_broadcast_confirm_prompt", message_preview=text "admin_broadcast_confirm_prompt_short"
) )
try: try:
await callback.message.edit_text( await callback.message.edit_text(
@@ -221,10 +233,15 @@ async def confirm_broadcast_callback_handler(
user_fsm_data = await state.get_data() user_fsm_data = await state.get_data()
if action == "send": if action == "send":
text = user_fsm_data.get("broadcast_text") # Создаем объект контента из сохраненных данных
content = MessageContent(
content_type=user_fsm_data.get("broadcast_content_type", "text"),
file_id=user_fsm_data.get("broadcast_file_id"),
text=user_fsm_data.get("broadcast_text")
)
entities = user_fsm_data.get("broadcast_entities", []) entities = user_fsm_data.get("broadcast_entities", [])
if not text: if not content.text and content.content_type == "text":
await callback.message.edit_text(_("admin_broadcast_error_no_message")) await callback.message.edit_text(_("admin_broadcast_error_no_message"))
await state.clear() await state.clear()
await callback.answer( await callback.answer(
@@ -247,7 +264,7 @@ async def confirm_broadcast_callback_handler(
failed_count = 0 failed_count = 0
admin_user = callback.from_user admin_user = callback.from_user
logging.info( logging.info(
f"Admin {admin_user.id} broadcasting '{text[:50]}...' to {len(user_ids)} users." f"Admin {admin_user.id} broadcasting '{(content.text or '')[:50]}...' to {len(user_ids)} users."
) )
# Get message queue manager # Get message queue manager
@@ -259,12 +276,25 @@ async def confirm_broadcast_callback_handler(
# Queue all messages for sending # Queue all messages for sending
for uid in user_ids: for uid in user_ids:
try: try:
await queue_manager.send_message( # Для медиа-сообщений используем caption_entities, для текста - entities
chat_id=uid, if content.content_type == "text":
text=text, await send_message_via_queue(
parse_mode="HTML", queue_manager,
disable_web_page_preview=True, 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 sent_count += 1
# Log successful queuing # Log successful queuing
@@ -275,7 +305,7 @@ async def confirm_broadcast_callback_handler(
"telegram_username": admin_user.username, "telegram_username": admin_user.username,
"telegram_first_name": admin_user.first_name, "telegram_first_name": admin_user.first_name,
"event_type": "admin_broadcast_queued", "event_type": "admin_broadcast_queued",
"content": f"To user {uid}: {text[:70]}...", "content": f"To user {uid}: [{content.content_type}] {(content.text or '')[:70]}...",
"is_admin_event": True, "is_admin_event": True,
"target_user_id": uid, "target_user_id": uid,
}, },
@@ -307,15 +337,24 @@ async def confirm_broadcast_callback_handler(
# Get queue stats for detailed report # Get queue stats for detailed report
queue_stats = queue_manager.get_queue_stats() queue_stats = queue_manager.get_queue_stats()
result_message = f"""🚀 Рассылка поставлена в очередь! result_message = (
📤 В очередь добавлено: {sent_count} _(
❌ Ошибок: {failed_count} "broadcast_queue_result",
default=(
📊 Статус очередей: "🚀 Рассылка поставлена в очередь!\n"
👥 Очередь пользователей: {queue_stats['user_queue_size']} сообщений "📤 В очередь добавлено: {sent_count}\n"
📢 Очередь групп: {queue_stats['group_queue_size']} сообщений "❌ Ошибок: {failed_count}\n\n"
"📊 Статус очередей:\n"
ℹ️ Сообщения будут отправлены автоматически с соблюдением лимитов Telegram.""" "👥 Очередь пользователей: {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( await callback.message.answer(
result_message, result_message,
reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n), reply_markup=get_back_to_admin_panel_keyboard(current_lang, i18n),
+8 -4
View File
@@ -77,7 +77,7 @@ async def view_payments_handler(callback: types.CallbackQuery, i18n_data: dict,
return return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = 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) 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 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 # Format payments text
text_parts = [_("admin_payments_header", default="💰 <b>Все платежи</b>")] text_parts = [_("admin_payments_header", default="💰 <b>Все платежи</b>")]
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): for i, payment in enumerate(payments, 1):
text_parts.append(f"<b>{page * page_size + i}.</b> {format_payment_text(payment, i18n, current_lang)}") text_parts.append(f"<b>{page * page_size + i}.</b> {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( await callback.message.reply_document(
document=file, document=file,
caption=_("admin_payments_export_success", caption=_("admin_payments_export_success",
default="📊 Экспорт платежей завершен!\nВсего записей: {count}", default="📊 Payments export completed!\nTotal records: {count}",
count=len(all_payments)) count=len(all_payments))
) )
await callback.answer( await callback.answer(
_("admin_export_sent", default="Файл отправлен!"), _("admin_export_sent", default="File sent!"),
show_alert=False show_alert=False
) )
+24 -11
View File
@@ -126,7 +126,7 @@ async def promo_management_handler(callback: types.CallbackQuery, i18n_data: dic
builder.row(*pagination_buttons) 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")) builder.row(InlineKeyboardButton(text=_("back_to_admin_panel_button"), callback_data="admin_action:main"))
# Формируем заголовок с информацией о страницах # Формируем заголовок с информацией о страницах
@@ -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: if not i18n or not callback.message or not current_lang:
return await callback.answer("Error processing request.", show_alert=True) return await callback.answer("Error processing request.", show_alert=True)
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
export_lang = "en"
try: try:
promo_id = int(callback.data.split(":")[1]) 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) output.seek(0)
file = types.BufferedInputFile(output.getvalue().encode('utf-8'), filename=f"promo_{promo.code}_activations.csv") 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): except (ValueError, IndexError):
await callback.answer(_("admin_promo_not_found"), show_alert=True) 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: if not i18n or not callback.message or not current_lang:
return await callback.answer("Error processing request.", show_alert=True) return await callback.answer("Error processing request.", show_alert=True)
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
export_lang = "en"
try: 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) 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() output = io.StringIO()
writer = csv.writer(output) writer = csv.writer(output)
# Заголовки CSV # CSV headers (forced to English)
writer.writerow([ writer.writerow([
"Код", "Бонусные дни", "Максимальные активации", "Текущие активации", i18n.gettext(export_lang, "admin_promo_csv_code"),
"Статус", "Активен", "Действителен до", "Создан", "Создал (Admin ID)" 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: 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 # Формируем данные для CSV
row = [ row = [
@@ -308,8 +321,8 @@ async def promo_export_all_handler(callback: types.CallbackQuery, i18n_data: dic
promo.max_activations, promo.max_activations,
promo.current_activations, promo.current_activations,
status_text, status_text,
"Да" if promo.is_active 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 "Без ограничений", 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_at.strftime("%Y-%m-%d %H:%M:%S") if promo.created_at else "N/A",
promo.created_by_admin_id or "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 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) await callback.message.answer_document(file, caption=caption)
except Exception as e: 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:")) @router.callback_query(F.data.startswith("promo_delete:"))
+64 -14
View File
@@ -31,6 +31,7 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession,
# Additional counters for detailed logging # Additional counters for detailed logging
users_without_telegram_id = 0 users_without_telegram_id = 0
users_not_found_in_db = 0 users_not_found_in_db = 0
users_created = 0
users_uuid_updated = 0 users_uuid_updated = 0
subscriptions_created = 0 subscriptions_created = 0
subscriptions_updated = 0 subscriptions_updated = 0
@@ -93,10 +94,33 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession,
if not existing_user: if not existing_user:
users_not_found_in_db += 1 users_not_found_in_db += 1
if telegram_id_from_panel: if telegram_id_from_panel:
logging.debug(f"Panel user with telegramId {telegram_id_from_panel} and UUID {panel_uuid} not found in local DB") # Create new user if they have telegram_id
try:
user_data = {
"user_id": telegram_id_from_panel,
"username": None, # Username will be updated when user interacts with bot
"first_name": None, # Panel doesn't provide this info
"last_name": None, # Panel doesn't provide this info
"language_code": "ru", # Default language
"panel_user_uuid": panel_uuid,
"is_banned": False,
"referred_by_id": None
}
new_user, was_created = await user_dal.create_user(session, user_data)
if was_created:
users_created += 1
logging.info(f"Created new user {telegram_id_from_panel} from panel sync with UUID {panel_uuid}")
existing_user = new_user
except Exception as e_create:
sync_errors.append(f"Error creating user {telegram_id_from_panel}: {str(e_create)}")
logging.error(f"Error creating user {telegram_id_from_panel}: {e_create}")
continue
else: else:
logging.debug(f"Panel user with UUID {panel_uuid} (no telegramId) not found in local DB") logging.debug(f"Panel user with UUID {panel_uuid} (no telegramId) not found in local DB - skipping")
continue continue
# User found in local DB # User found in local DB
users_found_in_db += 1 users_found_in_db += 1
@@ -112,6 +136,23 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession,
users_uuid_updated += 1 users_uuid_updated += 1
logging.info(f"Updated panel UUID for user {actual_user_id}: {panel_uuid}") 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 # Sync subscription data
panel_expire_at_iso = panel_user_dict.get("expireAt") panel_expire_at_iso = panel_user_dict.get("expireAt")
panel_status = panel_user_dict.get("status", "UNKNOWN") panel_status = panel_user_dict.get("status", "UNKNOWN")
@@ -218,20 +259,27 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession,
# Update sync status # Update sync status
status = "completed_with_errors" if sync_errors else "completed" status = "completed_with_errors" if sync_errors else "completed"
details = (f"📊 Статистика синхронизации:\n" # Build additional stats
f"🔍 Проверено записей панели: {panel_records_checked}\n" default_lang = settings.DEFAULT_LANGUAGE
f"👥 Найдено пользователей в БД: {users_found_in_db}\n" additional_stats = ""
f"🔄 Пользователей обновлено: {users_updated}\n"
f"📋 Подписок синхронизировано: {subscriptions_synced_count}\n"
f" ├── Создано новых: {subscriptions_created}\n"
f" └── Обновлено существующих: {subscriptions_updated}")
if users_without_telegram_id > 0: if users_without_telegram_id > 0:
details += f"\n⚠️ Записей без telegramId: {users_without_telegram_id}" additional_stats += i18n_instance.gettext(default_lang, "admin_sync_no_telegram_id", count=users_without_telegram_id)
if users_not_found_in_db > 0: if users_not_found_in_db > 0:
details += f"\n❌ Не найдено в БД: {users_not_found_in_db}" additional_stats += i18n_instance.gettext(default_lang, "admin_sync_not_found_in_db", count=users_not_found_in_db)
if sync_errors: if sync_errors:
details += f"\n🚫 Ошибок: {len(sync_errors)}" additional_stats += i18n_instance.gettext(default_lang, "admin_sync_errors", count=len(sync_errors))
# Build full details using localization
details = i18n_instance.gettext(default_lang, "admin_sync_details",
panel_records_checked=panel_records_checked,
users_found_in_db=users_found_in_db,
users_created=users_created,
users_updated=users_updated,
subscriptions_synced_count=subscriptions_synced_count,
subscriptions_created=subscriptions_created,
subscriptions_updated=subscriptions_updated,
additional_stats=additional_stats
)
await panel_sync_dal.update_panel_sync_status( await panel_sync_dal.update_panel_sync_status(
session, status, details, panel_records_checked, subscriptions_synced_count session, status, details, panel_records_checked, subscriptions_synced_count
@@ -244,6 +292,7 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession,
logging.info(f" Users without telegramId: {users_without_telegram_id}") logging.info(f" Users without telegramId: {users_without_telegram_id}")
logging.info(f" Users not found in local DB: {users_not_found_in_db}") logging.info(f" Users not found in local DB: {users_not_found_in_db}")
logging.info(f" Users found in local DB: {users_found_in_db}") logging.info(f" Users found in local DB: {users_found_in_db}")
logging.info(f" Users created: {users_created}")
logging.info(f" Users with UUID updated: {users_uuid_updated}") logging.info(f" Users with UUID updated: {users_uuid_updated}")
logging.info(f" Users updated overall: {users_updated}") logging.info(f" Users updated overall: {users_updated}")
logging.info(f" Subscriptions total synced: {subscriptions_synced_count}") logging.info(f" Subscriptions total synced: {subscriptions_synced_count}")
@@ -256,6 +305,7 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession,
"details": details, "details": details,
"users_processed": panel_records_checked, "users_processed": panel_records_checked,
"users_synced": users_found_in_db, "users_synced": users_found_in_db,
"users_created": users_created,
"subs_synced": subscriptions_synced_count, "subs_synced": subscriptions_synced_count,
"errors": sync_errors "errors": sync_errors
} }
+38 -8
View File
@@ -1,6 +1,7 @@
import logging import logging
import re import re
from aiogram import Router, F, types, Bot from aiogram import Router, F, types, Bot
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.utils.markdown import hcode, hbold from aiogram.utils.markdown import hcode, hbold
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
@@ -15,6 +16,7 @@ from bot.keyboards.inline.admin_keyboards import get_back_to_admin_panel_keyboar
from bot.services.subscription_service import SubscriptionService from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService from bot.services.panel_api_service import PanelApiService
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.utils import get_message_content, send_direct_message
from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton from aiogram.utils.keyboard import InlineKeyboardBuilder, InlineKeyboardButton
router = Router(name="admin_user_management_router") router = Router(name="admin_user_management_router")
@@ -418,7 +420,9 @@ async def handle_view_user_logs(callback: types.CallbackQuery, user: User,
), show_alert=True) ), show_alert=True)
return return
logs_text_parts = [f"📜 <b>Последние действия пользователя {user.user_id}:</b>\n"] logs_text_parts = [
f"{_('admin_user_recent_actions_title', default='📜 Последние действия пользователя {user_id}:', user_id=user.user_id)}\n"
]
for log in logs: for log in logs:
timestamp = log.timestamp.strftime('%Y-%m-%d %H:%M') if log.timestamp else 'N/A' timestamp = log.timestamp.strftime('%Y-%m-%d %H:%M') if log.timestamp else 'N/A'
@@ -578,7 +582,7 @@ async def process_subscription_days_handler(message: types.Message, state: FSMCo
await state.clear() await state.clear()
@router.message(AdminStates.waiting_for_direct_message_to_user, F.text) @router.message(AdminStates.waiting_for_direct_message_to_user)
async def process_direct_message_handler(message: types.Message, state: FSMContext, async def process_direct_message_handler(message: types.Message, state: FSMContext,
settings: Settings, i18n_data: dict, settings: Settings, i18n_data: dict,
bot: Bot, session: AsyncSession): bot: Bot, session: AsyncSession):
@@ -597,8 +601,9 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
await state.clear() await state.clear()
return return
message_text = message.text.strip() # Determine content similar to broadcast
if len(message_text) > 4000: text = (message.text or message.caption or "").strip()
if len(text) > 4000:
await message.answer(_( await message.answer(_(
"admin_user_message_too_long", "admin_user_message_too_long",
default="❌ Сообщение слишком длинное (максимум 4000 символов)" default="❌ Сообщение слишком длинное (максимум 4000 символов)"
@@ -613,15 +618,40 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
await state.clear() await state.clear()
return return
# Prepare message with admin signature # Prepare admin signature and get content
admin_signature = _( admin_signature = _(
"admin_direct_message_signature", "admin_direct_message_signature",
default="\n\n---\n💬 Сообщение от администратора" default="\n\n---\n💬 Сообщение от администратора"
) )
full_message = message_text + admin_signature
content = get_message_content(message)
# Send message to user if not content.text and not content.file_id:
await bot.send_message(target_user_id, full_message) await message.answer(_(
"admin_direct_empty_message",
default="❌ Пустое сообщение. Отправьте текст или медиа."
))
return
caption_with_signature = (content.text + admin_signature) if content.text else None
# Send to target user using our fancy match/case function
try:
await send_direct_message(
bot,
target_user_id,
content,
extra_text=admin_signature,
parse_mode="HTML",
disable_web_page_preview=True,
)
except TelegramBadRequest as e:
await message.answer(_(
"admin_broadcast_invalid_html",
default="❌ Некорректный HTML в сообщении. Пожалуйста, отправьте корректный HTML (поддерживаются теги Telegram) или уберите теги.\nОшибка: {error}",
error=str(e),
))
return
# Confirm to admin # Confirm to admin
await message.answer(_( await message.answer(_(
+1
View File
@@ -157,6 +157,7 @@ async def create_user_stats_result(session: AsyncSession, i18n_instance, lang: s
"🚫 Заблокированных: <b>{banned}</b>\n" "🚫 Заблокированных: <b>{banned}</b>\n"
"🎁 Привлечено по реферальной программе: <b>{referral}</b>", "🎁 Привлечено по реферальной программе: <b>{referral}</b>",
total=user_stats['total_users'], total=user_stats['total_users'],
active_today=user_stats['active_today'],
paid=user_stats['paid_subscriptions'], paid=user_stats['paid_subscriptions'],
trial=user_stats['trial_users'], trial=user_stats['trial_users'],
inactive=user_stats['inactive_users'], inactive=user_stats['inactive_users'],
+43 -49
View File
@@ -1,4 +1,5 @@
import logging import logging
import re
from aiogram import Router, F, types, Bot from aiogram import Router, F, types, Bot
from aiogram.utils.text_decorations import html_decoration as hd from aiogram.utils.text_decorations import html_decoration as hd
from aiogram.filters import CommandStart, Command from aiogram.filters import CommandStart, Command
@@ -42,10 +43,9 @@ async def send_main_menu(target_event: Union[types.Message,
await target_event.answer(err_msg_fallback, show_alert=True) await target_event.answer(err_msg_fallback, show_alert=True)
except Exception: except Exception:
pass pass
elif isinstance(target_event, types.Message) and hasattr( elif isinstance(target_event, types.Message):
target_event, 'chat') and target_event.chat:
try: try:
await target_event.chat.send_message(err_msg_fallback) await target_event.answer(err_msg_fallback)
except Exception: except Exception:
pass pass
return return
@@ -92,33 +92,40 @@ async def send_main_menu(target_event: Union[types.Message,
await target_message_obj.answer(text, reply_markup=reply_markup) await target_message_obj.answer(text, reply_markup=reply_markup)
if isinstance(target_event, types.CallbackQuery): if isinstance(target_event, types.CallbackQuery):
await target_event.answer() try:
await target_event.answer()
except Exception:
pass
except Exception as e_send_edit: except Exception as e_send_edit:
logging.warning( logging.warning(
f"Failed to send/edit main menu (user: {user_id}, is_edit: {is_edit}): {type(e_send_edit).__name__} - {e_send_edit}." 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( if is_edit and target_message_obj:
target_message_obj, 'chat') and target_message_obj.chat:
try: try:
await target_message_obj.chat.send_message( await target_message_obj.answer(text, reply_markup=reply_markup)
text, reply_markup=reply_markup)
except Exception as e_send_new: except Exception as e_send_new:
logging.error( logging.error(
f"Also failed to send new main menu message for user {user_id}: {e_send_new}" f"Also failed to send new main menu message for user {user_id}: {e_send_new}"
) )
if isinstance(target_event, types.CallbackQuery): if isinstance(target_event, types.CallbackQuery):
await target_event.answer( try:
_("error_occurred_try_again") if is_edit else None) await target_event.answer(
_("error_occurred_try_again") if is_edit else None)
except Exception:
pass
@router.message(CommandStart()) @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, async def start_command_handler(message: types.Message,
state: FSMContext, state: FSMContext,
settings: Settings, settings: Settings,
i18n_data: dict, i18n_data: dict,
subscription_service: SubscriptionService, subscription_service: SubscriptionService,
session: AsyncSession, session: AsyncSession,
command: Optional[CommandStart] = None): ref_match: Optional[re.Match] = None,
promo_match: Optional[re.Match] = None):
await state.clear() await state.clear()
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
@@ -130,28 +137,14 @@ async def start_command_handler(message: types.Message,
referred_by_user_id: Optional[int] = None referred_by_user_id: Optional[int] = None
promo_code_to_apply: Optional[str] = None promo_code_to_apply: Optional[str] = None
if command and command.args: if ref_match:
arg_payload = command.args potential_referrer_id = int(ref_match.group(1))
if arg_payload.startswith("ref_"): if await user_dal.get_user_by_id(session, potential_referrer_id):
try: referred_by_user_id = potential_referrer_id
potential_referrer_id_str = arg_payload.split("_")[1] elif promo_match:
if potential_referrer_id_str.isdigit(): promo_code_to_apply = promo_match.group(1)
potential_referrer_id = int(potential_referrer_id_str) logging.info(f"User {user_id} started with promo code: {promo_code_to_apply}")
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}"
)
db_user = await user_dal.get_user_by_id(session, user_id) db_user = await user_dal.get_user_by_id(session, user_id)
if not db_user: if not db_user:
@@ -165,24 +158,25 @@ async def start_command_handler(message: types.Message,
"registration_date": datetime.now(timezone.utc) "registration_date": datetime.now(timezone.utc)
} }
try: try:
db_user = await user_dal.create_user(session, user_data_to_create) db_user, created = await user_dal.create_user(session, user_data_to_create)
logging.info( if created:
f"New user {user_id} added to session. Referred by: {referred_by_user_id or 'N/A'}." logging.info(
) f"New user {user_id} added to session. Referred by: {referred_by_user_id or 'N/A'}."
# Send notification about new user registration
try:
from bot.services.notification_service import NotificationService
notification_service = NotificationService(message.bot, settings, i18n)
await notification_service.notify_new_user_registration(
user_id=user_id,
username=user.username,
first_name=user.first_name,
referred_by_id=referred_by_user_id
) )
except Exception as e:
logging.error(f"Failed to send new user notification: {e}") # Send notification about new user registration
try:
from bot.services.notification_service import NotificationService
notification_service = NotificationService(message.bot, settings, i18n)
await notification_service.notify_new_user_registration(
user_id=user_id,
username=user.username,
first_name=user.first_name,
referred_by_id=referred_by_user_id
)
except Exception as e:
logging.error(f"Failed to send new user notification: {e}")
except Exception as e_create: except Exception as e_create:
logging.error( logging.error(
+100 -29
View File
@@ -35,7 +35,10 @@ async def display_subscription_options(event: Union[types.Message,
if not i18n: if not i18n:
err_msg = "Language service error." err_msg = "Language service error."
if isinstance(event, types.CallbackQuery): 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): elif isinstance(event, types.Message):
await event.answer(err_msg) await event.answer(err_msg)
return return
@@ -54,8 +57,11 @@ async def display_subscription_options(event: Union[types.Message,
event, types.CallbackQuery) else event event, types.CallbackQuery) else event
if not target_message_obj: if not target_message_obj:
if isinstance(event, types.CallbackQuery): if isinstance(event, types.CallbackQuery):
await event.answer(get_text("error_occurred_try_again"), try:
show_alert=True) await event.answer(get_text("error_occurred_try_again"),
show_alert=True)
except Exception:
pass
return return
if isinstance(event, types.CallbackQuery): if isinstance(event, types.CallbackQuery):
@@ -65,7 +71,10 @@ async def display_subscription_options(event: Union[types.Message,
except Exception: except Exception:
await target_message_obj.answer(text_content, await target_message_obj.answer(text_content,
reply_markup=reply_markup) reply_markup=reply_markup)
await event.answer() try:
await event.answer()
except Exception:
pass
else: else:
await target_message_obj.answer(text_content, await target_message_obj.answer(text_content,
reply_markup=reply_markup) reply_markup=reply_markup)
@@ -81,8 +90,11 @@ async def select_subscription_period_callback_handler(
) if i18n else key ) if i18n else key
if not i18n or not callback.message: if not i18n or not callback.message:
await callback.answer(get_text("error_occurred_try_again"), try:
show_alert=True) await callback.answer(get_text("error_occurred_try_again"),
show_alert=True)
except Exception:
pass
return return
try: try:
@@ -90,7 +102,10 @@ async def select_subscription_period_callback_handler(
except (ValueError, IndexError): except (ValueError, IndexError):
logging.error( logging.error(
f"Invalid subscription period in callback_data: {callback.data}") 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 return
price_rub = settings.subscription_options.get(months) price_rub = settings.subscription_options.get(months)
@@ -98,7 +113,10 @@ async def select_subscription_period_callback_handler(
logging.error( logging.error(
f"Price not found for {months} months subscription period in settings.subscription_options." 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 return
currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL currency_symbol_val = settings.DEFAULT_CURRENCY_SYMBOL
@@ -125,7 +143,10 @@ async def select_subscription_period_callback_handler(
) )
await callback.message.answer(text_content, await callback.message.answer(text_content,
reply_markup=reply_markup) reply_markup=reply_markup)
await callback.answer() try:
await callback.answer()
except Exception:
pass
@router.callback_query(F.data.startswith("pay_stars:")) @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 get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
if not i18n or not callback.message: 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 return
try: try:
@@ -148,7 +172,10 @@ async def pay_stars_callback_handler(
stars_price = int(price_str) stars_price = int(price_str)
except (ValueError, IndexError): except (ValueError, IndexError):
logging.error(f"Invalid pay_stars data in callback: {callback.data}") 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 return
user_id = callback.from_user.id user_id = callback.from_user.id
@@ -158,10 +185,16 @@ async def pay_stars_callback_handler(
session, user_id, months, stars_price, payment_description) session, user_id, months, stars_price, payment_description)
if payment_id is None: if payment_id is None:
await callback.message.edit_text(get_text("error_payment_gateway")) 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 return
await callback.answer() try:
await callback.answer()
except Exception:
pass
@router.callback_query(F.data.startswith("pay_yk:")) @router.callback_query(F.data.startswith("pay_yk:"))
@@ -175,9 +208,11 @@ async def pay_yk_callback_handler(
) if i18n else key ) if i18n else key
if not i18n or not callback.message: if not i18n or not callback.message:
try:
await callback.answer(get_text("error_occurred_try_again"), await callback.answer(get_text("error_occurred_try_again"),
show_alert=True) show_alert=True)
except Exception:
pass
return return
if not yookassa_service or not yookassa_service.configured: if not yookassa_service or not yookassa_service.configured:
@@ -185,8 +220,11 @@ async def pay_yk_callback_handler(
target_msg_edit = callback.message target_msg_edit = callback.message
await target_msg_edit.edit_text(get_text("payment_service_unavailable") await target_msg_edit.edit_text(get_text("payment_service_unavailable")
) )
await callback.answer(get_text("payment_service_unavailable_alert"), try:
show_alert=True) await callback.answer(get_text("payment_service_unavailable_alert"),
show_alert=True)
except Exception:
pass
return return
try: try:
@@ -197,7 +235,10 @@ async def pay_yk_callback_handler(
except (ValueError, IndexError): except (ValueError, IndexError):
logging.error( logging.error(
f"Invalid pay_yk data in callback: {callback.data}") 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 return
user_id = callback.from_user.id user_id = callback.from_user.id
@@ -229,13 +270,19 @@ async def pay_yk_callback_handler(
exc_info=True) exc_info=True)
await callback.message.edit_text( await callback.message.edit_text(
get_text("error_creating_payment_record")) 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 return
if not db_payment_record: if not db_payment_record:
await callback.message.edit_text( await callback.message.edit_text(
get_text("error_creating_payment_record")) 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 return
yookassa_metadata = { yookassa_metadata = {
@@ -267,7 +314,10 @@ async def pay_yk_callback_handler(
exc_info=True) exc_info=True)
await callback.message.edit_text( await callback.message.edit_text(
get_text("error_payment_gateway_link_failed")) 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 return
await callback.message.edit_text( 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.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:")) @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 get_text = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
if not i18n or not callback.message: 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 return
if not cryptopay_service or not cryptopay_service.configured: if not cryptopay_service or not cryptopay_service.configured:
await callback.message.edit_text(get_text("payment_service_unavailable")) 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 return
try: try:
@@ -318,7 +377,10 @@ async def pay_crypto_callback_handler(
amount_val = float(amount_str) amount_val = float(amount_str)
except (ValueError, IndexError): except (ValueError, IndexError):
logging.error(f"Invalid pay_crypto data in callback: {callback.data}") 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 return
user_id = callback.from_user.id user_id = callback.from_user.id
@@ -334,7 +396,10 @@ async def pay_crypto_callback_handler(
) )
else: else:
await callback.message.edit_text(get_text("error_payment_gateway")) 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") @router.callback_query(F.data == "main_action:subscribe")
@@ -387,7 +452,10 @@ async def my_subscription_command_handler(
) )
if isinstance(event, types.CallbackQuery): if isinstance(event, types.CallbackQuery):
await event.answer() try:
await event.answer()
except Exception:
pass
try: try:
await event.message.edit_text(text, reply_markup=kb) await event.message.edit_text(text, reply_markup=kb)
except: except:
@@ -421,7 +489,10 @@ async def my_subscription_command_handler(
markup = get_back_to_main_menu_markup(current_lang, i18n) markup = get_back_to_main_menu_markup(current_lang, i18n)
if isinstance(event, types.CallbackQuery): if isinstance(event, types.CallbackQuery):
await event.answer() try:
await event.answer()
except Exception:
pass
try: try:
await event.message.edit_text(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True) await event.message.edit_text(text, reply_markup=markup, parse_mode="HTML", disable_web_page_preview=True)
except: except:
+73 -36
View File
@@ -11,6 +11,7 @@ from bot.services.notification_service import NotificationService
from bot.keyboards.inline.user_keyboards import ( from bot.keyboards.inline.user_keyboards import (
get_trial_confirmation_keyboard, get_trial_confirmation_keyboard,
get_main_menu_inline_keyboard, get_main_menu_inline_keyboard,
get_connect_and_main_keyboard,
) )
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from .start import send_main_menu from .start import send_main_menu
@@ -31,7 +32,10 @@ async def request_trial_confirmation_handler(
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
if not i18n or not callback.message: 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 return
show_trial_btn_in_menu_if_fail = False show_trial_btn_in_menu_if_fail = False
@@ -46,7 +50,10 @@ async def request_trial_confirmation_handler(
current_lang, i18n, settings, False current_lang, i18n, settings, False
), ),
) )
await callback.answer() try:
await callback.answer()
except Exception:
pass
return return
if await subscription_service.has_had_any_subscription(session, user_id): if await subscription_service.has_had_any_subscription(session, user_id):
@@ -56,7 +63,10 @@ async def request_trial_confirmation_handler(
current_lang, i18n, settings, False current_lang, i18n, settings, False
), ),
) )
await callback.answer() try:
await callback.answer()
except Exception:
pass
return return
# Directly activate trial without confirmation # Directly activate trial without confirmation
@@ -66,9 +76,13 @@ async def request_trial_confirmation_handler(
final_message_text_in_chat = "" final_message_text_in_chat = ""
show_trial_button_after_action = False show_trial_button_after_action = False
config_link_for_trial = None
if activation_result and activation_result.get("activated"): 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") end_date_obj = activation_result.get("end_date")
config_link_for_trial = activation_result.get("subscription_url") or _( config_link_for_trial = activation_result.get("subscription_url") or _(
@@ -106,7 +120,10 @@ async def request_trial_confirmation_handler(
else "trial_activation_failed" else "trial_activation_failed"
) )
final_message_text_in_chat = _(message_key_from_service) 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 ( if (
settings.TRIAL_ENABLED settings.TRIAL_ENABLED
and not await subscription_service.has_had_any_subscription( and not await subscription_service.has_had_any_subscription(
@@ -115,13 +132,21 @@ async def request_trial_confirmation_handler(
): ):
show_trial_button_after_action = True 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: try:
await callback.message.edit_text( await callback.message.edit_text(
final_message_text_in_chat, final_message_text_in_chat,
parse_mode="HTML", parse_mode="HTML",
reply_markup=get_main_menu_inline_keyboard( reply_markup=reply_markup,
current_lang, i18n, settings, show_trial_button_after_action
),
disable_web_page_preview=True, disable_web_page_preview=True,
) )
except Exception as e_edit: except Exception as e_edit:
@@ -129,17 +154,11 @@ async def request_trial_confirmation_handler(
f"Could not edit trial result message: {e_edit}. Sending new one." f"Could not edit trial result message: {e_edit}. Sending new one."
) )
if ( if callback.message:
callback.message await callback.message.answer(
and hasattr(callback.message, "chat")
and callback.message.chat
):
await callback.message.chat.send_message(
final_message_text_in_chat, final_message_text_in_chat,
parse_mode="HTML", parse_mode="HTML",
reply_markup=get_main_menu_inline_keyboard( reply_markup=reply_markup,
current_lang, i18n, settings, show_trial_button_after_action
),
disable_web_page_preview=True, disable_web_page_preview=True,
) )
@@ -160,20 +179,29 @@ async def confirm_activate_trial_handler(
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) if i18n else key
if not i18n or not callback.message: 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 return
if not settings.TRIAL_ENABLED: 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( await send_main_menu(
callback, settings, i18n_data, subscription_service, session, is_edit=True callback, settings, i18n_data, subscription_service, session, is_edit=True
) )
return return
if await subscription_service.has_had_any_subscription(session, user_id): if await subscription_service.has_had_any_subscription(session, user_id):
await callback.answer( try:
_("trial_already_had_subscription_or_trial"), show_alert=True await callback.answer(
) _("trial_already_had_subscription_or_trial"), show_alert=True
)
except Exception:
pass
await send_main_menu( await send_main_menu(
callback, settings, i18n_data, subscription_service, session, is_edit=True callback, settings, i18n_data, subscription_service, session, is_edit=True
) )
@@ -185,9 +213,13 @@ async def confirm_activate_trial_handler(
final_message_text_in_chat = "" final_message_text_in_chat = ""
show_trial_button_after_action = False show_trial_button_after_action = False
config_link_for_trial = None
if activation_result and activation_result.get("activated"): 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") end_date_obj = activation_result.get("end_date")
config_link_for_trial = activation_result.get("subscription_url") or _( config_link_for_trial = activation_result.get("subscription_url") or _(
@@ -221,7 +253,10 @@ async def confirm_activate_trial_handler(
else "trial_activation_failed" else "trial_activation_failed"
) )
final_message_text_in_chat = _(message_key_from_service) 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 ( if (
settings.TRIAL_ENABLED settings.TRIAL_ENABLED
and not await subscription_service.has_had_any_subscription( and not await subscription_service.has_had_any_subscription(
@@ -230,13 +265,21 @@ async def confirm_activate_trial_handler(
): ):
show_trial_button_after_action = True 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: try:
await callback.message.edit_text( await callback.message.edit_text(
final_message_text_in_chat, final_message_text_in_chat,
parse_mode="HTML", parse_mode="HTML",
reply_markup=get_main_menu_inline_keyboard( reply_markup=reply_markup,
current_lang, i18n, settings, show_trial_button_after_action
),
disable_web_page_preview=True, disable_web_page_preview=True,
) )
except Exception as e_edit: except Exception as e_edit:
@@ -244,17 +287,11 @@ async def confirm_activate_trial_handler(
f"Could not edit trial result message: {e_edit}. Sending new one." f"Could not edit trial result message: {e_edit}. Sending new one."
) )
if ( if callback.message:
callback.message await callback.message.answer(
and hasattr(callback.message, "chat")
and callback.message.chat
):
await callback.message.chat.send_message(
final_message_text_in_chat, final_message_text_in_chat,
parse_mode="HTML", parse_mode="HTML",
reply_markup=get_main_menu_inline_keyboard( reply_markup=reply_markup,
current_lang, i18n, settings, show_trial_button_after_action
),
disable_web_page_preview=True, disable_web_page_preview=True,
) )
+17 -41
View File
@@ -107,10 +107,10 @@ async def on_startup_configured(dispatcher: Dispatcher):
"STARTUP: Skipped setting Telegram webhook due to security or configuration error." "STARTUP: Skipped setting Telegram webhook due to security or configuration error."
) )
else: else:
logging.info( logging.error(
"STARTUP: WEBHOOK_BASE_URL not set in environment. Running in polling mode and clearing any existing webhook." "STARTUP: WEBHOOK_BASE_URL not set in environment. Webhook mode is required. Exiting."
) )
await bot.delete_webhook(drop_pending_updates=True) raise SystemExit("WEBHOOK_BASE_URL is required. Polling mode is disabled.")
if settings.SUBSCRIPTION_MINI_APP_URL: if settings.SUBSCRIPTION_MINI_APP_URL:
try: try:
@@ -271,54 +271,30 @@ async def run_bot(settings_param: Settings):
await register_all_routers(dp, settings_param) await register_all_routers(dp, settings_param)
tg_webhook_base = settings_param.WEBHOOK_BASE_URL tg_webhook_base = settings_param.WEBHOOK_BASE_URL
yk_webhook_base = settings_param.WEBHOOK_BASE_URL
should_run_aiohttp_server = bool(tg_webhook_base) or ( # Webhook mode is now required - exit if not configured
bool(yk_webhook_base) and bool(settings_param.yookassa_webhook_path) if not tg_webhook_base:
) logging.error("WEBHOOK_BASE_URL is required. Polling mode is disabled. Exiting.")
await dp.emit_shutdown()
telegram_uses_webhook_mode = bool(tg_webhook_base) raise SystemExit("WEBHOOK_BASE_URL is required. Polling mode is disabled.")
run_telegram_polling = not telegram_uses_webhook_mode
logging.info(f"--- Bot Run Mode Decision ---") logging.info(f"--- Bot Run Mode Decision ---")
logging.info( logging.info(f"Configured WEBHOOK_BASE_URL: '{tg_webhook_base}' -> Webhook Mode: ENABLED")
f"Configured WEBHOOK_BASE_URL: '{tg_webhook_base}' -> Telegram Webhook Mode: {telegram_uses_webhook_mode}" logging.info(f"YooKassa webhook path: '{settings_param.yookassa_webhook_path}'")
) logging.info(f"Decision: Run AIOHTTP server: ENABLED (required for webhooks)")
logging.info(
f"YooKassa webhook path: '{settings_param.yookassa_webhook_path}'"
)
logging.info(f"Decision: Run AIOHTTP server: {should_run_aiohttp_server}")
logging.info(f"Decision: Run Telegram Polling: {run_telegram_polling}")
logging.info(f"--- End Bot Run Mode Decision ---") logging.info(f"--- End Bot Run Mode Decision ---")
web_app_runner = None web_app_runner = None
main_tasks = [] main_tasks = []
if should_run_aiohttp_server: # Only run AIOHTTP server for webhook mode
async def web_server_task(): async def web_server_task():
await build_and_start_web_app(dp, bot, settings_param, local_async_session_factory) await build_and_start_web_app(dp, bot, settings_param, local_async_session_factory)
main_tasks.append(asyncio.create_task(web_server_task(), name="AIOHTTPServerTask")) main_tasks.append(asyncio.create_task(web_server_task(), name="AIOHTTPServerTask"))
if run_telegram_polling: logging.info("Starting bot in Webhook mode with AIOHTTP server...")
logging.info("Starting bot in Telegram Polling mode...") logging.info(f"Starting bot with main tasks: {[task.get_name() for task in main_tasks]}")
main_tasks.append(
asyncio.create_task(
dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types()),
name="TelegramPollingTask",
)
)
if not main_tasks:
logging.error(
"Bot is not configured for any run mode (neither Webhook nor Polling). Exiting."
)
await dp.emit_shutdown()
return
logging.info(
f"Starting bot with main tasks: {[task.get_name() for task in main_tasks]}"
)
try: try:
await asyncio.gather(*main_tasks) await asyncio.gather(*main_tasks)
+18
View File
@@ -36,6 +36,24 @@ class ProfileSyncMiddleware(BaseMiddleware):
logging.info( logging.info(
f"ProfileSyncMiddleware: Updated user {tg_user.id} profile fields: {list(update_payload.keys())}" 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: except Exception as e:
logging.error( logging.error(
f"ProfileSyncMiddleware: Failed to sync profile for user {getattr(tg_user, 'id', 'N/A')}: {e}", f"ProfileSyncMiddleware: Failed to sync profile for user {getattr(tg_user, 'id', 'N/A')}: {e}",
+5 -1
View File
@@ -1,4 +1,4 @@
from aiogram import Router from aiogram import Router, F
from bot.handlers.user import user_router_aggregate from bot.handlers.user import user_router_aggregate
from bot.handlers import inline_mode from bot.handlers import inline_mode
@@ -10,6 +10,10 @@ from config.settings import Settings
def build_root_router(settings: Settings) -> Router: def build_root_router(settings: Settings) -> Router:
root = Router(name="root") root = Router(name="root")
# Allow all updates only in private chats (messages, callback queries, etc.)
root.message.filter(F.chat.type == "private")
root.callback_query.filter(F.message.chat.type == "private")
# Public routers # Public routers
root.include_router(user_router_aggregate) root.include_router(user_router_aggregate)
root.include_router(inline_mode.router) root.include_router(inline_mode.router)
+58 -4
View File
@@ -8,9 +8,11 @@ from aiogram.types import InlineKeyboardMarkup
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from typing import Optional from typing import Optional
from config.settings import Settings from config.settings import Settings
from .panel_api_service import PanelApiService
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup from bot.keyboards.inline.user_keyboards import get_subscribe_only_markup
from db.dal import user_dal from db.dal import user_dal
from bot.utils.date_utils import add_months
EVENT_MAP = { EVENT_MAP = {
"user.expires_in_72_hours": (3, "subscription_72h_notification"), "user.expires_in_72_hours": (3, "subscription_72h_notification"),
@@ -19,11 +21,12 @@ EVENT_MAP = {
} }
class PanelWebhookService: 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.bot = bot
self.settings = settings self.settings = settings
self.i18n = i18n self.i18n = i18n
self.async_session_factory = async_session_factory self.async_session_factory = async_session_factory
self.panel_service = panel_service
async def _send_message( async def _send_message(
self, self,
@@ -48,7 +51,7 @@ class PanelWebhookService:
Returns True if an auto-renewal was performed (and renewal message sent), False otherwise. Returns True if an auto-renewal was performed (and renewal message sent), False otherwise.
""" """
from db.dal import subscription_dal, payment_dal from db.dal import subscription_dal, payment_dal
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone
try: try:
auto_renewed = False auto_renewed = False
@@ -68,9 +71,10 @@ class PanelWebhookService:
# This user has tribute payments, auto-renew for the same duration # This user has tribute payments, auto-renew for the same duration
logging.info(f"Auto-renewing tribute subscription for user {user_id} for {last_tribute_duration} months") logging.info(f"Auto-renewing tribute subscription for user {user_id} for {last_tribute_duration} months")
# Extend subscription by the last payment duration # Extend subscription by the last payment duration (calendar months)
new_end_date = datetime.now(timezone.utc) + timedelta(days=last_tribute_duration * 30) new_end_date = add_months(datetime.now(timezone.utc), last_tribute_duration)
# Update local DB subscription
await subscription_dal.update_subscription( await subscription_dal.update_subscription(
session, session,
sub.subscription_id, sub.subscription_id,
@@ -80,6 +84,56 @@ class PanelWebhookService:
'is_active': True '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 # Send auto-renewal notification
_ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k _ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k
+3 -1
View File
@@ -50,7 +50,9 @@ class ReferralService:
# If configured to apply referral bonuses only once per invited user, # If configured to apply referral bonuses only once per invited user,
# check if the referee already has succeeded payments. # check if the referee already has succeeded payments.
if self.settings.REFERRAL_ONE_BONUS_PER_REFEREE: # Use getattr with a safe default (True) to avoid AttributeError if
# running with an older settings schema.
if getattr(self.settings, "REFERRAL_ONE_BONUS_PER_REFEREE", True):
try: try:
succeeded_count = await payment_dal.count_user_succeeded_payments( succeeded_count = await payment_dal.count_user_succeeded_payments(
session, referee_user_id, exclude_payment_id=current_payment_db_id session, referee_user_id, exclude_payment_id=current_payment_db_id
+46 -17
View File
@@ -6,6 +6,7 @@ from aiogram import Bot
from bot.middlewares.i18n import JsonI18n from bot.middlewares.i18n import JsonI18n
from db.dal import user_dal, subscription_dal, promo_code_dal, payment_dal from db.dal import user_dal, subscription_dal, promo_code_dal, payment_dal
from bot.utils.date_utils import add_months
from db.models import User, Subscription from db.models import User, Subscription
from config.settings import Settings from config.settings import Settings
@@ -118,6 +119,11 @@ class SubscriptionService:
creation_response = await self.panel_service.create_panel_user( creation_response = await self.panel_service.create_panel_user(
username_on_panel=panel_username_on_panel_standard, username_on_panel=panel_username_on_panel_standard,
telegram_id=user_id, 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, specific_squad_uuids=self.settings.parsed_user_squad_uuids,
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes, default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY, default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY,
@@ -141,6 +147,11 @@ class SubscriptionService:
creation_response = await self.panel_service.create_panel_user( creation_response = await self.panel_service.create_panel_user(
username_on_panel=panel_username_on_panel_standard, username_on_panel=panel_username_on_panel_standard,
telegram_id=user_id, 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, specific_squad_uuids=self.settings.parsed_user_squad_uuids,
default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes, default_traffic_limit_bytes=self.settings.user_traffic_limit_bytes,
default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY, default_traffic_limit_strategy=self.settings.USER_TRAFFIC_STRATEGY,
@@ -232,23 +243,10 @@ class SubscriptionService:
"panel_user_uuid": actual_panel_uuid_from_api "panel_user_uuid": actual_panel_uuid_from_api
} }
if ( # Do not overwrite Telegram username with panel username.
actual_panel_username_from_api # Only update the local linkage to panel UUID here.
and actual_panel_username_from_api
!= panel_username_on_panel_standard
and (
db_user.username is None
or db_user.username != actual_panel_username_from_api
)
):
update_data_for_local_user["username"] = (
actual_panel_username_from_api
)
await user_dal.update_user(session, user_id, update_data_for_local_user) await user_dal.update_user(session, user_id, update_data_for_local_user)
db_user.panel_user_uuid = actual_panel_uuid_from_api db_user.panel_user_uuid = actual_panel_uuid_from_api
if "username" in update_data_for_local_user:
db_user.username = update_data_for_local_user["username"]
panel_user_created_or_linked_now = True panel_user_created_or_linked_now = True
current_local_panel_uuid = actual_panel_uuid_from_api current_local_panel_uuid = actual_panel_uuid_from_api
else: else:
@@ -270,8 +268,19 @@ class SubscriptionService:
logging.info( logging.info(
f"Panel user {current_local_panel_uuid} has telegramId '{panel_telegram_id_from_api}'. Updating on panel to '{user_id}'." 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( 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( panel_sub_link_id = panel_user_obj_from_api.get(
@@ -368,6 +377,15 @@ class SubscriptionService:
traffic_limit_bytes=self.settings.trial_traffic_limit_bytes, 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( updated_panel_user = await self.panel_service.update_user_details_on_panel(
panel_user_uuid, panel_update_payload panel_user_uuid, panel_update_payload
) )
@@ -437,7 +455,9 @@ class SubscriptionService:
): ):
start_date = current_active_sub.end_date start_date = current_active_sub.end_date
duration_days_total = months * 30 # base duration by months
end_after_months = add_months(start_date, months)
duration_days_total = (end_after_months - start_date).days
applied_promo_bonus_days = 0 applied_promo_bonus_days = 0
if promo_code_id_from_payment: if promo_code_id_from_payment:
@@ -508,6 +528,15 @@ class SubscriptionService:
traffic_limit_bytes=self.settings.user_traffic_limit_bytes, 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( updated_panel_user = await self.panel_service.update_user_details_on_panel(
panel_user_uuid, panel_update_payload panel_user_uuid, panel_update_payload
) )
+263 -1
View File
@@ -1 +1,263 @@
# Bot utilities package # Bot utilities package
from dataclasses import dataclass
from typing import Optional, Dict, Any
from aiogram import types
@dataclass
class MessageContent:
"""Класс для хранения информации о контенте сообщения"""
content_type: str
file_id: Optional[str] = None
text: Optional[str] = None
# Словари поддерживаемых параметров для каждого типа сообщения
SUPPORTED_PARAMS = {
"text": {"parse_mode", "entities", "disable_web_page_preview", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"photo": {"caption", "parse_mode", "caption_entities", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id", "has_spoiler"},
"video": {"duration", "width", "height", "thumbnail", "caption", "parse_mode", "caption_entities", "supports_streaming", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id", "has_spoiler"},
"animation": {"duration", "width", "height", "thumbnail", "caption", "parse_mode", "caption_entities", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id", "has_spoiler"},
"document": {"thumbnail", "caption", "parse_mode", "caption_entities", "disable_content_type_detection", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"audio": {"caption", "parse_mode", "caption_entities", "duration", "performer", "title", "thumbnail", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"voice": {"caption", "parse_mode", "caption_entities", "duration", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"sticker": {"disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
"video_note": {"duration", "length", "thumbnail", "disable_notification", "protect_content", "reply_markup", "reply_to_message_id", "allow_sending_without_reply", "message_thread_id"},
}
def filter_kwargs(content_type: str, kwargs: Dict[str, Any]) -> Dict[str, Any]:
"""Фильтрует kwargs, оставляя только поддерживаемые параметры для данного типа сообщения"""
supported = SUPPORTED_PARAMS.get(content_type, set())
return {k: v for k, v in kwargs.items() if k in supported}
def get_message_content(message: types.Message) -> MessageContent:
"""
Определяет тип контента сообщения и возвращает его данные.
Использует match/case вместо длинных if-elif цепочек.
"""
text = (message.text or message.caption or "").strip()
# Проверяем наличие медиа-контента
media_content = None
if message.photo:
media_content = ("photo", message.photo[-1].file_id)
elif message.video:
media_content = ("video", message.video.file_id)
elif message.animation:
media_content = ("animation", message.animation.file_id)
elif message.document:
media_content = ("document", message.document.file_id)
elif message.audio:
media_content = ("audio", message.audio.file_id)
elif message.voice:
media_content = ("voice", message.voice.file_id)
elif message.sticker:
media_content = ("sticker", message.sticker.file_id)
elif message.video_note:
media_content = ("video_note", message.video_note.file_id)
# Используем match/case для определения типа контента
match media_content:
case (content_type, file_id):
return MessageContent(content_type=content_type, file_id=file_id, text=text)
case None:
return MessageContent(content_type="text", text=text)
case _:
return MessageContent(content_type="text", text=text)
async def send_message_by_type(bot, chat_id: int, content: MessageContent, **kwargs) -> None:
"""
Отправляет сообщение указанного типа.
Использует match/case вместо длинных if-elif цепочек.
Автоматически фильтрует неподдерживаемые параметры.
"""
# Фильтруем kwargs для данного типа сообщения
filtered_kwargs = filter_kwargs(content.content_type, kwargs)
match content.content_type:
case "text":
await bot.send_message(
chat_id=chat_id,
text=content.text,
**filtered_kwargs
)
case "photo":
await bot.send_photo(
chat_id=chat_id,
photo=content.file_id,
caption=content.text or None,
**filtered_kwargs
)
case "video":
await bot.send_video(
chat_id=chat_id,
video=content.file_id,
caption=content.text or None,
**filtered_kwargs
)
case "animation":
await bot.send_animation(
chat_id=chat_id,
animation=content.file_id,
caption=content.text or None,
**filtered_kwargs
)
case "document":
await bot.send_document(
chat_id=chat_id,
document=content.file_id,
caption=content.text or None,
**filtered_kwargs
)
case "audio":
await bot.send_audio(
chat_id=chat_id,
audio=content.file_id,
caption=content.text or None,
**filtered_kwargs
)
case "voice":
await bot.send_voice(
chat_id=chat_id,
voice=content.file_id,
caption=content.text or None,
**filtered_kwargs
)
case "sticker":
await bot.send_sticker(
chat_id=chat_id,
sticker=content.file_id,
**filtered_kwargs
)
case "video_note":
await bot.send_video_note(
chat_id=chat_id,
video_note=content.file_id,
**filtered_kwargs
)
case _:
# Fallback для неизвестных типов - отправляем как текст
text_kwargs = filter_kwargs("text", kwargs)
await bot.send_message(
chat_id=chat_id,
text=content.text or "Unknown content type",
**text_kwargs
)
async def send_message_via_queue(queue_manager, uid: int, content: MessageContent, **kwargs) -> None:
"""
Отправляет сообщение через очередь в зависимости от типа контента.
Использует match/case вместо длинных if-elif цепочек.
Автоматически фильтрует неподдерживаемые параметры.
"""
# Фильтруем kwargs для данного типа сообщения
filtered_kwargs = filter_kwargs(content.content_type, kwargs)
match content.content_type:
case "text":
await queue_manager.send_message(
chat_id=uid, text=content.text, **filtered_kwargs
)
case "photo":
await queue_manager.send_photo(
chat_id=uid, photo=content.file_id, caption=content.text or None, **filtered_kwargs
)
case "video":
await queue_manager.send_video(
chat_id=uid, video=content.file_id, caption=content.text or None, **filtered_kwargs
)
case "animation":
await queue_manager.send_animation(
chat_id=uid, animation=content.file_id, caption=content.text or None, **filtered_kwargs
)
case "document":
await queue_manager.send_document(
chat_id=uid, document=content.file_id, caption=content.text or None, **filtered_kwargs
)
case "audio":
await queue_manager.send_audio(
chat_id=uid, audio=content.file_id, caption=content.text or None, **filtered_kwargs
)
case "voice":
await queue_manager.send_voice(
chat_id=uid, voice=content.file_id, caption=content.text or None, **filtered_kwargs
)
case "sticker":
await queue_manager.send_sticker(
chat_id=uid, sticker=content.file_id, **filtered_kwargs
)
case "video_note":
await queue_manager.send_video_note(
chat_id=uid, video_note=content.file_id, **filtered_kwargs
)
case _:
# Fallback для неизвестных типов - отправляем как текст
text_kwargs = filter_kwargs("text", kwargs)
await queue_manager.send_message(
chat_id=uid, text=content.text or "Unknown content type", **text_kwargs
)
async def send_direct_message(bot, chat_id: int, content: MessageContent, extra_text: str = "", **kwargs) -> None:
"""
Отправляет прямое сообщение с дополнительной обработкой для sticker и video_note.
Для этих типов медиа отправляется отдельное текстовое сообщение, т.к. они не поддерживают caption.
Автоматически фильтрует неподдерживаемые параметры.
"""
match content.content_type:
case "sticker":
# Отправляем стикер с отфильтрованными параметрами
sticker_kwargs = filter_kwargs("sticker", kwargs)
await bot.send_sticker(
chat_id=chat_id,
sticker=content.file_id,
**sticker_kwargs
)
# Если есть текст с подписью, отправляем отдельно
if content.text or extra_text:
text_to_send = (content.text + extra_text) if content.text else extra_text
text_kwargs = filter_kwargs("text", kwargs)
await bot.send_message(
chat_id,
text_to_send,
**text_kwargs
)
case "video_note":
# Отправляем видео-заметку с отфильтрованными параметрами
video_note_kwargs = filter_kwargs("video_note", kwargs)
await bot.send_video_note(
chat_id=chat_id,
video_note=content.file_id,
**video_note_kwargs
)
# Если есть текст с подписью, отправляем отдельно
if content.text or extra_text:
text_to_send = (content.text + extra_text) if content.text else extra_text
text_kwargs = filter_kwargs("text", kwargs)
await bot.send_message(
chat_id,
text_to_send,
**text_kwargs
)
case "text":
# Для текста объединяем с extra_text
final_text = (content.text + extra_text) if content.text else extra_text
text_kwargs = filter_kwargs("text", kwargs)
await bot.send_message(
chat_id=chat_id,
text=final_text,
**text_kwargs
)
case _:
# Для остальных типов медиа используем caption
final_caption = (content.text + extra_text) if content.text else None
await send_message_by_type(
bot, chat_id,
MessageContent(content.content_type, content.file_id, final_caption),
**kwargs
)
+27
View File
@@ -0,0 +1,27 @@
from datetime import datetime, timedelta
def add_months(base_dt: datetime, months_to_add: int) -> datetime:
"""Add calendar months to a datetime, clamping the day to the month's length.
Preserves tzinfo from base_dt.
"""
year = base_dt.year
month = base_dt.month + months_to_add
day = base_dt.day
# Normalize year and month
year += (month - 1) // 12
month = ((month - 1) % 12) + 1
# Determine last day of target month by rolling to next month's first day and subtracting 1 day
if month == 12:
next_month_first = datetime(year + 1, 1, 1, tzinfo=base_dt.tzinfo)
else:
next_month_first = datetime(year, month + 1, 1, tzinfo=base_dt.tzinfo)
last_day = (next_month_first - timedelta(days=1)).day
clamped_day = min(day, last_day)
return base_dt.replace(year=year, month=month, day=clamped_day)
+70
View File
@@ -151,6 +151,76 @@ class MessageQueueManager:
) )
await queue.add_message(message) await queue.add_message(message)
async def send_photo(self, chat_id: int, **kwargs) -> None:
"""Queue a send_photo call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_photo',
kwargs=kwargs
)
await queue.add_message(message)
async def send_video(self, chat_id: int, **kwargs) -> None:
"""Queue a send_video call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_video',
kwargs=kwargs
)
await queue.add_message(message)
async def send_animation(self, chat_id: int, **kwargs) -> None:
"""Queue a send_animation (GIF) call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_animation',
kwargs=kwargs
)
await queue.add_message(message)
async def send_audio(self, chat_id: int, **kwargs) -> None:
"""Queue a send_audio call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_audio',
kwargs=kwargs
)
await queue.add_message(message)
async def send_voice(self, chat_id: int, **kwargs) -> None:
"""Queue a send_voice call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_voice',
kwargs=kwargs
)
await queue.add_message(message)
async def send_sticker(self, chat_id: int, **kwargs) -> None:
"""Queue a send_sticker call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_sticker',
kwargs=kwargs
)
await queue.add_message(message)
async def send_video_note(self, chat_id: int, **kwargs) -> None:
"""Queue a send_video_note call"""
queue = self.group_queue if self._is_group_chat(chat_id) else self.user_queue
message = QueuedMessage(
chat_id=chat_id,
method_name='send_video_note',
kwargs=kwargs
)
await queue.add_message(message)
async def answer_callback_query(self, callback_query_id: str, **kwargs) -> None: async def answer_callback_query(self, callback_query_id: str, **kwargs) -> None:
"""Send callback query answer immediately (not rate limited)""" """Send callback query answer immediately (not rate limited)"""
await self.bot.answer_callback_query(callback_query_id, **kwargs) await self.bot.answer_callback_query(callback_query_id, **kwargs)
+11
View File
@@ -251,3 +251,14 @@ async def get_last_tribute_payment_duration(session: AsyncSession, user_id: int)
result = await session.execute(stmt) result = await session.execute(stmt)
return result.scalar_one_or_none() 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()
+33 -10
View File
@@ -4,7 +4,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select from sqlalchemy.future import select
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from sqlalchemy import update, delete, func, and_ from sqlalchemy import update, delete, func, and_
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy.dialects.postgresql import insert as pg_insert
from ..models import User, Subscription from ..models import User, Subscription
@@ -33,19 +34,41 @@ async def get_user_by_panel_uuid(
## Removed unused generic get_user helper to keep DAL explicit and simple ## Removed unused generic get_user helper to keep DAL explicit and simple
async def create_user(session: AsyncSession, user_data: Dict[str, Any]) -> User: async def create_user(session: AsyncSession, user_data: Dict[str, Any]) -> Tuple[User, bool]:
"""Create a user if not exists in a race-safe way.
Returns a tuple of (user, created_flag).
"""
if "registration_date" not in user_data: if "registration_date" not in user_data:
user_data["registration_date"] = datetime.now() user_data["registration_date"] = datetime.now(timezone.utc)
new_user = User(**user_data) # Use PostgreSQL upsert to avoid IntegrityError on concurrent inserts
session.add(new_user) stmt = (
await session.flush() pg_insert(User)
await session.refresh(new_user) .values(**user_data)
logging.info( .on_conflict_do_nothing(index_elements=[User.user_id])
f"New user {new_user.user_id} created in DAL. Referred by: {new_user.referred_by_id or 'N/A'}." .returning(User.user_id)
) )
return new_user
result = await session.execute(stmt)
inserted_row = result.first()
created = inserted_row is not None
# Fetch the user (inserted just now or pre-existing)
user_id: int = user_data["user_id"]
user = await get_user_by_id(session, user_id)
if created and user is not None:
logging.info(
f"New user {user.user_id} created in DAL. Referred by: {user.referred_by_id or 'N/A'}."
)
elif user is not None:
logging.info(
f"User {user.user_id} already exists in DAL. Proceeding without creation."
)
return user, created
async def update_user( async def update_user(
+55 -192
View File
@@ -11,18 +11,20 @@
"menu_server_status_button": "📊 Status", "menu_server_status_button": "📊 Status",
"menu_support_button": "💬 Support", "menu_support_button": "💬 Support",
"menu_terms_button": "📄 Terms of Service", "menu_terms_button": "📄 Terms of Service",
"back_to_main_menu_button": "⬅️ Back", "back_to_main_menu_button": "⬅️ Back",
"choose_language": "Choose language / Выберите язык:", "choose_language": "Choose language:",
"language_set_alert": "Language changed!", "language_set_alert": "Language changed!",
"error_occurred_try_again": "An error occurred, please try again.", "error_occurred_try_again": "An error occurred, please try again.",
"error_try_again": "Please try again.", "error_try_again": "Please try again.",
"error_displaying_menu": "Error displaying menu.", "error_displaying_menu": "Error displaying menu.",
"main_menu_unknown_action": "Unknown action.", "main_menu_unknown_action": "Unknown action.",
"select_subscription_period": "Select subscription period:", "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}", "subscribe_for_months_button": "{months} mo. - {price} {currency_symbol}",
"choose_payment_method": "Choose payment method:", "choose_payment_method": "Choose payment method:",
"pay_button": "💳 Pay", "pay_button": "💳 Pay",
"pay_with_yookassa_button": "💳 YooKassa", "pay_with_yookassa_button": "💳 YooKassa",
@@ -32,63 +34,34 @@
"connect_button": "🔗 Connect", "connect_button": "🔗 Connect",
"cancel_button": "❌ Cancel", "cancel_button": "❌ Cancel",
"payment_description_subscription": "Subscription payment for {months} mo.", "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:", "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_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<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇", "payment_successful_full": "✅ Payment successful!\nYour {months}-month subscription is active until {end_date}.\n\nConnection key:\n<code>{config_link}</code>\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<code>{config_link}</code>\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<code>{config_link}</code>\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<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇",
"payment_failed": "❌ Payment failed or was cancelled. Please try again or contact support.", "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": "<b>🔐 My Subscription:</b>\n\nStatus: <b>{status}</b>\nActive until: <b>{end_date}</b> (days left: {days_left})\n\nTraffic: <b>{traffic_used}</b> of <b>{traffic_limit}</b>\n\nConfiguration link:\n<code>{config_link}</code>",
"config_link_not_available": "not available, contact support", "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_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_prompt": "Please enter your promo code:",
"promo_code_not_found": "Promo code <code>{code}</code> not found, expired, or already used the maximum number of times.", "promo_code_not_found": "Promo code <code>{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>{code}</code>.", "promo_code_already_used_by_user": "You have already used promo code <code>{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<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇", "promo_code_applied_success_full": "✅ Promo code applied successfully!\nSubscription active until {end_date}.\n\nConnection key:\n<code>{config_link}</code>\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.", "error_applying_promo_bonus": "Failed to apply promo bonus. Please try again later or contact support.",
"promo_input_cancelled_short": "Promo code entry cancelled.", "promo_input_cancelled_short": "Promo code entry cancelled.",
"trial_feature_disabled": "Free trial is currently unavailable.", "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_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_confirm_activate_button": "✅ Activate!",
"trial_activated_alert": "Trial activated!", "trial_activated_alert": "Trial activated!",
"trial_activated_details_message": "🚀 Your {days}-day free trial is activated!\n\nValid until: <b>{end_date}</b>\nTraffic: <b>{traffic_gb} GB</b>\n\nYour configuration link:\n<code>{config_link}</code>", "trial_activated_details_message": "✅ Trial activated!\nYour {days}-day trial is active until {end_date}.\n\nConnection key:\n<code>{config_link}</code>\n\nTo connect, open the link and follow the instructions 👇",
"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", "yes_button": "Yes",
"no_button": "No", "no_button": "No",
"trial_cancelled_short": "Activation cancelled.",
"referral_program_info_new": "🎁 <b>Referral Program</b>\n\n📊 <b>Your stats:</b>\n👥 Friends invited: <b>{invited_count}</b>\n💳 Purchased subscription: <b>{purchased_count}</b>\n\n🔗 Your link:\n<code>{referral_link}</code>\n\n💰 <b>Invitation bonuses:</b>\n{bonus_details}\n\n📢 Share the link with friends and get bonuses!", "referral_program_info_new": "🎁 <b>Referral Program</b>\n\n📊 <b>Your stats:</b>\n👥 Friends invited: <b>{invited_count}</b>\n💳 Purchased subscription: <b>{purchased_count}</b>\n\n🔗 Your link:\n<code>{referral_link}</code>\n\n💰 <b>Invitation bonuses:</b>\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: <b>{inviter_bonus_days} days</b>\n ➢ Friend: <b>{referee_bonus_days} days</b>", "referral_bonus_per_period": "\n\n🎁 For a friend's {months}-month subscription:\n ➢ You: <b>{inviter_bonus_days} days</b>\n ➢ Friend: <b>{referee_bonus_days} days</b>",
"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_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}", "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", "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_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}.", "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.", "user_is_banned": "🚫 Your account is banned. Please contact support.",
"admin_panel_title": "Admin Panel", "admin_panel_title": "Admin Panel",
@@ -96,15 +69,12 @@
"admin_broadcast_button": "📢 Broadcast", "admin_broadcast_button": "📢 Broadcast",
"admin_create_promo_button": "🎁 Create Promo", "admin_create_promo_button": "🎁 Create Promo",
"admin_create_bulk_promo_button": "📦 Bulk Create", "admin_create_bulk_promo_button": "📦 Bulk Create",
"admin_manage_promos_button": "🛠 Manage",
"admin_view_promos_button": "👀 List",
"admin_ban_user_button": "🚫 Ban", "admin_ban_user_button": "🚫 Ban",
"admin_unban_user_button": "✅ Unban", "admin_unban_user_button": "✅ Unban",
"admin_view_banned_users_button": "📜 Ban List", "admin_view_banned_users_button": "📜 Ban List",
"admin_view_logs_menu_button": "📄 Logs", "admin_view_logs_menu_button": "📄 Logs",
"admin_sync_panel_button": "🔄 Sync", "admin_sync_panel_button": "🔄 Sync",
"admin_unknown_action": "Unknown admin action.", "admin_unknown_action": "Unknown admin action.",
"admin_stats_and_monitoring_section": "📊 Statistics", "admin_stats_and_monitoring_section": "📊 Statistics",
"admin_user_management_section": "👥 Users", "admin_user_management_section": "👥 Users",
"admin_promo_marketing_section": "🎁 Promos", "admin_promo_marketing_section": "🎁 Promos",
@@ -112,18 +82,13 @@
"admin_ban_management_section": "🚫 Bans", "admin_ban_management_section": "🚫 Bans",
"admin_users_management_button": "👤 Management", "admin_users_management_button": "👤 Management",
"back_to_user_management_button": "⬅️ To Users", "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", "back_to_admin_panel_button": "⬅️ To Admin",
"admin_stats_header": "📊 Bot Statistics", "admin_stats_header": "📊 Bot Statistics",
"admin_enhanced_users_stats_header": "Users", "admin_enhanced_users_stats_header": "Users",
"admin_financial_stats_header": "Financial Statistics", "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_recent_payments_header": "Recent Payments:",
"admin_stats_payment_item": "{status_emoji} {amount} {currency} from {user_info} ({p_status}) [{p_date}]", "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_stats_no_payments_found": "No payments found yet.",
"admin_view_payments_button": "💰 Payments", "admin_view_payments_button": "💰 Payments",
"admin_payments_header": "💰 <b>All Payments</b>", "admin_payments_header": "💰 <b>All Payments</b>",
"admin_no_payments_found": "No payments found.", "admin_no_payments_found": "No payments found.",
@@ -132,7 +97,6 @@
"admin_no_payments_to_export": "No payments to export.", "admin_no_payments_to_export": "No payments to export.",
"admin_payments_export_success": "📊 Payments export completed!\nTotal records: {count}", "admin_payments_export_success": "📊 Payments export completed!\nTotal records: {count}",
"admin_export_sent": "File sent!", "admin_export_sent": "File sent!",
"admin_csv_payment_id": "ID", "admin_csv_payment_id": "ID",
"admin_csv_user_id": "User ID", "admin_csv_user_id": "User ID",
"admin_csv_username": "Username", "admin_csv_username": "Username",
@@ -152,9 +116,8 @@
"admin_stats_sync_subs_synced": "Subscriptions Synced", "admin_stats_sync_subs_synced": "Subscriptions Synced",
"admin_stats_sync_details_label": "Details", "admin_stats_sync_details_label": "Details",
"admin_sync_status_never_run": "Panel sync never run.", "admin_sync_status_never_run": "Panel sync never run.",
"admin_broadcast_enter_message": "Enter the broadcast message (HTML supported):", "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_all_button": "👥 All",
"broadcast_target_active_button": "✅ Active", "broadcast_target_active_button": "✅ Active",
"broadcast_target_inactive_button": "⌛ Inactive", "broadcast_target_inactive_button": "⌛ Inactive",
@@ -163,39 +126,47 @@
"admin_broadcast_sending_started": "Starting broadcast...", "admin_broadcast_sending_started": "Starting broadcast...",
"admin_broadcast_error_no_message": "Error: no message to broadcast.", "admin_broadcast_error_no_message": "Error: no message to broadcast.",
"admin_broadcast_error_no_message_alert": "Broadcast message is empty!", "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": "Broadcast cancelled.",
"admin_broadcast_cancelled_alert": "Broadcast cancelled!", "admin_broadcast_cancelled_alert": "Broadcast cancelled!",
"admin_broadcast_cancelled_nav_back": "Broadcast cancelled. You are returned to the admin panel.", "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_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: <code>{example_format}</code>\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 330 alphanumeric characters.", "admin_promo_invalid_code_format": "Code must be 330 alphanumeric characters.",
"admin_promo_invalid_bonus_days": "Bonus days must be a positive number.", "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_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_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>{code}</code> created successfully!\nBonus: {bonus_days} days\nMax uses: {max_activations}\nValid until: {valid_until_str}.", "admin_promo_created_success": "✅ Promo code <code>{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>{code}</code> already exists.", "admin_promo_set_validity_days": "⏰ Set validity (days)",
"admin_promo_creation_failed": "❌ Failed to create promo code. Please try again later.", "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: <b>{created}</b> of <b>{total}</b>",
"admin_bulk_promo_settings": "🎁 Bonus days: <b>{bonus_days}</b>\n📊 Max activations: <b>{max_activations}</b>\n⏰ Validity: <b>{validity}</b>",
"admin_promo_list_page_info": "Page {current}/{total} ({count} promo codes)",
"admin_queue_status_button": "📊 Queue Status",
"admin_queue_status_title": "📊 Message Queue Status",
"admin_queue_status_info": "📤 <b>Message Queues:</b>\n\n👥 <b>Users (25 msg/sec):</b>\n 📋 In queue: {user_queue_size}\n 🔄 Processing: {user_processing}\n 📈 Sent per minute: {user_recent}\n\n📢 <b>Groups/channels (15 msg/min):</b>\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_active_promos_list_header": "Active Promo Codes:",
"admin_no_active_promos": "No active promo codes.", "admin_no_active_promos": "No active promo codes.",
"admin_promo_list_item": "<code>{code}</code>: +{bonus}d, {current}/{max} used, valid until {valid_until}",
"admin_promo_valid_indefinitely": "indefinite", "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_edit_button": "✏️ Edit",
"admin_promo_delete_button": "🗑 Delete", "admin_promo_delete_button": "🗑 Delete",
"admin_promo_edit_prompt": "Send new details for <code>{code}</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_deleted_success": "Promo {code} deleted.",
"admin_promo_not_found": "Promo not found.", "admin_promo_not_found": "Promo not found.",
"admin_promo_export_csv_button": "📄 Export to CSV", "admin_promo_export_csv_button": "📄 Export to CSV",
"admin_promo_export_caption": "📄 Activations for promo code {code}", "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_edit_select_field": "Select a field to edit:",
"admin_promo_prompt_bonus_days": "Enter the new number of bonus days:", "admin_promo_prompt_bonus_days": "Enter the new number of bonus days:",
"admin_promo_prompt_max_activations": "Enter the new maximum number of activations:", "admin_promo_prompt_max_activations": "Enter the new maximum number of activations:",
@@ -205,69 +176,34 @@
"admin_promo_edit_bonus_days": "🎁 Bonus Days", "admin_promo_edit_bonus_days": "🎁 Bonus Days",
"admin_promo_edit_max_activations": "🔢 Max Activations", "admin_promo_edit_max_activations": "🔢 Max Activations",
"admin_promo_edit_validity": "⏰ Validity", "admin_promo_edit_validity": "⏰ Validity",
"admin_ban_user_prompt": "Enter user ID or @username to ban:", "admin_ban_user_prompt": "Enter user ID or @username to ban:",
"admin_user_not_found_in_bot_db": "User <code>{user_id}</code> 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_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_unban_user_prompt": "Enter user ID or @username to unban:",
"admin_user_not_banned": "User {user_id_or_username} is not banned.", "admin_user_not_banned": "⚠️ User 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_banned_user_button_text": "{user_display} (ID: {user_id})", "admin_banned_user_button_text": "{user_display} (ID: {user_id})",
"prev_page_button": "⬅️ Prev.", "prev_page_button": "⬅️ Prev.",
"next_page_button": "Next ➡️", "next_page_button": "Next ➡️",
"admin_user_card_title": "User Profile: {user_display}", "admin_user_card_title": "User Card",
"user_card_info": "ID: <code>{user_id}</code>\nUsername: @{username}\nName: {first_name} {last_name}\nLanguage: {language_code}\nPanel UUID: <code>{panel_user_uuid}</code>\nStatus: <b>{ban_status}</b>\nRegistered: {reg_date}\nSubscription until: <b>{sub_end_date}</b>",
"user_card_banned": "BANNED",
"user_card_active": "Active",
"user_card_sub_na": "n/a",
"user_card_ban_button": "🚫 Ban", "user_card_ban_button": "🚫 Ban",
"user_card_unban_button": "✅ Unban", "user_card_unban_button": "✅ Unban",
"user_card_back_to_banned_list_button": "⬅️ Back to Ban List", "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_logs_menu_title": "Logs Menu:",
"admin_view_all_logs_button": "📜 All Message Logs", "admin_view_all_logs_button": "📜 All Message Logs",
"admin_view_user_logs_prompt_button": "👤 User Logs", "admin_view_user_logs_prompt_button": "👤 User Logs",
"admin_export_logs_csv_button": "📄 Export to CSV", "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_no_logs_found": "No logs found.",
"admin_log_entry_format": "<code>{timestamp_str}</code> - <b>{user_display}</b> (ID: {user_id})\n <i>{event_type}</i>: {content_preview}", "admin_log_entry_format": "<code>{timestamp_str}</code> - <b>{user_display}</b> (ID: {user_id})\n <i>{event_type}</i>: {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_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_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_started_simple": "🔄 Starting synchronization...",
"sync_success_simple": "✅ Synchronization completed successfully", "sync_success_simple": "✅ Synchronization completed successfully",
"sync_failed_simple": "❌ Synchronization failed", "sync_failed_simple": "❌ Synchronization failed",
"sync_errors_simple": "⚠️ Synchronization completed with errors ({errors_count} errors)", "sync_errors_simple": "⚠️ Synchronization completed with errors ({errors_count} errors)",
"sync_critical_error": "❌ Critical synchronization error", "sync_critical_error": "❌ Critical synchronization error",
"no_errors_placeholder": "none",
"admin_sync_initiated_from_panel": "Sync initiated...", "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_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.", "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_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_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_24h_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expires in 1 day — {end_date}.\n\nPlease renew it using the button below.",
@@ -275,15 +211,7 @@
"subscription_expired_yesterday_notification": "👋 Hi, {user_name}!\n\n⏳ Your VPN subscription expired yesterday ({end_date}).\n\nPlease renew it using the button below.", "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": "🚨 <b>Subscription Cancelled</b>\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_subscription_cancelled": "🚨 <b>Subscription Cancelled</b>\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": "🔄 <b>Subscription Auto-Renewed</b>\n\nYour Tribute subscription has been automatically renewed for {months} months.\nNew expiration date: {end_date}", "tribute_auto_renewal": "🔄 <b>Subscription Auto-Renewed</b>\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_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_subscription_info": "Subscription Information:",
"admin_user_reset_trial_button": "🔄 Reset Trial", "admin_user_reset_trial_button": "🔄 Reset Trial",
"admin_user_add_subscription_button": " Add Days", "admin_user_add_subscription_button": " Add Days",
@@ -294,7 +222,6 @@
"admin_user_search_new_button": "🔍 Find Another", "admin_user_search_new_button": "🔍 Find Another",
"admin_user_view_all_logs_button": "📋 All Actions", "admin_user_view_all_logs_button": "📋 All Actions",
"admin_user_back_to_card_button": "🔙 Back to Card", "admin_user_back_to_card_button": "🔙 Back to Card",
"admin_user_not_found": "❌ User not found: {input}", "admin_user_not_found": "❌ User not found: {input}",
"admin_user_not_found_action": "User not found", "admin_user_not_found_action": "User not found",
"admin_user_card_error": "❌ Error displaying user card", "admin_user_card_error": "❌ Error displaying user card",
@@ -310,7 +237,6 @@
"admin_user_unban_success": "✅ User {input} has been unbanned", "admin_user_unban_success": "✅ User {input} has been unbanned",
"admin_user_ban_error": "❌ Error banning user", "admin_user_ban_error": "❌ Error banning user",
"admin_user_unban_error": "❌ Error unbanning 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_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_too_long": "❌ Message too long (maximum 4000 characters)",
"admin_user_message_sent_success": "✅ Message sent to user {user_id}", "admin_user_message_sent_success": "✅ Message sent to user {user_id}",
@@ -318,27 +244,13 @@
"admin_user_no_logs": "📜 User has no actions", "admin_user_no_logs": "📜 User has no actions",
"admin_user_logs_error": "❌ Error loading user actions", "admin_user_logs_error": "❌ Error loading user actions",
"admin_direct_message_signature": "\n\n---\n💬 Message from administrator", "admin_direct_message_signature": "\n\n---\n💬 Message from administrator",
"inline_user_stats_message": "👥 <b>User Statistics</b>\n\n📊 Total: <b>{total}</b>\n📈 Active today: <b>{active_today}</b>\n💳 With paid subscription: <b>{paid}</b>\n🆓 On trial period: <b>{trial}</b>\n😴 Inactive: <b>{inactive}</b>\n🚫 Banned: <b>{banned}</b>\n🎁 Via referral program: <b>{referral}</b>", "inline_user_stats_message": "👥 <b>User Statistics</b>\n\n📊 Total: <b>{total}</b>\n📈 Active today: <b>{active_today}</b>\n💳 With paid subscription: <b>{paid}</b>\n🆓 On trial period: <b>{trial}</b>\n😴 Inactive: <b>{inactive}</b>\n🚫 Banned: <b>{banned}</b>\n🎁 Via referral program: <b>{referral}</b>",
"inline_user_stats_description": "Total: {total}, Paid: {active}",
"inline_admin_user_stats_title": "👥 User Statistics", "inline_admin_user_stats_title": "👥 User Statistics",
"inline_admin_user_stats_desc": "Total: {total}, Active: {paid}",
"inline_financial_stats_message": "💰 <b>Financial Statistics</b>\n\n📅 Today: <b>{today:.2f} RUB</b>\n ({today_count} payments)\n📅 Week: <b>{week:.2f} RUB</b>\n📅 Month: <b>{month:.2f} RUB</b>\n🏆 All time: <b>{all_time:.2f} RUB</b>", "inline_financial_stats_message": "💰 <b>Financial Statistics</b>\n\n📅 Today: <b>{today:.2f} RUB</b>\n ({today_count} payments)\n📅 Week: <b>{week:.2f} RUB</b>\n📅 Month: <b>{month:.2f} RUB</b>\n🏆 All time: <b>{all_time:.2f} RUB</b>",
"inline_admin_financial_stats_title": "💰 Financial Statistics", "inline_admin_financial_stats_title": "💰 Financial Statistics",
"inline_admin_financial_stats_desc": "Today: {today:.2f} RUB",
"inline_system_stats_message": "🖥 <b>Panel Statistics</b>\n\n🟢 Online: <b>{online}</b>\n📊 Active: <b>{active}</b>\n🔴 Disabled: <b>{disabled}</b>\n⏰ Expired: <b>{expired}</b>\n⚠️ Limited: <b>{limited}</b>\n👥 Total users: <b>{total}</b>\n💾 RAM Usage: <b>{memory:.1f}%</b>\n📊 Week traffic: <b>{week_traffic}</b>\n📊 Month traffic: <b>{month_traffic}</b>\n🔗 Active nodes: <b>{active_nodes}/{total_nodes}</b>", "inline_system_stats_message": "🖥 <b>Panel Statistics</b>\n\n🟢 Online: <b>{online}</b>\n📊 Active: <b>{active}</b>\n🔴 Disabled: <b>{disabled}</b>\n⏰ Expired: <b>{expired}</b>\n⚠️ Limited: <b>{limited}</b>\n👥 Total users: <b>{total}</b>\n💾 RAM Usage: <b>{memory:.1f}%</b>\n📊 Week traffic: <b>{week_traffic}</b>\n📊 Month traffic: <b>{month_traffic}</b>\n🔗 Active nodes: <b>{active_nodes}/{total_nodes}</b>",
"inline_admin_system_stats_title": "🖥 System Statistics", "inline_admin_system_stats_title": "🖥 System Statistics",
"inline_admin_system_stats_desc": "Online: {online}, Offline: {offline}",
"inline_admin_help_message": "🤖 <b>Bot Inline Mode</b>\n\n📱 <b>Available commands:</b>\n\n🎁 <b>ref</b> - share referral link\n👥 <b>stat</b> - user statistics\n💰 <b>financial</b> - financial statistics\n🖥 <b>system</b> - 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": "🤖 <b>Bot Inline Mode</b>\n\n📱 <b>Available commands:</b>\n\n🎁 <b>ref</b> - 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_referral_suffix": " (referral from {referrer_id})",
"log_new_user_registration": "👤 <b>New User</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Name: {user_display}{referral_text}\n📅 Time: {timestamp}", "log_new_user_registration": "👤 <b>New User</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Name: {user_display}{referral_text}\n📅 Time: {timestamp}",
"log_payment_received": "{provider_emoji} <b>Payment Received</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n📅 Period: <b>{months} mo.</b>\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}", "log_payment_received": "{provider_emoji} <b>Payment Received</b>\n\n👤 User: {user_display}\n💰 Amount: <b>{amount} {currency}</b>\n📅 Period: <b>{months} mo.</b>\n🏦 Provider: {payment_provider}\n🕐 Time: {timestamp}",
@@ -346,28 +258,10 @@
"log_trial_activation": "🆓 <b>Trial Activated</b>\n\n👤 User: {user_display}\n⏰ Valid until: <b>{end_date}</b>\n🕐 Time: {timestamp}", "log_trial_activation": "🆓 <b>Trial Activated</b>\n\n👤 User: {user_display}\n⏰ Valid until: <b>{end_date}</b>\n🕐 Time: {timestamp}",
"log_panel_sync": "{status_emoji} <b>Panel Synchronization</b>\n\n📊 Status: <b>{status}</b>\n👥 Users processed: <b>{users_processed}</b>\n📋 Subscriptions synced: <b>{subs_synced}</b>\n🕐 Time: {timestamp}\n\n📝 Details:\n{details}", "log_panel_sync": "{status_emoji} <b>Panel Synchronization</b>\n\n📊 Status: <b>{status}</b>\n👥 Users processed: <b>{users_processed}</b>\n📋 Subscriptions synced: <b>{subs_synced}</b>\n🕐 Time: {timestamp}\n\n📝 Details:\n{details}",
"log_suspicious_promo": "⚠️ <b>Suspicious Promo Code Attempt</b>\n\n👤 User: {user_display}\n🆔 ID: <code>{user_id}</code>\n📝 Input: <pre>{suspicious_input}</pre>\n🕐 Time: {timestamp}", "log_suspicious_promo": "⚠️ <b>Suspicious Promo Code Attempt</b>\n\n👤 User: {user_display}\n🆔 ID: <code>{user_id}</code>\n📝 Input: <pre>{suspicious_input}</pre>\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:\n<code>quantity|days|description</code>\n\nExample:\n<code>10|7|Weekly promo</code>",
"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": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 1 of 4:</b> Promo Code\n\nCurrent code: <b>{current_code}</b>\n\nEnter new promo code (3-30 characters, letters and numbers only) or send current one to keep:",
"admin_promo_edit_step2_bonus_days": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 2 of 4:</b> Bonus Days\n\nCode: <b>{code}</b>\nCurrent days: <b>{current_days}</b>\n\nEnter number of bonus days (1-365):",
"admin_promo_edit_step3_max_activations": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 3 of 4:</b> Max Activations\n\nCode: <b>{code}</b>\nDays: <b>{days}</b>\nCurrent activations: <b>{current_max}</b>\n\nEnter maximum number of activations (1-10000):",
"admin_promo_edit_step4_validity": "✏️ <b>Editing Promo Code</b>\n\n<b>Step 4 of 4:</b> Validity Period\n\nCode: <b>{code}</b>\nDays: <b>{days}</b>\nActivations: <b>{max_act}</b>\nCurrent validity: <b>{current_validity}</b>\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: <b>{code}</b>\n🎁 Bonus days: <b>{days}</b>\n🔢 Max activations: <b>{max_act}</b>\n⏰ Valid until: <b>{validity}</b>",
"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_started": "📄 Starting log export to CSV...",
"admin_logs_csv_export_success": "✅ Logs exported! File attached above.", "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_log_id": "Log ID",
"admin_csv_header_timestamp": "Timestamp", "admin_csv_header_timestamp": "Timestamp",
"admin_csv_header_user_id": "User ID", "admin_csv_header_user_id": "User ID",
@@ -378,44 +272,23 @@
"admin_csv_header_is_admin_event": "Admin Event", "admin_csv_header_is_admin_event": "Admin Event",
"admin_csv_header_target_user_id": "Target User ID", "admin_csv_header_target_user_id": "Target User ID",
"admin_csv_header_raw_update_preview": "Update Preview", "admin_csv_header_raw_update_preview": "Update Preview",
"admin_banned_users_empty": "📋 Banned Users\n\nList is empty", "admin_banned_users_empty": "📋 Banned Users\n\nList is empty",
"admin_banned_users_list": "📋 Banned Users ({count}):\n\n{users}", "admin_banned_users_list": "📋 Banned Users ({count}):\n\n{users}",
"admin_panel_stats_header": "Panel Statistics", "admin_panel_stats_header": "Panel Statistics",
"admin_bulk_promo_step1_quantity": "📦 <b>Bulk Promo Code Creation</b>\n\n<b>Step 1 of 4:</b> Quantity\n\nEnter the number of promo codes to create (1-1000):", "admin_bulk_promo_step1_quantity": "📦 <b>Bulk Promo Code Creation</b>\n\n<b>Step 1 of 4:</b> Quantity\n\nEnter the number of promo codes to create (1-1000):",
"admin_bulk_promo_step2_bonus_days": "📦 <b>Bulk Promo Code Creation</b>\n\n<b>Step 2 of 4:</b> Bonus Days\n\nQuantity: <b>{quantity}</b>\n\nEnter the number of bonus days (1-365):", "admin_bulk_promo_step2_bonus_days": "📦 <b>Bulk Promo Code Creation</b>\n\n<b>Step 2 of 4:</b> Bonus Days\n\nQuantity: <b>{quantity}</b>\n\nEnter the number of bonus days (1-365):",
"admin_bulk_promo_step3_max_activations": "📦 <b>Bulk Promo Code Creation</b>\n\n<b>Step 3 of 4:</b> Max Activations\n\nQuantity: <b>{quantity}</b>\nBonus days: <b>{bonus_days}</b>\n\nEnter the maximum number of activations for each promo code (1-10000):", "admin_bulk_promo_step3_max_activations": "📦 <b>Bulk Promo Code Creation</b>\n\n<b>Step 3 of 4:</b> Max Activations\n\nQuantity: <b>{quantity}</b>\nBonus days: <b>{bonus_days}</b>\n\nEnter the maximum number of activations for each promo code (1-10000):",
"admin_bulk_promo_step4_validity": "📦 <b>Bulk Promo Code Creation</b>\n\n<b>Step 4 of 4:</b> Validity Period\n\nQuantity: <b>{quantity}</b>\nBonus days: <b>{bonus_days}</b>\nMax activations: <b>{max_activations}</b>\n\nChoose the validity period for promo codes:", "admin_bulk_promo_step4_validity": "📦 <b>Bulk Promo Code Creation</b>\n\n<b>Step 4 of 4:</b> Validity Period\n\nQuantity: <b>{quantity}</b>\nBonus days: <b>{bonus_days}</b>\nMax activations: <b>{max_activations}</b>\n\nChoose the validity period for promo codes:",
"admin_bulk_promo_invalid_quantity": "❌ Quantity must be between 1 and 1000", "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_enter_validity_days": "⏰ Enter the number of validity days for promo codes (1-365):",
"admin_bulk_promo_creating": "⏳ Creating {quantity} promo codes...", "admin_bulk_promo_creating": "⏳ Creating {quantity} promo codes...",
"admin_promo_step1_code": "🎟 <b>Create Promo Code</b>\n\n<b>Step 1 of 4:</b> Promo Code\n\nEnter promo code (3-30 characters, letters and numbers only):", "admin_promo_step1_code": "🎟 <b>Create Promo Code</b>\n\n<b>Step 1 of 4:</b> Promo Code\n\nEnter promo code (3-30 characters, letters and numbers only):",
"admin_promo_step2_bonus_days": "🎟 <b>Create Promo Code</b>\n\n<b>Step 2 of 4:</b> Bonus Days\n\nCode: <b>{code}</b>\n\nEnter the number of bonus days (1-365):", "admin_promo_step2_bonus_days": "🎟 <b>Create Promo Code</b>\n\n<b>Step 2 of 4:</b> Bonus Days\n\nCode: <b>{code}</b>\n\nEnter the number of bonus days (1-365):",
"admin_promo_step3_max_activations": "🎟 <b>Create Promo Code</b>\n\n<b>Step 3 of 4:</b> Max Activations\n\nCode: <b>{code}</b>\nBonus days: <b>{bonus_days}</b>\n\nEnter the maximum number of activations (1-10000):", "admin_promo_step3_max_activations": "🎟 <b>Create Promo Code</b>\n\n<b>Step 3 of 4:</b> Max Activations\n\nCode: <b>{code}</b>\nBonus days: <b>{bonus_days}</b>\n\nEnter the maximum number of activations (1-10000):",
"admin_promo_step4_validity": "🎟 <b>Create Promo Code</b>\n\n<b>Step 4 of 4:</b> Validity Period\n\nCode: <b>{code}</b>\nBonus days: <b>{bonus_days}</b>\nMax activations: <b>{max_activations}</b>\n\nChoose the validity period for the promo code:", "admin_promo_step4_validity": "🎟 <b>Create Promo Code</b>\n\n<b>Step 4 of 4:</b> Validity Period\n\nCode: <b>{code}</b>\nBonus days: <b>{bonus_days}</b>\nMax activations: <b>{max_activations}</b>\n\nChoose the validity period for the promo code:",
"admin_promo_code_already_exists": "❌ A promo code with this code already exists", "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_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_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": "🆔 <b>ID:</b>", "admin_user_id_label": "🆔 <b>ID:</b>",
"admin_user_name_label": "👤 <b>Name:</b>", "admin_user_name_label": "👤 <b>Name:</b>",
"admin_user_username_label": "📱 <b>Username:</b>", "admin_user_username_label": "📱 <b>Username:</b>",
@@ -428,7 +301,6 @@
"admin_user_traffic_label": "📊 <b>Traffic:</b>", "admin_user_traffic_label": "📊 <b>Traffic:</b>",
"admin_user_subscription_label": "💼 <b>Subscription:</b>", "admin_user_subscription_label": "💼 <b>Subscription:</b>",
"admin_user_trial_label": "🏡 <b>Trial:</b>", "admin_user_trial_label": "🏡 <b>Trial:</b>",
"admin_user_status_banned": "🚫 Banned", "admin_user_status_banned": "🚫 Banned",
"admin_user_status_active": "✅ Active", "admin_user_status_active": "✅ Active",
"admin_user_trial_used": "Used", "admin_user_trial_used": "Used",
@@ -436,21 +308,12 @@
"admin_user_ban_action_banned": "banned", "admin_user_ban_action_banned": "banned",
"admin_user_ban_action_unbanned": "unbanned", "admin_user_ban_action_unbanned": "unbanned",
"admin_user_na_value": "N/A", "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_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": "📜 <b>Total actions:</b>", "admin_user_actions_count_label": "📜 <b>Total actions:</b>",
"admin_user_subscription_active_until": "⏰ <b>Active until:</b>", "admin_user_subscription_active_until": "⏰ <b>Active until:</b>",
"admin_user_subscription_error": "Loading error", "admin_user_subscription_error": "Loading error",
"admin_bulk_promo_unique_generation_failed": "Failed to create unique promo code",
"admin_promo_management_button": "🎟 Promo Management", "admin_promo_management_button": "🎟 Promo Management",
"admin_promo_management_title": "🎟 <b>Promo Code Management</b>\n\nSelect a promo code for detailed view:", "admin_promo_management_title": "🎟 <b>Promo Code Management</b>\n\nSelect a promo code for detailed view:",
"admin_promo_management_empty": "📭 No promo codes available", "admin_promo_management_empty": "📭 No promo codes available",
"admin_promo_card_title": "🎟 <b>Promo Code: {code}</b>", "admin_promo_card_title": "🎟 <b>Promo Code: {code}</b>",
@@ -470,43 +333,28 @@
"admin_promo_view_activations_button": "📋 Activations", "admin_promo_view_activations_button": "📋 Activations",
"admin_promo_back_to_list_button": "⬅️ Back to list", "admin_promo_back_to_list_button": "⬅️ Back to list",
"admin_promo_toggle_success": "✅ Promo code {code} {status}", "admin_promo_toggle_success": "✅ Promo code {code} {status}",
"admin_promo_toggle_failed": "❌ Error changing promo code status",
"admin_promo_no_activations": "📋 <b>Activations of promo code: {code}</b>\n\n❌ No activations found", "admin_promo_no_activations": "📋 <b>Activations of promo code: {code}</b>\n\n❌ No activations found",
"admin_promo_activations_header": "📋 <b>Activations of promo code: {code}</b>\n\n", "admin_promo_activations_header": "📋 <b>Activations of promo code: {code}</b>\n\n",
"admin_promo_activation_item": "👤 User ID: <b>{user_id}</b>\n📅 Date: <b>{date}</b>\n", "admin_promo_activation_item": "👤 User ID: <b>{user_id}</b>\n📅 Date: <b>{date}</b>\n",
"admin_promo_back_to_detail_button": "⬅️ Back to promo", "admin_promo_back_to_detail_button": "⬅️ Back to promo",
"admin_panel_online_label": "Online", "admin_panel_online_label": "Online",
"admin_panel_active_label": "Active", "admin_panel_active_label": "Active",
"admin_panel_disabled_label": "Disabled", "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_limited_label": "Limited",
"admin_panel_total_users_label": "Total users", "admin_panel_total_users_label": "Total users",
"admin_panel_cpu_usage_label": "CPU Usage",
"admin_panel_memory_usage_label": "RAM 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_week_label": "Traffic this week",
"admin_panel_traffic_month_label": "Traffic this month", "admin_panel_traffic_month_label": "Traffic this month",
"admin_panel_nodes_label": "Active nodes", "admin_panel_nodes_label": "Active nodes",
"admin_panel_system_stats_error": "Error getting system statistics", "admin_panel_system_stats_error": "Error getting system statistics",
"admin_panel_bandwidth_stats_error": "Error getting bandwidth 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_fetch_error": "Error fetching panel data",
"admin_panel_stats_error_details": "Details", "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_title": "Referral link",
"inline_referral_description": "Share referral link to get bonuses", "inline_referral_description": "Share referral link to get bonuses",
"inline_stats_title": "Bot statistics",
"inline_stats_description": "Total: {total}, Active: {active}",
"inline_financial_title": "Financial statistics",
"inline_financial_description": "Today: {today} RUB", "inline_financial_description": "Today: {today} RUB",
"inline_system_title": "System statistics",
"inline_system_description": "🟢 Online: {online}, 📊 Active: {active}", "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_total_label": "Total",
"admin_user_stats_paid_subs_label": "With paid subscription", "admin_user_stats_paid_subs_label": "With paid subscription",
"admin_user_stats_trial_label": "On trial period", "admin_user_stats_trial_label": "On trial period",
@@ -517,5 +365,20 @@
"admin_financial_week_label": "This week", "admin_financial_week_label": "This week",
"admin_financial_month_label": "This month", "admin_financial_month_label": "This month",
"admin_financial_all_time_label": "All time", "admin_financial_all_time_label": "All time",
"admin_financial_payments_label": "payments" "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_payments_pagination_info": "📊 Showing {shown} of {total} payments (page {current_page}/{total_pages})",
"my_subscription_details": "🔐 <b>My Subscription</b>\n\n⏰ Status: <b>{status}</b>\n📅 Active until: <b>{end_date}</b>\n📆 Days left: <b>{days_left}</b>\n\n🔗 Configuration link:\n<code>{config_link}</code>\n\n📊 Traffic:\nLimit: <b>{traffic_limit}</b>\nUsed: <b>{traffic_used}</b>",
"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"
} }
+48 -194
View File
@@ -11,18 +11,20 @@
"menu_server_status_button": "📊 Статус", "menu_server_status_button": "📊 Статус",
"menu_support_button": "💬 Поддержка", "menu_support_button": "💬 Поддержка",
"menu_terms_button": "📄 Условия сервиса", "menu_terms_button": "📄 Условия сервиса",
"back_to_main_menu_button": "⬅️ Назад", "back_to_main_menu_button": "⬅️ Назад",
"choose_language": "Выберите язык / Select language:", "choose_language": "Выберите язык / Select language:",
"language_set_alert": "Язык изменен!", "language_set_alert": "Язык изменен!",
"error_occurred_try_again": "Произошла ошибка, попробуйте снова.", "error_occurred_try_again": "Произошла ошибка, попробуйте снова.",
"error_try_again": "Попробуйте еще раз.", "error_try_again": "Попробуйте еще раз.",
"error_displaying_menu": "Ошибка отображения меню.", "error_displaying_menu": "Ошибка отображения меню.",
"main_menu_unknown_action": "Неизвестное действие.", "main_menu_unknown_action": "Неизвестное действие.",
"select_subscription_period": "Выберите срок подписки:", "select_subscription_period": "Выберите срок подписки:",
"no_subscription_options_available": "В данный момент нет доступных вариантов подписки.",
"subscribe_for_months_button": "{months} мес. - {price} {currency_symbol}", "subscribe_for_months_button": "{months} мес. - {price} {currency_symbol}",
"choose_payment_method": "Выберите способ оплаты:", "choose_payment_method": "Выберите способ оплаты:",
"pay_button": "💳 Оплатить", "pay_button": "💳 Оплатить",
"pay_with_yookassa_button": "💳 ЮKassa", "pay_with_yookassa_button": "💳 ЮKassa",
@@ -32,63 +34,34 @@
"connect_button": "🔗 Подключиться", "connect_button": "🔗 Подключиться",
"cancel_button": "❌ Отмена", "cancel_button": "❌ Отмена",
"payment_description_subscription": "Оплата подписки на {months} мес.", "payment_description_subscription": "Оплата подписки на {months} мес.",
"payment_service_unavailable": "Платежный сервис временно недоступен. Пожалуйста, попробуйте позже.",
"payment_service_unavailable_alert": "Платежный сервис недоступен",
"error_creating_payment_record": "Ошибка при создании записи о платеже. Попробуйте позже.",
"error_payment_gateway_link_failed": "Не удалось получить ссылку на оплату. Пожалуйста, свяжитесь с поддержкой.",
"payment_link_message": "Для оплаты подписки на {months} мес., нажмите кнопку ниже:", "payment_link_message": "Для оплаты подписки на {months} мес., нажмите кнопку ниже:",
"error_payment_gateway": "Ошибка платежного шлюза. Попробуйте позже или свяжитесь с поддержкой.",
"payment_successful_error_details": "✅ Оплата прошла успешно, но возникла ошибка при отображении деталей. Ваша подписка активна. Свяжитесь с поддержкой, если что-то не так.", "payment_successful_error_details": "✅ Оплата прошла успешно, но возникла ошибка при отображении деталей. Ваша подписка активна. Свяжитесь с поддержкой, если что-то не так.",
"payment_successful_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. активна до {end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇", "payment_successful_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. активна до {end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
"payment_successful_with_promo_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. (с учетом промокода на +{bonus_days} дней) активна до {end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
"payment_successful_with_referral_bonus_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. (базовая дата окончания: {base_end_date}) продлена на {bonus_days} бонусных дней за приглашение от {inviter_name} и теперь активна до {final_end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇", "payment_successful_with_referral_bonus_full": "✅ Оплата прошла успешно!\nВаша подписка на {months} мес. (базовая дата окончания: {base_end_date}) продлена на {bonus_days} бонусных дней за приглашение от {inviter_name} и теперь активна до {final_end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
"payment_failed": "❌ Оплата не удалась или была отменена. Пожалуйста, попробуйте еще раз или свяжитесь с поддержкой.", "payment_failed": "❌ Оплата не удалась или была отменена. Пожалуйста, попробуйте еще раз или свяжитесь с поддержкой.",
"error_processing_your_payment": "Произошла ошибка при обработке вашего платежа после его успеха. Пожалуйста, свяжитесь с поддержкой, указав детали.",
"my_subscription_details": "<b>🔐 Моя подписка:</b>\n\nСтатус: <b>{status}</b>\nАктивна до: <b>{end_date}</b> (осталось дней: {days_left})\n\nТрафик: <b>{traffic_used}</b> из <b>{traffic_limit}</b>\n\nСсылка на конфигурацию:\n<code>{config_link}</code>",
"config_link_not_available": "недоступна, обратитесь в поддержку", "config_link_not_available": "недоступна, обратитесь в поддержку",
"subscription_not_active": "У вас нет активной подписки. \nХотите приобрести?",
"status_active": "Активна",
"status_expired": "Истекла",
"status_disabled": "Отключена",
"traffic_unlimited": "Безлимитный", "traffic_unlimited": "Безлимитный",
"traffic_na": "н/д",
"error_service_unavailable": "Сервис временно недоступен. Пожалуйста, попробуйте позже.",
"promo_code_prompt": "Пожалуйста, введите ваш промокод:", "promo_code_prompt": "Пожалуйста, введите ваш промокод:",
"promo_code_not_found": "Промокод <code>{code}</code> не найден, истек или уже использован максимальное количество раз.", "promo_code_not_found": "Промокод <code>{code}</code> не найден, истек или уже использован максимальное количество раз.",
"promo_code_already_used_by_user": "Вы уже активировали промокод <code>{code}</code>.", "promo_code_already_used_by_user": "Вы уже активировали промокод <code>{code}</code>.",
"promo_code_no_active_subscription": "Для активации этого промокода у вас должна быть активная подписка.",
"promo_code_applied_success_full": "✅ Промокод успешно применен!\nПодписка активна до {end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇", "promo_code_applied_success_full": "✅ Промокод успешно применен!\nПодписка активна до {end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
"error_applying_promo_bonus": "Не удалось применить бонус по промокоду. Пожалуйста, попробуйте позже или свяжитесь с поддержкой.", "error_applying_promo_bonus": "Не удалось применить бонус по промокоду. Пожалуйста, попробуйте позже или свяжитесь с поддержкой.",
"promo_input_cancelled_short": "Ввод промокода отменен.", "promo_input_cancelled_short": "Ввод промокода отменен.",
"trial_feature_disabled": "Пробный период в данный момент недоступен.", "trial_feature_disabled": "Пробный период в данный момент недоступен.",
"trial_already_had_subscription_or_trial": "Вы уже использовали пробный период или у вас была платная подписка. Пробный период доступен только один раз для новых пользователей.", "trial_already_had_subscription_or_trial": "Вы уже использовали пробный период или у вас была платная подписка. Пробный период доступен только один раз для новых пользователей.",
"trial_confirm_prompt": "Хотите активировать бесплатный пробный период на {days} дней с лимитом трафика {traffic_gb}?",
"trial_confirm_activate_button": "✅ Активировать!", "trial_confirm_activate_button": "✅ Активировать!",
"trial_activated_alert": "Пробный период активирован!", "trial_activated_alert": "Пробный период активирован!",
"trial_activated_details_message": "🚀 Ваш пробный период на {days} дней активирован!\n\nДействует до: <b>{end_date}</b>\nТрафик: <b>{traffic_gb}</b>\n\nВаша ссылка на конфигурацию:\n<code>{config_link}</code>", "trial_activated_details_message": "✅ Пробный доступ активирован!\nВаш триал на {days} дн. действует до {end_date}.\n\nКлюч подключения:\n<code>{config_link}</code>\n\nЧтобы подключиться, перейдите по ссылке и следуйте инструкции 👇",
"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": "Да", "yes_button": "Да",
"no_button": "Нет", "no_button": "Нет",
"trial_cancelled_short": "Активация отменена.",
"referral_program_info_new": "🎁 <b>Реферальная программа</b>\n\n📊 <b>Твоя статистика:</b>\n👥 Приглашено друзей: <b>{invited_count}</b>\n💳 Купили подписку: <b>{purchased_count}</b>\n\n🔗 Твоя ссылка:\n<code>{referral_link}</code>\n\n💰 <b>Бонусы за приглашения:</b>\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!", "referral_program_info_new": "🎁 <b>Реферальная программа</b>\n\n📊 <b>Твоя статистика:</b>\n👥 Приглашено друзей: <b>{invited_count}</b>\n💳 Купили подписку: <b>{purchased_count}</b>\n\n🔗 Твоя ссылка:\n<code>{referral_link}</code>\n\n💰 <b>Бонусы за приглашения:</b>\n{bonus_details}\n\n📢 Поделись ссылкой с друзьями и получай бонусы!",
"referral_bonus_per_period": "\n\n🎁 За {months}-мес. подписку друга:\n ➢ Вы: <b>{inviter_bonus_days} дн.</b>\n ➢ Друг: <b>{referee_bonus_days} дн.</b>", "referral_bonus_per_period": "\n\n🎁 За {months}-мес. подписку друга:\n ➢ Вы: <b>{inviter_bonus_days} дн.</b>\n ➢ Друг: <b>{referee_bonus_days} дн.</b>",
"no_bonus_days": "0",
"referral_no_bonuses_configured": "\nРеферальные бонусы не настроены.",
"referral_link_for_copying_reminder": "Ссылка выше. Нажмите и удерживайте для копирования.",
"referral_share_message_button": "📩 Сообщение для друга", "referral_share_message_button": "📩 Сообщение для друга",
"referral_friend_message": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}", "referral_friend_message": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}",
"friend_placeholder": "друг", "friend_placeholder": "друг",
"referral_bonus_inviter_notification_extended": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислено {days} бонусных дней! Ваша подписка теперь активна до {new_end_date}.", "referral_bonus_inviter_notification_extended": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислено {days} бонусных дней! Ваша подписка теперь активна до {new_end_date}.",
"referral_bonus_inviter_notification_new_sub": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислена бонусная подписка на {days} дней! Она активна до {new_end_date}.", "referral_bonus_inviter_notification_new_sub": "🎉 Поздравляем! Ваш друг {referee_name} оплатил подписку. Вам начислена бонусная подписка на {days} дней! Она активна до {new_end_date}.",
"user_is_banned": "🚫 Ваш аккаунт заблокирован. Пожалуйста, свяжитесь со службой поддержки.", "user_is_banned": "🚫 Ваш аккаунт заблокирован. Пожалуйста, свяжитесь со службой поддержки.",
"admin_panel_title": "Панель администратора", "admin_panel_title": "Панель администратора",
@@ -96,15 +69,12 @@
"admin_broadcast_button": "📢 Рассылка", "admin_broadcast_button": "📢 Рассылка",
"admin_create_promo_button": "🎁 Создать промо", "admin_create_promo_button": "🎁 Создать промо",
"admin_create_bulk_promo_button": "📦 Массовое создание", "admin_create_bulk_promo_button": "📦 Массовое создание",
"admin_manage_promos_button": "🛠 Управление",
"admin_view_promos_button": "👀 Список",
"admin_ban_user_button": "🚫 Забанить", "admin_ban_user_button": "🚫 Забанить",
"admin_unban_user_button": "✅ Разбанить", "admin_unban_user_button": "✅ Разбанить",
"admin_view_banned_users_button": "📜 Бан-лист", "admin_view_banned_users_button": "📜 Бан-лист",
"admin_view_logs_menu_button": "📄 Логи", "admin_view_logs_menu_button": "📄 Логи",
"admin_sync_panel_button": "🔄 Синхронизация", "admin_sync_panel_button": "🔄 Синхронизация",
"admin_unknown_action": "Неизвестное действие администратора.", "admin_unknown_action": "Неизвестное действие администратора.",
"admin_stats_and_monitoring_section": "📊 Статистика", "admin_stats_and_monitoring_section": "📊 Статистика",
"admin_user_management_section": "👥 Пользователи", "admin_user_management_section": "👥 Пользователи",
"admin_promo_marketing_section": "🎁 Промокоды", "admin_promo_marketing_section": "🎁 Промокоды",
@@ -112,18 +82,13 @@
"admin_ban_management_section": "🚫 Блокировки", "admin_ban_management_section": "🚫 Блокировки",
"admin_users_management_button": "👤 Управление", "admin_users_management_button": "👤 Управление",
"back_to_user_management_button": "⬅️ К пользователям", "back_to_user_management_button": "⬅️ К пользователям",
"admin_action_cancelled_default": "Действие отменено. Возврат в меню.",
"admin_action_cancelled_default_alert": "Действие отменено",
"back_to_admin_panel_button": "⬅️ В админку", "back_to_admin_panel_button": "⬅️ В админку",
"admin_stats_header": "📊 Статистика Бота", "admin_stats_header": "📊 Статистика Бота",
"admin_enhanced_users_stats_header": "Пользователи", "admin_enhanced_users_stats_header": "Пользователи",
"admin_financial_stats_header": "Финансовая статистика", "admin_financial_stats_header": "Финансовая статистика",
"admin_stats_users": "👥 Пользователи: Всего - {total_users}, Забанено - {banned_users}, С активной подпиской - {active_subs}",
"admin_stats_recent_payments_header": "Последние платежи:", "admin_stats_recent_payments_header": "Последние платежи:",
"admin_stats_payment_item": "{status_emoji} {amount} {currency} от {user_info} ({p_status}) [{p_date}]", "admin_stats_payment_item": "{status_emoji} {amount} {currency} от {user_info} ({p_status}) [{p_date}]",
"admin_stats_no_payments_found": "Платежей пока нет.", "admin_stats_no_payments_found": "Платежей пока нет.",
"admin_view_payments_button": "💰 Платежи", "admin_view_payments_button": "💰 Платежи",
"admin_payments_header": "💰 <b>Все платежи</b>", "admin_payments_header": "💰 <b>Все платежи</b>",
"admin_no_payments_found": "Платежи не найдены.", "admin_no_payments_found": "Платежи не найдены.",
@@ -132,7 +97,6 @@
"admin_no_payments_to_export": "Нет платежей для экспорта.", "admin_no_payments_to_export": "Нет платежей для экспорта.",
"admin_payments_export_success": "📊 Экспорт платежей завершен!\nВсего записей: {count}", "admin_payments_export_success": "📊 Экспорт платежей завершен!\nВсего записей: {count}",
"admin_export_sent": "Файл отправлен!", "admin_export_sent": "Файл отправлен!",
"admin_csv_payment_id": "ID", "admin_csv_payment_id": "ID",
"admin_csv_user_id": "User ID", "admin_csv_user_id": "User ID",
"admin_csv_username": "Логин", "admin_csv_username": "Логин",
@@ -152,9 +116,8 @@
"admin_stats_sync_subs_synced": "Синхронизировано подписок", "admin_stats_sync_subs_synced": "Синхронизировано подписок",
"admin_stats_sync_details_label": "Детали", "admin_stats_sync_details_label": "Детали",
"admin_sync_status_never_run": "Синхронизация с панелью еще не проводилась.", "admin_sync_status_never_run": "Синхронизация с панелью еще не проводилась.",
"admin_broadcast_enter_message": "Введите сообщение для рассылки (HTML поддерживается):", "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_all_button": "👥 Все",
"broadcast_target_active_button": "✅ Активные", "broadcast_target_active_button": "✅ Активные",
"broadcast_target_inactive_button": "⌛ Неактивные", "broadcast_target_inactive_button": "⌛ Неактивные",
@@ -163,21 +126,20 @@
"admin_broadcast_sending_started": "Начинаю рассылку...", "admin_broadcast_sending_started": "Начинаю рассылку...",
"admin_broadcast_error_no_message": "Ошибка: сообщение для рассылки не найдено.", "admin_broadcast_error_no_message": "Ошибка: сообщение для рассылки не найдено.",
"admin_broadcast_error_no_message_alert": "Сообщение для рассылки пустое!", "admin_broadcast_error_no_message_alert": "Сообщение для рассылки пустое!",
"admin_broadcast_finished_stats": "🏁 Рассылка завершена!\nОтправлено: {sent_count}\nНе удалось отправить: {failed_count}",
"admin_broadcast_cancelled": "Рассылка отменена.", "admin_broadcast_cancelled": "Рассылка отменена.",
"admin_broadcast_cancelled_alert": "Рассылка отменена!", "admin_broadcast_cancelled_alert": "Рассылка отменена!",
"admin_broadcast_cancelled_nav_back": "Рассылка отменена. Вы возвращены в админ-панель.", "admin_broadcast_cancelled_nav_back": "Рассылка отменена. Вы возвращены в админ-панель.",
"admin_promo_create_prompt": "Введите детали промокода в формате: КОД ДНИ_БОНУСА МАКС_АКТИВАЦИЙ [СРОК_ДЕЙСТВИЯ_В_ДНЯХ_ОТ_СЕЙЧАС]\nПример: <code>{example_format}</code>\n(Срок действия необязателен, по умолчанию - бессрочный)",
"admin_promo_invalid_format": "Неверный формат ввода. Пожалуйста, используйте: КОД ДНИ_БОНУСА МАКС_АКТИВАЦИЙ [ДНИ_ДЕЙСТВИЯ]",
"admin_promo_invalid_code_format": "Код должен быть от 3 до 30 символов и содержать только буквы и цифры.", "admin_promo_invalid_code_format": "Код должен быть от 3 до 30 символов и содержать только буквы и цифры.",
"admin_promo_invalid_bonus_days": "Количество бонусных дней должно быть положительным числом.", "admin_promo_invalid_bonus_days": "Количество бонусных дней должно быть положительным числом.",
"admin_promo_invalid_max_activations": "Максимальное количество активаций должно быть положительным числом.", "admin_promo_invalid_max_activations": "Максимальное количество активаций должно быть положительным числом.",
"admin_promo_invalid_bonus_or_activations": "Количество бонусных дней и максимальных активаций должны быть положительными числами.",
"admin_promo_invalid_validity_days": "Срок действия промокода (в днях) должен быть положительным числом.", "admin_promo_invalid_validity_days": "Срок действия промокода (в днях) должен быть положительным числом.",
"admin_promo_invalid_values": "Неверные значения. {error}",
"admin_promo_invalid_format_general": "Ошибка парсинга деталей промокода. Проверьте формат.",
"admin_promo_created_success": "✅ Промокод <code>{code}</code> успешно создан!\nБонус: {bonus_days} дней\nМакс. активаций: {max_activations}\nДействителен: {valid_until_str}", "admin_promo_created_success": "✅ Промокод <code>{code}</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": "🚨 <b>Подписка отменена</b>\n\nВаша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, после чего подписка будет заблокирована.\n\nДля продления подписки нажмите кнопку ниже.",
"admin_promo_set_validity_days": "⏰ Установить срок (дни)", "admin_promo_set_validity_days": "⏰ Установить срок (дни)",
"admin_back_to_panel": "⬅️ В панель", "admin_back_to_panel": "⬅️ В панель",
"admin_promo_unlimited": "♾️ Неограниченно", "admin_promo_unlimited": "♾️ Неограниченно",
@@ -188,22 +150,28 @@
"admin_queue_status_button": "📊 Статус очередей", "admin_queue_status_button": "📊 Статус очередей",
"admin_queue_status_title": "📊 Статус очередей сообщений", "admin_queue_status_title": "📊 Статус очередей сообщений",
"admin_queue_status_info": "📤 <b>Очереди сообщений:</b>\n\n👥 <b>Пользователи (25 сообщ/сек):</b>\n 📋 В очереди: {user_queue_size}\n 🔄 Обрабатывается: {user_processing}\n 📈 Отправлено за минуту: {user_recent}\n\n📢 <b>Группы/каналы (15 сообщ/мин):</b>\n 📋 В очереди: {group_queue_size}\n 🔄 Обрабатывается: {group_processing}\n 📈 Отправлено за минуту: {group_recent}", "admin_queue_status_info": "📤 <b>Очереди сообщений:</b>\n\n👥 <b>Пользователи (25 сообщ/сек):</b>\n 📋 В очереди: {user_queue_size}\n 🔄 Обрабатывается: {user_processing}\n 📈 Отправлено за минуту: {user_recent}\n\n📢 <b>Группы/каналы (15 сообщ/мин):</b>\n 📋 В очереди: {group_queue_size}\n 🔄 Обрабатывается: {group_processing}\n 📈 Отправлено за минуту: {group_recent}",
"admin_promo_creation_failed_duplicate": "❌ Ошибка: Промокод <code>{code}</code> уже существует.",
"admin_promo_creation_failed": "❌ Не удалось создать промокод. Пожалуйста, попробуйте позже.",
"admin_active_promos_list_header": "Активные промокоды:", "admin_active_promos_list_header": "Активные промокоды:",
"admin_no_active_promos": "Нет активных промокодов.", "admin_no_active_promos": "Нет активных промокодов.",
"admin_promo_list_item": "<code>{code}</code>: +{bonus}дн, {current}/{max} акт., до {valid_until}",
"admin_promo_valid_indefinitely": "бессрочно", "admin_promo_valid_indefinitely": "бессрочно",
"admin_promo_valid_until_display": "до {date}",
"admin_manage_promos_title": "Управление промокодами",
"admin_promo_edit_button": "✏️ Изменить", "admin_promo_edit_button": "✏️ Изменить",
"admin_promo_delete_button": "🗑 Удалить", "admin_promo_delete_button": "🗑 Удалить",
"admin_promo_edit_prompt": "Отправьте новые данные для <code>{code}</code> в формате: КОД ДНИ_БОНУСА МАКС_АКТИВАЦИЙ [СРОК]",
"admin_promo_updated_success": "Промокод {code} обновлен.",
"admin_promo_deleted_success": "Промокод {code} удален.", "admin_promo_deleted_success": "Промокод {code} удален.",
"admin_promo_not_found": "Промокод не найден.", "admin_promo_not_found": "Промокод не найден.",
"admin_promo_export_csv_button": "📄 Экспорт в CSV", "admin_promo_export_csv_button": "📄 Экспорт в CSV",
"admin_promo_export_caption": "📄 Активации промокода {code}", "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_edit_select_field": "Выберите поле для редактирования:",
"admin_promo_prompt_bonus_days": "Введите новое количество бонусных дней:", "admin_promo_prompt_bonus_days": "Введите новое количество бонусных дней:",
"admin_promo_prompt_max_activations": "Введите новое максимальное количество активаций:", "admin_promo_prompt_max_activations": "Введите новое максимальное количество активаций:",
@@ -213,86 +181,36 @@
"admin_promo_edit_bonus_days": "🎁 Бонусные дни", "admin_promo_edit_bonus_days": "🎁 Бонусные дни",
"admin_promo_edit_max_activations": "🔢 Макс. активации", "admin_promo_edit_max_activations": "🔢 Макс. активации",
"admin_promo_edit_validity": "⏰ Срок действия", "admin_promo_edit_validity": "⏰ Срок действия",
"admin_ban_user_prompt": "Введите ID или @username пользователя для блокировки:", "admin_ban_user_prompt": "Введите ID или @username пользователя для блокировки:",
"admin_user_not_found_in_bot_db": "Пользователь <code>{user_id}</code> не найден в базе данных бота.",
"admin_cannot_ban_self_or_admin": "Вы не можете заблокировать себя или другого администратора.",
"admin_user_already_banned": "Пользователь {user_id_or_username} уже заблокирован.", "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_unban_user_prompt": "Введите ID или @username пользователя для разблокировки:",
"admin_user_not_banned": "Пользователь {user_id_or_username} не заблокирован.", "admin_user_not_banned": "⚠️ Пользователь не заблокирован",
"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_banned_user_button_text": "{user_display} (ID: {user_id})", "admin_banned_user_button_text": "{user_display} (ID: {user_id})",
"prev_page_button": "⬅️ Пред.", "prev_page_button": "⬅️ Пред.",
"next_page_button": "След. ➡️", "next_page_button": "След. ➡️",
"admin_user_card_title": "Карточка пользователя: {user_display}", "admin_user_card_title": "Карточка пользователя",
"user_card_info": "ID: <code>{user_id}</code>\nUsername: @{username}\nИмя: {first_name} {last_name}\nЯзык: {language_code}\nPanel UUID: <code>{panel_user_uuid}</code>\nСтатус: <b>{ban_status}</b>\nРегистрация: {reg_date}\nПодписка до: <b>{sub_end_date}</b>",
"user_card_banned": "ЗАБЛОКИРОВАН",
"user_card_active": "Активен",
"user_card_sub_na": "н/д",
"user_card_ban_button": "🚫 Заблокировать", "user_card_ban_button": "🚫 Заблокировать",
"user_card_unban_button": "✅ Разблокировать", "user_card_unban_button": "✅ Разблокировать",
"user_card_back_to_banned_list_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_logs_menu_title": "Меню логов:",
"admin_view_all_logs_button": "📜 Все логи сообщений", "admin_view_all_logs_button": "📜 Все логи сообщений",
"admin_view_user_logs_prompt_button": "👤 Логи пользователя", "admin_view_user_logs_prompt_button": "👤 Логи пользователя",
"admin_export_logs_csv_button": "📄 Экспорт в CSV", "admin_export_logs_csv_button": "📄 Экспорт в CSV",
"admin_all_logs_title": "Все логи (стр. {current_page}/{total_pages}):",
"admin_no_logs_found": "Логи не найдены.", "admin_no_logs_found": "Логи не найдены.",
"admin_log_entry_format": "<code>{timestamp_str}</code> - <b>{user_display}</b> (ID: {user_id})\n <i>{event_type}</i>: {content_preview}", "admin_log_entry_format": "<code>{timestamp_str}</code> - <b>{user_display}</b> (ID: {user_id})\n <i>{event_type}</i>: {content_preview}",
"system_or_unknown_user": "Система/Неизв.",
"admin_prompt_for_user_id_or_username_logs": "Введите ID или @username пользователя для просмотра его логов:", "admin_prompt_for_user_id_or_username_logs": "Введите ID или @username пользователя для просмотра его логов:",
"admin_log_user_not_found": "Пользователь по запросу \"{input}\" не найден в базе данных бота.", "admin_log_user_not_found": "Пользователь по запросу \"{input}\" не найден в базе данных бота.",
"admin_user_logs_title": "Логи пользователя {user_display} (стр. {current_page}/{total_pages}):",
"sync_started_simple": "🔄 Начинаю синхронизацию...", "sync_started_simple": "🔄 Начинаю синхронизацию...",
"sync_success_simple": "✅ Синхронизация успешно завершена", "sync_success_simple": "✅ Синхронизация успешно завершена",
"sync_failed_simple": "❌ Синхронизация завершилась с ошибкой", "sync_failed_simple": "❌ Синхронизация завершилась с ошибкой",
"sync_errors_simple": "⚠️ Синхронизация завершена с ошибками ({errors_count} ошибок)", "sync_errors_simple": "⚠️ Синхронизация завершена с ошибками ({errors_count} ошибок)",
"sync_critical_error": "❌ Критическая ошибка синхронизации", "sync_critical_error": "❌ Критическая ошибка синхронизации",
"no_errors_placeholder": "нет",
"admin_sync_initiated_from_panel": "Синхронизация запущена...", "admin_sync_initiated_from_panel": "Синхронизация запущена...",
"admin_broadcast_invalid_html": "❌ Некорректный HTML в сообщении. Пожалуйста, отправьте корректный HTML (поддерживаются теги Telegram) или уберите теги.", "admin_broadcast_invalid_html": "❌ Некорректный HTML в сообщении. Пожалуйста, отправьте корректный HTML (поддерживаются теги Telegram) или уберите теги.",
"admin_panel_user_creation_failed": "❌ Не удалось создать пользователя на панели для TG ID {user_id}. Панель недоступна?",
"error_displaying_logs_too_long": "Ошибка: логи слишком длинные для отображения одним сообщением. Попробуйте найти логи по конкретному пользователю.", "error_displaying_logs_too_long": "Ошибка: логи слишком длинные для отображения одним сообщением. Попробуйте найти логи по конкретному пользователю.",
"error_displaying_statistics": "Ошибка отображения статистики.", "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": "🚨 <b>Подписка отменена</b>\n\nВаша подписка Tribute была отменена. У вас есть 24 часа для восстановления доступа, после чего подписка будет заблокирована.\n\nДля продления подписки нажмите кнопку ниже.",
"tribute_auto_renewal": "🔄 <b>Подписка автоматически продлена</b>\n\nВаша подписка Tribute была автоматически продлена на {months} мес.\nНовая дата окончания: {end_date}", "tribute_auto_renewal": "🔄 <b>Подписка автоматически продлена</b>\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_management_prompt": "👤 Управление пользователями\n\nВведите ID пользователя или @username для поиска:",
"admin_user_card_title": "Карточка пользователя",
"admin_user_subscription_info": "Информация о подписке:", "admin_user_subscription_info": "Информация о подписке:",
"admin_user_reset_trial_button": "🔄 Сбросить триал", "admin_user_reset_trial_button": "🔄 Сбросить триал",
"admin_user_add_subscription_button": "➕ Добавить дни", "admin_user_add_subscription_button": "➕ Добавить дни",
@@ -303,7 +221,6 @@
"admin_user_search_new_button": "🔍 Новый поиск", "admin_user_search_new_button": "🔍 Новый поиск",
"admin_user_view_all_logs_button": "📋 Все логи", "admin_user_view_all_logs_button": "📋 Все логи",
"admin_user_back_to_card_button": "🔙 К карточке", "admin_user_back_to_card_button": "🔙 К карточке",
"admin_user_not_found": "❌ Пользователь не найден: {input}", "admin_user_not_found": "❌ Пользователь не найден: {input}",
"admin_user_not_found_action": "Пользователь не найден", "admin_user_not_found_action": "Пользователь не найден",
"admin_user_card_error": "❌ Ошибка отображения карточки пользователя", "admin_user_card_error": "❌ Ошибка отображения карточки пользователя",
@@ -319,7 +236,6 @@
"admin_user_unban_success": "✅ Пользователь {input} разблокирован", "admin_user_unban_success": "✅ Пользователь {input} разблокирован",
"admin_user_ban_error": "❌ Ошибка блокировки пользователя", "admin_user_ban_error": "❌ Ошибка блокировки пользователя",
"admin_user_unban_error": "❌ Ошибка разблокировки пользователя", "admin_user_unban_error": "❌ Ошибка разблокировки пользователя",
"admin_user_not_banned": "⚠️ Пользователь не заблокирован",
"admin_user_send_message_prompt": "✉️ Отправка сообщения пользователю {user_id}\n\nВведите текст сообщения:", "admin_user_send_message_prompt": "✉️ Отправка сообщения пользователю {user_id}\n\nВведите текст сообщения:",
"admin_user_message_too_long": "❌ Сообщение слишком длинное (максимум 4000 символов)", "admin_user_message_too_long": "❌ Сообщение слишком длинное (максимум 4000 символов)",
"admin_user_message_sent_success": "✅ Сообщение отправлено пользователю {user_id}", "admin_user_message_sent_success": "✅ Сообщение отправлено пользователю {user_id}",
@@ -327,26 +243,13 @@
"admin_user_no_logs": "📜 У пользователя нет действий", "admin_user_no_logs": "📜 У пользователя нет действий",
"admin_user_logs_error": "❌ Ошибка загрузки действий пользователя", "admin_user_logs_error": "❌ Ошибка загрузки действий пользователя",
"admin_direct_message_signature": "\n\n---\n💬 Сообщение от администратора", "admin_direct_message_signature": "\n\n---\n💬 Сообщение от администратора",
"inline_user_stats_message": "📊 <b>Статистика Бота</b>\n👥 Пользователи\n\n📊 Всего: <b>{total}</b>\n📈 Активных сегодня: <b>{active_today}</b>\n💳 С платной подпиской: <b>{paid}</b>\n🆓 На пробном периоде: <b>{trial}</b>\n😴 Неактивных: <b>{inactive}</b>\n🚫 Заблокированных: <b>{banned}</b>\n🎁 Привлечено по реферальной программе: <b>{referral}</b>",
"inline_user_stats_message": "📊 <b>Статистика Бота</b>\n👥 Пользователи\n\n📊 Всего: <b>{total}</b>\n💳 С платной подпиской: <b>{paid}</b>\n🆓 На пробном периоде: <b>{trial}</b>\n😴 Неактивных: <b>{inactive}</b>\n🚫 Заблокированных: <b>{banned}</b>\n🎁 Привлечено по реферальной программе: <b>{referral}</b>",
"inline_user_stats_description": "Всего: {total}, Платных: {active}", "inline_user_stats_description": "Всего: {total}, Платных: {active}",
"inline_admin_user_stats_title": "👥 Статистика пользователей", "inline_admin_user_stats_title": "👥 Статистика пользователей",
"inline_admin_user_stats_desc": "Всего: {total}, Активных: {paid}",
"inline_financial_stats_message": "💰 <b>Финансовая статистика</b>\n\n📅 За сегодня: <b>{today:.2f} RUB</b>\n ({today_count} платежей)\n📅 За неделю: <b>{week:.2f} RUB</b>\n📅 За месяц: <b>{month:.2f} RUB</b>\n🏆 За все время: <b>{all_time:.2f} RUB</b>", "inline_financial_stats_message": "💰 <b>Финансовая статистика</b>\n\n📅 За сегодня: <b>{today:.2f} RUB</b>\n ({today_count} платежей)\n📅 За неделю: <b>{week:.2f} RUB</b>\n📅 За месяц: <b>{month:.2f} RUB</b>\n🏆 За все время: <b>{all_time:.2f} RUB</b>",
"inline_admin_financial_stats_title": "💰 Финансовая статистика", "inline_admin_financial_stats_title": "💰 Финансовая статистика",
"inline_admin_financial_stats_desc": "Сегодня: {today:.2f} RUB",
"inline_system_stats_message": "🖥 <b>Статистика панели</b>\n\n🟢 Онлайн: <b>{online}</b>\n📊 Активных: <b>{active}</b>\n🔴 Отключенных: <b>{disabled}</b>\n⏰ Истекшие: <b>{expired}</b>\n⚠️ Ограниченные: <b>{limited}</b>\n👥 Всего пользователей: <b>{total}</b>\n💾 Использование RAM: <b>{memory:.1f}%</b>\n📊 Трафик за неделю: <b>{week_traffic}</b>\n📊 Трафик за месяц: <b>{month_traffic}</b>\n🔗 Активных нод: <b>{active_nodes}/{total_nodes}</b>", "inline_system_stats_message": "🖥 <b>Статистика панели</b>\n\n🟢 Онлайн: <b>{online}</b>\n📊 Активных: <b>{active}</b>\n🔴 Отключенных: <b>{disabled}</b>\n⏰ Истекшие: <b>{expired}</b>\n⚠️ Ограниченные: <b>{limited}</b>\n👥 Всего пользователей: <b>{total}</b>\n💾 Использование RAM: <b>{memory:.1f}%</b>\n📊 Трафик за неделю: <b>{week_traffic}</b>\n📊 Трафик за месяц: <b>{month_traffic}</b>\n🔗 Активных нод: <b>{active_nodes}/{total_nodes}</b>",
"inline_admin_system_stats_title": "🖥 Системная статистика", "inline_admin_system_stats_title": "🖥 Системная статистика",
"inline_admin_system_stats_desc": "Онлайн: {online}, Офлайн: {offline}",
"inline_admin_help_message": "🤖 <b>Inline режим бота</b>\n\n📱 <b>Доступные команды:</b>\n\n🎁 <b>реф/ref</b> - поделиться реферальной ссылкой\n👥 <b>стат/stat</b> - статистика пользователей\n💰 <b>финансы</b> - финансовая статистика\n🖥 <b>система</b> - системная статистика\n\n💡 Просто напишите @{bot_username} и начните вводить команду в любом чате!",
"inline_user_help_message": "🤖 <b>Inline режим бота</b>\n\n📱 <b>Доступные команды:</b>\n\n🎁 <b>реф/ref</b> - поделиться реферальной ссылкой\n\n💡 Просто напишите @{bot_username} и начните вводить 'реф' в любом чате!",
"inline_user_help_title": "🤖 Inline помощь",
"inline_user_help_desc": "Доступна команда: реф (реферальная ссылка)",
"log_referral_suffix": " (реферал от {referrer_id})", "log_referral_suffix": " (реферал от {referrer_id})",
"log_new_user_registration": "👤 <b>Новый пользователь</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Имя: {user_display}{referral_text}\n📅 Время: {timestamp}", "log_new_user_registration": "👤 <b>Новый пользователь</b>\n\n🆔 ID: <code>{user_id}</code>\n👤 Имя: {user_display}{referral_text}\n📅 Время: {timestamp}",
"log_payment_received": "{provider_emoji} <b>Получен платеж</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n📅 Период: <b>{months} мес.</b>\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}", "log_payment_received": "{provider_emoji} <b>Получен платеж</b>\n\n👤 Пользователь: {user_display}\n💰 Сумма: <b>{amount} {currency}</b>\n📅 Период: <b>{months} мес.</b>\n🏦 Провайдер: {payment_provider}\n🕐 Время: {timestamp}",
@@ -354,28 +257,10 @@
"log_trial_activation": "🆓 <b>Активирован триал</b>\n\n👤 Пользователь: {user_display}\n⏰ Действует до: <b>{end_date}</b>\n🕐 Время: {timestamp}", "log_trial_activation": "🆓 <b>Активирован триал</b>\n\n👤 Пользователь: {user_display}\n⏰ Действует до: <b>{end_date}</b>\n🕐 Время: {timestamp}",
"log_panel_sync": "{status_emoji} <b>Синхронизация с панелью</b>\n\n📊 Статус: <b>{status}</b>\n👥 Обработано пользователей: <b>{users_processed}</b>\n📋 Синхронизировано подписок: <b>{subs_synced}</b>\n🕐 Время: {timestamp}\n\n📝 Детали:\n{details}", "log_panel_sync": "{status_emoji} <b>Синхронизация с панелью</b>\n\n📊 Статус: <b>{status}</b>\n👥 Обработано пользователей: <b>{users_processed}</b>\n📋 Синхронизировано подписок: <b>{subs_synced}</b>\n🕐 Время: {timestamp}\n\n📝 Детали:\n{details}",
"log_suspicious_promo": "⚠️ <b>Подозрительная попытка ввода промокода</b>\n\n👤 Пользователь: {user_display}\n🆔 ID: <code>{user_id}</code>\n📝 Ввод: <pre>{suspicious_input}</pre>\n🕐 Время: {timestamp}", "log_suspicious_promo": "⚠️ <b>Подозрительная попытка ввода промокода</b>\n\n👤 Пользователь: {user_display}\n🆔 ID: <code>{user_id}</code>\n📝 Ввод: <pre>{suspicious_input}</pre>\n🕐 Время: {timestamp}",
"admin_general_cancel_operation": "Операция отменена ❌",
"admin_promo_codes_menu_bulk_create": "📦 Массовое создание",
"admin_bulk_promo_create_prompt": "📦 Массовое создание промокодов\n\nВведите данные в формате:\n<code>количество|дни|описание</code>\n\nПример:\n<code>10|7|Промо на неделю</code>",
"admin_bulk_promo_invalid_format": "❌ Неверный формат. Используйте: количество|дни|описание",
"admin_bulk_promo_created_success": "✅ Создано {count} промокодов на {days} дней!",
"admin_bulk_promo_codes_file": "📄 Все созданные промокоды в файле ({count} шт.)",
"admin_promo_edit_step1_code": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 1 из 4:</b> Код промокода\n\nТекущий код: <b>{current_code}</b>\n\nВведите новый код промокода (3-30 символов, только буквы и цифры) или отправьте текущий для сохранения:",
"admin_promo_edit_step2_bonus_days": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКод: <b>{code}</b>\nТекущие дни: <b>{current_days}</b>\n\nВведите количество бонусных дней (1-365):",
"admin_promo_edit_step3_max_activations": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nТекущие активации: <b>{current_max}</b>\n\nВведите максимальное количество активаций (1-10000):",
"admin_promo_edit_step4_validity": "✏️ <b>Редактирование промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nДни: <b>{days}</b>\nАктивации: <b>{max_act}</b>\nТекущий срок: <b>{current_validity}</b>\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🎟 Код: <b>{code}</b>\n🎁 Бонусные дни: <b>{days}</b>\n🔢 Максимальные активации: <b>{max_act}</b>\n⏰ Действует до: <b>{validity}</b>",
"admin_promo_update_failed": "❌ Ошибка обновления промокода",
"admin_logs_export_csv": "📄 Экспорт в CSV",
"admin_logs_csv_export_started": "📄 Начинаю экспорт логов в CSV...", "admin_logs_csv_export_started": "📄 Начинаю экспорт логов в CSV...",
"admin_logs_csv_export_success": "✅ Логи экспортированы! Файл прикреплен выше.", "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_log_id": "ID Лога",
"admin_csv_header_timestamp": "Время", "admin_csv_header_timestamp": "Время",
"admin_csv_header_user_id": "ID Пользователя", "admin_csv_header_user_id": "ID Пользователя",
@@ -386,44 +271,23 @@
"admin_csv_header_is_admin_event": "Админ событие", "admin_csv_header_is_admin_event": "Админ событие",
"admin_csv_header_target_user_id": "ID Целевого юзера", "admin_csv_header_target_user_id": "ID Целевого юзера",
"admin_csv_header_raw_update_preview": "Превью обновления", "admin_csv_header_raw_update_preview": "Превью обновления",
"admin_banned_users_empty": "📋 Заблокированные пользователи\n\nСписок пуст", "admin_banned_users_empty": "📋 Заблокированные пользователи\n\nСписок пуст",
"admin_banned_users_list": "📋 Заблокированные пользователи ({count}):\n\n{users}", "admin_banned_users_list": "📋 Заблокированные пользователи ({count}):\n\n{users}",
"admin_panel_stats_header": "Статистика панели", "admin_panel_stats_header": "Статистика панели",
"admin_bulk_promo_step1_quantity": "📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 1 из 4:</b> Количество промокодов\n\nВведите количество промокодов для создания (1-1000):", "admin_bulk_promo_step1_quantity": "📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 1 из 4:</b> Количество промокодов\n\nВведите количество промокодов для создания (1-1000):",
"admin_bulk_promo_step2_bonus_days": "📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКоличество: <b>{quantity}</b>\n\nВведите количество бонусных дней (1-365):", "admin_bulk_promo_step2_bonus_days": "📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКоличество: <b>{quantity}</b>\n\nВведите количество бонусных дней (1-365):",
"admin_bulk_promo_step3_max_activations": "📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКоличество: <b>{quantity}</b>\nБонусные дни: <b>{bonus_days}</b>\n\nВведите максимальное количество активаций для каждого промокода (1-10000):", "admin_bulk_promo_step3_max_activations": "📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКоличество: <b>{quantity}</b>\nБонусные дни: <b>{bonus_days}</b>\n\nВведите максимальное количество активаций для каждого промокода (1-10000):",
"admin_bulk_promo_step4_validity": "📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКоличество: <b>{quantity}</b>\nБонусные дни: <b>{bonus_days}</b>\nМакс. активации: <b>{max_activations}</b>\n\nВыберите срок действия промокодов:", "admin_bulk_promo_step4_validity": "📦 <b>Массовое создание промокодов</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКоличество: <b>{quantity}</b>\nБонусные дни: <b>{bonus_days}</b>\nМакс. активации: <b>{max_activations}</b>\n\nВыберите срок действия промокодов:",
"admin_bulk_promo_invalid_quantity": "❌ Количество должно быть от 1 до 1000", "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_enter_validity_days": "⏰ Введите количество дней действия промокодов (1-365):",
"admin_bulk_promo_creating": "⏳ Создаю {quantity} промокодов...", "admin_bulk_promo_creating": "⏳ Создаю {quantity} промокодов...",
"admin_promo_step1_code": "🎟 <b>Создание промокода</b>\n\n<b>Шаг 1 из 4:</b> Код промокода\n\nВведите код промокода (3-30 символов, только буквы и цифры):", "admin_promo_step1_code": "🎟 <b>Создание промокода</b>\n\n<b>Шаг 1 из 4:</b> Код промокода\n\nВведите код промокода (3-30 символов, только буквы и цифры):",
"admin_promo_step2_bonus_days": "🎟 <b>Создание промокода</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКод: <b>{code}</b>\n\nВведите количество бонусных дней (1-365):", "admin_promo_step2_bonus_days": "🎟 <b>Создание промокода</b>\n\n<b>Шаг 2 из 4:</b> Бонусные дни\n\nКод: <b>{code}</b>\n\nВведите количество бонусных дней (1-365):",
"admin_promo_step3_max_activations": "🎟 <b>Создание промокода</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКод: <b>{code}</b>\nБонусные дни: <b>{bonus_days}</b>\n\nВведите максимальное количество активаций (1-10000):", "admin_promo_step3_max_activations": "🎟 <b>Создание промокода</b>\n\n<b>Шаг 3 из 4:</b> Максимальные активации\n\nКод: <b>{code}</b>\nБонусные дни: <b>{bonus_days}</b>\n\nВведите максимальное количество активаций (1-10000):",
"admin_promo_step4_validity": "🎟 <b>Создание промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nБонусные дни: <b>{bonus_days}</b>\nМакс. активации: <b>{max_activations}</b>\n\nВыберите срок действия промокода:", "admin_promo_step4_validity": "🎟 <b>Создание промокода</b>\n\n<b>Шаг 4 из 4:</b> Срок действия\n\nКод: <b>{code}</b>\nБонусные дни: <b>{bonus_days}</b>\nМакс. активации: <b>{max_activations}</b>\n\nВыберите срок действия промокода:",
"admin_promo_code_already_exists": "❌ Промокод с таким кодом уже существует", "admin_promo_code_already_exists": "❌ Промокод с таким кодом уже существует",
"admin_promo_processing_error": "❌ Ошибка обработки промокода",
"admin_promo_unlimited_validity": "♾️ Неограниченно", "admin_promo_unlimited_validity": "♾️ Неограниченно",
"admin_promo_set_validity": "⏰ Установить срок",
"admin_promo_enter_validity_days": "⏰ Введите количество дней действия промокода (1-365):", "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": "🆔 <b>ID:</b>", "admin_user_id_label": "🆔 <b>ID:</b>",
"admin_user_name_label": "👤 <b>Имя:</b>", "admin_user_name_label": "👤 <b>Имя:</b>",
"admin_user_username_label": "📱 <b>Username:</b>", "admin_user_username_label": "📱 <b>Username:</b>",
@@ -436,7 +300,6 @@
"admin_user_traffic_label": "📊 <b>Трафик:</b>", "admin_user_traffic_label": "📊 <b>Трафик:</b>",
"admin_user_subscription_label": "💼 <b>Подписка:</b>", "admin_user_subscription_label": "💼 <b>Подписка:</b>",
"admin_user_trial_label": "🏡 <b>Триал:</b>", "admin_user_trial_label": "🏡 <b>Триал:</b>",
"admin_user_status_banned": "🚫 Заблокирован", "admin_user_status_banned": "🚫 Заблокирован",
"admin_user_status_active": "✅ Активен", "admin_user_status_active": "✅ Активен",
"admin_user_trial_used": "Использовал", "admin_user_trial_used": "Использовал",
@@ -444,21 +307,12 @@
"admin_user_ban_action_banned": "заблокирован", "admin_user_ban_action_banned": "заблокирован",
"admin_user_ban_action_unbanned": "разблокирован", "admin_user_ban_action_unbanned": "разблокирован",
"admin_user_na_value": "N/A", "admin_user_na_value": "N/A",
"admin_user_subscription_active": "Активна до {end_date}",
"admin_user_subscription_expired": "Истекла {end_date}",
"admin_user_subscription_none": "Нет активной подписки", "admin_user_subscription_none": "Нет активной подписки",
"admin_user_logs_count": "Всего записей: {count}",
"admin_user_logs_entry": "📅 {timestamp}\n📝 {event_type}: {content}",
"admin_user_actions_count_label": "📜 <b>Всего действий:</b>", "admin_user_actions_count_label": "📜 <b>Всего действий:</b>",
"admin_user_subscription_active_until": "⏰ <b>Действует до:</b>", "admin_user_subscription_active_until": "⏰ <b>Действует до:</b>",
"admin_user_subscription_error": "Ошибка загрузки", "admin_user_subscription_error": "Ошибка загрузки",
"admin_bulk_promo_unique_generation_failed": "Не удалось создать уникальный промокод",
"admin_promo_management_button": "🎟 Управление промокодами", "admin_promo_management_button": "🎟 Управление промокодами",
"admin_promo_management_title": "🎟 <b>Управление промокодами</b>\n\nВыберите промокод для детального просмотра:", "admin_promo_management_title": "🎟 <b>Управление промокодами</b>\n\nВыберите промокод для детального просмотра:",
"admin_promo_management_empty": "📭 Промокоды отсутствуют", "admin_promo_management_empty": "📭 Промокоды отсутствуют",
"admin_promo_card_title": "🎟 <b>Промокод: {code}</b>", "admin_promo_card_title": "🎟 <b>Промокод: {code}</b>",
@@ -478,43 +332,28 @@
"admin_promo_view_activations_button": "📋 Активации", "admin_promo_view_activations_button": "📋 Активации",
"admin_promo_back_to_list_button": "⬅️ К списку", "admin_promo_back_to_list_button": "⬅️ К списку",
"admin_promo_toggle_success": "✅ Промокод {code} {status}", "admin_promo_toggle_success": "✅ Промокод {code} {status}",
"admin_promo_toggle_failed": "❌ Ошибка изменения статуса промокода",
"admin_promo_no_activations": "📋 <b>Активации промокода: {code}</b>\n\n❌ Активаций не найдено", "admin_promo_no_activations": "📋 <b>Активации промокода: {code}</b>\n\n❌ Активаций не найдено",
"admin_promo_activations_header": "📋 <b>Активации промокода: {code}</b>\n\n", "admin_promo_activations_header": "📋 <b>Активации промокода: {code}</b>\n\n",
"admin_promo_activation_item": "👤 User ID: <b>{user_id}</b>\n📅 Дата: <b>{date}</b>\n", "admin_promo_activation_item": "👤 User ID: <b>{user_id}</b>\n📅 Дата: <b>{date}</b>\n",
"admin_promo_back_to_detail_button": "⬅️ К промокоду", "admin_promo_back_to_detail_button": "⬅️ К промокоду",
"admin_panel_online_label": "Онлайн", "admin_panel_online_label": "Онлайн",
"admin_panel_active_label": "Активных", "admin_panel_active_label": "Активных",
"admin_panel_disabled_label": "Отключенных", "admin_panel_disabled_label": "Отключенных",
"admin_panel_offline_label": "Офлайн",
"admin_panel_expired_label": "Истекшие", "admin_panel_expired_label": "Истекшие",
"admin_panel_limited_label": "Ограниченные", "admin_panel_limited_label": "Ограниченные",
"admin_panel_total_users_label": "Всего пользователей", "admin_panel_total_users_label": "Всего пользователей",
"admin_panel_cpu_usage_label": "Загрузка CPU",
"admin_panel_memory_usage_label": "Использование RAM", "admin_panel_memory_usage_label": "Использование RAM",
"admin_panel_traffic_today_label": "Трафик сегодня",
"admin_panel_traffic_week_label": "Трафик за неделю", "admin_panel_traffic_week_label": "Трафик за неделю",
"admin_panel_traffic_month_label": "Трафик за месяц", "admin_panel_traffic_month_label": "Трафик за месяц",
"admin_panel_nodes_label": "Активных нод", "admin_panel_nodes_label": "Активных нод",
"admin_panel_system_stats_error": "Ошибка получения системной статистики", "admin_panel_system_stats_error": "Ошибка получения системной статистики",
"admin_panel_bandwidth_stats_error": "Ошибка получения статистики трафика", "admin_panel_bandwidth_stats_error": "Ошибка получения статистики трафика",
"admin_panel_nodes_stats_error": "Ошибка получения статистики нод",
"admin_panel_stats_fetch_error": "Ошибка получения данных с панели", "admin_panel_stats_fetch_error": "Ошибка получения данных с панели",
"admin_panel_stats_error_details": "Детали", "admin_panel_stats_error_details": "Детали",
"inline_referral_message": "🚀 Привет! Попробуй этот VPN - быстрый, надёжный и доступный!\n\n🎁 По моей ссылке тебе дадут бонусные дни к подписке!\n\n{referral_link}",
"inline_referral_title": "Реферальная ссылка", "inline_referral_title": "Реферальная ссылка",
"inline_referral_description": "Поделиться реферальной ссылкой для получения бонусов", "inline_referral_description": "Поделиться реферальной ссылкой для получения бонусов",
"inline_stats_title": "Статистика бота",
"inline_stats_description": "Всего: {total}, Активных: {active}",
"inline_financial_title": "Финансовая статистика",
"inline_financial_description": "Сегодня: {today} RUB", "inline_financial_description": "Сегодня: {today} RUB",
"inline_system_title": "Системная статистика",
"inline_system_description": "🟢 Онлайн: {online}, 📊 Активных: {active}", "inline_system_description": "🟢 Онлайн: {online}, 📊 Активных: {active}",
"inline_admin_help_title": "Помощь по командам",
"inline_admin_help_description": "Доступны команды: реф, стат, финансы, система",
"admin_user_stats_total_label": "Всего", "admin_user_stats_total_label": "Всего",
"admin_user_stats_paid_subs_label": "С платной подпиской", "admin_user_stats_paid_subs_label": "С платной подпиской",
"admin_user_stats_trial_label": "На пробном периоде", "admin_user_stats_trial_label": "На пробном периоде",
@@ -525,5 +364,20 @@
"admin_financial_week_label": "За неделю", "admin_financial_week_label": "За неделю",
"admin_financial_month_label": "За месяц", "admin_financial_month_label": "За месяц",
"admin_financial_all_time_label": "За все время", "admin_financial_all_time_label": "За все время",
"admin_financial_payments_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_payments_pagination_info": "📊 Показано {shown} из {total} платежей (стр. {current_page}/{total_pages})",
"my_subscription_details": "🔐 <b>Моя подписка</b>\n\n⏰ Статус: <b>{status}</b>\n📅 Действует до: <b>{end_date}</b>\n📆 Осталось дней: <b>{days_left}</b>\n\n🔗 Ссылка на конфигурацию:\n<code>{config_link}</code>\n\n📊 Трафик:\nЛимит: <b>{traffic_limit}</b>\nИспользовано: <b>{traffic_used}</b>",
"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": "Неактивна"
} }