diff --git a/app/routes/subscription.py b/app/routes/subscription.py index a64d353..bf08877 100644 --- a/app/routes/subscription.py +++ b/app/routes/subscription.py @@ -8,7 +8,7 @@ from app.db import crud from app.dependencies import DBDep, SubUserDep, StartDateDep, EndDateDep from app.models.user import UserResponse from app.templates import render_template -from app.utils.share import encode_title, generate_subscription, generate_v2ray_links +from app.utils.share import encode_title, generate_subscription from config import ( SUB_PROFILE_TITLE, SUB_SUPPORT_URL, @@ -43,13 +43,7 @@ def user_subscription( user: UserResponse = UserResponse.model_validate(db_user) if "text/html" in accept_header: - links = generate_v2ray_links( - user.inbounds, - user.key, - user.model_dump( - exclude={"subscription_url", "services", "inbounds", "links"} - ), - ) + links = generate_subscription(user=user, config_format="v2ray").split() return HTMLResponse( render_template(SUBSCRIPTION_PAGE_TEMPLATE, {"user": user, "links": links}) ) @@ -70,31 +64,16 @@ def user_subscription( crud.update_user_sub(db, db_user, user_agent) if re.match("^([Cc]lash-verge|[Cc]lash-?[Mm]eta)", user_agent): - conf = generate_subscription( - user=user, config_format="clash-meta", as_base64=False - ) + conf = generate_subscription(user=user, config_format="clash-meta") return Response(content=conf, media_type="text/yaml", headers=response_headers) - elif re.match("^([Cc]lash|[Ss]tash)", user_agent): - conf = generate_subscription(user=user, config_format="clash", as_base64=False) + conf = generate_subscription(user=user, config_format="clash") return Response(content=conf, media_type="text/yaml", headers=response_headers) - elif re.match("^(SFA|SFI|SFM|SFT)", user_agent): - conf = generate_subscription( - user=user, config_format="sing-box", as_base64=False - ) - return Response( - content=conf, media_type="application/json", headers=response_headers - ) - - elif re.match("^(SS|SSR|SSD|SSS|Outline|Shadowsocks|SSconf)", user_agent): - conf = generate_subscription( - user=user, config_format="outline", as_base64=False - ) + conf = generate_subscription(user=user, config_format="sing-box") return Response( content=conf, media_type="application/json", headers=response_headers ) - else: conf = generate_subscription(user=user, config_format="v2ray", as_base64=True) return Response(content=conf, media_type="text/plain", headers=response_headers) @@ -118,10 +97,10 @@ def user_get_usage( def user_subscription_with_client_type( db_user: SubUserDep, request: Request, - client_type: str = Path(..., regex="sing-box|clash-meta|clash|outline|v2ray"), + client_type: str = Path(regex="sing-box|clash-meta|clash|xray|v2ray"), ): """ - Subscription link, v2ray, clash, sing-box, outline and clash-meta supported + Subscription by client type; v2ray, xray, sing-box, clash and clash-meta formats supported """ user: UserResponse = UserResponse.model_validate(db_user) @@ -140,34 +119,25 @@ def user_subscription_with_client_type( } if client_type == "clash-meta": - conf = generate_subscription( - user=user, config_format="clash-meta", as_base64=False - ) + conf = generate_subscription(user=user, config_format="clash-meta") return Response(content=conf, media_type="text/yaml", headers=response_headers) elif client_type == "sing-box": - conf = generate_subscription( - user=user, config_format="sing-box", as_base64=False - ) + conf = generate_subscription(user=user, config_format="sing-box") return Response( content=conf, media_type="application/json", headers=response_headers ) - elif client_type == "clash": - conf = generate_subscription(user=user, config_format="clash", as_base64=False) + conf = generate_subscription(user=user, config_format="clash") return Response(content=conf, media_type="text/yaml", headers=response_headers) elif client_type == "v2ray": conf = generate_subscription(user=user, config_format="v2ray", as_base64=True) return Response(content=conf, media_type="text/plain", headers=response_headers) - - elif client_type == "outline": - conf = generate_subscription( - user=user, config_format="outline", as_base64=False - ) + elif client_type == "xray": return Response( - content=conf, media_type="application/json", headers=response_headers + content=generate_subscription(user=user, config_format="xray"), + headers=response_headers, ) - else: raise HTTPException(status_code=400, detail="Invalid subscription type") diff --git a/app/utils/keygen.py b/app/utils/keygen.py index 1b2654f..c8ed478 100644 --- a/app/utils/keygen.py +++ b/app/utils/keygen.py @@ -1,7 +1,5 @@ -# import hashlib import xxhash import uuid -from app.models.proxy import ProxyTypes def gen_uuid(key: str) -> str: @@ -10,14 +8,3 @@ def gen_uuid(key: str) -> str: def gen_password(key: str) -> str: return xxhash.xxh128(key.encode()).hexdigest() - - -def generate_settings(key: str, protocol: ProxyTypes) -> dict: - if protocol in (ProxyTypes.Trojan, ProxyTypes.Shadowsocks): - return ( - ProxyTypes(protocol) - .settings_model(password=gen_password(key)) - .dict(no_obj=True) - ) - elif protocol in (ProxyTypes.VMess, ProxyTypes.VLESS): - return ProxyTypes(protocol).settings_model(id=gen_uuid(key)).dict(no_obj=True) diff --git a/app/utils/share.py b/app/utils/share.py index d93fc18..e150edb 100644 --- a/app/utils/share.py +++ b/app/utils/share.py @@ -7,7 +7,8 @@ from typing import TYPE_CHECKING, Literal, Union, List from uuid import UUID from jdatetime import date as jd -from v2share import V2Data, SingBoxConfig, ClashConfig, ClashMetaConfig +from v2share import V2Data, SingBoxConfig, ClashConfig, ClashMetaConfig, XrayConfig +from v2share.links import LinksConfig from app.db import GetDB, crud from app.models.proxy import ( @@ -15,6 +16,7 @@ from app.models.proxy import ( ) from app.utils.keygen import gen_uuid, gen_password from app.utils.system import get_public_ip, readable_size +from app.models.user import UserStatus if TYPE_CHECKING: from app.models.user import UserResponse @@ -26,69 +28,37 @@ STATUS_EMOJIS = { "expired": "âŒ›ī¸", "limited": "đŸĒĢ", "disabled": "❌", - "connect_to_start": "🔌", + "on_hold": "🔌", } - -def generate_v2ray_links(inbounds: list, key: str, extra_data: dict) -> list: - format_variables = setup_format_variables(extra_data) - return process_inbounds_and_tags(inbounds, key, format_variables, mode="v2ray") - - -def generate_clash_subscription( - inbounds: list, key: str, extra_data: dict, is_meta: bool = False -) -> str: - if is_meta is True: - conf = ClashMetaConfig() - else: - conf = ClashConfig() - - format_variables = setup_format_variables(extra_data) - return process_inbounds_and_tags( - inbounds, key, format_variables, mode="clash", conf=conf - ) - - -def generate_singbox_subscription(inbounds: list, key: str, extra_data: dict) -> str: - conf = SingBoxConfig() - - format_variables = setup_format_variables(extra_data) - return process_inbounds_and_tags( - inbounds, key, format_variables, mode="sing-box", conf=conf - ) - - -def generate_v2ray_subscription(links: list) -> str: - return base64.b64encode("\n".join(links).encode()).decode() +subscription_handlers = { + "v2ray": LinksConfig, + "xray": XrayConfig, + "clash-meta": ClashMetaConfig, + "clash": ClashConfig, + "sing-box": SingBoxConfig, +} def generate_subscription( user: "UserResponse", - config_format: Literal["v2ray", "clash-meta", "clash", "sing-box"], - as_base64: bool, + config_format: Literal["v2ray", "xray", "clash-meta", "clash", "sing-box"], + as_base64: bool = False, ) -> str: - kwargs = { - # "proxies": user.proxies, - "inbounds": user.inbounds, - "key": user.key, - "extra_data": user.__dict__, - } - if config_format == "v2ray": - config = "\n".join(generate_v2ray_links(**kwargs)) - elif config_format == "clash-meta": - config = generate_clash_subscription(**kwargs, is_meta=True) - elif config_format == "clash": - config = generate_clash_subscription(**kwargs) - elif config_format == "sing-box": - config = generate_singbox_subscription(**kwargs) - else: + inbounds = user.inbounds + key = user.key + extra_data = user.model_dump(exclude={"subscription_url", "services", "inbounds"}) + format_variables = setup_format_variables(extra_data) + + if config_format not in subscription_handlers.keys(): raise ValueError(f'Unsupported format "{config_format}"') - if as_base64: - config = base64.b64encode(config.encode()).decode() + config = process_inbounds_and_tags( + inbounds, key, format_variables, conf=subscription_handlers[config_format]() + ) - return config + return config if not as_base64 else base64.b64encode(config.encode()).decode() def format_time_left(seconds_left: int) -> str: @@ -114,9 +84,6 @@ def format_time_left(seconds_left: int) -> str: return " ".join(result) -from app.models.user import UserStatus - - def setup_format_variables(extra_data: dict) -> dict: user_status = extra_data.get("status") @@ -179,13 +146,11 @@ def setup_format_variables(extra_data: dict) -> dict: def process_inbounds_and_tags( inbounds: list, - ukey: str, + key: str, format_variables: dict, - mode: str = "v2ray", conf=None, ) -> Union[List, str]: salt = secrets.token_hex(8) - results = [] for inb in inbounds: inb_id, inbound, protocol, tag = ( @@ -232,25 +197,19 @@ def process_inbounds_and_tags( sni=sni, host=req_host, tls=host_tls or inbound["tls"], - alpn=host.alpn.value or inbound.get("alpn", ""), + alpn=host.alpn.value or None, path=( host.path.format_map(format_variables) if host.path - else inbound.get("path", "") + else inbound.get("path") ), fingerprint=host.fingerprint.value, allow_insecure=host.allowinsecure, - uuid=UUID(gen_uuid(ukey)), - password=gen_password(ukey), + uuid=UUID(gen_uuid(key)), + password=gen_password(key), ) - if mode == "v2ray": - results.append(data.to_link()) - elif mode in ["clash", "sing-box"]: - conf.add_proxies([data]) - - if mode in ["clash", "sing-box"]: - return conf.render() - return results + conf.add_proxies([data]) + return conf.render() def encode_title(text: str) -> str: diff --git a/requirements.txt b/requirements.txt index 8724bbb..62917c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ anyio==4.2.0 APScheduler==3.10.4 commentjson==0.9.0 cryptography==42.0.4 -fastapi==0.110.0 +fastapi==0.111.0 fastapi-pagination==0.12.21 fastapi-responses==0.2.1 grpclib==0.4.7 @@ -12,7 +12,7 @@ Jinja2==3.1.4 passlib==1.7.4 protobuf==4.25.2 psutil==5.9.7 -pydantic==2.5.3 +pydantic==2.7.1 PyMySQL==1.1.1 pyOpenSSL==24.0.0 pyTelegramBotAPI==4.15.2 @@ -22,12 +22,12 @@ python-dateutil==2.8.2 python-jose==3.3.0 pytz==2023.3.post1 qrcode==7.4.2 -Requests==2.32.2 +requests==2.32.2 rich==13.7.0 SQLAlchemy==2.0.25 -starlette==0.36.3 -typer==0.9.0 -uvicorn==0.25.0 +starlette==0.37.2 +typer==0.12.3 +uvicorn==0.29.0 websockets==12.0 PyYAML==6.0.1 aiohttp==3.9.4 @@ -35,4 +35,4 @@ python-multipart==0.0.7 xxhash==3.4.1 grpcio==1.60.0 psycopg==3.1.18 -v2share==0.1.0a6 \ No newline at end of file +v2share==0.1.0b3