up version and fix negative balance
This commit is contained in:
Executable → Regular
+93
-58
@@ -67,6 +67,18 @@ def is_ascii_only(value: str) -> bool:
|
|||||||
return all(ord(ch) < 128 for ch in value)
|
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():
|
def warn_english_only():
|
||||||
"""Предупреждение о необходимости английской раскладки."""
|
"""Предупреждение о необходимости английской раскладки."""
|
||||||
console.print("[red]Обнаружен ввод с неанглийской раскладкой.[/red]")
|
console.print("[red]Обнаружен ввод с неанглийской раскладкой.[/red]")
|
||||||
@@ -508,8 +520,50 @@ def update_from_beta():
|
|||||||
console.print("[green]Обновление с ветки dev завершено.[/green]")
|
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():
|
def update_from_release():
|
||||||
if not safe_confirm("[yellow]Подтвердите обновление Solobot до одного из последних релизов[/yellow]"):
|
if not safe_confirm("[yellow]Подтвердите обновление Solobot до релиза или патча[/yellow]"):
|
||||||
return
|
return
|
||||||
|
|
||||||
console.print("[red]ВНИМАНИЕ! Папка бота будет полностью перезаписана![/red]")
|
console.print("[red]ВНИМАНИЕ! Папка бота будет полностью перезаписана![/red]")
|
||||||
@@ -525,65 +579,46 @@ def update_from_release():
|
|||||||
install_rsync_if_needed()
|
install_rsync_if_needed()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get("https://api.github.com/repos/Vladless/Solo_bot/releases", timeout=10)
|
rel_resp = requests.get(
|
||||||
releases = response.json()[:3]
|
"https://api.github.com/repos/Vladless/Solo_bot/releases",
|
||||||
tag_choices = [r["tag_name"] for r in releases]
|
timeout=10,
|
||||||
|
|
||||||
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)],
|
|
||||||
)
|
)
|
||||||
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
|
return
|
||||||
|
|
||||||
console.print(f"[cyan]Клонируем релиз {tag_name} во временную папку...[/cyan]")
|
console.print(f"[cyan]Клонируем {tag_name} во временную папку...[/cyan]")
|
||||||
subprocess.run(["rm", "-rf", TEMP_DIR])
|
_do_update_to_tag(tag_name, update_buttons, update_img)
|
||||||
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]")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[red]❌ Ошибка при обновлении: {e}[/red]")
|
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("№", justify="center", style="cyan", no_wrap=True)
|
||||||
table.add_column("Источник", style="white")
|
table.add_column("Источник", style="white")
|
||||||
table.add_row("1", "Обновить до BETA")
|
table.add_row("1", "Обновить до BETA")
|
||||||
table.add_row("2", "Обновить/откатить до релиза")
|
table.add_row("2", "Обновить до релиза (релизы и патчи)")
|
||||||
table.add_row("3", "Назад в меню")
|
table.add_row("3", "Назад в меню")
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
@@ -612,7 +647,7 @@ def show_update_menu():
|
|||||||
|
|
||||||
|
|
||||||
def show_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("№", justify="center", style="cyan", no_wrap=True)
|
||||||
table.add_column("Операция", style="white")
|
table.add_column("Операция", style="white")
|
||||||
table.add_row("1", "Запустить бота (systemd)")
|
table.add_row("1", "Запустить бота (systemd)")
|
||||||
|
|||||||
@@ -451,6 +451,7 @@ async def create_key(
|
|||||||
selected_device_limit: int | None = None,
|
selected_device_limit: int | None = None,
|
||||||
selected_traffic_gb: int | None = None,
|
selected_traffic_gb: int | None = None,
|
||||||
selected_price_rub: 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
|
from_user = message_or_query.from_user if isinstance(message_or_query, CallbackQuery | Message) else None
|
||||||
if from_user:
|
if from_user:
|
||||||
@@ -466,9 +467,12 @@ async def create_key(
|
|||||||
|
|
||||||
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
|
use_country_selection = bool(MODES_CONFIG.get("COUNTRY_SELECTION_ENABLED", USE_COUNTRY_SELECTION))
|
||||||
|
|
||||||
if state and any(
|
if state and (
|
||||||
value is not None
|
skip_balance_charge is not None
|
||||||
for value in (selected_duration_days, selected_device_limit, selected_traffic_gb, selected_price_rub)
|
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()
|
state_data = await state.get_data()
|
||||||
if selected_duration_days is not None:
|
if selected_duration_days is not None:
|
||||||
@@ -479,6 +483,8 @@ async def create_key(
|
|||||||
state_data["config_selected_traffic_gb"] = selected_traffic_gb
|
state_data["config_selected_traffic_gb"] = selected_traffic_gb
|
||||||
if selected_price_rub is not None:
|
if selected_price_rub is not None:
|
||||||
state_data["config_selected_price_rub"] = selected_price_rub
|
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)
|
await state.set_data(state_data)
|
||||||
|
|
||||||
if use_country_selection:
|
if use_country_selection:
|
||||||
@@ -493,6 +499,7 @@ async def create_key(
|
|||||||
selected_device_limit=selected_device_limit,
|
selected_device_limit=selected_device_limit,
|
||||||
selected_traffic_gb=selected_traffic_gb,
|
selected_traffic_gb=selected_traffic_gb,
|
||||||
selected_price_rub=selected_price_rub,
|
selected_price_rub=selected_price_rub,
|
||||||
|
skip_balance_charge=skip_balance_charge,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await key_cluster_mode(
|
await key_cluster_mode(
|
||||||
@@ -505,4 +512,5 @@ async def create_key(
|
|||||||
selected_device_limit=selected_device_limit,
|
selected_device_limit=selected_device_limit,
|
||||||
selected_traffic_gb=selected_traffic_gb,
|
selected_traffic_gb=selected_traffic_gb,
|
||||||
selected_price_rub=selected_price_rub,
|
selected_price_rub=selected_price_rub,
|
||||||
|
skip_balance_charge=skip_balance_charge,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ async def key_cluster_mode(
|
|||||||
selected_device_limit: int | None = None,
|
selected_device_limit: int | None = None,
|
||||||
selected_traffic_gb: int | None = None,
|
selected_traffic_gb: int | None = None,
|
||||||
selected_price_rub: int | None = None,
|
selected_price_rub: int | None = None,
|
||||||
|
skip_balance_charge: bool | None = None,
|
||||||
):
|
):
|
||||||
target_message = None
|
target_message = None
|
||||||
safe_to_edit = False
|
safe_to_edit = False
|
||||||
@@ -93,6 +94,7 @@ async def key_cluster_mode(
|
|||||||
try:
|
try:
|
||||||
data = await state.get_data() if state else {}
|
data = await state.get_data() if state else {}
|
||||||
is_trial = data.get("is_trial", False)
|
is_trial = data.get("is_trial", False)
|
||||||
|
skip_balance_charge = bool(skip_balance_charge)
|
||||||
|
|
||||||
if selected_device_limit is None:
|
if selected_device_limit is None:
|
||||||
selected_device_limit = data.get("config_selected_device_limit") or data.get("selected_device_limit")
|
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]:
|
if trial_status in [0, -1]:
|
||||||
await update_trial(session, tg_id, 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))
|
await update_balance(session, tg_id, -int(price_to_charge))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ async def key_country_mode(
|
|||||||
selected_device_limit: int | None = None,
|
selected_device_limit: int | None = None,
|
||||||
selected_traffic_gb: int | None = None,
|
selected_traffic_gb: int | None = None,
|
||||||
selected_price_rub: int | None = None,
|
selected_price_rub: int | None = None,
|
||||||
|
skip_balance_charge: bool | None = None,
|
||||||
):
|
):
|
||||||
target_message = None
|
target_message = None
|
||||||
safe_to_edit = False
|
safe_to_edit = False
|
||||||
@@ -96,7 +97,10 @@ async def key_country_mode(
|
|||||||
if state and plan:
|
if state and plan:
|
||||||
await state.update_data(tariff_id=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()
|
data = await state.get_data()
|
||||||
if selected_device_limit is not None:
|
if selected_device_limit is not None:
|
||||||
data["config_selected_device_limit"] = selected_device_limit
|
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
|
data["config_selected_traffic_gb"] = selected_traffic_gb
|
||||||
if selected_price_rub is not None:
|
if selected_price_rub is not None:
|
||||||
data["config_selected_price_rub"] = selected_price_rub
|
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)
|
await state.set_data(data)
|
||||||
|
|
||||||
if isinstance(message_or_query, CallbackQuery) and message_or_query.message:
|
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 {}
|
data = await state.get_data() if state else {}
|
||||||
is_trial = data.get("is_trial", False)
|
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")
|
selected_traffic_gb = data.get("config_selected_traffic_gb")
|
||||||
if selected_traffic_gb is None:
|
if selected_traffic_gb is None:
|
||||||
@@ -723,9 +730,12 @@ async def finalize_key_creation(
|
|||||||
trial_status = await get_trial(session, tg_id)
|
trial_status = await get_trial(session, tg_id)
|
||||||
if trial_status in [0, -1]:
|
if trial_status in [0, -1]:
|
||||||
await update_trial(session, tg_id, 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))
|
await update_balance(session, tg_id, -int(price_to_charge))
|
||||||
|
|
||||||
|
if state:
|
||||||
|
await state.update_data(skip_balance_charge=False)
|
||||||
|
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Binary file not shown.
+1
-1
@@ -92,4 +92,4 @@ def get_git_commit_number() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def get_version() -> 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()}"
|
||||||
|
|||||||
Reference in New Issue
Block a user