gitscope
https://dev.kronoz.win/r/marzneshin.git
app/utils/share.py · v0.4.1
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 db68a58 Dawsh 2y ago from typing import 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 f1ab16c Dawsh 2y ago from v2share import (
13 f1ab16c Dawsh 2y ago V2Data,
14 f1ab16c Dawsh 2y ago SingBoxConfig,
15 f1ab16c Dawsh 2y ago ClashConfig,
16 f1ab16c Dawsh 2y ago ClashMetaConfig,
17 f1ab16c Dawsh 2y ago XrayConfig,
18 f1ab16c Dawsh 2y ago )
19 d6600c5 Dawsh 2y ago from v2share.links import LinksConfig
20 a47eb4c Dawsh 2y ago
21 8111eb7 Dawsh 2y ago from app.config.env import (
22 8111eb7 Dawsh 2y ago XRAY_SUBSCRIPTION_TEMPLATE,
23 8111eb7 Dawsh 2y ago SINGBOX_SUBSCRIPTION_TEMPLATE,
24 8111eb7 Dawsh 2y ago CLASH_SUBSCRIPTION_TEMPLATE,
25 fb4b69d Dawsh 2y ago SUBSCRIPTION_PAGE_TEMPLATE,
26 8111eb7 Dawsh 2y ago )
27 f446990 Dawsh 2y ago from app.db import GetDB, crud
28 01cdf2e Dawsh 2y ago from app.models.proxy import (
29 01cdf2e Dawsh 2y ago InboundHostSecurity,
30 01cdf2e Dawsh 2y ago )
31 fb4b69d Dawsh 2y ago from app.models.settings import SubscriptionSettings
32 d377c4c Dawsh 2y ago from app.models.user import UserResponse, UserExpireStrategy
33 fb4b69d Dawsh 2y ago from app.templates import render_template
34 f446990 Dawsh 2y ago from app.utils.keygen import gen_uuid, gen_password
35 a47eb4c Dawsh 2y ago from app.utils.system import get_public_ip, readable_size
36 a47eb4c Dawsh 2y ago
37 a47eb4c Dawsh 2y ago SERVER_IP = get_public_ip()
38 a47eb4c Dawsh 2y ago
39 fb4b69d Dawsh 2y ago ACTIVITY_EMOJIS = {
40 fb4b69d Dawsh 2y ago True: "✅",
41 fb4b69d Dawsh 2y ago False: "❌",
42 a47eb4c Dawsh 2y ago }
43 a47eb4c Dawsh 2y ago
44 d6600c5 Dawsh 2y ago subscription_handlers = {
45 0178858 Dawsh 2y ago "links": LinksConfig,
46 d6600c5 Dawsh 2y ago "xray": XrayConfig,
47 d6600c5 Dawsh 2y ago "clash-meta": ClashMetaConfig,
48 d6600c5 Dawsh 2y ago "clash": ClashConfig,
49 d6600c5 Dawsh 2y ago "sing-box": SingBoxConfig,
50 d6600c5 Dawsh 2y ago }
51 a47eb4c Dawsh 2y ago
52 406d607 dawsh 2y ago handlers_templates = {
53 406d607 dawsh 2y ago LinksConfig: None,
54 406d607 dawsh 2y ago XrayConfig: XRAY_SUBSCRIPTION_TEMPLATE
55 406d607 dawsh 2y ago or resources.files("app.templates") / "xray.json",
56 406d607 dawsh 2y ago ClashConfig: CLASH_SUBSCRIPTION_TEMPLATE,
57 406d607 dawsh 2y ago ClashMetaConfig: CLASH_SUBSCRIPTION_TEMPLATE,
58 406d607 dawsh 2y ago SingBoxConfig: SINGBOX_SUBSCRIPTION_TEMPLATE
59 406d607 dawsh 2y ago or resources.files("app.templates") / "sing-box.json",
60 406d607 dawsh 2y ago }
61 406d607 dawsh 2y ago
62 a47eb4c Dawsh 2y ago
63 fb4b69d Dawsh 2y ago def generate_subscription_template(
64 fb4b69d Dawsh 2y ago db_user, subscription_settings: SubscriptionSettings
65 fb4b69d Dawsh 2y ago ):
66 fb4b69d Dawsh 2y ago links = generate_subscription(
67 fb4b69d Dawsh 2y ago user=db_user,
68 fb4b69d Dawsh 2y ago config_format="links",
69 fb4b69d Dawsh 2y ago use_placeholder=not db_user.is_active
70 fb4b69d Dawsh 2y ago and subscription_settings.placeholder_if_disabled,
71 fb4b69d Dawsh 2y ago placeholder_remark=subscription_settings.placeholder_remark,
72 fb4b69d Dawsh 2y ago shuffle=subscription_settings.shuffle_configs,
73 fb4b69d Dawsh 2y ago ).split()
74 fb4b69d Dawsh 2y ago return render_template(
75 fb4b69d Dawsh 2y ago SUBSCRIPTION_PAGE_TEMPLATE,
76 fb4b69d Dawsh 2y ago {"user": UserResponse.model_validate(db_user), "links": links},
77 fb4b69d Dawsh 2y ago )
78 fb4b69d Dawsh 2y ago
79 fb4b69d Dawsh 2y ago
80 a47eb4c Dawsh 2y ago def generate_subscription(
81 a47eb4c Dawsh 2y ago user: "UserResponse",
82 0178858 Dawsh 2y ago config_format: Literal["links", "xray", "clash-meta", "clash", "sing-box"],
83 d6600c5 Dawsh 2y ago as_base64: bool = False,
84 fb4b69d Dawsh 2y ago use_placeholder: bool = False,
85 fb4b69d Dawsh 2y ago placeholder_remark: str = "disabled",
86 fb4b69d Dawsh 2y ago shuffle: bool = False,
87 a47eb4c Dawsh 2y ago ) -> str:
88 db68a58 Dawsh 2y ago extra_data = UserResponse.model_validate(user).model_dump(
89 db68a58 Dawsh 2y ago exclude={"subscription_url", "services", "inbounds"}
90 db68a58 Dawsh 2y ago )
91 d6600c5 Dawsh 2y ago format_variables = setup_format_variables(extra_data)
92 d6600c5 Dawsh 2y ago
93 d6600c5 Dawsh 2y ago if config_format not in subscription_handlers.keys():
94 a47eb4c Dawsh 2y ago raise ValueError(f'Unsupported format "{config_format}"')
95 a47eb4c Dawsh 2y ago
96 406d607 dawsh 2y ago subscription_handler_class = subscription_handlers[config_format]
97 406d607 dawsh 2y ago if template_path := handlers_templates[subscription_handler_class]:
98 f1ab16c Dawsh 2y ago subscription_handler = subscription_handler_class(
99 f1ab16c Dawsh 2y ago template_path=template_path
100 f1ab16c Dawsh 2y ago )
101 406d607 dawsh 2y ago else:
102 406d607 dawsh 2y ago subscription_handler = subscription_handler_class()
103 406d607 dawsh 2y ago
104 fb4b69d Dawsh 2y ago if use_placeholder:
105 fb4b69d Dawsh 2y ago placeholder_config = V2Data(
106 fb4b69d Dawsh 2y ago "vmess",
107 fb4b69d Dawsh 2y ago placeholder_remark.format_map(format_variables),
108 fb4b69d Dawsh 2y ago "127.0.0.1",
109 fb4b69d Dawsh 2y ago 80,
110 fb4b69d Dawsh 2y ago )
111 fb4b69d Dawsh 2y ago configs = [placeholder_config]
112 fb4b69d Dawsh 2y ago
113 fb4b69d Dawsh 2y ago else:
114 fb4b69d Dawsh 2y ago configs = generate_user_configs(
115 fb4b69d Dawsh 2y ago user.inbounds,
116 fb4b69d Dawsh 2y ago user.key,
117 fb4b69d Dawsh 2y ago format_variables,
118 fb4b69d Dawsh 2y ago )
119 fb4b69d Dawsh 2y ago
120 fb4b69d Dawsh 2y ago if shuffle:
121 fb4b69d Dawsh 2y ago random.shuffle(configs)
122 fb4b69d Dawsh 2y ago
123 fb4b69d Dawsh 2y ago subscription_handler.add_proxies(configs)
124 fb4b69d Dawsh 2y ago config = subscription_handler.render()
125 a47eb4c Dawsh 2y ago
126 f1ab16c Dawsh 2y ago return (
127 f1ab16c Dawsh 2y ago config if not as_base64 else base64.b64encode(config.encode()).decode()
128 f1ab16c Dawsh 2y ago )
129 a47eb4c Dawsh 2y ago
130 a47eb4c Dawsh 2y ago
131 a47eb4c Dawsh 2y ago def format_time_left(seconds_left: int) -> str:
132 a47eb4c Dawsh 2y ago if not seconds_left or seconds_left <= 0:
133 a47eb4c Dawsh 2y ago return "∞"
134 a47eb4c Dawsh 2y ago
135 a47eb4c Dawsh 2y ago minutes, seconds = divmod(seconds_left, 60)
136 a47eb4c Dawsh 2y ago hours, minutes = divmod(minutes, 60)
137 a47eb4c Dawsh 2y ago days, hours = divmod(hours, 24)
138 a47eb4c Dawsh 2y ago months, days = divmod(days, 30)
139 a47eb4c Dawsh 2y ago
140 a47eb4c Dawsh 2y ago result = []
141 a47eb4c Dawsh 2y ago if months:
142 a47eb4c Dawsh 2y ago result.append(f"{months}m")
143 a47eb4c Dawsh 2y ago if days:
144 a47eb4c Dawsh 2y ago result.append(f"{days}d")
145 a47eb4c Dawsh 2y ago if hours and (days < 7):
146 a47eb4c Dawsh 2y ago result.append(f"{hours}h")
147 a47eb4c Dawsh 2y ago if minutes and not (months or days):
148 a47eb4c Dawsh 2y ago result.append(f"{minutes}m")
149 a47eb4c Dawsh 2y ago if seconds and not (months or days):
150 a47eb4c Dawsh 2y ago result.append(f"{seconds}s")
151 a47eb4c Dawsh 2y ago return " ".join(result)
152 a47eb4c Dawsh 2y ago
153 01cdf2e Dawsh 2y ago
154 a47eb4c Dawsh 2y ago def setup_format_variables(extra_data: dict) -> dict:
155 d377c4c Dawsh 2y ago expire_strategy = extra_data.get("expire_strategy")
156 d377c4c Dawsh 2y ago expire_datetime = extra_data.get("expire_date")
157 d377c4c Dawsh 2y ago usage_duration = extra_data.get("usage_duration")
158 a47eb4c Dawsh 2y ago
159 d377c4c Dawsh 2y ago if expire_strategy != UserExpireStrategy.START_ON_FIRST_USE:
160 f446990 Dawsh 2y ago if expire_datetime is not None:
161 f446990 Dawsh 2y ago seconds_left = (expire_datetime - dt.utcnow()).total_seconds()
162 a47eb4c Dawsh 2y ago expire_date = expire_datetime.date()
163 a47eb4c Dawsh 2y ago jalali_expire_date = jd.fromgregorian(
164 f1ab16c Dawsh 2y ago year=expire_date.year,
165 f1ab16c Dawsh 2y ago month=expire_date.month,
166 f1ab16c Dawsh 2y ago day=expire_date.day,
167 a47eb4c Dawsh 2y ago ).strftime("%Y-%m-%d")
168 a47eb4c Dawsh 2y ago days_left = (expire_datetime - dt.utcnow()).days + 1
169 a47eb4c Dawsh 2y ago time_left = format_time_left(seconds_left)
170 a47eb4c Dawsh 2y ago else:
171 d377c4c Dawsh 2y ago days_left, time_left, expire_date, jalali_expire_date = "∞" * 4
172 a47eb4c Dawsh 2y ago else:
173 d377c4c Dawsh 2y ago if usage_duration is not None and usage_duration >= 0:
174 d377c4c Dawsh 2y ago days_left = timedelta(seconds=usage_duration).days
175 d377c4c Dawsh 2y ago time_left = format_time_left(usage_duration)
176 a47eb4c Dawsh 2y ago expire_date = "-"
177 a47eb4c Dawsh 2y ago jalali_expire_date = "-"
178 a47eb4c Dawsh 2y ago else:
179 d377c4c Dawsh 2y ago days_left, time_left, expire_date, jalali_expire_date = "∞" * 4
180 a47eb4c Dawsh 2y ago
181 a47eb4c Dawsh 2y ago if extra_data.get("data_limit"):
182 a47eb4c Dawsh 2y ago data_limit = readable_size(extra_data["data_limit"])
183 a47eb4c Dawsh 2y ago data_left = extra_data["data_limit"] - extra_data["used_traffic"]
184 a47eb4c Dawsh 2y ago if data_left < 0:
185 a47eb4c Dawsh 2y ago data_left = 0
186 a47eb4c Dawsh 2y ago data_left = readable_size(data_left)
187 a47eb4c Dawsh 2y ago else:
188 fb4b69d Dawsh 2y ago data_limit, data_left = "∞" * 2
189 a47eb4c Dawsh 2y ago
190 fb4b69d Dawsh 2y ago status_emoji = ACTIVITY_EMOJIS.get(extra_data.get("is_active")) or ""
191 a47eb4c Dawsh 2y ago
192 19a42f0 Dawsh 2y ago format_variables = defaultdict(
193 19a42f0 Dawsh 2y ago lambda: "<missing>",
194 19a42f0 Dawsh 2y ago {
195 19a42f0 Dawsh 2y ago "SERVER_IP": SERVER_IP,
196 19a42f0 Dawsh 2y ago "USERNAME": extra_data.get("username", "{USERNAME}"),
197 19a42f0 Dawsh 2y ago "DATA_USAGE": readable_size(extra_data.get("used_traffic")),
198 19a42f0 Dawsh 2y ago "DATA_LIMIT": data_limit,
199 19a42f0 Dawsh 2y ago "DATA_LEFT": data_left,
200 19a42f0 Dawsh 2y ago "DAYS_LEFT": days_left,
201 19a42f0 Dawsh 2y ago "EXPIRE_DATE": expire_date,
202 19a42f0 Dawsh 2y ago "JALALI_EXPIRE_DATE": jalali_expire_date,
203 19a42f0 Dawsh 2y ago "TIME_LEFT": time_left,
204 19a42f0 Dawsh 2y ago "STATUS_EMOJI": status_emoji,
205 19a42f0 Dawsh 2y ago },
206 19a42f0 Dawsh 2y ago )
207 a47eb4c Dawsh 2y ago
208 a47eb4c Dawsh 2y ago return format_variables
209 a47eb4c Dawsh 2y ago
210 a47eb4c Dawsh 2y ago
211 fb4b69d Dawsh 2y ago def generate_user_configs(
212 a47eb4c Dawsh 2y ago inbounds: list,
213 d6600c5 Dawsh 2y ago key: str,
214 a47eb4c Dawsh 2y ago format_variables: dict,
215 a47eb4c Dawsh 2y ago ) -> Union[List, str]:
216 fb4b69d Dawsh 2y ago
217 a47eb4c Dawsh 2y ago salt = secrets.token_hex(8)
218 fb4b69d Dawsh 2y ago configs = []
219 01cdf2e Dawsh 2y ago
220 a47eb4c Dawsh 2y ago for inb in inbounds:
221 01cdf2e Dawsh 2y ago inb_id, inbound, protocol, tag = (
222 01cdf2e Dawsh 2y ago inb.id,
223 01cdf2e Dawsh 2y ago json.loads(inb.config),
224 01cdf2e Dawsh 2y ago inb.protocol,
225 01cdf2e Dawsh 2y ago inb.tag,
226 01cdf2e Dawsh 2y ago )
227 a47eb4c Dawsh 2y ago
228 a47eb4c Dawsh 2y ago format_variables.update({"PROTOCOL": protocol.name})
229 a47eb4c Dawsh 2y ago if not inbound:
230 a47eb4c Dawsh 2y ago continue
231 a47eb4c Dawsh 2y ago
232 a47eb4c Dawsh 2y ago format_variables.update({"TRANSPORT": inbound["network"]})
233 a47eb4c Dawsh 2y ago with GetDB() as db:
234 a47eb4c Dawsh 2y ago hosts = crud.get_inbound_hosts(db, inb_id)
235 f446990 Dawsh 2y ago
236 a47eb4c Dawsh 2y ago for host in hosts:
237 a88ed47 Mardin 2y ago if host.is_disabled:
238 a88ed47 Mardin 2y ago continue
239 fe923dc Dawsh 2y ago host_snis = host.sni.split(",") if host.sni else []
240 fe923dc Dawsh 2y ago sni_list = host_snis or inbound["sni"]
241 a47eb4c Dawsh 2y ago if sni_list:
242 a47eb4c Dawsh 2y ago sni = random.choice(sni_list).replace("*", salt)
243 fe923dc Dawsh 2y ago else:
244 fe923dc Dawsh 2y ago sni = ""
245 a47eb4c Dawsh 2y ago
246 fe923dc Dawsh 2y ago host_hosts = host.host.split(",") if host.host else []
247 fe923dc Dawsh 2y ago req_host_list = host_hosts or inbound["host"]
248 a47eb4c Dawsh 2y ago if req_host_list:
249 a47eb4c Dawsh 2y ago req_host = random.choice(req_host_list).replace("*", salt)
250 fe923dc Dawsh 2y ago else:
251 fe923dc Dawsh 2y ago req_host = ""
252 01cdf2e Dawsh 2y ago
253 01cdf2e Dawsh 2y ago host_tls = (
254 01cdf2e Dawsh 2y ago None
255 01cdf2e Dawsh 2y ago if host.security == InboundHostSecurity.inbound_default
256 01cdf2e Dawsh 2y ago else host.security.value
257 01cdf2e Dawsh 2y ago )
258 f446990 Dawsh 2y ago data = V2Data(
259 f446990 Dawsh 2y ago protocol.value,
260 f446990 Dawsh 2y ago host.remark.format_map(format_variables),
261 f446990 Dawsh 2y ago host.address.format_map(format_variables),
262 f446990 Dawsh 2y ago host.port or inbound["port"],
263 a9f8ac2 Dawsh 2y ago transport_type=inbound["network"],
264 f446990 Dawsh 2y ago sni=sni,
265 f446990 Dawsh 2y ago host=req_host,
266 f446990 Dawsh 2y ago tls=host_tls or inbound["tls"],
267 8825243 Dawsh 2y ago header_type=inbound.get("header_type"),
268 244c9d8 Dawsh 2y ago alpn=host.alpn if host.alpn != "none" else None,
269 f446990 Dawsh 2y ago path=(
270 f446990 Dawsh 2y ago host.path.format_map(format_variables)
271 f446990 Dawsh 2y ago if host.path
272 d6600c5 Dawsh 2y ago else inbound.get("path")
273 f446990 Dawsh 2y ago ),
274 8825243 Dawsh 2y ago fingerprint=host.fingerprint.value or inbound.get("fp"),
275 8825243 Dawsh 2y ago reality_pbk=inbound.get("pbk"),
276 8825243 Dawsh 2y ago reality_sid=inbound.get("sid"),
277 8825243 Dawsh 2y ago flow=inbound.get("flow"),
278 f446990 Dawsh 2y ago allow_insecure=host.allowinsecure,
279 d6600c5 Dawsh 2y ago uuid=UUID(gen_uuid(key)),
280 d6600c5 Dawsh 2y ago password=gen_password(key),
281 ab8556b Dawsh 2y ago enable_mux=host.mux,
282 03039e3 Dawsh 2y ago http_headers={
283 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"
284 03039e3 Dawsh 2y ago },
285 248ca9c Dawsh 2y ago shadowsocks_method="chacha20-ietf-poly1305",
286 a47eb4c Dawsh 2y ago )
287 ab8556b Dawsh 2y ago if host.fragment:
288 ab8556b Dawsh 2y ago data.fragment = True
289 ab8556b Dawsh 2y ago data.fragment_packets = host.fragment["packets"]
290 ab8556b Dawsh 2y ago data.fragment_length = host.fragment["length"]
291 ab8556b Dawsh 2y ago data.fragment_interval = host.fragment["interval"]
292 fb4b69d Dawsh 2y ago configs.append(data)
293 fb4b69d Dawsh 2y ago
294 fb4b69d Dawsh 2y ago return configs
295 a47eb4c Dawsh 2y ago
296 a47eb4c Dawsh 2y ago
297 a47eb4c Dawsh 2y ago def encode_title(text: str) -> str:
298 a47eb4c Dawsh 2y ago return f"base64:{base64.b64encode(text.encode()).decode()}"