module architecture/ formatting
This commit is contained in:
@@ -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
@@ -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,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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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}"))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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()}"
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user