| 1 |
a47eb4c |
Dawsh |
2y ago |
import re |
| 2 |
a47eb4c |
Dawsh |
2y ago |
|
| 3 |
f7af2b3 |
Dawsh |
2y ago |
from fastapi import APIRouter |
| 4 |
c13a1aa |
Dawsh |
2y ago |
from fastapi import Header, HTTPException, Path, Request, Response |
| 5 |
a47eb4c |
Dawsh |
2y ago |
from fastapi.responses import HTMLResponse |
| 6 |
a47eb4c |
Dawsh |
2y ago |
|
| 7 |
c13a1aa |
Dawsh |
2y ago |
from app.db import crud |
| 8 |
41ba8cb |
Dawsh |
2y ago |
from app.dependencies import DBDep, SubUserDep, StartDateDep, EndDateDep |
| 9 |
a47eb4c |
Dawsh |
2y ago |
from app.models.user import UserResponse |
| 10 |
a47eb4c |
Dawsh |
2y ago |
from app.templates import render_template |
| 11 |
f7af2b3 |
Dawsh |
2y ago |
from app.utils.share import encode_title, generate_subscription, generate_v2ray_links |
| 12 |
a47eb4c |
Dawsh |
2y ago |
from config import ( |
| 13 |
a47eb4c |
Dawsh |
2y ago |
SUB_PROFILE_TITLE, |
| 14 |
a47eb4c |
Dawsh |
2y ago |
SUB_SUPPORT_URL, |
| 15 |
a47eb4c |
Dawsh |
2y ago |
SUB_UPDATE_INTERVAL, |
| 16 |
367fdfc |
Dawsh |
2y ago |
SUBSCRIPTION_PAGE_TEMPLATE |
| 17 |
a47eb4c |
Dawsh |
2y ago |
) |
| 18 |
a47eb4c |
Dawsh |
2y ago |
|
| 19 |
2de9d72 |
Dawsh |
2y ago |
router = APIRouter(prefix="/sub", tags=['Subscription']) |
| 20 |
f7af2b3 |
Dawsh |
2y ago |
|
| 21 |
a47eb4c |
Dawsh |
2y ago |
|
| 22 |
c13a1aa |
Dawsh |
2y ago |
def get_subscription_user_info(user: UserResponse) -> dict: |
| 23 |
c13a1aa |
Dawsh |
2y ago |
return { |
| 24 |
c13a1aa |
Dawsh |
2y ago |
"upload": 0, |
| 25 |
c13a1aa |
Dawsh |
2y ago |
"download": user.used_traffic, |
| 26 |
c13a1aa |
Dawsh |
2y ago |
"total": user.data_limit, |
| 27 |
c13a1aa |
Dawsh |
2y ago |
"expire": user.expire, |
| 28 |
c13a1aa |
Dawsh |
2y ago |
} |
| 29 |
c13a1aa |
Dawsh |
2y ago |
|
| 30 |
c13a1aa |
Dawsh |
2y ago |
|
| 31 |
2de9d72 |
Dawsh |
2y ago |
@router.get("/{username}/{key}") |
| 32 |
c13a1aa |
Dawsh |
2y ago |
def user_subscription(db_user: SubUserDep, |
| 33 |
a47eb4c |
Dawsh |
2y ago |
request: Request, |
| 34 |
c13a1aa |
Dawsh |
2y ago |
db: DBDep, |
| 35 |
a47eb4c |
Dawsh |
2y ago |
user_agent: str = Header(default="")): |
| 36 |
a47eb4c |
Dawsh |
2y ago |
""" |
| 37 |
a47eb4c |
Dawsh |
2y ago |
Subscription link, V2ray and Clash supported |
| 38 |
a47eb4c |
Dawsh |
2y ago |
""" |
| 39 |
a47eb4c |
Dawsh |
2y ago |
accept_header = request.headers.get("Accept", "") |
| 40 |
a47eb4c |
Dawsh |
2y ago |
|
| 41 |
c13a1aa |
Dawsh |
2y ago |
user: UserResponse = UserResponse.model_validate(db_user) |
| 42 |
a47eb4c |
Dawsh |
2y ago |
|
| 43 |
a47eb4c |
Dawsh |
2y ago |
if "text/html" in accept_header: |
| 44 |
f7af2b3 |
Dawsh |
2y ago |
links = generate_v2ray_links(user.inbounds, |
| 45 |
f7af2b3 |
Dawsh |
2y ago |
user.key, |
| 46 |
f7af2b3 |
Dawsh |
2y ago |
user.model_dump( |
| 47 |
f7af2b3 |
Dawsh |
2y ago |
exclude={"subscription_url", "services", "inbounds", "links"} |
| 48 |
f7af2b3 |
Dawsh |
2y ago |
)) |
| 49 |
a47eb4c |
Dawsh |
2y ago |
return HTMLResponse( |
| 50 |
a47eb4c |
Dawsh |
2y ago |
render_template( |
| 51 |
a47eb4c |
Dawsh |
2y ago |
SUBSCRIPTION_PAGE_TEMPLATE, |
| 52 |
f7af2b3 |
Dawsh |
2y ago |
{"user": user, "links": links} |
| 53 |
a47eb4c |
Dawsh |
2y ago |
) |
| 54 |
a47eb4c |
Dawsh |
2y ago |
) |
| 55 |
a47eb4c |
Dawsh |
2y ago |
|
| 56 |
a47eb4c |
Dawsh |
2y ago |
response_headers = { |
| 57 |
a47eb4c |
Dawsh |
2y ago |
"content-disposition": f'attachment; filename="{user.username}"', |
| 58 |
a47eb4c |
Dawsh |
2y ago |
"profile-web-page-url": str(request.url), |
| 59 |
a47eb4c |
Dawsh |
2y ago |
"support-url": SUB_SUPPORT_URL, |
| 60 |
a47eb4c |
Dawsh |
2y ago |
"profile-title": encode_title(SUB_PROFILE_TITLE), |
| 61 |
a47eb4c |
Dawsh |
2y ago |
"profile-update-interval": SUB_UPDATE_INTERVAL, |
| 62 |
a47eb4c |
Dawsh |
2y ago |
"subscription-userinfo": "; ".join( |
| 63 |
a47eb4c |
Dawsh |
2y ago |
f"{key}={val}" |
| 64 |
a47eb4c |
Dawsh |
2y ago |
for key, val in get_subscription_user_info(user).items() |
| 65 |
a47eb4c |
Dawsh |
2y ago |
if val is not None |
| 66 |
a47eb4c |
Dawsh |
2y ago |
) |
| 67 |
a47eb4c |
Dawsh |
2y ago |
} |
| 68 |
a47eb4c |
Dawsh |
2y ago |
|
| 69 |
c13a1aa |
Dawsh |
2y ago |
crud.update_user_sub(db, db_user, user_agent) |
| 70 |
a47eb4c |
Dawsh |
2y ago |
|
| 71 |
a47eb4c |
Dawsh |
2y ago |
if re.match('^([Cc]lash-verge|[Cc]lash-?[Mm]eta)', user_agent): |
| 72 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="clash-meta", as_base64=False) |
| 73 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="text/yaml", headers=response_headers) |
| 74 |
a47eb4c |
Dawsh |
2y ago |
|
| 75 |
a47eb4c |
Dawsh |
2y ago |
elif re.match('^([Cc]lash|[Ss]tash)', user_agent): |
| 76 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="clash", as_base64=False) |
| 77 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="text/yaml", headers=response_headers) |
| 78 |
a47eb4c |
Dawsh |
2y ago |
|
| 79 |
a47eb4c |
Dawsh |
2y ago |
elif re.match('^(SFA|SFI|SFM|SFT)', user_agent): |
| 80 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="sing-box", as_base64=False) |
| 81 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="application/json", headers=response_headers) |
| 82 |
a47eb4c |
Dawsh |
2y ago |
|
| 83 |
a47eb4c |
Dawsh |
2y ago |
elif re.match('^(SS|SSR|SSD|SSS|Outline|Shadowsocks|SSconf)', user_agent): |
| 84 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="outline", as_base64=False) |
| 85 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="application/json", headers=response_headers) |
| 86 |
a47eb4c |
Dawsh |
2y ago |
|
| 87 |
a47eb4c |
Dawsh |
2y ago |
else: |
| 88 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="v2ray", as_base64=True) |
| 89 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="text/plain", headers=response_headers) |
| 90 |
a47eb4c |
Dawsh |
2y ago |
|
| 91 |
a47eb4c |
Dawsh |
2y ago |
|
| 92 |
2de9d72 |
Dawsh |
2y ago |
@router.get("/{username}/{key}/info", response_model=UserResponse) |
| 93 |
c13a1aa |
Dawsh |
2y ago |
def user_subscription_info(db_user: SubUserDep): |
| 94 |
c13a1aa |
Dawsh |
2y ago |
return db_user |
| 95 |
a47eb4c |
Dawsh |
2y ago |
|
| 96 |
a47eb4c |
Dawsh |
2y ago |
|
| 97 |
2de9d72 |
Dawsh |
2y ago |
@router.get("/{username}/{key}/usage") |
| 98 |
c13a1aa |
Dawsh |
2y ago |
def user_get_usage(db_user: SubUserDep, |
| 99 |
c13a1aa |
Dawsh |
2y ago |
db: DBDep, |
| 100 |
41ba8cb |
Dawsh |
2y ago |
start_date: StartDateDep, |
| 101 |
41ba8cb |
Dawsh |
2y ago |
end_date: EndDateDep): |
| 102 |
c13a1aa |
Dawsh |
2y ago |
usages = crud.get_user_usages(db, db_user, start_date, end_date) |
| 103 |
a47eb4c |
Dawsh |
2y ago |
|
| 104 |
c13a1aa |
Dawsh |
2y ago |
return {"usages": usages, "username": db_user.username} |
| 105 |
a47eb4c |
Dawsh |
2y ago |
|
| 106 |
a47eb4c |
Dawsh |
2y ago |
|
| 107 |
2de9d72 |
Dawsh |
2y ago |
@router.get("/{username}/{key}/{client_type}") |
| 108 |
a47eb4c |
Dawsh |
2y ago |
def user_subscription_with_client_type( |
| 109 |
c13a1aa |
Dawsh |
2y ago |
db_user: SubUserDep, |
| 110 |
eb74041 |
Dawsh |
2y ago |
request: Request, |
| 111 |
c13a1aa |
Dawsh |
2y ago |
client_type: str = Path(..., regex="sing-box|clash-meta|clash|outline|v2ray")): |
| 112 |
a47eb4c |
Dawsh |
2y ago |
""" |
| 113 |
a47eb4c |
Dawsh |
2y ago |
Subscription link, v2ray, clash, sing-box, outline and clash-meta supported |
| 114 |
a47eb4c |
Dawsh |
2y ago |
""" |
| 115 |
a47eb4c |
Dawsh |
2y ago |
|
| 116 |
c13a1aa |
Dawsh |
2y ago |
user: UserResponse = UserResponse.model_validate(db_user) |
| 117 |
a47eb4c |
Dawsh |
2y ago |
|
| 118 |
a47eb4c |
Dawsh |
2y ago |
response_headers = { |
| 119 |
a47eb4c |
Dawsh |
2y ago |
"content-disposition": f'attachment; filename="{user.username}"', |
| 120 |
a47eb4c |
Dawsh |
2y ago |
"profile-web-page-url": str(request.url), |
| 121 |
a47eb4c |
Dawsh |
2y ago |
"support-url": SUB_SUPPORT_URL, |
| 122 |
a47eb4c |
Dawsh |
2y ago |
"profile-title": encode_title(SUB_PROFILE_TITLE), |
| 123 |
a47eb4c |
Dawsh |
2y ago |
"profile-update-interval": SUB_UPDATE_INTERVAL, |
| 124 |
a47eb4c |
Dawsh |
2y ago |
"subscription-userinfo": "; ".join( |
| 125 |
a47eb4c |
Dawsh |
2y ago |
f"{key}={val}" |
| 126 |
a47eb4c |
Dawsh |
2y ago |
for key, val in get_subscription_user_info(user).items() |
| 127 |
a47eb4c |
Dawsh |
2y ago |
if val is not None |
| 128 |
a47eb4c |
Dawsh |
2y ago |
) |
| 129 |
a47eb4c |
Dawsh |
2y ago |
} |
| 130 |
a47eb4c |
Dawsh |
2y ago |
|
| 131 |
a47eb4c |
Dawsh |
2y ago |
if client_type == "clash-meta": |
| 132 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="clash-meta", as_base64=False) |
| 133 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="text/yaml", headers=response_headers) |
| 134 |
a47eb4c |
Dawsh |
2y ago |
|
| 135 |
a47eb4c |
Dawsh |
2y ago |
elif client_type == "sing-box": |
| 136 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="sing-box", as_base64=False) |
| 137 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="application/json", headers=response_headers) |
| 138 |
a47eb4c |
Dawsh |
2y ago |
|
| 139 |
a47eb4c |
Dawsh |
2y ago |
elif client_type == "clash": |
| 140 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="clash", as_base64=False) |
| 141 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="text/yaml", headers=response_headers) |
| 142 |
a47eb4c |
Dawsh |
2y ago |
|
| 143 |
a47eb4c |
Dawsh |
2y ago |
elif client_type == "v2ray": |
| 144 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="v2ray", as_base64=True) |
| 145 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="text/plain", headers=response_headers) |
| 146 |
a47eb4c |
Dawsh |
2y ago |
|
| 147 |
a47eb4c |
Dawsh |
2y ago |
elif client_type == "outline": |
| 148 |
a47eb4c |
Dawsh |
2y ago |
conf = generate_subscription(user=user, config_format="outline", as_base64=False) |
| 149 |
a47eb4c |
Dawsh |
2y ago |
return Response(content=conf, media_type="application/json", headers=response_headers) |
| 150 |
a47eb4c |
Dawsh |
2y ago |
|
| 151 |
a47eb4c |
Dawsh |
2y ago |
else: |
| 152 |
a47eb4c |
Dawsh |
2y ago |
raise HTTPException(status_code=400, detail="Invalid subscription type") |