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