diff --git a/cli_launcher.py b/cli_launcher.py
index a2312c87..a4eff79a 100644
--- a/cli_launcher.py
+++ b/cli_launcher.py
@@ -2,13 +2,17 @@ import os
import re
import subprocess
import sys
+
import requests
+
from rich.console import Console
from rich.prompt import Confirm, Prompt
from rich.table import Table
from rich.text import Text
+
from config import BOT_SERVICE
+
try:
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
@@ -27,10 +31,12 @@ SERVICE_NAME = BOT_SERVICE
console = Console()
+
def is_service_exists(service_name):
result = subprocess.run(["systemctl", "list-unit-files", service_name], capture_output=True, text=True)
return service_name in result.stdout
+
def print_logo():
logo = Text(
"""
@@ -45,6 +51,7 @@ def print_logo():
)
console.print(logo)
+
def backup_project():
console.print("[yellow]📦 Создаётся резервная копия проекта...[/yellow]")
with console.status("[bold cyan]Копирование файлов...[/bold cyan]"):
@@ -52,11 +59,13 @@ def backup_project():
subprocess.run(["cp", "-r", PROJECT_DIR, BACK_DIR])
console.print(f"[green]✅ Бэкап сохранён в: {BACK_DIR}[/green]")
+
def install_rsync_if_needed():
if subprocess.run(["which", "rsync"], capture_output=True).returncode != 0:
console.print("[blue]📦 Установка rsync...[/blue]")
os.system("sudo apt update && sudo apt install -y rsync")
+
def clean_project_dir_safe():
console.print("[yellow]🧹 Очистка проекта перед обновлением (кроме config и кнопок)...[/yellow]")
preserved_paths = {
@@ -92,6 +101,7 @@ def install_git_if_needed():
console.print("[blue]Установка Git...[/blue]")
os.system("sudo apt update && sudo apt install -y git")
+
def install_dependencies():
console.print("[blue]🔧 Установка зависимостей...[/blue]")
with console.status("[bold green]Устанавливаются зависимости...[/bold green]"):
@@ -106,6 +116,7 @@ def install_dependencies():
except subprocess.CalledProcessError:
console.print("[red]❌ Ошибка при установке зависимостей.[/red]")
+
def restart_service():
if is_service_exists(SERVICE_NAME):
console.print("[blue]🚀 Перезапуск службы...[/blue]")
@@ -114,6 +125,7 @@ def restart_service():
else:
console.print(f"[red]❌ Служба {SERVICE_NAME} не найдена.[/red]")
+
def get_local_version():
path = os.path.join(PROJECT_DIR, "bot.py")
if not os.path.isfile(path):
@@ -125,6 +137,7 @@ def get_local_version():
return match.group(1)
return None
+
def get_remote_version(branch="main"):
try:
url = f"https://raw.githubusercontent.com/Vladless/Solo_bot/{branch}/bot.py"
@@ -138,6 +151,7 @@ def get_remote_version(branch="main"):
return None
return None
+
def update_from_beta():
local_version = get_local_version()
remote_version = get_remote_version(branch="dev")
@@ -173,6 +187,7 @@ def update_from_beta():
restart_service()
console.print("[green]✅ Обновление с ветки dev завершено.[/green]")
+
def update_from_release():
if not Confirm.ask("[yellow]🔁 Подтвердите обновление Solobot до одного из последних релизов[/yellow]"):
return
@@ -194,8 +209,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]
@@ -236,8 +250,9 @@ def show_update_menu():
elif choice == "2":
update_from_release()
+
def show_menu():
- table = Table(title=f"Solobot CLI v0.1.4", title_style="bold magenta", header_style="bold blue")
+ table = Table(title="Solobot CLI v0.1.4", title_style="bold magenta", header_style="bold blue")
table.add_column("№", justify="center", style="cyan", no_wrap=True)
table.add_column("Операция", style="white")
table.add_row("1", "Запустить бота (systemd)")
@@ -250,6 +265,7 @@ def show_menu():
table.add_row("8", "Выход")
console.print(table)
+
def main():
os.chdir(PROJECT_DIR)
print_logo()
@@ -295,5 +311,6 @@ def main():
except KeyboardInterrupt:
console.print("\n[bold red]⏹ Прерывание. Выход из CLI.[/bold red]")
+
if __name__ == "__main__":
main()
diff --git a/database.py b/database.py
index 9c0531dc..ca9cb90d 100644
--- a/database.py
+++ b/database.py
@@ -244,7 +244,13 @@ async def add_user(
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (tg_id) DO NOTHING
""",
- tg_id, username, first_name, last_name, language_code, is_bot, source_code
+ tg_id,
+ username,
+ first_name,
+ last_name,
+ language_code,
+ is_bot,
+ source_code,
)
logger.info(f"[DB] Новый пользователь добавлен: {tg_id} (source: {source_code})")
except Exception as e:
@@ -1361,7 +1367,9 @@ async def get_all_keys(session: Any = None):
await conn.close()
-async def check_notifications_bulk(notification_type: str, hours: int, session: Any, tg_ids: list[int] = None, emails: list[str] = None) -> list[dict]:
+async def check_notifications_bulk(
+ notification_type: str, hours: int, session: Any, tg_ids: list[int] = None, emails: list[str] = None
+) -> list[dict]:
"""
Проверяет, какие пользователи могут получить уведомление указанного типа, и возвращает их данные.
"""
@@ -1380,25 +1388,25 @@ async def check_notifications_bulk(notification_type: str, hours: int, session:
WHERE (n.last_notification_time IS NULL OR NOW() - n.last_notification_time > ($2 * INTERVAL '1 hour'))
"""
params = [notification_type, hours]
-
+
if tg_ids is not None:
query += " AND u.tg_id = ANY($3)"
params.append(tg_ids)
if emails is not None:
query += " AND k.email = ANY($" + str(len(params) + 1) + ")"
params.append(emails)
-
- if notification_type == 'inactive_trial':
+
+ if notification_type == "inactive_trial":
query += """
AND u.trial IN (0, -1)
AND u.tg_id NOT IN (SELECT tg_id FROM blocked_users)
AND u.tg_id NOT IN (SELECT DISTINCT tg_id FROM keys)
"""
-
+
query += """
GROUP BY u.tg_id, k.email, u.username, u.first_name, u.last_name
"""
-
+
users = await session.fetch(query, *params)
logger.info(f"Найдено {len(users)} пользователей, готовых к уведомлению типа {notification_type}")
return [
@@ -1408,7 +1416,9 @@ async def check_notifications_bulk(notification_type: str, hours: int, session:
"username": user["username"],
"first_name": user["first_name"],
"last_name": user["last_name"],
- "last_notification_time": int(user["last_notification_time"]) if user["last_notification_time"] else None,
+ "last_notification_time": int(user["last_notification_time"])
+ if user["last_notification_time"]
+ else None,
}
for user in users
]
@@ -1423,7 +1433,10 @@ async def create_tracking_source(name: str, code: str, type_: str, created_by: i
INSERT INTO tracking_sources (name, code, type, created_by)
VALUES ($1, $2, $3, $4)
""",
- name, code, type_, created_by
+ name,
+ code,
+ type_,
+ created_by,
)
@@ -1446,7 +1459,8 @@ async def get_all_tracking_sources(session) -> list[dict]:
async def get_tracking_source_stats(code: str, session) -> dict:
- result = await session.fetchrow("""
+ result = await session.fetchrow(
+ """
SELECT
ts.name,
ts.code,
@@ -1463,5 +1477,7 @@ async def get_tracking_source_stats(code: str, session) -> dict:
LEFT JOIN payments p ON p.tg_id = u.tg_id
WHERE ts.code = $1
GROUP BY ts.code, ts.name, ts.created_at
- """, code)
+ """,
+ code,
+ )
return dict(result) if result else {}
diff --git a/handlers/__init__.py b/handlers/__init__.py
index b2519c9d..de7b59b3 100644
--- a/handlers/__init__.py
+++ b/handlers/__init__.py
@@ -12,8 +12,8 @@ from .notifications import router as notifications_router
from .pay import router as pay_router
from .payments import router as payments_router
from .profile import router as profile_router
-from .start import router as start_router
from .refferal import router as refferal_router
+from .start import router as start_router
router = Router(name="handlers_main_router")
@@ -30,5 +30,5 @@ router.include_routers(
keys_router,
instructions_router,
admin_router,
- refferal_router
+ refferal_router,
)
diff --git a/handlers/admin/__init__.py b/handlers/admin/__init__.py
index caff535a..94bf8ce5 100644
--- a/handlers/admin/__init__.py
+++ b/handlers/admin/__init__.py
@@ -2,6 +2,7 @@ __all__ = ("router",)
from aiogram import Router
+from .ads import router as ads_router
from .backups import router as backups_router
from .bans import router as bans_router
from .clusters import router as clusters_router
@@ -13,7 +14,6 @@ from .sender import router as sender_router
from .servers import router as servers_router
from .stats import router as stats_router
from .users import router as users_router
-from .ads import router as ads_router
router = Router(name="admins_main_router")
@@ -30,5 +30,5 @@ router.include_routers(
coupons_router,
restart_router,
bans_router,
- ads_router
+ ads_router,
)
diff --git a/handlers/admin/ads/ads_handler.py b/handlers/admin/ads/ads_handler.py
index 601b58bd..5c109364 100644
--- a/handlers/admin/ads/ads_handler.py
+++ b/handlers/admin/ads/ads_handler.py
@@ -16,13 +16,14 @@ from logger import logger
from ..panel.keyboard import AdminPanelCallback
from .keyboard import (
AdminAdsCallback,
+ build_ads_delete_confirm_kb,
build_ads_kb,
build_ads_list_kb,
build_ads_stats_kb,
- build_ads_delete_confirm_kb,
- build_cancel_input_kb
+ build_cancel_input_kb,
)
+
router = Router()
@@ -33,33 +34,27 @@ class AdminAdsState(StatesGroup):
@router.callback_query(AdminPanelCallback.filter(F.action == "ads"), IsAdminFilter())
async def handle_ads_menu(callback_query: CallbackQuery):
- await callback_query.message.edit_text(
- text="📊 Аналитика рекламы:", reply_markup=build_ads_kb()
- )
+ await callback_query.message.edit_text(text="📊 Аналитика рекламы:", reply_markup=build_ads_kb())
@router.callback_query(AdminAdsCallback.filter(F.action == "create"), IsAdminFilter())
async def handle_ads_create(callback_query: CallbackQuery, state: FSMContext):
await state.set_state(AdminAdsState.waiting_for_new_name)
await callback_query.message.edit_text(
- "📝 Введите название новой ссылки:",
- reply_markup=build_cancel_input_kb()
+ "📝 Введите название новой ссылки:", reply_markup=build_cancel_input_kb()
)
-
@router.message(AdminAdsState.waiting_for_new_name, IsAdminFilter())
async def handle_ads_name_input(message: Message, state: FSMContext):
name = message.text.strip()
await state.update_data(name=name)
await state.set_state(AdminAdsState.waiting_for_new_code)
await message.answer(
- f"🔗 Введите код ссылки для: {name}.",
- reply_markup=build_cancel_input_kb()
+ f"🔗 Введите код ссылки для: {name}.", reply_markup=build_cancel_input_kb()
)
-
@router.message(AdminAdsState.waiting_for_new_code, IsAdminFilter())
async def handle_ads_code_input(message: Message, state: FSMContext, session):
code = message.text.strip()
@@ -69,11 +64,7 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session):
try:
await create_tracking_source(
- name=name,
- code=code_with_prefix,
- type_="utm",
- created_by=message.from_user.id,
- session=session
+ name=name, code=code_with_prefix, type_="utm", created_by=message.from_user.id, session=session
)
stats = await get_tracking_source_stats(code_with_prefix, session)
if not stats:
@@ -81,7 +72,7 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session):
return
msg = format_ads_stats(stats, USERNAME_BOT)
await message.answer(
- text=msg,
+ text=msg,
reply_markup=build_ads_stats_kb(code_with_prefix),
)
@@ -92,13 +83,14 @@ async def handle_ads_code_input(message: Message, state: FSMContext, session):
await state.clear()
-
@router.callback_query(AdminAdsCallback.filter(F.action == "list"), IsAdminFilter())
async def handle_ads_list(callback_query: CallbackQuery, session):
try:
ads = await get_all_tracking_sources(session)
reply_markup = build_ads_list_kb(ads, current_page=1, total_pages=1)
- await callback_query.message.edit_text("📋 Выберите ссылку для просмотра статистики:", reply_markup=reply_markup)
+ await callback_query.message.edit_text(
+ "📋 Выберите ссылку для просмотра статистики:", reply_markup=reply_markup
+ )
except Exception as e:
logger.error(f"Ошибка при получении списка UTM: {e}")
await callback_query.message.edit_text("Произошла ошибка при получении списка.")
@@ -113,7 +105,9 @@ async def handle_ads_view(callback_query: CallbackQuery, callback_data: AdminAds
await callback_query.message.edit_text("❌ Источник не найден или не содержит данных.")
return
msg = format_ads_stats(stats, USERNAME_BOT)
- await callback_query.message.edit_text(text=msg, reply_markup=build_ads_stats_kb(code), parse_mode=ParseMode.HTML)
+ await callback_query.message.edit_text(
+ text=msg, reply_markup=build_ads_stats_kb(code), parse_mode=ParseMode.HTML
+ )
except Exception as e:
logger.error(f"Ошибка при просмотре статистики: {e}")
await callback_query.message.edit_text("❌ Ошибка при получении статистики.")
@@ -132,15 +126,9 @@ async def handle_ads_delete_confirm(callback_query: CallbackQuery, callback_data
async def handle_ads_delete(callback_query: CallbackQuery, callback_data: AdminAdsCallback, session):
code = callback_data.code
try:
- await session.execute(
- "UPDATE users SET source_code = NULL WHERE source_code = $1",
- code
- )
+ await session.execute("UPDATE users SET source_code = NULL WHERE source_code = $1", code)
await session.execute("DELETE FROM tracking_sources WHERE code = $1", code)
- await callback_query.message.edit_text(
- f"🗑️ Ссылка {code} удалена.",
- reply_markup=build_ads_kb()
- )
+ await callback_query.message.edit_text(f"🗑️ Ссылка {code} удалена.", reply_markup=build_ads_kb())
except Exception as e:
logger.error(f"Ошибка при удалении метки {code}: {e}", exc_info=True)
await callback_query.message.edit_text("❌ Не удалось удалить ссылку.")
@@ -152,14 +140,11 @@ def format_ads_stats(stats: dict, username_bot: str) -> str:
f"📌 Название: {stats['name']}\n"
f"🔗 Ссылка: https://t.me/{username_bot}?start={stats['code']}\n"
f"🕓 Создана: {stats['created_at'].strftime('%d.%m.%Y %H:%M')}\n\n"
-
f"💡 Активность:\n"
f"└ 🆕 Регистраций: {stats.get('registrations', 0)}\n"
f"└ 🧪 Триалов: {stats.get('trials', 0)}\n"
-
f"\n💰 Финансовая информация:\n"
f"└ 💳 Покупок: {stats.get('payments', 0)}\n\n"
-
f"Просмотр статистики и управление рекламными ссылками."
)
@@ -167,7 +152,4 @@ def format_ads_stats(stats: dict, username_bot: str) -> str:
@router.callback_query(AdminAdsCallback.filter(F.action == "cancel_input"), IsAdminFilter())
async def handle_ads_cancel_input(callback_query: CallbackQuery, state: FSMContext):
await state.clear()
- await callback_query.message.edit_text(
- text="📊 Аналитика рекламы:",
- reply_markup=build_ads_kb()
- )
+ await callback_query.message.edit_text(text="📊 Аналитика рекламы:", reply_markup=build_ads_kb())
diff --git a/handlers/admin/ads/keyboard.py b/handlers/admin/ads/keyboard.py
index fb46a531..5faf0e32 100644
--- a/handlers/admin/ads/keyboard.py
+++ b/handlers/admin/ads/keyboard.py
@@ -1,6 +1,6 @@
-from aiogram.utils.keyboard import InlineKeyboardBuilder
-from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
from aiogram.filters.callback_data import CallbackData
+from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
+from aiogram.utils.keyboard import InlineKeyboardBuilder
from ..panel.keyboard import build_admin_back_btn
@@ -9,6 +9,7 @@ class AdminAdsCallback(CallbackData, prefix="admin_ads"):
action: str
code: str | None = None
+
def build_ads_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="➕ Новая ссылка", callback_data=AdminAdsCallback(action="create").pack())
@@ -60,20 +61,17 @@ def build_ads_delete_confirm_kb(code: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="✅ Да, удалить",
- callback_data=AdminAdsCallback(action="delete", code=code,).pack()
- )
- builder.button(
- text="❌ Отмена",
- callback_data=AdminAdsCallback(action="view", code=code).pack()
+ callback_data=AdminAdsCallback(
+ action="delete",
+ code=code,
+ ).pack(),
)
+ builder.button(text="❌ Отмена", callback_data=AdminAdsCallback(action="view", code=code).pack())
builder.adjust(1)
return builder.as_markup()
def build_cancel_input_kb() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
- builder.button(
- text="❌ Отмена",
- callback_data=AdminAdsCallback(action="cancel_input", code="none").pack()
- )
+ builder.button(text="❌ Отмена", callback_data=AdminAdsCallback(action="cancel_input", code="none").pack())
return builder.as_markup()
diff --git a/handlers/admin/clusters/clusters_handler.py b/handlers/admin/clusters/clusters_handler.py
index e16e224e..33b59d35 100644
--- a/handlers/admin/clusters/clusters_handler.py
+++ b/handlers/admin/clusters/clusters_handler.py
@@ -1,7 +1,6 @@
import asyncio
import time
-from datetime import datetime, timedelta
from typing import Any
import asyncpg
diff --git a/handlers/admin/clusters/keyboard.py b/handlers/admin/clusters/keyboard.py
index a36a0b66..9e087c04 100644
--- a/handlers/admin/clusters/keyboard.py
+++ b/handlers/admin/clusters/keyboard.py
@@ -4,7 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.buttons import BACK
-from ..panel.keyboard import build_admin_back_btn, AdminPanelCallback
+from ..panel.keyboard import AdminPanelCallback, build_admin_back_btn
from ..servers.keyboard import AdminServerCallback
@@ -96,12 +96,7 @@ def build_cluster_management_kb(cluster_name: str) -> InlineKeyboardMarkup:
callback_data=AdminClusterCallback(action="rename", data=cluster_name).pack(),
)
)
- builder.row(
- InlineKeyboardButton(
- text="🔙 Назад",
- callback_data=AdminPanelCallback(action="clusters").pack()
- )
- )
+ builder.row(InlineKeyboardButton(text="🔙 Назад", callback_data=AdminPanelCallback(action="clusters").pack()))
return builder.as_markup()
diff --git a/handlers/admin/panel/keyboard.py b/handlers/admin/panel/keyboard.py
index 709620be..a6813f35 100644
--- a/handlers/admin/panel/keyboard.py
+++ b/handlers/admin/panel/keyboard.py
@@ -26,7 +26,6 @@ def build_panel_kb() -> InlineKeyboardMarkup:
InlineKeyboardButton(text="🤖 Управление", callback_data=AdminPanelCallback(action="management").pack()),
)
builder.row(
-
InlineKeyboardButton(text="📢 Рассылка", callback_data=AdminPanelCallback(action="sender").pack()),
InlineKeyboardButton(text="🎟️ Купоны", callback_data=AdminPanelCallback(action="coupons").pack()),
)
diff --git a/handlers/admin/sender/keyboard.py b/handlers/admin/sender/keyboard.py
index 6e3c25ce..46438dcb 100644
--- a/handlers/admin/sender/keyboard.py
+++ b/handlers/admin/sender/keyboard.py
@@ -23,11 +23,7 @@ def build_sender_kb() -> InlineKeyboardMarkup:
text="📍 Не использовавшие триал", callback_data=AdminSenderCallback(type="untrial").pack()
)
)
- builder.row(
- InlineKeyboardButton(
- text="🔥 Горячие лиды", callback_data=AdminSenderCallback(type="hotleads").pack()
- )
- )
+ builder.row(InlineKeyboardButton(text="🔥 Горячие лиды", callback_data=AdminSenderCallback(type="hotleads").pack()))
builder.row(
InlineKeyboardButton(text="📢 Кластер", callback_data=AdminSenderCallback(type="cluster-select").pack())
)
diff --git a/handlers/admin/servers/keyboard.py b/handlers/admin/servers/keyboard.py
index 826f2c9b..6609c278 100644
--- a/handlers/admin/servers/keyboard.py
+++ b/handlers/admin/servers/keyboard.py
@@ -18,30 +18,19 @@ def build_manage_server_kb(server_name: str, cluster_name: str, enabled: bool) -
toggle_text = "🔴 Отключить" if enabled else "🟢 Включить"
toggle_action = "disable" if enabled else "enable"
- builder.button(
- text=toggle_text,
- callback_data=AdminServerCallback(action=toggle_action, data=server_name).pack()
- )
+ builder.button(text=toggle_text, callback_data=AdminServerCallback(action=toggle_action, data=server_name).pack())
builder.button(
- text="📈 Задать лимит",
- callback_data=AdminServerCallback(action="set_limit", data=server_name).pack()
+ text="📈 Задать лимит", callback_data=AdminServerCallback(action="set_limit", data=server_name).pack()
)
- 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()
+ 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)
- return builder.as_markup()
\ No newline at end of file
+ return builder.as_markup()
diff --git a/handlers/admin/servers/servers_handler.py b/handlers/admin/servers/servers_handler.py
index ec161bb3..69bdfd08 100644
--- a/handlers/admin/servers/servers_handler.py
+++ b/handlers/admin/servers/servers_handler.py
@@ -9,12 +9,14 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from database import get_servers
from filters.admin import IsAdminFilter
from handlers.buttons import BACK
+
from ..panel.keyboard import build_admin_back_kb
from .keyboard import (
AdminServerCallback,
build_manage_server_kb,
)
+
router = Router()
@@ -178,20 +180,14 @@ async def process_callback_delete_server(
)
-@router.callback_query(
- AdminServerCallback.filter(F.action.in_(["enable", "disable"])), IsAdminFilter()
-)
+@router.callback_query(AdminServerCallback.filter(F.action.in_(["enable", "disable"])), IsAdminFilter())
async def toggle_server_enabled(callback_query: CallbackQuery, callback_data: AdminServerCallback, session: Any):
server_name = callback_data.data
action = callback_data.action
new_status = action == "enable"
- await session.execute(
- "UPDATE servers SET enabled = $1 WHERE server_name = $2",
- new_status,
- server_name
- )
+ await session.execute("UPDATE servers SET enabled = $1 WHERE server_name = $2", new_status, server_name)
servers = await get_servers(include_enabled=True)
@@ -241,10 +237,7 @@ async def save_server_limit(message: types.Message, state: FSMContext, session:
server_name = data["server_name"]
new_value = limit if limit > 0 else None
- await session.execute(
- "UPDATE servers SET max_keys = $1 WHERE server_name = $2",
- new_value, server_name
- )
+ await session.execute("UPDATE servers SET max_keys = $1 WHERE server_name = $2", new_value, server_name)
servers = await get_servers(include_enabled=True)
cluster_name, server = next(
@@ -268,8 +261,7 @@ async def save_server_limit(message: types.Message, state: FSMContext, session:
)
await message.answer(
- text,
- reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=server.get("enabled", True))
+ text, reply_markup=build_manage_server_kb(server_name, cluster_name, enabled=server.get("enabled", True))
)
await state.clear()
diff --git a/handlers/admin/users/users_handler.py b/handlers/admin/users/users_handler.py
index 8a896127..83fabe76 100644
--- a/handlers/admin/users/users_handler.py
+++ b/handlers/admin/users/users_handler.py
@@ -1,6 +1,6 @@
import asyncio
-import uuid
import time
+import uuid
from datetime import datetime, timedelta, timezone
from typing import Any
@@ -20,12 +20,12 @@ from database import (
delete_user_data,
get_balance,
get_client_id_by_email,
+ get_key_details,
get_servers,
+ set_user_balance,
update_balance,
update_key_expiry,
update_trial,
- get_key_details,
- set_user_balance
)
from filters.admin import IsAdminFilter
from handlers.keys.key_utils import (
@@ -628,12 +628,8 @@ async def process_user_search(
created_at = user_data["created_at"].astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y")
updated_at = user_data["updated_at"].astimezone(MOSCOW_TZ).strftime("%H:%M:%S %d.%m.%Y")
- referral_count = await session.fetchval(
- "SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id
- )
- key_records = await session.fetch(
- "SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id
- )
+ referral_count = await session.fetchval("SELECT COUNT(*) FROM referrals WHERE referrer_tg_id = $1", tg_id)
+ key_records = await session.fetch("SELECT email, expiry_time FROM keys WHERE tg_id = $1", tg_id)
text = (
f"📊 Информация о пользователе"
diff --git a/handlers/captcha.py b/handlers/captcha.py
index 5df53f65..f1ed579b 100644
--- a/handlers/captcha.py
+++ b/handlers/captcha.py
@@ -8,7 +8,7 @@ from aiogram.fsm.context import FSMContext
from aiogram.types import CallbackQuery, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from handlers.texts import CAPTCHA_PROMPT_MSG, CAPTCHA_EMOJIS
+from handlers.texts import CAPTCHA_EMOJIS, CAPTCHA_PROMPT_MSG
from logger import logger
from .utils import edit_or_send_message
diff --git a/handlers/coupons.py b/handlers/coupons.py
index fbff35d4..5cc576a4 100644
--- a/handlers/coupons.py
+++ b/handlers/coupons.py
@@ -14,8 +14,8 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
from config import ADMIN_ID
from database import (
add_user,
- check_user_exists,
check_coupon_usage,
+ check_user_exists,
create_coupon_usage,
get_coupon_by_code,
get_keys,
diff --git a/handlers/keys/key_freeze.py b/handlers/keys/key_freeze.py
index c655dc4d..203875f6 100644
--- a/handlers/keys/key_freeze.py
+++ b/handlers/keys/key_freeze.py
@@ -98,7 +98,6 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac
added_days = max(leftover / (1000 * 86400), 0.01)
total_gb = int((added_days / 30) * TOTAL_GB * 1024**3)
-
await renew_key_in_cluster(
cluster_id=cluster_id,
email=email,
diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py
index 95f1ac6e..64a93b2a 100644
--- a/handlers/keys/key_mode/key_country_mode.py
+++ b/handlers/keys/key_mode/key_country_mode.py
@@ -24,8 +24,8 @@ from config import (
)
from database import (
add_user,
- check_user_exists,
check_server_name_by_cluster,
+ check_user_exists,
get_key_details,
get_trial,
update_balance,
@@ -247,7 +247,6 @@ async def finalize_key_creation(
)
logger.info(f"[User] Новый пользователь {tg_id} добавлен")
-
expiry_time = expiry_time.astimezone(moscow_tz)
if old_key_name:
@@ -459,7 +458,7 @@ async def check_server_availability(server_info: dict, session: Any) -> bool:
panel_type = server_info.get("panel_type", "3x-ui").lower()
enabled = server_info.get("enabled", True)
max_keys = server_info.get("max_keys")
-
+
if not enabled:
logger.info(f"[Ping] Сервер {server_name} выключен (enabled = FALSE).")
return False
@@ -494,4 +493,4 @@ async def check_server_availability(server_info: dict, session: Any) -> bool:
return False
except Exception as e:
logger.warning(f"[Ping] Ошибка при проверке сервера {server_name}: {e}")
- return False
\ No newline at end of file
+ return False
diff --git a/handlers/keys/key_mode/key_create.py b/handlers/keys/key_mode/key_create.py
index 90c9efe9..e84ea9c9 100644
--- a/handlers/keys/key_mode/key_create.py
+++ b/handlers/keys/key_mode/key_create.py
@@ -180,11 +180,7 @@ async def create_key(
Также отвечает за первичное подключение пользователя.
"""
if not await check_user_exists(tg_id):
- from_user = (
- message_or_query.from_user
- if isinstance(message_or_query, (CallbackQuery, Message))
- else None
- )
+ from_user = message_or_query.from_user if isinstance(message_or_query, CallbackQuery | Message) else None
if from_user:
await add_user(
tg_id=from_user.id,
diff --git a/handlers/keys/key_renew.py b/handlers/keys/key_renew.py
index 7e0663a1..6d7f8d4b 100644
--- a/handlers/keys/key_renew.py
+++ b/handlers/keys/key_renew.py
@@ -76,7 +76,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, session: Any
)
)
- builder.row(InlineKeyboardButton(text=BACK, callback_data="view_keys"))
+ builder.row(InlineKeyboardButton(text=BACK, callback_data=f"view_key|{record['email']}"))
balance = await get_balance(tg_id)
diff --git a/handlers/keys/key_utils.py b/handlers/keys/key_utils.py
index 86c5c458..4fb06dcd 100644
--- a/handlers/keys/key_utils.py
+++ b/handlers/keys/key_utils.py
@@ -5,6 +5,7 @@ from typing import Any
import asyncpg
+from bot import bot
from config import (
DATABASE_URL,
LIMIT_IP,
@@ -15,7 +16,7 @@ from config import (
TOTAL_GB,
)
from database import delete_notification, get_servers, store_key
-from handlers.utils import get_least_loaded_cluster, check_server_key_limit
+from handlers.utils import check_server_key_limit, get_least_loaded_cluster
from logger import logger
from panels.remnawave import RemnawaveAPI
from panels.three_xui import (
@@ -24,12 +25,10 @@ from panels.three_xui import (
delete_client,
extend_client_key,
get_client_traffic,
+ get_xui_instance,
toggle_client,
- get_xui_instance
)
-from bot import bot
-
async def create_key_on_cluster(
cluster_id: str,
@@ -66,14 +65,14 @@ async def create_key_on_cluster(
async with asyncpg.create_pool(DATABASE_URL) as pool:
async with pool.acquire() as conn:
remnawave_servers = [
- s for s in enabled_servers
- if s.get("panel_type", "3x-ui").lower() == "remnawave"
- and await check_server_key_limit(s, conn)
+ s
+ for s in enabled_servers
+ if s.get("panel_type", "3x-ui").lower() == "remnawave" and await check_server_key_limit(s, conn)
]
xui_servers = [
- s for s in enabled_servers
- if s.get("panel_type", "3x-ui").lower() == "3x-ui"
- and await check_server_key_limit(s, conn)
+ s
+ for s in enabled_servers
+ if s.get("panel_type", "3x-ui").lower() == "3x-ui" and await check_server_key_limit(s, conn)
]
if not remnawave_servers and not xui_servers:
@@ -789,4 +788,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
\ No newline at end of file
+ raise
diff --git a/handlers/keys/key_view.py b/handlers/keys/key_view.py
index a809c2ab..33751f28 100644
--- a/handlers/keys/key_view.py
+++ b/handlers/keys/key_view.py
@@ -17,7 +17,6 @@ from config import (
CONNECT_PHONE_BUTTON,
ENABLE_DELETE_KEY_BUTTON,
ENABLE_UPDATE_SUBSCRIPTION_BUTTON,
- PUBLIC_LINK,
QRCODE,
TOGGLE_CLIENT,
USE_COUNTRY_SELECTION,
@@ -48,7 +47,14 @@ from handlers.texts import (
NO_SUBSCRIPTIONS_MSG,
key_message,
)
-from handlers.utils import edit_or_send_message, handle_error, is_full_remnawave_cluster, format_days, format_hours, format_minutes
+from handlers.utils import (
+ edit_or_send_message,
+ format_days,
+ format_hours,
+ format_minutes,
+ handle_error,
+ is_full_remnawave_cluster,
+)
from logger import logger
@@ -221,7 +227,9 @@ async def process_callback_view_key(callback_query: CallbackQuery, session: Any)
days = total_seconds // 86400
hours = (total_seconds % 86400) // 3600
minutes = (total_seconds % 3600) // 60
- days_left_message = f"Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}"
+ days_left_message = (
+ f"Осталось: {format_days(days)}, {format_hours(hours)}, {format_minutes(minutes)}"
+ )
formatted_expiry_date = expiry_date.strftime("%d %B %Y года")
response_message = key_message(
diff --git a/handlers/keys/subscriptions.py b/handlers/keys/subscriptions.py
index 02dfba77..820a9023 100644
--- a/handlers/keys/subscriptions.py
+++ b/handlers/keys/subscriptions.py
@@ -135,7 +135,9 @@ def format_time_left(expiry_time_ms: int | None) -> str:
return f"{days}D,{hours}H ⏳" if days else f"{hours}H ⏳"
-def prepare_headers(user_agent: str, project_name: str, subscription_info: str, subscription_userinfo: str) -> dict[str, str]:
+def prepare_headers(
+ user_agent: str, project_name: str, subscription_info: str, subscription_userinfo: str
+) -> dict[str, str]:
if "Happ" in user_agent:
encoded_project_name = f"{project_name}"
announce_str = f"↖️Бот | {subscription_info} | Поддержка↗️"
@@ -160,7 +162,7 @@ def prepare_headers(user_agent: str, project_name: str, subscription_info: str,
}
elif "v2raytun" in user_agent:
encoded_project_name = f"{project_name}\n{subscription_info}"
- announce_str = f"🔑 Выберите сервер ⬇️ | 💬 Поддержка ➡️"
+ announce_str = "🔑 Выберите сервер ⬇️ | 💬 Поддержка ➡️"
return {
"Content-Type": "text/plain; charset=utf-8",
"Content-Disposition": "inline",
diff --git a/handlers/notifications/general_notifications.py b/handlers/notifications/general_notifications.py
index ba9fbdf9..87d72713 100644
--- a/handlers/notifications/general_notifications.py
+++ b/handlers/notifications/general_notifications.py
@@ -22,8 +22,8 @@ from config import (
)
from database import (
add_notification,
- check_notifications_bulk,
check_notification_time,
+ check_notifications_bulk,
delete_key,
delete_notification,
get_all_keys,
@@ -48,7 +48,7 @@ from handlers.texts import (
from handlers.utils import format_hours, format_minutes
from logger import logger
-from .notify_utils import send_notification, send_messages_with_limit
+from .notify_utils import send_messages_with_limit, send_notification
from .special_notifications import notify_inactive_trial_users, notify_users_no_traffic
@@ -331,19 +331,15 @@ async def handle_expired_keys(bot: Bot, conn: asyncpg.Connection, current_time:
if hours > 0:
if minutes > 0:
delay_message = KEY_EXPIRED_DELAY_HOURS_MINUTES_MSG.format(
- email=email,
- hours_formatted=format_hours(hours),
- minutes_formatted=format_minutes(minutes)
+ email=email, hours_formatted=format_hours(hours), minutes_formatted=format_minutes(minutes)
)
else:
delay_message = KEY_EXPIRED_DELAY_HOURS_MSG.format(
- email=email,
- hours_formatted=format_hours(hours)
+ email=email, hours_formatted=format_hours(hours)
)
else:
delay_message = KEY_EXPIRED_DELAY_MINUTES_MSG.format(
- email=email,
- minutes_formatted=format_minutes(minutes)
+ email=email, minutes_formatted=format_minutes(minutes)
)
else:
delay_message = KEY_EXPIRED_NO_DELAY_MSG.format(email=email)
@@ -431,7 +427,9 @@ async def process_auto_renew_or_notify(
if result:
logger.info(f"✅ Уведомление о продлении подписки {email} отправлено пользователю {tg_id}.")
else:
- logger.warning(f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}.")
+ logger.warning(
+ f"📢 Не удалось отправить уведомление о продлении подписки {email} пользователю {tg_id}."
+ )
except KeyError as e:
logger.error(f"❌ Ошибка форматирования сообщения KEY_RENEWED: отсутствует ключ {e}")
except Exception as e:
@@ -443,4 +441,4 @@ async def process_auto_renew_or_notify(
logger.info(f"📢 Отправлено уведомление об истекающей подписке {email} пользователю {tg_id}.")
await add_notification(tg_id, notification_id, session=conn)
else:
- logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {email} пользователю {tg_id}.")
\ No newline at end of file
+ logger.warning(f"📢 Не удалось отправить уведомление об истекающей подписке {email} пользователю {tg_id}.")
diff --git a/handlers/notifications/notify_utils.py b/handlers/notifications/notify_utils.py
index a6de8b37..679db645 100644
--- a/handlers/notifications/notify_utils.py
+++ b/handlers/notifications/notify_utils.py
@@ -16,16 +16,10 @@ async def send_messages_with_limit(bot: Bot, messages: list[dict], messages_per_
"""
batch_size = messages_per_second
for i in range(0, len(messages), batch_size):
- batch = messages[i:i + batch_size]
+ batch = messages[i : i + batch_size]
tasks = []
for msg in batch:
- tasks.append(send_notification(
- bot,
- msg["tg_id"],
- msg.get("photo"),
- msg["text"],
- msg.get("keyboard")
- ))
+ tasks.append(send_notification(bot, msg["tg_id"], msg.get("photo"), msg["text"], msg.get("keyboard")))
try:
await asyncio.gather(*tasks, return_exceptions=True)
except Exception as e:
@@ -50,6 +44,7 @@ def rate_limited_send(func):
tg_id = kwargs.get("tg_id") or args[1]
logger.error(f"❌ Ошибка отправки сообщения пользователю {tg_id}: {e}")
return False
+
return wrapper
@@ -65,7 +60,7 @@ async def send_notification(
"""
if image_filename is None:
return await _send_text_notification(bot, tg_id, caption, keyboard)
-
+
photo_path = os.path.join("img", image_filename)
if os.path.isfile(photo_path):
return await _send_photo_notification(bot, tg_id, photo_path, image_filename, caption, keyboard)
@@ -114,4 +109,4 @@ async def _send_text_notification(
return False
except Exception as e:
logger.error(f"Неизвестная ошибка при отправке сообщения для пользователя {tg_id}: {e}")
- return False
\ No newline at end of file
+ return False
diff --git a/handlers/notifications/special_notifications.py b/handlers/notifications/special_notifications.py
index 2110a2b4..bbfb9a8f 100644
--- a/handlers/notifications/special_notifications.py
+++ b/handlers/notifications/special_notifications.py
@@ -20,7 +20,8 @@ from handlers.keys.key_utils import get_user_traffic
from handlers.texts import TRIAL_INACTIVE_BONUS_MSG, TRIAL_INACTIVE_FIRST_MSG, ZERO_TRAFFIC_MSG
from handlers.utils import format_days
from logger import logger
-from .notify_utils import send_notification, send_messages_with_limit
+
+from .notify_utils import send_messages_with_limit, send_notification
router = Router()
@@ -70,8 +71,7 @@ async def notify_inactive_trial_users(bot: Bot, conn: asyncpg.Connection):
await conn.execute("UPDATE users SET trial = -1 WHERE tg_id = $1", tg_id)
else:
message = TRIAL_INACTIVE_FIRST_MSG.format(
- display_name=display_name,
- trial_time_formatted=format_days(TRIAL_TIME)
+ display_name=display_name, trial_time_formatted=format_days(TRIAL_TIME)
)
messages.append({
@@ -175,4 +175,4 @@ async def notify_users_no_traffic(bot: Bot, conn: asyncpg.Connection, current_ti
except Exception as e:
logger.error(f"Ошибка обновления notified для пользователя {tg_id}: {e}")
- logger.info("✅ Обработка пользователей с нулевым трафиком завершена.")
\ No newline at end of file
+ logger.info("✅ Обработка пользователей с нулевым трафиком завершена.")
diff --git a/handlers/pay.py b/handlers/pay.py
index 6ed10c0f..d363795f 100644
--- a/handlers/pay.py
+++ b/handlers/pay.py
@@ -1,8 +1,10 @@
+import os
+
+from typing import Any
+
from aiogram import F, Router
from aiogram.types import CallbackQuery, InlineKeyboardButton
from aiogram.utils.keyboard import InlineKeyboardBuilder
-from typing import Any
-import os
from config import (
CRYPTO_BOT_ENABLE,
@@ -12,9 +14,19 @@ from config import (
YOOKASSA_ENABLE,
YOOMONEY_ENABLE,
)
-from handlers.buttons import CRYPTOBOT, MAIN_MENU, ROBOKASSA, STARS, YOOKASSA, YOOMONEY, PAYMENT, BALANCE_HISTORY, COUPON
-from handlers.texts import PAYMENT_METHODS_MSG, BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT
from database import get_last_payments
+from handlers.buttons import (
+ BALANCE_HISTORY,
+ COUPON,
+ CRYPTOBOT,
+ MAIN_MENU,
+ PAYMENT,
+ ROBOKASSA,
+ STARS,
+ YOOKASSA,
+ YOOMONEY,
+)
+from handlers.texts import BALANCE_HISTORY_HEADER, BALANCE_MANAGEMENT_TEXT, PAYMENT_METHODS_MSG
from .utils import edit_or_send_message
@@ -131,4 +143,4 @@ async def balance_history_handler(callback_query: CallbackQuery, session: Any):
reply_markup=builder.as_markup(),
media_path=None,
disable_web_page_preview=False,
- )
\ No newline at end of file
+ )
diff --git a/handlers/refferal.py b/handlers/refferal.py
index 3927e72c..658f087c 100644
--- a/handlers/refferal.py
+++ b/handlers/refferal.py
@@ -1,15 +1,14 @@
import os
from io import BytesIO
-
from typing import Any
import asyncpg
import qrcode
from aiogram import F, Router
-from aiogram.fsm.context import FSMContext
from aiogram.enums import ParseMode
+from aiogram.fsm.context import FSMContext
from aiogram.types import (
CallbackQuery,
InlineKeyboardButton,
@@ -20,6 +19,7 @@ from aiogram.types import (
)
from aiogram.utils.keyboard import InlineKeyboardBuilder
+from bot import bot
from config import (
ADMIN_ID,
DATABASE_URL,
@@ -28,7 +28,7 @@ from config import (
TRIAL_TIME,
USERNAME_BOT,
)
-from database import get_referral_stats, get_referral_by_referred_id, add_referral, add_user, check_user_exists
+from database import add_referral, add_user, check_user_exists, get_referral_by_referred_id, get_referral_stats
from handlers.buttons import (
BACK,
INVITE,
@@ -36,17 +36,22 @@ from handlers.buttons import (
QR,
TOP_FIVE,
)
-from handlers.texts import INVITE_TEXT_NON_INLINE, TOP_REFERRALS_TEXT, REFERRAL_OFFERS, REFERRAL_SUCCESS_MSG, NEW_REFERRAL_NOTIFICATION
+from handlers.texts import (
+ INVITE_TEXT_NON_INLINE,
+ NEW_REFERRAL_NOTIFICATION,
+ REFERRAL_OFFERS,
+ REFERRAL_SUCCESS_MSG,
+ TOP_REFERRALS_TEXT,
+)
from logger import logger
from .texts import get_referral_link, invite_message_send
from .utils import edit_or_send_message, format_days
-from bot import bot
-
router = Router()
+
@router.callback_query(F.data == "invite")
@router.message(F.text == "/invite")
async def invite_handler(callback_query_or_message: Message | CallbackQuery):
@@ -253,4 +258,4 @@ async def handle_referral_link(referral_code: str, message: Message, state: FSMC
except Exception as e:
logger.error(f"Ошибка при обработке реферальной ссылки {referral_code}: {e}")
await message.answer("❌ Произошла ошибка при обработке реферальной ссылки.")
- return
\ No newline at end of file
+ return
diff --git a/handlers/start.py b/handlers/start.py
index 73637ba7..1927cf35 100644
--- a/handlers/start.py
+++ b/handlers/start.py
@@ -24,14 +24,14 @@ from config import (
SUPPORT_CHAT_URL,
)
from database import (
- get_trial,
add_user,
check_user_exists,
+ get_trial,
)
-from handlers.payments.gift import handle_gift_link
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.payments.gift import handle_gift_link
from handlers.profile import process_callback_view_profile
from handlers.texts import (
NOT_SUBSCRIBED_YET_MSG,
@@ -44,8 +44,8 @@ from handlers.texts import (
from logger import logger
from .admin.panel.keyboard import AdminPanelCallback
-from .utils import edit_or_send_message
from .refferal import handle_referral_link
+from .utils import edit_or_send_message
router = Router()
@@ -219,14 +219,9 @@ async def process_start_logic(
async def handle_utm_link(utm_code, message, state, session):
-
- exists = await session.fetchval(
- "SELECT 1 FROM tracking_sources WHERE code = $1", utm_code
- )
+ exists = await session.fetchval("SELECT 1 FROM tracking_sources WHERE code = $1", utm_code)
if exists:
- current_code = await session.fetchval(
- "SELECT source_code FROM users WHERE tg_id = $1", message.chat.id
- )
+ current_code = await session.fetchval("SELECT source_code FROM users WHERE tg_id = $1", message.chat.id)
if current_code is None:
user_exists = await check_user_exists(message.chat.id)
from_user = message.from_user
@@ -242,10 +237,7 @@ async def handle_utm_link(utm_code, message, state, session):
source_code=utm_code,
)
else:
- await session.execute(
- "UPDATE users SET source_code = $1 WHERE tg_id = $2",
- utm_code, message.chat.id
- )
+ await session.execute("UPDATE users SET source_code = $1 WHERE tg_id = $2", utm_code, message.chat.id)
logger.info(f"[UTM] Привязана {utm_code} к пользователю {message.chat.id}")
else:
await message.answer("❌ UTM ссылка не найдена.")
diff --git a/handlers/utils.py b/handlers/utils.py
index dc4b09d6..189bae49 100644
--- a/handlers/utils.py
+++ b/handlers/utils.py
@@ -11,7 +11,7 @@ import asyncpg
from aiogram.types import BufferedInputFile, InlineKeyboardMarkup, InputMediaPhoto, Message
from bot import bot
-from config import DATABASE_URL, ADMIN_ID
+from config import ADMIN_ID, DATABASE_URL
from database import get_all_keys, get_servers
from logger import logger
@@ -85,8 +85,7 @@ async def check_server_key_limit(server_info: dict, conn) -> bool:
if usage_percent >= 0.9:
notif_key = f"server_warn_{server_name}"
already_sent = await conn.fetchval(
- "SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = 0 AND notification_type = $1)",
- notif_key
+ "SELECT EXISTS (SELECT 1 FROM notifications WHERE tg_id = 0 AND notification_type = $1)", notif_key
)
if not already_sent:
for admin_id in ADMIN_ID:
@@ -130,12 +129,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_months(months: int) -> str:
"""Форматирует количество месяцев с правильным склонением"""
if months <= 0:
return "0 месяцев"
return f"{months} {get_plural_form(months, 'месяц', 'месяца', 'месяцев')}"
+
def format_days(days: int) -> str:
"""
Форматирует количество дней с правильным склонением.
@@ -144,12 +145,14 @@ def format_days(days: int) -> str:
return "0 дней"
return f"{days} {get_plural_form(days, 'день', 'дня', 'дней')}"
+
def format_hours(hours: int) -> str:
"""Форматирует количество часов с правильным склонением"""
if hours <= 0:
return "0 часов"
return f"{hours} {get_plural_form(hours, 'час', 'часа', 'часов')}"
+
def format_minutes(minutes: int) -> str:
"""Форматирует количество минут с правильным склонением"""
if minutes <= 0:
@@ -252,4 +255,4 @@ def sanitize_key_name(key_name: str) -> str:
Returns:
str: Очищенное название ключа в нижнем регистре.
"""
- return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
\ No newline at end of file
+ return re.sub(r"[^a-z0-9@._-]", "", key_name.lower())
diff --git a/panels/three_xui.py b/panels/three_xui.py
index 0ea7c5ff..b932ef86 100644
--- a/panels/three_xui.py
+++ b/panels/three_xui.py
@@ -1,18 +1,20 @@
+import time
+
from dataclasses import dataclass
from typing import Any
import httpx
import py3xui
+
from py3xui import AsyncApi
-import time
from config import (
- LIMIT_IP,
- SUPERNODE,
- XUI_TOKEN,
- USE_XUI_TOKEN,
ADMIN_PASSWORD,
ADMIN_USERNAME,
+ LIMIT_IP,
+ SUPERNODE,
+ USE_XUI_TOKEN,
+ XUI_TOKEN,
)
from logger import logger
@@ -36,6 +38,7 @@ class ClientConfig:
_xui_instance_cache: dict[str, tuple[AsyncApi, float]] = {}
SESSION_TTL = 1800
+
async def get_xui_instance(api_url: str) -> AsyncApi:
key = f"{api_url}|{ADMIN_USERNAME}"
current_time = time.time()
@@ -46,7 +49,7 @@ async def get_xui_instance(api_url: str) -> AsyncApi:
if current_time - last_login < SESSION_TTL:
return xui
else:
- logger.info(f"[XUI Cache] Сессия устарела (>30 минут), переподключение...")
+ logger.info("[XUI Cache] Сессия устарела (>30 минут), переподключение...")
await xui.login()
_xui_instance_cache[key] = (xui, current_time)
return xui
@@ -65,7 +68,6 @@ async def get_xui_instance(api_url: str) -> AsyncApi:
async def add_client(xui: py3xui.AsyncApi, config: ClientConfig) -> dict[str, Any]:
try:
-
client = py3xui.Client(
id=config.client_id,
email=config.email.lower(),
@@ -107,7 +109,6 @@ async def extend_client_key(
tg_id: int,
) -> bool | None:
try:
-
client = await xui.client.get_by_email(email)
if not client or not client.id:
logger.warning(f"Клиент с email {email} не найден или не имеет ID.")
@@ -158,7 +159,6 @@ async def delete_client(
bool: True если удаление успешно, False в противном случае
"""
try:
-
if SUPERNODE:
await xui.client.delete(inbound_id, client_id)
logger.info(f"Клиент с ID {client_id} был удален успешно (SUPERNODE)")
@@ -185,7 +185,6 @@ async def delete_client(
async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str, Any]:
try:
-
traffic_data = await xui.client.get_traffic_by_id(client_id)
if not traffic_data:
logger.warning(f"Трафик для клиента {client_id} не найден.")
@@ -205,7 +204,6 @@ async def get_client_traffic(xui: py3xui.AsyncApi, client_id: str) -> dict[str,
async def toggle_client(xui: py3xui.AsyncApi, inbound_id: int, email: str, client_id: str, enable: bool = True) -> bool:
try:
-
client = await xui.client.get_by_email(email)
if not client:
logger.warning(f"Клиент с email {email} и ID {client_id} не найден.")