RemnaWave-bot Update
This commit is contained in:
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from app.config import PaymentPlanSettings, Settings
|
||||
from app.config import PAYMENT_PRODUCT_PLAN_SPECS, PaymentPlanSettings, Settings
|
||||
from app.db.base import utcnow
|
||||
from app.db.models import BotSetting
|
||||
|
||||
@@ -34,11 +34,14 @@ class BotConfigSnapshot:
|
||||
referral_discount_percent: int
|
||||
referral_bonus_days: int
|
||||
payment_plans_raw: str
|
||||
payment_product_plans_raw: str
|
||||
payment_transfer_text: str
|
||||
payment_support_text: str
|
||||
payment_plan_traffic_limit_gb: int
|
||||
payment_plan_traffic_reset_period: str
|
||||
payment_internal_squad_uuids_raw: str
|
||||
payment_vpn_squad_uuids_raw: str
|
||||
payment_white_squad_uuids_raw: str
|
||||
payment_external_squad_uuid: str
|
||||
payment_username_prefix: str
|
||||
payment_user_tag: str
|
||||
@@ -75,12 +78,15 @@ class BotConfigSnapshot:
|
||||
|
||||
@property
|
||||
def payment_internal_squad_uuids(self) -> list[str]:
|
||||
values: list[str] = []
|
||||
for raw_part in self.payment_internal_squad_uuids_raw.split(","):
|
||||
part = raw_part.strip()
|
||||
if part:
|
||||
values.append(part)
|
||||
return values
|
||||
return self._parse_csv_list(self.payment_internal_squad_uuids_raw)
|
||||
|
||||
@property
|
||||
def payment_vpn_squad_uuids(self) -> list[str]:
|
||||
return self._parse_csv_list(self.payment_vpn_squad_uuids_raw)
|
||||
|
||||
@property
|
||||
def payment_white_squad_uuids(self) -> list[str]:
|
||||
return self._parse_csv_list(self.payment_white_squad_uuids_raw)
|
||||
|
||||
@property
|
||||
def notification_days_list(self) -> list[int]:
|
||||
@@ -95,6 +101,13 @@ class BotConfigSnapshot:
|
||||
|
||||
@property
|
||||
def payment_plans(self) -> list[PaymentPlanSettings]:
|
||||
product_plans = self._parse_product_plans(
|
||||
self.payment_product_plans_raw,
|
||||
default_days=30,
|
||||
)
|
||||
if product_plans:
|
||||
return product_plans
|
||||
|
||||
values: list[PaymentPlanSettings] = []
|
||||
for raw_part in self.payment_plans_raw.split(","):
|
||||
part = raw_part.strip()
|
||||
@@ -120,6 +133,80 @@ class BotConfigSnapshot:
|
||||
|
||||
return sorted(values, key=lambda item: item.days)
|
||||
|
||||
@staticmethod
|
||||
def _parse_csv_list(raw_value: str) -> list[str]:
|
||||
values: list[str] = []
|
||||
for raw_part in raw_value.split(","):
|
||||
part = raw_part.strip()
|
||||
if part:
|
||||
values.append(part)
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def _parse_product_plans(
|
||||
raw_value: str,
|
||||
*,
|
||||
default_days: int,
|
||||
) -> list[PaymentPlanSettings]:
|
||||
values: list[PaymentPlanSettings] = []
|
||||
seen_codes: set[str] = set()
|
||||
|
||||
for raw_part in raw_value.split(","):
|
||||
part = raw_part.strip()
|
||||
if not part:
|
||||
continue
|
||||
|
||||
fragments = [fragment.strip() for fragment in part.split(":")]
|
||||
|
||||
# Legacy format: code:amount (2 fields)
|
||||
if len(fragments) == 2:
|
||||
code, raw_amount = fragments
|
||||
pairs = [(str(default_days), raw_amount)]
|
||||
# Standard format: code:days:amount (3 fields)
|
||||
elif len(fragments) == 3:
|
||||
code = fragments[0]
|
||||
pairs = [(fragments[1], fragments[2])]
|
||||
# Extended format: code:days1:amount1:days2:amount2:... (odd ≥ 5)
|
||||
elif len(fragments) >= 5 and len(fragments) % 2 == 1:
|
||||
code = fragments[0]
|
||||
pairs = [
|
||||
(fragments[i], fragments[i + 1])
|
||||
for i in range(1, len(fragments), 2)
|
||||
]
|
||||
else:
|
||||
continue
|
||||
|
||||
normalized_code = code.lower()
|
||||
spec = PAYMENT_PRODUCT_PLAN_SPECS.get(normalized_code)
|
||||
if spec is None or normalized_code in seen_codes:
|
||||
continue
|
||||
|
||||
title, description, squad_groups = spec
|
||||
for raw_days, raw_amount in pairs:
|
||||
if not raw_days.isdigit() or not raw_amount.isdigit():
|
||||
continue
|
||||
|
||||
days = int(raw_days)
|
||||
amount_rub = int(raw_amount)
|
||||
if days <= 0 or amount_rub <= 0:
|
||||
continue
|
||||
|
||||
plan_code = f"{normalized_code}_{days}"
|
||||
values.append(
|
||||
PaymentPlanSettings(
|
||||
code=plan_code,
|
||||
days=days,
|
||||
amount_rub=amount_rub,
|
||||
title=title,
|
||||
description=description,
|
||||
squad_groups=squad_groups,
|
||||
)
|
||||
)
|
||||
|
||||
seen_codes.add(normalized_code)
|
||||
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def _compact_text(raw_value: str, *, max_length: int = 80) -> str:
|
||||
normalized = " ".join(raw_value.split()).strip()
|
||||
@@ -176,6 +263,7 @@ class StaticBotConfigService:
|
||||
referral_discount_percent=self._settings.referral_discount_percent,
|
||||
referral_bonus_days=self._settings.referral_bonus_days,
|
||||
payment_plans_raw=self._settings.payment_plans_raw,
|
||||
payment_product_plans_raw=self._settings.payment_product_plans_raw,
|
||||
payment_transfer_text=self._settings.payment_transfer_text,
|
||||
payment_support_text=self._settings.payment_support_text,
|
||||
payment_plan_traffic_limit_gb=self._settings.payment_plan_traffic_limit_gb,
|
||||
@@ -184,6 +272,8 @@ class StaticBotConfigService:
|
||||
notification_days_before="1,3",
|
||||
notification_check_interval_hours=6,
|
||||
payment_internal_squad_uuids_raw=self._settings.payment_internal_squad_uuids_raw,
|
||||
payment_vpn_squad_uuids_raw=self._settings.payment_vpn_squad_uuids_raw,
|
||||
payment_white_squad_uuids_raw=self._settings.payment_white_squad_uuids_raw,
|
||||
payment_external_squad_uuid=self._settings.payment_external_squad_uuid,
|
||||
payment_username_prefix=self._settings.payment_username_prefix,
|
||||
payment_user_tag=self._settings.payment_user_tag,
|
||||
@@ -233,9 +323,24 @@ class BotConfigService(StaticBotConfigService):
|
||||
label="Тарифы и цены",
|
||||
section="pricing",
|
||||
prompt="Введите тарифы в формате `30:250,180:600,365:1000`.",
|
||||
description="Каждая пара — это `дни:цена_в_рублях`.",
|
||||
description="Каждая пара - это `дни:цена_в_рублях`.",
|
||||
placeholder="30:250,180:600,365:1000",
|
||||
),
|
||||
"payment_product_plans_raw": BotConfigFieldSpec(
|
||||
key="payment_product_plans_raw",
|
||||
label="Типы подписок и цены",
|
||||
section="pricing",
|
||||
prompt=(
|
||||
"Введите типы подписок в формате "
|
||||
"`vpn_white:30:450:180:2200:365:4000,white:30:250,vpn:30:200`.\n"
|
||||
"Каждый продукт: `код:дни1:цена1:дни2:цена2:...`"
|
||||
),
|
||||
description=(
|
||||
"Если заполнено, заменяет обычные PAYMENT_PLANS. "
|
||||
"Коды: vpn_white, white, vpn. Формат: `код:дни1:цена1[:дни2:цена2:...]`."
|
||||
),
|
||||
placeholder="vpn_white:30:450:180:2200:365:4000,white:30:250:180:1200,vpn:30:200:180:1000",
|
||||
),
|
||||
"payment_plan_traffic_limit_gb": BotConfigFieldSpec(
|
||||
key="payment_plan_traffic_limit_gb",
|
||||
label="Лимит трафика, GB",
|
||||
@@ -332,6 +437,22 @@ class BotConfigService(StaticBotConfigService):
|
||||
description="Эти группы назначаются пользователю в Remnawave.",
|
||||
placeholder="11111111-1111-1111-1111-111111111111",
|
||||
),
|
||||
"payment_vpn_squad_uuids_raw": BotConfigFieldSpec(
|
||||
key="payment_vpn_squad_uuids_raw",
|
||||
label="UUID VPN-групп",
|
||||
section="provisioning",
|
||||
prompt="Введите UUID squad для тарифа VPN через запятую.",
|
||||
description="Назначается тарифам `vpn` и `vpn_white`.",
|
||||
placeholder="11111111-1111-1111-1111-111111111111",
|
||||
),
|
||||
"payment_white_squad_uuids_raw": BotConfigFieldSpec(
|
||||
key="payment_white_squad_uuids_raw",
|
||||
label="UUID групп белых списков",
|
||||
section="provisioning",
|
||||
prompt="Введите UUID squad для белых списков через запятую.",
|
||||
description="Назначается тарифам `white` и `vpn_white`.",
|
||||
placeholder="22222222-2222-2222-2222-222222222222",
|
||||
),
|
||||
"payment_external_squad_uuid": BotConfigFieldSpec(
|
||||
key="payment_external_squad_uuid",
|
||||
label="UUID внешней группы",
|
||||
@@ -429,6 +550,12 @@ class BotConfigService(StaticBotConfigService):
|
||||
default=self._settings.referral_bonus_days,
|
||||
),
|
||||
payment_plans_raw=str(overrides.get("payment_plans_raw", self._settings.payment_plans_raw)),
|
||||
payment_product_plans_raw=str(
|
||||
overrides.get(
|
||||
"payment_product_plans_raw",
|
||||
self._settings.payment_product_plans_raw,
|
||||
)
|
||||
),
|
||||
payment_transfer_text=str(overrides.get("payment_transfer_text", self._settings.payment_transfer_text)),
|
||||
payment_support_text=str(overrides.get("payment_support_text", self._settings.payment_support_text)),
|
||||
payment_plan_traffic_limit_gb=self._parse_int(
|
||||
@@ -447,6 +574,18 @@ class BotConfigService(StaticBotConfigService):
|
||||
self._settings.payment_internal_squad_uuids_raw,
|
||||
)
|
||||
),
|
||||
payment_vpn_squad_uuids_raw=str(
|
||||
overrides.get(
|
||||
"payment_vpn_squad_uuids_raw",
|
||||
self._settings.payment_vpn_squad_uuids_raw,
|
||||
)
|
||||
),
|
||||
payment_white_squad_uuids_raw=str(
|
||||
overrides.get(
|
||||
"payment_white_squad_uuids_raw",
|
||||
self._settings.payment_white_squad_uuids_raw,
|
||||
)
|
||||
),
|
||||
payment_external_squad_uuid=str(
|
||||
overrides.get(
|
||||
"payment_external_squad_uuid",
|
||||
@@ -533,11 +672,17 @@ class BotConfigService(StaticBotConfigService):
|
||||
"bot_support_ticket_link",
|
||||
"payment_review_link",
|
||||
"bot_start_image_path",
|
||||
"payment_product_plans_raw",
|
||||
"payment_vpn_squad_uuids_raw",
|
||||
"payment_white_squad_uuids_raw",
|
||||
"payment_external_squad_uuid",
|
||||
"payment_user_tag",
|
||||
"payment_username_prefix",
|
||||
"payment_internal_squad_uuids_raw",
|
||||
"payment_vpn_squad_uuids_raw",
|
||||
"payment_white_squad_uuids_raw",
|
||||
"payment_plans_raw",
|
||||
"payment_product_plans_raw",
|
||||
"payment_plan_traffic_reset_period",
|
||||
"bot_brand_name",
|
||||
"notification_days_before",
|
||||
@@ -611,6 +756,20 @@ class BotConfigService(StaticBotConfigService):
|
||||
plans = self._parse_payment_plans(value)
|
||||
return ",".join(f"{plan.days}:{plan.amount_rub}" for plan in plans)
|
||||
|
||||
if key == "payment_product_plans_raw":
|
||||
plans = self._parse_product_plans(value)
|
||||
# Re-serialize: group plans by product code prefix
|
||||
product_groups: dict[str, list[PaymentPlanSettings]] = {}
|
||||
for plan in plans:
|
||||
# Extract product code from plan code (e.g. "vpn_white" from "vpn_white_30")
|
||||
product_code = "_".join(plan.code.split("_")[:-1]) if "_" in plan.code and plan.code.split("_")[-1].isdigit() else plan.code
|
||||
product_groups.setdefault(product_code, []).append(plan)
|
||||
parts = []
|
||||
for product_code, group_plans in product_groups.items():
|
||||
pairs = ":".join(f"{p.days}:{p.amount_rub}" for p in group_plans)
|
||||
parts.append(f"{product_code}:{pairs}")
|
||||
return ",".join(parts)
|
||||
|
||||
if key == "payment_plan_traffic_limit_gb":
|
||||
amount = self._parse_non_negative_int(value, "Лимит трафика")
|
||||
return str(amount)
|
||||
@@ -652,6 +811,12 @@ class BotConfigService(StaticBotConfigService):
|
||||
raise ValueError("Нужно указать хотя бы один UUID внутренней группы.")
|
||||
return ",".join(normalized_parts)
|
||||
|
||||
if key in {"payment_vpn_squad_uuids_raw", "payment_white_squad_uuids_raw"}:
|
||||
normalized_parts = [part.strip() for part in value.split(",") if part.strip()]
|
||||
if not normalized_parts:
|
||||
raise ValueError("Нужно указать хотя бы один UUID группы.")
|
||||
return ",".join(normalized_parts)
|
||||
|
||||
if key == "payment_external_squad_uuid":
|
||||
return value
|
||||
|
||||
@@ -711,6 +876,75 @@ class BotConfigService(StaticBotConfigService):
|
||||
raise ValueError(f"{label} должен быть целым неотрицательным числом.")
|
||||
return int(raw_value.strip())
|
||||
|
||||
@staticmethod
|
||||
def _parse_product_plans(raw_value: str) -> list[PaymentPlanSettings]:
|
||||
values: list[PaymentPlanSettings] = []
|
||||
seen_codes: set[str] = set()
|
||||
|
||||
for raw_part in raw_value.split(","):
|
||||
part = raw_part.strip()
|
||||
if not part:
|
||||
continue
|
||||
|
||||
fragments = [fragment.strip() for fragment in part.split(":")]
|
||||
|
||||
# Legacy: code:amount (2 fields)
|
||||
if len(fragments) == 2:
|
||||
code, raw_amount = fragments
|
||||
pairs = [("30", raw_amount)]
|
||||
# Standard: code:days:amount (3 fields)
|
||||
elif len(fragments) == 3:
|
||||
code = fragments[0]
|
||||
pairs = [(fragments[1], fragments[2])]
|
||||
# Extended: code:days1:amount1:days2:amount2:... (odd ≥ 5)
|
||||
elif len(fragments) >= 5 and len(fragments) % 2 == 1:
|
||||
code = fragments[0]
|
||||
pairs = [
|
||||
(fragments[i], fragments[i + 1])
|
||||
for i in range(1, len(fragments), 2)
|
||||
]
|
||||
else:
|
||||
raise ValueError(
|
||||
"Типы подписок: `код:дни1:цена1[:дни2:цена2:...]`, "
|
||||
"например `vpn_white:30:450:180:2200,white:30:250,vpn:30:200`."
|
||||
)
|
||||
|
||||
normalized_code = code.lower()
|
||||
spec = PAYMENT_PRODUCT_PLAN_SPECS.get(normalized_code)
|
||||
if spec is None:
|
||||
raise ValueError("Неизвестный код подписки. Доступны: vpn_white, white, vpn.")
|
||||
if normalized_code in seen_codes:
|
||||
raise ValueError("Коды подписок не должны повторяться.")
|
||||
|
||||
title, description, squad_groups = spec
|
||||
for raw_days, raw_amount in pairs:
|
||||
if not raw_days.isdigit() or not raw_amount.isdigit():
|
||||
raise ValueError("Дни и цена должны быть целыми положительными числами.")
|
||||
|
||||
days = int(raw_days)
|
||||
amount_rub = int(raw_amount)
|
||||
if days <= 0 or amount_rub <= 0:
|
||||
raise ValueError("У подписок дни и цена должны быть больше нуля.")
|
||||
|
||||
plan_code = f"{normalized_code}_{days}"
|
||||
values.append(
|
||||
PaymentPlanSettings(
|
||||
code=plan_code,
|
||||
days=days,
|
||||
amount_rub=amount_rub,
|
||||
title=title,
|
||||
description=description,
|
||||
squad_groups=squad_groups,
|
||||
)
|
||||
)
|
||||
|
||||
seen_codes.add(normalized_code)
|
||||
|
||||
if not values:
|
||||
raise ValueError("Нужно указать хотя бы один тип подписки.")
|
||||
|
||||
return values
|
||||
|
||||
@staticmethod
|
||||
def _parse_payment_plans(raw_value: str) -> list[PaymentPlanSettings]:
|
||||
values: list[PaymentPlanSettings] = []
|
||||
|
||||
Reference in New Issue
Block a user