diff --git a/cli_launcher.py b/cli_launcher.py old mode 100755 new mode 100644 index d9c2414c..ee677e36 --- a/cli_launcher.py +++ b/cli_launcher.py @@ -67,6 +67,18 @@ def is_ascii_only(value: str) -> bool: return all(ord(ch) < 128 for ch in value) +def _parse_tag_version(tag_name: str) -> tuple[int, ...]: + """Извлекает кортеж (major, minor, patch, ...) из тега для сортировки. v.5.1 -> (5, 1), v4 -> (4, 0).""" + s = tag_name.strip().lstrip("v.") + parts = [] + for part in re.split(r"[.\s]+", s): + try: + parts.append(int(part)) + except ValueError: + break + return tuple(parts) if parts else (0,) + + def warn_english_only(): """Предупреждение о необходимости английской раскладки.""" console.print("[red]Обнаружен ввод с неанглийской раскладкой.[/red]") @@ -508,8 +520,50 @@ def update_from_beta(): console.print("[green]Обновление с ветки dev завершено.[/green]") +def _do_update_to_tag(tag_name: str, update_buttons: bool, update_img: bool) -> None: + """Общая логика обновления до указанного тега (релиз или произвольный тег).""" + subprocess.run(["rm", "-rf", TEMP_DIR]) + subprocess.run( + ["git", "clone", "--branch", tag_name, "--depth", "1", GITHUB_REPO, TEMP_DIR], + check=True, + ) + + console.print("[red]Начинается перезапись файлов бота![/red]") + subprocess.run(["sudo", "rm", "-rf", os.path.join(PROJECT_DIR, "venv")]) + clean_project_dir_safe(update_buttons=update_buttons, update_img=update_img) + + exclude_options = "" + if not update_img: + exclude_options += "--exclude=img " + if not update_buttons: + exclude_options += "--exclude=handlers/buttons.py " + exclude_options += "--exclude=modules " + + rsync_cmd = ["rsync", "-a"] + exclude_options.split() + [f"{TEMP_DIR}/", f"{PROJECT_DIR}/"] + subprocess.run(rsync_cmd) + + modules_path = os.path.join(PROJECT_DIR, "modules") + if not os.path.exists(modules_path): + console.print("[yellow]Папка modules отсутствует — создаю вручную...[/yellow]") + try: + os.makedirs(modules_path, exist_ok=True) + console.print("[green]Папка modules успешно создана.[/green]") + except Exception as e: + console.print(f"[red]❌ Не удалось создать папку modules: {e}[/red]") + + if os.path.exists(os.path.join(TEMP_DIR, ".git")): + subprocess.run(["cp", "-r", os.path.join(TEMP_DIR, ".git"), PROJECT_DIR]) + + subprocess.run(["rm", "-rf", TEMP_DIR]) + + install_dependencies() + fix_permissions() + restart_service() + console.print(f"[green]Обновление до {tag_name} завершено.[/green]") + + def update_from_release(): - if not safe_confirm("[yellow]Подтвердите обновление Solobot до одного из последних релизов[/yellow]"): + if not safe_confirm("[yellow]Подтвердите обновление Solobot до релиза или патча[/yellow]"): return console.print("[red]ВНИМАНИЕ! Папка бота будет полностью перезаписана![/red]") @@ -525,65 +579,46 @@ def update_from_release(): install_rsync_if_needed() try: - response = requests.get("https://api.github.com/repos/Vladless/Solo_bot/releases", timeout=10) - releases = response.json()[:3] - tag_choices = [r["tag_name"] for r in releases] - - if not tag_choices: - raise ValueError("Не удалось получить список релизов") - - console.print("\n[bold green]Доступные релизы:[/bold green]") - for idx, tag in enumerate(tag_choices, 1): - console.print(f"[cyan]{idx}.[/cyan] {tag}") - - selected = safe_prompt( - "[bold blue]Выберите номер релиза[/bold blue]", - choices=[str(i) for i in range(1, len(tag_choices) + 1)], + rel_resp = requests.get( + "https://api.github.com/repos/Vladless/Solo_bot/releases", + timeout=10, ) - tag_name = tag_choices[int(selected) - 1] + releases = rel_resp.json() if rel_resp.status_code == 200 else [] + release_tag_names = {r["tag_name"] for r in releases} - if not safe_confirm(f"[yellow]Подтвердите установку релиза {tag_name}[/yellow]"): + tags_resp = requests.get( + "https://api.github.com/repos/Vladless/Solo_bot/tags", + params={"per_page": 50}, + timeout=10, + ) + if tags_resp.status_code != 200: + raise ValueError("Не удалось получить список тегов") + tags_data = tags_resp.json() + all_tag_names = [t["name"] for t in tags_data] + + tag_names = [name for name in all_tag_names if _parse_tag_version(name)[0] >= 4] + tag_names.sort(key=_parse_tag_version) + + if not tag_names: + raise ValueError("Нет доступных тегов (ожидаются версии начиная с 4)") + + console.print("\n[bold green]Релизы и патчи:[/bold green]") + for idx, name in enumerate(tag_names, 1): + label = " [dim](релиз)[/dim]" if name in release_tag_names else " [dim](патч)[/dim]" + console.print(f"[cyan]{idx}.[/cyan] {name}{label}") + + choices = [str(i) for i in range(1, len(tag_names) + 1)] + selected = safe_prompt( + "[bold blue]Выберите номер версии[/bold blue]", + choices=choices, + ) + tag_name = tag_names[int(selected) - 1] + + if not safe_confirm(f"[yellow]Установить {tag_name}?[/yellow]"): return - console.print(f"[cyan]Клонируем релиз {tag_name} во временную папку...[/cyan]") - subprocess.run(["rm", "-rf", TEMP_DIR]) - subprocess.run( - ["git", "clone", "--branch", tag_name, GITHUB_REPO, TEMP_DIR], - check=True, - ) - - console.print("[red]Начинается перезапись файлов бота![/red]") - subprocess.run(["sudo", "rm", "-rf", os.path.join(PROJECT_DIR, "venv")]) - clean_project_dir_safe(update_buttons=update_buttons, update_img=update_img) - - exclude_options = "" - if not update_img: - exclude_options += "--exclude=img " - if not update_buttons: - exclude_options += "--exclude=handlers/buttons.py " - exclude_options += "--exclude=modules " - - rsync_cmd = ["rsync", "-a"] + exclude_options.split() + [f"{TEMP_DIR}/", f"{PROJECT_DIR}/"] - subprocess.run(rsync_cmd) - - modules_path = os.path.join(PROJECT_DIR, "modules") - if not os.path.exists(modules_path): - console.print("[yellow]Папка modules отсутствует — создаю вручную...[/yellow]") - try: - os.makedirs(modules_path, exist_ok=True) - console.print("[green]Папка modules успешно создана.[/green]") - except Exception as e: - console.print(f"[red]❌ Не удалось создать папку modules: {e}[/red]") - - if os.path.exists(os.path.join(TEMP_DIR, ".git")): - subprocess.run(["cp", "-r", os.path.join(TEMP_DIR, ".git"), PROJECT_DIR]) - - subprocess.run(["rm", "-rf", TEMP_DIR]) - - install_dependencies() - fix_permissions() - restart_service() - console.print(f"[green]Обновление до релиза {tag_name} завершено.[/green]") + console.print(f"[cyan]Клонируем {tag_name} во временную папку...[/cyan]") + _do_update_to_tag(tag_name, update_buttons, update_img) except Exception as e: console.print(f"[red]❌ Ошибка при обновлении: {e}[/red]") @@ -599,7 +634,7 @@ def show_update_menu(): table.add_column("№", justify="center", style="cyan", no_wrap=True) table.add_column("Источник", style="white") table.add_row("1", "Обновить до BETA") - table.add_row("2", "Обновить/откатить до релиза") + table.add_row("2", "Обновить до релиза (релизы и патчи)") table.add_row("3", "Назад в меню") console.print(table) @@ -612,7 +647,7 @@ def show_update_menu(): def show_menu(): - table = Table(title="Solobot CLI v0.3.9", title_style="bold magenta", header_style="bold blue") + table = Table(title="Solobot CLI v0.4.0", 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)") diff --git a/handlers/keys/key_create.py b/handlers/keys/key_create.py index 3599ec8b..a90c998c 100644 --- a/handlers/keys/key_create.py +++ b/handlers/keys/key_create.py @@ -451,6 +451,7 @@ async def create_key( selected_device_limit: int | None = None, selected_traffic_gb: int | None = None, selected_price_rub: int | None = None, + skip_balance_charge: bool | None = None, ): from_user = message_or_query.from_user if isinstance(message_or_query, CallbackQuery | Message) else None if from_user: @@ -466,9 +467,12 @@ async def create_key( use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION)) - if state and any( - value is not None - for value in (selected_duration_days, selected_device_limit, selected_traffic_gb, selected_price_rub) + if state and ( + skip_balance_charge is not None + or any( + value is not None + for value in (selected_duration_days, selected_device_limit, selected_traffic_gb, selected_price_rub) + ) ): state_data = await state.get_data() if selected_duration_days is not None: @@ -479,6 +483,8 @@ async def create_key( state_data["config_selected_traffic_gb"] = selected_traffic_gb if selected_price_rub is not None: state_data["config_selected_price_rub"] = selected_price_rub + if skip_balance_charge is not None: + state_data["skip_balance_charge"] = skip_balance_charge await state.set_data(state_data) if use_country_selection: @@ -493,6 +499,7 @@ async def create_key( selected_device_limit=selected_device_limit, selected_traffic_gb=selected_traffic_gb, selected_price_rub=selected_price_rub, + skip_balance_charge=skip_balance_charge, ) else: await key_cluster_mode( @@ -505,4 +512,5 @@ async def create_key( selected_device_limit=selected_device_limit, selected_traffic_gb=selected_traffic_gb, selected_price_rub=selected_price_rub, + skip_balance_charge=skip_balance_charge, ) diff --git a/handlers/keys/key_mode/key_cluster_mode.py b/handlers/keys/key_mode/key_cluster_mode.py index c7c32408..03bd31a8 100644 --- a/handlers/keys/key_mode/key_cluster_mode.py +++ b/handlers/keys/key_mode/key_cluster_mode.py @@ -69,6 +69,7 @@ async def key_cluster_mode( selected_device_limit: int | None = None, selected_traffic_gb: int | None = None, selected_price_rub: int | None = None, + skip_balance_charge: bool | None = None, ): target_message = None safe_to_edit = False @@ -93,6 +94,7 @@ async def key_cluster_mode( try: data = await state.get_data() if state else {} is_trial = data.get("is_trial", False) + skip_balance_charge = bool(skip_balance_charge) if selected_device_limit is None: selected_device_limit = data.get("config_selected_device_limit") or data.get("selected_device_limit") @@ -182,7 +184,7 @@ async def key_cluster_mode( if trial_status in [0, -1]: await update_trial(session, tg_id, 1) - if price_to_charge: + if price_to_charge and not skip_balance_charge: await update_balance(session, tg_id, -int(price_to_charge)) except Exception as e: diff --git a/handlers/keys/key_mode/key_country_mode.py b/handlers/keys/key_mode/key_country_mode.py index b6a18862..b4327ef5 100644 --- a/handlers/keys/key_mode/key_country_mode.py +++ b/handlers/keys/key_mode/key_country_mode.py @@ -89,6 +89,7 @@ async def key_country_mode( selected_device_limit: int | None = None, selected_traffic_gb: int | None = None, selected_price_rub: int | None = None, + skip_balance_charge: bool | None = None, ): target_message = None safe_to_edit = False @@ -96,7 +97,10 @@ async def key_country_mode( if state and plan: await state.update_data(tariff_id=plan) - if state and any(value is not None for value in (selected_device_limit, selected_traffic_gb, selected_price_rub)): + if state and ( + skip_balance_charge is not None + or any(value is not None for value in (selected_device_limit, selected_traffic_gb, selected_price_rub)) + ): data = await state.get_data() if selected_device_limit is not None: data["config_selected_device_limit"] = selected_device_limit @@ -104,6 +108,8 @@ async def key_country_mode( data["config_selected_traffic_gb"] = selected_traffic_gb if selected_price_rub is not None: data["config_selected_price_rub"] = selected_price_rub + if skip_balance_charge is not None: + data["skip_balance_charge"] = skip_balance_charge await state.set_data(data) if isinstance(message_or_query, CallbackQuery) and message_or_query.message: @@ -499,6 +505,7 @@ async def finalize_key_creation( data = await state.get_data() if state else {} is_trial = data.get("is_trial", False) + skip_balance_charge = bool(data.get("skip_balance_charge", False)) selected_traffic_gb = data.get("config_selected_traffic_gb") if selected_traffic_gb is None: @@ -723,9 +730,12 @@ async def finalize_key_creation( trial_status = await get_trial(session, tg_id) if trial_status in [0, -1]: await update_trial(session, tg_id, 1) - if not is_trial and price_to_charge: + if not is_trial and price_to_charge and not skip_balance_charge: await update_balance(session, tg_id, -int(price_to_charge)) + if state: + await state.update_data(skip_balance_charge=False) + await session.commit() except Exception as e: diff --git a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so index f2dd0e9c..617334e1 100644 Binary files a/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so and b/handlers/payments/gift.cpython-312-x86_64-linux-gnu.so differ diff --git a/utils/versioning.py b/utils/versioning.py index ca5630c8..1953da05 100644 --- a/utils/versioning.py +++ b/utils/versioning.py @@ -92,4 +92,4 @@ def get_git_commit_number() -> str: def get_version() -> str: - return f"v.5.1.1 {get_git_commit_number()}" + return f"v.5.1.2 {get_git_commit_number()}"