gitscope
https://dev.kronoz.win/r/marzneshin.git
app/utils/share.py · v0.3.0
Source
1 a47eb4c Dawsh 2y ago import base64
2 a47eb4c Dawsh 2y ago import json
3 a47eb4c Dawsh 2y ago import random
4 a47eb4c Dawsh 2y ago import secrets
5 19a42f0 Dawsh 2y ago from collections import defaultdict
6 a47eb4c Dawsh 2y ago from datetime import datetime as dt, timedelta
7 406d607 dawsh 2y ago from importlib import resources
8 a47eb4c Dawsh 2y ago from typing import TYPE_CHECKING, Literal, Union, List
9 a47eb4c Dawsh 2y ago from uuid import UUID
10 a47eb4c Dawsh 2y ago
11 a47eb4c Dawsh 2y ago from jdatetime import date as jd
12 d6600c5 Dawsh 2y ago from v2share import V2Data, SingBoxConfig, ClashConfig, ClashMetaConfig, XrayConfig
13 d6600c5 Dawsh 2y ago from v2share.links import LinksConfig
14 a47eb4c Dawsh 2y ago
15 f446990 Dawsh 2y ago from app.db import GetDB, crud
16 01cdf2e Dawsh 2y ago from app.models.proxy import (
17 01cdf2e Dawsh 2y ago InboundHostSecurity,
18 01cdf2e Dawsh 2y ago )
19 8825243 Dawsh 2y ago from app.models.user import UserStatus
20 f446990 Dawsh 2y ago from app.utils.keygen import gen_uuid, gen_password
21 a47eb4c Dawsh 2y ago from app.utils.system import get_public_ip, readable_size
22 406d607 dawsh 2y ago from config import (
23 406d607 dawsh 2y ago XRAY_SUBSCRIPTION_TEMPLATE,
24 406d607 dawsh 2y ago SINGBOX_SUBSCRIPTION_TEMPLATE,
25 406d607 dawsh 2y ago CLASH_SUBSCRIPTION_TEMPLATE,
26 406d607 dawsh 2y ago )
27 a47eb4c Dawsh 2y ago
28 a47eb4c Dawsh 2y ago if TYPE_CHECKING:
29 a47eb4c Dawsh 2y ago from app.models.user import UserResponse
30 a47eb4c Dawsh 2y ago
31 a47eb4c Dawsh 2y ago SERVER_IP = get_public_ip()
32 a47eb4c Dawsh 2y ago
33 a47eb4c Dawsh 2y ago STATUS_EMOJIS = {
34 a47eb4c Dawsh 2y ago "active": "✅",
35 a47eb4c Dawsh 2y ago "expired": "⌛️",
36 a47eb4c Dawsh 2y ago "limited": "🪫",
37 a47eb4c Dawsh 2y ago "disabled": "❌",
38 d6600c5 Dawsh 2y ago "on_hold": "🔌",
39 a47eb4c Dawsh 2y ago }
40 a47eb4c Dawsh 2y ago
41 d6600c5 Dawsh 2y ago subscription_handlers = {
42 d6600c5 Dawsh 2y ago "v2ray": LinksConfig,
43 d6600c5 Dawsh 2y ago "xray": XrayConfig,
44 d6600c5 Dawsh 2y ago "clash-meta": ClashMetaConfig,
45 d6600c5 Dawsh 2y ago "clash": ClashConfig,
46 d6600c5 Dawsh 2y ago "sing-box": SingBoxConfig,
47 d6600c5 Dawsh 2y ago }
48 a47eb4c Dawsh 2y ago
49 406d607 dawsh 2y ago handlers_templates = {
50 406d607 dawsh 2y ago LinksConfig: None,
51 406d607 dawsh 2y ago XrayConfig: XRAY_SUBSCRIPTION_TEMPLATE
52 406d607 dawsh 2y ago or resources.files("app.templates") / "xray.json",
53 406d607 dawsh 2y ago ClashConfig: CLASH_SUBSCRIPTION_TEMPLATE,
54 406d607 dawsh 2y ago ClashMetaConfig: CLASH_SUBSCRIPTION_TEMPLATE,
55 406d607 dawsh 2y ago SingBoxConfig: SINGBOX_SUBSCRIPTION_TEMPLATE
56 406d607 dawsh 2y ago or resources.files("app.templates") / "sing-box.json",
57 406d607 dawsh 2y ago }
58 406d607 dawsh 2y ago
59 a47eb4c Dawsh 2y ago
60 a47eb4c Dawsh 2y ago def generate_subscription(
61 a47eb4c Dawsh 2y ago user: "UserResponse",
62 d6600c5 Dawsh 2y ago config_format: Literal["v2ray", "xray", "clash-meta", "clash", "sing-box"],
63 d6600c5 Dawsh 2y ago as_base64: bool = False,
64 a47eb4c Dawsh 2y ago ) -> str:
65 a47eb4c Dawsh 2y ago
66 d6600c5 Dawsh 2y ago inbounds = user.inbounds
67 d6600c5 Dawsh 2y ago key = user.key
68 d6600c5 Dawsh 2y ago extra_data = user.model_dump(exclude={"subscription_url", "services", "inbounds"})
69 d6600c5 Dawsh 2y ago format_variables = setup_format_variables(extra_data)
70 d6600c5 Dawsh 2y ago
71 d6600c5 Dawsh 2y ago if config_format not in subscription_handlers.keys():
72 a47eb4c Dawsh 2y ago raise ValueError(f'Unsupported format "{config_format}"')
73 a47eb4c Dawsh 2y ago
74 406d607 dawsh 2y ago subscription_handler_class = subscription_handlers[config_format]
75 406d607 dawsh 2y ago if template_path := handlers_templates[subscription_handler_class]:
76 406d607 dawsh 2y ago subscription_handler = subscription_handler_class(template_path=template_path)
77 406d607 dawsh 2y ago else:
78 406d607 dawsh 2y ago subscription_handler = subscription_handler_class()
79 406d607 dawsh 2y ago
80 d6600c5 Dawsh 2y ago config = process_inbounds_and_tags(
81 406d607 dawsh 2y ago inbounds, key, format_variables, conf=subscription_handler
82 d6600c5 Dawsh 2y ago )
83 a47eb4c Dawsh 2y ago
84 d6600c5 Dawsh 2y ago return config if not as_base64 else base64.b64encode(config.encode()).decode()
85 a47eb4c Dawsh 2y ago
86 a47eb4c Dawsh 2y ago
87 a47eb4c Dawsh 2y ago def format_time_left(seconds_left: int) -> str:
88 a47eb4c Dawsh 2y ago if not seconds_left or seconds_left <= 0:
89 a47eb4c Dawsh 2y ago return "∞"
90 a47eb4c Dawsh 2y ago
91 a47eb4c Dawsh 2y ago minutes, seconds = divmod(seconds_left, 60)
92 a47eb4c Dawsh 2y ago hours, minutes = divmod(minutes, 60)
93 a47eb4c Dawsh 2y ago days, hours = divmod(hours, 24)
94 a47eb4c Dawsh 2y ago months, days = divmod(days, 30)
95 a47eb4c Dawsh 2y ago
96 a47eb4c Dawsh 2y ago result = []
97 a47eb4c Dawsh 2y ago if months:
98 a47eb4c Dawsh 2y ago result.append(f"{months}m")
99 a47eb4c Dawsh 2y ago if days:
100 a47eb4c Dawsh 2y ago result.append(f"{days}d")
101 a47eb4c Dawsh 2y ago if hours and (days < 7):
102 a47eb4c Dawsh 2y ago result.append(f"{hours}h")
103 a47eb4c Dawsh 2y ago if minutes and not (months or days):
104 a47eb4c Dawsh 2y ago result.append(f"{minutes}m")
105 a47eb4c Dawsh 2y ago if seconds and not (months or days):
106 a47eb4c Dawsh 2y ago result.append(f"{seconds}s")
107 a47eb4c Dawsh 2y ago return " ".join(result)
108 a47eb4c Dawsh 2y ago
109 01cdf2e Dawsh 2y ago
110 a47eb4c Dawsh 2y ago def setup_format_variables(extra_data: dict) -> dict:
111 a47eb4c Dawsh 2y ago
112 a47eb4c Dawsh 2y ago user_status = extra_data.get("status")
113 f446990 Dawsh 2y ago expire_datetime = extra_data.get("expire")
114 a47eb4c Dawsh 2y ago on_hold_expire_duration = extra_data.get("on_hold_expire_duration")
115 a47eb4c Dawsh 2y ago
116 a47eb4c Dawsh 2y ago if user_status != UserStatus.on_hold:
117 f446990 Dawsh 2y ago if expire_datetime is not None:
118 f446990 Dawsh 2y ago seconds_left = (expire_datetime - dt.utcnow()).total_seconds()
119 a47eb4c Dawsh 2y ago expire_date = expire_datetime.date()
120 a47eb4c Dawsh 2y ago jalali_expire_date = jd.fromgregorian(
121 a47eb4c Dawsh 2y ago year=expire_date.year, month=expire_date.month, day=expire_date.day
122 a47eb4c Dawsh 2y ago ).strftime("%Y-%m-%d")
123 a47eb4c Dawsh 2y ago days_left = (expire_datetime - dt.utcnow()).days + 1
124 a47eb4c Dawsh 2y ago time_left = format_time_left(seconds_left)
125 a47eb4c Dawsh 2y ago else:
126 a47eb4c Dawsh 2y ago days_left = "∞"
127 a47eb4c Dawsh 2y ago time_left = "∞"
128 a47eb4c Dawsh 2y ago expire_date = "∞"
129 a47eb4c Dawsh 2y ago jalali_expire_date = "∞"
130 a47eb4c Dawsh 2y ago else:
131 a47eb4c Dawsh 2y ago if on_hold_expire_duration is not None and on_hold_expire_duration >= 0:
132 a47eb4c Dawsh 2y ago days_left = timedelta(seconds=on_hold_expire_duration).days
133 a47eb4c Dawsh 2y ago time_left = format_time_left(on_hold_expire_duration)
134 a47eb4c Dawsh 2y ago expire_date = "-"
135 a47eb4c Dawsh 2y ago jalali_expire_date = "-"
136 a47eb4c Dawsh 2y ago else:
137 a47eb4c Dawsh 2y ago days_left = "∞"
138 a47eb4c Dawsh 2y ago time_left = "∞"
139 a47eb4c Dawsh 2y ago expire_date = "∞"
140 a47eb4c Dawsh 2y ago jalali_expire_date = "∞"
141 a47eb4c Dawsh 2y ago
142 a47eb4c Dawsh 2y ago if extra_data.get("data_limit"):
143 a47eb4c Dawsh 2y ago data_limit = readable_size(extra_data["data_limit"])
144 a47eb4c Dawsh 2y ago data_left = extra_data["data_limit"] - extra_data["used_traffic"]
145 a47eb4c Dawsh 2y ago if data_left < 0:
146 a47eb4c Dawsh 2y ago data_left = 0
147 a47eb4c Dawsh 2y ago data_left = readable_size(data_left)
148 a47eb4c Dawsh 2y ago else:
149 a47eb4c Dawsh 2y ago data_limit = "∞"
150 a47eb4c Dawsh 2y ago data_left = "∞"
151 a47eb4c Dawsh 2y ago
152 a47eb4c Dawsh 2y ago status_emoji = STATUS_EMOJIS.get(extra_data.get("status")) or ""
153 a47eb4c Dawsh 2y ago
154 19a42f0 Dawsh 2y ago format_variables = defaultdict(
155 19a42f0 Dawsh 2y ago lambda: "<missing>",
156 19a42f0 Dawsh 2y ago {
157 19a42f0 Dawsh 2y ago "SERVER_IP": SERVER_IP,
158 19a42f0 Dawsh 2y ago "USERNAME": extra_data.get("username", "{USERNAME}"),
159 19a42f0 Dawsh 2y ago "DATA_USAGE": readable_size(extra_data.get("used_traffic")),
160 19a42f0 Dawsh 2y ago "DATA_LIMIT": data_limit,
161 19a42f0 Dawsh 2y ago "DATA_LEFT": data_left,
162 19a42f0 Dawsh 2y ago "DAYS_LEFT": days_left,
163 19a42f0 Dawsh 2y ago "EXPIRE_DATE": expire_date,
164 19a42f0 Dawsh 2y ago "JALALI_EXPIRE_DATE": jalali_expire_date,
165 19a42f0 Dawsh 2y ago "TIME_LEFT": time_left,
166 19a42f0 Dawsh 2y ago "STATUS_EMOJI": status_emoji,
167 19a42f0 Dawsh 2y ago },
168 19a42f0 Dawsh 2y ago )
169 a47eb4c Dawsh 2y ago
170 a47eb4c Dawsh 2y ago return format_variables
171 a47eb4c Dawsh 2y ago
172 a47eb4c Dawsh 2y ago
173 a47eb4c Dawsh 2y ago def process_inbounds_and_tags(
174 a47eb4c Dawsh 2y ago inbounds: list,
175 d6600c5 Dawsh 2y ago key: str,
176 a47eb4c Dawsh 2y ago format_variables: dict,
177 a47eb4c Dawsh 2y ago conf=None,
178 a47eb4c Dawsh 2y ago ) -> Union[List, str]:
179 a47eb4c Dawsh 2y ago salt = secrets.token_hex(8)
180 01cdf2e Dawsh 2y ago
181 a47eb4c Dawsh 2y ago for inb in inbounds:
182 01cdf2e Dawsh 2y ago inb_id, inbound, protocol, tag = (
183 01cdf2e Dawsh 2y ago inb.id,
184 01cdf2e Dawsh 2y ago json.loads(inb.config),
185 01cdf2e Dawsh 2y ago inb.protocol,
186 01cdf2e Dawsh 2y ago inb.tag,
187 01cdf2e Dawsh 2y ago )
188 a47eb4c Dawsh 2y ago
189 a47eb4c Dawsh 2y ago format_variables.update({"PROTOCOL": protocol.name})
190 a47eb4c Dawsh 2y ago if not inbound:
191 a47eb4c Dawsh 2y ago continue
192 a47eb4c Dawsh 2y ago
193 a47eb4c Dawsh 2y ago format_variables.update({"TRANSPORT": inbound["network"]})
194 a47eb4c Dawsh 2y ago with GetDB() as db:
195 a47eb4c Dawsh 2y ago hosts = crud.get_inbound_hosts(db, inb_id)
196 f446990 Dawsh 2y ago
197 a47eb4c Dawsh 2y ago for host in hosts:
198 fe923dc Dawsh 2y ago host_snis = host.sni.split(",") if host.sni else []
199 fe923dc Dawsh 2y ago sni_list = host_snis or inbound["sni"]
200 a47eb4c Dawsh 2y ago if sni_list:
201 a47eb4c Dawsh 2y ago sni = random.choice(sni_list).replace("*", salt)
202 fe923dc Dawsh 2y ago else:
203 fe923dc Dawsh 2y ago sni = ""
204 a47eb4c Dawsh 2y ago
205 fe923dc Dawsh 2y ago host_hosts = host.host.split(",") if host.host else []
206 fe923dc Dawsh 2y ago req_host_list = host_hosts or inbound["host"]
207 a47eb4c Dawsh 2y ago if req_host_list:
208 a47eb4c Dawsh 2y ago req_host = random.choice(req_host_list).replace("*", salt)
209 fe923dc Dawsh 2y ago else:
210 fe923dc Dawsh 2y ago req_host = ""
211 01cdf2e Dawsh 2y ago
212 01cdf2e Dawsh 2y ago host_tls = (
213 01cdf2e Dawsh 2y ago None
214 01cdf2e Dawsh 2y ago if host.security == InboundHostSecurity.inbound_default
215 01cdf2e Dawsh 2y ago else host.security.value
216 01cdf2e Dawsh 2y ago )
217 f446990 Dawsh 2y ago data = V2Data(
218 f446990 Dawsh 2y ago protocol.value,
219 f446990 Dawsh 2y ago host.remark.format_map(format_variables),
220 f446990 Dawsh 2y ago host.address.format_map(format_variables),
221 f446990 Dawsh 2y ago host.port or inbound["port"],
222 a9f8ac2 Dawsh 2y ago transport_type=inbound["network"],
223 f446990 Dawsh 2y ago sni=sni,
224 f446990 Dawsh 2y ago host=req_host,
225 f446990 Dawsh 2y ago tls=host_tls or inbound["tls"],
226 8825243 Dawsh 2y ago header_type=inbound.get("header_type"),
227 d6600c5 Dawsh 2y ago alpn=host.alpn.value or None,
228 f446990 Dawsh 2y ago path=(
229 f446990 Dawsh 2y ago host.path.format_map(format_variables)
230 f446990 Dawsh 2y ago if host.path
231 d6600c5 Dawsh 2y ago else inbound.get("path")
232 f446990 Dawsh 2y ago ),
233 8825243 Dawsh 2y ago fingerprint=host.fingerprint.value or inbound.get("fp"),
234 8825243 Dawsh 2y ago reality_pbk=inbound.get("pbk"),
235 8825243 Dawsh 2y ago reality_sid=inbound.get("sid"),
236 8825243 Dawsh 2y ago flow=inbound.get("flow"),
237 f446990 Dawsh 2y ago allow_insecure=host.allowinsecure,
238 d6600c5 Dawsh 2y ago uuid=UUID(gen_uuid(key)),
239 d6600c5 Dawsh 2y ago password=gen_password(key),
240 ab8556b Dawsh 2y ago enable_mux=host.mux,
241 03039e3 Dawsh 2y ago http_headers={
242 03039e3 Dawsh 2y ago "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.3"
243 03039e3 Dawsh 2y ago },
244 a47eb4c Dawsh 2y ago )
245 ab8556b Dawsh 2y ago if host.fragment:
246 ab8556b Dawsh 2y ago data.fragment = True
247 ab8556b Dawsh 2y ago data.fragment_packets = host.fragment["packets"]
248 ab8556b Dawsh 2y ago data.fragment_length = host.fragment["length"]
249 ab8556b Dawsh 2y ago data.fragment_interval = host.fragment["interval"]
250 d6600c5 Dawsh 2y ago conf.add_proxies([data])
251 d6600c5 Dawsh 2y ago return conf.render()
252 a47eb4c Dawsh 2y ago
253 a47eb4c Dawsh 2y ago
254 a47eb4c Dawsh 2y ago def encode_title(text: str) -> str:
255 a47eb4c Dawsh 2y ago return f"base64:{base64.b64encode(text.encode()).decode()}"