module architecture/ formatting

This commit is contained in:
Vladless
2025-07-31 23:42:45 +03:00
parent de61c04f4a
commit 1565ec4820
30 changed files with 261 additions and 148 deletions
+3 -91
View File
@@ -1,10 +1,5 @@
import os
import subprocess
import time
import traceback
from functools import lru_cache
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
@@ -17,6 +12,7 @@ from aiogram.utils.markdown import hbold
from config import ADMIN_ID, API_TOKEN
from filters.private import IsPrivateFilter
from logger import logger
from utils.modules_loader import load_modules_from_folder
bot = Bot(token=API_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
@@ -24,92 +20,8 @@ storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
_last_check_time = 0
_last_git_info = ""
def _get_git_commit_number_uncached() -> str:
repo_url = "https://github.com/Vladless/Solo_bot"
cwd = os.path.abspath(os.path.dirname(__file__))
if not os.path.isdir(os.path.join(cwd, ".git")):
cwd = "/root/Prod/Solo_bot"
logger.info(f"[Git] .git не найден в текущем каталоге, используем {cwd}")
env = os.environ.copy()
env["GIT_DIR"] = os.path.join(cwd, ".git")
env["GIT_WORK_TREE"] = cwd
try:
local_number = (
subprocess.check_output(["git", "rev-list", "--count", "HEAD"], cwd=cwd, env=env).decode().strip()
)
local_hash = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=cwd, env=env).decode().strip()
try:
branch = (
subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, env=env).decode().strip()
)
if branch == "HEAD":
describe = (
subprocess.check_output(
["git", "describe", "--tags", "--exact-match"],
cwd=cwd,
env=env,
stderr=subprocess.DEVNULL,
)
.decode()
.strip()
)
branch = "main" if describe.startswith("v") or "release" in describe.lower() else "dev"
except Exception:
branch = "dev"
except Exception as e:
logger.error(f"[Git] Ошибка при получении локального коммита: {e}")
return f"\n(Требуется обновление через CLI (команда <code>sudo solobot</code>): {e})"
try:
subprocess.check_output(["git", "fetch", "origin"], cwd=cwd, env=env)
remote_commit = subprocess.check_output(
["git", "ls-remote", "origin", f"refs/heads/{branch}"], cwd=cwd, env=env
).decode()
remote_hash = remote_commit.split()[0]
remote_number = (
subprocess.check_output(["git", "rev-list", "--count", remote_hash], cwd=cwd, env=env).decode().strip()
)
if local_hash == remote_hash:
logger.info("[Git] Локальная версия актуальна")
return "\n(Актуальная версия)"
return (
f'\n(commit <a href="{repo_url}/commit/{local_hash}">'
f"#{local_number}</a> / actual commit "
f'<a href="{repo_url}/commit/{remote_hash}">#{remote_number}</a>)'
)
except Exception as e:
logger.error(f"[Git] Ошибка при получении удалённого коммита: {e}")
return "\n(Требуется обновление через CLI, команда <code>sudo solobot</code>)"
@lru_cache(maxsize=1)
def _cached_git_info() -> str:
return _get_git_commit_number_uncached()
def get_git_commit_number() -> str:
global _last_check_time, _last_git_info
now = time.time()
if now - _last_check_time > 3600:
_last_check_time = now
_cached_git_info.cache_clear()
_last_git_info = _cached_git_info()
return _last_git_info
def get_version() -> str:
return f"v4.4-Release{get_git_commit_number()}"
for mod_router in load_modules_from_folder():
dp.include_router(mod_router)
dp.message.filter(IsPrivateFilter())
+38 -4
View File
@@ -1,3 +1,4 @@
import locale
import os
import re
import shutil
@@ -18,15 +19,39 @@ from rich.table import Table
from config import BOT_SERVICE
def ensure_utf8_locale():
try:
current_locale = locale.getlocale()
if current_locale and current_locale[1] == "UTF-8":
return
except Exception:
pass
console.print("[yellow]⏳ Проверка и установка локали UTF-8...[/yellow]")
os.environ["LC_ALL"] = "en_US.UTF-8"
os.environ["LANG"] = "en_US.UTF-8"
result = subprocess.run(["locale", "-a"], capture_output=True, text=True)
if "en_US.utf8" not in result.stdout.lower():
console.print("[blue]Добавляю локаль en_US.UTF-8 в систему...[/blue]")
try:
subprocess.run(["sudo", "locale-gen", "en_US.UTF-8"], check=True)
subprocess.run(["sudo", "update-locale", "LANG=en_US.UTF-8"], check=True)
console.print("[green]Локаль успешно установлена.[/green]")
except Exception as e:
console.print(f"[red]❌ Ошибка при установке локали: {e}[/red]")
else:
console.print("[green]Локаль UTF-8 уже доступна в системе.[/green]")
try:
sys.stdin.reconfigure(encoding="utf-8")
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
if not os.environ.get("LC_ALL", "").endswith("UTF-8"):
os.environ["LC_ALL"] = "en_US.UTF-8"
os.environ["LANG"] = "en_US.UTF-8"
ensure_utf8_locale()
console = Console()
@@ -155,8 +180,13 @@ def clean_project_dir_safe(update_buttons=False, update_img=False):
os.path.join(PROJECT_DIR, "config.py"),
os.path.join(PROJECT_DIR, "handlers", "texts.py"),
os.path.join(PROJECT_DIR, ".git"),
os.path.join(PROJECT_DIR, "modules"),
}
for root, _, files in os.walk(os.path.join(PROJECT_DIR, "modules")):
for file in files:
preserved_paths.add(os.path.join(root, file))
if not update_buttons:
preserved_paths.add(os.path.join(PROJECT_DIR, "handlers", "buttons.py"))
if not update_img:
@@ -183,6 +213,8 @@ def clean_project_dir_safe(update_buttons=False, update_img=False):
continue
if not update_img and os.path.abspath(dir_path) == os.path.join(PROJECT_DIR, "img"):
continue
if os.path.abspath(dir_path) == os.path.join(PROJECT_DIR, "modules"):
continue
try:
os.rmdir(dir_path)
except Exception:
@@ -306,6 +338,7 @@ def update_from_beta():
exclude_options += "--exclude=img "
if not update_buttons:
exclude_options += "--exclude=handlers/buttons.py "
exclude_options += "--exclude=modules "
subprocess.run(f"rsync -a {exclude_options} {TEMP_DIR}/ {PROJECT_DIR}/", shell=True)
@@ -374,6 +407,7 @@ def update_from_release():
exclude_options += "--exclude=img "
if not update_buttons:
exclude_options += "--exclude=handlers/buttons.py "
exclude_options += "--exclude=modules "
subprocess.run(f"rsync -a {exclude_options} {TEMP_DIR}/ {PROJECT_DIR}/", shell=True)
@@ -414,7 +448,7 @@ def show_update_menu():
def show_menu():
table = Table(title="Solobot CLI v0.2.8", title_style="bold magenta", header_style="bold blue")
table = Table(title="Solobot CLI v0.2.9", 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)")
+1 -1
View File
@@ -1,8 +1,8 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery
from backup import backup_database
from filters.admin import IsAdminFilter
from utils.backup import backup_database
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
+6 -2
View File
@@ -11,7 +11,6 @@ from py3xui import AsyncApi
from sqlalchemy import delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from backup import create_backup_and_send_to_admins
from config import (
ADMIN_PASSWORD,
ADMIN_USERNAME,
@@ -30,6 +29,7 @@ from handlers.keys.key_utils import (
)
from logger import logger
from panels.remnawave import RemnawaveAPI
from utils.backup import create_backup_and_send_to_admins
from ..panel.keyboard import AdminPanelCallback, build_admin_back_kb
from .keyboard import (
@@ -404,7 +404,11 @@ async def handle_cluster_availability(
country_code = node_info.get("country_code", "Unknown")
online_users = node_info.get("online_users", 0)
flag = "".join(chr(ord(c) + 127397) for c in country_code.upper()) if country_code != "Unknown" and len(country_code) == 2 else country_code
flag = (
"".join(chr(ord(c) + 127397) for c in country_code.upper())
if country_code != "Unknown" and len(country_code) == 2
else country_code
)
result_text += f"{flag} ({node_name}): {online_users} онлайн\n"
except Exception as e:
+11 -2
View File
@@ -5,6 +5,8 @@ from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from handlers.buttons import BACK, MAIN_MENU
from hooks.hooks import run_hooks
from logger import logger
class AdminPanelCallback(CallbackData, prefix="admin_panel"):
@@ -17,7 +19,7 @@ class AdminPanelCallback(CallbackData, prefix="admin_panel"):
super().__init__(**data)
def build_panel_kb(admin_role: str) -> InlineKeyboardMarkup:
async def build_panel_kb(admin_role: str) -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(
text="👤 Поиск пользователя",
@@ -27,7 +29,6 @@ def build_panel_kb(admin_role: str) -> InlineKeyboardMarkup:
text="🔑 Поиск по подписке",
callback_data=AdminPanelCallback(action="search_key").pack(),
)
builder.button(
text="🖥️ Управление серверами",
callback_data=AdminPanelCallback(action="clusters").pack(),
@@ -57,6 +58,14 @@ def build_panel_kb(admin_role: str) -> InlineKeyboardMarkup:
),
)
try:
buttons = await run_hooks("admin_panel", admin_role=admin_role)
for btn in buttons:
if btn:
builder.row(btn)
except Exception as e:
logger.error(f"[Hooks] Ошибка в admin_panel хуке: {e}")
builder.button(
text=MAIN_MENU,
callback_data="profile",
+3 -3
View File
@@ -6,10 +6,10 @@ from aiogram.types import CallbackQuery, Message
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from bot import get_version
from database.models import Admin
from filters.admin import IsAdminFilter
from logger import logger
from utils.versioning import get_version
from .keyboard import AdminPanelCallback, build_panel_kb
@@ -26,7 +26,7 @@ async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMC
result = await session.execute(select(Admin.role).where(Admin.tg_id == callback_query.from_user.id))
role = result.scalar_one_or_none() or "admin"
markup = build_panel_kb(admin_role=role)
markup = await build_panel_kb(admin_role=role)
if callback_query.message.text:
try:
@@ -69,6 +69,6 @@ async def handle_admin_message(message: Message, state: FSMContext, session: Asy
await message.answer(
text=text,
reply_markup=build_panel_kb(admin_role=role),
reply_markup=await build_panel_kb(admin_role=role),
disable_web_page_preview=True,
)
+3 -1
View File
@@ -63,7 +63,9 @@ def build_edit_server_fields_kb(server_name: str, server_data: dict) -> InlineKe
)
)
builder.row(InlineKeyboardButton(text="🔑 Inbound ID/Squads", callback_data=f"edit_server_field|{server_name}|inbound_id"))
builder.row(
InlineKeyboardButton(text="🔑 Inbound ID/Squads", callback_data=f"edit_server_field|{server_name}|inbound_id")
)
builder.row(InlineKeyboardButton(text="⚙️ Тип панели", callback_data=f"select_panel_type|{server_name}"))
+2 -9
View File
@@ -1158,15 +1158,8 @@ async def process_user_search(
else:
referrer_text = f"\n🤝 Пригласил: <b>{referrer_tg_id}</b>"
stmt = (
select(
func.count(Payment.id),
func.sum(Payment.amount)
)
.where(
Payment.status == "success",
Payment.tg_id == tg_id
)
stmt = select(func.count(Payment.id), func.sum(Payment.amount)).where(
Payment.status == "success", Payment.tg_id == tg_id
)
result = await session.execute(stmt)
topups_amount, topups_sum = result.one_or_none() or (0, 0.0)
@@ -23,7 +23,6 @@ from database import (
update_balance,
update_trial,
)
from handlers.buttons import (
CONNECT_DEVICE,
CONNECT_PHONE,
+1 -2
View File
@@ -30,12 +30,11 @@ from database import (
check_server_name_by_cluster,
check_user_exists,
get_key_details,
get_tariff_by_id,
get_trial,
update_balance,
update_trial,
get_tariff_by_id,
)
from database.models import Key, Server, Tariff
from handlers.buttons import BACK, CONNECT_DEVICE, CONNECT_PHONE, MAIN_MENU, MY_SUB, PC_BUTTON, SUPPORT, TV_BUTTON
from handlers.keys.key_utils import create_client_on_server
+3 -1
View File
@@ -93,7 +93,9 @@ async def handle_key_creation(
if not trial_tariffs:
await edit_or_send_message(
target_message=(
message_or_query.message if isinstance(message_or_query, CallbackQuery) else message_or_query
message_or_query.message
if isinstance(message_or_query, CallbackQuery)
else message_or_query
),
text="❌ Пробная подписка временно недоступна.",
reply_markup=InlineKeyboardBuilder()
+1 -4
View File
@@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import PUBLIC_LINK, REMNAWAVE_LOGIN, REMNAWAVE_PASSWORD, SUPERNODE
from database import delete_notification, get_servers, get_tariff_by_id, store_key
from database.models import Key, Server, Tariff
from handlers.utils import check_server_key_limit, get_least_loaded_cluster
from logger import logger
@@ -753,9 +752,7 @@ async def get_user_traffic(session: AsyncSession, tg_id: int, email: str) -> dic
result = await session.execute(
select(Server)
.where(Server.enabled.is_(True))
.where(
Server.server_name.in_(server_ids) | Server.cluster_name.in_(server_ids)
)
.where(Server.server_name.in_(server_ids) | Server.cluster_name.in_(server_ids))
)
server_rows = result.scalars().all()
if not server_rows:
+1 -1
View File
@@ -5,7 +5,7 @@ from aiogram.exceptions import TelegramBadRequest
from aiogram.types import CallbackQuery
from sqlalchemy.ext.asyncio import AsyncSession
from database import delete_key, get_key_details, get_servers
from database import delete_key, get_key_details
from handlers.buttons import APPLY, BACK, CANCEL
from handlers.keys.key_utils import delete_key_from_cluster, update_subscription
from handlers.keys.key_view import process_callback_view_key
@@ -46,7 +46,7 @@ from handlers.texts import (
KEY_EXPIRY_24H,
get_renewal_message,
)
from handlers.utils import format_days, format_hours, format_minutes, format_months, get_russian_month
from handlers.utils import format_hours, format_minutes, get_russian_month
from logger import logger
from .hot_leads_notifications import notify_hot_leads
@@ -69,7 +69,7 @@ async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker):
async with notification_lock:
try:
async with sessionmaker() as session:
logger.info("🔔 Запуск обработки уведомлений")
logger.info("Запуск обработки уведомлений")
current_time = int(datetime.now(moscow_tz).timestamp() * 1000)
threshold_10h = int((datetime.now(moscow_tz) + timedelta(hours=10)).timestamp() * 1000)
@@ -115,7 +115,7 @@ async def periodic_notifications(bot: Bot, *, sessionmaker: async_sessionmaker):
except Exception as e:
logger.error(f"Ошибка в notify_hot_leads: {e}")
logger.info("Уведомления завершены")
logger.info("Уведомления завершены")
except Exception as e:
logger.error(f"Ошибка в periodic_notifications: {e}")
@@ -19,7 +19,7 @@ from logger import logger
async def notify_hot_leads(bot: Bot, session: AsyncSession):
logger.info("🚀 Запуск уведомлений для горячих лидов.")
logger.info("Запуск уведомлений для горячих лидов.")
try:
leads = await get_hot_leads(session)
@@ -51,7 +51,7 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession):
result = await send_notification(bot, tg_id, None, HOT_LEAD_MESSAGE, keyboard)
if result:
await add_notification(session, tg_id, "hot_lead_step_2")
logger.info(f"🔥 Шаг 2 — отправлено первое уведомление: {tg_id}")
logger.info(f"Шаг 2 — отправлено первое уведомление: {tg_id}")
notified += 1
continue
@@ -97,7 +97,7 @@ async def notify_hot_leads(bot: Bot, session: AsyncSession):
logger.info(f"⚡ Шаг 3 — отправлено финальное уведомление: {tg_id}")
notified += 1
logger.info(f"Уведомления завершены. Отправлено: {notified}")
logger.info(f"Уведомления завершены. Отправлено: {notified}")
except Exception as e:
logger.error(f"❌ Ошибка в notify_hot_leads: {e}")
@@ -20,7 +20,6 @@ from database import (
mark_trial_extended,
update_key_notified,
)
from database.models import Key
from database.tariffs import get_tariffs
from handlers.buttons import CONNECT_DEVICE, CONNECT_PHONE, MAIN_MENU, PC_BUTTON, TV_BUTTON
from handlers.keys.key_utils import get_user_traffic
@@ -48,7 +47,7 @@ async def notify_inactive_trial_users(bot: Bot, session: AsyncSession):
if not trial_tariffs:
logger.error("[Notifications] Триальный тариф не найден")
return
trial_days = trial_tariffs[0]["duration_days"]
for user in users:
@@ -100,7 +99,7 @@ async def notify_inactive_trial_users(bot: Bot, session: AsyncSession):
await add_notification(session, msg["tg_id"], msg["notification_id"])
sent_count += 1
logger.info(f"Отправлено {sent_count} уведомлений неактивным пользователям.")
logger.info("Проверка пользователей с неактивным пробным периодом завершена.")
logger.info("Проверка пользователей с неактивным пробным периодом завершена.")
async def notify_users_no_traffic(bot: Bot, session: AsyncSession, current_time: int, keys: list):
+1 -1
View File
@@ -18,7 +18,7 @@ from config import (
from handlers.buttons import BACK, PAY_2, HELEKET_CRYPTO
from handlers.texts import (
HELEKET_CRYPTO_DESCRIPTION, HELEKET_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS, HELEKET_PAYMENT_TITLE
HELEKET_CRYPTO_DESCRIPTION, HELEKET_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS
)
from handlers.utils import edit_or_send_message
from logger import logger
+2 -2
View File
@@ -12,13 +12,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from config import (
KASSAI_ENABLE, KASSAI_API_KEY, KASSAI_SECRET_KEY, KASSAI_DOMAIN, KASSAI_SHOP_ID,
REDIRECT_LINK, FAIL_REDIRECT_LINK, WEBHOOK_HOST, KASSAI_IP, KASSAI_SUCCESS_URL, KASSAI_FAILURE_URL
KASSAI_IP, KASSAI_SUCCESS_URL, KASSAI_FAILURE_URL
)
from handlers.buttons import BACK, PAY_2, KASSAI_CARDS, KASSAI_SBP
from handlers.texts import (
KASSAI_CARDS_DESCRIPTION, KASSAI_SBP_DESCRIPTION,
KASSAI_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS, KASSAI_PAYMENT_TITLE
KASSAI_PAYMENT_MESSAGE, ENTER_SUM, PAYMENT_OPTIONS
)
from handlers.utils import edit_or_send_message
from logger import logger
+9
View File
@@ -29,6 +29,7 @@ from handlers.buttons import (
TRIAL_SUB,
)
from handlers.texts import ADD_SUBSCRIPTION_HINT
from hooks.hooks import run_hooks
from logger import logger
from .admin.panel.keyboard import AdminPanelCallback
@@ -101,6 +102,14 @@ async def process_callback_view_profile(
if row_buttons:
builder.row(*row_buttons)
try:
buttons = await run_hooks("profile_menu", chat_id=chat_id, admin=admin, session=session)
for btn in buttons:
if btn:
builder.row(btn)
except Exception as e:
logger.error(f"[Hooks] Ошибка в универсальном profile_menu хуке: {e}")
if INSTRUCTIONS_BUTTON:
builder.row(InlineKeyboardButton(text=INSTRUCTIONS, callback_data="instructions"))
if admin:
+3 -3
View File
@@ -20,7 +20,7 @@ from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from bot import bot
from config import ADMIN_ID, INLINE_MODE, REFERRAL_BONUS_PERCENTAGES, TOP_REFERRAL_BUTTON, USERNAME_BOT, REFERRAL_QR
from config import ADMIN_ID, INLINE_MODE, REFERRAL_BONUS_PERCENTAGES, REFERRAL_QR, TOP_REFERRAL_BUTTON, USERNAME_BOT
from database import (
add_referral,
add_user,
@@ -113,12 +113,12 @@ async def invite_handler(callback_query_or_message: Message | CallbackQuery, ses
@router.inline_query(F.query.in_(["referral", "ref", "invite"]))
async def inline_referral_handler(inline_query: InlineQuery, session: AsyncSession):
referral_link = f"https://t.me/{USERNAME_BOT}?start=referral_{inline_query.from_user.id}"
trial_tariffs = await get_tariffs(session, group_code="trial")
if not trial_tariffs:
await inline_query.answer(results=[], cache_time=0)
return
trial_days = trial_tariffs[0]["duration_days"]
trial_time_formatted = format_days(trial_days)
+4
View File
@@ -51,6 +51,7 @@ from handlers.texts import (
WELCOME_TEXT,
get_about_vpn,
)
from hooks.hooks import run_hooks
from logger import logger
from .admin.panel.keyboard import AdminPanelCallback
@@ -182,6 +183,9 @@ async def process_start_logic(
gift_detected = False
text_parts = text.split("-")
for part in text_parts:
await run_hooks("start_link", message=message, state=state, session=session, user_data=user_data, part=part)
for part in text_parts:
if "coupons" in part:
logger.info(f"Обнаружена ссылка на купон: {part}")
-7
View File
@@ -176,13 +176,6 @@ def format_hours(hours: int) -> str:
return f"{hours} {get_plural_form(hours, 'час', 'часа', 'часов')}"
def format_minutes(minutes: int) -> str:
"""Форматирует количество минут с правильным склонением"""
if minutes <= 0:
return "0 минут"
return f"{minutes} {get_plural_form(minutes, 'минута', 'минуты', 'минут')}"
async def edit_or_send_message(
target_message: Message,
text: str,
+33
View File
@@ -0,0 +1,33 @@
import inspect
from collections.abc import Awaitable, Callable
from typing import Any
from logger import logger
_hooks: dict[str, list[Callable[..., Any]]] = {}
def register_hook(name: str, func: Callable[..., Any]):
if name not in _hooks:
_hooks[name] = []
_hooks[name].append(func)
logger.info(f"[Hook] Зарегистрирован хук '{name}': {func.__name__}")
async def run_hooks(name: str, **kwargs) -> list[Any]:
results = []
for func in _hooks.get(name, []):
try:
if inspect.iscoroutinefunction(func):
result = await func(**kwargs)
else:
result = func(**kwargs)
if result:
results.append(result)
except Exception as e:
from logger import logger
logger.error(f"[HOOK:{name}] Ошибка в {func.__name__}: {e}")
return results
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -108,7 +108,7 @@ async def check_servers(session: AsyncSession):
server_info_list.append((server_name, server_host))
tasks.append(ping_server(server_host))
logger.info(f"🔍 Начинаем проверку {len(server_info_list)} серверов...")
logger.info(f"Начинаем проверку {len(server_info_list)} серверов...")
results = await asyncio.gather(*tasks, return_exceptions=True)
View File
+30
View File
@@ -0,0 +1,30 @@
import importlib
import pkgutil
from pathlib import Path
from aiogram import Router
from logger import logger
def load_modules_from_folder(folder: str = "modules") -> list[Router]:
routers = []
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)]):
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)
logger.info(f"[Modules] Загружен модуль: {module_path}")
else:
logger.warning(f"[Modules] В модуле {module_path} не найден router")
except Exception as e:
logger.error(f"[Modules] Ошибка при загрузке {module_path}: {e}")
return routers
+95
View File
@@ -0,0 +1,95 @@
import os
import subprocess
import time
from functools import lru_cache
from logger import logger
_last_check_time = 0
_last_git_info = ""
def _get_git_commit_number_uncached() -> str:
repo_url = "https://github.com/Vladless/Solo_bot"
cwd = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if not os.path.isdir(os.path.join(cwd, ".git")):
cwd = "/root/Solo_bot"
logger.info(f"[Git] .git не найден в текущем каталоге, используем {cwd}")
env = os.environ.copy()
env["GIT_DIR"] = os.path.join(cwd, ".git")
env["GIT_WORK_TREE"] = cwd
try:
local_number = (
subprocess.check_output(["git", "rev-list", "--count", "HEAD"], cwd=cwd, env=env).decode().strip()
)
local_hash = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=cwd, env=env).decode().strip()
try:
branch = (
subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=cwd, env=env).decode().strip()
)
if branch == "HEAD":
describe = (
subprocess.check_output(
["git", "describe", "--tags", "--exact-match"],
cwd=cwd,
env=env,
stderr=subprocess.DEVNULL,
)
.decode()
.strip()
)
branch = "main" if describe.startswith("v") or "release" in describe.lower() else "dev"
except Exception:
branch = "dev"
except Exception as e:
logger.error(f"[Git] Ошибка при получении локального коммита: {e}")
return f"\n(Требуется обновление через CLI (команда <code>sudo solobot</code>): {e})"
try:
subprocess.check_output(["git", "fetch", "origin"], cwd=cwd, env=env)
remote_commit = subprocess.check_output(
["git", "ls-remote", "origin", f"refs/heads/{branch}"], cwd=cwd, env=env
).decode()
remote_hash = remote_commit.split()[0]
remote_number = (
subprocess.check_output(["git", "rev-list", "--count", remote_hash], cwd=cwd, env=env).decode().strip()
)
if local_hash == remote_hash:
logger.info("[Git] Локальная версия актуальна")
return "\n(Актуальная версия)"
return (
f'\n(commit <a href="{repo_url}/commit/{local_hash}">'
f"#{local_number}</a> / actual commit "
f'<a href="{repo_url}/commit/{remote_hash}">#{remote_number}</a>)'
)
except Exception as e:
logger.error(f"[Git] Ошибка при получении удалённого коммита: {e}")
return "\n(Требуется обновление через CLI, команда <code>sudo solobot</code>)"
@lru_cache(maxsize=1)
def _cached_git_info() -> str:
return _get_git_commit_number_uncached()
def get_git_commit_number() -> str:
global _last_check_time, _last_git_info
now = time.time()
if now - _last_check_time > 3600:
_last_check_time = now
_cached_git_info.cache_clear()
_last_git_info = _cached_git_info()
return _last_git_info
def get_version() -> str:
return f"dev-mod{get_git_commit_number()}"
+1 -2
View File
@@ -3,9 +3,8 @@ import json
from aiohttp import web
from config import KASSAI_SECRET_KEY, KASSAI_SHOP_ID
from config import KASSAI_SECRET_KEY
from database import add_payment, async_session_maker, update_balance
from handlers.payments.kassai import verify_kassai_signature
from handlers.payments.utils import send_payment_success_notification
from logger import logger