hiddifypanel 10.11.0__py3-none-any.whl → 10.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. hiddifypanel/VERSION +1 -1
  2. hiddifypanel/VERSION.py +2 -2
  3. hiddifypanel/auth.py +15 -5
  4. hiddifypanel/hutils/encode.py +0 -1
  5. hiddifypanel/hutils/proxy/__init__.py +1 -0
  6. hiddifypanel/hutils/proxy/shared.py +20 -10
  7. hiddifypanel/hutils/proxy/singbox.py +47 -46
  8. hiddifypanel/hutils/proxy/xray.py +39 -23
  9. hiddifypanel/hutils/proxy/xrayjson.py +391 -0
  10. hiddifypanel/hutils/random.py +4 -0
  11. hiddifypanel/models/config.py +7 -2
  12. hiddifypanel/models/config_enum.py +9 -5
  13. hiddifypanel/panel/admin/DomainAdmin.py +3 -2
  14. hiddifypanel/panel/admin/SettingAdmin.py +22 -11
  15. hiddifypanel/panel/admin/templates/model/user_list.html +44 -20
  16. hiddifypanel/panel/commercial/restapi/v1/tgmsg.py +14 -10
  17. hiddifypanel/panel/commercial/restapi/v2/user/apps_api.py +17 -23
  18. hiddifypanel/panel/common_bp/login.py +2 -2
  19. hiddifypanel/panel/init_db.py +1 -1
  20. hiddifypanel/panel/user/templates/base_xray_config.json.j2 +125 -0
  21. hiddifypanel/panel/user/user.py +9 -1
  22. hiddifypanel/static/images/hiddify.png +0 -0
  23. hiddifypanel/static/images/hiddify1.png +0 -0
  24. hiddifypanel/static/new/assets/hiddify-logo-7617d937.png +0 -0
  25. hiddifypanel/static/new/assets/hiddify-logo-7617d937_old.png +0 -0
  26. hiddifypanel/templates/admin-layout.html +22 -11
  27. hiddifypanel/templates/master.html +48 -25
  28. hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
  29. hiddifypanel/translations/en/LC_MESSAGES/messages.po +28 -22
  30. hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
  31. hiddifypanel/translations/fa/LC_MESSAGES/messages.po +15 -11
  32. hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
  33. hiddifypanel/translations/pt/LC_MESSAGES/messages.po +8 -8
  34. hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
  35. hiddifypanel/translations/ru/LC_MESSAGES/messages.po +111 -53
  36. hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
  37. hiddifypanel/translations/zh/LC_MESSAGES/messages.po +108 -58
  38. hiddifypanel/translations.i18n/en.json +18 -17
  39. hiddifypanel/translations.i18n/fa.json +7 -6
  40. hiddifypanel/translations.i18n/pt.json +3 -2
  41. hiddifypanel/translations.i18n/ru.json +53 -52
  42. hiddifypanel/translations.i18n/zh.json +58 -57
  43. {hiddifypanel-10.11.0.dist-info → hiddifypanel-10.12.0.dist-info}/METADATA +1 -1
  44. {hiddifypanel-10.11.0.dist-info → hiddifypanel-10.12.0.dist-info}/RECORD +48 -44
  45. {hiddifypanel-10.11.0.dist-info → hiddifypanel-10.12.0.dist-info}/LICENSE.md +0 -0
  46. {hiddifypanel-10.11.0.dist-info → hiddifypanel-10.12.0.dist-info}/WHEEL +0 -0
  47. {hiddifypanel-10.11.0.dist-info → hiddifypanel-10.12.0.dist-info}/entry_points.txt +0 -0
  48. {hiddifypanel-10.11.0.dist-info → hiddifypanel-10.12.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,391 @@
1
+ import datetime
2
+ import json
3
+ import copy
4
+ from flask import render_template, request, g
5
+ from hiddifypanel import hutils
6
+ from hiddifypanel.models import Proxy, ProxyTransport, ProxyL3, ProxyCDN, ProxyProto, Domain, ConfigEnum, DomainType
7
+ from flask_babel import gettext as _
8
+ from .xray import is_muxable_agent
9
+ OUTBOUND_LEVEL = 8
10
+
11
+
12
+ def configs_as_json(domains: list[Domain], remarks: str) -> str:
13
+ '''Returns xray configs as json'''
14
+ outbounds = []
15
+ for proxy in hutils.proxy.get_valid_proxies(domains):
16
+ outbound = to_xray(proxy)
17
+ if 'msg' not in outbound:
18
+ outbounds.append(outbound)
19
+
20
+ outbounds_len = len(outbounds)
21
+ # reutrn no outbound
22
+ if outbounds_len < 1:
23
+ return ''
24
+
25
+ all_configs = []
26
+ base_config = json.loads(render_template('base_xray_config.json.j2', remarks=remarks))
27
+ # multiple outbounds needs multiple whole base config not just one with multiple outbounds (at least for v2rayng)
28
+ # https://github.com/2dust/v2rayNG/pull/2827#issue-2127534078
29
+ if outbounds_len > 1:
30
+ for out in outbounds:
31
+ base_config['remarks'] = out['tag']
32
+ base_config['outbounds'].insert(0, out)
33
+ all_configs.append(copy.deepcopy(base_config))
34
+ del base_config['outbounds'][0]
35
+ else: # single outbound
36
+ base_config['outbounds'].insert(0, outbounds[0])
37
+ all_configs = base_config
38
+
39
+ json_configs = json.dumps(all_configs, indent=2, cls=hutils.proxy.ProxyJsonEncoder)
40
+ return json_configs
41
+
42
+
43
+ def to_xray(proxy: dict) -> dict:
44
+ outbound = {
45
+ 'tag': f'{proxy["extra_info"]} {proxy["name"]} § {proxy["port"]} {proxy["dbdomain"].id}',
46
+ 'protocol': str(proxy['proto']),
47
+ 'settings': {},
48
+ 'streamSettings': {},
49
+ 'mux': { # default value
50
+ 'enabled': False,
51
+ 'concurrency': -1
52
+ }
53
+ }
54
+ # add multiplex to outbound
55
+ add_multiplex(outbound, proxy)
56
+
57
+ # add stream setting to outbound
58
+ add_stream_settings(outbound, proxy)
59
+
60
+ # add protocol settings to outbound
61
+ add_proto_settings(outbound, proxy)
62
+
63
+ return outbound
64
+
65
+ # region proto settings
66
+
67
+
68
+ def add_proto_settings(base: dict, proxy: dict):
69
+ if proxy['proto'] == ProxyProto.wireguard:
70
+ add_wireguard_settings(base, proxy)
71
+ elif proxy['proto'] == ProxyProto.ss:
72
+ add_shadowsocks_settings(base, proxy)
73
+ elif proxy['proto'] == ProxyProto.vless:
74
+ add_vless_settings(base, proxy)
75
+ elif proxy['proto'] == ProxyProto.vmess:
76
+ add_vmess_settings(base, proxy)
77
+ elif proxy['proto'] == ProxyProto.trojan:
78
+ proxy['password'] = proxy['uuid']
79
+ add_trojan_settings(base, proxy)
80
+
81
+
82
+ def add_wireguard_settings(base: dict, proxy: dict):
83
+
84
+ base['settings']['secretKey'] = proxy['wg_pk']
85
+ base['settings']['reversed'] = [0, 0, 0]
86
+ base['settings']['mtu'] = 1380 # optional
87
+ base['settings']['peers'] = [{
88
+ 'endpoint': f'{proxy["server"]}:{int(proxy["port"])}',
89
+ 'publicKey': proxy["wg_server_pub"]
90
+ # 'allowedIPs':'', 'preSharedKey':'', 'keepAlive':'' # optionals
91
+ }]
92
+
93
+ # optionals
94
+ # base['settings']['address'] = [f'{proxy["wg_ipv4"]}/32',f'{proxy["wg_ipv6"]}/128']
95
+ # base['settings']['workers'] = 4
96
+ # base['settings']['domainStrategy'] = 'ForceIP' # default
97
+
98
+
99
+ def add_vless_settings(base: dict, proxy: dict):
100
+ base['settings']['vnext'] = [
101
+ {
102
+ 'address': proxy['server'],
103
+ 'port': proxy['port'],
104
+ "users": [
105
+ {
106
+ 'id': proxy['uuid'],
107
+ 'encryption': 'none',
108
+ # 'security': 'auto',
109
+ 'flow': 'xtls-rprx-vision' if (proxy['transport'] == ProxyTransport.XTLS or base['streamSettings']['security'] == 'reality') else '',
110
+ 'level': OUTBOUND_LEVEL
111
+ }
112
+ ]
113
+ }
114
+ ]
115
+
116
+
117
+ def add_vmess_settings(base: dict, proxy: dict):
118
+ base['settings']['vnext'] = [
119
+ {
120
+ "address": proxy['server'],
121
+ "port": proxy['port'],
122
+ "users": [
123
+ {
124
+ "id": proxy['uuid'],
125
+ "security": proxy['cipher'],
126
+ "level": OUTBOUND_LEVEL
127
+ }
128
+ ]
129
+ }
130
+ ]
131
+
132
+
133
+ def add_trojan_settings(base: dict, proxy: dict):
134
+ base['settings']['servers'] = [
135
+ {
136
+ # 'email': proxy['uuid'], optional
137
+ 'address': proxy['server'],
138
+ 'port': proxy['port'],
139
+ 'password': proxy['password'],
140
+ 'level': OUTBOUND_LEVEL
141
+ }
142
+ ]
143
+
144
+
145
+ def add_shadowsocks_settings(base: dict, proxy: dict):
146
+ base['settings']['servers'] = [
147
+ {
148
+ 'address': proxy['server'],
149
+ 'port': proxy['port'],
150
+ 'method': proxy['cipher'],
151
+ 'password': proxy['password'],
152
+ 'uot': True,
153
+ 'level': OUTBOUND_LEVEL
154
+ # 'email': '', optional
155
+ }
156
+ ]
157
+
158
+ # endregion
159
+
160
+
161
+ # region stream settings
162
+
163
+ def add_stream_settings(base: dict, proxy: dict):
164
+ ss = base['streamSettings']
165
+ ss['security'] = 'none' # default
166
+
167
+ # security
168
+ if proxy['l3'] == ProxyL3.reality:
169
+ ss['security'] = 'reality'
170
+ elif proxy['l3'] in [ProxyL3.tls, ProxyL3.tls_h2, ProxyL3.tls_h2_h1]:
171
+ ss['security'] = 'tls'
172
+
173
+ # network and transport settings
174
+ if ss['security'] == 'tls' or 'xtls':
175
+ ss['tlsSettings'] = {
176
+ 'serverName': proxy['sni'],
177
+ 'allowInsecure': proxy['allow_insecure'],
178
+ 'fingerprint': proxy['fingerprint'],
179
+ 'alpn': [proxy['alpn']],
180
+ # 'minVersion': '1.2',
181
+ # 'disableSystemRoot': '',
182
+ # 'enableSessionResumption': '',
183
+ # 'pinnedPeerCertificateChainSha256': '',
184
+ # 'certificates': '',
185
+ # 'maxVersion': '1.3', # Go lang sets
186
+ # 'cipherSuites': '', # Go lang sets
187
+ # 'rejectUnknownSni': '', # default is false
188
+ }
189
+ if ss['security'] == 'reality':
190
+ ss['network'] = proxy['transport']
191
+ add_reality_stream(ss, proxy)
192
+ if proxy['l3'] == ProxyL3.kcp:
193
+ ss['network'] = 'kcp'
194
+ add_kcp_stream(ss, proxy)
195
+
196
+ if proxy['l3'] == ProxyL3.h3_quic:
197
+ add_quic_stream(ss, proxy)
198
+
199
+ if proxy['transport'] == 'tcp' or ss['security'] == 'reality' or (ss['security'] == 'none' and proxy['transport'] not in [ProxyTransport.httpupgrade, ProxyTransport.WS]):
200
+ ss['network'] = proxy['transport']
201
+ add_tcp_stream(ss, proxy)
202
+ if proxy['transport'] == ProxyTransport.h2 and ss['security'] == 'none' and ss['security'] != 'reality':
203
+ ss['network'] = proxy['transport']
204
+ add_http_stream(ss, proxy)
205
+ if proxy['transport'] == ProxyTransport.grpc:
206
+ ss['network'] = proxy['transport']
207
+ add_grpc_stream(ss, proxy)
208
+ if proxy['transport'] == ProxyTransport.httpupgrade:
209
+ ss['network'] = proxy['transport']
210
+ add_httpupgrade_stream(ss, proxy)
211
+ if proxy['transport'] == 'ws':
212
+ ss['network'] = proxy['transport']
213
+ add_ws_stream(ss, proxy)
214
+
215
+ # tls fragmentaion
216
+ add_tls_fragmentation_stream_settings(base, proxy)
217
+
218
+
219
+ def add_tcp_stream(ss: dict, proxy: dict):
220
+ if proxy['l3'] == ProxyL3.http:
221
+ ss['tcpSettings'] = {
222
+ 'header': {
223
+ 'type': 'http',
224
+ 'request': {
225
+ 'path': [proxy['path']]
226
+ }
227
+ }
228
+ # 'acceptProxyProtocol': False
229
+ }
230
+ else:
231
+ ss['tcpSettings'] = {
232
+ 'header': {
233
+ 'type': 'none'
234
+ }
235
+ # 'acceptProxyProtocol': False
236
+ }
237
+
238
+
239
+ def add_http_stream(ss: dict, proxy: dict):
240
+ ss['httpSettings'] = {
241
+ 'host': proxy['host'],
242
+ 'path': proxy['path'],
243
+ # 'read_idle_timeout': 10, # default disabled
244
+ # 'health_check_timeout': 15, # default is 15
245
+ # 'method': 'PUT', # default is 15
246
+ # 'headers': {
247
+
248
+ # }
249
+ }
250
+
251
+
252
+ def add_ws_stream(ss: dict, proxy: dict):
253
+ ss['wsSettings'] = {
254
+ 'path': proxy['path'],
255
+ 'headers': {
256
+ "Host": proxy['host']
257
+ }
258
+ # 'acceptProxyProtocol': False,
259
+ }
260
+
261
+
262
+ def add_grpc_stream(ss: dict, proxy: dict):
263
+ ss['grpcSettings'] = {
264
+ 'serviceName': proxy['path'], # proxy['path'] is equal toproxy['grpc_service_name']
265
+ 'idle_timeout': 115, # by default, the health check is not enabled. may solve some "connection drop" issues
266
+ 'health_check_timeout': 20, # default is 20
267
+ # 'initial_windows_size': 0, # 0 means disabled. greater than 65535 means Dynamic Window mechanism will be disabled
268
+ # 'permit_without_stream': False, # health check performed when there are no sub-connections
269
+ # 'multiMode': false, # experimental
270
+ }
271
+
272
+
273
+ def add_httpupgrade_stream(ss: dict, proxy: dict):
274
+ ss['httpupgradeSettings'] = {
275
+ 'path': proxy['path'],
276
+ 'host': proxy['host'],
277
+ # 'acceptProxyProtocol': '', for inbounds only
278
+ }
279
+
280
+
281
+ def add_kcp_stream(ss: dict, proxy: dict):
282
+ # TODO: fix server side configs first
283
+ ss['kcpSettings'] = {}
284
+ return
285
+ ss['kcpSettings'] = {
286
+ 'seed': proxy['path'],
287
+ # 'mtu': 1350, # optional, default value is written
288
+ # 'tti': 50, # optional, default value is written
289
+ # 'uplinkCapacity': 5, # optional, default value is written
290
+ # 'downlinkCapacity': 20, # optional, default value is written
291
+ # 'congestion':False, # optional, default value is written
292
+ # 'readBufferSize': 2,# optional, default value is written
293
+ # 'writeBufferSize':2 # optional, default value is written
294
+ # 'header': { # must be same as server (hiddify doesn't use yet)
295
+ # 'type': 'none' # choices: none(default), srtp, utp, wechat-video, dtls, wireguards
296
+ # }
297
+ }
298
+
299
+
300
+ def add_quic_stream(ss: dict, proxy: dict):
301
+ # TODO: fix server side configs first
302
+ ss['quicSettings'] = {}
303
+ return
304
+ ss['quicSettings'] = {
305
+ 'security': 'chacha20-poly1305',
306
+ 'key': proxy['path'],
307
+ 'header': {
308
+ 'type': 'none'
309
+ }
310
+ }
311
+
312
+
313
+ def add_reality_stream(ss: dict, proxy: dict):
314
+ ss['realitySettings'] = {
315
+ 'serverName': proxy['sni'],
316
+ 'fingerprint': proxy['fingerprint'],
317
+ 'shortId': proxy['reality_short_id'],
318
+ 'publicKey': proxy['reality_pbk'],
319
+ 'show': False,
320
+ }
321
+
322
+
323
+ def add_tls_fragmentation_stream_settings(base: dict, proxy: dict):
324
+ '''Adds tls fragment in the outbounds if tls fragmentation is enabled'''
325
+ if base['streamSettings']['security'] in ['tls', 'reality']:
326
+ if proxy.get('tls_fragment_enable'):
327
+ base['streamSettings']['sockopt'] = {
328
+ 'dialerProxy': 'fragment',
329
+ 'tcpKeepAliveIdle': 100,
330
+ 'tcpNoDelay': True, # recommended to be enabled with "tcpMptcp": true.
331
+ "mark": 255
332
+ # 'tcpFastOpen': True, # the system default setting be used.
333
+ # 'tcpKeepAliveInterval': 0, # 0 means default GO lang settings, -1 means not enable
334
+ # 'tcpcongestion': bbr, # Not configuring means using the system default value
335
+ # 'tcpMptcp': True, # need to be enabled in both server and client configuration (not supported by panel yet)
336
+ }
337
+
338
+ # endregion
339
+
340
+
341
+ def add_multiplex(base: dict, proxy: dict):
342
+ if proxy.get('mux_enable') != "xray":
343
+ return
344
+
345
+ concurrency = proxy['mux_max_connections']
346
+ if concurrency and concurrency > 0:
347
+ base['mux']['enabled'] = True
348
+ base['mux']['concurrency'] = concurrency
349
+ base['mux']['xudpConcurrency'] = concurrency
350
+ base['mux']['xudpProxyUDP443'] = 'reject'
351
+
352
+
353
+ def add_tls_tricks_to_dict(d: dict, proxy: dict):
354
+ if proxy.get('tls_fragment_enable'):
355
+ if g.user_agent.get('is_shadowrocket'):
356
+ d['fragment'] = f'1,{proxy["tls_fragment_size"]},{proxy["tls_fragment_sleep"]}'
357
+ else:
358
+ d['fragment'] = f'{proxy["tls_fragment_size"]},{proxy["tls_fragment_sleep"]},tlshello'
359
+
360
+ if proxy.get("tls_mixed_case"):
361
+ d['mc'] = 1
362
+ if proxy.get("tls_padding_enable"):
363
+ d['padsize'] = proxy["tls_padding_length"]
364
+
365
+
366
+ def add_mux_to_dict(d: dict, proxy):
367
+ if not is_muxable_agent(proxy):
368
+ return
369
+
370
+ # according to github.com/hiddify/ray2sing/
371
+ d['muxtype'] = proxy["mux_protocol"]
372
+ d['muxmaxc'] = proxy["mux_max_connections"]
373
+ d['mux'] = proxy['mux_min_streams']
374
+ d['muxsmax'] = proxy["mux_max_streams"]
375
+ d['muxpad'] = proxy["mux_padding_enable"]
376
+
377
+ if proxy.get('mux_brutal_enable'):
378
+ d['muxup'] = proxy["mux_brutal_up_mbps"]
379
+ d['muxdown'] = proxy["mux_brutal_down_mbps"]
380
+
381
+
382
+ def add_tls_tricks_to_link(proxy: dict) -> str:
383
+ out = {}
384
+ add_tls_tricks_to_dict(out, proxy)
385
+ return hutils.encode.convert_dict_to_url(out)
386
+
387
+
388
+ def add_mux_to_link(proxy: dict) -> str:
389
+ out = {}
390
+ add_mux_to_dict(out, proxy)
391
+ return hutils.encode.convert_dict_to_url(out)
@@ -48,3 +48,7 @@ def get_random_unused_port():
48
48
  while __is_in_used_port(port):
49
49
  port = random.randint(11000, 60000)
50
50
  return port
51
+
52
+
53
+ def random_case(string):
54
+ return ''.join(random.choice((str.upper, str.lower))(c) for c in string)
@@ -63,14 +63,19 @@ def hconfig(key: ConfigEnum, child_id: int | None = None) -> str | int | None:
63
63
  except BaseException:
64
64
  error(f'{key} error!')
65
65
  raise
66
-
66
+ if key.type == int and value != None:
67
+ return int(value)
67
68
  return value
68
69
 
69
70
 
70
71
  def set_hconfig(key: ConfigEnum, value: str | int | bool, child_id: int | None = None, commit: bool = True):
71
72
  if child_id is None:
72
73
  child_id = Child.current.id
74
+
73
75
  print(f"chainging .... {key}---{value}---{child_id}---{commit}")
76
+ if key.type == int and value != None:
77
+ int(value) # for testing int
78
+
74
79
  # hconfig.invalidate(key, child_id)
75
80
  # get_hconfigs.invalidate(child_id)
76
81
  hconfig.invalidate(key, child_id)
@@ -113,7 +118,7 @@ def get_hconfigs(child_id: int | None = None, json=False) -> dict:
113
118
  child_id = Child.current.id
114
119
 
115
120
  return {**{f'{u.key}' if json else u.key: u.value for u in BoolConfig.query.filter(BoolConfig.child_id == child_id).all() if u.key.type == bool},
116
- **{f'{u.key}' if json else u.key: u.value for u in StrConfig.query.filter(StrConfig.child_id == child_id).all() if u.key.type != bool},
121
+ **{f'{u.key}' if json else u.key: int(u.value) if u.key.type == int and u.value != None else u.value for u in StrConfig.query.filter(StrConfig.child_id == child_id).all() if u.key.type != bool},
117
122
  # ConfigEnum.telegram_fakedomain:hdomain(DomainType.telegram_faketls),
118
123
  # ConfigEnum.ssfaketls_fakedomain:hdomain(DomainType.ss_faketls),
119
124
  # ConfigEnum.fake_cdn_domain:hdomain(DomainType.fake_cdn)
@@ -55,6 +55,10 @@ def _StrConfigDscr(category: ConfigCategory, apply_mode: ApplyMode = ApplyMode.n
55
55
  return category, apply_mode, str, show_in_parent
56
56
 
57
57
 
58
+ def _IntConfigDscr(category: ConfigCategory, apply_mode: ApplyMode = ApplyMode.nothing, show_in_parent: bool = True, hide_in_virtual_child=False):
59
+ return category, apply_mode, int, show_in_parent
60
+
61
+
58
62
  class ConfigEnum(metaclass=FastEnum):
59
63
  # category: ConfigCategory
60
64
  __slots__ = ('category', 'apply_mode', 'type', 'show_in_parent', 'hide_in_virtual_child')
@@ -128,13 +132,13 @@ class ConfigEnum(metaclass=FastEnum):
128
132
  # mux
129
133
  mux_enable = _BoolConfigDscr(ConfigCategory.mux, ApplyMode.apply)
130
134
  mux_protocol = _StrConfigDscr(ConfigCategory.mux, ApplyMode.apply)
131
- mux_max_connections = _StrConfigDscr(ConfigCategory.mux, ApplyMode.apply)
132
- mux_min_streams = _StrConfigDscr(ConfigCategory.mux, ApplyMode.apply)
133
- mux_max_streams = _StrConfigDscr(ConfigCategory.mux, ApplyMode.apply)
135
+ mux_max_connections = _IntConfigDscr(ConfigCategory.mux, ApplyMode.apply)
136
+ mux_min_streams = _IntConfigDscr(ConfigCategory.mux, ApplyMode.apply)
137
+ mux_max_streams = _IntConfigDscr(ConfigCategory.mux, ApplyMode.apply)
134
138
  mux_padding_enable = _BoolConfigDscr(ConfigCategory.mux, ApplyMode.apply)
135
139
  mux_brutal_enable = _BoolConfigDscr(ConfigCategory.mux, ApplyMode.apply)
136
- mux_brutal_up_mbps = _StrConfigDscr(ConfigCategory.mux, ApplyMode.apply)
137
- mux_brutal_down_mbps = _StrConfigDscr(ConfigCategory.mux, ApplyMode.apply)
140
+ mux_brutal_up_mbps = _IntConfigDscr(ConfigCategory.mux, ApplyMode.apply)
141
+ mux_brutal_down_mbps = _IntConfigDscr(ConfigCategory.mux, ApplyMode.apply)
138
142
 
139
143
  http_ports = _StrConfigDscr(ConfigCategory.http, ApplyMode.apply)
140
144
  kcp_ports = _StrConfigDscr(ConfigCategory.hidden, ApplyMode.apply)
@@ -234,8 +234,9 @@ class DomainAdmin(AdminLTEModelView):
234
234
  if not hutils.network.is_in_same_asn(d, ipv4_list[0]):
235
235
  server_asn = hutils.network.get_ip_asn_name(ipv4_list[0])
236
236
  domain_asn = hutils.network.get_ip_asn_name(dip) # type: ignore
237
- msg = _("selected domain for REALITY is not in the same ASN. To better use of the protocol, it is better to find a domain in the same ASN.")+(f"<br> Server ASN={server_asn}<br>{d}_ASN={domain_asn}" if server_asn or domain_asn else "")
238
- hutils.flask.flash(msg, 'warning')
237
+ msg = _("selected domain for REALITY is not in the same ASN. To better use of the protocol, it is better to find a domain in the same ASN.") + \
238
+ (f"<br> Server ASN={server_asn}<br>{d}_ASN={domain_asn}" if server_asn or domain_asn else "")
239
+ hutils.flask.flash(msg, 'warning')
239
240
 
240
241
  for d in model.servernames.split(","):
241
242
  if not hutils.network.fallback_domain_compatible_with_servernames(model.domain, d):
@@ -122,7 +122,6 @@ class SettingAdmin(FlaskView):
122
122
 
123
123
 
124
124
  def get_config_form():
125
-
126
125
  strconfigs = StrConfig.query.filter(StrConfig.child_id == Child.current.id).all()
127
126
  boolconfigs = BoolConfig.query.filter(BoolConfig.child_id == Child.current.id).all()
128
127
  bool_types = {c.key: 'bool' for c in boolconfigs}
@@ -154,7 +153,8 @@ def get_config_form():
154
153
  if c.key in bool_types:
155
154
  field = SwitchField(_(f'config.{c.key}.label'), default=c.value, description=_(f'config.{c.key}.description'))
156
155
  elif c.key == ConfigEnum.core_type:
157
- field = wtf.SelectField(_(f"config.{c.key}.label"), choices=[("xray", _("Xray")), ("singbox", _("SingBox"))], description=_(f"config.{c.key}.description"), default=hconfig(c.key))
156
+ field = wtf.SelectField(_(f"config.{c.key}.label"), choices=[("xray", _("Xray")), ("singbox", _(
157
+ "SingBox"))], description=_(f"config.{c.key}.description"), default=hconfig(c.key))
158
158
  elif c.key == ConfigEnum.warp_mode:
159
159
  field = wtf.SelectField(
160
160
  _(f"config.{c.key}.label"), choices=[("disable", _("Disable")), ("all", _("All")), ("custom", _("Only Blocked and Local websites"))],
@@ -168,7 +168,8 @@ def get_config_form():
168
168
  description=_(f"config.{c.key}.description"),
169
169
  default=hconfig(c.key))
170
170
  elif c.key == ConfigEnum.country:
171
- field = wtf.SelectField(_(f"config.{c.key}.label"), choices=[("ir", _("Iran")), ("zh", _("China")), ("other", _("Others"))], description=_(f"config.{c.key}.description"), default=hconfig(c.key))
171
+ field = wtf.SelectField(_(f"config.{c.key}.label"), choices=[("ir", _("Iran")), ("zh", _(
172
+ "China")), ("other", _("Others"))], description=_(f"config.{c.key}.description"), default=hconfig(c.key))
172
173
  elif c.key == ConfigEnum.package_mode:
173
174
  package_modes = [("release", _("Release")), ("beta", _("Beta"))]
174
175
  if hconfig(c.key) == "develop":
@@ -193,9 +194,14 @@ def get_config_form():
193
194
  elif c.key == ConfigEnum.telegram_lib:
194
195
  # if hconfig(ConfigEnum.telegram_lib)=='python':
195
196
  # continue6
196
- libs = [("python", _("lib.telegram.python")), ("tgo", _("lib.telegram.go")),
197
- ("orig", _("lib.telegram.orignal")), ("erlang", _("lib.telegram.erlang"))]
198
- field = wtf.SelectField(_("config.telegram_lib.label"), choices=libs, description=_("config.telegram_lib.description"), default=hconfig(ConfigEnum.telegram_lib))
197
+ libs = [
198
+ ("erlang", _("lib.telegram.erlang")),
199
+ ("python", _("lib.telegram.python")),
200
+ ("tgo", _("lib.telegram.go")),
201
+ # ("orig", _("lib.telegram.orignal")),
202
+ ]
203
+ field = wtf.SelectField(_("config.telegram_lib.label"), choices=libs, description=_(
204
+ "config.telegram_lib.description"), default=hconfig(ConfigEnum.telegram_lib))
199
205
  elif c.key == ConfigEnum.mux_protocol:
200
206
  choices = [("smux", 'smux'), ("yamux", "yamux"), ("h2mux", "h2mux")]
201
207
  field = wtf.SelectField(_(f"config.{c.key}.label"), choices=choices, description=_(f"config.{c.key}.description"), default=hconfig(c.key))
@@ -203,11 +209,13 @@ def get_config_form():
203
209
  elif c.key == ConfigEnum.warp_sites:
204
210
  validators = [wtf.validators.Length(max=2048)]
205
211
  render_kw = {'class': "ltr", 'maxlength': 2048}
206
- field = wtf.TextAreaField(_(f'config.{c.key}.label'), validators, default=c.value, description=_(f'config.{c.key}.description'), render_kw=render_kw)
212
+ field = wtf.TextAreaField(_(f'config.{c.key}.label'), validators, default=c.value,
213
+ description=_(f'config.{c.key}.description'), render_kw=render_kw)
207
214
  elif c.key == ConfigEnum.branding_freetext:
208
215
  validators = [wtf.validators.Length(max=2048)]
209
216
  render_kw = {'class': "ltr", 'maxlength': 2048}
210
- field = custom_widgets.CKTextAreaField(_(f'config.{c.key}.label'), validators, default=c.value, description=_(f'config.{c.key}.description'), render_kw=render_kw)
217
+ field = custom_widgets.CKTextAreaField(_(f'config.{c.key}.label'), validators, default=c.value,
218
+ description=_(f'config.{c.key}.description'), render_kw=render_kw)
211
219
  else:
212
220
  render_kw = {'class': "ltr"}
213
221
  validators = []
@@ -239,11 +247,13 @@ def get_config_form():
239
247
  if c.key == ConfigEnum.telegram_bot_token:
240
248
  validators.append(wtf.validators.Regexp("()|^([0-9]{8,12}:[a-zA-Z0-9_-]{30,40})|$", re.IGNORECASE, _("config.Invalid telegram bot token")))
241
249
  if c.key == ConfigEnum.branding_site:
242
- validators.append(wtf.validators.Regexp("()|(http(s|)://([A-Za-z0-9\\-\\.]+\\.[a-zA-Z]{2,})/?.*)", re.IGNORECASE, _("config.Invalid brand link")))
250
+ validators.append(wtf.validators.Regexp(
251
+ "()|(http(s|)://([A-Za-z0-9\\-\\.]+\\.[a-zA-Z]{2,})/?.*)", re.IGNORECASE, _("config.Invalid brand link")))
243
252
  # render_kw['required']=""
244
253
 
245
254
  if 'secret' in c.key:
246
- validators.append(wtf.validators.Regexp("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", re.IGNORECASE, _('config.invalid uuid')))
255
+ validators.append(wtf.validators.Regexp(
256
+ "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", re.IGNORECASE, _('config.invalid uuid')))
247
257
  render_kw['required'] = ""
248
258
 
249
259
  if c.key == ConfigEnum.proxy_path:
@@ -274,7 +284,8 @@ def get_config_form():
274
284
  if c.key == ConfigEnum.reality_public_key and g.account.mode in [AdminMode.super_admin]:
275
285
  extra_info = f" <a href='{hurl_for('admin.Actions:change_reality_keys')}'>{_('Change')}</a>"
276
286
 
277
- field = wtf.StringField(_(f'config.{c.key}.label'), validators, default=c.value, description=_(f'config.{c.key}.description') + extra_info, render_kw=render_kw)
287
+ field = wtf.StringField(_(f'config.{c.key}.label'), validators, default=c.value,
288
+ description=_(f'config.{c.key}.description') + extra_info, render_kw=render_kw)
278
289
  setattr(CategoryForm, f'{c.key}', field)
279
290
 
280
291
  multifield = wtf.FormField(CategoryForm, Markup('<i class="fa-solid fa-plus"></i>&nbsp' + _(f'config.{cat}.label')))
@@ -7,26 +7,37 @@
7
7
  <summary>
8
8
  {{_("Send Message to User's Telegram")}}
9
9
  </summary>
10
- <button onclick="show_send_message('all')" type="button" class="btn hbtn bg-h-blue">{{_("All Users")}}</button>
11
- <button onclick="show_send_message('expired')" type="button" class="btn hbtn bg-h-red">
12
- {{_("Expired Users")}}</button>
13
- <button onclick="show_send_message('active')" type="button" class="btn hbtn bg-h-green">
14
- {{_("Active Users")}}</button>
15
- <br>
16
- <button onclick="show_send_message('offline 1h')" type="button" class="btn hbtn bg-h-orange">
17
- {{_("Offline more than 1 hour")}}</button>
18
- <button onclick="show_send_message('offline 1d')" type="button" class="btn hbtn bg-h-red">
19
- {{_("Offline more than 1 day")}}</button>
20
- <button onclick="show_send_message('offline 1w')" type="button" class="btn hbtn bg-h-grey">
21
- {{_("Offline more than 1 week")}}</button>
10
+ <div style="margin-top: 10px;">
11
+ <button onclick="show_send_message('all')" type="button" class="btn hbtn bg-h-green">
12
+ {{_("All Users")}}
13
+ </button>
14
+ <button onclick="show_send_message('active')" type="button" class="btn hbtn bg-h-green" style="margin-left: 5px;margin-right: 5px;">
15
+ {{_("Active Users")}}
16
+ </button>
17
+ <button onclick="show_send_message('selected')" type="button" class="btn hbtn bg-h-green">
18
+ {{_("Seleted Users")}}
19
+ </button>
20
+ </div>
21
+
22
+ <div style="margin-top: 10px;">
23
+ <button onclick="show_send_message('offline 1h')" type="button" class="btn hbtn bg-h-red">
24
+ {{_("Offline more than 1 hour")}}</button>
25
+ <button onclick="show_send_message('offline 1d')" type="button" class="btn hbtn bg-h-red" style="margin-left: 5px;">
26
+ {{_("Offline more than 1 day")}}</button>
27
+ <button onclick="show_send_message('offline 1w')" type="button" class="btn hbtn bg-h-red" style="margin-left: 5px;margin-right: 5px;">
28
+ {{_("Offline more than 1 week")}}
29
+ </button>
30
+ <button onclick="show_send_message('expired')" type="button" class="btn hbtn bg-h-red">
31
+ {{_("Expired Users")}}
32
+ </button>
33
+ </div>
22
34
  </details>
23
35
  </div>
24
36
  {% endif %}
25
37
  {{super()}}
26
38
 
27
39
  <div class="callout callout-success">
28
- {{_('User usage will be updated every 6 minutes. To update it now click <a href="%(link)s"
29
- class="btn btn-info">here</a>',link=hurl_for("admin.Actions:update_usage"))}}
40
+ {{_('User usage will be updated every 6 minutes. To update it now click <a href="%(link)s" class="btn btn-info">here</a>',link=hurl_for("admin.Actions:update_usage"))}}
30
41
 
31
42
  </div>
32
43
  <style>
@@ -69,23 +80,36 @@
69
80
  {% block tail_js%}
70
81
  {{super()}}
71
82
  <script>
83
+ function get_selected_users_id() {
84
+ let ids = [];
85
+ $('input[type="checkbox"][name="rowid"]:checked[value][class="action-checkbox"]').each(function () {
86
+ ids.push($(this).val());
87
+ });
88
+ return ids
89
+ }
72
90
  function show_send_message(id) {
91
+ if (id == 'selected') {
92
+ id = get_selected_users_id();
93
+ if (id.length == 0) {
94
+ alert(`{{_("Please select at least one user")}}`);
95
+ return
96
+ }
97
+ }
73
98
  bootbox.prompt({
74
99
  title: '{{_("Please type your message to send to the telegram:")}}',
75
100
  inputType: 'textarea',
76
101
  locale: '{{get_locale()}}',
77
- callback: function (result) {
78
- console.log(result);
79
- if (!result) return
102
+ callback: function (msg) {
103
+ console.log(msg);
104
+ if (!msg) return
80
105
  // let dialog = bootbox.dialog({
81
106
  // message: '<p><i class="fas fa-spin fa-spinner"></i> {{_("Sending")}}</p>'
82
107
  // });
83
-
84
108
  $.ajax({
85
109
  type: "POST",
86
110
  url: '{{hurl_for("api_v1.sendmsgresource")}}',
87
111
 
88
- data: JSON.stringify({ 'text': result, 'id': id }),
112
+ data: JSON.stringify({ 'text': msg, 'id': id }),
89
113
  contentType: "application/json",
90
114
  dataType: "json",
91
115
  error: function (jqx, status, error) {
@@ -131,4 +155,4 @@
131
155
  }
132
156
 
133
157
  </script>
134
- {% endblock %}
158
+ {% endblock %}