Merge pull request #124 from machka-pasla/dev

yookassa bug fix and other
This commit is contained in:
machka pasla
2025-11-10 15:49:45 +03:00
committed by GitHub
16 changed files with 304 additions and 46 deletions
+1
View File
@@ -44,6 +44,7 @@ YOOKASSA_RETURN_URL=https://t.me/your_bot #
YOOKASSA_DEFAULT_RECEIPT_EMAIL=your_email@example.com # Default email for sending receipts
YOOKASSA_VAT_CODE=1 # VAT code
YOOKASSA_AUTOPAYMENTS_ENABLED=False # Auto-renew toggle
YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING=True # Force automatic card binding when autopay is enabled (set to False to show the save-card checkbox)
# FreeKassa Payment Gateway Configuration
FREEKASSA_MERCHANT_ID=your_shop_id # Your shop ID in FreeKassa
+2
View File
@@ -83,6 +83,8 @@
| `YOOKASSA_ENABLED` | Включить/выключить YooKassa (`true`/`false`). |
| `YOOKASSA_SHOP_ID` | ID вашего магазина в YooKassa. |
| `YOOKASSA_SECRET_KEY`| Секретный ключ магазина YooKassa. |
| `YOOKASSA_AUTOPAYMENTS_ENABLED` | Включить автопродление (сохранение карт, автосписания, управление способами оплаты). |
| `YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING` | Требовать обязательную привязку карты при оплате с автосписанием. Установите `false`, чтобы пользователю показывался чекбокс «Сохранить карту». |
| `CRYPTOPAY_ENABLED` | Включить/выключить CryptoPay (`true`/`false`). |
| `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. |
| `FREEKASSA_ENABLED` | Включить/выключить FreeKassa (`true`/`false`). |
+2 -1
View File
@@ -212,6 +212,7 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession,
"is_active": panel_status == "ACTIVE",
"status_from_panel": panel_status,
"traffic_limit_bytes": settings.user_traffic_limit_bytes,
"auto_renew_enabled": False,
}
created_sub = await subscription_dal.upsert_subscription(
session, sub_payload
@@ -436,4 +437,4 @@ async def sync_status_command_handler(
else:
response_text = _("admin_sync_status_never_run")
await message.answer(response_text, parse_mode="HTML")
await message.answer(response_text, parse_mode="HTML")
+134 -29
View File
@@ -4,7 +4,7 @@ from aiogram import Router, F, types, Bot
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
from aiogram.utils.markdown import hcode, hbold
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, Callable, Awaitable
from sqlalchemy.ext.asyncio import AsyncSession
from datetime import datetime, timezone
@@ -104,7 +104,8 @@ async def user_search_prompt_handler(callback: types.CallbackQuery,
await state.set_state(AdminStates.waiting_for_user_search)
def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyboardBuilder:
def get_user_card_keyboard(user_id: int, i18n_instance, lang: str,
referrer_id: Optional[int] = None) -> InlineKeyboardBuilder:
"""Generate keyboard for user management actions"""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
@@ -139,13 +140,26 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb
callback_data=f"user_action:refresh:{user_id}"
)
# Row 4: Destructive action
# Row 4: Quick links
builder.button(
text=_(key="user_card_open_profile_button",
default="👤 Открыть профиль"),
url=f"tg://user?id={user_id}"
)
if referrer_id:
builder.button(
text=_(key="user_card_open_referrer_profile_button",
default="👤 Открыть профиль пригласившего"),
url=f"tg://user?id={referrer_id}"
)
# Row 5: Destructive action
builder.button(
text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"),
callback_data=f"user_action:delete_user:{user_id}"
)
# Row 5: Navigation
# Row 6: Navigation
builder.button(
text=_(key="admin_user_search_new_button", default="🔍 Найти другого"),
callback_data="admin_action:users_management"
@@ -155,10 +169,61 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str) -> InlineKeyb
callback_data="admin_action:main"
)
builder.adjust(2, 2, 2, 1, 2)
quick_links_width = 2 if referrer_id else 1
builder.adjust(2, 2, 2, quick_links_width, 1, 2)
return builder
def _remove_profile_link_buttons(
markup: Optional[types.InlineKeyboardMarkup]) -> Optional[types.InlineKeyboardMarkup]:
"""Drop buttons that rely on tg://user links to avoid BUTTON_USER_INVALID errors."""
if not markup or not markup.inline_keyboard:
return None
cleaned_rows = []
for row in markup.inline_keyboard:
filtered_row = [
button for button in row
if not (getattr(button, "url", None) and button.url.startswith("tg://user?id="))
]
if filtered_row:
cleaned_rows.append(filtered_row)
if not cleaned_rows:
return None
return types.InlineKeyboardMarkup(inline_keyboard=cleaned_rows)
async def _send_with_profile_link_fallback(
sender: Callable[..., Awaitable[Any]],
*,
text: str,
markup: Optional[types.InlineKeyboardMarkup],
user_id: int,
parse_mode: Optional[str] = "HTML") -> None:
"""Send text with markup and fallback if Telegram rejects tg://user buttons."""
send_kwargs: Dict[str, Any] = {"text": text, "reply_markup": markup}
if parse_mode is not None:
send_kwargs["parse_mode"] = parse_mode
try:
await sender(**send_kwargs)
except TelegramBadRequest as exc:
message = getattr(exc, "message", "") or str(exc)
if "BUTTON_USER_INVALID" not in message:
raise
logging.warning(
"Telegram rejected profile buttons for user %s: %s. Retrying without tg:// links.",
user_id,
message,
)
fallback_markup = _remove_profile_link_buttons(markup)
send_kwargs["reply_markup"] = fallback_markup
await sender(**send_kwargs)
async def format_user_card(user: User, session: AsyncSession,
subscription_service: SubscriptionService,
i18n_instance, lang: str,
@@ -315,11 +380,18 @@ async def process_user_search_handler(message: types.Message, state: FSMContext,
try:
referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
user_card_text = await format_user_card(user_model, session, subscription_service, i18n, current_lang, referral_service)
keyboard = get_user_card_keyboard(user_model.user_id, i18n, current_lang)
keyboard = get_user_card_keyboard(
user_model.user_id,
i18n,
current_lang,
user_model.referred_by_id
)
await message.answer(
user_card_text,
reply_markup=keyboard.as_markup(),
await _send_with_profile_link_fallback(
message.answer,
text=user_card_text,
markup=keyboard.as_markup(),
user_id=user_model.user_id,
parse_mode="HTML"
)
except Exception as e:
@@ -580,18 +652,28 @@ async def handle_refresh_user_card(callback: types.CallbackQuery, user: User,
_settings = _Settings()
referral_service = ReferralService(_settings, subscription_service, callback.message.bot, i18n_instance)
user_card_text = await format_user_card(fresh_user, session, subscription_service, i18n_instance, lang, referral_service)
keyboard = get_user_card_keyboard(fresh_user.user_id, i18n_instance, lang)
keyboard = get_user_card_keyboard(
fresh_user.user_id,
i18n_instance,
lang,
fresh_user.referred_by_id
)
markup = keyboard.as_markup()
try:
await callback.message.edit_text(
user_card_text,
reply_markup=keyboard.as_markup(),
await _send_with_profile_link_fallback(
callback.message.edit_text,
text=user_card_text,
markup=markup,
user_id=fresh_user.user_id,
parse_mode="HTML"
)
except Exception:
await callback.message.answer(
user_card_text,
reply_markup=keyboard.as_markup(),
await _send_with_profile_link_fallback(
callback.message.answer,
text=user_card_text,
markup=markup,
user_id=fresh_user.user_id,
parse_mode="HTML"
)
@@ -864,11 +946,18 @@ async def process_subscription_days_handler(message: types.Message, state: FSMCo
if user:
referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
user_card_text = await format_user_card(user, session, subscription_service, i18n, current_lang, referral_service)
keyboard = get_user_card_keyboard(user.user_id, i18n, current_lang)
keyboard = get_user_card_keyboard(
user.user_id,
i18n,
current_lang,
user.referred_by_id
)
await message.answer(
user_card_text,
reply_markup=keyboard.as_markup(),
await _send_with_profile_link_fallback(
message.answer,
text=user_card_text,
markup=keyboard.as_markup(),
user_id=user.user_id,
parse_mode="HTML"
)
else:
@@ -973,11 +1062,18 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
subscription_service = SubscriptionService(settings, panel_service)
referral_service = ReferralService(settings, subscription_service, bot, i18n)
user_card_text = await format_user_card(target_user, session, subscription_service, i18n, current_lang, referral_service)
keyboard = get_user_card_keyboard(target_user.user_id, i18n, current_lang)
keyboard = get_user_card_keyboard(
target_user.user_id,
i18n,
current_lang,
target_user.referred_by_id
)
await message.answer(
user_card_text,
reply_markup=keyboard.as_markup(),
await _send_with_profile_link_fallback(
message.answer,
text=user_card_text,
markup=keyboard.as_markup(),
user_id=target_user.user_id,
parse_mode="HTML"
)
@@ -1272,22 +1368,31 @@ async def user_card_from_list_handler(callback: types.CallbackQuery,
return
# Create keyboard with back to list button
keyboard = get_user_card_keyboard(user_id, i18n, current_lang)
keyboard = get_user_card_keyboard(
user_id,
i18n,
current_lang,
user.referred_by_id
)
keyboard.button(
text=_("admin_user_back_to_list_button", default="⬅️ К списку"),
callback_data=f"admin_action:users_list:{page}"
)
keyboard.adjust(2, 2, 2, 2, 1)
quick_links_width = 2 if user.referred_by_id else 1
keyboard.adjust(2, 2, 2, quick_links_width, 1, 2, 1)
# Format user card
try:
from bot.services.referral_service import ReferralService
referral_service = ReferralService(settings, subscription_service, bot, i18n)
user_card_text = await format_user_card(user, session, subscription_service, i18n, current_lang, referral_service)
markup = keyboard.as_markup()
await callback.message.edit_text(
user_card_text,
reply_markup=keyboard.as_markup(),
await _send_with_profile_link_fallback(
callback.message.edit_text,
text=user_card_text,
markup=markup,
user_id=user.user_id,
parse_mode="HTML"
)
await callback.answer()
+12
View File
@@ -170,6 +170,18 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
card_last4=display_last4,
card_network=display_network,
)
try:
await user_billing_dal.upsert_user_payment_method(
session,
user_id=user_id,
provider_payment_method_id=pm_id,
provider="yookassa",
card_last4=display_last4,
card_network=display_network,
set_default=True,
)
except Exception:
logging.exception("Failed to persist multi-card YooKassa method from webhook")
except Exception:
logging.exception("Failed to persist YooKassa payment method from webhook")
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
+11
View File
@@ -357,6 +357,17 @@ async def start_command_handler(message: types.Message,
db_user, created = await user_dal.create_user(session, user_data_to_create)
if created:
try:
await session.commit()
except Exception as commit_error:
await session.rollback()
logging.error(
f"Failed to commit new user {user_id}: {commit_error}",
exc_info=True,
)
await message.answer(_("error_occurred_processing_request"))
return
logging.info(
f"New user {user_id} added to session. Referred by: {referred_by_user_id or 'N/A'}."
)
+21 -1
View File
@@ -16,7 +16,7 @@ from bot.keyboards.inline.user_keyboards import (
from bot.services.subscription_service import SubscriptionService
from bot.services.panel_api_service import PanelApiService
from bot.middlewares.i18n import JsonI18n
from db.dal import subscription_dal
from db.dal import subscription_dal, user_billing_dal
from db.models import Subscription
router = Router(name="user_subscription_core_router")
@@ -455,6 +455,14 @@ async def toggle_autorenew_handler(
if sub.provider == "tribute":
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True)
return
if enable:
has_saved_card = await user_billing_dal.user_has_saved_payment_method(session, callback.from_user.id)
if not has_saved_card:
try:
await callback.answer(get_text("autorenew_enable_requires_card"), show_alert=True)
except Exception:
pass
return
# Show confirmation popup and inline buttons
confirm_text = get_text("autorenew_confirm_enable") if enable else get_text("autorenew_confirm_disable")
@@ -505,6 +513,18 @@ async def confirm_autorenew_handler(
if sub.provider == "tribute":
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True)
return
if enable:
has_saved_card = await user_billing_dal.user_has_saved_payment_method(session, callback.from_user.id)
if not has_saved_card:
try:
await callback.answer(get_text("autorenew_enable_requires_card"), show_alert=True)
except Exception:
pass
try:
await my_subscription_command_handler(callback, i18n_data, settings, panel_service, subscription_service, session, bot)
except Exception:
pass
return
await subscription_dal.update_subscription(session, sub.subscription_id, {"auto_renew_enabled": enable})
await session.commit()
+8 -2
View File
@@ -408,6 +408,9 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
user_id = callback.from_user.id
currency_code_for_yk = "RUB"
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
autopay_require_binding = bool(
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
)
saved_methods: List = []
if autopay_enabled:
try:
@@ -463,7 +466,7 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
months=months,
price_rub=price_rub,
currency_code_for_yk=currency_code_for_yk,
save_payment_method=autopay_enabled,
save_payment_method=autopay_enabled and autopay_require_binding,
back_callback=f"subscribe_period:{months}",
)
try:
@@ -520,6 +523,9 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
user_id = callback.from_user.id
currency_code_for_yk = "RUB"
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
autopay_require_binding = bool(
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
)
await _initiate_yk_payment(
callback,
@@ -533,7 +539,7 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
months=months,
price_rub=price_rub,
currency_code_for_yk=currency_code_for_yk,
save_payment_method=autopay_enabled,
save_payment_method=autopay_enabled and autopay_require_binding,
back_callback=f"subscribe_period:{months}",
)
try:
+7
View File
@@ -384,6 +384,13 @@ def get_user_card_keyboard(user_id: int,
builder.button(
text=_(key="user_card_ban_button"),
callback_data=f"admin_ban_confirm:{user_id}:{banned_list_page}")
builder.button(
text=_(
key="user_card_open_profile_button",
default="👤 Open profile"
),
url=f"tg://user?id={user_id}"
)
builder.button(
text=_(key="user_card_back_to_banned_list_button"),
callback_data=f"admin_action:view_banned:{banned_list_page}")
+61 -10
View File
@@ -1,10 +1,11 @@
import logging
import asyncio
from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.text_decorations import html_decoration as hd
from aiogram.exceptions import TelegramRetryAfter
from datetime import datetime, timezone
from typing import Optional, Union, Dict, Any
from typing import Optional, Union, Dict, Any, Callable
from config.settings import Settings
from sqlalchemy.orm import sessionmaker
@@ -34,8 +35,45 @@ class NotificationService:
if username:
base_display = f"{base_display} ({username_for_display(username)})"
return base_display
@staticmethod
def _build_profile_keyboard(
translate: Callable[..., str],
user_id: int,
referrer_id: Optional[int] = None,
) -> InlineKeyboardMarkup:
"""Create inline keyboard with links to user (and referrer) profiles."""
buttons = [
[
InlineKeyboardButton(
text=translate(
"log_open_profile_link",
default="👤 Открыть профиль",
),
url=f"tg://user?id={user_id}",
)
]
]
if referrer_id:
buttons.append([
InlineKeyboardButton(
text=translate(
"log_open_referrer_profile_button",
default="👤 Открыть профиль пригласившего",
),
url=f"tg://user?id={referrer_id}",
)
])
return InlineKeyboardMarkup(inline_keyboard=buttons)
async def _send_to_log_channel(self, message: str, thread_id: Optional[int] = None):
async def _send_to_log_channel(
self,
message: str,
thread_id: Optional[int] = None,
reply_markup: Optional[InlineKeyboardMarkup] = None,
):
"""Send message to configured log channel/group using message queue"""
if not self.settings.LOG_CHAT_ID:
return
@@ -49,6 +87,7 @@ class NotificationService:
text=message,
parse_mode="HTML",
disable_web_page_preview=True,
reply_markup=reply_markup,
message_thread_id=thread_id or self.settings.LOG_THREAD_ID
)
except Exception as e:
@@ -64,6 +103,8 @@ class NotificationService:
"parse_mode": "HTML",
"disable_web_page_preview": True
}
if reply_markup:
kwargs["reply_markup"] = reply_markup
# Add thread ID for supergroups if specified
if final_thread_id:
@@ -124,7 +165,12 @@ class NotificationService:
referral_text = ""
if referred_by_id:
referral_text = _("log_referral_suffix", default=" (реферал от {referrer_id})", referrer_id=referred_by_id)
referrer_link = hd.link(str(referred_by_id), f"tg://user?id={referred_by_id}")
referral_text = _(
"log_referral_suffix",
default=" (реферал от {referrer_link})",
referrer_link=referrer_link,
)
message = _(
"log_new_user_registration",
@@ -137,9 +183,10 @@ class NotificationService:
referral_text=referral_text,
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
)
# Send to log channel
await self._send_to_log_channel(message)
profile_keyboard = self._build_profile_keyboard(_, user_id, referred_by_id)
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
async def notify_payment_received(self, user_id: int, amount: float, currency: str,
months: int, payment_provider: str,
@@ -182,7 +229,8 @@ class NotificationService:
)
# Send to log channel
await self._send_to_log_channel(message)
profile_keyboard = self._build_profile_keyboard(_, user_id)
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
async def notify_promo_activation(self, user_id: int, promo_code: str, bonus_days: int,
username: Optional[str] = None):
@@ -212,7 +260,8 @@ class NotificationService:
)
# Send to log channel
await self._send_to_log_channel(message)
profile_keyboard = self._build_profile_keyboard(_, user_id)
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
async def notify_trial_activation(self, user_id: int, end_date: datetime,
username: Optional[str] = None):
@@ -240,7 +289,8 @@ class NotificationService:
)
# Send to log channel
await self._send_to_log_channel(message)
profile_keyboard = self._build_profile_keyboard(_, user_id)
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
async def notify_panel_sync(self, status: str, details: str,
users_processed: int, subs_synced: int,
@@ -275,7 +325,7 @@ class NotificationService:
details=details
)
# Send to log channel
# Send to log channel
await self._send_to_log_channel(message)
async def notify_suspicious_promo_attempt(
@@ -308,7 +358,8 @@ class NotificationService:
timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"))
# Send to log channel
await self._send_to_log_channel(message)
profile_keyboard = self._build_profile_keyboard(_, user_id)
await self._send_to_log_channel(message, reply_markup=profile_keyboard)
async def send_custom_notification(self, message: str, to_admins: bool = False,
to_log_channel: bool = True, thread_id: Optional[int] = None):
+2
View File
@@ -177,6 +177,8 @@ class ReferralService:
"ACTIVE_BONUS",
"traffic_limit_bytes":
self.settings.user_traffic_limit_bytes,
"auto_renew_enabled":
False,
}
try:
await subscription_dal.deactivate_other_active_subscriptions(
+11 -1
View File
@@ -498,6 +498,15 @@ class SubscriptionService:
session, panel_user_uuid, panel_sub_link_id
)
auto_renew_should_enable = False
if (
provider == "yookassa"
and getattr(self.settings, "YOOKASSA_AUTOPAYMENTS_ENABLED", False)
):
auto_renew_should_enable = await user_billing_dal.user_has_saved_payment_method(
session, user_id
)
sub_payload = {
"user_id": user_id,
"panel_user_uuid": panel_user_uuid,
@@ -510,7 +519,7 @@ class SubscriptionService:
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
"provider": provider,
"skip_notifications": provider == "tribute" and self.settings.TRIBUTE_SKIP_NOTIFICATIONS,
"auto_renew_enabled": True,
"auto_renew_enabled": auto_renew_should_enable,
}
try:
new_or_updated_sub = await subscription_dal.upsert_subscription(
@@ -616,6 +625,7 @@ class SubscriptionService:
"is_active": True,
"status_from_panel": "ACTIVE_BONUS",
"traffic_limit_bytes": traffic_limit,
"auto_renew_enabled": False,
}
await subscription_dal.deactivate_other_active_subscriptions(
session, panel_uuid, panel_sub_uuid
+4
View File
@@ -41,6 +41,10 @@ class Settings(BaseSettings):
YOOKASSA_PAYMENT_SUBJECT: str = Field(default="service")
# Single toggle to enable recurring payments (saving cards, managing payment methods, auto-renew)
YOOKASSA_AUTOPAYMENTS_ENABLED: bool = Field(default=False)
YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING: bool = Field(
default=True,
description="When true, new YooKassa payments in autopay mode force card binding without a user checkbox."
)
WEBHOOK_BASE_URL: Optional[str] = None
+16
View File
@@ -162,3 +162,19 @@ async def delete_user_payment_method_by_provider_id(
await session.delete(method)
await session.flush()
return True
async def user_has_saved_payment_method(
session: AsyncSession,
user_id: int,
provider: str = "yookassa",
) -> bool:
"""Return True if the user has at least one saved payment method."""
try:
methods = await list_user_payment_methods(session, user_id, provider)
if methods:
return True
billing = await get_user_billing(session, user_id)
return bool(billing and billing.yookassa_payment_method_id)
except Exception:
return False
+6 -1
View File
@@ -210,6 +210,8 @@
"admin_user_card_title": "User Card",
"user_card_ban_button": "🚫 Ban",
"user_card_unban_button": "✅ Unban",
"user_card_open_profile_button": "👤 Open profile",
"user_card_open_referrer_profile_button": "👤 Referrer profile",
"user_card_back_to_banned_list_button": "⬅️ Back to Ban List",
"admin_logs_menu_title": "Logs Menu:",
"admin_view_all_logs_button": "📜 All Message Logs",
@@ -291,7 +293,9 @@
"inline_admin_financial_stats_title": "💰 Financial Statistics",
"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",
"log_referral_suffix": " (referral from {referrer_id})",
"log_referral_suffix": " (referral from {referrer_link})",
"log_open_profile_link": "👤 Open profile",
"log_open_referrer_profile_button": "👤 Referrer profile",
"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_promo_activation": "🎁 <b>Promo Code Activated</b>\n\n👤 User: {user_display}\n🏷 Code: <code>{promo_code}</code>\n🎯 Bonus: <b>+{bonus_days}d</b>\n🕐 Time: {timestamp}",
@@ -439,6 +443,7 @@
"subscription_tribute_notice": "Paid via Tribute. Renew using your Tribute link.",
"subscription_tribute_notice_with_link": "Paid via Tribute. Renew: {link}",
"subscription_autorenew_not_supported_for_tribute": "Auto-renew is handled by Tribute. Manage renewal in the Tribute app/link.",
"autorenew_enable_requires_card": "Link a payment card in Payment Methods before enabling auto-renew.",
"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.",
+6 -1
View File
@@ -220,6 +220,8 @@
"admin_user_card_title": "Карточка пользователя",
"user_card_ban_button": "🚫 Заблокировать",
"user_card_unban_button": "✅ Разблокировать",
"user_card_open_profile_button": "👤 Открыть профиль",
"user_card_open_referrer_profile_button": "👤 Профиль пригласившего",
"user_card_back_to_banned_list_button": "⬅️ К списку забаненных",
"admin_logs_menu_title": "Меню логов:",
"admin_view_all_logs_button": "📜 Все логи сообщений",
@@ -291,7 +293,9 @@
"inline_admin_financial_stats_title": "💰 Финансовая статистика",
"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": "🖥 Системная статистика",
"log_referral_suffix": " (реферал от {referrer_id})",
"log_referral_suffix": " (реферал от {referrer_link})",
"log_open_profile_link": "👤 Открыть профиль",
"log_open_referrer_profile_button": "👤 Профиль пригласившего",
"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_promo_activation": "🎁 <b>Активирован промокод</b>\n\n👤 Пользователь: {user_display}\n🏷 Код: <code>{promo_code}</code>\n🎯 Бонус: <b>+{bonus_days} дн.</b>\n🕐 Время: {timestamp}",
@@ -439,6 +443,7 @@
"subscription_tribute_notice": "Оплачено через Tribute. Продление делайте по ссылке Tribute.",
"subscription_tribute_notice_with_link": "Оплачено через Tribute. Продлить: {link}",
"subscription_autorenew_not_supported_for_tribute": "Автопродление управляется Tribute. Управляйте продлением в приложении/ссылке Tribute.",
"autorenew_enable_requires_card": "Прежде чем включать автоплатёж, привяжите карту в разделе «Способы оплаты».",
"subscription_not_active": "У вас нет активной подписки.",
"error_service_unavailable": "Сервис недоступен. Попробуйте позже.",
"error_payment_gateway": "Ошибка платежного сервиса. Попробуйте позже.",