Revert "Add internal squad management for users and trials"

This commit is contained in:
Egor
2025-12-08 04:17:05 +03:00
committed by GitHub
parent 42b67bdc47
commit 0892f494d9
14 changed files with 30 additions and 349 deletions

View File

@@ -91,7 +91,6 @@ class Settings(BaseSettings):
TRIAL_PAYMENT_ENABLED: bool = False
TRIAL_ACTIVATION_PRICE: int = 0
TRIAL_USER_TAG: Optional[str] = None
TRIAL_INTERNAL_SQUADS: Optional[str] = None
DEFAULT_TRAFFIC_LIMIT_GB: int = 100
DEFAULT_DEVICE_LIMIT: int = 1
DEFAULT_TRAFFIC_RESET_STRATEGY: str = "MONTH"
@@ -817,28 +816,6 @@ class Settings(BaseSettings):
def get_trial_user_tag(self) -> Optional[str]:
return self._normalize_user_tag(self.TRIAL_USER_TAG, "TRIAL_USER_TAG")
def get_trial_internal_squads(self) -> list[str]:
raw_value = self.TRIAL_INTERNAL_SQUADS
if raw_value is None:
return []
if isinstance(raw_value, str):
items = [item.strip() for item in re.split(r"[,\n]", raw_value) if item.strip()]
elif isinstance(raw_value, (list, tuple, set)):
items = [str(item).strip() for item in raw_value if str(item).strip()]
else:
return []
seen = set()
unique_items: list[str] = []
for item in items:
lowered = item.lower()
if lowered in seen:
continue
seen.add(lowered)
unique_items.append(item)
return unique_items
def get_paid_subscription_user_tag(self) -> Optional[str]:
return self._normalize_user_tag(
self.PAID_SUBSCRIPTION_USER_TAG,

View File

@@ -72,20 +72,6 @@ async def create_trial_subscription(
end_date = datetime.utcnow() + timedelta(days=duration_days)
trial_internal_squads = settings.get_trial_internal_squads()
trial_user: Optional[User] = None
if trial_internal_squads:
try:
trial_user = await db.get(User, user_id)
if trial_user is not None:
trial_user.active_internal_squads = trial_internal_squads
except Exception as error:
logger.warning(
"Не удалось применить internal squads для триала пользователя %s: %s",
user_id,
error,
)
subscription = Subscription(
user_id=user_id,
status=SubscriptionStatus.ACTIVE.value,
@@ -145,30 +131,10 @@ async def create_paid_subscription(
) -> Subscription:
end_date = datetime.utcnow() + timedelta(days=duration_days)
if device_limit is None:
device_limit = settings.DEFAULT_DEVICE_LIMIT
trial_internal_squads = settings.get_trial_internal_squads()
if trial_internal_squads:
try:
paid_user = await db.get(User, user_id)
if paid_user and paid_user.active_internal_squads:
current_set = {
str(item).strip().lower()
for item in paid_user.active_internal_squads
if str(item).strip()
}
trial_set = {item.lower() for item in trial_internal_squads}
if current_set == trial_set:
paid_user.active_internal_squads = []
except Exception as error:
logger.warning(
"Не удалось сбросить trial internal squads при покупке для пользователя %s: %s",
user_id,
error,
)
subscription = Subscription(
user_id=user_id,
status=SubscriptionStatus.ACTIVE.value,

View File

@@ -2,7 +2,7 @@ import logging
import secrets
import string
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Iterable
from typing import Optional, List, Dict
from sqlalchemy import select, and_, or_, func, case, nullslast, text
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload, joinedload
@@ -34,26 +34,6 @@ def generate_referral_code() -> str:
return f"ref{code_suffix}"
def _normalize_internal_squads(value: Optional[Iterable[str]]) -> Optional[list[str]]:
if value is None:
return None
try:
items = [str(item).strip() for item in value if str(item).strip()]
except TypeError:
return None
seen = set()
normalized: list[str] = []
for item in items:
lowered = item.lower()
if lowered in seen:
continue
seen.add(lowered)
normalized.append(item)
return normalized
async def get_user_by_id(db: AsyncSession, user_id: int) -> Optional[User]:
result = await db.execute(
select(User)
@@ -191,8 +171,7 @@ async def create_user_no_commit(
last_name: str = None,
language: str = "ru",
referred_by_id: int = None,
referral_code: str = None,
active_internal_squads: Optional[Iterable[str]] = None,
referral_code: str = None
) -> User:
"""
Создает пользователя без немедленного коммита для пакетной обработки
@@ -218,7 +197,6 @@ async def create_user_no_commit(
has_had_paid_subscription=False,
has_made_first_topup=False,
promo_group_id=promo_group_id,
active_internal_squads=_normalize_internal_squads(active_internal_squads),
)
db.add(user)
@@ -244,8 +222,7 @@ async def create_user(
last_name: str = None,
language: str = "ru",
referred_by_id: int = None,
referral_code: str = None,
active_internal_squads: Optional[Iterable[str]] = None,
referral_code: str = None
) -> User:
if not referral_code:
@@ -271,7 +248,6 @@ async def create_user(
has_had_paid_subscription=False,
has_made_first_topup=False,
promo_group_id=promo_group_id,
active_internal_squads=_normalize_internal_squads(active_internal_squads),
)
db.add(user)
@@ -319,8 +295,6 @@ async def update_user(
for field, value in kwargs.items():
if field in ("first_name", "last_name"):
value = sanitize_telegram_name(value)
if field == "active_internal_squads":
value = _normalize_internal_squads(value)
if hasattr(user, field):
setattr(user, field, value)

View File

@@ -600,7 +600,6 @@ class User(Base):
promo_offer_discount_source = Column(String(100), nullable=True)
promo_offer_discount_expires_at = Column(DateTime, nullable=True)
last_remnawave_sync = Column(DateTime, nullable=True)
active_internal_squads = Column(JSON, nullable=True)
trojan_password = Column(String(255), nullable=True)
vless_uuid = Column(String(255), nullable=True)
ss_password = Column(String(255), nullable=True)

View File

@@ -1489,40 +1489,6 @@ async def ensure_user_promo_offer_discount_columns():
return False
async def ensure_user_internal_squads_column() -> bool:
try:
column_exists = await check_column_exists('users', 'active_internal_squads')
if column_exists:
return True
async with engine.begin() as conn:
db_type = await get_database_type()
if db_type == 'sqlite':
column_def = 'JSON NULL'
elif db_type == 'postgresql':
column_def = 'JSONB NULL'
elif db_type == 'mysql':
column_def = 'JSON NULL'
else:
raise ValueError(f"Unsupported database type: {db_type}")
await conn.execute(
text(
f"ALTER TABLE users ADD COLUMN active_internal_squads {column_def}"
)
)
logger.info("✅ Добавлена колонка active_internal_squads в таблицу users")
return True
except Exception as error:
logger.error(
"Ошибка добавления колонки active_internal_squads в users: %s",
error,
)
return False
async def ensure_promo_offer_template_active_duration_column() -> bool:
try:
column_exists = await check_column_exists('promo_offer_templates', 'active_discount_hours')
@@ -4001,12 +3967,6 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Не удалось обновить пользовательские промо-скидки")
internal_squads_ready = await ensure_user_internal_squads_column()
if internal_squads_ready:
logger.info("✅ Колонка active_internal_squads для users готова")
else:
logger.warning("⚠️ Не удалось обновить колонку active_internal_squads для users")
effect_types_updated = await migrate_discount_offer_effect_types()
if effect_types_updated:
logger.info("✅ Типы эффектов промо-предложений обновлены")
@@ -4307,7 +4267,6 @@ async def check_migration_status():
"promo_offer_templates_active_discount_column": False,
"promo_offer_logs_table": False,
"subscription_temporary_access_table": False,
"users_active_internal_squads_column": False,
}
status["has_made_first_topup_column"] = await check_column_exists('users', 'has_made_first_topup')
@@ -4344,7 +4303,6 @@ async def check_migration_status():
status["users_promo_offer_discount_expires_column"] = await check_column_exists('users', 'promo_offer_discount_expires_at')
status["users_referral_commission_percent_column"] = await check_column_exists('users', 'referral_commission_percent')
status["subscription_crypto_link_column"] = await check_column_exists('subscriptions', 'subscription_crypto_link')
status["users_active_internal_squads_column"] = await check_column_exists('users', 'active_internal_squads')
media_fields_exist = (
await check_column_exists('broadcast_history', 'has_media') and

View File

@@ -4606,11 +4606,7 @@ async def admin_buy_subscription_execute(
username=target_user.username,
telegram_id=target_user.telegram_id
),
active_internal_squads=(
list(target_user.active_internal_squads or [])
if target_user.active_internal_squads is not None
else list(subscription.connected_squads or [])
),
active_internal_squads=subscription.connected_squads,
)
if hwid_limit is not None:
@@ -4636,11 +4632,7 @@ async def admin_buy_subscription_execute(
username=target_user.username,
telegram_id=target_user.telegram_id
),
active_internal_squads=(
list(target_user.active_internal_squads or [])
if target_user.active_internal_squads is not None
else list(subscription.connected_squads or [])
),
active_internal_squads=subscription.connected_squads,
)
if hwid_limit is not None:

View File

@@ -312,11 +312,7 @@ class MonitoringService:
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=(
list(user.active_internal_squads or [])
if user.active_internal_squads is not None
else list(subscription.connected_squads or [])
),
active_internal_squads=subscription.connected_squads,
)
if hwid_limit is not None:

View File

@@ -846,60 +846,33 @@ class RemnaWaveService:
except Exception as e:
logger.error(f"Error updating squad inbounds: {e}")
return False
@staticmethod
def _serialize_internal_squad(squad) -> Dict[str, Any]:
inbounds = [
asdict(inbound) if is_dataclass(inbound) else inbound
for inbound in getattr(squad, "inbounds", []) or []
]
return {
'uuid': getattr(squad, 'uuid', ''),
'name': getattr(squad, 'name', ''),
'members_count': getattr(squad, 'members_count', 0),
'inbounds_count': getattr(squad, 'inbounds_count', 0),
'inbounds': inbounds,
}
async def get_all_squads(self) -> List[Dict[str, Any]]:
try:
async with self.get_api_client() as api:
squads = await api.get_internal_squads()
result = []
for squad in squads:
result.append(self._serialize_internal_squad(squad))
inbounds = [
asdict(inbound) if is_dataclass(inbound) else inbound
for inbound in squad.inbounds or []
]
result.append({
'uuid': squad.uuid,
'name': squad.name,
'members_count': squad.members_count,
'inbounds_count': squad.inbounds_count,
'inbounds': inbounds,
})
logger.info(f"✅ Получено {len(result)} сквадов из Remnawave")
return result
except Exception as e:
logger.error(f"Ошибка получения сквадов из Remnawave: {e}")
return []
async def get_internal_squad(self, uuid: str) -> Optional[Dict[str, Any]]:
try:
async with self.get_api_client() as api:
squad = await api.get_internal_squad_by_uuid(uuid)
if not squad:
return None
return self._serialize_internal_squad(squad)
except Exception as error:
logger.error("Ошибка получения internal squad %s: %s", uuid, error)
return None
async def get_internal_squad_accessible_nodes(self, uuid: str) -> List[Dict[str, Any]]:
try:
async with self.get_api_client() as api:
nodes = await api.get_internal_squad_accessible_nodes(uuid)
return [
asdict(node) if is_dataclass(node) else node
for node in nodes or []
]
except Exception as error:
logger.error("Ошибка получения нод internal squad %s: %s", uuid, error)
return []
async def create_squad(self, name: str, inbounds: List[str]) -> Optional[str]:
try:
@@ -1731,12 +1704,6 @@ class RemnaWaveService:
telegram_id=user.telegram_id,
)
internal_squads = (
list(user.active_internal_squads or [])
if user.active_internal_squads is not None
else list(subscription.connected_squads or [])
)
create_kwargs = dict(
username=username,
expire_at=expire_at,
@@ -1749,7 +1716,7 @@ class RemnaWaveService:
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=internal_squads,
active_internal_squads=subscription.connected_squads,
)
if hwid_limit is not None:
@@ -1763,7 +1730,7 @@ class RemnaWaveService:
traffic_limit_bytes=create_kwargs['traffic_limit_bytes'],
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
description=create_kwargs['description'],
active_internal_squads=internal_squads,
active_internal_squads=subscription.connected_squads,
)
if hwid_limit is not None:

View File

@@ -139,63 +139,6 @@ class SubscriptionService:
return settings.get_paid_subscription_user_tag()
@staticmethod
def _normalize_internal_squads(value: Optional[Iterable[str]]) -> Optional[list[str]]:
if value is None:
return None
try:
items = [str(item).strip() for item in value if str(item).strip()]
except TypeError:
return None
seen = set()
normalized: list[str] = []
for item in items:
lowered = item.lower()
if lowered in seen:
continue
seen.add(lowered)
normalized.append(item)
return normalized
@staticmethod
def _select_internal_squads(user: User, subscription: Subscription) -> Optional[list[str]]:
if user.active_internal_squads is not None:
return SubscriptionService._normalize_internal_squads(user.active_internal_squads)
return SubscriptionService._normalize_internal_squads(subscription.connected_squads)
async def _resolve_internal_squad_uuids(
self,
api: RemnaWaveAPI,
squads: Optional[Iterable[str]],
) -> Optional[list[str]]:
normalized = self._normalize_internal_squads(squads)
if normalized is None:
return None
if not normalized:
return []
try:
available = await api.get_internal_squads()
uuid_lookup = {squad.uuid.lower(): squad.uuid for squad in available}
name_lookup = {squad.name.lower(): squad.uuid for squad in available}
except Exception as error:
logger.warning("Не удалось получить список internal squads: %s", error)
return normalized
resolved: list[str] = []
for item in normalized:
lowered = item.lower()
if lowered in uuid_lookup:
resolved.append(uuid_lookup[lowered])
continue
if lowered in name_lookup:
resolved.append(name_lookup[lowered])
continue
logger.warning("Не удалось сопоставить internal squad '%s' с панелью", item)
return resolved
@property
def is_configured(self) -> bool:
return self._config_error is None
@@ -232,24 +175,16 @@ class SubscriptionService:
if not user:
logger.error(f"Пользователь {subscription.user_id} не найден")
return None
validation_success = await self.validate_and_clean_subscription(db, subscription, user)
if not validation_success:
logger.error(f"Ошибка валидации подписки для пользователя {user.telegram_id}")
return None
user_tag = self._resolve_user_tag(subscription)
requested_internal_squads = self._select_internal_squads(user, subscription)
async with self.get_api_client() as api:
hwid_limit = resolve_hwid_device_limit_for_payload(subscription)
resolved_internal_squads = await self._resolve_internal_squad_uuids(
api,
requested_internal_squads,
)
internal_squads_payload = (
resolved_internal_squads if resolved_internal_squads is not None else []
)
existing_users = await api.get_user_by_telegram_id(user.telegram_id)
if existing_users:
logger.info(f"🔄 Найден существующий пользователь в панели для {user.telegram_id}")
@@ -272,7 +207,7 @@ class SubscriptionService:
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=internal_squads_payload,
active_internal_squads=subscription.connected_squads,
)
if user_tag is not None:
@@ -310,7 +245,7 @@ class SubscriptionService:
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=internal_squads_payload,
active_internal_squads=subscription.connected_squads,
)
if user_tag is not None:
@@ -378,17 +313,9 @@ class SubscriptionService:
logger.info(f"🔔 Статус подписки {subscription.id} автоматически изменен на 'expired'")
user_tag = self._resolve_user_tag(subscription)
requested_internal_squads = self._select_internal_squads(user, subscription)
async with self.get_api_client() as api:
hwid_limit = resolve_hwid_device_limit_for_payload(subscription)
resolved_internal_squads = await self._resolve_internal_squad_uuids(
api,
requested_internal_squads,
)
internal_squads_payload = (
resolved_internal_squads if resolved_internal_squads is not None else []
)
update_kwargs = dict(
uuid=user.remnawave_uuid,
@@ -401,7 +328,7 @@ class SubscriptionService:
username=user.username,
telegram_id=user.telegram_id
),
active_internal_squads=internal_squads_payload,
active_internal_squads=subscription.connected_squads,
)
if user_tag is not None:

View File

@@ -654,15 +654,6 @@ class BotConfigurationService:
"warning": "Неверный формат будет проигнорирован при создании пользователя.",
"dependencies": "Активация триала и включенная интеграция с RemnaWave",
},
"TRIAL_INTERNAL_SQUADS": {
"description": (
"Список internal squads, которые нужно назначать пользователям с триальной подпиской."
),
"format": "Укажите названия сквадов через запятую или с новой строки.",
"example": "Default, Trial Access",
"warning": "При оплате подписки эти сквады будут сброшены.",
"dependencies": "RemnaWave API и активированный триал",
},
"PAID_SUBSCRIPTION_USER_TAG": {
"description": (
"Тег, который бот ставит пользователю при покупке платной подписки в панели RemnaWave."

View File

@@ -14,8 +14,6 @@ from app.database.crud.server_squad import (
from ..dependencies import get_db_session, require_api_token
from ..schemas.remnawave import (
RemnaWaveConnectionStatus,
RemnaWaveAccessibleNode,
RemnaWaveAccessibleNodeListResponse,
RemnaWaveGenericSyncResponse,
RemnaWaveInboundsResponse,
RemnaWaveNode,
@@ -152,50 +150,8 @@ async def get_system_statistics(
if not stats or "system" not in stats:
raise HTTPException(status.HTTP_502_BAD_GATEWAY, "Не удалось получить статистику RemnaWave")
stats["last_updated"] = _parse_last_updated(stats.get("last_updated"))
return RemnaWaveSystemStatsResponse(**stats)
@router.get("/internal-squads", response_model=RemnaWaveSquadListResponse)
async def list_internal_squads(
_: Any = Security(require_api_token),
) -> RemnaWaveSquadListResponse:
service = _get_service()
_ensure_service_configured(service)
squads = await service.get_all_squads()
items = [RemnaWaveSquad(**squad) for squad in squads]
return RemnaWaveSquadListResponse(items=items, total=len(items))
@router.get("/internal-squads/{squad_uuid}", response_model=RemnaWaveSquad)
async def get_internal_squad(
squad_uuid: str,
_: Any = Security(require_api_token),
) -> RemnaWaveSquad:
service = _get_service()
_ensure_service_configured(service)
squad = await service.get_internal_squad(squad_uuid)
if not squad:
raise HTTPException(status.HTTP_404_NOT_FOUND, "Squad not found")
return RemnaWaveSquad(**squad)
@router.get(
"/internal-squads/{squad_uuid}/nodes",
response_model=RemnaWaveAccessibleNodeListResponse,
)
async def get_internal_squad_nodes(
squad_uuid: str,
_: Any = Security(require_api_token),
) -> RemnaWaveAccessibleNodeListResponse:
service = _get_service()
_ensure_service_configured(service)
nodes = await service.get_internal_squad_accessible_nodes(squad_uuid)
items = [RemnaWaveAccessibleNode(**node) for node in nodes]
return RemnaWaveAccessibleNodeListResponse(items=items, total=len(items))
stats["last_updated"] = _parse_last_updated(stats.get("last_updated"))
return RemnaWaveSystemStatsResponse(**stats)
@router.get("/nodes", response_model=RemnaWaveNodeListResponse)

View File

@@ -88,7 +88,6 @@ def _serialize_user(user: User) -> UserResponse:
created_at=user.created_at,
updated_at=user.updated_at,
last_activity=user.last_activity,
active_internal_squads=list(user.active_internal_squads or []),
promo_group=_serialize_promo_group(promo_group),
subscription=_serialize_subscription(subscription),
)
@@ -206,7 +205,6 @@ async def create_user_endpoint(
last_name=payload.last_name,
language=payload.language,
referred_by_id=payload.referred_by_id,
active_internal_squads=payload.active_internal_squads,
)
if payload.promo_group_id and payload.promo_group_id != user.promo_group_id:
@@ -265,9 +263,6 @@ async def update_user_endpoint(
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Promo group not found")
updates["promo_group_id"] = promo_group.id
if payload.active_internal_squads is not None:
updates["active_internal_squads"] = payload.active_internal_squads
if payload.referral_code is not None and payload.referral_code != found_user.referral_code:
existing_code_owner = await get_user_by_referral_code(db, payload.referral_code)
if existing_code_owner and existing_code_owner.id != found_user.id:

View File

@@ -137,20 +137,6 @@ class RemnaWaveSquadListResponse(BaseModel):
total: int
class RemnaWaveAccessibleNode(BaseModel):
uuid: str
node_name: str
country_code: str
config_profile_uuid: str
config_profile_name: str
active_inbounds: List[str] = Field(default_factory=list)
class RemnaWaveAccessibleNodeListResponse(BaseModel):
items: List[RemnaWaveAccessibleNode]
total: int
class RemnaWaveSquadCreateRequest(BaseModel):
name: str
inbound_uuids: List[str] = Field(default_factory=list)

View File

@@ -49,7 +49,6 @@ class UserResponse(BaseModel):
created_at: datetime
updated_at: datetime
last_activity: Optional[datetime] = None
active_internal_squads: List[str] = Field(default_factory=list)
promo_group: Optional[PromoGroupSummary] = None
subscription: Optional[SubscriptionSummary] = None
@@ -69,7 +68,6 @@ class UserCreateRequest(BaseModel):
language: str = "ru"
referred_by_id: Optional[int] = None
promo_group_id: Optional[int] = None
active_internal_squads: Optional[List[str]] = Field(default=None)
class UserUpdateRequest(BaseModel):
@@ -82,7 +80,6 @@ class UserUpdateRequest(BaseModel):
referral_code: Optional[str] = None
has_had_paid_subscription: Optional[bool] = None
has_made_first_topup: Optional[bool] = None
active_internal_squads: Optional[List[str]] = Field(default=None)
class BalanceUpdateRequest(BaseModel):