| 1 |
a47eb4c |
Dawsh |
2y ago |
import re |
| 2 |
12b255b |
Dawsh |
2y ago |
import secrets |
| 3 |
a47eb4c |
Dawsh |
2y ago |
from datetime import datetime |
| 4 |
a47eb4c |
Dawsh |
2y ago |
from enum import Enum |
| 5 |
6457378 |
Dawsh |
2y ago |
from typing import List, Optional, Union, Annotated |
| 6 |
a47eb4c |
Dawsh |
2y ago |
|
| 7 |
6457378 |
Dawsh |
2y ago |
from pydantic import field_validator, ConfigDict, BaseModel, Field, validator, computed_field, ValidationInfo, constr, \ |
| 8 |
6457378 |
Dawsh |
2y ago |
StringConstraints |
| 9 |
a47eb4c |
Dawsh |
2y ago |
|
| 10 |
6a606d6 |
Dawsh |
2y ago |
from config import XRAY_SUBSCRIPTION_URL_PREFIX |
| 11 |
a47eb4c |
Dawsh |
2y ago |
|
| 12 |
8b9d507 |
Dawsh |
2y ago |
USERNAME_REGEXP = re.compile(r"^\w{3,32}$") |
| 13 |
a47eb4c |
Dawsh |
2y ago |
|
| 14 |
a47eb4c |
Dawsh |
2y ago |
|
| 15 |
a47eb4c |
Dawsh |
2y ago |
class ReminderType(str, Enum): |
| 16 |
a47eb4c |
Dawsh |
2y ago |
expiration_date = "expiration_date" |
| 17 |
a47eb4c |
Dawsh |
2y ago |
data_usage = "data_usage" |
| 18 |
a47eb4c |
Dawsh |
2y ago |
|
| 19 |
a47eb4c |
Dawsh |
2y ago |
|
| 20 |
a47eb4c |
Dawsh |
2y ago |
class UserStatus(str, Enum): |
| 21 |
a47eb4c |
Dawsh |
2y ago |
active = "active" |
| 22 |
a47eb4c |
Dawsh |
2y ago |
disabled = "disabled" |
| 23 |
a47eb4c |
Dawsh |
2y ago |
limited = "limited" |
| 24 |
a47eb4c |
Dawsh |
2y ago |
expired = "expired" |
| 25 |
a47eb4c |
Dawsh |
2y ago |
on_hold = "on_hold" |
| 26 |
a47eb4c |
Dawsh |
2y ago |
|
| 27 |
12b255b |
Dawsh |
2y ago |
|
| 28 |
f7af2b3 |
Dawsh |
2y ago |
# from app.utils.share import generate_v2ray_links |
| 29 |
a47eb4c |
Dawsh |
2y ago |
|
| 30 |
12b255b |
Dawsh |
2y ago |
|
| 31 |
a47eb4c |
Dawsh |
2y ago |
class UserStatusModify(str, Enum): |
| 32 |
a47eb4c |
Dawsh |
2y ago |
active = "active" |
| 33 |
a47eb4c |
Dawsh |
2y ago |
disabled = "disabled" |
| 34 |
a47eb4c |
Dawsh |
2y ago |
on_hold = "on_hold" |
| 35 |
a47eb4c |
Dawsh |
2y ago |
|
| 36 |
a47eb4c |
Dawsh |
2y ago |
|
| 37 |
a47eb4c |
Dawsh |
2y ago |
class UserStatusCreate(str, Enum): |
| 38 |
a47eb4c |
Dawsh |
2y ago |
active = "active" |
| 39 |
a47eb4c |
Dawsh |
2y ago |
on_hold = "on_hold" |
| 40 |
a47eb4c |
Dawsh |
2y ago |
|
| 41 |
a47eb4c |
Dawsh |
2y ago |
|
| 42 |
a47eb4c |
Dawsh |
2y ago |
class UserDataLimitResetStrategy(str, Enum): |
| 43 |
a47eb4c |
Dawsh |
2y ago |
no_reset = "no_reset" |
| 44 |
a47eb4c |
Dawsh |
2y ago |
day = "day" |
| 45 |
a47eb4c |
Dawsh |
2y ago |
week = "week" |
| 46 |
a47eb4c |
Dawsh |
2y ago |
month = "month" |
| 47 |
a47eb4c |
Dawsh |
2y ago |
year = "year" |
| 48 |
a47eb4c |
Dawsh |
2y ago |
|
| 49 |
a47eb4c |
Dawsh |
2y ago |
|
| 50 |
a47eb4c |
Dawsh |
2y ago |
class UserBase(BaseModel): |
| 51 |
a47eb4c |
Dawsh |
2y ago |
# proxies: Dict[ProxyTypes, ProxySettings] = {} |
| 52 |
912788f |
Mardin |
2y ago |
id: Optional[int] = None |
| 53 |
6457378 |
Dawsh |
2y ago |
username: Annotated[str, StringConstraints(to_lower=True)] |
| 54 |
a47eb4c |
Dawsh |
2y ago |
expire: Optional[int] = Field(None, nullable=True) |
| 55 |
a47eb4c |
Dawsh |
2y ago |
key: str = Field(default_factory=lambda: secrets.token_hex(16)) |
| 56 |
a47eb4c |
Dawsh |
2y ago |
data_limit: Optional[int] = Field( |
| 57 |
a47eb4c |
Dawsh |
2y ago |
ge=0, default=None, description="data_limit can be 0 or greater" |
| 58 |
a47eb4c |
Dawsh |
2y ago |
) |
| 59 |
a47eb4c |
Dawsh |
2y ago |
data_limit_reset_strategy: UserDataLimitResetStrategy = ( |
| 60 |
a47eb4c |
Dawsh |
2y ago |
UserDataLimitResetStrategy.no_reset |
| 61 |
a47eb4c |
Dawsh |
2y ago |
) |
| 62 |
a47eb4c |
Dawsh |
2y ago |
# inbounds: Dict[ProxyTypes, List[str]] = {} |
| 63 |
a47eb4c |
Dawsh |
2y ago |
note: Optional[str] = Field(None, nullable=True) |
| 64 |
a47eb4c |
Dawsh |
2y ago |
sub_updated_at: Optional[datetime] = Field(None, nullable=True) |
| 65 |
a47eb4c |
Dawsh |
2y ago |
sub_last_user_agent: Optional[str] = Field(None, nullable=True) |
| 66 |
a47eb4c |
Dawsh |
2y ago |
online_at: Optional[datetime] = Field(None, nullable=True) |
| 67 |
a47eb4c |
Dawsh |
2y ago |
on_hold_expire_duration: Optional[int] = Field(None, nullable=True) |
| 68 |
a47eb4c |
Dawsh |
2y ago |
on_hold_timeout: Optional[Union[datetime, None]] = Field(None, nullable=True) |
| 69 |
a47eb4c |
Dawsh |
2y ago |
|
| 70 |
a47eb4c |
Dawsh |
2y ago |
@field_validator("username") |
| 71 |
a47eb4c |
Dawsh |
2y ago |
@classmethod |
| 72 |
a47eb4c |
Dawsh |
2y ago |
def validate_username(cls, v: str): |
| 73 |
a47eb4c |
Dawsh |
2y ago |
if not USERNAME_REGEXP.match(v): |
| 74 |
a47eb4c |
Dawsh |
2y ago |
raise ValueError( |
| 75 |
8b9d507 |
Dawsh |
2y ago |
"Username should be 3 to 32 characters containing A-Z, a-z, 0-9 and underscores in between." |
| 76 |
a47eb4c |
Dawsh |
2y ago |
) |
| 77 |
a47eb4c |
Dawsh |
2y ago |
return v |
| 78 |
a47eb4c |
Dawsh |
2y ago |
|
| 79 |
a47eb4c |
Dawsh |
2y ago |
@field_validator("note") |
| 80 |
a47eb4c |
Dawsh |
2y ago |
@classmethod |
| 81 |
a47eb4c |
Dawsh |
2y ago |
def validate_note(cls, v: str): |
| 82 |
a47eb4c |
Dawsh |
2y ago |
if v and len(v) > 500: |
| 83 |
a47eb4c |
Dawsh |
2y ago |
raise ValueError("User's note can be a maximum of 500 character") |
| 84 |
a47eb4c |
Dawsh |
2y ago |
return v |
| 85 |
a47eb4c |
Dawsh |
2y ago |
|
| 86 |
a47eb4c |
Dawsh |
2y ago |
@field_validator("on_hold_expire_duration", "on_hold_timeout", check_fields=False) |
| 87 |
a47eb4c |
Dawsh |
2y ago |
@classmethod |
| 88 |
a47eb4c |
Dawsh |
2y ago |
def validate_timeout(cls, v: Optional[int]): |
| 89 |
a47eb4c |
Dawsh |
2y ago |
# Check if expire is 0 or None and timeout is not 0 or None |
| 90 |
a47eb4c |
Dawsh |
2y ago |
if (v in (0, None)): |
| 91 |
a47eb4c |
Dawsh |
2y ago |
return None |
| 92 |
a47eb4c |
Dawsh |
2y ago |
return v |
| 93 |
a47eb4c |
Dawsh |
2y ago |
model_config = ConfigDict(from_attributes=True) |
| 94 |
a47eb4c |
Dawsh |
2y ago |
|
| 95 |
4d647dd |
Dawsh |
2y ago |
|
| 96 |
a47eb4c |
Dawsh |
2y ago |
from app.models.service import ServiceBase |
| 97 |
f6a32ea |
Dawsh |
2y ago |
from app.models.proxy import InboundBase |
| 98 |
a47eb4c |
Dawsh |
2y ago |
|
| 99 |
4d647dd |
Dawsh |
2y ago |
|
| 100 |
6457378 |
Dawsh |
2y ago |
class User(UserBase): |
| 101 |
a47eb4c |
Dawsh |
2y ago |
pass |
| 102 |
a47eb4c |
Dawsh |
2y ago |
|
| 103 |
a47eb4c |
Dawsh |
2y ago |
|
| 104 |
a47eb4c |
Dawsh |
2y ago |
class UserCreate(User): |
| 105 |
a47eb4c |
Dawsh |
2y ago |
status: UserStatusCreate = Field(None, validate_default=True) |
| 106 |
a47eb4c |
Dawsh |
2y ago |
services: List[int] = Field([]) |
| 107 |
a47eb4c |
Dawsh |
2y ago |
model_config = ConfigDict(json_schema_extra={ |
| 108 |
a47eb4c |
Dawsh |
2y ago |
"example": { |
| 109 |
a47eb4c |
Dawsh |
2y ago |
"username": "user1234", |
| 110 |
a47eb4c |
Dawsh |
2y ago |
"services": [1, 2, 3], |
| 111 |
a47eb4c |
Dawsh |
2y ago |
"expire": 0, |
| 112 |
a47eb4c |
Dawsh |
2y ago |
"data_limit": 0, |
| 113 |
a47eb4c |
Dawsh |
2y ago |
"data_limit_reset_strategy": "no_reset", |
| 114 |
a47eb4c |
Dawsh |
2y ago |
"status": "active", |
| 115 |
a47eb4c |
Dawsh |
2y ago |
"note": "", |
| 116 |
a47eb4c |
Dawsh |
2y ago |
"on_hold_timeout": "2023-11-03T20:30:00", |
| 117 |
a47eb4c |
Dawsh |
2y ago |
"on_hold_expire_duration": 0, |
| 118 |
a47eb4c |
Dawsh |
2y ago |
} |
| 119 |
a47eb4c |
Dawsh |
2y ago |
}) |
| 120 |
a47eb4c |
Dawsh |
2y ago |
|
| 121 |
a47eb4c |
Dawsh |
2y ago |
# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. |
| 122 |
a47eb4c |
Dawsh |
2y ago |
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. |
| 123 |
a47eb4c |
Dawsh |
2y ago |
#@validator("status", pre=True, always=True) |
| 124 |
a47eb4c |
Dawsh |
2y ago |
#def validate_status(cls, value): |
| 125 |
a47eb4c |
Dawsh |
2y ago |
|
| 126 |
a47eb4c |
Dawsh |
2y ago |
# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. |
| 127 |
a47eb4c |
Dawsh |
2y ago |
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. |
| 128 |
a47eb4c |
Dawsh |
2y ago |
@field_validator("status", mode="before") |
| 129 |
a47eb4c |
Dawsh |
2y ago |
def validate_status(cls, status: str, info: ValidationInfo) -> str: |
| 130 |
a47eb4c |
Dawsh |
2y ago |
if not status or status not in UserStatusCreate.__members__: |
| 131 |
a47eb4c |
Dawsh |
2y ago |
return UserStatusCreate.active # Set to the default if not valid |
| 132 |
a47eb4c |
Dawsh |
2y ago |
on_hold_expire = info.data.get("on_hold_expire_duration") |
| 133 |
a47eb4c |
Dawsh |
2y ago |
expire = info.data.get("expire") |
| 134 |
a47eb4c |
Dawsh |
2y ago |
if status == UserStatusCreate.on_hold: |
| 135 |
4d647dd |
Dawsh |
2y ago |
if on_hold_expire == 0 or on_hold_expire is None: |
| 136 |
a47eb4c |
Dawsh |
2y ago |
raise ValueError("User cannot be on hold without a valid on_hold_expire_duration.") |
| 137 |
a47eb4c |
Dawsh |
2y ago |
if expire: |
| 138 |
a47eb4c |
Dawsh |
2y ago |
raise ValueError("User cannot be on hold with specified expire.") |
| 139 |
a47eb4c |
Dawsh |
2y ago |
return status |
| 140 |
a47eb4c |
Dawsh |
2y ago |
|
| 141 |
a47eb4c |
Dawsh |
2y ago |
|
| 142 |
a47eb4c |
Dawsh |
2y ago |
class UserModify(User): |
| 143 |
9bfa731 |
Dawsh |
2y ago |
status: Optional[UserStatusModify] = Field(None) |
| 144 |
9bfa731 |
Dawsh |
2y ago |
services: List[int] = Field([]) |
| 145 |
9bfa731 |
Dawsh |
2y ago |
data_limit_reset_strategy: Optional[UserDataLimitResetStrategy] = Field(None) |
| 146 |
a47eb4c |
Dawsh |
2y ago |
model_config = ConfigDict(json_schema_extra={ |
| 147 |
a47eb4c |
Dawsh |
2y ago |
"example": { |
| 148 |
a47eb4c |
Dawsh |
2y ago |
"username": "mammad1234", |
| 149 |
a47eb4c |
Dawsh |
2y ago |
"services": [1, 2, 3], |
| 150 |
a47eb4c |
Dawsh |
2y ago |
"expire": 0, |
| 151 |
a47eb4c |
Dawsh |
2y ago |
"data_limit": 0, |
| 152 |
a47eb4c |
Dawsh |
2y ago |
"data_limit_reset_strategy": "no_reset", |
| 153 |
a47eb4c |
Dawsh |
2y ago |
"status": "active", |
| 154 |
a47eb4c |
Dawsh |
2y ago |
"note": "", |
| 155 |
a47eb4c |
Dawsh |
2y ago |
"on_hold_timeout": "2023-11-03T20:30:00", |
| 156 |
a47eb4c |
Dawsh |
2y ago |
"on_hold_expire_duration": 0, |
| 157 |
a47eb4c |
Dawsh |
2y ago |
} |
| 158 |
a47eb4c |
Dawsh |
2y ago |
}) |
| 159 |
a47eb4c |
Dawsh |
2y ago |
|
| 160 |
a47eb4c |
Dawsh |
2y ago |
|
| 161 |
a47eb4c |
Dawsh |
2y ago |
# TODO[pydantic]: We couldn't refactor the `validator`, please replace it by `field_validator` manually. |
| 162 |
a47eb4c |
Dawsh |
2y ago |
# Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-validators for more information. |
| 163 |
a47eb4c |
Dawsh |
2y ago |
@validator("status", pre=True, always=True) |
| 164 |
a47eb4c |
Dawsh |
2y ago |
def validate_status(cls, status, values): |
| 165 |
a47eb4c |
Dawsh |
2y ago |
on_hold_expire = values.get("on_hold_expire_duration") |
| 166 |
a47eb4c |
Dawsh |
2y ago |
expire = values.get("expire") |
| 167 |
a47eb4c |
Dawsh |
2y ago |
if status == UserStatusCreate.on_hold: |
| 168 |
a47eb4c |
Dawsh |
2y ago |
if not on_hold_expire: |
| 169 |
a47eb4c |
Dawsh |
2y ago |
raise ValueError("User cannot be on hold without a valid on_hold_expire_duration.") |
| 170 |
a47eb4c |
Dawsh |
2y ago |
if expire: |
| 171 |
a47eb4c |
Dawsh |
2y ago |
raise ValueError("User cannot be on hold with specified expire.") |
| 172 |
a47eb4c |
Dawsh |
2y ago |
return status |
| 173 |
a47eb4c |
Dawsh |
2y ago |
|
| 174 |
a47eb4c |
Dawsh |
2y ago |
|
| 175 |
a47eb4c |
Dawsh |
2y ago |
class UserResponse(User): |
| 176 |
a47eb4c |
Dawsh |
2y ago |
id: int |
| 177 |
a47eb4c |
Dawsh |
2y ago |
status: UserStatus |
| 178 |
a47eb4c |
Dawsh |
2y ago |
used_traffic: int |
| 179 |
a47eb4c |
Dawsh |
2y ago |
lifetime_used_traffic: int = 0 |
| 180 |
a47eb4c |
Dawsh |
2y ago |
created_at: datetime |
| 181 |
a47eb4c |
Dawsh |
2y ago |
inbounds: List[InboundBase] = Field([]) |
| 182 |
a47eb4c |
Dawsh |
2y ago |
services: List[ServiceBase] = Field([]) |
| 183 |
f7af2b3 |
Dawsh |
2y ago |
links: List[str] | None = [] # Field(None) |
| 184 |
9bfa731 |
Dawsh |
2y ago |
|
| 185 |
a47eb4c |
Dawsh |
2y ago |
# subscription_url: str = "" |
| 186 |
a47eb4c |
Dawsh |
2y ago |
model_config = ConfigDict(from_attributes=True) |
| 187 |
9bfa731 |
Dawsh |
2y ago |
|
| 188 |
5f664c5 |
Dawsh |
2y ago |
@computed_field |
| 189 |
5f664c5 |
Dawsh |
2y ago |
@property |
| 190 |
5f664c5 |
Dawsh |
2y ago |
def service_ids(self) -> List[int]: |
| 191 |
5f664c5 |
Dawsh |
2y ago |
return [s.id for s in self.services] |
| 192 |
5f664c5 |
Dawsh |
2y ago |
|
| 193 |
a47eb4c |
Dawsh |
2y ago |
@computed_field |
| 194 |
a47eb4c |
Dawsh |
2y ago |
@property |
| 195 |
a47eb4c |
Dawsh |
2y ago |
def subscription_url(self) -> str: |
| 196 |
a47eb4c |
Dawsh |
2y ago |
salt = secrets.token_hex(8) |
| 197 |
a47eb4c |
Dawsh |
2y ago |
url_prefix = (XRAY_SUBSCRIPTION_URL_PREFIX).replace('*', salt) |
| 198 |
6a606d6 |
Dawsh |
2y ago |
return f"{url_prefix}/sub/{self.username}/{self.key}" |
| 199 |
a47eb4c |
Dawsh |
2y ago |
|
| 200 |
a47eb4c |
Dawsh |
2y ago |
|
| 201 |
a47eb4c |
Dawsh |
2y ago |
class UsersResponse(BaseModel): |
| 202 |
a47eb4c |
Dawsh |
2y ago |
total: int |
| 203 |
a47eb4c |
Dawsh |
2y ago |
users: List[UserResponse] |
| 204 |
a47eb4c |
Dawsh |
2y ago |
|
| 205 |
a47eb4c |
Dawsh |
2y ago |
|
| 206 |
a47eb4c |
Dawsh |
2y ago |
class UserUsageResponse(BaseModel): |
| 207 |
a47eb4c |
Dawsh |
2y ago |
node_id: Union[int, None] = None |
| 208 |
a47eb4c |
Dawsh |
2y ago |
node_name: str |
| 209 |
a47eb4c |
Dawsh |
2y ago |
used_traffic: int |
| 210 |
a47eb4c |
Dawsh |
2y ago |
|
| 211 |
a47eb4c |
Dawsh |
2y ago |
|
| 212 |
a47eb4c |
Dawsh |
2y ago |
class UserUsagesResponse(BaseModel): |
| 213 |
a47eb4c |
Dawsh |
2y ago |
username: str |
| 214 |
a47eb4c |
Dawsh |
2y ago |
usages: List[UserUsageResponse] |