remnawave in country_mode/save shortid/bug fixes

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