gitscope
← marzneshin / v0.7.2 / app/routes/subscription.py
https://dev.kronoz.win/r/marzneshin.git
app/routes/subscription.py · Python
Blame Raw
  1import re
  2from collections import defaultdict
  3
  4from fastapi import APIRouter
  5from fastapi import Header, HTTPException, Path, Request, Response
  6from starlette.responses import HTMLResponse
  7
  8from app.db import crud
  9from app.db.models import Settings
 10from app.dependencies import DBDep, SubUserDep, StartDateDep, EndDateDep
 11from app.models.settings import SubscriptionSettings
 12from app.models.system import TrafficUsageSeries
 13from app.models.user import UserResponse
 14from app.utils.share import (
 15    encode_title,
 16    generate_subscription,
 17    generate_subscription_template,
 18)
 19
 20router = APIRouter(prefix="/sub", tags=["Subscription"])
 21
 22
 23config_mimetype = defaultdict(
 24    lambda: "text/plain",
 25    {
 26        "links": "text/plain",
 27        "base64-links": "text/plain",
 28        "sing-box": "application/json",
 29        "xray": "application/json",
 30        "clash": "text/yaml",
 31        "clash-meta": "text/yaml",
 32        "template": "text/html",
 33        "block": "text/plain",
 34    },
 35)
 36
 37
 38def get_subscription_user_info(user: UserResponse) -> dict:
 39    return {
 40        "upload": 0,
 41        "download": user.used_traffic,
 42        "total": user.data_limit or 0,
 43        "expire": (
 44            int(user.expire_date.timestamp())
 45            if user.expire_strategy == "fixed_date"
 46            else 0
 47        ),
 48    }
 49
 50
 51@router.get("/{username}/{key}")
 52def user_subscription(
 53    db_user: SubUserDep,
 54    request: Request,
 55    db: DBDep,
 56    user_agent: str = Header(default=""),
 57):
 58    """
 59    Subscription link, result format depends on subscription settings
 60    """
 61
 62    user: UserResponse = UserResponse.model_validate(db_user)
 63
 64    crud.update_user_sub(db, db_user, user_agent)
 65
 66    subscription_settings = SubscriptionSettings.model_validate(
 67        db.query(Settings.subscription).first()[0]
 68    )
 69
 70    if (
 71        subscription_settings.template_on_acceptance
 72        and "text/html" in request.headers.get("Accept", [])
 73    ):
 74        return HTMLResponse(
 75            generate_subscription_template(db_user, subscription_settings)
 76        )
 77
 78    response_headers = {
 79        "content-disposition": f'attachment; filename="{user.username}"',
 80        "profile-web-page-url": str(request.url),
 81        "support-url": subscription_settings.support_link,
 82        "profile-title": encode_title(subscription_settings.profile_title),
 83        "profile-update-interval": str(subscription_settings.update_interval),
 84        "subscription-userinfo": "; ".join(
 85            f"{key}={val}"
 86            for key, val in get_subscription_user_info(user).items()
 87        ),
 88    }
 89
 90    for rule in subscription_settings.rules:
 91        if re.match(rule.pattern, user_agent):
 92            if rule.result.value == "template":
 93                return HTMLResponse(
 94                    generate_subscription_template(
 95                        db_user, subscription_settings
 96                    )
 97                )
 98            elif rule.result.value == "block":
 99                raise HTTPException(404)
100            elif rule.result.value == "base64-links":
101                b64 = True
102                config_format = "links"
103            else:
104                b64 = False
105                config_format = rule.result.value
106
107            conf = generate_subscription(
108                user=db_user,
109                config_format=config_format,
110                as_base64=b64,
111                use_placeholder=not user.is_active
112                and subscription_settings.placeholder_if_disabled,
113                placeholder_remark=subscription_settings.placeholder_remark,
114                shuffle=subscription_settings.shuffle_configs,
115            )
116            return Response(
117                content=conf,
118                media_type=config_mimetype[rule.result],
119                headers=response_headers,
120            )
121
122
123@router.get("/{username}/{key}/info", response_model=UserResponse)
124def user_subscription_info(db_user: SubUserDep):
125    return db_user
126
127
128@router.get("/{username}/{key}/usage", response_model=TrafficUsageSeries)
129def user_get_usage(
130    db_user: SubUserDep,
131    db: DBDep,
132    start_date: StartDateDep,
133    end_date: EndDateDep,
134):
135    per_day = (end_date - start_date).total_seconds() > 3 * 86400
136    return crud.get_user_total_usage(
137        db, db_user, start_date, end_date, per_day=per_day
138    )
139
140
141client_type_mime_type = {
142    "sing-box": "application/json",
143    "wireguard": "application/json",
144    "clash-meta": "text/yaml",
145    "clash": "text/yaml",
146    "xray": "application/json",
147    "v2ray": "text/plain",
148}
149
150
151@router.get("/{username}/{key}/{client_type}")
152def user_subscription_with_client_type(
153    db: DBDep,
154    db_user: SubUserDep,
155    request: Request,
156    client_type: str = Path(
157        regex="^(sing-box|clash-meta|clash|xray|v2ray|links|wireguard)$"
158    ),
159):
160    """
161    Subscription by client type; v2ray, xray, sing-box, clash and clash-meta formats supported
162    """
163
164    user: UserResponse = UserResponse.model_validate(db_user)
165
166    subscription_settings = SubscriptionSettings.model_validate(
167        db.query(Settings.subscription).first()[0]
168    )
169
170    response_headers = {
171        "content-disposition": f'attachment; filename="{user.username}"',
172        "profile-web-page-url": str(request.url),
173        "support-url": subscription_settings.support_link,
174        "profile-title": encode_title(subscription_settings.profile_title),
175        "profile-update-interval": str(subscription_settings.update_interval),
176        "subscription-userinfo": "; ".join(
177            f"{key}={val}"
178            for key, val in get_subscription_user_info(user).items()
179        ),
180    }
181
182    conf = generate_subscription(
183        user=db_user,
184        config_format="links" if client_type == "v2ray" else client_type,
185        as_base64=client_type == "v2ray",
186        use_placeholder=not user.is_active
187        and subscription_settings.placeholder_if_disabled,
188        placeholder_remark=subscription_settings.placeholder_remark,
189        shuffle=subscription_settings.shuffle_configs,
190    )
191    return Response(
192        content=conf,
193        media_type=client_type_mime_type[client_type],
194        headers=response_headers,
195    )