diff --git a/api/routes/keys.py b/api/routes/keys.py
index 790d3be4..da06675e 100644
--- a/api/routes/keys.py
+++ b/api/routes/keys.py
@@ -75,7 +75,6 @@ async def edit_key_by_email(
session: AsyncSession = Depends(get_session),
admin: Admin = Depends(verify_admin_token),
):
- logger.info(f"[API] edit_key_by_email called for email={email}, payload={key_update.dict(exclude_unset=False)}")
result = await session.execute(select(Key).where(Key.email == email))
db_key = result.scalar_one_or_none()
@@ -96,7 +95,6 @@ async def edit_key_by_email(
try:
new_expiry_time = db_key.expiry_time
- logger.info(f"[API] renew_key_in_cluster new_expiry_time (ms) = {new_expiry_time}")
await renew_key_in_cluster(
cluster_id=db_key.server_id,
email=db_key.email,
@@ -123,7 +121,6 @@ async def create_key_api(
session: AsyncSession = Depends(get_session),
admin: Admin = Depends(verify_admin_token),
):
- logger.info(f"[API] Запрос на создание ключа: {payload.dict()}")
try:
await create_key_on_cluster(
diff --git a/bot.py b/bot.py
index dfc44c2c..d92f1080 100644
--- a/bot.py
+++ b/bot.py
@@ -1,6 +1,8 @@
import os
import subprocess
import traceback
+import time
+from functools import lru_cache
from aiogram import Bot, Dispatcher
from aiogram.client.default import DefaultBotProperties
@@ -21,59 +23,61 @@ storage = MemoryStorage()
dp = Dispatcher(bot=bot, storage=storage)
-def get_git_commit_number() -> str:
+_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()
-
+ 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()
- )
-
+ 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()
- )
-
- if describe.startswith("v") or "release" in describe.lower():
- branch = "main"
- else:
- branch = "dev"
+ 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 (команда sudo solobot): {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()
- )
+ 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 (
@@ -81,12 +85,28 @@ def get_git_commit_number() -> str:
f"#{local_number} / actual commit "
f'#{remote_number})'
)
-
- except Exception:
+ except Exception as e:
+ logger.error(f"[Git] Ошибка при получении удалённого коммита: {e}")
return "\n(Требуется обновление через CLI, команда sudo solobot)"
-version = f"v4.4-preRelease{get_git_commit_number()}"
+@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-preRelease{get_git_commit_number()}"
dp.message.filter(IsPrivateFilter())
diff --git a/cli_launcher.py b/cli_launcher.py
index 6d5bd7c6..7ac8cf4a 100755
--- a/cli_launcher.py
+++ b/cli_launcher.py
@@ -2,6 +2,7 @@ import os
import re
import subprocess
import sys
+import shutil
import requests
@@ -69,18 +70,51 @@ def backup_project():
console.print(f"[green]✅ Бэкап сохранён в: {BACK_DIR}[/green]")
+def auto_update_cli():
+ """Обновляет CLI, если отличается от последней версии. Перезапускает при необходимости."""
+ console.print("[yellow]🔄 Проверка обновлений CLI...[/yellow]")
+ try:
+ url = "https://raw.githubusercontent.com/Vladless/Solo_bot/dev/cli_launcher.py"
+ response = requests.get(url, timeout=10)
+ if response.status_code != 200:
+ console.print("[red]⚠️ Не удалось получить обновление CLI[/red]")
+ return
+
+ latest_text = response.text
+ current_path = os.path.realpath(__file__)
+ with open(current_path, encoding="utf-8") as f:
+ current_text = f.read()
+
+ if current_text != latest_text:
+ console.print("[green]🆕 Доступна новая версия CLI. Обновляю...[/green]")
+ with open(current_path, "w", encoding="utf-8") as f:
+ f.write(latest_text)
+ os.chmod(current_path, 0o755)
+ console.print("[green]✅ CLI обновлён. Перезапуск...[/green]")
+ os.execv(sys.executable, [sys.executable, current_path])
+ else:
+ console.print("[green]✅ CLI уже актуален[/green]")
+ except Exception as e:
+ console.print(f"[red]❌ Ошибка при автообновлении CLI: {e}[/red]")
+
+
def fix_permissions():
"""Устанавливает корректные права на файлы проекта"""
console.print("[yellow]🔧 Устанавливаю права на файлы...[/yellow]")
- try:
- user = os.getenv("SUDO_USER") or os.getenv("USER")
- if user:
- subprocess.run(["sudo", "chown", "-R", f"{user}:{user}", PROJECT_DIR], check=True)
+ if os.geteuid() != 0:
+ console.print("[yellow]⚠️ Некоторые действия могут требовать прав администратора (sudo)[/yellow]")
+
+ try:
+ stat_info = os.stat(PROJECT_DIR)
+ uid = stat_info.st_uid
+ user = subprocess.check_output(["id", "-nu", str(uid)], text=True).strip()
+
+ subprocess.run(["sudo", "chown", "-R", f"{user}:{user}", PROJECT_DIR], check=True)
subprocess.run(["sudo", "chmod", "-R", "u=rwX,go=rX", PROJECT_DIR], check=True)
- console.print("[green]✅ Права успешно установлены[/green]")
- except subprocess.CalledProcessError as e:
+ console.print(f"[green]✅ Права установлены для пользователя {user}[/green]")
+ except Exception as e:
console.print(f"[red]❌ Ошибка при установке прав: {e}[/red]")
@@ -138,19 +172,26 @@ def install_git_if_needed():
def install_dependencies():
console.print("[blue]🔧 Установка зависимостей...[/blue]")
+
+ python312_path = shutil.which("python3.12")
+ if not python312_path:
+ console.print("[red]❌ Не найден python3.12 в системе[/red]")
+ console.print("[yellow]📦 Установите Python 3.12: sudo apt install python3.12 python3.12-venv[/yellow]")
+ sys.exit(1)
+
with console.status("[bold green]Устанавливаются зависимости...[/bold green]"):
try:
if not os.path.exists("venv"):
- console.print("[yellow]⚠️ Виртуальное окружение не найдено. Создаю...[/yellow]")
- subprocess.run("python3 -m venv venv", shell=True, check=True)
+ console.print("[yellow]⚠️ Виртуальное окружение не найдено. Создаю через python3.12...[/yellow]")
+ subprocess.run(f"{python312_path} -m venv venv", shell=True, check=True)
subprocess.run(
"bash -c 'source venv/bin/activate && pip install -r requirements.txt'",
shell=True,
check=True,
)
- except subprocess.CalledProcessError:
- console.print("[red]❌ Ошибка при установке зависимостей.[/red]")
+ except subprocess.CalledProcessError as e:
+ console.print(f"[red]❌ Ошибка при установке зависимостей: {e}[/red]")
def restart_service():
@@ -189,7 +230,6 @@ def get_remote_version(branch="main"):
def update_from_beta():
- update_cli_launcher()
local_version = get_local_version()
remote_version = get_remote_version(branch="dev")
@@ -246,7 +286,6 @@ def update_from_beta():
def update_from_release():
- update_cli_launcher()
if not Confirm.ask("[yellow]🔁 Подтвердите обновление Solobot до одного из последних релизов[/yellow]"):
return
@@ -340,7 +379,7 @@ def show_update_menu():
def show_menu():
- table = Table(title="Solobot CLI v0.2.0", title_style="bold magenta", header_style="bold blue")
+ table = Table(title="Solobot CLI v0.2.7", 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)")
@@ -350,31 +389,13 @@ def show_menu():
table.add_row("5", "Показать логи (80 строк)")
table.add_row("6", "Показать статус")
table.add_row("7", "Обновить Solobot")
- table.add_row("8", "Обновить CLI лаунчер")
- table.add_row("9", "Выход")
+ table.add_row("8", "Выход")
console.print(table)
-def update_cli_launcher():
- """Обновляет CLI лаунчер с dev ветки"""
- console.print("[yellow]🔄 Обновление CLI лаунчера...[/yellow]")
- try:
- url = "https://raw.githubusercontent.com/Vladless/Solo_bot/dev/cli_launcher.py"
- response = requests.get(url, timeout=10)
-
- if response.status_code == 200:
- with open(os.path.join(PROJECT_DIR, "cli_launcher.py"), "w", encoding="utf-8") as f:
- f.write(response.text)
- console.print("[green]✅ CLI лаунчер успешно обновлён[/green]")
- os.chmod(os.path.join(PROJECT_DIR, "cli_launcher.py"), 0o755)
- else:
- console.print("[red]❌ Не удалось загрузить новый CLI[/red]")
- except Exception as e:
- console.print(f"[red]❌ Ошибка при обновлении CLI: {e}[/red]")
-
-
def main():
os.chdir(PROJECT_DIR)
+ auto_update_cli()
print_logo()
try:
while True:
@@ -424,11 +445,6 @@ def main():
elif choice == "7":
show_update_menu()
elif choice == "8":
- if Confirm.ask("[yellow]Обновить CLI лаунчер с dev ветки?[/yellow]"):
- update_cli_launcher()
- elif choice == "9":
- if Confirm.ask("[yellow]Хотите обновить CLI перед выходом?[/yellow]"):
- update_cli_launcher()
console.print("[bold cyan]Выход из CLI. Удачного дня![/bold cyan]")
break
except KeyboardInterrupt:
diff --git a/database/importer.py b/database/importer.py
index 717ae347..3dc8d5a0 100644
--- a/database/importer.py
+++ b/database/importer.py
@@ -8,6 +8,7 @@ from itertools import cycle
from sqlalchemy import select
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
+from config import USE_COUNTRY_SELECTION
from database.models import Key, Server, User
@@ -16,15 +17,23 @@ async def import_keys_from_3xui_db(db_path: str, session: AsyncSession) -> tuple
imported = 0
skipped = 0
- result = await session.execute(
- select(Server.cluster_name)
- .where(Server.enabled is True, Server.panel_type == "3x-ui", Server.cluster_name.isnot(None))
- .distinct()
- )
- clusters = [row[0] for row in result.fetchall()]
- if not clusters:
- raise RuntimeError("❌ Не найдено доступных кластеров для 3x-ui")
- cluster_cycle = cycle(clusters)
+ if USE_COUNTRY_SELECTION:
+ result = await session.execute(
+ select(Server.name)
+ .where(Server.enabled == True, Server.panel_type == "3x-ui")
+ )
+ else:
+ result = await session.execute(
+ select(Server.cluster_name)
+ .where(Server.enabled == True, Server.panel_type == "3x-ui", Server.cluster_name.isnot(None))
+ .distinct()
+ )
+
+ server_ids = [row[0] for row in result.fetchall()]
+ if not server_ids:
+ raise RuntimeError("❌ Не найдено доступных серверов или кластеров для 3x-ui")
+
+ server_cycle = cycle(server_ids)
try:
conn = sqlite3.connect(db_path)
@@ -58,7 +67,7 @@ async def import_keys_from_3xui_db(db_path: str, session: AsyncSession) -> tuple
email = c.get("email")
expiry_time = int(c.get("expiryTime") or now_ts)
created_at = now_ts
- server_id = next(cluster_cycle)
+ server_id = next(server_cycle)
if not tg_id or not client_id:
continue
@@ -112,4 +121,4 @@ async def import_keys_from_3xui_db(db_path: str, session: AsyncSession) -> tuple
continue
await session.commit()
- return imported, skipped
+ return imported, skipped
\ No newline at end of file
diff --git a/database/tariffs.py b/database/tariffs.py
index c7750868..871ba41c 100644
--- a/database/tariffs.py
+++ b/database/tariffs.py
@@ -130,7 +130,6 @@ async def check_tariff_exists(session: AsyncSession, tariff_id: int):
result = await session.execute(select(Tariff).where(Tariff.id == tariff_id, Tariff.is_active.is_(True)))
tariff = result.scalar_one_or_none()
if tariff:
- logger.info(f"[TARIFF] Тариф {tariff_id} найден в БД: {tariff.group_code}")
return True
logger.warning(f"[TARIFF] Тариф {tariff_id} не найден в БД")
return False
diff --git a/handlers/admin/management/keyboard.py b/handlers/admin/management/keyboard.py
index 491790cb..4954d9ca 100644
--- a/handlers/admin/management/keyboard.py
+++ b/handlers/admin/management/keyboard.py
@@ -28,13 +28,17 @@ def build_management_kb(admin_role: str) -> InlineKeyboardMarkup:
callback_data=AdminPanelCallback(action="restart").pack(),
)
builder.button(
- text="🌐 Сменить домен",
+ text="🌐 Сменить домен подписок",
callback_data=AdminPanelCallback(action="change_domain").pack(),
)
builder.button(
text="🔑 Восстановить пробники",
callback_data=AdminPanelCallback(action="restore_trials").pack(),
)
+ builder.button(
+ text="📤 Загрузить файл",
+ callback_data=AdminPanelCallback(action="upload_file").pack(),
+ )
maintenance_text = "🛠️ Выключить тех. работы" if maintenance.maintenance_mode else "🛠️ Включить тех. работы"
builder.button(
text=maintenance_text,
@@ -54,11 +58,11 @@ def build_database_kb() -> InlineKeyboardMarkup:
callback_data=AdminPanelCallback(action="backups").pack(),
)
builder.button(
- text="♻️ Восстановить БД",
+ text="♻️ Восстановить БД из бэкапа",
callback_data=AdminPanelCallback(action="restore_db").pack(),
)
builder.button(
- text="📤 Получить данные БД",
+ text="📤 Получить данные БД из панели",
callback_data=AdminPanelCallback(action="export_db").pack(),
)
builder.row(build_admin_back_btn())
diff --git a/handlers/admin/management/management_handler.py b/handlers/admin/management/management_handler.py
index 59492866..aa311d3d 100644
--- a/handlers/admin/management/management_handler.py
+++ b/handlers/admin/management/management_handler.py
@@ -54,6 +54,10 @@ class Import3xuiStates(StatesGroup):
waiting_for_file = State()
+class FileUploadState(StatesGroup):
+ waiting_for_file = State()
+
+
class DatabaseState(StatesGroup):
waiting_for_backup_file = State()
@@ -92,7 +96,6 @@ async def request_new_domain(callback_query: CallbackQuery, state: FSMContext):
async def process_new_domain(message: Message, state: FSMContext, session: AsyncSession):
"""Обновляет домен в таблице keys."""
new_domain = message.text.strip()
- logger.info(f"[DomainChange] Новый домен, введённый администратором: '{new_domain}'")
if not new_domain or " " in new_domain or not new_domain.replace(".", "").isalnum():
logger.warning("[DomainChange] Некорректный домен")
@@ -103,7 +106,6 @@ async def process_new_domain(message: Message, state: FSMContext, session: Async
return
new_domain_url = f"https://{new_domain}"
- logger.info(f"[DomainChange] Новый домен с протоколом: '{new_domain_url}'")
try:
stmt = (
@@ -326,8 +328,6 @@ async def restore_database(message: Message, state: FSMContext, bot: Bot):
if signature == b"PGDMP":
is_custom_dump = True
- logger.info(f"[Restore] Определён формат: {'custom' if is_custom_dump else 'plain'}")
-
subprocess.run(
[
"sudo",
@@ -645,6 +645,46 @@ async def handle_resync_after_import(callback: CallbackQuery, session: AsyncSess
await callback.message.edit_text(
f"🔁 Перевыпуск завершён:\n✅ Успешно: {success}\n❌ Ошибки: {failed}",
- parse_mode="HTML",
reply_markup=build_back_to_db_menu(),
)
+
+
+@router.callback_query(AdminPanelCallback.filter(F.action == "upload_file"))
+async def prompt_for_file_upload(callback: CallbackQuery, state: FSMContext):
+ await callback.message.edit_text(
+ "📤 Загрузка файла\n\n"
+ "Вы можете заменить файл в корневой директории бота.\n\n"
+ "📁 Отправьте файл с таким же именем и расширением, "
+ "как у уже существующего файла. Он будет автоматически заменён.",
+ reply_markup=build_admin_back_kb("management"),
+ )
+ await state.set_state(FileUploadState.waiting_for_file)
+
+
+
+@router.message(FileUploadState.waiting_for_file, F.document)
+async def handle_admin_file_upload(message: Message, state: FSMContext):
+ document = message.document
+ file_name = document.file_name
+
+ if not file_name or "." not in file_name:
+ await message.answer("❌ У файла должно быть имя с расширением.")
+ return
+
+ dest_path = os.path.abspath(f"./{file_name}")
+
+ try:
+ await message.bot.download(document, destination=dest_path)
+ await message.answer(
+ f"✅ Файл {file_name} успешно загружен и заменён.\n\n"
+ "🔄 Перезагрузите бота, чтобы изменения вступили в силу.",
+ reply_markup=build_admin_back_kb("management"),
+ )
+ except Exception as e:
+ logger.error(f"[Upload File] Ошибка при загрузке файла {file_name}: {e}")
+ await message.answer(
+ f"❌ Не удалось сохранить файл: {e}",
+ reply_markup=build_admin_back_kb("management"),
+ )
+ await state.clear()
+
diff --git a/handlers/admin/panel/panel_handler.py b/handlers/admin/panel/panel_handler.py
index a56ac18b..ed1759e3 100644
--- a/handlers/admin/panel/panel_handler.py
+++ b/handlers/admin/panel/panel_handler.py
@@ -6,7 +6,7 @@ from aiogram.types import CallbackQuery, Message
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
-from bot import version
+from bot import get_version
from database.models import Admin
from filters.admin import IsAdminFilter
from logger import logger
@@ -19,7 +19,7 @@ router = Router()
@router.callback_query(AdminPanelCallback.filter(F.action == "admin"), IsAdminFilter())
async def handle_admin_callback_query(callback_query: CallbackQuery, state: FSMContext, session: AsyncSession):
- text = f"🤖 Панель администратора\n\nВерсия бота:\n
{version}" + text = f"🤖 Панель администратора\n\nВерсия бота:\n
{get_version()}" await state.clear() @@ -60,7 +60,7 @@ async def handle_admin_callback_query_simple(callback_query: CallbackQuery, stat @router.message(Command("admin"), IsAdminFilter()) async def handle_admin_message(message: Message, state: FSMContext, session: AsyncSession): - text = f"🤖 Панель администратора\n\nВерсия бота:\n
{version}" + text = f"🤖 Панель администратора\n\nВерсия бота:\n
{get_version()}" await state.clear() diff --git a/handlers/keys/key_freeze.py b/handlers/keys/key_freeze.py index be275389..8b03fe32 100644 --- a/handlers/keys/key_freeze.py +++ b/handlers/keys/key_freeze.py @@ -98,7 +98,6 @@ async def process_callback_unfreeze_subscription_confirm(callback_query: Callbac now_ms = int(time.time() * 1000) leftover = record["expiry_time"] - logger.info(f"[Unfreeze Debug] expiry_time из БД: {leftover}") if leftover < 0: leftover = 0 new_expiry_time = now_ms + leftover diff --git a/handlers/profile.py b/handlers/profile.py index 46f13808..b33333b6 100644 --- a/handlers/profile.py +++ b/handlers/profile.py @@ -70,7 +70,6 @@ async def process_callback_view_profile( username = "Пользователь" image_path = os.path.join("img", "profile.jpg") - logger.info(f"Переход в профиль. Используется изображение: {image_path}") key_count = await get_key_count(session, chat_id) balance = await get_balance(session, chat_id) or 0 diff --git a/handlers/start.py b/handlers/start.py index 421221bf..b55d2044 100644 --- a/handlers/start.py +++ b/handlers/start.py @@ -300,7 +300,6 @@ async def handle_utm_link( async def show_start_menu(message: Message, admin: bool, session: AsyncSession): """Функция для отображения стандартного меню через редактирование сообщения.""" - logger.info(f"Показываю главное меню для пользователя {message.chat.id}") image_path = os.path.join("img", "pic.jpg") builder = InlineKeyboardBuilder() @@ -308,7 +307,6 @@ async def show_start_menu(message: Message, admin: bool, session: AsyncSession): trial_status = None if session is not None: trial_status = await get_trial(session, message.chat.id) - logger.info(f"Trial status для {message.chat.id}: {trial_status}") else: logger.warning(f"Сессия базы данных отсутствует, пропускаем проверку триала для {message.chat.id}")