remnawave in country_mode/save shortid/bug fixes
This commit is contained in:
@@ -19,7 +19,7 @@ bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTM
|
||||
storage = MemoryStorage()
|
||||
dp = Dispatcher(bot=bot, storage=storage)
|
||||
|
||||
version = "4.2-a140499"
|
||||
version = "4.2-a160401"
|
||||
|
||||
register_middleware(dp)
|
||||
|
||||
|
||||
+23
-18
@@ -1,18 +1,21 @@
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import requests
|
||||
|
||||
from rich.console import Console
|
||||
from rich.prompt import Prompt, Confirm
|
||||
from rich.prompt import Confirm, Prompt
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
import re
|
||||
|
||||
from config import BOT_SERVICE
|
||||
|
||||
|
||||
try:
|
||||
sys.stdin.reconfigure(encoding='utf-8')
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
sys.stdin.reconfigure(encoding="utf-8")
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -30,14 +33,17 @@ console = Console()
|
||||
|
||||
|
||||
def print_logo():
|
||||
logo = Text("""
|
||||
logo = Text(
|
||||
"""
|
||||
███████╗ ██████╗ ██╗ ██████╗ ██████╗ ██████╗ ████████╗
|
||||
██╔════╝██╔═══██╗██║ ██╔═══██╗██╔══██╗██╔═══██╗╚══██╔══╝
|
||||
███████╗██║ ██║██║ ██║ ██║██████╔╝██║ ██║ ██║
|
||||
╚════██║██║ ██║██║ ██║ ██║██╔══██╗██║ ██║ ██║
|
||||
███████║╚██████╔╝███████╗╚██████╔╝██████╔╝╚██████╔╝ ██║
|
||||
╚══════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝
|
||||
""", style="bold cyan")
|
||||
""",
|
||||
style="bold cyan",
|
||||
)
|
||||
console.print(logo)
|
||||
|
||||
|
||||
@@ -69,7 +75,9 @@ def install_dependencies():
|
||||
console.print("[yellow]⚠️ Виртуальное окружение не найдено. Создаю...[/yellow]")
|
||||
subprocess.run("python3 -m venv venv", shell=True, check=True)
|
||||
|
||||
subprocess.run("bash -c 'source venv/bin/activate && pip install -r requirements.txt'", shell=True, check=True)
|
||||
subprocess.run(
|
||||
"bash -c 'source venv/bin/activate && pip install -r requirements.txt'", shell=True, check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
console.print("[red]❌ Ошибка при установке зависимостей.[/red]")
|
||||
|
||||
@@ -84,7 +92,7 @@ def get_local_version():
|
||||
path = os.path.join(PROJECT_DIR, "bot.py")
|
||||
if not os.path.isfile(path):
|
||||
return None
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
match = re.search(r'version\s*=\s*["\'](.+?)["\']', line)
|
||||
if match:
|
||||
@@ -133,7 +141,7 @@ def update_from_beta():
|
||||
console.print("[red]❌ Ошибка при клонировании. Обновление отменено.[/red]")
|
||||
return
|
||||
|
||||
subprocess.run(f'rsync -a --exclude=img --exclude=handlers/buttons.py {TEMP_DIR}/ {PROJECT_DIR}/', shell=True)
|
||||
subprocess.run(f"rsync -a --exclude=img --exclude=handlers/buttons.py {TEMP_DIR}/ {PROJECT_DIR}/", shell=True)
|
||||
subprocess.run(["rm", "-rf", TEMP_DIR])
|
||||
|
||||
install_dependencies()
|
||||
@@ -150,9 +158,7 @@ def update_from_release():
|
||||
install_rsync_if_needed()
|
||||
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://api.github.com/repos/Vladless/Solo_bot/releases", timeout=10
|
||||
)
|
||||
response = requests.get("https://api.github.com/repos/Vladless/Solo_bot/releases", timeout=10)
|
||||
releases = response.json()[:3]
|
||||
tag_choices = [r["tag_name"] for r in releases]
|
||||
|
||||
@@ -164,8 +170,7 @@ def update_from_release():
|
||||
console.print(f"[cyan]{idx}.[/cyan] {tag}")
|
||||
|
||||
selected = Prompt.ask(
|
||||
"[bold blue]Выберите номер релиза[/bold blue]",
|
||||
choices=[str(i) for i in range(1, len(tag_choices) + 1)]
|
||||
"[bold blue]Выберите номер релиза[/bold blue]", choices=[str(i) for i in range(1, len(tag_choices) + 1)]
|
||||
)
|
||||
tag_name = tag_choices[int(selected) - 1]
|
||||
|
||||
@@ -173,7 +178,7 @@ def update_from_release():
|
||||
subprocess.run(["rm", "-rf", TEMP_DIR])
|
||||
subprocess.run(f"git clone --depth 1 --branch {tag_name} {GITHUB_REPO} {TEMP_DIR}", shell=True, check=True)
|
||||
|
||||
subprocess.run(f'rsync -a --exclude=img --exclude=handlers/buttons.py {TEMP_DIR}/ {PROJECT_DIR}/', shell=True)
|
||||
subprocess.run(f"rsync -a --exclude=img --exclude=handlers/buttons.py {TEMP_DIR}/ {PROJECT_DIR}/", shell=True)
|
||||
subprocess.run(["rm", "-rf", TEMP_DIR])
|
||||
|
||||
install_dependencies()
|
||||
@@ -223,8 +228,8 @@ def main():
|
||||
if os.geteuid() != 0:
|
||||
console.print("[bold red]⛔ Требуется запуск от имени root или через sudo.[/bold red]")
|
||||
sys.exit(1)
|
||||
|
||||
os.chdir(PROJECT_DIR)
|
||||
|
||||
os.chdir(PROJECT_DIR)
|
||||
print_logo()
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
@@ -10,14 +11,27 @@ from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from py3xui import AsyncApi
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backup import create_backup_and_send_to_admins
|
||||
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, TOTAL_GB, USE_COUNTRY_SELECTION, REMNAWAVE_PASSWORD, REMNAWAVE_LOGIN
|
||||
from config import (
|
||||
ADMIN_PASSWORD,
|
||||
ADMIN_USERNAME,
|
||||
DATABASE_URL,
|
||||
REMNAWAVE_LOGIN,
|
||||
REMNAWAVE_PASSWORD,
|
||||
TOTAL_GB,
|
||||
USE_COUNTRY_SELECTION,
|
||||
)
|
||||
from database import check_unique_server_name, get_servers, update_key_expiry
|
||||
from filters.admin import IsAdminFilter
|
||||
from handlers.keys.key_utils import create_client_on_server, create_key_on_cluster, renew_key_in_cluster, delete_key_from_cluster
|
||||
from handlers.keys.key_utils import (
|
||||
create_client_on_server,
|
||||
create_key_on_cluster,
|
||||
delete_key_from_cluster,
|
||||
renew_key_in_cluster,
|
||||
)
|
||||
from logger import logger
|
||||
from panels.remnawave import RemnawaveAPI
|
||||
|
||||
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
|
||||
from .keyboard import (
|
||||
@@ -26,10 +40,9 @@ from .keyboard import (
|
||||
build_cluster_management_kb,
|
||||
build_clusters_editor_kb,
|
||||
build_manage_cluster_kb,
|
||||
build_panel_type_kb,
|
||||
build_sync_cluster_kb,
|
||||
build_panel_type_kb
|
||||
)
|
||||
from panels.remnawave import RemnawaveAPI
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -194,7 +207,7 @@ async def handle_subscription_url_input(message: Message, state: FSMContext):
|
||||
|
||||
await message.answer(
|
||||
text=f"<b>Введите inbound_id для сервера {server_name} в кластере {cluster_name}:</b>\n\n"
|
||||
f"Для Remnawave это UUID Инбаунда, для 3x-ui — просто ID (например, <code>1</code>).",
|
||||
f"Для Remnawave это UUID Инбаунда, для 3x-ui — просто ID (например, <code>1</code>).",
|
||||
reply_markup=build_admin_back_kb("clusters"),
|
||||
)
|
||||
await state.set_state(AdminClusterStates.waiting_for_inbound_id)
|
||||
@@ -216,7 +229,9 @@ async def handle_inbound_id_input(message: Message, state: FSMContext):
|
||||
|
||||
|
||||
@router.callback_query(AdminClusterCallback.filter(F.action.in_(["panel_3xui", "panel_remnawave"])), IsAdminFilter())
|
||||
async def handle_panel_type_selection(callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext):
|
||||
async def handle_panel_type_selection(
|
||||
callback_query: CallbackQuery, callback_data: AdminClusterCallback, state: FSMContext
|
||||
):
|
||||
panel_type = "3x-ui" if callback_data.action == "panel_3xui" else "remnawave"
|
||||
|
||||
user_data = await state.get_data()
|
||||
@@ -437,7 +452,7 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
|
||||
|
||||
try:
|
||||
query_keys = """
|
||||
SELECT tg_id, client_id, email, expiry_time
|
||||
SELECT tg_id, client_id, email, expiry_time, remnawave_link
|
||||
FROM keys
|
||||
WHERE server_id = $1
|
||||
"""
|
||||
@@ -458,18 +473,17 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
|
||||
await delete_key_from_cluster(cluster_name, key["email"], key["client_id"])
|
||||
|
||||
await session.execute(
|
||||
"DELETE FROM keys WHERE tg_id = $1 AND client_id = $2",
|
||||
key["tg_id"],
|
||||
key["client_id"]
|
||||
"DELETE FROM keys WHERE tg_id = $1 AND client_id = $2", key["tg_id"], key["client_id"]
|
||||
)
|
||||
|
||||
result = await create_key_on_cluster(
|
||||
await create_key_on_cluster(
|
||||
cluster_name,
|
||||
key["tg_id"],
|
||||
key["client_id"],
|
||||
key["email"],
|
||||
key["expiry_time"],
|
||||
session=session,
|
||||
remnawave_link=key.get("remnawave_link"),
|
||||
)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
@@ -477,7 +491,6 @@ async def handle_sync_cluster(callback_query: types.CallbackQuery, callback_data
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при синхронизации ключа {key['client_id']} в {cluster_name}: {e}")
|
||||
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=f"✅ Ключи успешно синхронизированы для кластера {cluster_name}",
|
||||
reply_markup=build_admin_back_kb("clusters"),
|
||||
@@ -622,8 +635,7 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
existing_cluster = await conn.fetchval(
|
||||
"SELECT cluster_name FROM servers WHERE cluster_name = $1 LIMIT 1",
|
||||
new_cluster_name
|
||||
"SELECT cluster_name FROM servers WHERE cluster_name = $1 LIMIT 1", new_cluster_name
|
||||
)
|
||||
if existing_cluster:
|
||||
await message.answer(
|
||||
@@ -632,23 +644,16 @@ async def handle_new_cluster_name_input(message: Message, state: FSMContext, ses
|
||||
)
|
||||
return
|
||||
|
||||
keys_count = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM keys WHERE server_id = $1",
|
||||
old_cluster_name
|
||||
)
|
||||
keys_count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", old_cluster_name)
|
||||
|
||||
async with conn.transaction():
|
||||
await conn.execute(
|
||||
"UPDATE servers SET cluster_name = $1 WHERE cluster_name = $2",
|
||||
new_cluster_name,
|
||||
old_cluster_name
|
||||
"UPDATE servers SET cluster_name = $1 WHERE cluster_name = $2", new_cluster_name, old_cluster_name
|
||||
)
|
||||
|
||||
if keys_count > 0:
|
||||
await conn.execute(
|
||||
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
|
||||
new_cluster_name,
|
||||
old_cluster_name
|
||||
"UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_cluster_name
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
@@ -729,7 +734,7 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess
|
||||
existing_server = await conn.fetchval(
|
||||
"SELECT server_name FROM servers WHERE cluster_name = $1 AND server_name = $2 LIMIT 1",
|
||||
cluster_name,
|
||||
new_server_name
|
||||
new_server_name,
|
||||
)
|
||||
if existing_server:
|
||||
await message.answer(
|
||||
@@ -738,24 +743,19 @@ async def handle_new_server_name_input(message: Message, state: FSMContext, sess
|
||||
)
|
||||
return
|
||||
|
||||
keys_count = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM keys WHERE server_id = $1",
|
||||
old_server_name
|
||||
)
|
||||
keys_count = await conn.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", old_server_name)
|
||||
|
||||
async with conn.transaction():
|
||||
await conn.execute(
|
||||
"UPDATE servers SET server_name = $1 WHERE cluster_name = $2 AND server_name = $3",
|
||||
new_server_name,
|
||||
cluster_name,
|
||||
old_server_name
|
||||
old_server_name,
|
||||
)
|
||||
|
||||
if keys_count > 0:
|
||||
await conn.execute(
|
||||
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
|
||||
new_server_name,
|
||||
old_server_name
|
||||
"UPDATE keys SET server_id = $1 WHERE server_id = $2", new_server_name, old_server_name
|
||||
)
|
||||
|
||||
final_text = f"✅ Название сервера успешно изменено с '{old_server_name}' на '{new_server_name}' в кластере '{cluster_name}'!"
|
||||
@@ -787,20 +787,14 @@ async def handle_server_transfer(callback_query: CallbackQuery, state: FSMContex
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
async with conn.transaction():
|
||||
await conn.execute(
|
||||
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
|
||||
new_server_name,
|
||||
old_server_name
|
||||
)
|
||||
await conn.execute("UPDATE keys SET server_id = $1 WHERE server_id = $2", new_server_name, old_server_name)
|
||||
|
||||
await conn.execute(
|
||||
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
|
||||
cluster_name,
|
||||
old_server_name
|
||||
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, old_server_name
|
||||
)
|
||||
|
||||
base_text = f"✅ Ключи успешно перенесены на сервер '{new_server_name}', сервер '{old_server_name}' удален!"
|
||||
sync_reminder = "\n\n⚠️ Не забудьте сделать \"Синхронизацию\"."
|
||||
sync_reminder = '\n\n⚠️ Не забудьте сделать "Синхронизацию".'
|
||||
final_text = base_text + (sync_reminder if USE_COUNTRY_SELECTION else "")
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
@@ -831,21 +825,13 @@ async def handle_cluster_transfer(callback_query: CallbackQuery, state: FSMConte
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
async with conn.transaction():
|
||||
await conn.execute("UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_server_name)
|
||||
await conn.execute(
|
||||
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
|
||||
new_cluster_name,
|
||||
old_server_name
|
||||
)
|
||||
await conn.execute(
|
||||
"UPDATE keys SET server_id = $1 WHERE server_id = $2",
|
||||
new_cluster_name,
|
||||
old_cluster_name
|
||||
"UPDATE keys SET server_id = $1 WHERE server_id = $2", new_cluster_name, old_cluster_name
|
||||
)
|
||||
|
||||
await conn.execute(
|
||||
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
|
||||
cluster_name,
|
||||
old_server_name
|
||||
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, old_server_name
|
||||
)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Optional
|
||||
|
||||
from aiogram.filters.callback_data import CallbackData
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
@@ -134,13 +132,7 @@ def build_sync_cluster_kb(cluster_servers: list, cluster_name: str) -> InlineKey
|
||||
|
||||
def build_panel_type_kb() -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(
|
||||
text="🌐 3X-UI",
|
||||
callback_data=AdminClusterCallback(action="panel_3xui").pack()
|
||||
)
|
||||
builder.button(
|
||||
text="🌀 Remnawave",
|
||||
callback_data=AdminClusterCallback(action="panel_remnawave").pack()
|
||||
)
|
||||
builder.button(text="🌐 3X-UI", callback_data=AdminClusterCallback(action="panel_3xui").pack())
|
||||
builder.button(text="🌀 Remnawave", callback_data=AdminClusterCallback(action="panel_remnawave").pack())
|
||||
builder.row(build_admin_back_btn("clusters"))
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from datetime import datetime
|
||||
import html
|
||||
import pytz
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import pytz
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -217,18 +219,15 @@ async def handle_coupon_delete(callback_query: CallbackQuery, callback_data: Adm
|
||||
coupon_code = callback_data.coupon_code
|
||||
kb = InlineKeyboardBuilder()
|
||||
kb.button(
|
||||
text="✅ Да, удалить",
|
||||
callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=True).pack()
|
||||
text="✅ Да, удалить", callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=True).pack()
|
||||
)
|
||||
kb.button(
|
||||
text="❌ Нет, отменить",
|
||||
callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=False).pack()
|
||||
text="❌ Нет, отменить", callback_data=AdminCouponDeleteCallback(coupon_code=coupon_code, confirm=False).pack()
|
||||
)
|
||||
kb.adjust(1)
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
f"Вы уверены, что хотите удалить купон <b>{coupon_code}</b>?",
|
||||
reply_markup=kb.as_markup()
|
||||
f"Вы уверены, что хотите удалить купон <b>{coupon_code}</b>?", reply_markup=kb.as_markup()
|
||||
)
|
||||
|
||||
|
||||
@@ -242,15 +241,13 @@ async def confirm_coupon_delete(callback_query: CallbackQuery, callback_data: Ad
|
||||
result = await delete_coupon(coupon_code, session)
|
||||
if not result:
|
||||
await callback_query.message.edit_text(
|
||||
f"❌ Купон с кодом {coupon_code} не найден.",
|
||||
reply_markup=build_admin_back_kb("coupons")
|
||||
f"❌ Купон с кодом {coupon_code} не найден.", reply_markup=build_admin_back_kb("coupons")
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при удалении купона: {e}")
|
||||
await callback_query.message.edit_text(
|
||||
"Произошла ошибка при удалении купона.",
|
||||
reply_markup=build_admin_back_kb("coupons")
|
||||
"Произошла ошибка при удалении купона.", reply_markup=build_admin_back_kb("coupons")
|
||||
)
|
||||
return
|
||||
|
||||
@@ -295,7 +292,11 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
|
||||
return
|
||||
|
||||
title = f"Купон {coupon['code']}"
|
||||
description = f"Получи {coupon['amount']} рублей!" if coupon["amount"] > 0 else f"Продли подписку на {format_days(coupon['days'])}!"
|
||||
description = (
|
||||
f"Получи {coupon['amount']} рублей!"
|
||||
if coupon["amount"] > 0
|
||||
else f"Продли подписку на {format_days(coupon['days'])}!"
|
||||
)
|
||||
message_text = (
|
||||
f"🎫 <b>Купон:</b> {coupon['code']}\n"
|
||||
f"{'💰 <b>Бонус:</b> ' + str(coupon['amount']) + ' рублей' if coupon['amount'] > 0 else '⏳ <b>Продление:</b> ' + format_days(coupon['days'])}\n"
|
||||
@@ -309,15 +310,8 @@ async def inline_coupon_handler(inline_query: InlineQuery, session: Any):
|
||||
id=coupon_code,
|
||||
title=title,
|
||||
description=description,
|
||||
input_message_content=InputTextMessageContent(
|
||||
message_text=message_text,
|
||||
parse_mode=ParseMode.HTML
|
||||
),
|
||||
input_message_content=InputTextMessageContent(message_text=message_text, parse_mode=ParseMode.HTML),
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
await inline_query.answer(
|
||||
results=[result],
|
||||
cache_time=86400,
|
||||
is_personal=True
|
||||
)
|
||||
await inline_query.answer(results=[result], cache_time=86400, is_personal=True)
|
||||
|
||||
@@ -12,7 +12,7 @@ from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
|
||||
|
||||
class AdminCouponDeleteCallback(CallbackData, prefix="admin_coupon_delete"):
|
||||
coupon_code: str
|
||||
confirm: Optional[bool] = None
|
||||
confirm: bool | None = None
|
||||
|
||||
|
||||
def build_coupons_kb() -> InlineKeyboardMarkup:
|
||||
@@ -59,7 +59,11 @@ def build_coupons_list_kb(coupons: list, current_page: int, total_pages: int) ->
|
||||
def format_coupons_list(coupons: list, username_bot: str) -> str:
|
||||
coupon_list = "📜 Список всех купонов:\n\n"
|
||||
for coupon in coupons:
|
||||
value_text = f"💰 <b>Сумма:</b> {coupon['amount']} рублей" if coupon["amount"] > 0 else f"⏳ <b>{format_days(coupon['days'])}</b>"
|
||||
value_text = (
|
||||
f"💰 <b>Сумма:</b> {coupon['amount']} рублей"
|
||||
if coupon["amount"] > 0
|
||||
else f"⏳ <b>{format_days(coupon['days'])}</b>"
|
||||
)
|
||||
coupon_list += (
|
||||
f"🏷️ <b>Код:</b> {coupon['code']}\n"
|
||||
f"{value_text}\n"
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.filters import Command
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from bot import version
|
||||
from filters.admin import IsAdminFilter
|
||||
from logger import logger
|
||||
|
||||
from .keyboard import AdminPanelCallback, build_panel_kb
|
||||
|
||||
from logger import logger
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ def build_manage_server_kb(server_name: str, cluster_name: str) -> InlineKeyboar
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
builder.button(text="🗑️ Удалить", callback_data=AdminServerCallback(action="delete", data=server_name).pack())
|
||||
builder.button(text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack())
|
||||
builder.button(
|
||||
text="✏️ Сменить название", callback_data=AdminServerCallback(action="rename", data=server_name).pack()
|
||||
)
|
||||
builder.button(text=BACK, callback_data=AdminClusterCallback(action="manage", data=cluster_name).pack())
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -4,10 +4,10 @@ from aiogram import F, Router, types
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from handlers.buttons import BACK
|
||||
|
||||
from database import delete_server, get_servers
|
||||
from filters.admin import IsAdminFilter
|
||||
from handlers.buttons import BACK
|
||||
|
||||
from ..panel.keyboard import build_admin_back_kb
|
||||
from .keyboard import (
|
||||
@@ -73,10 +73,7 @@ async def process_callback_delete_server(
|
||||
)
|
||||
return
|
||||
|
||||
keys_count = await session.fetchval(
|
||||
"SELECT COUNT(*) FROM keys WHERE server_id = $1",
|
||||
server_name
|
||||
)
|
||||
keys_count = await session.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", server_name)
|
||||
|
||||
if keys_count > 0:
|
||||
await state.update_data(server_name=server_name, cluster_name=cluster_name)
|
||||
@@ -87,7 +84,7 @@ async def process_callback_delete_server(
|
||||
FROM servers
|
||||
WHERE server_name != $1
|
||||
""",
|
||||
server_name
|
||||
server_name,
|
||||
)
|
||||
|
||||
if all_servers:
|
||||
@@ -96,13 +93,12 @@ async def process_callback_delete_server(
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=f"{server['server_name']} ({server['key_count']})",
|
||||
callback_data=f"transfer_to_server|{server['server_name']}|{server_name}"
|
||||
callback_data=f"transfer_to_server|{server['server_name']}|{server_name}",
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=BACK,
|
||||
callback_data=AdminServerCallback(action="manage", data=server_name).pack()
|
||||
text=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -114,22 +110,16 @@ async def process_callback_delete_server(
|
||||
return
|
||||
|
||||
remaining_servers = await session.fetchval(
|
||||
"SELECT COUNT(*) FROM servers WHERE cluster_name = $1 AND server_name != $2",
|
||||
cluster_name,
|
||||
server_name
|
||||
"SELECT COUNT(*) FROM servers WHERE cluster_name = $1 AND server_name != $2", cluster_name, server_name
|
||||
)
|
||||
|
||||
if remaining_servers == 0:
|
||||
other_clusters = await session.fetch(
|
||||
"SELECT DISTINCT cluster_name FROM servers WHERE cluster_name != $1",
|
||||
cluster_name
|
||||
"SELECT DISTINCT cluster_name FROM servers WHERE cluster_name != $1", cluster_name
|
||||
)
|
||||
|
||||
if other_clusters:
|
||||
cluster_keys_count = await session.fetchval(
|
||||
"SELECT COUNT(*) FROM keys WHERE server_id = $1",
|
||||
cluster_name
|
||||
)
|
||||
cluster_keys_count = await session.fetchval("SELECT COUNT(*) FROM keys WHERE server_id = $1", cluster_name)
|
||||
|
||||
if cluster_keys_count > 0:
|
||||
await state.update_data(server_name=server_name, cluster_name=cluster_name)
|
||||
@@ -141,7 +131,7 @@ async def process_callback_delete_server(
|
||||
WHERE cluster_name != $1
|
||||
GROUP BY cluster_name
|
||||
""",
|
||||
cluster_name
|
||||
cluster_name,
|
||||
)
|
||||
|
||||
builder = InlineKeyboardBuilder()
|
||||
@@ -149,13 +139,12 @@ async def process_callback_delete_server(
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=f"{cluster['cluster_name']} ({cluster['key_count']})",
|
||||
callback_data=f"transfer_to_cluster|{cluster['cluster_name']}|{cluster_name}|{server_name}"
|
||||
callback_data=f"transfer_to_cluster|{cluster['cluster_name']}|{cluster_name}|{server_name}",
|
||||
)
|
||||
)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=BACK,
|
||||
callback_data=AdminServerCallback(action="manage", data=server_name).pack()
|
||||
text=BACK, callback_data=AdminServerCallback(action="manage", data=server_name).pack()
|
||||
)
|
||||
)
|
||||
|
||||
@@ -167,9 +156,7 @@ async def process_callback_delete_server(
|
||||
return
|
||||
|
||||
await session.execute(
|
||||
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
|
||||
cluster_name,
|
||||
server_name
|
||||
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, server_name
|
||||
)
|
||||
await callback_query.message.edit_text(
|
||||
text=f"✅ Сервер '{server_name}' удален. Кластер '{cluster_name}' также удален, так как в нем не осталось серверов.",
|
||||
@@ -177,9 +164,7 @@ async def process_callback_delete_server(
|
||||
)
|
||||
else:
|
||||
await session.execute(
|
||||
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2",
|
||||
cluster_name,
|
||||
server_name
|
||||
"DELETE FROM servers WHERE cluster_name = $1 AND server_name = $2", cluster_name, server_name
|
||||
)
|
||||
await callback_query.message.edit_text(
|
||||
text=f"✅ Сервер '{server_name}' удален.",
|
||||
|
||||
@@ -826,7 +826,9 @@ async def handle_users_export_referrals(
|
||||
|
||||
|
||||
@router.callback_query(AdminUserEditorCallback.filter(F.action == "users_create_key"), IsAdminFilter())
|
||||
async def handle_create_key_start(callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any):
|
||||
async def handle_create_key_start(
|
||||
callback_query: CallbackQuery, callback_data: AdminUserEditorCallback, state: FSMContext, session: Any
|
||||
):
|
||||
tg_id = callback_data.tg_id
|
||||
await state.update_data(tg_id=tg_id)
|
||||
|
||||
@@ -838,8 +840,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data:
|
||||
|
||||
if not countries:
|
||||
await callback_query.message.edit_text(
|
||||
"❌ Нет доступных стран для создания ключа.",
|
||||
reply_markup=build_editor_kb(tg_id)
|
||||
"❌ Нет доступных стран для создания ключа.", reply_markup=build_editor_kb(tg_id)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -850,8 +851,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data:
|
||||
builder.row(build_admin_back_btn())
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
"🌍 <b>Выберите страну для создания ключа:</b>",
|
||||
reply_markup=builder.as_markup()
|
||||
"🌍 <b>Выберите страну для создания ключа:</b>", reply_markup=builder.as_markup()
|
||||
)
|
||||
return
|
||||
|
||||
@@ -862,8 +862,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data:
|
||||
|
||||
if not cluster_names:
|
||||
await callback_query.message.edit_text(
|
||||
"❌ Нет доступных кластеров для создания ключа.",
|
||||
reply_markup=build_editor_kb(tg_id)
|
||||
"❌ Нет доступных кластеров для создания ключа.", reply_markup=build_editor_kb(tg_id)
|
||||
)
|
||||
return
|
||||
|
||||
@@ -873,8 +872,7 @@ async def handle_create_key_start(callback_query: CallbackQuery, callback_data:
|
||||
builder.row(build_admin_back_btn())
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
"🌐 <b>Выберите кластер для создания ключа:</b>",
|
||||
reply_markup=builder.as_markup()
|
||||
"🌐 <b>Выберите кластер для создания ключа:</b>", reply_markup=builder.as_markup()
|
||||
)
|
||||
|
||||
|
||||
@@ -891,8 +889,7 @@ async def handle_create_key_country(callback_query: CallbackQuery, state: FSMCon
|
||||
builder.row(build_admin_back_btn())
|
||||
|
||||
await callback_query.message.edit_text(
|
||||
text=f"🕒 <b>Выберите срок действия ключа для страны {country}:</b>",
|
||||
reply_markup=builder.as_markup()
|
||||
text=f"🕒 <b>Выберите срок действия ключа для страны {country}:</b>", reply_markup=builder.as_markup()
|
||||
)
|
||||
|
||||
|
||||
@@ -928,15 +925,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo
|
||||
|
||||
if USE_COUNTRY_SELECTION and "country" in data:
|
||||
country = data["country"]
|
||||
await create_key_on_cluster(
|
||||
country,
|
||||
tg_id,
|
||||
client_id,
|
||||
email,
|
||||
expiry_ms,
|
||||
plan=months,
|
||||
session=session
|
||||
)
|
||||
await create_key_on_cluster(country, tg_id, client_id, email, expiry_ms, plan=months, session=session)
|
||||
|
||||
await state.clear()
|
||||
await callback_query.message.edit_text(
|
||||
@@ -946,15 +935,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo
|
||||
|
||||
elif "cluster_name" in data:
|
||||
cluster_name = data["cluster_name"]
|
||||
await create_key_on_cluster(
|
||||
cluster_name,
|
||||
tg_id,
|
||||
client_id,
|
||||
email,
|
||||
expiry_ms,
|
||||
plan=months,
|
||||
session=session
|
||||
)
|
||||
await create_key_on_cluster(cluster_name, tg_id, client_id, email, expiry_ms, plan=months, session=session)
|
||||
|
||||
await state.clear()
|
||||
await callback_query.message.edit_text(
|
||||
@@ -968,8 +949,7 @@ async def handle_create_key_duration(callback_query: CallbackQuery, state: FSMCo
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании ключа: {e}")
|
||||
await callback_query.message.edit_text(
|
||||
"❌ Не удалось создать ключ. Попробуйте позже.",
|
||||
reply_markup=build_editor_kb(data.get("tg_id", 0))
|
||||
"❌ Не удалось создать ключ. Попробуйте позже.", reply_markup=build_editor_kb(data.get("tg_id", 0))
|
||||
)
|
||||
|
||||
|
||||
|
||||
+14
-18
@@ -1,16 +1,20 @@
|
||||
import html
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
import pytz
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
import pytz
|
||||
import html
|
||||
from datetime import datetime
|
||||
|
||||
from config import ADMIN_ID
|
||||
from database import (
|
||||
add_connection,
|
||||
check_connection_exists,
|
||||
check_coupon_usage,
|
||||
create_coupon_usage,
|
||||
get_coupon_by_code,
|
||||
@@ -18,18 +22,16 @@ from database import (
|
||||
update_balance,
|
||||
update_coupon_usage_count,
|
||||
update_key_expiry,
|
||||
check_connection_exists,
|
||||
add_connection,
|
||||
)
|
||||
from handlers.buttons import MAIN_MENU
|
||||
from handlers.keys.key_utils import renew_key_in_cluster
|
||||
from handlers.profile import process_callback_view_profile
|
||||
from handlers.texts import (
|
||||
COUPON_ALREADY_USED_MSG,
|
||||
COUPON_INPUT_PROMPT,
|
||||
COUPON_NOT_FOUND_MSG,
|
||||
)
|
||||
from handlers.utils import edit_or_send_message, format_days
|
||||
from handlers.profile import process_callback_view_profile
|
||||
from logger import logger
|
||||
|
||||
|
||||
@@ -67,9 +69,7 @@ async def process_coupon_code(message: Message, state: FSMContext, session: Any)
|
||||
await activate_coupon(message, state, session, coupon_code=coupon_code)
|
||||
|
||||
|
||||
async def activate_coupon(
|
||||
message: Message, state: FSMContext, session: Any, coupon_code: str, admin: bool = False
|
||||
):
|
||||
async def activate_coupon(message: Message, state: FSMContext, session: Any, coupon_code: str, admin: bool = False):
|
||||
logger.info(f"Активация купона: {coupon_code}")
|
||||
coupon_record = await get_coupon_by_code(coupon_code, session)
|
||||
|
||||
@@ -172,9 +172,7 @@ async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext,
|
||||
return
|
||||
|
||||
key = await session.fetchrow(
|
||||
"SELECT * FROM keys WHERE tg_id = $1 AND client_id = $2",
|
||||
callback_query.from_user.id,
|
||||
client_id
|
||||
"SELECT * FROM keys WHERE tg_id = $1 AND client_id = $2", callback_query.from_user.id, client_id
|
||||
)
|
||||
if not key or key["is_frozen"]:
|
||||
await callback_query.message.edit_text("❌ Выбранная подписка не найдена или заморожена.")
|
||||
@@ -186,11 +184,7 @@ async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext,
|
||||
new_expiry = max(now_ms, current_expiry) + (coupon["days"] * 86400 * 1000)
|
||||
|
||||
await renew_key_in_cluster(
|
||||
cluster_id=key["server_id"],
|
||||
email=key["email"],
|
||||
client_id=client_id,
|
||||
new_expiry_time=new_expiry,
|
||||
total_gb=0
|
||||
cluster_id=key["server_id"], email=key["email"], client_id=client_id, new_expiry_time=new_expiry, total_gb=0
|
||||
)
|
||||
await update_key_expiry(client_id, new_expiry, session)
|
||||
|
||||
@@ -198,7 +192,9 @@ async def handle_key_extension(callback_query: CallbackQuery, state: FSMContext,
|
||||
await create_coupon_usage(coupon["id"], callback_query.from_user.id, session)
|
||||
|
||||
alias = key.get("alias") or key["email"]
|
||||
expiry_date = datetime.fromtimestamp(new_expiry / 1000, tz=pytz.timezone("Europe/Moscow")).strftime("%d.%m.%y, %H:%M")
|
||||
expiry_date = datetime.fromtimestamp(new_expiry / 1000, tz=pytz.timezone("Europe/Moscow")).strftime(
|
||||
"%d.%m.%y, %H:%M"
|
||||
)
|
||||
await callback_query.message.answer(
|
||||
f"✅ Купон активирован, подписка <b>{alias}</b> продлена на {format_days(coupon['days'])}⏳ до {expiry_date}📆."
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiogram import F, Router
|
||||
|
||||
@@ -2,20 +2,14 @@ __all__ = ("router",)
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
from .keys import router as keys_router
|
||||
from .key_view import router as view_router
|
||||
from .key_renew import router as renew_router
|
||||
from .key_freeze import router as freeze_router
|
||||
from .key_connect import router as connect_router
|
||||
from .key_freeze import router as freeze_router
|
||||
from .key_mode import router as key_mode_router
|
||||
from .key_renew import router as renew_router
|
||||
from .key_view import router as view_router
|
||||
from .keys import router as keys_router
|
||||
|
||||
|
||||
router = Router(name="keys_main_router")
|
||||
|
||||
router.include_routers(
|
||||
keys_router,
|
||||
view_router,
|
||||
renew_router,
|
||||
freeze_router,
|
||||
connect_router,
|
||||
key_mode_router
|
||||
)
|
||||
router.include_routers(keys_router, view_router, renew_router, freeze_router, connect_router, key_mode_router)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
@@ -6,19 +7,10 @@ import asyncpg
|
||||
import qrcode
|
||||
|
||||
from aiogram import F, Router, types
|
||||
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
CONNECT_ANDROID,
|
||||
CONNECT_IOS,
|
||||
DATABASE_URL,
|
||||
DOWNLOAD_ANDROID,
|
||||
DOWNLOAD_IOS,
|
||||
INSTRUCTIONS_BUTTON
|
||||
)
|
||||
|
||||
from config import CONNECT_ANDROID, CONNECT_IOS, DATABASE_URL, DOWNLOAD_ANDROID, DOWNLOAD_IOS, INSTRUCTIONS_BUTTON
|
||||
from handlers.buttons import (
|
||||
ANDROID,
|
||||
BACK,
|
||||
@@ -108,7 +100,7 @@ async def process_callback_connect_phone(callback_query: CallbackQuery):
|
||||
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{key_link}"),
|
||||
)
|
||||
if INSTRUCTIONS_BUTTON:
|
||||
builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
|
||||
builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{email}"))
|
||||
|
||||
await edit_or_send_message(
|
||||
@@ -234,4 +226,4 @@ async def show_qr_code(callback_query: types.CallbackQuery, session: Any):
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при генерации QR: {e}", exc_info=True)
|
||||
await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.")
|
||||
await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.")
|
||||
|
||||
@@ -3,7 +3,6 @@ import time
|
||||
from typing import Any
|
||||
|
||||
from aiogram import F, Router
|
||||
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
@@ -216,4 +215,4 @@ async def process_callback_freeze_subscription_confirm(callback_query: CallbackQ
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await handle_error(tg_id, callback_query, f"Ошибка при заморозке подписки: {e}")
|
||||
await handle_error(tg_id, callback_query, f"Ошибка при заморозке подписки: {e}")
|
||||
|
||||
@@ -2,9 +2,10 @@ __all__ = ("router",)
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
from .key_create import router as create_router
|
||||
from .key_cluster_mode import router as cluster_router
|
||||
from .key_country_mode import router as country_router
|
||||
from .key_create import router as create_router
|
||||
|
||||
|
||||
router = Router(name="key_mode_router")
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
import pytz
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message, FSInputFile, WebAppInfo
|
||||
from aiogram.types import CallbackQuery, FSInputFile, InlineKeyboardButton, Message, WebAppInfo
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from bot import bot
|
||||
@@ -20,19 +20,17 @@ from database import (
|
||||
update_balance,
|
||||
update_trial,
|
||||
)
|
||||
from handlers.buttons import (
|
||||
CONNECT_DEVICE,
|
||||
CONNECT_PHONE,
|
||||
MAIN_MENU,
|
||||
PC_BUTTON,
|
||||
TV_BUTTON,
|
||||
SUPPORT
|
||||
)
|
||||
from handlers.buttons import CONNECT_DEVICE, CONNECT_PHONE, MAIN_MENU, PC_BUTTON, SUPPORT, TV_BUTTON
|
||||
from handlers.keys.key_utils import create_key_on_cluster
|
||||
from handlers.texts import (
|
||||
key_message_success,
|
||||
)
|
||||
from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster, is_full_remnawave_cluster
|
||||
from handlers.utils import (
|
||||
edit_or_send_message,
|
||||
generate_random_email,
|
||||
get_least_loaded_cluster,
|
||||
is_full_remnawave_cluster,
|
||||
)
|
||||
from logger import logger
|
||||
|
||||
|
||||
@@ -63,9 +61,7 @@ async def key_cluster_mode(
|
||||
|
||||
try:
|
||||
least_loaded_cluster = await get_least_loaded_cluster()
|
||||
await create_key_on_cluster(
|
||||
least_loaded_cluster, tg_id, client_id, email, expiry_timestamp, plan, session
|
||||
)
|
||||
await create_key_on_cluster(least_loaded_cluster, tg_id, client_id, email, expiry_timestamp, plan, session)
|
||||
logger.info(f"[Key Creation] Ключ создан на кластере {least_loaded_cluster} для пользователя {tg_id}")
|
||||
|
||||
key_record = await get_key_details(email, session)
|
||||
@@ -150,4 +146,4 @@ async def key_cluster_mode(
|
||||
)
|
||||
|
||||
if state:
|
||||
await state.clear()
|
||||
await state.clear()
|
||||
|
||||
@@ -17,30 +17,21 @@ from config import (
|
||||
ADMIN_USERNAME,
|
||||
CONNECT_PHONE_BUTTON,
|
||||
PUBLIC_LINK,
|
||||
REMNAWAVE_LOGIN,
|
||||
REMNAWAVE_PASSWORD,
|
||||
RENEWAL_PRICES,
|
||||
SUPPORT_CHAT_URL,
|
||||
REMNAWAVE_LOGIN,
|
||||
REMNAWAVE_PASSWORD
|
||||
)
|
||||
from database import (
|
||||
add_connection,
|
||||
check_connection_exists,
|
||||
check_server_name_by_cluster,
|
||||
get_key_details,
|
||||
get_trial,
|
||||
update_balance,
|
||||
update_trial,
|
||||
check_server_name_by_cluster,
|
||||
)
|
||||
from handlers.buttons import (
|
||||
BACK,
|
||||
CONNECT_DEVICE,
|
||||
CONNECT_PHONE,
|
||||
MAIN_MENU,
|
||||
PC_BUTTON,
|
||||
SUPPORT,
|
||||
TV_BUTTON,
|
||||
SUPPORT
|
||||
)
|
||||
from handlers.buttons import BACK, CONNECT_DEVICE, CONNECT_PHONE, MAIN_MENU, PC_BUTTON, SUPPORT, TV_BUTTON
|
||||
from handlers.keys.key_utils import create_client_on_server
|
||||
from handlers.texts import (
|
||||
SELECT_COUNTRY_MSG,
|
||||
@@ -48,8 +39,8 @@ from handlers.texts import (
|
||||
)
|
||||
from handlers.utils import edit_or_send_message, generate_random_email, get_least_loaded_cluster
|
||||
from logger import logger
|
||||
from panels.three_xui import delete_client
|
||||
from panels.remnawave import RemnawaveAPI
|
||||
from panels.three_xui import delete_client
|
||||
|
||||
|
||||
router = Router()
|
||||
@@ -73,7 +64,6 @@ async def key_country_mode(
|
||||
least_loaded_cluster,
|
||||
)
|
||||
|
||||
|
||||
if not servers:
|
||||
logger.error(f"Нет серверов в кластере {least_loaded_cluster}")
|
||||
error_message = "❌ Нет доступных серверов для создания ключа."
|
||||
@@ -88,7 +78,7 @@ async def key_country_mode(
|
||||
tasks = [asyncio.create_task(check_server_availability(server)) for server in servers]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
for server, result in zip(servers, results):
|
||||
for server, result in zip(servers, results, strict=False):
|
||||
if result is True:
|
||||
available_servers.append(server["server_name"])
|
||||
|
||||
@@ -173,7 +163,7 @@ async def change_location_callback(callback_query: CallbackQuery, session: Any):
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
for server, result in zip(servers, results):
|
||||
for server, result in zip(servers, results, strict=False):
|
||||
if result is True:
|
||||
available_servers.append(server["server_name"])
|
||||
|
||||
@@ -266,6 +256,8 @@ async def finalize_key_creation(
|
||||
email = key_name.lower()
|
||||
expiry_timestamp = int(expiry_time.timestamp() * 1000)
|
||||
|
||||
remna = None
|
||||
|
||||
try:
|
||||
server_info = await session.fetchrow(
|
||||
"SELECT api_url, inbound_id, server_name, panel_type FROM servers WHERE server_name = $1",
|
||||
@@ -280,31 +272,53 @@ async def finalize_key_creation(
|
||||
remnawave_link = None
|
||||
created_at = int(datetime.now(moscow_tz).timestamp() * 1000)
|
||||
|
||||
if old_key_name and panel_type == "3x-ui":
|
||||
if old_key_name:
|
||||
old_server_id = old_key_details.get("server_id")
|
||||
if old_server_id:
|
||||
old_server_info = await session.fetchrow(
|
||||
"SELECT api_url, inbound_id, server_name FROM servers WHERE server_name = $1",
|
||||
"SELECT api_url, inbound_id, server_name, panel_type FROM servers WHERE server_name = $1",
|
||||
old_server_id,
|
||||
)
|
||||
if old_server_info:
|
||||
xui = AsyncApi(
|
||||
old_server_info["api_url"],
|
||||
username=ADMIN_USERNAME,
|
||||
password=ADMIN_PASSWORD,
|
||||
logger=logger,
|
||||
)
|
||||
await delete_client(
|
||||
xui,
|
||||
old_server_info["inbound_id"],
|
||||
email,
|
||||
client_id,
|
||||
)
|
||||
old_panel_type = old_server_info["panel_type"].lower()
|
||||
try:
|
||||
if old_panel_type == "3x-ui":
|
||||
xui = AsyncApi(
|
||||
old_server_info["api_url"],
|
||||
username=ADMIN_USERNAME,
|
||||
password=ADMIN_PASSWORD,
|
||||
logger=logger,
|
||||
)
|
||||
await delete_client(
|
||||
xui,
|
||||
old_server_info["inbound_id"],
|
||||
email,
|
||||
client_id,
|
||||
)
|
||||
await session.execute(
|
||||
"UPDATE keys SET key = NULL WHERE tg_id = $1 AND email = $2",
|
||||
tg_id,
|
||||
email,
|
||||
)
|
||||
logger.info(f"[Delete] Удалён клиент {email} с 3x-ui сервера {old_server_id}")
|
||||
elif old_panel_type == "remnawave":
|
||||
remna = RemnawaveAPI(old_server_info["api_url"])
|
||||
if await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
|
||||
await remna.delete_user(client_id)
|
||||
await session.execute(
|
||||
"UPDATE keys SET remnawave_link = NULL WHERE tg_id = $1 AND email = $2",
|
||||
tg_id,
|
||||
email,
|
||||
)
|
||||
logger.info(f"[Delete] Удалён клиент {client_id} с Remnawave сервера {old_server_id}")
|
||||
else:
|
||||
logger.warning(f"[Delete] Не удалось авторизоваться в Remnawave ({old_server_id})")
|
||||
except Exception as e:
|
||||
logger.warning(f"[Delete] Ошибка при удалении клиента с сервера {old_server_id}: {e}")
|
||||
|
||||
if panel_type == "remnawave":
|
||||
remna = RemnawaveAPI(server_info["api_url"])
|
||||
logged_in = await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD)
|
||||
if not logged_in:
|
||||
if not await remna.login(REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD):
|
||||
raise ValueError(f"❌ Не удалось авторизоваться в Remnawave ({selected_country})")
|
||||
|
||||
expire_at = datetime.utcfromtimestamp(expiry_timestamp / 1000).isoformat() + "Z"
|
||||
@@ -323,6 +337,14 @@ async def finalize_key_creation(
|
||||
remnawave_link = result.get("subscriptionUrl")
|
||||
logger.info(f"[Key Creation] Remnawave пользователь создан: {result}")
|
||||
|
||||
if old_key_name:
|
||||
await session.execute(
|
||||
"UPDATE keys SET client_id = $1 WHERE tg_id = $2 AND email = $3",
|
||||
client_id,
|
||||
tg_id,
|
||||
email,
|
||||
)
|
||||
|
||||
if panel_type == "3x-ui":
|
||||
semaphore = asyncio.Semaphore(2)
|
||||
await create_client_on_server(
|
||||
@@ -344,6 +366,21 @@ async def finalize_key_creation(
|
||||
tg_id,
|
||||
old_key_name,
|
||||
)
|
||||
if panel_type == "3x-ui":
|
||||
await session.execute(
|
||||
"UPDATE keys SET key = $1 WHERE tg_id = $2 AND email = $3",
|
||||
public_link,
|
||||
tg_id,
|
||||
email,
|
||||
)
|
||||
elif panel_type == "remnawave":
|
||||
await session.execute(
|
||||
"UPDATE keys SET remnawave_link = $1 WHERE tg_id = $2 AND email = $3",
|
||||
remnawave_link,
|
||||
tg_id,
|
||||
email,
|
||||
)
|
||||
|
||||
else:
|
||||
await session.execute(
|
||||
"""
|
||||
@@ -428,10 +465,9 @@ async def check_server_availability(server_info: dict) -> bool:
|
||||
logger.info(f"[Ping] 3x-ui сервер {server_name} доступен.")
|
||||
return True
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
except TimeoutError:
|
||||
logger.warning(f"[Ping] Сервер {server_name} не ответил вовремя.")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning(f"[Ping] Ошибка при проверке сервера {server_name}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
from .key_cluster_mode import key_cluster_mode
|
||||
from .key_country_mode import key_country_mode
|
||||
|
||||
from config import (
|
||||
NOTIFY_EXTRA_DAYS,
|
||||
@@ -40,6 +38,9 @@ from handlers.texts import (
|
||||
from handlers.utils import edit_or_send_message
|
||||
from logger import logger
|
||||
|
||||
from .key_cluster_mode import key_cluster_mode
|
||||
from .key_country_mode import key_country_mode
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
@@ -199,4 +200,4 @@ async def create_key(
|
||||
session=session,
|
||||
message_or_query=message_or_query,
|
||||
plan=plan,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Any
|
||||
import asyncpg
|
||||
|
||||
from aiogram import F, Router
|
||||
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
|
||||
+35
-14
@@ -1,13 +1,24 @@
|
||||
import asyncio
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
|
||||
from py3xui import AsyncApi
|
||||
|
||||
from config import ADMIN_PASSWORD, ADMIN_USERNAME, DATABASE_URL, LIMIT_IP, PUBLIC_LINK, SUPERNODE, TOTAL_GB, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD
|
||||
from database import get_servers, store_key, delete_notification
|
||||
from config import (
|
||||
ADMIN_PASSWORD,
|
||||
ADMIN_USERNAME,
|
||||
DATABASE_URL,
|
||||
LIMIT_IP,
|
||||
PUBLIC_LINK,
|
||||
REMNAWAVE_LOGIN,
|
||||
REMNAWAVE_PASSWORD,
|
||||
SUPERNODE,
|
||||
TOTAL_GB,
|
||||
)
|
||||
from database import delete_notification, get_servers, store_key
|
||||
from handlers.utils import get_least_loaded_cluster
|
||||
from logger import logger
|
||||
from panels.remnawave import RemnawaveAPI
|
||||
@@ -20,8 +31,6 @@ from panels.three_xui import (
|
||||
toggle_client,
|
||||
)
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
async def create_key_on_cluster(
|
||||
cluster_id: str,
|
||||
@@ -31,6 +40,7 @@ async def create_key_on_cluster(
|
||||
expiry_timestamp: int,
|
||||
plan: int = None,
|
||||
session=None,
|
||||
remnawave_link: str = None,
|
||||
):
|
||||
try:
|
||||
servers = await get_servers()
|
||||
@@ -71,6 +81,9 @@ async def create_key_on_cluster(
|
||||
logger.warning("Нет inbound_id у серверов Remnawave")
|
||||
else:
|
||||
traffic_limit_bytes = int((plan or 1) * TOTAL_GB * 1024**3)
|
||||
short_uuid = None
|
||||
if remnawave_link and "/" in remnawave_link:
|
||||
short_uuid = remnawave_link.rstrip("/").split("/")[-1]
|
||||
|
||||
user_data = {
|
||||
"username": email,
|
||||
@@ -81,6 +94,9 @@ async def create_key_on_cluster(
|
||||
"activeUserInbounds": inbound_ids,
|
||||
}
|
||||
|
||||
if short_uuid:
|
||||
user_data["shortUuid"] = short_uuid
|
||||
|
||||
result = await remna.create_user(user_data)
|
||||
if not result:
|
||||
logger.error("Ошибка при создании пользователя в Remnawave")
|
||||
@@ -243,8 +259,13 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
|
||||
|
||||
if remnawave_inbound_ids:
|
||||
remnawave_server = next(
|
||||
(srv for srv in cluster if srv.get("panel_type", "").lower() == "remnawave" and srv.get("inbound_id") in remnawave_inbound_ids),
|
||||
None
|
||||
(
|
||||
srv
|
||||
for srv in cluster
|
||||
if srv.get("panel_type", "").lower() == "remnawave"
|
||||
and srv.get("inbound_id") in remnawave_inbound_ids
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not remnawave_server:
|
||||
@@ -258,7 +279,7 @@ async def renew_key_in_cluster(cluster_id, email, client_id, new_expiry_time, to
|
||||
uuid=client_id,
|
||||
expire_at=expire_iso,
|
||||
active_user_inbounds=remnawave_inbound_ids,
|
||||
traffic_limit_bytes=total_gb
|
||||
traffic_limit_bytes=total_gb,
|
||||
)
|
||||
if updated:
|
||||
logger.info(f"Подписка Remnawave {client_id} успешно продлена")
|
||||
@@ -426,9 +447,9 @@ async def update_key_on_cluster(tg_id, client_id, email, expiry_time, cluster_id
|
||||
remnawave_key = result.get("subscriptionUrl")
|
||||
logger.info(f"[Update] Remnawave: клиент заново создан, новый UUID: {remnawave_client_id}")
|
||||
else:
|
||||
logger.error(f"[Update] Ошибка создания Remnawave клиента")
|
||||
logger.error("[Update] Ошибка создания Remnawave клиента")
|
||||
else:
|
||||
logger.error(f"[Update] Не удалось авторизоваться в Remnawave")
|
||||
logger.error("[Update] Не удалось авторизоваться в Remnawave")
|
||||
|
||||
if not remnawave_client_id:
|
||||
logger.warning(f"[Update] Remnawave client_id не получен. Используется исходный: {client_id}")
|
||||
@@ -509,9 +530,7 @@ async def update_subscription(tg_id: int, email: str, session: Any, cluster_over
|
||||
)
|
||||
new_cluster_id = cluster_override or await get_least_loaded_cluster()
|
||||
|
||||
new_client_id, remnawave_key = await update_key_on_cluster(
|
||||
tg_id, client_id, email, expiry_time, new_cluster_id
|
||||
)
|
||||
new_client_id, remnawave_key = await update_key_on_cluster(tg_id, client_id, email, expiry_time, new_cluster_id)
|
||||
|
||||
servers = await get_servers()
|
||||
cluster_servers = servers.get(new_cluster_id, [])
|
||||
@@ -608,7 +627,9 @@ async def get_user_traffic(session: Any, tg_id: int, email: str) -> dict[str, An
|
||||
client_id = row["client_id"]
|
||||
server_id = row["server_id"]
|
||||
|
||||
matched_servers = [s for s in servers_map.values() if s["server_name"] == server_id or s["cluster_name"] == server_id]
|
||||
matched_servers = [
|
||||
s for s in servers_map.values() if s["server_name"] == server_id or s["cluster_name"] == server_id
|
||||
]
|
||||
for server_info in matched_servers:
|
||||
tasks.append(fetch_traffic(server_info, client_id))
|
||||
|
||||
@@ -773,4 +794,4 @@ async def reset_traffic_in_cluster(cluster_id: str, email: str) -> None:
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Reset Traffic] Ошибка при сбросе трафика клиента {email} в кластере {cluster_id}: {e}")
|
||||
raise
|
||||
raise
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from aiogram import F, Router
|
||||
from aiogram.types import CallbackQuery, Message, InlineKeyboardButton, WebAppInfo
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
import pytz
|
||||
import html
|
||||
import os
|
||||
import re
|
||||
|
||||
from datetime import datetime
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from typing import Any
|
||||
|
||||
import pytz
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.fsm.state import State, StatesGroup
|
||||
from aiogram.types import CallbackQuery, InlineKeyboardButton, Message, WebAppInfo
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from config import (
|
||||
CONNECT_PHONE_BUTTON,
|
||||
@@ -52,6 +54,7 @@ from logger import logger
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class RenameKeyState(StatesGroup):
|
||||
waiting_for_new_alias = State()
|
||||
|
||||
@@ -129,7 +132,7 @@ async def handle_rename_key(callback: CallbackQuery, state: FSMContext):
|
||||
await edit_or_send_message(
|
||||
target_message=callback.message,
|
||||
text="✏️ Введите новое имя подписки (до 10 символов):",
|
||||
reply_markup=builder.as_markup()
|
||||
reply_markup=builder.as_markup(),
|
||||
)
|
||||
|
||||
|
||||
@@ -142,7 +145,9 @@ async def handle_new_alias_input(message: Message, state: FSMContext, session: A
|
||||
return
|
||||
|
||||
if not alias or not re.match(r"^[a-zA-Zа-яА-ЯёЁ0-9@._-]+$", alias):
|
||||
await message.answer("❌ Введены недопустимые символы или имя пустое. Используйте только буквы, цифры и @._-\nПовторите ввод.")
|
||||
await message.answer(
|
||||
"❌ Введены недопустимые символы или имя пустое. Используйте только буквы, цифры и @._-\nПовторите ввод."
|
||||
)
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
@@ -303,4 +308,4 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
|
||||
tg_id,
|
||||
callback_query,
|
||||
f"Ошибка при получении информации о ключе: {e}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Any
|
||||
|
||||
from aiogram import F, Router, types
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from aiogram.types import CallbackQuery
|
||||
|
||||
from database import (
|
||||
@@ -17,11 +16,11 @@ from handlers.buttons import (
|
||||
BACK,
|
||||
CANCEL,
|
||||
)
|
||||
from handlers.keys.key_view import process_callback_view_key
|
||||
from handlers.keys.key_utils import (
|
||||
delete_key_from_cluster,
|
||||
update_subscription,
|
||||
)
|
||||
from handlers.keys.key_view import process_callback_view_key
|
||||
from handlers.texts import (
|
||||
DELETE_KEY_CONFIRM_MSG,
|
||||
KEY_DELETED_MSG_SIMPLE,
|
||||
|
||||
@@ -335,8 +335,8 @@ async def handle_subscription(request: web.Request, old_subscription: bool = Fal
|
||||
time_left = format_time_left(expiry_time_ms)
|
||||
|
||||
urls = await get_subscription_urls(
|
||||
server_id, email, conn, include_remnawave_key=client_data.get("remnawave_link")
|
||||
)
|
||||
server_id, email, conn, include_remnawave_key=client_data.get("remnawave_link")
|
||||
)
|
||||
|
||||
if not urls:
|
||||
return web.Response(text="❌ Сервер не найден.", status=404)
|
||||
|
||||
@@ -24,12 +24,12 @@ from database import (
|
||||
add_notification,
|
||||
check_notification_time,
|
||||
delete_key,
|
||||
delete_notification,
|
||||
get_all_keys,
|
||||
get_balance,
|
||||
get_last_notification_time,
|
||||
update_balance,
|
||||
update_key_expiry,
|
||||
delete_notification
|
||||
)
|
||||
from handlers.keys.key_utils import delete_key_from_cluster, renew_key_in_cluster
|
||||
from handlers.notifications.notify_kb import build_notification_expired_kb, build_notification_kb
|
||||
@@ -329,9 +329,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
keyboard,
|
||||
)
|
||||
await add_notification(tg_id, notification_id, session=conn)
|
||||
logger.info(
|
||||
f"Отправлено уведомление о необходимости продления подписки {email} пользователю {tg_id}."
|
||||
)
|
||||
logger.info(f"Отправлено уведомление о необходимости продления подписки {email} пользователю {tg_id}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось отправить уведомление о продлении подписки пользователю {tg_id}: {e}")
|
||||
|
||||
@@ -340,13 +338,7 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
|
||||
|
||||
|
||||
async def process_auto_renew_or_notify(
|
||||
bot,
|
||||
conn,
|
||||
key: dict,
|
||||
notification_id: str,
|
||||
renewal_period_months: int,
|
||||
standard_photo: str,
|
||||
standard_caption: str
|
||||
bot, conn, key: dict, notification_id: str, renewal_period_months: int, standard_photo: str, standard_caption: str
|
||||
):
|
||||
"""
|
||||
Если баланс пользователя позволяет, продлевает ключ на максимальный возможный срок и списывает средства;
|
||||
|
||||
+10
-10
@@ -1,12 +1,12 @@
|
||||
import html
|
||||
from io import BytesIO
|
||||
import os
|
||||
|
||||
from io import BytesIO
|
||||
from typing import Any
|
||||
|
||||
import asyncpg
|
||||
import qrcode
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.context import FSMContext
|
||||
@@ -50,7 +50,7 @@ from handlers.buttons import (
|
||||
PAYMENT,
|
||||
QR,
|
||||
TOP_FIVE,
|
||||
TRIAL_SUB
|
||||
TRIAL_SUB,
|
||||
)
|
||||
from handlers.texts import BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT, INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT
|
||||
from logger import logger
|
||||
@@ -308,16 +308,16 @@ async def show_referral_qr(callback_query: CallbackQuery):
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при генерации QR-кода для реферальной ссылки: {e}", exc_info=True)
|
||||
await callback_query.message.answer("❌ Произошла ошибка при создании QR-кода.")
|
||||
|
||||
|
||||
|
||||
@router.callback_query(F.data == "top_referrals")
|
||||
async def top_referrals_handler(callback_query: CallbackQuery):
|
||||
conn = await asyncpg.connect(DATABASE_URL)
|
||||
try:
|
||||
user_referral_count = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1",
|
||||
callback_query.from_user.id
|
||||
) or 0
|
||||
user_referral_count = (
|
||||
await conn.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", callback_query.from_user.id)
|
||||
or 0
|
||||
)
|
||||
|
||||
personal_block = "Твоё место в рейтинге:\n"
|
||||
if user_referral_count > 0:
|
||||
@@ -330,7 +330,7 @@ async def top_referrals_handler(callback_query: CallbackQuery):
|
||||
HAVING COUNT(*) > $1
|
||||
) AS better_users
|
||||
""",
|
||||
user_referral_count
|
||||
user_referral_count,
|
||||
)
|
||||
personal_block += f"{user_position}. {callback_query.from_user.id} - {user_referral_count} чел."
|
||||
else:
|
||||
|
||||
+13
-12
@@ -29,10 +29,10 @@ from database import (
|
||||
check_connection_exists,
|
||||
get_referral_by_referred_id,
|
||||
get_trial,
|
||||
update_balance,
|
||||
)
|
||||
from handlers.buttons import ABOUT_VPN, BACK, CHANNEL, MAIN_MENU, SUPPORT
|
||||
from handlers.buttons import ABOUT_VPN, BACK, CHANNEL, MAIN_MENU, SUPPORT, TRIAL_SUB
|
||||
from handlers.captcha import generate_captcha
|
||||
from handlers.coupons import activate_coupon
|
||||
from handlers.keys.key_mode.key_create import create_key
|
||||
from handlers.profile import process_callback_view_profile
|
||||
from handlers.texts import (
|
||||
@@ -47,7 +47,6 @@ from handlers.texts import (
|
||||
get_about_vpn,
|
||||
)
|
||||
from logger import logger
|
||||
from handlers.coupons import activate_coupon
|
||||
|
||||
from .admin.panel.keyboard import AdminPanelCallback
|
||||
from .utils import edit_or_send_message
|
||||
@@ -69,13 +68,15 @@ async def start_command(message: Message, state: FSMContext, session: Any, admin
|
||||
logger.info(f"Вызвана функция start_command для пользователя {message.chat.id}")
|
||||
|
||||
if CAPTCHA_ENABLE and captcha:
|
||||
captcha_data = await generate_captcha(message, state)
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text=captcha_data["text"],
|
||||
reply_markup=captcha_data["markup"],
|
||||
)
|
||||
return
|
||||
connection_exists = await check_connection_exists(message.chat.id)
|
||||
if not connection_exists:
|
||||
captcha_data = await generate_captcha(message, state)
|
||||
await edit_or_send_message(
|
||||
target_message=message,
|
||||
text=captcha_data["text"],
|
||||
reply_markup=captcha_data["markup"],
|
||||
)
|
||||
return
|
||||
|
||||
state_data = await state.get_data()
|
||||
text_to_process = state_data.get("original_text", message.text)
|
||||
@@ -269,7 +270,7 @@ async def show_start_menu(message: Message, admin: bool, session: Any):
|
||||
trial_status = await get_trial(message.chat.id, session)
|
||||
logger.info(f"Trial status для {message.chat.id}: {trial_status}")
|
||||
if trial_status == 0:
|
||||
builder.row(InlineKeyboardButton(text="🎁 Пробная подписка", callback_data="create_key"))
|
||||
builder.row(InlineKeyboardButton(text=TRIAL_SUB, callback_data="create_key"))
|
||||
else:
|
||||
logger.warning(f"Сессия базы данных отсутствует, пропускаем проверку триала для {message.chat.id}")
|
||||
|
||||
@@ -321,4 +322,4 @@ async def handle_about_vpn(callback_query: CallbackQuery):
|
||||
reply_markup=builder.as_markup(),
|
||||
media_path=image_path,
|
||||
force_text=False,
|
||||
)
|
||||
)
|
||||
|
||||
+3
-2
@@ -158,13 +158,14 @@ def get_plural_form(num: int, form1: str, form2: str, form3: str) -> str:
|
||||
return form3
|
||||
return {1: form1, 2: form2, 3: form2, 4: form2}.get(n % 10, form3)
|
||||
|
||||
|
||||
def format_days(days: int) -> str:
|
||||
"""
|
||||
Форматирует количество дней с правильным склонением.
|
||||
|
||||
|
||||
Args:
|
||||
days (int): Количество дней.
|
||||
|
||||
|
||||
Returns:
|
||||
str: Строка с числом и склонённым словом "день/дня/дней".
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user