Merge pull request #124 from machka-pasla/dev
yookassa bug fix and other
This commit is contained in:
@@ -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_DEFAULT_RECEIPT_EMAIL=your_email@example.com # Default email for sending receipts
|
||||||
YOOKASSA_VAT_CODE=1 # VAT code
|
YOOKASSA_VAT_CODE=1 # VAT code
|
||||||
YOOKASSA_AUTOPAYMENTS_ENABLED=False # Auto-renew toggle
|
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 Payment Gateway Configuration
|
||||||
FREEKASSA_MERCHANT_ID=your_shop_id # Your shop ID in FreeKassa
|
FREEKASSA_MERCHANT_ID=your_shop_id # Your shop ID in FreeKassa
|
||||||
|
|||||||
@@ -83,6 +83,8 @@
|
|||||||
| `YOOKASSA_ENABLED` | Включить/выключить YooKassa (`true`/`false`). |
|
| `YOOKASSA_ENABLED` | Включить/выключить YooKassa (`true`/`false`). |
|
||||||
| `YOOKASSA_SHOP_ID` | ID вашего магазина в YooKassa. |
|
| `YOOKASSA_SHOP_ID` | ID вашего магазина в YooKassa. |
|
||||||
| `YOOKASSA_SECRET_KEY`| Секретный ключ магазина YooKassa. |
|
| `YOOKASSA_SECRET_KEY`| Секретный ключ магазина YooKassa. |
|
||||||
|
| `YOOKASSA_AUTOPAYMENTS_ENABLED` | Включить автопродление (сохранение карт, автосписания, управление способами оплаты). |
|
||||||
|
| `YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING` | Требовать обязательную привязку карты при оплате с автосписанием. Установите `false`, чтобы пользователю показывался чекбокс «Сохранить карту». |
|
||||||
| `CRYPTOPAY_ENABLED` | Включить/выключить CryptoPay (`true`/`false`). |
|
| `CRYPTOPAY_ENABLED` | Включить/выключить CryptoPay (`true`/`false`). |
|
||||||
| `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. |
|
| `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. |
|
||||||
| `FREEKASSA_ENABLED` | Включить/выключить FreeKassa (`true`/`false`). |
|
| `FREEKASSA_ENABLED` | Включить/выключить FreeKassa (`true`/`false`). |
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ async def perform_sync(panel_service: PanelApiService, session: AsyncSession,
|
|||||||
"is_active": panel_status == "ACTIVE",
|
"is_active": panel_status == "ACTIVE",
|
||||||
"status_from_panel": panel_status,
|
"status_from_panel": panel_status,
|
||||||
"traffic_limit_bytes": settings.user_traffic_limit_bytes,
|
"traffic_limit_bytes": settings.user_traffic_limit_bytes,
|
||||||
|
"auto_renew_enabled": False,
|
||||||
}
|
}
|
||||||
created_sub = await subscription_dal.upsert_subscription(
|
created_sub = await subscription_dal.upsert_subscription(
|
||||||
session, sub_payload
|
session, sub_payload
|
||||||
@@ -436,4 +437,4 @@ async def sync_status_command_handler(
|
|||||||
else:
|
else:
|
||||||
response_text = _("admin_sync_status_never_run")
|
response_text = _("admin_sync_status_never_run")
|
||||||
|
|
||||||
await message.answer(response_text, parse_mode="HTML")
|
await message.answer(response_text, parse_mode="HTML")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from aiogram import Router, F, types, Bot
|
|||||||
from aiogram.exceptions import TelegramBadRequest
|
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, Callable, Awaitable
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from datetime import datetime, timezone
|
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)
|
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"""
|
"""Generate keyboard for user management actions"""
|
||||||
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
|
||||||
builder = InlineKeyboardBuilder()
|
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}"
|
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(
|
builder.button(
|
||||||
text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"),
|
text=_(key="admin_user_delete_button", default="❌ Удалить пользователя"),
|
||||||
callback_data=f"user_action:delete_user:{user_id}"
|
callback_data=f"user_action:delete_user:{user_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Row 5: Navigation
|
# Row 6: Navigation
|
||||||
builder.button(
|
builder.button(
|
||||||
text=_(key="admin_user_search_new_button", default="🔍 Найти другого"),
|
text=_(key="admin_user_search_new_button", default="🔍 Найти другого"),
|
||||||
callback_data="admin_action:users_management"
|
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"
|
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
|
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,
|
async def format_user_card(user: User, session: AsyncSession,
|
||||||
subscription_service: SubscriptionService,
|
subscription_service: SubscriptionService,
|
||||||
i18n_instance, lang: str,
|
i18n_instance, lang: str,
|
||||||
@@ -315,11 +380,18 @@ async def process_user_search_handler(message: types.Message, state: FSMContext,
|
|||||||
try:
|
try:
|
||||||
referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
|
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)
|
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(
|
await _send_with_profile_link_fallback(
|
||||||
user_card_text,
|
message.answer,
|
||||||
reply_markup=keyboard.as_markup(),
|
text=user_card_text,
|
||||||
|
markup=keyboard.as_markup(),
|
||||||
|
user_id=user_model.user_id,
|
||||||
parse_mode="HTML"
|
parse_mode="HTML"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -580,18 +652,28 @@ async def handle_refresh_user_card(callback: types.CallbackQuery, user: User,
|
|||||||
_settings = _Settings()
|
_settings = _Settings()
|
||||||
referral_service = ReferralService(_settings, subscription_service, callback.message.bot, i18n_instance)
|
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)
|
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:
|
try:
|
||||||
await callback.message.edit_text(
|
await _send_with_profile_link_fallback(
|
||||||
user_card_text,
|
callback.message.edit_text,
|
||||||
reply_markup=keyboard.as_markup(),
|
text=user_card_text,
|
||||||
|
markup=markup,
|
||||||
|
user_id=fresh_user.user_id,
|
||||||
parse_mode="HTML"
|
parse_mode="HTML"
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
await callback.message.answer(
|
await _send_with_profile_link_fallback(
|
||||||
user_card_text,
|
callback.message.answer,
|
||||||
reply_markup=keyboard.as_markup(),
|
text=user_card_text,
|
||||||
|
markup=markup,
|
||||||
|
user_id=fresh_user.user_id,
|
||||||
parse_mode="HTML"
|
parse_mode="HTML"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -864,11 +946,18 @@ async def process_subscription_days_handler(message: types.Message, state: FSMCo
|
|||||||
if user:
|
if user:
|
||||||
referral_service = ReferralService(settings, subscription_service, message.bot, i18n)
|
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)
|
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(
|
await _send_with_profile_link_fallback(
|
||||||
user_card_text,
|
message.answer,
|
||||||
reply_markup=keyboard.as_markup(),
|
text=user_card_text,
|
||||||
|
markup=keyboard.as_markup(),
|
||||||
|
user_id=user.user_id,
|
||||||
parse_mode="HTML"
|
parse_mode="HTML"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -973,11 +1062,18 @@ async def process_direct_message_handler(message: types.Message, state: FSMConte
|
|||||||
subscription_service = SubscriptionService(settings, panel_service)
|
subscription_service = SubscriptionService(settings, panel_service)
|
||||||
referral_service = ReferralService(settings, subscription_service, bot, i18n)
|
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)
|
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(
|
await _send_with_profile_link_fallback(
|
||||||
user_card_text,
|
message.answer,
|
||||||
reply_markup=keyboard.as_markup(),
|
text=user_card_text,
|
||||||
|
markup=keyboard.as_markup(),
|
||||||
|
user_id=target_user.user_id,
|
||||||
parse_mode="HTML"
|
parse_mode="HTML"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1272,22 +1368,31 @@ async def user_card_from_list_handler(callback: types.CallbackQuery,
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Create keyboard with back to list button
|
# 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(
|
keyboard.button(
|
||||||
text=_("admin_user_back_to_list_button", default="⬅️ К списку"),
|
text=_("admin_user_back_to_list_button", default="⬅️ К списку"),
|
||||||
callback_data=f"admin_action:users_list:{page}"
|
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
|
# Format user card
|
||||||
try:
|
try:
|
||||||
from bot.services.referral_service import ReferralService
|
from bot.services.referral_service import ReferralService
|
||||||
referral_service = ReferralService(settings, subscription_service, bot, i18n)
|
referral_service = ReferralService(settings, subscription_service, bot, i18n)
|
||||||
user_card_text = await format_user_card(user, session, subscription_service, i18n, current_lang, referral_service)
|
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(
|
await _send_with_profile_link_fallback(
|
||||||
user_card_text,
|
callback.message.edit_text,
|
||||||
reply_markup=keyboard.as_markup(),
|
text=user_card_text,
|
||||||
|
markup=markup,
|
||||||
|
user_id=user.user_id,
|
||||||
parse_mode="HTML"
|
parse_mode="HTML"
|
||||||
)
|
)
|
||||||
await callback.answer()
|
await callback.answer()
|
||||||
|
|||||||
@@ -170,6 +170,18 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
card_last4=display_last4,
|
card_last4=display_last4,
|
||||||
card_network=display_network,
|
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:
|
except Exception:
|
||||||
logging.exception("Failed to persist YooKassa payment method from webhook")
|
logging.exception("Failed to persist YooKassa payment method from webhook")
|
||||||
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
||||||
|
|||||||
@@ -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)
|
db_user, created = await user_dal.create_user(session, user_data_to_create)
|
||||||
|
|
||||||
if created:
|
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(
|
logging.info(
|
||||||
f"New user {user_id} added to session. Referred by: {referred_by_user_id or 'N/A'}."
|
f"New user {user_id} added to session. Referred by: {referred_by_user_id or 'N/A'}."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from bot.keyboards.inline.user_keyboards import (
|
|||||||
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 db.dal import subscription_dal
|
from db.dal import subscription_dal, user_billing_dal
|
||||||
from db.models import Subscription
|
from db.models import Subscription
|
||||||
|
|
||||||
router = Router(name="user_subscription_core_router")
|
router = Router(name="user_subscription_core_router")
|
||||||
@@ -455,6 +455,14 @@ async def toggle_autorenew_handler(
|
|||||||
if sub.provider == "tribute":
|
if sub.provider == "tribute":
|
||||||
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True)
|
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True)
|
||||||
return
|
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
|
# Show confirmation popup and inline buttons
|
||||||
confirm_text = get_text("autorenew_confirm_enable") if enable else get_text("autorenew_confirm_disable")
|
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":
|
if sub.provider == "tribute":
|
||||||
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True)
|
await callback.answer(get_text("subscription_autorenew_not_supported_for_tribute"), show_alert=True)
|
||||||
return
|
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 subscription_dal.update_subscription(session, sub.subscription_id, {"auto_renew_enabled": enable})
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|||||||
@@ -408,6 +408,9 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
currency_code_for_yk = "RUB"
|
currency_code_for_yk = "RUB"
|
||||||
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
|
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
|
||||||
|
autopay_require_binding = bool(
|
||||||
|
getattr(settings, 'YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING', True)
|
||||||
|
)
|
||||||
saved_methods: List = []
|
saved_methods: List = []
|
||||||
if autopay_enabled:
|
if autopay_enabled:
|
||||||
try:
|
try:
|
||||||
@@ -463,7 +466,7 @@ async def pay_yk_callback_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
months=months,
|
months=months,
|
||||||
price_rub=price_rub,
|
price_rub=price_rub,
|
||||||
currency_code_for_yk=currency_code_for_yk,
|
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}",
|
back_callback=f"subscribe_period:{months}",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -520,6 +523,9 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
user_id = callback.from_user.id
|
user_id = callback.from_user.id
|
||||||
currency_code_for_yk = "RUB"
|
currency_code_for_yk = "RUB"
|
||||||
autopay_enabled = bool(getattr(settings, 'YOOKASSA_AUTOPAYMENTS_ENABLED', False))
|
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(
|
await _initiate_yk_payment(
|
||||||
callback,
|
callback,
|
||||||
@@ -533,7 +539,7 @@ async def pay_yk_new_card_handler(callback: types.CallbackQuery, settings: Setti
|
|||||||
months=months,
|
months=months,
|
||||||
price_rub=price_rub,
|
price_rub=price_rub,
|
||||||
currency_code_for_yk=currency_code_for_yk,
|
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}",
|
back_callback=f"subscribe_period:{months}",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -384,6 +384,13 @@ def get_user_card_keyboard(user_id: int,
|
|||||||
builder.button(
|
builder.button(
|
||||||
text=_(key="user_card_ban_button"),
|
text=_(key="user_card_ban_button"),
|
||||||
callback_data=f"admin_ban_confirm:{user_id}:{banned_list_page}")
|
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(
|
builder.button(
|
||||||
text=_(key="user_card_back_to_banned_list_button"),
|
text=_(key="user_card_back_to_banned_list_button"),
|
||||||
callback_data=f"admin_action:view_banned:{banned_list_page}")
|
callback_data=f"admin_action:view_banned:{banned_list_page}")
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
from aiogram import Bot
|
from aiogram import Bot
|
||||||
|
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||||
from aiogram.utils.text_decorations import html_decoration as hd
|
from aiogram.utils.text_decorations import html_decoration as hd
|
||||||
from aiogram.exceptions import TelegramRetryAfter
|
from aiogram.exceptions import TelegramRetryAfter
|
||||||
from datetime import datetime, timezone
|
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 config.settings import Settings
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
@@ -34,8 +35,45 @@ class NotificationService:
|
|||||||
if username:
|
if username:
|
||||||
base_display = f"{base_display} ({username_for_display(username)})"
|
base_display = f"{base_display} ({username_for_display(username)})"
|
||||||
return base_display
|
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"""
|
"""Send message to configured log channel/group using message queue"""
|
||||||
if not self.settings.LOG_CHAT_ID:
|
if not self.settings.LOG_CHAT_ID:
|
||||||
return
|
return
|
||||||
@@ -49,6 +87,7 @@ class NotificationService:
|
|||||||
text=message,
|
text=message,
|
||||||
parse_mode="HTML",
|
parse_mode="HTML",
|
||||||
disable_web_page_preview=True,
|
disable_web_page_preview=True,
|
||||||
|
reply_markup=reply_markup,
|
||||||
message_thread_id=thread_id or self.settings.LOG_THREAD_ID
|
message_thread_id=thread_id or self.settings.LOG_THREAD_ID
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -64,6 +103,8 @@ class NotificationService:
|
|||||||
"parse_mode": "HTML",
|
"parse_mode": "HTML",
|
||||||
"disable_web_page_preview": True
|
"disable_web_page_preview": True
|
||||||
}
|
}
|
||||||
|
if reply_markup:
|
||||||
|
kwargs["reply_markup"] = reply_markup
|
||||||
|
|
||||||
# Add thread ID for supergroups if specified
|
# Add thread ID for supergroups if specified
|
||||||
if final_thread_id:
|
if final_thread_id:
|
||||||
@@ -124,7 +165,12 @@ class NotificationService:
|
|||||||
|
|
||||||
referral_text = ""
|
referral_text = ""
|
||||||
if referred_by_id:
|
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 = _(
|
message = _(
|
||||||
"log_new_user_registration",
|
"log_new_user_registration",
|
||||||
@@ -137,9 +183,10 @@ class NotificationService:
|
|||||||
referral_text=referral_text,
|
referral_text=referral_text,
|
||||||
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
timestamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send to log channel
|
# 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,
|
async def notify_payment_received(self, user_id: int, amount: float, currency: str,
|
||||||
months: int, payment_provider: str,
|
months: int, payment_provider: str,
|
||||||
@@ -182,7 +229,8 @@ class NotificationService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Send to log channel
|
# 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,
|
async def notify_promo_activation(self, user_id: int, promo_code: str, bonus_days: int,
|
||||||
username: Optional[str] = None):
|
username: Optional[str] = None):
|
||||||
@@ -212,7 +260,8 @@ class NotificationService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Send to log channel
|
# 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,
|
async def notify_trial_activation(self, user_id: int, end_date: datetime,
|
||||||
username: Optional[str] = None):
|
username: Optional[str] = None):
|
||||||
@@ -240,7 +289,8 @@ class NotificationService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Send to log channel
|
# 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,
|
async def notify_panel_sync(self, status: str, details: str,
|
||||||
users_processed: int, subs_synced: int,
|
users_processed: int, subs_synced: int,
|
||||||
@@ -275,7 +325,7 @@ class NotificationService:
|
|||||||
details=details
|
details=details
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send to log channel
|
# Send to log channel
|
||||||
await self._send_to_log_channel(message)
|
await self._send_to_log_channel(message)
|
||||||
|
|
||||||
async def notify_suspicious_promo_attempt(
|
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"))
|
timestamp=datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"))
|
||||||
|
|
||||||
# Send to log channel
|
# 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,
|
async def send_custom_notification(self, message: str, to_admins: bool = False,
|
||||||
to_log_channel: bool = True, thread_id: Optional[int] = None):
|
to_log_channel: bool = True, thread_id: Optional[int] = None):
|
||||||
|
|||||||
@@ -177,6 +177,8 @@ class ReferralService:
|
|||||||
"ACTIVE_BONUS",
|
"ACTIVE_BONUS",
|
||||||
"traffic_limit_bytes":
|
"traffic_limit_bytes":
|
||||||
self.settings.user_traffic_limit_bytes,
|
self.settings.user_traffic_limit_bytes,
|
||||||
|
"auto_renew_enabled":
|
||||||
|
False,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
await subscription_dal.deactivate_other_active_subscriptions(
|
await subscription_dal.deactivate_other_active_subscriptions(
|
||||||
|
|||||||
@@ -498,6 +498,15 @@ class SubscriptionService:
|
|||||||
session, panel_user_uuid, panel_sub_link_id
|
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 = {
|
sub_payload = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"panel_user_uuid": panel_user_uuid,
|
"panel_user_uuid": panel_user_uuid,
|
||||||
@@ -510,7 +519,7 @@ class SubscriptionService:
|
|||||||
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
|
"traffic_limit_bytes": self.settings.user_traffic_limit_bytes,
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"skip_notifications": provider == "tribute" and self.settings.TRIBUTE_SKIP_NOTIFICATIONS,
|
"skip_notifications": provider == "tribute" and self.settings.TRIBUTE_SKIP_NOTIFICATIONS,
|
||||||
"auto_renew_enabled": True,
|
"auto_renew_enabled": auto_renew_should_enable,
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
new_or_updated_sub = await subscription_dal.upsert_subscription(
|
new_or_updated_sub = await subscription_dal.upsert_subscription(
|
||||||
@@ -616,6 +625,7 @@ class SubscriptionService:
|
|||||||
"is_active": True,
|
"is_active": True,
|
||||||
"status_from_panel": "ACTIVE_BONUS",
|
"status_from_panel": "ACTIVE_BONUS",
|
||||||
"traffic_limit_bytes": traffic_limit,
|
"traffic_limit_bytes": traffic_limit,
|
||||||
|
"auto_renew_enabled": False,
|
||||||
}
|
}
|
||||||
await subscription_dal.deactivate_other_active_subscriptions(
|
await subscription_dal.deactivate_other_active_subscriptions(
|
||||||
session, panel_uuid, panel_sub_uuid
|
session, panel_uuid, panel_sub_uuid
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ class Settings(BaseSettings):
|
|||||||
YOOKASSA_PAYMENT_SUBJECT: str = Field(default="service")
|
YOOKASSA_PAYMENT_SUBJECT: str = Field(default="service")
|
||||||
# Single toggle to enable recurring payments (saving cards, managing payment methods, auto-renew)
|
# Single toggle to enable recurring payments (saving cards, managing payment methods, auto-renew)
|
||||||
YOOKASSA_AUTOPAYMENTS_ENABLED: bool = Field(default=False)
|
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
|
WEBHOOK_BASE_URL: Optional[str] = None
|
||||||
|
|
||||||
|
|||||||
@@ -162,3 +162,19 @@ async def delete_user_payment_method_by_provider_id(
|
|||||||
await session.delete(method)
|
await session.delete(method)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
return True
|
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
@@ -210,6 +210,8 @@
|
|||||||
"admin_user_card_title": "User Card",
|
"admin_user_card_title": "User Card",
|
||||||
"user_card_ban_button": "🚫 Ban",
|
"user_card_ban_button": "🚫 Ban",
|
||||||
"user_card_unban_button": "✅ Unban",
|
"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",
|
"user_card_back_to_banned_list_button": "⬅️ Back to Ban List",
|
||||||
"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",
|
||||||
@@ -291,7 +293,9 @@
|
|||||||
"inline_admin_financial_stats_title": "💰 Financial Statistics",
|
"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_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",
|
||||||
"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_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}",
|
||||||
"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}",
|
"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": "Paid via Tribute. Renew using your Tribute link.",
|
||||||
"subscription_tribute_notice_with_link": "Paid via Tribute. Renew: {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.",
|
"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.",
|
"subscription_not_active": "You don't have an active subscription.",
|
||||||
"error_service_unavailable": "Service unavailable. Please try again later.",
|
"error_service_unavailable": "Service unavailable. Please try again later.",
|
||||||
"error_payment_gateway": "Payment service error. Please try again later.",
|
"error_payment_gateway": "Payment service error. Please try again later.",
|
||||||
|
|||||||
+6
-1
@@ -220,6 +220,8 @@
|
|||||||
"admin_user_card_title": "Карточка пользователя",
|
"admin_user_card_title": "Карточка пользователя",
|
||||||
"user_card_ban_button": "🚫 Заблокировать",
|
"user_card_ban_button": "🚫 Заблокировать",
|
||||||
"user_card_unban_button": "✅ Разблокировать",
|
"user_card_unban_button": "✅ Разблокировать",
|
||||||
|
"user_card_open_profile_button": "👤 Открыть профиль",
|
||||||
|
"user_card_open_referrer_profile_button": "👤 Профиль пригласившего",
|
||||||
"user_card_back_to_banned_list_button": "⬅️ К списку забаненных",
|
"user_card_back_to_banned_list_button": "⬅️ К списку забаненных",
|
||||||
"admin_logs_menu_title": "Меню логов:",
|
"admin_logs_menu_title": "Меню логов:",
|
||||||
"admin_view_all_logs_button": "📜 Все логи сообщений",
|
"admin_view_all_logs_button": "📜 Все логи сообщений",
|
||||||
@@ -291,7 +293,9 @@
|
|||||||
"inline_admin_financial_stats_title": "💰 Финансовая статистика",
|
"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_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": "🖥 Системная статистика",
|
||||||
"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_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}",
|
||||||
"log_promo_activation": "🎁 <b>Активирован промокод</b>\n\n👤 Пользователь: {user_display}\n🏷 Код: <code>{promo_code}</code>\n🎯 Бонус: <b>+{bonus_days} дн.</b>\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": "Оплачено через Tribute. Продление делайте по ссылке Tribute.",
|
||||||
"subscription_tribute_notice_with_link": "Оплачено через Tribute. Продлить: {link}",
|
"subscription_tribute_notice_with_link": "Оплачено через Tribute. Продлить: {link}",
|
||||||
"subscription_autorenew_not_supported_for_tribute": "Автопродление управляется Tribute. Управляйте продлением в приложении/ссылке Tribute.",
|
"subscription_autorenew_not_supported_for_tribute": "Автопродление управляется Tribute. Управляйте продлением в приложении/ссылке Tribute.",
|
||||||
|
"autorenew_enable_requires_card": "Прежде чем включать автоплатёж, привяжите карту в разделе «Способы оплаты».",
|
||||||
"subscription_not_active": "У вас нет активной подписки.",
|
"subscription_not_active": "У вас нет активной подписки.",
|
||||||
"error_service_unavailable": "Сервис недоступен. Попробуйте позже.",
|
"error_service_unavailable": "Сервис недоступен. Попробуйте позже.",
|
||||||
"error_payment_gateway": "Ошибка платежного сервиса. Попробуйте позже.",
|
"error_payment_gateway": "Ошибка платежного сервиса. Попробуйте позже.",
|
||||||
|
|||||||
Reference in New Issue
Block a user