4 Commits

Author SHA1 Message Date
Vladless f3fc136d1e update yoomoney webhook 2026-03-14 00:20:02 +03:00
Vladless eea908da36 up version and fix negative balance 2026-03-14 00:01:51 +03:00
Capybara-z 04ff996d14 fix update user info 2026-03-13 22:41:48 +03:00
Capybara-z c3dd90a9a9 fix resolve subscription link fallback logic 2026-03-13 02:30:51 +03:00
10 changed files with 136 additions and 71 deletions
Executable → Regular
+93 -58
View File
@@ -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)")
+11 -3
View File
@@ -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,
)
+3 -1
View File
@@ -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:
+12 -2
View File
@@ -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:
+13 -3
View File
@@ -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:
+1 -1
View File
@@ -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,
)
+2 -2
View File
@@ -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
View File
@@ -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()}"