| 1 |
12b255b |
Dawsh |
2y ago |
import secrets |
| 2 |
a47eb4c |
Dawsh |
2y ago |
from datetime import datetime |
| 3 |
a47eb4c |
Dawsh |
2y ago |
from enum import Enum |
| 4 |
857e289 |
Mrdn |
2y ago |
from typing import List, Optional, Union, Annotated, Literal |
| 5 |
a47eb4c |
Dawsh |
2y ago |
|
| 6 |
01cdf2e |
Dawsh |
2y ago |
from pydantic import ( |
| 7 |
01cdf2e |
Dawsh |
2y ago |
field_validator, |
| 8 |
01cdf2e |
Dawsh |
2y ago |
ConfigDict, |
| 9 |
01cdf2e |
Dawsh |
2y ago |
BaseModel, |
| 10 |
01cdf2e |
Dawsh |
2y ago |
Field, |
| 11 |
01cdf2e |
Dawsh |
2y ago |
computed_field, |
| 12 |
01cdf2e |
Dawsh |
2y ago |
StringConstraints, |
| 13 |
857e289 |
Mrdn |
2y ago |
model_validator, |
| 14 |
01cdf2e |
Dawsh |
2y ago |
) |
| 15 |
a47eb4c |
Dawsh |
2y ago |
|
| 16 |
5c78ce8 |
Dawsh |
2y ago |
from config import SUBSCRIPTION_URL_PREFIX |
| 17 |
a47eb4c |
Dawsh |
2y ago |
|
| 18 |
f446990 |
Dawsh |
2y ago |
USERNAME_REGEXP = r"^\w{3,32}$" |
| 19 |
a47eb4c |
Dawsh |
2y ago |
|
| 20 |
a47eb4c |
Dawsh |
2y ago |
|
| 21 |
a47eb4c |
Dawsh |
2y ago |
class ReminderType(str, Enum): |
| 22 |
a47eb4c |
Dawsh |
2y ago |
expiration_date = "expiration_date" |
| 23 |
a47eb4c |
Dawsh |
2y ago |
data_usage = "data_usage" |
| 24 |
a47eb4c |
Dawsh |
2y ago |
|
| 25 |
a47eb4c |
Dawsh |
2y ago |
|
| 26 |
a47eb4c |
Dawsh |
2y ago |
class UserStatus(str, Enum): |
| 27 |
a47eb4c |
Dawsh |
2y ago |
active = "active" |
| 28 |
cba9caa |
Dawsh |
2y ago |
on_hold = "on_hold" |
| 29 |
a47eb4c |
Dawsh |
2y ago |
limited = "limited" |
| 30 |
a47eb4c |
Dawsh |
2y ago |
expired = "expired" |
| 31 |
a47eb4c |
Dawsh |
2y ago |
|
| 32 |
12b255b |
Dawsh |
2y ago |
|
| 33 |
a47eb4c |
Dawsh |
2y ago |
class UserStatusModify(str, Enum): |
| 34 |
a47eb4c |
Dawsh |
2y ago |
active = "active" |
| 35 |
a47eb4c |
Dawsh |
2y ago |
on_hold = "on_hold" |
| 36 |
a47eb4c |
Dawsh |
2y ago |
|
| 37 |
a47eb4c |
Dawsh |
2y ago |
|
| 38 |
a47eb4c |
Dawsh |
2y ago |
class UserStatusCreate(str, Enum): |
| 39 |
a47eb4c |
Dawsh |
2y ago |
active = "active" |
| 40 |
a47eb4c |
Dawsh |
2y ago |
on_hold = "on_hold" |
| 41 |
a47eb4c |
Dawsh |
2y ago |
|
| 42 |
a47eb4c |
Dawsh |
2y ago |
|
| 43 |
a47eb4c |
Dawsh |
2y ago |
class UserDataLimitResetStrategy(str, Enum): |
| 44 |
a47eb4c |
Dawsh |
2y ago |
no_reset = "no_reset" |
| 45 |
a47eb4c |
Dawsh |
2y ago |
day = "day" |
| 46 |
a47eb4c |
Dawsh |
2y ago |
week = "week" |
| 47 |
a47eb4c |
Dawsh |
2y ago |
month = "month" |
| 48 |
a47eb4c |
Dawsh |
2y ago |
year = "year" |
| 49 |
a47eb4c |
Dawsh |
2y ago |
|
| 50 |
a47eb4c |
Dawsh |
2y ago |
|
| 51 |
a47eb4c |
Dawsh |
2y ago |
class UserBase(BaseModel): |
| 52 |
912788f |
Mardin |
2y ago |
id: Optional[int] = None |
| 53 |
f446990 |
Dawsh |
2y ago |
username: Annotated[str, StringConstraints(to_lower=True, pattern=USERNAME_REGEXP)] |
| 54 |
857e289 |
Mrdn |
2y ago |
expire: Union[datetime, None, Literal[0]] = Field(None) |
| 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 |
cba9caa |
Dawsh |
2y ago |
enabled: bool = Field(default=True) |
| 60 |
a47eb4c |
Dawsh |
2y ago |
data_limit_reset_strategy: UserDataLimitResetStrategy = ( |
| 61 |
a47eb4c |
Dawsh |
2y ago |
UserDataLimitResetStrategy.no_reset |
| 62 |
a47eb4c |
Dawsh |
2y ago |
) |
| 63 |
f446990 |
Dawsh |
2y ago |
note: Optional[Annotated[str, Field(max_length=500)]] = None |
| 64 |
857e289 |
Mrdn |
2y ago |
sub_updated_at: Optional[datetime] = Field(None) |
| 65 |
857e289 |
Mrdn |
2y ago |
sub_last_user_agent: Optional[str] = Field(None) |
| 66 |
857e289 |
Mrdn |
2y ago |
online_at: Optional[datetime] = Field(None) |
| 67 |
857e289 |
Mrdn |
2y ago |
on_hold_expire_duration: Optional[int] = Field(None) |
| 68 |
857e289 |
Mrdn |
2y ago |
on_hold_timeout: Optional[datetime] = Field(None) |
| 69 |
a47eb4c |
Dawsh |
2y ago |
|
| 70 |
a47eb4c |
Dawsh |
2y ago |
model_config = ConfigDict(from_attributes=True) |
| 71 |
a47eb4c |
Dawsh |
2y ago |
|
| 72 |
4d647dd |
Dawsh |
2y ago |
|
| 73 |
a47eb4c |
Dawsh |
2y ago |
from app.models.service import ServiceBase |
| 74 |
f6a32ea |
Dawsh |
2y ago |
from app.models.proxy import InboundBase |
| 75 |
a47eb4c |
Dawsh |
2y ago |
|
| 76 |
4d647dd |
Dawsh |
2y ago |
|
| 77 |
6457378 |
Dawsh |
2y ago |
class User(UserBase): |
| 78 |
a47eb4c |
Dawsh |
2y ago |
pass |
| 79 |
a47eb4c |
Dawsh |
2y ago |
|
| 80 |
a47eb4c |
Dawsh |
2y ago |
|
| 81 |
a47eb4c |
Dawsh |
2y ago |
class UserCreate(User): |
| 82 |
857e289 |
Mrdn |
2y ago |
status: UserStatusCreate = Field(UserStatusCreate.active) |
| 83 |
a47eb4c |
Dawsh |
2y ago |
services: List[int] = Field([]) |
| 84 |
01cdf2e |
Dawsh |
2y ago |
model_config = ConfigDict( |
| 85 |
01cdf2e |
Dawsh |
2y ago |
json_schema_extra={ |
| 86 |
01cdf2e |
Dawsh |
2y ago |
"example": { |
| 87 |
01cdf2e |
Dawsh |
2y ago |
"username": "user1234", |
| 88 |
01cdf2e |
Dawsh |
2y ago |
"services": [1, 2, 3], |
| 89 |
3b7feef |
Mrdn |
2y ago |
"expire": "2023-11-03T20:30:00", |
| 90 |
01cdf2e |
Dawsh |
2y ago |
"data_limit": 0, |
| 91 |
01cdf2e |
Dawsh |
2y ago |
"data_limit_reset_strategy": "no_reset", |
| 92 |
01cdf2e |
Dawsh |
2y ago |
"status": "active", |
| 93 |
01cdf2e |
Dawsh |
2y ago |
"note": "", |
| 94 |
01cdf2e |
Dawsh |
2y ago |
"on_hold_timeout": "2023-11-03T20:30:00", |
| 95 |
01cdf2e |
Dawsh |
2y ago |
"on_hold_expire_duration": 0, |
| 96 |
01cdf2e |
Dawsh |
2y ago |
} |
| 97 |
a47eb4c |
Dawsh |
2y ago |
} |
| 98 |
01cdf2e |
Dawsh |
2y ago |
) |
| 99 |
a47eb4c |
Dawsh |
2y ago |
|
| 100 |
857e289 |
Mrdn |
2y ago |
@model_validator(mode="after") |
| 101 |
857e289 |
Mrdn |
2y ago |
def validate_expiry(self): |
| 102 |
857e289 |
Mrdn |
2y ago |
if self.status == UserStatusCreate.on_hold: |
| 103 |
857e289 |
Mrdn |
2y ago |
if not self.on_hold_expire_duration: |
| 104 |
01cdf2e |
Dawsh |
2y ago |
raise ValueError( |
| 105 |
01cdf2e |
Dawsh |
2y ago |
"User cannot be on hold without a valid on_hold_expire_duration." |
| 106 |
01cdf2e |
Dawsh |
2y ago |
) |
| 107 |
857e289 |
Mrdn |
2y ago |
if self.expire: |
| 108 |
a47eb4c |
Dawsh |
2y ago |
raise ValueError("User cannot be on hold with specified expire.") |
| 109 |
5c78ce8 |
Dawsh |
2y ago |
elif self.status == UserStatusCreate.active and ( |
| 110 |
5c78ce8 |
Dawsh |
2y ago |
self.on_hold_expire_duration or self.on_hold_timeout |
| 111 |
5c78ce8 |
Dawsh |
2y ago |
): |
| 112 |
5c78ce8 |
Dawsh |
2y ago |
raise ValueError("on hold parametrs set when user status isn't on_hold.") |
| 113 |
857e289 |
Mrdn |
2y ago |
return self |
| 114 |
857e289 |
Mrdn |
2y ago |
|
| 115 |
857e289 |
Mrdn |
2y ago |
@field_validator("expire") |
| 116 |
857e289 |
Mrdn |
2y ago |
@classmethod |
| 117 |
f446990 |
Dawsh |
2y ago |
def validate_expire(cls, v: Union[datetime, None, Literal[0]]): |
| 118 |
f446990 |
Dawsh |
2y ago |
if isinstance(v, datetime) and (v.tzinfo is not None): |
| 119 |
857e289 |
Mrdn |
2y ago |
raise ValueError( |
| 120 |
857e289 |
Mrdn |
2y ago |
"Expire date should be offset naive, and preferably in utc timezone." |
| 121 |
857e289 |
Mrdn |
2y ago |
) |
| 122 |
857e289 |
Mrdn |
2y ago |
if isinstance(v, datetime) and v < datetime.utcnow(): |
| 123 |
857e289 |
Mrdn |
2y ago |
raise ValueError("Expire date should be in the future, not in the past.") |
| 124 |
857e289 |
Mrdn |
2y ago |
return v |
| 125 |
a47eb4c |
Dawsh |
2y ago |
|
| 126 |
a47eb4c |
Dawsh |
2y ago |
|
| 127 |
a47eb4c |
Dawsh |
2y ago |
class UserModify(User): |
| 128 |
9bfa731 |
Dawsh |
2y ago |
status: Optional[UserStatusModify] = Field(None) |
| 129 |
9bfa731 |
Dawsh |
2y ago |
services: List[int] = Field([]) |
| 130 |
9bfa731 |
Dawsh |
2y ago |
data_limit_reset_strategy: Optional[UserDataLimitResetStrategy] = Field(None) |
| 131 |
01cdf2e |
Dawsh |
2y ago |
model_config = ConfigDict( |
| 132 |
01cdf2e |
Dawsh |
2y ago |
json_schema_extra={ |
| 133 |
01cdf2e |
Dawsh |
2y ago |
"example": { |
| 134 |
01cdf2e |
Dawsh |
2y ago |
"username": "mammad1234", |
| 135 |
01cdf2e |
Dawsh |
2y ago |
"services": [1, 2, 3], |
| 136 |
01cdf2e |
Dawsh |
2y ago |
"expire": 0, |
| 137 |
01cdf2e |
Dawsh |
2y ago |
"data_limit": 0, |
| 138 |
01cdf2e |
Dawsh |
2y ago |
"data_limit_reset_strategy": "no_reset", |
| 139 |
01cdf2e |
Dawsh |
2y ago |
"status": "active", |
| 140 |
01cdf2e |
Dawsh |
2y ago |
"note": "", |
| 141 |
01cdf2e |
Dawsh |
2y ago |
"on_hold_timeout": "2023-11-03T20:30:00", |
| 142 |
01cdf2e |
Dawsh |
2y ago |
"on_hold_expire_duration": 0, |
| 143 |
01cdf2e |
Dawsh |
2y ago |
} |
| 144 |
a47eb4c |
Dawsh |
2y ago |
} |
| 145 |
01cdf2e |
Dawsh |
2y ago |
) |
| 146 |
a47eb4c |
Dawsh |
2y ago |
|
| 147 |
857e289 |
Mrdn |
2y ago |
@model_validator(mode="after") |
| 148 |
857e289 |
Mrdn |
2y ago |
def validate_expiry(self): |
| 149 |
857e289 |
Mrdn |
2y ago |
if self.on_hold_expire_duration and self.expire: |
| 150 |
857e289 |
Mrdn |
2y ago |
raise ValueError( |
| 151 |
857e289 |
Mrdn |
2y ago |
"can't set both expire and on hold expire at the same time." |
| 152 |
857e289 |
Mrdn |
2y ago |
) |
| 153 |
857e289 |
Mrdn |
2y ago |
return self |
| 154 |
857e289 |
Mrdn |
2y ago |
|
| 155 |
857e289 |
Mrdn |
2y ago |
@field_validator("expire") |
| 156 |
857e289 |
Mrdn |
2y ago |
@classmethod |
| 157 |
f446990 |
Dawsh |
2y ago |
def validate_expire(cls, v: Union[datetime, None, Literal[0]]): |
| 158 |
857e289 |
Mrdn |
2y ago |
if isinstance(v, datetime) and v.tzinfo is not None: |
| 159 |
857e289 |
Mrdn |
2y ago |
raise ValueError( |
| 160 |
857e289 |
Mrdn |
2y ago |
"Expire date should be offset naive, and preferably in utc timezone." |
| 161 |
857e289 |
Mrdn |
2y ago |
) |
| 162 |
857e289 |
Mrdn |
2y ago |
if isinstance(v, datetime) and v < datetime.utcnow(): |
| 163 |
857e289 |
Mrdn |
2y ago |
raise ValueError("Expire date should be in the future, not in the past.") |
| 164 |
857e289 |
Mrdn |
2y ago |
return v |
| 165 |
a47eb4c |
Dawsh |
2y ago |
|
| 166 |
a47eb4c |
Dawsh |
2y ago |
|
| 167 |
a47eb4c |
Dawsh |
2y ago |
class UserResponse(User): |
| 168 |
a47eb4c |
Dawsh |
2y ago |
id: int |
| 169 |
a47eb4c |
Dawsh |
2y ago |
status: UserStatus |
| 170 |
a47eb4c |
Dawsh |
2y ago |
used_traffic: int |
| 171 |
a47eb4c |
Dawsh |
2y ago |
lifetime_used_traffic: int = 0 |
| 172 |
a47eb4c |
Dawsh |
2y ago |
created_at: datetime |
| 173 |
a47eb4c |
Dawsh |
2y ago |
inbounds: List[InboundBase] = Field([]) |
| 174 |
a47eb4c |
Dawsh |
2y ago |
services: List[ServiceBase] = Field([]) |
| 175 |
f7af2b3 |
Dawsh |
2y ago |
links: List[str] | None = [] # Field(None) |
| 176 |
9bfa731 |
Dawsh |
2y ago |
|
| 177 |
a47eb4c |
Dawsh |
2y ago |
# subscription_url: str = "" |
| 178 |
a47eb4c |
Dawsh |
2y ago |
model_config = ConfigDict(from_attributes=True) |
| 179 |
9bfa731 |
Dawsh |
2y ago |
|
| 180 |
5f664c5 |
Dawsh |
2y ago |
@computed_field |
| 181 |
5f664c5 |
Dawsh |
2y ago |
@property |
| 182 |
5f664c5 |
Dawsh |
2y ago |
def service_ids(self) -> List[int]: |
| 183 |
5f664c5 |
Dawsh |
2y ago |
return [s.id for s in self.services] |
| 184 |
5f664c5 |
Dawsh |
2y ago |
|
| 185 |
a47eb4c |
Dawsh |
2y ago |
@computed_field |
| 186 |
a47eb4c |
Dawsh |
2y ago |
@property |
| 187 |
a47eb4c |
Dawsh |
2y ago |
def subscription_url(self) -> str: |
| 188 |
a47eb4c |
Dawsh |
2y ago |
salt = secrets.token_hex(8) |
| 189 |
5c78ce8 |
Dawsh |
2y ago |
url_prefix = SUBSCRIPTION_URL_PREFIX.replace("*", salt) |
| 190 |
6a606d6 |
Dawsh |
2y ago |
return f"{url_prefix}/sub/{self.username}/{self.key}" |
| 191 |
a47eb4c |
Dawsh |
2y ago |
|
| 192 |
a47eb4c |
Dawsh |
2y ago |
|
| 193 |
a47eb4c |
Dawsh |
2y ago |
class UserUsageResponse(BaseModel): |
| 194 |
a47eb4c |
Dawsh |
2y ago |
node_id: Union[int, None] = None |
| 195 |
a47eb4c |
Dawsh |
2y ago |
node_name: str |
| 196 |
a47eb4c |
Dawsh |
2y ago |
used_traffic: int |
| 197 |
a47eb4c |
Dawsh |
2y ago |
|
| 198 |
a47eb4c |
Dawsh |
2y ago |
|
| 199 |
a47eb4c |
Dawsh |
2y ago |
class UserUsagesResponse(BaseModel): |
| 200 |
a47eb4c |
Dawsh |
2y ago |
username: str |
| 201 |
a47eb4c |
Dawsh |
2y ago |
usages: List[UserUsageResponse] |