Module management/ TRIBUTE webapp/ small gift fix/ backup selection in CLI
This commit is contained in:
+2
-1
@@ -59,4 +59,5 @@ Dockerfile
|
||||
setup.py
|
||||
.ruff_cache
|
||||
.github/workflows/
|
||||
modules/
|
||||
modules/
|
||||
storage/
|
||||
@@ -13,17 +13,16 @@ from config import ADMIN_ID, API_TOKEN
|
||||
from database import async_session_maker
|
||||
from filters.private import IsPrivateFilter
|
||||
from logger import logger
|
||||
from utils.modules_loader import load_modules_from_folder
|
||||
from utils.modules_loader import load_modules_from_folder, modules_hub
|
||||
|
||||
|
||||
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
|
||||
storage = MemoryStorage()
|
||||
dp = Dispatcher(bot=bot, storage=storage)
|
||||
|
||||
dp.include_router(modules_hub)
|
||||
|
||||
for mod_router in load_modules_from_folder():
|
||||
dp.include_router(mod_router)
|
||||
|
||||
load_modules_from_folder()
|
||||
|
||||
dp.message.filter(IsPrivateFilter())
|
||||
dp.callback_query.filter(IsPrivateFilter())
|
||||
|
||||
+60
-18
@@ -51,11 +51,10 @@ try:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
console = Console()
|
||||
ensure_utf8_locale()
|
||||
|
||||
console = Console()
|
||||
|
||||
BACK_DIR = os.path.expanduser("~/.solobot_backup")
|
||||
BACK_DIR = os.path.expanduser("~/.solobot_backups")
|
||||
TEMP_DIR = os.path.expanduser("~/.solobot_tmp")
|
||||
PROJECT_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
IS_ROOT_DIR = PROJECT_DIR == "/root"
|
||||
@@ -97,29 +96,74 @@ def print_logo():
|
||||
console.print(f"[bold green]Директория бота:[/bold green] [yellow]{PROJECT_DIR}[/yellow]\n")
|
||||
|
||||
|
||||
def list_backups():
|
||||
if not os.path.isdir(BACK_DIR):
|
||||
return []
|
||||
pairs = []
|
||||
for name in os.listdir(BACK_DIR):
|
||||
path = os.path.join(BACK_DIR, name)
|
||||
if os.path.isdir(path):
|
||||
try:
|
||||
mtime = os.path.getmtime(path)
|
||||
except Exception:
|
||||
mtime = 0
|
||||
pairs.append((mtime, path))
|
||||
pairs.sort(reverse=True)
|
||||
return [p for _, p in pairs]
|
||||
|
||||
|
||||
def prune_old_backups():
|
||||
backups = list_backups()
|
||||
for path in backups[3:]:
|
||||
try:
|
||||
shutil.rmtree(path, ignore_errors=True)
|
||||
except Exception:
|
||||
subprocess.run(["sudo", "rm", "-rf", path])
|
||||
|
||||
|
||||
def backup_project():
|
||||
from datetime import datetime
|
||||
|
||||
os.makedirs(BACK_DIR, exist_ok=True)
|
||||
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
dst = os.path.join(BACK_DIR, f"backup-{ts}")
|
||||
console.print("[yellow]Создаётся резервная копия проекта...[/yellow]")
|
||||
with console.status("[bold cyan]Копирование файлов...[/bold cyan]"):
|
||||
subprocess.run(["rm", "-rf", BACK_DIR])
|
||||
subprocess.run(["cp", "-r", PROJECT_DIR, BACK_DIR])
|
||||
console.print(f"[green]Бэкап сохранён в: {BACK_DIR}[/green]")
|
||||
subprocess.run(["cp", "-r", PROJECT_DIR, dst])
|
||||
console.print(f"[green]Бэкап сохранён в: {dst}[/green]")
|
||||
prune_old_backups()
|
||||
|
||||
|
||||
def restore_from_backup():
|
||||
if not os.path.isdir(BACK_DIR):
|
||||
console.print(f"[red]❌ Бэкап не найден: {BACK_DIR}[/red]")
|
||||
from datetime import datetime
|
||||
|
||||
backups = list_backups()[:3]
|
||||
if not backups:
|
||||
console.print(f"[red]❌ Бэкапы не найдены: {BACK_DIR}[/red]")
|
||||
return
|
||||
|
||||
console.print("\n[bold green]Доступные бэкапы:[/bold green]")
|
||||
shown = []
|
||||
for idx, path in enumerate(backups, 1):
|
||||
try:
|
||||
mtime = os.path.getmtime(path)
|
||||
dt = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
dt = "unknown"
|
||||
console.print(f"[cyan]{idx}.[/cyan] {os.path.basename(path)} [dim]{dt}[/dim]")
|
||||
shown.append((idx, path))
|
||||
|
||||
try:
|
||||
mtime = os.path.getmtime(BACK_DIR)
|
||||
from datetime import datetime
|
||||
|
||||
dt = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
||||
console.print(f"[cyan]Обнаружен бэкап от: {dt}[/cyan]")
|
||||
choice = Prompt.ask(
|
||||
"[bold blue]Выберите номер бэкапа[/bold blue]",
|
||||
choices=[str(i) for i, _ in shown],
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
console.print("[red]Внимание: текущие файлы проекта будут перезаписаны содержимым бэкапа.[/red]")
|
||||
sel_path = shown[int(choice) - 1][1]
|
||||
|
||||
console.print("[red]Внимание: текущие файлы проекта будут перезаписаны выбранным бэкапом.[/red]")
|
||||
if not Confirm.ask("[yellow]Продолжить восстановление из бэкапа?[/yellow]"):
|
||||
return
|
||||
|
||||
@@ -130,7 +174,7 @@ def restore_from_backup():
|
||||
install_rsync_if_needed()
|
||||
|
||||
console.print("[yellow]Копирую файлы из бэкапа в проект...[/yellow]")
|
||||
rc = subprocess.run(f"rsync -a --delete {BACK_DIR}/ {PROJECT_DIR}/", shell=True).returncode
|
||||
rc = subprocess.run(f"rsync -a --delete {sel_path}/ {PROJECT_DIR}/", shell=True).returncode
|
||||
if rc != 0:
|
||||
console.print("[red]❌ Ошибка rsync при восстановлении[/red]")
|
||||
return
|
||||
@@ -142,7 +186,6 @@ def restore_from_backup():
|
||||
|
||||
|
||||
def auto_update_cli():
|
||||
"""Обновляет CLI, если отличается от последней версии. Перезапускает при необходимости."""
|
||||
console.print("[yellow]Проверка обновлений CLI...[/yellow]")
|
||||
try:
|
||||
url = "https://raw.githubusercontent.com/Vladless/Solo_bot/dev/cli_launcher.py"
|
||||
@@ -170,7 +213,6 @@ def auto_update_cli():
|
||||
|
||||
|
||||
def fix_permissions():
|
||||
"""Устанавливает корректные права на все файлы и папки проекта"""
|
||||
console.print("[yellow]Восстанавливаю владельца и права доступа к проекту...[/yellow]")
|
||||
|
||||
try:
|
||||
|
||||
@@ -9,6 +9,7 @@ from .clusters import router as clusters_router
|
||||
from .coupons import router as coupons_router
|
||||
from .gifts import router as gifts_router
|
||||
from .management import router as management_router
|
||||
from .module import router as modules_router
|
||||
from .panel import router as panel_router
|
||||
from .restart import router as restart_router
|
||||
from .sender import router as sender_router
|
||||
@@ -21,6 +22,7 @@ from .users import router as users_router
|
||||
router = Router(name="admins_main_router")
|
||||
|
||||
router.include_routers(
|
||||
modules_router,
|
||||
panel_router,
|
||||
management_router,
|
||||
servers_router,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
__all__ = ("router",)
|
||||
|
||||
from .module_handler import router
|
||||
@@ -0,0 +1,88 @@
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from handlers.admin.panel.keyboard import AdminPanelCallback
|
||||
from handlers.buttons import BACK
|
||||
from utils.modules_manager import manager
|
||||
|
||||
|
||||
def build_modules_kb(page: int, total_pages: int, items: list[tuple[str, str | None]]) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
for name, _ in items:
|
||||
label = name if manager.is_enabled(name) else f"{name} (off)"
|
||||
builder.button(
|
||||
text=label,
|
||||
callback_data=AdminPanelCallback(action=f"module__{name}", page=page).pack(),
|
||||
)
|
||||
|
||||
if items:
|
||||
builder.adjust(2)
|
||||
|
||||
if total_pages > 1:
|
||||
row = []
|
||||
if page > 1:
|
||||
row.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ Назад",
|
||||
callback_data=AdminPanelCallback(action="modules", page=page - 1).pack(),
|
||||
)
|
||||
)
|
||||
row.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{page}/{total_pages}",
|
||||
callback_data=AdminPanelCallback(action="modules", page=page).pack(),
|
||||
)
|
||||
)
|
||||
if page < total_pages:
|
||||
row.append(
|
||||
InlineKeyboardButton(
|
||||
text="Вперед ➡️",
|
||||
callback_data=AdminPanelCallback(action="modules", page=page + 1).pack(),
|
||||
)
|
||||
)
|
||||
builder.row(*row)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text=BACK,
|
||||
callback_data=AdminPanelCallback(action="admin", page=1).pack(),
|
||||
)
|
||||
)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def build_module_menu_kb(name: str, page: int) -> InlineKeyboardMarkup:
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
enabled = manager.is_enabled(name)
|
||||
|
||||
if enabled:
|
||||
builder.button(
|
||||
text="🔁 Перезапустить",
|
||||
callback_data=AdminPanelCallback(action=f"module_restart__{name}", page=page).pack(),
|
||||
)
|
||||
builder.button(
|
||||
text="🛑 Остановить",
|
||||
callback_data=AdminPanelCallback(action=f"module_stop__{name}", page=page).pack(),
|
||||
)
|
||||
else:
|
||||
builder.button(
|
||||
text="▶️ Запустить",
|
||||
callback_data=AdminPanelCallback(action=f"module_start__{name}", page=page).pack(),
|
||||
)
|
||||
|
||||
builder.button(
|
||||
text="🔄 Обновить",
|
||||
callback_data=AdminPanelCallback(action=f"module_update__{name}", page=page).pack(),
|
||||
)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️ К списку",
|
||||
callback_data=AdminPanelCallback(action="modules", page=page).pack(),
|
||||
)
|
||||
)
|
||||
builder.adjust(1)
|
||||
return builder.as_markup()
|
||||
@@ -0,0 +1,167 @@
|
||||
import os
|
||||
|
||||
from math import ceil
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from filters.admin import IsAdminFilter
|
||||
from handlers.admin.panel.keyboard import AdminPanelCallback
|
||||
from utils.modules_manager import manager
|
||||
|
||||
from .keyboard import build_module_menu_kb, build_modules_kb
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
def list_installed_modules() -> list[tuple[str, str | None]]:
|
||||
base = "modules"
|
||||
if not os.path.isdir(base):
|
||||
return []
|
||||
items: list[tuple[str, str | None]] = []
|
||||
for name in sorted(os.listdir(base)):
|
||||
path = os.path.join(base, name)
|
||||
if os.path.isdir(path) and not name.startswith("."):
|
||||
ver = None
|
||||
vp = os.path.join(path, "VERSION")
|
||||
if os.path.isfile(vp):
|
||||
try:
|
||||
with open(vp, encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
ver = line
|
||||
break
|
||||
except Exception:
|
||||
ver = None
|
||||
items.append((name, ver))
|
||||
return items
|
||||
|
||||
|
||||
@router.callback_query(AdminPanelCallback.filter(F.action == "modules"), IsAdminFilter())
|
||||
async def handle_modules(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
await state.clear()
|
||||
|
||||
data = callback_query.data or ""
|
||||
try:
|
||||
page = int(data.split("page=")[-1].split("&")[0]) if "page=" in data else 1
|
||||
except Exception:
|
||||
page = 1
|
||||
|
||||
all_items = list_installed_modules()
|
||||
items = [(n, v) for n, v in all_items if n != "web_admin_panel"]
|
||||
|
||||
per_page = 12
|
||||
total_pages = max(1, (len(items) + per_page - 1) // per_page)
|
||||
page = max(1, min(page, total_pages))
|
||||
start = (page - 1) * per_page
|
||||
chunk = items[start : start + per_page]
|
||||
|
||||
if chunk:
|
||||
|
||||
def fmt(n, v):
|
||||
return f"{n} v{v}" if v else n
|
||||
|
||||
lines = "\n".join(f"• {fmt(n, v)}" for n, v in chunk)
|
||||
text = f"🧩 Мои модули\n\nНайдено: {len(items)}\n<blockquote>{lines}</blockquote>"
|
||||
else:
|
||||
text = "🧩 Мои модули\n\nМодулей не найдено."
|
||||
|
||||
markup = build_modules_kb(page, total_pages, chunk)
|
||||
try:
|
||||
await callback_query.message.edit_text(text=text, reply_markup=markup, disable_web_page_preview=True)
|
||||
except TelegramBadRequest as e:
|
||||
if "message is not modified" in str(e).lower():
|
||||
return
|
||||
raise
|
||||
|
||||
|
||||
@router.callback_query(AdminPanelCallback.filter(F.action.startswith("module_restart__")), IsAdminFilter())
|
||||
async def handle_module_restart(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
await state.clear()
|
||||
|
||||
packed = AdminPanelCallback.unpack(callback_query.data)
|
||||
action = packed.action
|
||||
page = packed.page or 1
|
||||
name = action.split("module_restart__", 1)[-1]
|
||||
|
||||
try:
|
||||
await manager.restart(name)
|
||||
result = "✅ Модуль перезапущен."
|
||||
except Exception as e:
|
||||
result = f"❌ Ошибка перезапуска: {e}"
|
||||
|
||||
items = dict(list_installed_modules())
|
||||
ver = items.get(name)
|
||||
title = f"{name} v{ver}" if ver else name
|
||||
text = f"🧩 Модуль: <b>{title}</b>\n\n{result}"
|
||||
|
||||
markup = build_module_menu_kb(name, page)
|
||||
try:
|
||||
await callback_query.message.edit_text(text=text, reply_markup=markup, disable_web_page_preview=True)
|
||||
except TelegramBadRequest as e:
|
||||
if "message is not modified" in str(e).lower():
|
||||
return
|
||||
raise
|
||||
|
||||
|
||||
@router.callback_query(AdminPanelCallback.filter(F.action.startswith("module_stop__")), IsAdminFilter())
|
||||
async def handle_module_stop(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
await state.clear()
|
||||
|
||||
packed = AdminPanelCallback.unpack(callback_query.data)
|
||||
action = packed.action
|
||||
page = packed.page or 1
|
||||
name = action.split("module_stop__", 1)[-1]
|
||||
|
||||
try:
|
||||
await manager.stop(name)
|
||||
result = "🛑 Модуль остановлен."
|
||||
except Exception as e:
|
||||
result = f"❌ Ошибка остановки: {e}"
|
||||
|
||||
items = dict(list_installed_modules())
|
||||
ver = items.get(name)
|
||||
title = f"{name} v{ver}" if ver else name
|
||||
text = f"🧩 Модуль: <b>{title}</b>\n\n{result}"
|
||||
|
||||
markup = build_module_menu_kb(name, page)
|
||||
try:
|
||||
await callback_query.message.edit_text(text=text, reply_markup=markup, disable_web_page_preview=True)
|
||||
except TelegramBadRequest as e:
|
||||
if "message is not modified" in str(e).lower():
|
||||
return
|
||||
raise
|
||||
|
||||
|
||||
@router.callback_query(AdminPanelCallback.filter(F.action.startswith("module_start__")), IsAdminFilter())
|
||||
async def handle_module_start(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
|
||||
await state.clear()
|
||||
|
||||
packed = AdminPanelCallback.unpack(callback_query.data)
|
||||
action = packed.action
|
||||
page = packed.page or 1
|
||||
name = action.split("module_start__", 1)[-1]
|
||||
|
||||
try:
|
||||
await manager.start(name)
|
||||
result = "▶️ Модуль запущен."
|
||||
except Exception as e:
|
||||
result = f"❌ Ошибка запуска: {e}"
|
||||
|
||||
items = dict(list_installed_modules())
|
||||
ver = items.get(name)
|
||||
title = f"{name} v{ver}" if ver else name
|
||||
text = f"🧩 Модуль: <b>{title}</b>\n\n{result}"
|
||||
|
||||
markup = build_module_menu_kb(name, page)
|
||||
try:
|
||||
await callback_query.message.edit_text(text=text, reply_markup=markup, disable_web_page_preview=True)
|
||||
except TelegramBadRequest as e:
|
||||
if "message is not modified" in str(e).lower():
|
||||
return
|
||||
raise
|
||||
@@ -47,6 +47,10 @@ async def build_panel_kb(admin_role: str) -> InlineKeyboardMarkup:
|
||||
InlineKeyboardButton(text="💸 Тарифы", callback_data=AdminPanelCallback(action="tariffs").pack()),
|
||||
InlineKeyboardButton(text="🎁 Подарки", callback_data=AdminPanelCallback(action="gifts").pack()),
|
||||
)
|
||||
builder.button(
|
||||
text="🧩 Мои модули",
|
||||
callback_data=AdminPanelCallback(action="modules").pack(),
|
||||
)
|
||||
builder.button(
|
||||
text="🤖 Управление ботом",
|
||||
callback_data=AdminPanelCallback(action="management").pack(),
|
||||
@@ -70,9 +74,9 @@ async def build_panel_kb(admin_role: str) -> InlineKeyboardMarkup:
|
||||
)
|
||||
|
||||
if admin_role == "superadmin":
|
||||
builder.adjust(1, 1, 1, 1, 2, 2, 1, 2, 1)
|
||||
builder.adjust(1, 1, 1, 1, 2, 2, 1, 1, 2, 1)
|
||||
else:
|
||||
builder.adjust(1, 1, 1, 2, 1, 1)
|
||||
builder.adjust(1, 1, 1, 2, 1, 1, 1)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
@@ -109,10 +109,10 @@ async def process_windows_menu(callback_query: CallbackQuery, session: Any):
|
||||
builder.row(InlineKeyboardButton(text=DOWNLOAD_PC_BUTTON, url=DOWNLOAD_PC))
|
||||
|
||||
if key_link and "happ://crypt" in key_link:
|
||||
processed_link = urllib.parse.quote(key_link, safe='')
|
||||
processed_link = urllib.parse.quote(key_link, safe="")
|
||||
else:
|
||||
processed_link = key_link
|
||||
|
||||
|
||||
builder.row(InlineKeyboardButton(text=CONNECT_WINDOWS_BUTTON, url=f"{CONNECT_WINDOWS}{processed_link}"))
|
||||
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_pc|{key_name}"))
|
||||
@@ -140,10 +140,10 @@ async def process_macos_menu(callback_query: CallbackQuery, session: Any):
|
||||
builder.row(InlineKeyboardButton(text=DOWNLOAD_MACOS_BUTTON, url=DOWNLOAD_MACOS))
|
||||
|
||||
if key_link and "happ://crypt" in key_link:
|
||||
processed_link = urllib.parse.quote(key_link, safe='')
|
||||
processed_link = urllib.parse.quote(key_link, safe="")
|
||||
else:
|
||||
processed_link = key_link
|
||||
|
||||
|
||||
builder.row(InlineKeyboardButton(text=CONNECT_MACOS_BUTTON, url=f"{CONNECT_MACOS}{processed_link}"))
|
||||
builder.row(InlineKeyboardButton(text=SUPPORT, url=SUPPORT_CHAT_URL))
|
||||
builder.row(InlineKeyboardButton(text=BACK, callback_data=f"connect_pc|{key_name}"))
|
||||
|
||||
@@ -108,10 +108,10 @@ async def process_callback_connect_phone(callback_query: CallbackQuery, session:
|
||||
InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID),
|
||||
)
|
||||
if key_link and "happ://crypt" in key_link:
|
||||
processed_link = urllib.parse.quote(key_link, safe='')
|
||||
processed_link = urllib.parse.quote(key_link, safe="")
|
||||
else:
|
||||
processed_link = key_link
|
||||
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{processed_link}"),
|
||||
InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{processed_link}"),
|
||||
@@ -148,10 +148,10 @@ async def process_callback_connect_ios(callback_query: CallbackQuery, session: A
|
||||
builder.row(InlineKeyboardButton(text=DOWNLOAD_IOS_BUTTON, url=DOWNLOAD_IOS))
|
||||
|
||||
if key_link and "happ://crypt" in key_link:
|
||||
processed_link = urllib.parse.quote(key_link, safe='')
|
||||
processed_link = urllib.parse.quote(key_link, safe="")
|
||||
else:
|
||||
processed_link = key_link
|
||||
|
||||
|
||||
builder.row(InlineKeyboardButton(text=IMPORT_IOS, url=f"{CONNECT_IOS}{processed_link}"))
|
||||
if INSTRUCTIONS_BUTTON:
|
||||
builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
|
||||
@@ -186,10 +186,10 @@ async def process_callback_connect_android(callback_query: CallbackQuery, sessio
|
||||
builder.row(InlineKeyboardButton(text=DOWNLOAD_ANDROID_BUTTON, url=DOWNLOAD_ANDROID))
|
||||
|
||||
if key_link and "happ://crypt" in key_link:
|
||||
processed_link = urllib.parse.quote(key_link, safe='')
|
||||
processed_link = urllib.parse.quote(key_link, safe="")
|
||||
else:
|
||||
processed_link = key_link
|
||||
|
||||
|
||||
builder.row(InlineKeyboardButton(text=IMPORT_ANDROID, url=f"{CONNECT_ANDROID}{processed_link}"))
|
||||
if INSTRUCTIONS_BUTTON:
|
||||
builder.row(InlineKeyboardButton(text=MANUAL_INSTRUCTIONS, callback_data="instructions"))
|
||||
|
||||
@@ -29,9 +29,9 @@ from handlers.buttons import (
|
||||
MAIN_MENU,
|
||||
MY_SUB,
|
||||
PC_BUTTON,
|
||||
ROUTER_BUTTON,
|
||||
SUPPORT,
|
||||
TV_BUTTON,
|
||||
ROUTER_BUTTON
|
||||
)
|
||||
from handlers.keys.operations import create_key_on_cluster
|
||||
from handlers.texts import key_message_success
|
||||
|
||||
@@ -168,17 +168,17 @@ async def handle_key_creation(
|
||||
|
||||
try:
|
||||
hook_results = await run_hooks(
|
||||
"purchase_tariff_group_override",
|
||||
chat_id=tg_id,
|
||||
admin=False,
|
||||
"purchase_tariff_group_override",
|
||||
chat_id=tg_id,
|
||||
admin=False,
|
||||
session=session,
|
||||
original_group=group_code
|
||||
original_group=group_code,
|
||||
)
|
||||
for hook_result in hook_results:
|
||||
if hook_result.get("override_group"):
|
||||
group_code = hook_result["override_group"]
|
||||
logger.info(f"[PURCHASE] Тарифная группа переопределена хуком: {group_code}")
|
||||
|
||||
|
||||
if hook_result.get("discount_info"):
|
||||
await state.update_data(discount_info=hook_result["discount_info"])
|
||||
break
|
||||
@@ -392,11 +392,11 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state:
|
||||
|
||||
try:
|
||||
hook_results = await run_hooks(
|
||||
"check_discount_validity",
|
||||
chat_id=tg_id,
|
||||
admin=False,
|
||||
"check_discount_validity",
|
||||
chat_id=tg_id,
|
||||
admin=False,
|
||||
session=session,
|
||||
tariff_group=tariff.get("group_code")
|
||||
tariff_group=tariff.get("group_code"),
|
||||
)
|
||||
for hook_result in hook_results:
|
||||
if not hook_result.get("valid", True):
|
||||
@@ -412,7 +412,6 @@ async def select_tariff_plan(callback_query: CallbackQuery, session: Any, state:
|
||||
except Exception as e:
|
||||
logger.warning(f"[PURCHASE] Ошибка при проверке скидок через хуки: {e}")
|
||||
|
||||
|
||||
duration_days = tariff["duration_days"]
|
||||
price_rub = tariff["price_rub"]
|
||||
|
||||
|
||||
@@ -100,17 +100,14 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
|
||||
|
||||
try:
|
||||
hook_results = await run_hooks(
|
||||
"renewal_forbidden_groups",
|
||||
chat_id=tg_id,
|
||||
admin=False,
|
||||
session=session
|
||||
"renewal_forbidden_groups", chat_id=tg_id, admin=False, session=session
|
||||
)
|
||||
for hook_result in hook_results:
|
||||
additional_groups = hook_result.get("additional_groups", [])
|
||||
forbidden_groups.extend(additional_groups)
|
||||
except Exception as e:
|
||||
logger.warning(f"[RENEW] Ошибка при получении дополнительных групп: {e}")
|
||||
|
||||
|
||||
if current_tariff["group_code"] not in forbidden_groups:
|
||||
group_code = current_tariff["group_code"]
|
||||
|
||||
@@ -121,11 +118,7 @@ async def process_callback_renew_key(callback_query: CallbackQuery, state: FSMCo
|
||||
|
||||
try:
|
||||
hook_results = await run_hooks(
|
||||
"purchase_tariff_group_override",
|
||||
chat_id=tg_id,
|
||||
admin=False,
|
||||
session=session,
|
||||
original_group=group_code
|
||||
"purchase_tariff_group_override", chat_id=tg_id, admin=False, session=session, original_group=group_code
|
||||
)
|
||||
for hook_result in hook_results:
|
||||
if hook_result.get("override_group"):
|
||||
@@ -268,11 +261,7 @@ async def show_tariffs_in_renew_subgroup(callback: CallbackQuery, state: FSMCont
|
||||
|
||||
try:
|
||||
hook_results = await run_hooks(
|
||||
"purchase_tariff_group_override",
|
||||
chat_id=tg_id,
|
||||
admin=False,
|
||||
session=session,
|
||||
original_group=group_code
|
||||
"purchase_tariff_group_override", chat_id=tg_id, admin=False, session=session, original_group=group_code
|
||||
)
|
||||
for hook_result in hook_results:
|
||||
if hook_result.get("override_group"):
|
||||
|
||||
@@ -20,6 +20,7 @@ from config import (
|
||||
CONNECT_PHONE_BUTTON,
|
||||
ENABLE_DELETE_KEY_BUTTON,
|
||||
ENABLE_UPDATE_SUBSCRIPTION_BUTTON,
|
||||
HAPP_CRYPTOLINK,
|
||||
HWID_RESET_BUTTON,
|
||||
QRCODE,
|
||||
REMNAWAVE_LOGIN,
|
||||
@@ -28,7 +29,6 @@ from config import (
|
||||
RENEW_BUTTON_BEFORE_DAYS,
|
||||
TOGGLE_CLIENT,
|
||||
USE_COUNTRY_SELECTION,
|
||||
HAPP_CRYPTOLINK
|
||||
)
|
||||
from database import get_key_details, get_keys, get_servers, get_tariff_by_id
|
||||
from database.models import Key
|
||||
|
||||
@@ -514,18 +514,13 @@ async def process_auto_renew_or_notify(
|
||||
forbidden_groups = ["discounts", "discounts_max", "gifts", "trial"]
|
||||
|
||||
try:
|
||||
hook_results = await run_hooks(
|
||||
"renewal_forbidden_groups",
|
||||
chat_id=tg_id,
|
||||
admin=False,
|
||||
session=conn
|
||||
)
|
||||
hook_results = await run_hooks("renewal_forbidden_groups", chat_id=tg_id, admin=False, session=conn)
|
||||
for hook_result in hook_results:
|
||||
additional_groups = hook_result.get("additional_groups", [])
|
||||
forbidden_groups.extend(additional_groups)
|
||||
except Exception as e:
|
||||
logger.warning(f"[AUTO_RENEW] Ошибка при получении дополнительных групп: {e}")
|
||||
|
||||
|
||||
if current_tariff["group_code"] in forbidden_groups:
|
||||
cluster_tariffs = [t for t in tariffs if t["is_active"] and balance >= t["price_rub"]]
|
||||
if cluster_tariffs:
|
||||
|
||||
@@ -4,6 +4,7 @@ from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
from handlers.texts import FAST_PAY_NOT_ENOUGH
|
||||
from handlers.buttons import RUB_CURRENCY, USD_CURRENCY, STARS, MAIN_MENU
|
||||
from config import TRIBUTE_LINK
|
||||
from .currency_rates import format_for_user
|
||||
|
||||
|
||||
@@ -11,8 +12,17 @@ def build_currency_choice_kb(show_stars: bool, *, prefix: str = "choose_payment_
|
||||
kb = InlineKeyboardBuilder()
|
||||
kb.row(InlineKeyboardButton(text=RUB_CURRENCY, callback_data=f"{prefix}|RUB"))
|
||||
kb.row(InlineKeyboardButton(text=USD_CURRENCY, callback_data=f"{prefix}|USD"))
|
||||
trib = (TRIBUTE_LINK or "").strip()
|
||||
|
||||
if show_stars:
|
||||
kb.row(InlineKeyboardButton(text=STARS, callback_data=f"{prefix}|STARS"))
|
||||
row = [InlineKeyboardButton(text=STARS, callback_data=f"{prefix}|STARS")]
|
||||
if trib.startswith("https://"):
|
||||
row.append(InlineKeyboardButton(text="TRIBUTE", url=trib))
|
||||
kb.row(*row)
|
||||
else:
|
||||
if trib.startswith("https://"):
|
||||
kb.row(InlineKeyboardButton(text="TRIBUTE", url=trib))
|
||||
|
||||
kb.row(InlineKeyboardButton(text=MAIN_MENU, callback_data="profile"))
|
||||
return kb
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+41
-12
@@ -1,24 +1,55 @@
|
||||
import inspect
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from logger import logger
|
||||
|
||||
|
||||
_hooks: dict[str, list[Callable[..., Any]]] = {}
|
||||
_hooks: dict[str, list[tuple[Callable[..., Any], str | None]]] = {}
|
||||
|
||||
|
||||
def register_hook(name: str, func: Callable[..., Any]):
|
||||
if name not in _hooks:
|
||||
_hooks[name] = []
|
||||
_hooks[name].append(func)
|
||||
def owner(func: Callable[..., Any]) -> str | None:
|
||||
m = getattr(func, "__module__", "") or ""
|
||||
if m.startswith("modules."):
|
||||
parts = m.split(".")
|
||||
return parts[1] if len(parts) > 1 else None
|
||||
return None
|
||||
|
||||
|
||||
def register_hook(name: str, func: Callable[..., Any] | None = None):
|
||||
if func is None:
|
||||
|
||||
def deco(f: Callable[..., Any]):
|
||||
_hooks.setdefault(name, []).append((f, owner(f)))
|
||||
logger.info(f"[Hook] Зарегистрирован хук '{name}': {f.__name__}")
|
||||
return f
|
||||
|
||||
return deco
|
||||
_hooks.setdefault(name, []).append((func, owner(func)))
|
||||
logger.info(f"[Hook] Зарегистрирован хук '{name}': {func.__name__}")
|
||||
|
||||
|
||||
async def run_hooks(name: str, **kwargs) -> list[Any]:
|
||||
results = []
|
||||
for func in _hooks.get(name, []):
|
||||
def unregister_module_hooks(module_name: str):
|
||||
for k, lst in list(_hooks.items()):
|
||||
filtered = [(f, owner) for (f, owner) in lst if owner != module_name]
|
||||
if filtered:
|
||||
_hooks[k] = filtered
|
||||
else:
|
||||
_hooks.pop(k, None)
|
||||
|
||||
|
||||
async def run_hooks(name: str, require_enabled: bool = True, **kwargs) -> list[Any]:
|
||||
results: list[Any] = []
|
||||
for func, owner in _hooks.get(name, []):
|
||||
if require_enabled and owner:
|
||||
try:
|
||||
from utils.modules_manager import manager
|
||||
|
||||
if not manager.is_enabled(owner):
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if inspect.iscoroutinefunction(func):
|
||||
result = await func(**kwargs)
|
||||
@@ -27,7 +58,5 @@ async def run_hooks(name: str, **kwargs) -> list[Any]:
|
||||
if result:
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
from logger import logger
|
||||
|
||||
logger.error(f"[HOOK:{name}] Ошибка в {func.__name__}: {e}")
|
||||
logger.error(f"[HOOK:{name}] Ошибка в {getattr(func, '__name__', func)}: {e}")
|
||||
return results
|
||||
|
||||
+22
-8
@@ -7,6 +7,11 @@ from aiogram import Router
|
||||
|
||||
from logger import logger
|
||||
|
||||
from .modules_manager import manager
|
||||
|
||||
|
||||
modules_hub = Router(name="modules_hub")
|
||||
|
||||
|
||||
def load_modules_from_folder(folder: str = "modules") -> list[Router]:
|
||||
routers = []
|
||||
@@ -17,11 +22,18 @@ def load_modules_from_folder(folder: str = "modules") -> list[Router]:
|
||||
return []
|
||||
|
||||
for _finder, name, _ispkg in pkgutil.iter_modules([str(base_path)]):
|
||||
if not manager.should_autostart(name):
|
||||
logger.info(f"[Modules] Пропуск автозапуска модуля '{name}' (отключён).")
|
||||
continue
|
||||
|
||||
module_path = f"{folder}.{name}.router"
|
||||
try:
|
||||
mod = importlib.import_module(module_path)
|
||||
if hasattr(mod, "router") and isinstance(mod.router, Router):
|
||||
routers.append(mod.router)
|
||||
router = getattr(mod, "router", None)
|
||||
if isinstance(router, Router):
|
||||
modules_hub.include_router(router)
|
||||
manager.adopt(name, router)
|
||||
routers.append(router)
|
||||
logger.info(f"[Modules] Загружен модуль: {module_path}")
|
||||
else:
|
||||
logger.warning(f"[Modules] В модуле {module_path} не найден router")
|
||||
@@ -33,12 +45,15 @@ def load_modules_from_folder(folder: str = "modules") -> list[Router]:
|
||||
def load_module_webhooks(folder: str = "modules") -> list[dict]:
|
||||
webhooks = []
|
||||
base_path = Path(folder)
|
||||
|
||||
if not base_path.exists():
|
||||
logger.warning(f"[Modules] Папка {folder} не найдена, пропускаем загрузку вебхуков.")
|
||||
return []
|
||||
|
||||
for _finder, name, _ispkg in pkgutil.iter_modules([str(base_path)]):
|
||||
if not manager.should_autostart(name):
|
||||
logger.info(f"[Modules] Пропуск вебхуков модуля '{name}' (отключён).")
|
||||
continue
|
||||
|
||||
module_path = f"{folder}.{name}"
|
||||
try:
|
||||
router_module = importlib.import_module(f"{module_path}.router")
|
||||
@@ -47,22 +62,23 @@ def load_module_webhooks(folder: str = "modules") -> list[dict]:
|
||||
if isinstance(webhook_data, dict) and "path" in webhook_data and "handler" in webhook_data:
|
||||
webhooks.append(webhook_data)
|
||||
logger.info(f"[Modules] Найден вебхук в модуле {name}: {webhook_data['path']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Modules] Ошибка при загрузке вебхуков из {module_path}: {e}")
|
||||
|
||||
return webhooks
|
||||
|
||||
|
||||
def load_module_fast_flow_handlers(folder: str = "modules") -> dict:
|
||||
handlers = {}
|
||||
base_path = Path(folder)
|
||||
|
||||
if not base_path.exists():
|
||||
logger.warning(f"[Modules] Папка {folder} не найдена, пропускаем загрузку быстрого флоу.")
|
||||
return {}
|
||||
|
||||
for _finder, name, _ispkg in pkgutil.iter_modules([str(base_path)]):
|
||||
if not manager.should_autostart(name):
|
||||
logger.info(f"[Modules] Пропуск fast-flow модуля '{name}' (отключён).")
|
||||
continue
|
||||
|
||||
module_path = f"{folder}.{name}"
|
||||
try:
|
||||
router_module = importlib.import_module(f"{module_path}.router")
|
||||
@@ -80,8 +96,6 @@ def load_module_fast_flow_handlers(folder: str = "modules") -> dict:
|
||||
logger.info(f"[Modules] Найден обработчик быстрого флоу в модуле {name}: {payment_key}")
|
||||
elif fast_flow_data is None:
|
||||
logger.info(f"[Modules] Быстрое флоу отключено в модуле {name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"[Modules] Ошибка при загрузке быстрого флоу из {module_path}: {e}")
|
||||
|
||||
return handlers
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import Router
|
||||
|
||||
from hooks.hooks import unregister_module_hooks
|
||||
from logger import logger
|
||||
|
||||
|
||||
IGNORE_SUBMODULES = {"models", "schemas", "db"}
|
||||
STATE_FILE = os.getenv("MODULES_STATE_FILE", "storage/modules_state.json")
|
||||
|
||||
|
||||
class ModuleRecord:
|
||||
def __init__(self, name: str, pkg: str) -> None:
|
||||
self.name = name
|
||||
self.pkg = pkg
|
||||
self.router: Router | None = None
|
||||
self.enabled: bool = False
|
||||
|
||||
|
||||
class ModulesManager:
|
||||
def __init__(self, base: str = "modules") -> None:
|
||||
self.base = base
|
||||
self.registry: dict[str, ModuleRecord] = {}
|
||||
self.disabled: set[str] = set()
|
||||
self._load_state()
|
||||
|
||||
def pkg(self, name: str) -> str:
|
||||
return f"{self.base}.{name}"
|
||||
|
||||
def _load_state(self) -> None:
|
||||
try:
|
||||
if os.path.isfile(STATE_FILE):
|
||||
with open(STATE_FILE, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
self.disabled = set(data.get("disabled", []))
|
||||
else:
|
||||
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
||||
self._save_state()
|
||||
except Exception as e:
|
||||
logger.warning(f"[Modules] Не удалось загрузить состояние: {e}")
|
||||
|
||||
def _save_state(self) -> None:
|
||||
try:
|
||||
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
||||
with open(STATE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump({"disabled": sorted(self.disabled)}, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
logger.warning(f"[Modules] Не удалось сохранить состояние: {e}")
|
||||
|
||||
def adopt(self, name: str, router: Router):
|
||||
rec = self.registry.get(name) or ModuleRecord(name, self.pkg(name))
|
||||
rec.router = router
|
||||
rec.enabled = True
|
||||
self.registry[name] = rec
|
||||
|
||||
async def start(self, name: str) -> None:
|
||||
rec = self.registry.get(name) or ModuleRecord(name, self.pkg(name))
|
||||
if rec.enabled:
|
||||
logger.info(f"[Modules] {name} уже активен.")
|
||||
return
|
||||
|
||||
try:
|
||||
unregister_module_hooks(name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.purge_selective(rec.pkg)
|
||||
|
||||
mod = importlib.import_module(f"{rec.pkg}.router")
|
||||
router = getattr(mod, "router", None)
|
||||
if not isinstance(router, Router):
|
||||
raise RuntimeError(f"[Modules] В модуле {name} не найден router")
|
||||
|
||||
from utils.modules_loader import modules_hub
|
||||
|
||||
modules_hub.include_router(router)
|
||||
|
||||
rec.router = router
|
||||
rec.enabled = True
|
||||
self.registry[name] = rec
|
||||
|
||||
if name in self.disabled:
|
||||
self.disabled.discard(name)
|
||||
self._save_state()
|
||||
|
||||
logger.info(f"[Modules] {name} запущен.")
|
||||
|
||||
async def stop(self, name: str) -> None:
|
||||
rec = self.registry.get(name)
|
||||
if not rec or not rec.enabled:
|
||||
logger.info(f"[Modules] {name} уже остановлен или не найден.")
|
||||
if name not in self.disabled:
|
||||
self.disabled.add(name)
|
||||
self._save_state()
|
||||
return
|
||||
|
||||
try:
|
||||
unregister_module_hooks(name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from utils.modules_loader import modules_hub
|
||||
|
||||
sub = getattr(modules_hub, "_sub_routers", None) or getattr(modules_hub, "sub_routers", None)
|
||||
if sub and rec.router in sub:
|
||||
sub.remove(rec.router)
|
||||
|
||||
rec.router = None
|
||||
rec.enabled = False
|
||||
|
||||
if name not in self.disabled:
|
||||
self.disabled.add(name)
|
||||
self._save_state()
|
||||
|
||||
logger.info(f"[Modules] {name} остановлен.")
|
||||
|
||||
async def restart(self, name: str) -> None:
|
||||
logger.info(f"[Modules] Перезапуск {name}...")
|
||||
await self.stop(name)
|
||||
await self.start(name)
|
||||
|
||||
def purge_selective(self, root_pkg: str) -> None:
|
||||
to_del = []
|
||||
for m in list(sys.modules):
|
||||
if m == root_pkg or m.startswith(root_pkg + "."):
|
||||
tail = m[len(root_pkg) :].lstrip(".")
|
||||
top = tail.split(".", 1)[0] if tail else ""
|
||||
if top and top in IGNORE_SUBMODULES:
|
||||
continue
|
||||
to_del.append(m)
|
||||
for m in to_del:
|
||||
sys.modules.pop(m, None)
|
||||
importlib.invalidate_caches()
|
||||
|
||||
def is_enabled(self, name: str) -> bool:
|
||||
rec = self.registry.get(name)
|
||||
if not rec or not rec.router:
|
||||
return False
|
||||
try:
|
||||
from utils.modules_loader import modules_hub
|
||||
except Exception:
|
||||
return bool(rec.enabled)
|
||||
sub = getattr(modules_hub, "_sub_routers", None) or getattr(modules_hub, "sub_routers", None)
|
||||
return bool(sub and rec.router in sub)
|
||||
|
||||
def is_disabled(self, name: str) -> bool:
|
||||
return name in self.disabled
|
||||
|
||||
def should_autostart(self, name: str) -> bool:
|
||||
return name not in self.disabled
|
||||
|
||||
|
||||
manager = ModulesManager()
|
||||
Reference in New Issue
Block a user