Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dace4c2938 | ||
|
|
b69c2ab18d | ||
|
|
56fc88c10e | ||
|
|
e74f801aec | ||
|
|
acf5f060a6 | ||
|
|
fb2672732d | ||
|
|
f4730c3ceb | ||
|
|
e6b3a7c66f | ||
|
|
24cf193f3c | ||
|
|
85bf0fb6fd | ||
|
|
b576d04501 | ||
|
|
97268b9906 | ||
|
|
1091b7b846 | ||
|
|
f191c1813e | ||
|
|
e78f9bf291 | ||
|
|
5c74a08ed8 | ||
|
|
14950fd559 | ||
|
|
711b9a2487 | ||
|
|
6e7eb6acfd | ||
|
|
b42fae8772 | ||
|
|
f707662125 | ||
|
|
60c6e0e961 | ||
|
|
b23b75b72e | ||
|
|
c69f02f7c0 | ||
|
|
f22e359684 | ||
|
|
d2402fea77 | ||
|
|
9b8ddb39da |
@@ -14,3 +14,5 @@ __pycache__/
|
|||||||
# Игнорировать Docker артефакты (если вдруг)
|
# Игнорировать Docker артефакты (если вдруг)
|
||||||
*.log
|
*.log
|
||||||
*.pid
|
*.pid
|
||||||
|
locales/ru_backup.json
|
||||||
|
locales/en_backup.json
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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:"))
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(_(
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 3–30 alphanumeric characters.",
|
"admin_promo_invalid_code_format": "Code must be 3–30 alphanumeric characters.",
|
||||||
"admin_promo_invalid_bonus_days": "Bonus days must be a positive number.",
|
"admin_promo_invalid_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
@@ -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": "Неактивна"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user