mirror of
https://github.com/Gozargah/Marzban.git
synced 2026-05-17 00:25:53 +03:00
Migrate to Pydantic V2 (#1495)
* chore: update pydantic to version 2.10.2 and refactor model validators * refactor: simplify field validators by removing unnecessary pre and always flags * remove typing_extensions==4.9.0 from requirements.txt * refactor: remove allow_reuse flag from status field validator * refactor: simplify field validators by removing pre and always flags * refactor: update user model imports and enhance account class with abstract method * refactor: update model_config to use dictionary format in Admin and SubscriptionUserResponse classes * fix typo in UserDataResetByNext * change pre=True to mode="before" * refactor: update validation methods and model configuration in User and Proxy classes * change pre=False with mode="after" * Migrated to Pydantic V2 * fix: custom subscriptions not workong * some small changes * add missing properties to example schema * replace from_orm with model_validate --------- Co-authored-by: MahdiButcher <madibutchercoding@gmail.com> Co-authored-by: Mahdi Butcher <MahdiButcherCoding@gmail.com>
This commit is contained in:
@@ -613,7 +613,7 @@ def revoke_user_sub(db: Session, dbuser: User) -> User:
|
||||
"""
|
||||
dbuser.sub_revoked_at = datetime.utcnow()
|
||||
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
for proxy_type, settings in user.proxies.copy().items():
|
||||
settings.revoke()
|
||||
user.proxies[proxy_type] = settings
|
||||
|
||||
@@ -14,7 +14,7 @@ def validate_admin(db: Session, username: str, password: str) -> Optional[AdminV
|
||||
return AdminValidationResult(username=username, is_sudo=True)
|
||||
|
||||
dbadmin = crud.get_admin(db, username)
|
||||
if dbadmin and AdminInDB.from_orm(dbadmin).verify_password(password):
|
||||
if dbadmin and AdminInDB.model_validate(dbadmin).verify_password(password):
|
||||
return AdminValidationResult(username=dbadmin.username, is_sudo=dbadmin.is_sudo)
|
||||
|
||||
return None
|
||||
@@ -40,7 +40,8 @@ def validate_dates(start: Optional[Union[str, datetime]], end: Optional[Union[st
|
||||
"""Validate if start and end dates are correct and if end is after start."""
|
||||
try:
|
||||
if start:
|
||||
start_date = start if isinstance(start, datetime) else datetime.fromisoformat(start).astimezone(timezone.utc)
|
||||
start_date = start if isinstance(start, datetime) else datetime.fromisoformat(
|
||||
start).astimezone(timezone.utc)
|
||||
else:
|
||||
start_date = datetime.now(timezone.utc) - timedelta(days=30)
|
||||
if end:
|
||||
|
||||
@@ -12,7 +12,7 @@ def remove_expired_users():
|
||||
deleted_users = crud.autodelete_expired_users(db, USER_AUTODELETE_INCLUDE_LIMITED_ACCOUNTS)
|
||||
|
||||
for user in deleted_users:
|
||||
report.user_deleted(user.username, SYSTEM_ADMIN, user_admin=Admin.from_orm(user.admin))
|
||||
report.user_deleted(user.username, SYSTEM_ADMIN, user_admin=Admin.model_validate(user.admin))
|
||||
logger.log(logging.INFO, "Expired user %s deleted." % user.username)
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ def add_notification_reminders(db: Session, user: "User", now: datetime = dateti
|
||||
if usage_percent >= percent:
|
||||
if not get_notification_reminder(db, user.id, ReminderType.data_usage, threshold=percent):
|
||||
report.data_usage_percent_reached(
|
||||
db, usage_percent, UserResponse.from_orm(user),
|
||||
db, usage_percent, UserResponse.model_validate(user),
|
||||
user.id, user.expire, threshold=percent
|
||||
)
|
||||
break
|
||||
@@ -37,17 +37,19 @@ def add_notification_reminders(db: Session, user: "User", now: datetime = dateti
|
||||
if expire_days <= days_left:
|
||||
if not get_notification_reminder(db, user.id, ReminderType.expiration_date, threshold=days_left):
|
||||
report.expire_days_reached(
|
||||
db, expire_days, UserResponse.from_orm(user),
|
||||
db, expire_days, UserResponse.model_validate(user),
|
||||
user.id, user.expire, threshold=days_left
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
def reset_user_by_next_report(db: Session, user: "User"):
|
||||
user = reset_user_by_next(db, user)
|
||||
|
||||
|
||||
xray.operations.update_user(user)
|
||||
|
||||
report.user_data_reset_by_next(user=UserResponse.from_orm(user), user_admin=user.admin)
|
||||
|
||||
report.user_data_reset_by_next(user=UserResponse.model_validate(user), user_admin=user.admin)
|
||||
|
||||
|
||||
def review():
|
||||
now = datetime.utcnow()
|
||||
@@ -60,15 +62,15 @@ def review():
|
||||
|
||||
if (limited or expired) and user.next_plan is not None:
|
||||
if user.next_plan is not None:
|
||||
|
||||
|
||||
if user.next_plan.fire_on_either:
|
||||
reset_user_by_next_report(db, user)
|
||||
continue
|
||||
|
||||
|
||||
elif limited and expired:
|
||||
reset_user_by_next_report(db, user)
|
||||
continue
|
||||
|
||||
|
||||
if limited:
|
||||
status = UserStatus.limited
|
||||
elif expired:
|
||||
@@ -82,7 +84,7 @@ def review():
|
||||
update_user_status(db, user, status)
|
||||
|
||||
report.status_change(username=user.username, status=status,
|
||||
user=UserResponse.from_orm(user), user_admin=user.admin)
|
||||
user=UserResponse.model_validate(user), user_admin=user.admin)
|
||||
|
||||
logger.info(f"User \"{user.username}\" status changed to {status}")
|
||||
|
||||
@@ -108,7 +110,7 @@ def review():
|
||||
start_user_expire(db, user)
|
||||
|
||||
report.status_change(username=user.username, status=status,
|
||||
user=UserResponse.from_orm(user), user_admin=user.admin)
|
||||
user=UserResponse.model_validate(user), user_admin=user.admin)
|
||||
|
||||
logger.info(f"User \"{user.username}\" status changed to {status}")
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Optional
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from passlib.context import CryptContext
|
||||
from pydantic import BaseModel, validator
|
||||
from pydantic import ConfigDict, field_validator, BaseModel
|
||||
|
||||
from app.db import Session, crud, get_db
|
||||
from app.utils.jwt import get_admin_payload
|
||||
@@ -21,12 +21,10 @@ class Token(BaseModel):
|
||||
class Admin(BaseModel):
|
||||
username: str
|
||||
is_sudo: bool
|
||||
telegram_id: Optional[int]
|
||||
discord_webhook: Optional[str]
|
||||
users_usage: Optional[int]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
telegram_id: Optional[int] = None
|
||||
discord_webhook: Optional[str] = None
|
||||
users_usage: Optional[int] = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@classmethod
|
||||
def get_admin(cls, token: str, db: Session):
|
||||
@@ -47,7 +45,7 @@ class Admin(BaseModel):
|
||||
if dbadmin.password_reset_at > payload.get("created_at"):
|
||||
return
|
||||
|
||||
return cls.from_orm(dbadmin)
|
||||
return cls.model_validate(dbadmin)
|
||||
|
||||
@classmethod
|
||||
def get_current(cls,
|
||||
@@ -60,13 +58,12 @@ class Admin(BaseModel):
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
return admin
|
||||
|
||||
@classmethod
|
||||
def check_sudo_admin(cls,
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(oauth2_scheme)):
|
||||
db: Session = Depends(get_db),
|
||||
token: str = Depends(oauth2_scheme)):
|
||||
admin = cls.get_admin(token, db)
|
||||
if not admin:
|
||||
raise HTTPException(
|
||||
@@ -81,16 +78,18 @@ class Admin(BaseModel):
|
||||
)
|
||||
return admin
|
||||
|
||||
|
||||
class AdminCreate(Admin):
|
||||
password: str
|
||||
telegram_id: Optional[int]
|
||||
discord_webhook: Optional[str]
|
||||
telegram_id: Optional[int] = None
|
||||
discord_webhook: Optional[str] = None
|
||||
|
||||
@property
|
||||
def hashed_password(self):
|
||||
return pwd_context.hash(self.password)
|
||||
|
||||
@validator("discord_webhook")
|
||||
@field_validator("discord_webhook")
|
||||
@classmethod
|
||||
def validate_discord_webhook(cls, value):
|
||||
if value and not value.startswith("https://discord.com"):
|
||||
raise ValueError("Discord webhook must start with 'https://discord.com'")
|
||||
@@ -98,17 +97,18 @@ class AdminCreate(Admin):
|
||||
|
||||
|
||||
class AdminModify(BaseModel):
|
||||
password: Optional[str]
|
||||
password: Optional[str] = None
|
||||
is_sudo: bool
|
||||
telegram_id: Optional[int]
|
||||
discord_webhook: Optional[str]
|
||||
telegram_id: Optional[int] = None
|
||||
discord_webhook: Optional[str] = None
|
||||
|
||||
@property
|
||||
def hashed_password(self):
|
||||
if self.password:
|
||||
return pwd_context.hash(self.password)
|
||||
|
||||
@validator("discord_webhook")
|
||||
@field_validator("discord_webhook")
|
||||
@classmethod
|
||||
def validate_discord_webhook(cls, value):
|
||||
if value and not value.startswith("https://discord.com"):
|
||||
raise ValueError("Discord webhook must start with 'https://discord.com'")
|
||||
@@ -126,6 +126,7 @@ class AdminInDB(Admin):
|
||||
def verify_password(self, plain_password):
|
||||
return pwd_context.verify(plain_password, self.hashed_password)
|
||||
|
||||
|
||||
class AdminValidationResult(BaseModel):
|
||||
username: str
|
||||
is_sudo: bool
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from enum import Enum
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import ConfigDict, BaseModel, Field
|
||||
|
||||
|
||||
class NodeStatus(str, Enum):
|
||||
@@ -26,18 +26,16 @@ class Node(BaseModel):
|
||||
|
||||
class NodeCreate(Node):
|
||||
add_as_new_host: bool = True
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "DE node",
|
||||
"address": "192.168.1.1",
|
||||
"port": 62050,
|
||||
"api_port": 62051,
|
||||
"add_as_new_host": True,
|
||||
"usage_coefficient": 1
|
||||
}
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"name": "DE node",
|
||||
"address": "192.168.1.1",
|
||||
"port": 62050,
|
||||
"api_port": 62051,
|
||||
"add_as_new_host": True,
|
||||
"usage_coefficient": 1
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class NodeModify(Node):
|
||||
@@ -47,32 +45,28 @@ class NodeModify(Node):
|
||||
api_port: Optional[int] = Field(None, nullable=True)
|
||||
status: Optional[NodeStatus] = Field(None, nullable=True)
|
||||
usage_coefficient: Optional[float] = Field(None, nullable=True)
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "DE node",
|
||||
"address": "192.168.1.1",
|
||||
"port": 62050,
|
||||
"api_port": 62051,
|
||||
"status": "disabled",
|
||||
"usage_coefficient": 1.0
|
||||
}
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"name": "DE node",
|
||||
"address": "192.168.1.1",
|
||||
"port": 62050,
|
||||
"api_port": 62051,
|
||||
"status": "disabled",
|
||||
"usage_coefficient": 1.0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class NodeResponse(Node):
|
||||
id: int
|
||||
xray_version: Optional[str]
|
||||
xray_version: Optional[str] = None
|
||||
status: NodeStatus
|
||||
message: Optional[str]
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
message: Optional[str] = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class NodeUsageResponse(BaseModel):
|
||||
node_id: Optional[int]
|
||||
node_id: Optional[int] = None
|
||||
node_name: str
|
||||
uplink: int
|
||||
downlink: int
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
from typing import Optional, Union
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic import field_validator, ConfigDict, BaseModel, Field
|
||||
|
||||
from app.utils.system import random_password
|
||||
from xray_api.types.account import (
|
||||
@@ -53,10 +53,10 @@ class ProxyTypes(str, Enum):
|
||||
return ShadowsocksSettings
|
||||
|
||||
|
||||
class ProxySettings(BaseModel):
|
||||
class ProxySettings(BaseModel, use_enum_values=True):
|
||||
@classmethod
|
||||
def from_dict(cls, proxy_type: ProxyTypes, _dict: dict):
|
||||
return ProxyTypes(proxy_type).settings_model.parse_obj(_dict)
|
||||
return ProxyTypes(proxy_type).settings_model.model_validate(_dict)
|
||||
|
||||
def dict(self, *, no_obj=False, **kwargs):
|
||||
if no_obj:
|
||||
@@ -154,11 +154,9 @@ class ProxyHost(BaseModel):
|
||||
fragment_setting: Optional[str] = Field(None, nullable=True)
|
||||
noise_setting: Optional[str] = Field(None, nullable=True)
|
||||
random_user_agent: Union[bool, None] = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@validator("remark", pre=False, always=True)
|
||||
@field_validator("remark", mode="after")
|
||||
def validate_remark(cls, v):
|
||||
try:
|
||||
v.format_map(FormatVariables())
|
||||
@@ -167,7 +165,7 @@ class ProxyHost(BaseModel):
|
||||
|
||||
return v
|
||||
|
||||
@validator("address", pre=False, always=True)
|
||||
@field_validator("address", mode="after")
|
||||
def validate_address(cls, v):
|
||||
try:
|
||||
v.format_map(FormatVariables())
|
||||
@@ -176,7 +174,8 @@ class ProxyHost(BaseModel):
|
||||
|
||||
return v
|
||||
|
||||
@validator("fragment_setting", check_fields=False)
|
||||
@field_validator("fragment_setting", check_fields=False)
|
||||
@classmethod
|
||||
def validate_fragment(cls, v):
|
||||
if v and not FRAGMENT_PATTERN.match(v):
|
||||
raise ValueError(
|
||||
@@ -184,7 +183,8 @@ class ProxyHost(BaseModel):
|
||||
)
|
||||
return v
|
||||
|
||||
@validator("noise_setting", check_fields=False)
|
||||
@field_validator("noise_setting", check_fields=False)
|
||||
@classmethod
|
||||
def validate_noise(cls, v):
|
||||
if v:
|
||||
if not NOISE_PATTERN.match(v):
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import re
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Dict, List, Optional, Union
|
||||
import random
|
||||
import secrets
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
from app import xray
|
||||
from app.models.proxy import ProxySettings, ProxyTypes
|
||||
from app.models.admin import Admin
|
||||
from app.utils.jwt import create_subscription_token
|
||||
from app.models.proxy import ProxySettings, ProxyTypes
|
||||
from app.subscription.share import generate_v2ray_links
|
||||
from app.utils.jwt import create_subscription_token
|
||||
from config import XRAY_SUBSCRIPTION_PATH, XRAY_SUBSCRIPTION_URL_PREFIX
|
||||
|
||||
USERNAME_REGEXP = re.compile(r"^(?=\w{3,32}\b)[a-zA-Z0-9-_@.]+(?:_[a-zA-Z0-9-_@.]+)*$")
|
||||
@@ -48,14 +47,14 @@ class UserDataLimitResetStrategy(str, Enum):
|
||||
month = "month"
|
||||
year = "year"
|
||||
|
||||
|
||||
class NextPlanModel(BaseModel):
|
||||
data_limit: Optional[int]
|
||||
expire: Optional[int]
|
||||
data_limit: Optional[int] = None
|
||||
expire: Optional[int] = None
|
||||
add_remaining_traffic: bool = False
|
||||
fire_on_either: bool = True
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class User(BaseModel):
|
||||
proxies: Dict[ProxyTypes, ProxySettings] = {}
|
||||
@@ -75,10 +74,10 @@ class User(BaseModel):
|
||||
on_hold_timeout: Optional[Union[datetime, None]] = Field(None, nullable=True)
|
||||
|
||||
auto_delete_in_days: Optional[int] = Field(None, nullable=True)
|
||||
|
||||
|
||||
next_plan: Optional[NextPlanModel] = Field(None, nullable=True)
|
||||
|
||||
@validator("proxies", pre=True, always=True)
|
||||
|
||||
@field_validator("proxies", mode="before")
|
||||
def validate_proxies(cls, v, values, **kwargs):
|
||||
if not v:
|
||||
raise ValueError("Each user needs at least one proxy")
|
||||
@@ -88,7 +87,8 @@ class User(BaseModel):
|
||||
for proxy_type in v
|
||||
}
|
||||
|
||||
@validator("username", check_fields=False)
|
||||
@field_validator("username", check_fields=False)
|
||||
@classmethod
|
||||
def validate_username(cls, v):
|
||||
if not USERNAME_REGEXP.match(v):
|
||||
raise ValueError(
|
||||
@@ -96,13 +96,14 @@ class User(BaseModel):
|
||||
)
|
||||
return v
|
||||
|
||||
@validator("note", check_fields=False)
|
||||
@field_validator("note", check_fields=False)
|
||||
@classmethod
|
||||
def validate_note(cls, v):
|
||||
if v and len(v) > 500:
|
||||
raise ValueError("User's note can be a maximum of 500 character")
|
||||
raise ValueError("User's note can be a maximum of 500 character", mode="before")
|
||||
return v
|
||||
|
||||
@validator("on_hold_expire_duration", "on_hold_timeout", pre=True, always=True)
|
||||
@field_validator("on_hold_expire_duration", "on_hold_timeout")
|
||||
def validate_timeout(cls, v, values):
|
||||
# Check if expire is 0 or None and timeout is not 0 or None
|
||||
if (v in (0, None)):
|
||||
@@ -113,34 +114,32 @@ class User(BaseModel):
|
||||
class UserCreate(User):
|
||||
username: str
|
||||
status: UserStatusCreate = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"username": "user1234",
|
||||
"proxies": {
|
||||
"vmess": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"},
|
||||
"vless": {},
|
||||
},
|
||||
"inbounds": {
|
||||
"vmess": ["VMess TCP", "VMess Websocket"],
|
||||
"vless": ["VLESS TCP REALITY", "VLESS GRPC REALITY"],
|
||||
},
|
||||
"next_plan": {
|
||||
"data_limit": 0,
|
||||
"expire": 0,
|
||||
"add_remaining_traffic": False,
|
||||
"fire_on_either": True
|
||||
},
|
||||
"expire": 0,
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"username": "user1234",
|
||||
"proxies": {
|
||||
"vmess": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"},
|
||||
"vless": {},
|
||||
},
|
||||
"inbounds": {
|
||||
"vmess": ["VMess TCP", "VMess Websocket"],
|
||||
"vless": ["VLESS TCP REALITY", "VLESS GRPC REALITY"],
|
||||
},
|
||||
"next_plan": {
|
||||
"data_limit": 0,
|
||||
"data_limit_reset_strategy": "no_reset",
|
||||
"status": "active",
|
||||
"note": "",
|
||||
"on_hold_timeout": "2023-11-03T20:30:00",
|
||||
"on_hold_expire_duration": 0,
|
||||
}
|
||||
"expire": 0,
|
||||
"add_remaining_traffic": False,
|
||||
"fire_on_either": True
|
||||
},
|
||||
"expire": 0,
|
||||
"data_limit": 0,
|
||||
"data_limit_reset_strategy": "no_reset",
|
||||
"status": "active",
|
||||
"note": "",
|
||||
"on_hold_timeout": "2023-11-03T20:30:00",
|
||||
"on_hold_expire_duration": 0,
|
||||
}
|
||||
})
|
||||
|
||||
@property
|
||||
def excluded_inbounds(self):
|
||||
@@ -153,9 +152,9 @@ class UserCreate(User):
|
||||
|
||||
return excluded
|
||||
|
||||
@validator("inbounds", pre=True, always=True)
|
||||
@field_validator("inbounds", mode="before")
|
||||
def validate_inbounds(cls, inbounds, values, **kwargs):
|
||||
proxies = values.get("proxies", [])
|
||||
proxies = values.data.get("proxies", [])
|
||||
|
||||
# delete inbounds that are for protocols not activated
|
||||
for proxy_type in inbounds.copy():
|
||||
@@ -182,16 +181,10 @@ class UserCreate(User):
|
||||
|
||||
return inbounds
|
||||
|
||||
@validator("status", pre=True, always=True)
|
||||
def validate_status(cls, value):
|
||||
if not value or value not in UserStatusCreate.__members__:
|
||||
return UserStatusCreate.active # Set to the default if not valid
|
||||
return value
|
||||
|
||||
@validator("status", pre=True, always=True, allow_reuse=True)
|
||||
@field_validator("status", mode="before")
|
||||
def validate_status(cls, status, values):
|
||||
on_hold_expire = values.get("on_hold_expire_duration")
|
||||
expire = values.get("expire")
|
||||
on_hold_expire = values.data.get("on_hold_expire_duration")
|
||||
expire = values.data.get("expire")
|
||||
if status == UserStatusCreate.on_hold:
|
||||
if (on_hold_expire == 0 or on_hold_expire is None):
|
||||
raise ValueError("User cannot be on hold without a valid on_hold_expire_duration.")
|
||||
@@ -203,33 +196,31 @@ class UserCreate(User):
|
||||
class UserModify(User):
|
||||
status: UserStatusModify = None
|
||||
data_limit_reset_strategy: UserDataLimitResetStrategy = None
|
||||
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"proxies": {
|
||||
"vmess": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"},
|
||||
"vless": {},
|
||||
},
|
||||
"inbounds": {
|
||||
"vmess": ["VMess TCP", "VMess Websocket"],
|
||||
"vless": ["VLESS TCP REALITY", "VLESS GRPC REALITY"],
|
||||
},
|
||||
"next_plan": {
|
||||
"data_limit": 0,
|
||||
"expire": 0,
|
||||
"add_remaining_traffic": False,
|
||||
"fire_on_either": True
|
||||
},
|
||||
"expire": 0,
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"proxies": {
|
||||
"vmess": {"id": "35e4e39c-7d5c-4f4b-8b71-558e4f37ff53"},
|
||||
"vless": {},
|
||||
},
|
||||
"inbounds": {
|
||||
"vmess": ["VMess TCP", "VMess Websocket"],
|
||||
"vless": ["VLESS TCP REALITY", "VLESS GRPC REALITY"],
|
||||
},
|
||||
"next_plan": {
|
||||
"data_limit": 0,
|
||||
"data_limit_reset_strategy": "no_reset",
|
||||
"status": "active",
|
||||
"note": "",
|
||||
"on_hold_timeout": "2023-11-03T20:30:00",
|
||||
"on_hold_expire_duration": 0,
|
||||
}
|
||||
"expire": 0,
|
||||
"add_remaining_traffic": False,
|
||||
"fire_on_either": True
|
||||
},
|
||||
"expire": 0,
|
||||
"data_limit": 0,
|
||||
"data_limit_reset_strategy": "no_reset",
|
||||
"status": "active",
|
||||
"note": "",
|
||||
"on_hold_timeout": "2023-11-03T20:30:00",
|
||||
"on_hold_expire_duration": 0,
|
||||
}
|
||||
})
|
||||
|
||||
@property
|
||||
def excluded_inbounds(self):
|
||||
@@ -242,7 +233,7 @@ class UserModify(User):
|
||||
|
||||
return excluded
|
||||
|
||||
@validator("inbounds", pre=True, always=True)
|
||||
@field_validator("inbounds", mode="before")
|
||||
def validate_inbounds(cls, inbounds, values, **kwargs):
|
||||
# check with inbounds, "proxies" is optional on modifying
|
||||
# so inbounds particularly can be modified
|
||||
@@ -258,7 +249,7 @@ class UserModify(User):
|
||||
|
||||
return inbounds
|
||||
|
||||
@validator("proxies", pre=True, always=True)
|
||||
@field_validator("proxies", mode="before")
|
||||
def validate_proxies(cls, v):
|
||||
return {
|
||||
proxy_type: ProxySettings.from_dict(
|
||||
@@ -266,10 +257,10 @@ class UserModify(User):
|
||||
for proxy_type in v
|
||||
}
|
||||
|
||||
@validator("status", pre=True, always=True, allow_reuse=True)
|
||||
@field_validator("status", mode="before")
|
||||
def validate_status(cls, status, values):
|
||||
on_hold_expire = values.get("on_hold_expire_duration")
|
||||
expire = values.get("expire")
|
||||
on_hold_expire = values.data.get("on_hold_expire_duration")
|
||||
expire = values.data.get("expire")
|
||||
if status == UserStatusCreate.on_hold:
|
||||
if (on_hold_expire == 0 or on_hold_expire is None):
|
||||
raise ValueError("User cannot be on hold without a valid on_hold_expire_duration.")
|
||||
@@ -289,29 +280,27 @@ class UserResponse(User):
|
||||
proxies: dict
|
||||
excluded_inbounds: Dict[ProxyTypes, List[str]] = {}
|
||||
|
||||
admin: Optional[Admin]
|
||||
admin: Optional[Admin] = None
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@validator("links", pre=False, always=True)
|
||||
def validate_links(cls, v, values, **kwargs):
|
||||
if not v:
|
||||
return generate_v2ray_links(
|
||||
values.get("proxies", {}), values.get("inbounds", {}), extra_data=values, reverse=False,
|
||||
@model_validator(mode="after")
|
||||
def validate_links(self):
|
||||
if not self.links:
|
||||
self.links = generate_v2ray_links(
|
||||
self.proxies, self.inbounds, extra_data=self.model_dump(), reverse=False,
|
||||
)
|
||||
return v
|
||||
return self
|
||||
|
||||
@validator("subscription_url", pre=False, always=True)
|
||||
def validate_subscription_url(cls, v, values, **kwargs):
|
||||
if not v:
|
||||
@model_validator(mode="after")
|
||||
def validate_subscription_url(self):
|
||||
if not self.subscription_url:
|
||||
salt = secrets.token_hex(8)
|
||||
url_prefix = (XRAY_SUBSCRIPTION_URL_PREFIX).replace('*', salt)
|
||||
token = create_subscription_token(values["username"])
|
||||
return f"{url_prefix}/{XRAY_SUBSCRIPTION_PATH}/{token}"
|
||||
return v
|
||||
token = create_subscription_token(self.username)
|
||||
self.subscription_url = f"{url_prefix}/{XRAY_SUBSCRIPTION_PATH}/{token}"
|
||||
return self
|
||||
|
||||
@validator("proxies", pre=True, always=True)
|
||||
@field_validator("proxies", mode="before")
|
||||
def validate_proxies(cls, v, values, **kwargs):
|
||||
if isinstance(v, list):
|
||||
v = {p.type: p.settings for p in v}
|
||||
@@ -319,28 +308,12 @@ class UserResponse(User):
|
||||
|
||||
|
||||
class SubscriptionUserResponse(UserResponse):
|
||||
class Config:
|
||||
orm_mode = True
|
||||
fields = {
|
||||
field: {"include": True} for field in [
|
||||
"username",
|
||||
"status",
|
||||
"expire",
|
||||
"data_limit",
|
||||
"data_limit_reset_strategy",
|
||||
"used_traffic",
|
||||
"lifetime_used_traffic",
|
||||
"proxies",
|
||||
"created_at",
|
||||
"sub_updated_at",
|
||||
"online_at",
|
||||
"links",
|
||||
"subscription_url",
|
||||
"sub_updated_at",
|
||||
"sub_last_user_agent",
|
||||
"online_at",
|
||||
]
|
||||
}
|
||||
admin: Admin | None = Field(default=None, exclude=True)
|
||||
excluded_inbounds: Dict[ProxyTypes, List[str]] | None = Field(None, exclude=True)
|
||||
note: str | None = Field(None, exclude=True)
|
||||
inbounds: Dict[ProxyTypes, List[str]] | None = Field(None, exclude=True)
|
||||
auto_delete_in_days: int | None = Field(None, exclude=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class UsersResponse(BaseModel):
|
||||
@@ -349,7 +322,7 @@ class UsersResponse(BaseModel):
|
||||
|
||||
|
||||
class UserUsageResponse(BaseModel):
|
||||
node_id: Union[int, None]
|
||||
node_id: Union[int, None] = None
|
||||
node_name: str
|
||||
used_traffic: int
|
||||
|
||||
@@ -358,5 +331,6 @@ class UserUsagesResponse(BaseModel):
|
||||
username: str
|
||||
usages: List[UserUsageResponse]
|
||||
|
||||
|
||||
class UsersUsagesResponse(BaseModel):
|
||||
usages: List[UserUsageResponse]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from pydantic import field_validator, ConfigDict, BaseModel, Field
|
||||
|
||||
from app import xray
|
||||
from app.models.proxy import ProxyTypes
|
||||
@@ -21,33 +21,36 @@ class UserTemplate(BaseModel):
|
||||
|
||||
|
||||
class UserTemplateCreate(UserTemplate):
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "my template 1",
|
||||
"inbounds": {"vmess": ["VMESS_INBOUND"], "vless": ["VLESS_INBOUND"]},
|
||||
"data_limit": 0,
|
||||
"expire_duration": 0,
|
||||
}
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"name": "my template 1",
|
||||
"username_prefix": None,
|
||||
"username_suffix": None,
|
||||
"inbounds": {"vmess": ["VMESS_INBOUND"], "vless": ["VLESS_INBOUND"]},
|
||||
"data_limit": 0,
|
||||
"expire_duration": 0,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class UserTemplateModify(UserTemplate):
|
||||
class Config:
|
||||
schema_extra = {
|
||||
"example": {
|
||||
"name": "my template 1",
|
||||
"inbounds": {"vmess": ["VMESS_INBOUND"], "vless": ["VLESS_INBOUND"]},
|
||||
"data_limit": 0,
|
||||
"expire_duration": 0,
|
||||
}
|
||||
model_config = ConfigDict(json_schema_extra={
|
||||
"example": {
|
||||
"name": "my template 1",
|
||||
"username_prefix": None,
|
||||
"username_suffix": None,
|
||||
"inbounds": {"vmess": ["VMESS_INBOUND"], "vless": ["VLESS_INBOUND"]},
|
||||
"data_limit": 0,
|
||||
"expire_duration": 0,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
class UserTemplateResponse(UserTemplate):
|
||||
id: int
|
||||
|
||||
@validator("inbounds", pre=True)
|
||||
@field_validator("inbounds", mode="before")
|
||||
@classmethod
|
||||
def validate_inbounds(cls, v):
|
||||
final = {}
|
||||
inbound_tags = [i.tag for i in v]
|
||||
@@ -59,6 +62,4 @@ class UserTemplateResponse(UserTemplate):
|
||||
else:
|
||||
final[protocol] = [inbound["tag"]]
|
||||
return final
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -53,7 +53,7 @@ def user_subscription(
|
||||
user_agent: str = Header(default="")
|
||||
):
|
||||
"""Provides a subscription link based on the user agent (Clash, V2Ray, etc.)."""
|
||||
user: UserResponse = UserResponse.from_orm(dbuser)
|
||||
user: UserResponse = UserResponse.model_validate(dbuser)
|
||||
|
||||
accept_header = request.headers.get("Accept", "")
|
||||
if "text/html" in accept_header:
|
||||
@@ -159,7 +159,7 @@ def user_subscription_with_client_type(
|
||||
user_agent: str = Header(default="")
|
||||
):
|
||||
"""Provides a subscription link based on the specified client type (e.g., Clash, V2Ray)."""
|
||||
user: UserResponse = UserResponse.from_orm(dbuser)
|
||||
user: UserResponse = UserResponse.model_validate(dbuser)
|
||||
|
||||
response_headers = {
|
||||
"content-disposition": f'attachment; filename="{user.username}"',
|
||||
|
||||
@@ -63,7 +63,7 @@ def add_user(
|
||||
raise HTTPException(status_code=409, detail="User already exists")
|
||||
|
||||
bg.add_task(xray.operations.add_user, dbuser=dbuser)
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
report.user_created(user=user, user_id=dbuser.id, by=admin, user_admin=dbuser.admin)
|
||||
logger.info(f'New user "{dbuser.username}" added')
|
||||
return user
|
||||
@@ -75,7 +75,7 @@ def get_user(dbuser: UserResponse = Depends(get_validated_user)):
|
||||
return dbuser
|
||||
|
||||
|
||||
@router.put("/user/{username}", response_model=UserResponse, responses={400: responses._400,403: responses._403, 404: responses._404})
|
||||
@router.put("/user/{username}", response_model=UserResponse, responses={400: responses._400, 403: responses._403, 404: responses._404})
|
||||
def modify_user(
|
||||
modified_user: UserModify,
|
||||
bg: BackgroundTasks,
|
||||
@@ -97,7 +97,7 @@ def modify_user(
|
||||
- **on_hold_timeout**: New UTC timestamp for when `on_hold` status should start or end. Only applicable if status is changed to 'on_hold'.
|
||||
- **on_hold_expire_duration**: New duration (in seconds) for how long the user should stay in `on_hold` status. Only applicable if status is changed to 'on_hold'.
|
||||
- **next_plan**: Next user plan (resets after use).
|
||||
|
||||
|
||||
Note: Fields set to `null` or omitted will not be modified.
|
||||
"""
|
||||
|
||||
@@ -110,7 +110,7 @@ def modify_user(
|
||||
|
||||
old_status = dbuser.status
|
||||
dbuser = crud.update_user(db, dbuser, modified_user)
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
|
||||
if user.status in [UserStatus.active, UserStatus.on_hold]:
|
||||
bg.add_task(xray.operations.update_user, dbuser=dbuser)
|
||||
@@ -168,7 +168,7 @@ def reset_user_data_usage(
|
||||
if dbuser.status in [UserStatus.active, UserStatus.on_hold]:
|
||||
bg.add_task(xray.operations.add_user, dbuser=dbuser)
|
||||
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
bg.add_task(
|
||||
report.user_data_usage_reset, user=user, user_admin=dbuser.admin, by=admin
|
||||
)
|
||||
@@ -189,7 +189,7 @@ def revoke_user_subscription(
|
||||
|
||||
if dbuser.status in [UserStatus.active, UserStatus.on_hold]:
|
||||
bg.add_task(xray.operations.update_user, dbuser=dbuser)
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
bg.add_task(
|
||||
report.user_subscription_revoked, user=user, user_admin=dbuser.admin, by=admin
|
||||
)
|
||||
@@ -253,7 +253,7 @@ def reset_users_data_usage(
|
||||
return {"detail": "Users successfully reset."}
|
||||
|
||||
|
||||
@router.get("/user/{username}/usage", response_model=UserUsagesResponse,responses={403: responses._403, 404: responses._404})
|
||||
@router.get("/user/{username}/usage", response_model=UserUsagesResponse, responses={403: responses._403, 404: responses._404})
|
||||
def get_user_usage(
|
||||
dbuser: UserResponse = Depends(get_validated_user),
|
||||
start: str = "",
|
||||
@@ -268,7 +268,7 @@ def get_user_usage(
|
||||
return {"usages": usages, "username": dbuser.username}
|
||||
|
||||
|
||||
@router.post("/user/{username}/active-next", response_model=UserResponse,responses={403: responses._403, 404: responses._404})
|
||||
@router.post("/user/{username}/active-next", response_model=UserResponse, responses={403: responses._403, 404: responses._404})
|
||||
def active_next_plan(
|
||||
bg: BackgroundTasks,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -279,14 +279,14 @@ def active_next_plan(
|
||||
|
||||
if (dbuser is None or dbuser.next_plan is None):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"User doesn't have next plan",
|
||||
)
|
||||
status_code=404,
|
||||
detail=f"User doesn't have next plan",
|
||||
)
|
||||
|
||||
if dbuser.status in [UserStatus.active, UserStatus.on_hold]:
|
||||
bg.add_task(xray.operations.add_user, dbuser=dbuser)
|
||||
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
bg.add_task(
|
||||
report.user_data_reset_by_next, user=user, user_admin=dbuser.admin,
|
||||
)
|
||||
@@ -326,7 +326,7 @@ def set_owner(
|
||||
raise HTTPException(status_code=404, detail="Admin not found")
|
||||
|
||||
dbuser = crud.set_owner(db, dbuser, new_admin)
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
|
||||
logger.info(f'{user.username}"owner successfully set to{admin.username}')
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import copy
|
||||
import json
|
||||
from random import choice
|
||||
from uuid import UUID
|
||||
from app.utils.helpers import yml_uuid_representer
|
||||
|
||||
import yaml
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
@@ -42,6 +44,8 @@ class ClashConfiguration(object):
|
||||
def render(self, reverse=False):
|
||||
if reverse:
|
||||
self.data['proxies'].reverse()
|
||||
|
||||
yaml.add_representer(UUID, yml_uuid_representer)
|
||||
return yaml.dump(
|
||||
yaml.load(
|
||||
render_template(
|
||||
@@ -49,6 +53,7 @@ class ClashConfiguration(object):
|
||||
{"conf": self.data, "proxy_remarks": self.proxy_remarks}
|
||||
),
|
||||
Loader=yaml.SafeLoader
|
||||
|
||||
),
|
||||
sort_keys=False,
|
||||
allow_unicode=True,
|
||||
|
||||
@@ -314,7 +314,7 @@ def process_inbounds_and_tags(
|
||||
remark=host["remark"].format_map(format_variables),
|
||||
address=address.format_map(format_variables),
|
||||
inbound=host_inbound,
|
||||
settings=settings.dict(no_obj=True)
|
||||
settings=settings.model_dump()
|
||||
)
|
||||
|
||||
return conf.render(reverse=reverse)
|
||||
|
||||
@@ -2,6 +2,7 @@ import copy
|
||||
import json
|
||||
from random import choice
|
||||
|
||||
from app.utils.helpers import UUIDEncoder
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
from app.subscription.funcs import get_grpc_gun
|
||||
@@ -65,7 +66,7 @@ class SingBoxConfiguration(str):
|
||||
|
||||
if reverse:
|
||||
self.config["outbounds"].reverse()
|
||||
return json.dumps(self.config, indent=4)
|
||||
return json.dumps(self.config, indent=4,cls=UUIDEncoder)
|
||||
|
||||
@staticmethod
|
||||
def tls_config(sni=None, fp=None, tls=None, pbk=None,
|
||||
|
||||
@@ -6,6 +6,7 @@ from random import choice
|
||||
from typing import Union
|
||||
from urllib.parse import quote
|
||||
from uuid import UUID
|
||||
from app.utils.helpers import UUIDEncoder
|
||||
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
@@ -517,7 +518,7 @@ class V2rayJsonConfig(str):
|
||||
def render(self, reverse=False):
|
||||
if reverse:
|
||||
self.config.reverse()
|
||||
return json.dumps(self.config, indent=4)
|
||||
return json.dumps(self.config, indent=4,cls=UUIDEncoder)
|
||||
|
||||
@staticmethod
|
||||
def tls_config(sni=None, fp=None, alpn=None, ais: bool = False) -> dict:
|
||||
|
||||
@@ -331,7 +331,7 @@ def edit_command(call: types.CallbackQuery):
|
||||
'❌ User not found.',
|
||||
show_alert=True
|
||||
)
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
mem_store.set(f'{call.message.chat.id}:username', username)
|
||||
mem_store.set(f'{call.message.chat.id}:data_limit', db_user.data_limit)
|
||||
|
||||
@@ -619,7 +619,7 @@ def edit_note_step(message: types.Message):
|
||||
last_note = db_user.note
|
||||
modify = UserModify(note=note)
|
||||
db_user = crud.update_user(db, db_user, modify)
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
bot.reply_to(
|
||||
message, get_user_info_text(db_user), parse_mode="html",
|
||||
reply_markup=BotKeyboard.user_menu(user_info={'status': user.status, 'username': user.username}))
|
||||
@@ -647,7 +647,7 @@ def user_command(call: types.CallbackQuery):
|
||||
db_user = crud.get_user(db, username)
|
||||
if not db_user:
|
||||
return bot.answer_callback_query(call.id, '❌ User not found.', show_alert=True)
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
bot.edit_message_text(
|
||||
get_user_info_text(db_user),
|
||||
call.message.chat.id, call.message.message_id, parse_mode="HTML",
|
||||
@@ -674,7 +674,7 @@ def links_command(call: types.CallbackQuery):
|
||||
if not db_user:
|
||||
return bot.answer_callback_query(call.id, "User not found!", show_alert=True)
|
||||
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
|
||||
text = f"<code>{user.subscription_url}</code>\n\n\n"
|
||||
for link in user.links:
|
||||
@@ -702,7 +702,7 @@ def genqr_command(call: types.CallbackQuery):
|
||||
if not db_user:
|
||||
return bot.answer_callback_query(call.id, "User not found!", show_alert=True)
|
||||
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
|
||||
bot.answer_callback_query(call.id, "Generating QR code...")
|
||||
|
||||
@@ -783,12 +783,12 @@ def template_charge_command(call: types.CallbackQuery):
|
||||
template = crud.get_user_template(db, template_id)
|
||||
if not template:
|
||||
return bot.answer_callback_query(call.id, "Template not found!", show_alert=True)
|
||||
template = UserTemplateResponse.from_orm(template)
|
||||
template = UserTemplateResponse.model_validate(template)
|
||||
|
||||
db_user = crud.get_user(db, username)
|
||||
if not db_user:
|
||||
return bot.answer_callback_query(call.id, "User not found!", show_alert=True)
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
if (user.data_limit and not user.expire) or (not user.data_limit and user.expire):
|
||||
expire = (datetime.fromtimestamp(db_user.expire) if db_user.expire else today)
|
||||
expire += relativedelta(seconds=template.expire_duration)
|
||||
@@ -917,7 +917,7 @@ def add_user_from_template(call: types.CallbackQuery):
|
||||
template = crud.get_user_template(db, template_id)
|
||||
if not template:
|
||||
return bot.answer_callback_query(call.id, "Template not found!", show_alert=True)
|
||||
template = UserTemplateResponse.from_orm(template)
|
||||
template = UserTemplateResponse.model_validate(template)
|
||||
|
||||
text = get_template_info_text(template)
|
||||
if template.username_prefix:
|
||||
@@ -975,7 +975,7 @@ def random_username(call: types.CallbackQuery):
|
||||
if template.username_suffix:
|
||||
username += template.username_suffix
|
||||
|
||||
template = UserTemplateResponse.from_orm(template)
|
||||
template = UserTemplateResponse.model_validate(template)
|
||||
mem_store.set(f"{call.message.chat.id}:username", username)
|
||||
mem_store.set(f"{call.message.chat.id}:data_limit", template.data_limit)
|
||||
mem_store.set(f"{call.message.chat.id}:protocols", template.inbounds)
|
||||
@@ -1066,7 +1066,7 @@ def add_user_from_template_username_step(message: types.Message):
|
||||
wait_msg = bot.send_message(message.chat.id, '❌ Username already exists.')
|
||||
schedule_delete_message(message.chat.id, wait_msg.message_id, message.message_id)
|
||||
return bot.register_next_step_handler(wait_msg, add_user_from_template_username_step)
|
||||
template = UserTemplateResponse.from_orm(template)
|
||||
template = UserTemplateResponse.model_validate(template)
|
||||
mem_store.set(f"{message.chat.id}:username", username)
|
||||
mem_store.set(f"{message.chat.id}:data_limit", template.data_limit)
|
||||
mem_store.set(f"{message.chat.id}:protocols", template.inbounds)
|
||||
@@ -1570,7 +1570,7 @@ def confirm_user_command(call: types.CallbackQuery):
|
||||
crud.reset_user_data_usage(db, db_user)
|
||||
if db_user.status in [UserStatus.active, UserStatus.on_hold]:
|
||||
xray.operations.add_user(db_user)
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
bot.edit_message_text(
|
||||
get_user_info_text(db_user),
|
||||
call.message.chat.id,
|
||||
@@ -1608,12 +1608,12 @@ def confirm_user_command(call: types.CallbackQuery):
|
||||
template = crud.get_user_template(db, template_id)
|
||||
if not template:
|
||||
return bot.answer_callback_query(call.id, "Template not found!", show_alert=True)
|
||||
template = UserTemplateResponse.from_orm(template)
|
||||
template = UserTemplateResponse.model_validate(template)
|
||||
|
||||
db_user = crud.get_user(db, username)
|
||||
if not db_user:
|
||||
return bot.answer_callback_query(call.id, "User not found!", show_alert=True)
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
|
||||
inbounds = template.inbounds
|
||||
proxies = {p.type.value: p.settings for p in db_user.proxies}
|
||||
@@ -1731,10 +1731,10 @@ def confirm_user_command(call: types.CallbackQuery):
|
||||
proxies=proxies,
|
||||
inbounds=inbounds
|
||||
)
|
||||
last_user = UserResponse.from_orm(db_user)
|
||||
last_user = UserResponse.model_validate(db_user)
|
||||
db_user = crud.update_user(db, db_user, modify)
|
||||
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
|
||||
if user.status == UserStatus.active:
|
||||
xray.operations.update_user(db_user)
|
||||
@@ -1856,7 +1856,7 @@ def confirm_user_command(call: types.CallbackQuery):
|
||||
with GetDB() as db:
|
||||
db_user = crud.create_user(db, new_user)
|
||||
proxies = db_user.proxies
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
xray.operations.add_user(db_user)
|
||||
if mem_store.get(f"{call.message.chat.id}:is_bulk", False):
|
||||
schedule_delete_message(call.message.chat.id, call.message.id)
|
||||
@@ -2115,7 +2115,7 @@ def confirm_user_command(call: types.CallbackQuery):
|
||||
if not db_user:
|
||||
return bot.answer_callback_query(call.id, text=f"User not found!", show_alert=True)
|
||||
db_user = crud.revoke_user_sub(db, db_user)
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
bot.answer_callback_query(call.id, "✅ Subscription Successfully Revoked!")
|
||||
bot.edit_message_text(
|
||||
get_user_info_text(db_user),
|
||||
@@ -2156,7 +2156,7 @@ def search_user(message: types.Message):
|
||||
if not db_user:
|
||||
bot.reply_to(message, f'❌ User «{username}» not found.')
|
||||
continue
|
||||
user = UserResponse.from_orm(db_user)
|
||||
user = UserResponse.model_validate(db_user)
|
||||
bot.reply_to(
|
||||
message,
|
||||
get_user_info_text(db_user),
|
||||
|
||||
@@ -21,7 +21,7 @@ def usage_command(message):
|
||||
|
||||
if not dbuser:
|
||||
return bot.reply_to(message, "No user found with this username")
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
|
||||
statuses = {
|
||||
'active': '✅',
|
||||
|
||||
@@ -47,7 +47,7 @@ def time_to_string(time: dt):
|
||||
|
||||
|
||||
def get_user_info_text(db_user: User) -> str:
|
||||
user: UserResponse = UserResponse.from_orm(db_user)
|
||||
user: UserResponse = UserResponse.model_validate(db_user)
|
||||
data_limit = readable_size(user.data_limit) if user.data_limit else "Unlimited"
|
||||
used_traffic = readable_size(user.used_traffic) if user.used_traffic else "-"
|
||||
data_left = readable_size(user.data_limit - user.used_traffic) if user.data_limit else "-"
|
||||
@@ -58,7 +58,8 @@ def get_user_info_text(db_user: User) -> str:
|
||||
online_at = time_to_string(user.online_at) if user.online_at else "-"
|
||||
sub_updated_at = time_to_string(user.sub_updated_at) if user.sub_updated_at else "-"
|
||||
if user.status == UserStatus.on_hold:
|
||||
expiry_text = f"⏰ <b>On Hold Duration:</b> <code>{on_hold_duration} days</code> (auto start at <code>{on_hold_timeout}</code>)"
|
||||
expiry_text = f"⏰ <b>On Hold Duration:</b> <code>{on_hold_duration} days</code> (auto start at <code>{
|
||||
on_hold_timeout}</code>)"
|
||||
else:
|
||||
expiry_text = f"📅 <b>Expiry Date:</b> <code>{expiry_date}</code> ({time_left})"
|
||||
return f"""\
|
||||
@@ -71,7 +72,7 @@ def get_user_info_text(db_user: User) -> str:
|
||||
{expiry_text}
|
||||
|
||||
🔌 <b>Online at:</b> {online_at}
|
||||
🔄 <b>Subscription updated at:</b> {sub_updated_at}
|
||||
🔄 <b>Subscription updated at:</b> {sub_updated_at}
|
||||
📱 <b>Subscription last agent:</b> <blockquote>{user.sub_last_user_agent or "-"}</blockquote>
|
||||
|
||||
📝 <b>Note:</b> <blockquote expandable>{user.note or "empty"}</blockquote>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from datetime import datetime as dt
|
||||
import json
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
def calculate_usage_percent(used_traffic: int, data_limit: int) -> float:
|
||||
@@ -7,3 +9,15 @@ def calculate_usage_percent(used_traffic: int, data_limit: int) -> float:
|
||||
|
||||
def calculate_expiration_days(expire: int) -> int:
|
||||
return (dt.fromtimestamp(expire) - dt.utcnow()).days
|
||||
|
||||
|
||||
def yml_uuid_representer(dumper, data):
|
||||
return dumper.represent_scalar('tag:yaml.org,2002:str', str(data))
|
||||
|
||||
|
||||
class UUIDEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, UUID):
|
||||
# if the obj is uuid, we simply return the value of uuid
|
||||
return obj.hex
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
@@ -101,7 +101,7 @@ class UserDataResetByNext(UserNotification):
|
||||
|
||||
|
||||
class UserSubscriptionRevoked(UserNotification):
|
||||
action = Notification.Type = Notification.Type.subscription_revoked
|
||||
action: Notification.Type = Notification.Type.subscription_revoked
|
||||
by: Admin
|
||||
user: UserResponse
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ def _alter_inbound_user(api: XRayAPI, inbound_tag: str, account: Account):
|
||||
|
||||
|
||||
def add_user(dbuser: "DBUser"):
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
email = f"{dbuser.id}.{dbuser.username}"
|
||||
|
||||
for proxy_type, inbound_tags in user.inbounds.items():
|
||||
@@ -101,7 +101,7 @@ def remove_user(dbuser: "DBUser"):
|
||||
|
||||
|
||||
def update_user(dbuser: "DBUser"):
|
||||
user = UserResponse.from_orm(dbuser)
|
||||
user = UserResponse.model_validate(dbuser)
|
||||
email = f"{dbuser.id}.{dbuser.username}"
|
||||
|
||||
active_inbounds = []
|
||||
|
||||
@@ -29,7 +29,7 @@ def get_link(
|
||||
in order to work correctly.
|
||||
"""
|
||||
with GetDB() as db:
|
||||
user: UserResponse = UserResponse.from_orm(utils.get_user(db, username))
|
||||
user: UserResponse = UserResponse.model_validate(utils.get_user(db, username))
|
||||
print(user.subscription_url)
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ def get_config(
|
||||
otherwise will be shown in the terminal.
|
||||
"""
|
||||
with GetDB() as db:
|
||||
user: UserResponse = UserResponse.from_orm(utils.get_user(db, username))
|
||||
user: UserResponse = UserResponse.model_validate(utils.get_user(db, username))
|
||||
conf: str = generate_subscription(
|
||||
user=user, config_format=config_format.name, as_base64=as_base64
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ psutil==5.9.4
|
||||
pyOpenSSL==24.2.1
|
||||
PySocks==1.7.1
|
||||
pyTelegramBotAPI==4.9.0
|
||||
pydantic==1.10.16
|
||||
pydantic==2.10.3
|
||||
python-dateutil==2.8.2
|
||||
python-decouple==3.6
|
||||
python-dotenv==0.21.1
|
||||
@@ -37,8 +37,7 @@ rsa==4.9
|
||||
sniffio==1.3.0
|
||||
starlette==0.40.0
|
||||
typer==0.7.0
|
||||
typing_extensions==4.9.0
|
||||
urllib3==1.26.19
|
||||
uvicorn==0.27.0.post1
|
||||
websocket-client==1.7.0
|
||||
websockets==12.0
|
||||
websockets==12.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from abc import ABC, abstractproperty
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from uuid import UUID
|
||||
|
||||
@@ -19,8 +19,9 @@ class Account(BaseModel, ABC):
|
||||
email: str
|
||||
level: int = 0
|
||||
|
||||
@abstractproperty
|
||||
def message(self) -> TypedMessage:
|
||||
@property
|
||||
@abstractmethod
|
||||
def message(self):
|
||||
pass
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
||||
Reference in New Issue
Block a user