Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3fc136d1e | |||
| eea908da36 | |||
| 04ff996d14 | |||
| c3dd90a9a9 |
Executable → Regular
+93
-58
@@ -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)")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -168,15 +168,25 @@ async def make_aggregated_link(
|
||||
if legacy_links_enabled:
|
||||
logger.info("[agg_link] LEGACY non-vless -> base link")
|
||||
return f"{base}/{email}/{tg_id}"
|
||||
|
||||
happ_cryptolink_enabled = bool(MODES_CONFIG.get("HAPP_CRYPTOLINK_ENABLED", HAPP_CRYPTOLINK))
|
||||
|
||||
if remna_link_override and (
|
||||
remna_link_override.lower().startswith("vless://") or remna_link_override.startswith(("http", "happ://"))
|
||||
):
|
||||
if not happ_cryptolink_enabled:
|
||||
logger.info("[agg_link] choose override Remnawave (non-vless)")
|
||||
return remna_link_override
|
||||
|
||||
best_vless, sub_url, happ_link = await _try_build_remna_vless(remna, email)
|
||||
if happ_link:
|
||||
logger.info("[agg_link] choose Remnawave cryptoLink (non-vless)")
|
||||
return happ_link
|
||||
if remna_link_override and (
|
||||
remna_link_override.lower().startswith("vless://") or remna_link_override.startswith(("http", "happ://"))
|
||||
):
|
||||
logger.info("[agg_link] choose override Remnawave (non-vless)")
|
||||
return remna_link_override
|
||||
if happ_link:
|
||||
logger.info("[agg_link] choose Remnawave cryptoLink (non-vless)")
|
||||
return happ_link
|
||||
kd = await get_key_details(session, email)
|
||||
stored = kd.get("remnawave_link") if kd else None
|
||||
if stored:
|
||||
|
||||
@@ -265,7 +265,7 @@ async def create_key_on_cluster(
|
||||
client_id=final_client_id,
|
||||
tg_id=tg_id,
|
||||
subgroup_code=subgroup_code,
|
||||
remna_link_override=remnawave_key,
|
||||
remna_link_override=remnawave_key or remnawave_link_value,
|
||||
plan=plan,
|
||||
)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+2
-2
@@ -23,7 +23,7 @@ from config import (
|
||||
)
|
||||
from core.bootstrap import BUTTONS_CONFIG, MODES_CONFIG
|
||||
from database import (
|
||||
add_user,
|
||||
upsert_user,
|
||||
get_coupon_by_code,
|
||||
get_user_snapshot,
|
||||
upsert_source_if_empty,
|
||||
@@ -172,7 +172,7 @@ async def process_start_logic(
|
||||
if gift_detected:
|
||||
return
|
||||
|
||||
await add_user(session=session, **user_data)
|
||||
await upsert_user(session=session, **user_data)
|
||||
|
||||
tl = (text or "").strip().lower()
|
||||
if tl == "trial":
|
||||
|
||||
+1
-1
@@ -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()}"
|
||||
|
||||
Reference in New Issue
Block a user