mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-19 19:32:10 +00:00
Merge pull request #2127 from BEDOLAGA-DEV/xs3s5d-bedolaga/add-ability-to-assign-internal-squads
Support user-specific internal squads
This commit is contained in:
@@ -171,7 +171,8 @@ async def create_user_no_commit(
|
||||
last_name: str = None,
|
||||
language: str = "ru",
|
||||
referred_by_id: int = None,
|
||||
referral_code: str = None
|
||||
referral_code: str = None,
|
||||
active_internal_squads: Optional[List[str]] = None,
|
||||
) -> User:
|
||||
"""
|
||||
Создает пользователя без немедленного коммита для пакетной обработки
|
||||
@@ -197,6 +198,7 @@ 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=active_internal_squads,
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
@@ -222,7 +224,8 @@ async def create_user(
|
||||
last_name: str = None,
|
||||
language: str = "ru",
|
||||
referred_by_id: int = None,
|
||||
referral_code: str = None
|
||||
referral_code: str = None,
|
||||
active_internal_squads: Optional[List[str]] = None,
|
||||
) -> User:
|
||||
|
||||
if not referral_code:
|
||||
@@ -248,6 +251,7 @@ async def create_user(
|
||||
has_had_paid_subscription=False,
|
||||
has_made_first_topup=False,
|
||||
promo_group_id=promo_group_id,
|
||||
active_internal_squads=active_internal_squads,
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
|
||||
@@ -585,6 +585,7 @@ class User(Base):
|
||||
updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
|
||||
last_activity = Column(DateTime, default=func.now())
|
||||
remnawave_uuid = Column(String(255), nullable=True, unique=True)
|
||||
active_internal_squads = Column(JSON, nullable=True)
|
||||
broadcasts = relationship("BroadcastHistory", back_populates="admin")
|
||||
referrals = relationship("User", backref="referrer", remote_side=[id], foreign_keys="User.referred_by_id")
|
||||
subscription = relationship("Subscription", back_populates="user", uselist=False)
|
||||
|
||||
@@ -3650,6 +3650,36 @@ async def add_promo_group_priority_column() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def add_user_active_internal_squads_column() -> bool:
|
||||
"""Добавляет колонку active_internal_squads в таблицу users."""
|
||||
column_exists = await check_column_exists('users', 'active_internal_squads')
|
||||
if column_exists:
|
||||
logger.info("Колонка active_internal_squads уже существует в users")
|
||||
return True
|
||||
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
db_type = await get_database_type()
|
||||
|
||||
if db_type == 'sqlite':
|
||||
column_def = 'JSON'
|
||||
elif db_type == 'postgresql':
|
||||
column_def = 'JSONB'
|
||||
else:
|
||||
column_def = 'JSON'
|
||||
|
||||
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(f"Ошибка добавления колонки active_internal_squads: {error}")
|
||||
return False
|
||||
|
||||
|
||||
async def create_user_promo_groups_table() -> bool:
|
||||
"""Создает таблицу user_promo_groups для связи Many-to-Many между users и promo_groups."""
|
||||
table_exists = await check_table_exists("user_promo_groups")
|
||||
@@ -3993,6 +4023,13 @@ async def run_universal_migration():
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с добавлением priority в promo_groups")
|
||||
|
||||
logger.info("=== ДОБАВЛЕНИЕ INTERNAL SQUADS ДЛЯ USERS ===")
|
||||
internal_squads_ready = await add_user_active_internal_squads_column()
|
||||
if internal_squads_ready:
|
||||
logger.info("✅ Колонка active_internal_squads в users готова")
|
||||
else:
|
||||
logger.warning("⚠️ Проблемы с добавлением active_internal_squads в users")
|
||||
|
||||
logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ USER_PROMO_GROUPS ===")
|
||||
user_promo_groups_ready = await create_user_promo_groups_table()
|
||||
if user_promo_groups_ready:
|
||||
|
||||
@@ -32,6 +32,7 @@ from app.services.admin_notification_service import AdminNotificationService
|
||||
from app.database.crud.promo_group import get_promo_groups_with_counts
|
||||
from app.utils.decorators import admin_required, error_handler
|
||||
from app.utils.formatters import format_datetime, format_time_ago
|
||||
from app.utils.internal_squads import resolve_user_internal_squads
|
||||
from app.utils.user_utils import get_effective_referral_commission_percent
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
from app.external.remnawave_api import TrafficLimitStrategy
|
||||
@@ -3401,7 +3402,7 @@ async def _show_servers_for_user(
|
||||
user = await get_user_by_id(db, user_id)
|
||||
current_squads = []
|
||||
if user and user.subscription:
|
||||
current_squads = user.subscription.connected_squads or []
|
||||
current_squads = resolve_user_internal_squads(user, user.subscription)
|
||||
|
||||
all_servers, _ = await get_all_server_squads(db, available_only=False)
|
||||
|
||||
@@ -3490,18 +3491,19 @@ async def toggle_user_server(
|
||||
if not server:
|
||||
await callback.answer("❌ Сервер не найден", show_alert=True)
|
||||
return
|
||||
|
||||
|
||||
subscription = user.subscription
|
||||
current_squads = list(subscription.connected_squads or [])
|
||||
|
||||
current_squads = resolve_user_internal_squads(user, subscription)
|
||||
|
||||
if server.squad_uuid in current_squads:
|
||||
current_squads.remove(server.squad_uuid)
|
||||
action_text = "удален"
|
||||
else:
|
||||
current_squads.append(server.squad_uuid)
|
||||
action_text = "добавлен"
|
||||
|
||||
|
||||
subscription.connected_squads = current_squads
|
||||
user.active_internal_squads = current_squads
|
||||
subscription.updated_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(subscription)
|
||||
@@ -4590,8 +4592,10 @@ async def admin_buy_subscription_execute(
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
from app.external.remnawave_api import UserStatus, TrafficLimitStrategy
|
||||
remnawave_service = RemnaWaveService()
|
||||
|
||||
|
||||
hwid_limit = resolve_hwid_device_limit_for_payload(subscription)
|
||||
active_squads = resolve_user_internal_squads(target_user, subscription)
|
||||
target_user.active_internal_squads = active_squads
|
||||
|
||||
if target_user.remnawave_uuid:
|
||||
async with remnawave_service.get_api_client() as api:
|
||||
@@ -4606,7 +4610,7 @@ async def admin_buy_subscription_execute(
|
||||
username=target_user.username,
|
||||
telegram_id=target_user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=active_squads,
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
@@ -4632,7 +4636,7 @@ async def admin_buy_subscription_execute(
|
||||
username=target_user.username,
|
||||
telegram_id=target_user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=active_squads,
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
|
||||
@@ -38,6 +38,7 @@ from app.database.crud.user import (
|
||||
subtract_user_balance,
|
||||
cleanup_expired_promo_offer_discounts,
|
||||
)
|
||||
from app.utils.internal_squads import resolve_user_internal_squads
|
||||
from app.utils.timezone import format_local_datetime
|
||||
from app.utils.subscription_utils import (
|
||||
resolve_hwid_device_limit_for_payload,
|
||||
@@ -312,7 +313,9 @@ class MonitoringService:
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=resolve_user_internal_squads(
|
||||
user, subscription
|
||||
),
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
|
||||
@@ -42,6 +42,7 @@ from app.database.models import (
|
||||
from app.utils.subscription_utils import (
|
||||
resolve_hwid_device_limit_for_payload,
|
||||
)
|
||||
from app.utils.internal_squads import resolve_user_internal_squads
|
||||
from app.utils.timezone import get_local_timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -182,6 +183,70 @@ class RemnaWaveService:
|
||||
self._config_error or "RemnaWave API не настроен"
|
||||
)
|
||||
|
||||
async def resolve_internal_squad_uuids(self, identifiers: List[str]) -> List[str]:
|
||||
"""Конвертирует названия/UUID сквадов в UUID через панель RemnaWave."""
|
||||
|
||||
if not identifiers:
|
||||
return []
|
||||
|
||||
cleaned: List[str] = []
|
||||
seen = set()
|
||||
|
||||
for ident in identifiers:
|
||||
if not ident:
|
||||
continue
|
||||
|
||||
value = str(ident).strip()
|
||||
if not value or value in seen:
|
||||
continue
|
||||
|
||||
seen.add(value)
|
||||
cleaned.append(value)
|
||||
|
||||
if not cleaned:
|
||||
return []
|
||||
|
||||
try:
|
||||
async with self.get_api_client() as api:
|
||||
squads = await api.get_internal_squads()
|
||||
except Exception as error:
|
||||
logger.error("❌ Не удалось получить список Internal Squads: %s", error)
|
||||
return []
|
||||
|
||||
name_to_uuid = {squad.name.lower(): squad.uuid for squad in squads}
|
||||
uuid_set = {squad.uuid for squad in squads}
|
||||
|
||||
resolved: List[str] = []
|
||||
for value in cleaned:
|
||||
if value in uuid_set:
|
||||
resolved.append(value)
|
||||
continue
|
||||
|
||||
mapped = name_to_uuid.get(value.lower())
|
||||
if mapped:
|
||||
resolved.append(mapped)
|
||||
else:
|
||||
logger.warning("⚠️ Неизвестный Internal Squad: %s", value)
|
||||
|
||||
return resolved
|
||||
|
||||
async def normalize_active_internal_squads(self, active_squads: List[Any]) -> List[str]:
|
||||
"""Приводит входные данные о сквадах (имя/UUID/словарь) к списку UUID."""
|
||||
|
||||
identifiers: List[str] = []
|
||||
|
||||
for squad in active_squads or []:
|
||||
identifier = None
|
||||
if isinstance(squad, dict):
|
||||
identifier = squad.get('uuid') or squad.get('name')
|
||||
else:
|
||||
identifier = squad
|
||||
|
||||
if identifier:
|
||||
identifiers.append(str(identifier))
|
||||
|
||||
return await self.resolve_internal_squad_uuids(identifiers)
|
||||
|
||||
def _ensure_user_remnawave_uuid(
|
||||
self,
|
||||
user: "User",
|
||||
@@ -1017,6 +1082,7 @@ class RemnaWaveService:
|
||||
uuid=subscription.user.remnawave_uuid,
|
||||
active_internal_squads=new_squads,
|
||||
)
|
||||
subscription.user.active_internal_squads = new_squads
|
||||
panel_updated += 1
|
||||
except Exception as error:
|
||||
panel_failed += 1
|
||||
@@ -1506,14 +1572,9 @@ class RemnaWaveService:
|
||||
traffic_used_gb = used_traffic_bytes / (1024**3)
|
||||
|
||||
active_squads = panel_user.get('activeInternalSquads', [])
|
||||
squad_uuids = []
|
||||
if isinstance(active_squads, list):
|
||||
for squad in active_squads:
|
||||
if isinstance(squad, dict) and 'uuid' in squad:
|
||||
squad_uuids.append(squad['uuid'])
|
||||
elif isinstance(squad, str):
|
||||
squad_uuids.append(squad)
|
||||
|
||||
squad_uuids = await self.normalize_active_internal_squads(active_squads)
|
||||
user.active_internal_squads = squad_uuids
|
||||
|
||||
subscription_data = {
|
||||
'user_id': user.id,
|
||||
'status': status.value,
|
||||
@@ -1647,16 +1708,11 @@ class RemnaWaveService:
|
||||
)
|
||||
if panel_crypto_link and subscription.subscription_crypto_link != panel_crypto_link:
|
||||
subscription.subscription_crypto_link = panel_crypto_link
|
||||
|
||||
|
||||
active_squads = panel_user.get('activeInternalSquads', [])
|
||||
squad_uuids = []
|
||||
if isinstance(active_squads, list):
|
||||
for squad in active_squads:
|
||||
if isinstance(squad, dict) and 'uuid' in squad:
|
||||
squad_uuids.append(squad['uuid'])
|
||||
elif isinstance(squad, str):
|
||||
squad_uuids.append(squad)
|
||||
|
||||
squad_uuids = await self.normalize_active_internal_squads(active_squads)
|
||||
user.active_internal_squads = squad_uuids
|
||||
|
||||
current_squads = set(subscription.connected_squads or [])
|
||||
new_squads = set(squad_uuids)
|
||||
|
||||
@@ -1694,6 +1750,7 @@ class RemnaWaveService:
|
||||
try:
|
||||
subscription = user.subscription
|
||||
hwid_limit = resolve_hwid_device_limit_for_payload(subscription)
|
||||
active_squads = resolve_user_internal_squads(user, subscription)
|
||||
|
||||
expire_at = self._safe_expire_at_for_panel(subscription.end_date)
|
||||
status = UserStatus.ACTIVE if subscription.is_active else UserStatus.DISABLED
|
||||
@@ -1716,7 +1773,7 @@ class RemnaWaveService:
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=active_squads,
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
@@ -1730,7 +1787,7 @@ class RemnaWaveService:
|
||||
traffic_limit_bytes=create_kwargs['traffic_limit_bytes'],
|
||||
traffic_limit_strategy=TrafficLimitStrategy.MONTH,
|
||||
description=create_kwargs['description'],
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=active_squads,
|
||||
)
|
||||
|
||||
if hwid_limit is not None:
|
||||
|
||||
@@ -11,6 +11,7 @@ from app.external.remnawave_api import (
|
||||
TrafficLimitStrategy, RemnaWaveAPIError
|
||||
)
|
||||
from app.database.crud.user import get_user_by_id
|
||||
from app.utils.internal_squads import resolve_user_internal_squads
|
||||
from app.utils.pricing_utils import (
|
||||
calculate_months_from_days,
|
||||
get_remaining_months,
|
||||
@@ -207,7 +208,9 @@ class SubscriptionService:
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=resolve_user_internal_squads(
|
||||
user, subscription
|
||||
),
|
||||
)
|
||||
|
||||
if user_tag is not None:
|
||||
@@ -245,7 +248,9 @@ class SubscriptionService:
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=resolve_user_internal_squads(
|
||||
user, subscription
|
||||
),
|
||||
)
|
||||
|
||||
if user_tag is not None:
|
||||
@@ -328,7 +333,9 @@ class SubscriptionService:
|
||||
username=user.username,
|
||||
telegram_id=user.telegram_id
|
||||
),
|
||||
active_internal_squads=subscription.connected_squads,
|
||||
active_internal_squads=resolve_user_internal_squads(
|
||||
user, subscription
|
||||
),
|
||||
)
|
||||
|
||||
if user_tag is not None:
|
||||
|
||||
21
app/utils/internal_squads.py
Normal file
21
app/utils/internal_squads.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from typing import List, Optional
|
||||
|
||||
from app.database.models import Subscription, User
|
||||
|
||||
|
||||
def resolve_user_internal_squads(
|
||||
user: Optional[User], subscription: Optional[Subscription]
|
||||
) -> List[str]:
|
||||
"""Возвращает список Internal Squads для пользователя.
|
||||
|
||||
Приоритет у персональных сквадов пользователя. Если они не заданы,
|
||||
используется список сквадов из подписки.
|
||||
"""
|
||||
|
||||
if user and getattr(user, "active_internal_squads", None) is not None:
|
||||
return list(user.active_internal_squads or [])
|
||||
|
||||
if subscription:
|
||||
return list(subscription.connected_squads or [])
|
||||
|
||||
return []
|
||||
@@ -17,6 +17,7 @@ from app.database.crud.user import (
|
||||
update_user,
|
||||
)
|
||||
from app.database.models import PromoGroup, Subscription, User, UserStatus
|
||||
from app.services.remnawave_service import RemnaWaveService
|
||||
|
||||
from ..dependencies import get_db_session, require_api_token
|
||||
from ..schemas.users import (
|
||||
@@ -30,6 +31,7 @@ from ..schemas.users import (
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
remnawave_service = RemnaWaveService()
|
||||
|
||||
|
||||
def _serialize_promo_group(group: Optional[PromoGroup]) -> Optional[PromoGroupSummary]:
|
||||
@@ -90,6 +92,7 @@ def _serialize_user(user: User) -> UserResponse:
|
||||
last_activity=user.last_activity,
|
||||
promo_group=_serialize_promo_group(promo_group),
|
||||
subscription=_serialize_subscription(subscription),
|
||||
active_internal_squads=list(getattr(user, "active_internal_squads", []) or []),
|
||||
)
|
||||
|
||||
|
||||
@@ -197,6 +200,12 @@ async def create_user_endpoint(
|
||||
if existing:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "User with this telegram_id already exists")
|
||||
|
||||
active_squads = None
|
||||
if payload.active_internal_squads is not None:
|
||||
active_squads = await remnawave_service.resolve_internal_squad_uuids(
|
||||
payload.active_internal_squads
|
||||
)
|
||||
|
||||
user = await create_user(
|
||||
db,
|
||||
telegram_id=payload.telegram_id,
|
||||
@@ -205,6 +214,7 @@ async def create_user_endpoint(
|
||||
last_name=payload.last_name,
|
||||
language=payload.language,
|
||||
referred_by_id=payload.referred_by_id,
|
||||
active_internal_squads=active_squads,
|
||||
)
|
||||
|
||||
if payload.promo_group_id and payload.promo_group_id != user.promo_group_id:
|
||||
@@ -249,6 +259,10 @@ async def update_user_endpoint(
|
||||
updates["has_had_paid_subscription"] = payload.has_had_paid_subscription
|
||||
if payload.has_made_first_topup is not None:
|
||||
updates["has_made_first_topup"] = payload.has_made_first_topup
|
||||
if payload.active_internal_squads is not None:
|
||||
updates["active_internal_squads"] = await remnawave_service.resolve_internal_squad_uuids(
|
||||
payload.active_internal_squads
|
||||
)
|
||||
|
||||
if payload.status is not None:
|
||||
try:
|
||||
|
||||
@@ -51,6 +51,7 @@ class UserResponse(BaseModel):
|
||||
last_activity: Optional[datetime] = None
|
||||
promo_group: Optional[PromoGroupSummary] = None
|
||||
subscription: Optional[SubscriptionSummary] = None
|
||||
active_internal_squads: List[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class UserListResponse(BaseModel):
|
||||
@@ -68,6 +69,10 @@ 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,
|
||||
description="Названия или UUID Internal Squads, которые нужно назначить пользователю"
|
||||
)
|
||||
|
||||
|
||||
class UserUpdateRequest(BaseModel):
|
||||
@@ -80,6 +85,10 @@ 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,
|
||||
description="Названия или UUID Internal Squads, которые нужно назначить пользователю"
|
||||
)
|
||||
|
||||
|
||||
class BalanceUpdateRequest(BaseModel):
|
||||
|
||||
Reference in New Issue
Block a user