gitscope
https://dev.kronoz.win/r/marzneshin.git
app/utils/share.py · v0.4.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 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 print(subscription_settings.shuffle_configs)
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 if shuffle:
122 fb4b69d Dawsh 2y ago random.shuffle(configs)
123 fb4b69d Dawsh 2y ago
124 fb4b69d Dawsh 2y ago subscription_handler.add_proxies(configs)
125 fb4b69d Dawsh 2y ago config = subscription_handler.render()
126 a47eb4c Dawsh 2y ago
127 f1ab16c Dawsh 2y ago return (
128 f1ab16c Dawsh 2y ago config if not as_base64 else base64.b64encode(config.encode()).decode()
129 f1ab16c Dawsh 2y ago )
130 a47eb4c Dawsh 2y ago
131 a47eb4c Dawsh 2y ago
132 a47eb4c Dawsh 2y ago def format_time_left(seconds_left: int) -> str:
133 a47eb4c Dawsh 2y ago if not seconds_left or seconds_left <= 0:
134 a47eb4c Dawsh 2y ago return "∞"
135 a47eb4c Dawsh 2y ago
136 a47eb4c Dawsh 2y ago minutes, seconds = divmod(seconds_left, 60)
137 a47eb4c Dawsh 2y ago hours, minutes = divmod(minutes, 60)
138 a47eb4c Dawsh 2y ago days, hours = divmod(hours, 24)
139 a47eb4c Dawsh 2y ago months, days = divmod(days, 30)
140 a47eb4c Dawsh 2y ago
141 a47eb4c Dawsh 2y ago result = []
142 a47eb4c Dawsh 2y ago if months:
143 a47eb4c Dawsh 2y ago result.append(f"{months}m")
144 a47eb4c Dawsh 2y ago if days:
145 a47eb4c Dawsh 2y ago result.append(f"{days}d")
146 a47eb4c Dawsh 2y ago if hours and (days < 7):
147 a47eb4c Dawsh 2y ago result.append(f"{hours}h")
148 a47eb4c Dawsh 2y ago if minutes and not (months or days):
149 a47eb4c Dawsh 2y ago result.append(f"{minutes}m")
150 a47eb4c Dawsh 2y ago if seconds and not (months or days):
151 a47eb4c Dawsh 2y ago result.append(f"{seconds}s")
152 a47eb4c Dawsh 2y ago return " ".join(result)
153 a47eb4c Dawsh 2y ago
154 01cdf2e Dawsh 2y ago
155 a47eb4c Dawsh 2y ago def setup_format_variables(extra_data: dict) -> dict:
156 d377c4c Dawsh 2y ago expire_strategy = extra_data.get("expire_strategy")
157 d377c4c Dawsh 2y ago expire_datetime = extra_data.get("expire_date")
158 d377c4c Dawsh 2y ago usage_duration = extra_data.get("usage_duration")
159 a47eb4c Dawsh 2y ago
160 d377c4c Dawsh 2y ago if expire_strategy != UserExpireStrategy.START_ON_FIRST_USE:
161 f446990 Dawsh 2y ago if expire_datetime is not None:
162 f446990 Dawsh 2y ago seconds_left = (expire_datetime - dt.utcnow()).total_seconds()
163 a47eb4c Dawsh 2y ago expire_date = expire_datetime.date()
164 a47eb4c Dawsh 2y ago jalali_expire_date = jd.fromgregorian(
165 f1ab16c Dawsh 2y ago year=expire_date.year,
166 f1ab16c Dawsh 2y ago month=expire_date.month,
167 f1ab16c Dawsh 2y ago day=expire_date.day,
168 a47eb4c Dawsh 2y ago ).strftime("%Y-%m-%d")
169 a47eb4c Dawsh 2y ago days_left = (expire_datetime - dt.utcnow()).days + 1
170 a47eb4c Dawsh 2y ago time_left = format_time_left(seconds_left)
171 a47eb4c Dawsh 2y ago else:
172 d377c4c Dawsh 2y ago days_left, time_left, expire_date, jalali_expire_date = "∞" * 4
173 a47eb4c Dawsh 2y ago else:
174 d377c4c Dawsh 2y ago if usage_duration is not None and usage_duration >= 0:
175 d377c4c Dawsh 2y ago days_left = timedelta(seconds=usage_duration).days
176 d377c4c Dawsh 2y ago time_left = format_time_left(usage_duration)
177 a47eb4c Dawsh 2y ago expire_date = "-"
178 a47eb4c Dawsh 2y ago jalali_expire_date = "-"
179 a47eb4c Dawsh 2y ago else:
180 d377c4c Dawsh 2y ago days_left, time_left, expire_date, jalali_expire_date = "∞" * 4
181 a47eb4c Dawsh 2y ago
182 a47eb4c Dawsh 2y ago if extra_data.get("data_limit"):
183 a47eb4c Dawsh 2y ago data_limit = readable_size(extra_data["data_limit"])
184 a47eb4c Dawsh 2y ago data_left = extra_data["data_limit"] - extra_data["used_traffic"]
185 a47eb4c Dawsh 2y ago if data_left < 0:
186 a47eb4c Dawsh 2y ago data_left = 0
187 a47eb4c Dawsh 2y ago data_left = readable_size(data_left)
188 a47eb4c Dawsh 2y ago else:
189 fb4b69d Dawsh 2y ago data_limit, data_left = "∞" * 2
190 a47eb4c Dawsh 2y ago
191 fb4b69d Dawsh 2y ago status_emoji = ACTIVITY_EMOJIS.get(extra_data.get("is_active")) or ""
192 a47eb4c Dawsh 2y ago
193 19a42f0 Dawsh 2y ago format_variables = defaultdict(
194 19a42f0 Dawsh 2y ago lambda: "<missing>",
195 19a42f0 Dawsh 2y ago {
196 19a42f0 Dawsh 2y ago "SERVER_IP": SERVER_IP,
197 19a42f0 Dawsh 2y ago "USERNAME": extra_data.get("username", "{USERNAME}"),
198 19a42f0 Dawsh 2y ago "DATA_USAGE": readable_size(extra_data.get("used_traffic")),
199 19a42f0 Dawsh 2y ago "DATA_LIMIT": data_limit,
200 19a42f0 Dawsh 2y ago "DATA_LEFT": data_left,
201 19a42f0 Dawsh 2y ago "DAYS_LEFT": days_left,
202 19a42f0 Dawsh 2y ago "EXPIRE_DATE": expire_date,
203 19a42f0 Dawsh 2y ago "JALALI_EXPIRE_DATE": jalali_expire_date,
204 19a42f0 Dawsh 2y ago "TIME_LEFT": time_left,
205 19a42f0 Dawsh 2y ago "STATUS_EMOJI": status_emoji,
206 19a42f0 Dawsh 2y ago },
207 19a42f0 Dawsh 2y ago )
208 a47eb4c Dawsh 2y ago
209 a47eb4c Dawsh 2y ago return format_variables
210 a47eb4c Dawsh 2y ago
211 a47eb4c Dawsh 2y ago
212 fb4b69d Dawsh 2y ago def generate_user_configs(
213 a47eb4c Dawsh 2y ago inbounds: list,
214 d6600c5 Dawsh 2y ago key: str,
215 a47eb4c Dawsh 2y ago format_variables: dict,
216 a47eb4c Dawsh 2y ago ) -> Union[List, str]:
217 fb4b69d Dawsh 2y ago
218 a47eb4c Dawsh 2y ago salt = secrets.token_hex(8)
219 fb4b69d Dawsh 2y ago configs = []
220 01cdf2e Dawsh 2y ago
221 a47eb4c Dawsh 2y ago for inb in inbounds:
222 01cdf2e Dawsh 2y ago inb_id, inbound, protocol, tag = (
223 01cdf2e Dawsh 2y ago inb.id,
224 01cdf2e Dawsh 2y ago json.loads(inb.config),
225 01cdf2e Dawsh 2y ago inb.protocol,
226 01cdf2e Dawsh 2y ago inb.tag,
227 01cdf2e Dawsh 2y ago )
228 a47eb4c Dawsh 2y ago
229 a47eb4c Dawsh 2y ago format_variables.update({"PROTOCOL": protocol.name})
230 a47eb4c Dawsh 2y ago if not inbound:
231 a47eb4c Dawsh 2y ago continue
232 a47eb4c Dawsh 2y ago
233 a47eb4c Dawsh 2y ago format_variables.update({"TRANSPORT": inbound["network"]})
234 a47eb4c Dawsh 2y ago with GetDB() as db:
235 a47eb4c Dawsh 2y ago hosts = crud.get_inbound_hosts(db, inb_id)
236 f446990 Dawsh 2y ago
237 a47eb4c Dawsh 2y ago for host in hosts:
238 a88ed47 Mardin 2y ago if host.is_disabled:
239 a88ed47 Mardin 2y ago continue
240 fe923dc Dawsh 2y ago host_snis = host.sni.split(",") if host.sni else []
241 fe923dc Dawsh 2y ago sni_list = host_snis or inbound["sni"]
242 a47eb4c Dawsh 2y ago if sni_list:
243 a47eb4c Dawsh 2y ago sni = random.choice(sni_list).replace("*", salt)
244 fe923dc Dawsh 2y ago else:
245 fe923dc Dawsh 2y ago sni = ""
246 a47eb4c Dawsh 2y ago
247 fe923dc Dawsh 2y ago host_hosts = host.host.split(",") if host.host else []
248 fe923dc Dawsh 2y ago req_host_list = host_hosts or inbound["host"]
249 a47eb4c Dawsh 2y ago if req_host_list:
250 a47eb4c Dawsh 2y ago req_host = random.choice(req_host_list).replace("*", salt)
251 fe923dc Dawsh 2y ago else:
252 fe923dc Dawsh 2y ago req_host = ""
253 01cdf2e Dawsh 2y ago
254 01cdf2e Dawsh 2y ago host_tls = (
255 01cdf2e Dawsh 2y ago None
256 01cdf2e Dawsh 2y ago if host.security == InboundHostSecurity.inbound_default
257 01cdf2e Dawsh 2y ago else host.security.value
258 01cdf2e Dawsh 2y ago )
259 f446990 Dawsh 2y ago data = V2Data(
260 f446990 Dawsh 2y ago protocol.value,
261 f446990 Dawsh 2y ago host.remark.format_map(format_variables),
262 f446990 Dawsh 2y ago host.address.format_map(format_variables),
263 f446990 Dawsh 2y ago host.port or inbound["port"],
264 a9f8ac2 Dawsh 2y ago transport_type=inbound["network"],
265 f446990 Dawsh 2y ago sni=sni,
266 f446990 Dawsh 2y ago host=req_host,
267 f446990 Dawsh 2y ago tls=host_tls or inbound["tls"],
268 8825243 Dawsh 2y ago header_type=inbound.get("header_type"),
269 d6600c5 Dawsh 2y ago alpn=host.alpn.value or None,
270 f446990 Dawsh 2y ago path=(
271 f446990 Dawsh 2y ago host.path.format_map(format_variables)
272 f446990 Dawsh 2y ago if host.path
273 d6600c5 Dawsh 2y ago else inbound.get("path")
274 f446990 Dawsh 2y ago ),
275 8825243 Dawsh 2y ago fingerprint=host.fingerprint.value or inbound.get("fp"),
276 8825243 Dawsh 2y ago reality_pbk=inbound.get("pbk"),
277 8825243 Dawsh 2y ago reality_sid=inbound.get("sid"),
278 8825243 Dawsh 2y ago flow=inbound.get("flow"),
279 f446990 Dawsh 2y ago allow_insecure=host.allowinsecure,
280 d6600c5 Dawsh 2y ago uuid=UUID(gen_uuid(key)),
281 d6600c5 Dawsh 2y ago password=gen_password(key),
282 ab8556b Dawsh 2y ago enable_mux=host.mux,
283 03039e3 Dawsh 2y ago http_headers={
284 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"
285 03039e3 Dawsh 2y ago },
286 248ca9c Dawsh 2y ago shadowsocks_method="chacha20-ietf-poly1305",
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()}"