hiddifypanel 10.30.6.dev0__py3-none-any.whl → 10.30.7.dev0__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.
hiddifypanel/VERSION CHANGED
@@ -1 +1 @@
1
- 10.30.6.dev0
1
+ 10.30.7.dev0
hiddifypanel/VERSION.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__='10.30.6.dev0'
1
+ __version__='10.30.7.dev0'
2
2
  from datetime import datetime
3
- __release_date__= datetime.strptime('2024-07-05','%Y-%m-%d')
3
+ __release_date__= datetime.strptime('2024-07-09','%Y-%m-%d')
@@ -4,6 +4,7 @@ from .abstract_driver import DriverABS
4
4
  from flask import current_app
5
5
  import json
6
6
  from collections import defaultdict
7
+ from hiddifypanel.cache import cache
7
8
 
8
9
 
9
10
  class SingboxApi(DriverABS):
@@ -18,6 +19,7 @@ class SingboxApi(DriverABS):
18
19
  json_data = json.load(f)
19
20
  return {u.split("@")[0]: 1 for u in json_data['experimental']['v2ray_api']['stats']['users']}
20
21
 
22
+ @cache.cache(ttl=300)
21
23
  def get_inbound_tags(self):
22
24
  try:
23
25
  xray_client = self.get_singbox_client()
@@ -29,6 +29,12 @@ class SSHLibertyBridgeApi(DriverABS):
29
29
 
30
30
  def remove_client(self, user):
31
31
  redis_client = self.get_ssh_redis_client()
32
+ if user.ed25519_public_key is None:
33
+ members = redis_client.smembers(USERS_SET)
34
+ for member in members:
35
+ if member.startswith(user.uuid):
36
+ redis_client.srem(USERS_SET, member)
37
+
32
38
  redis_client.srem(USERS_SET, f'{user.uuid}::{user.ed25519_public_key}')
33
39
  redis_client.hdel(USERS_USAGE, f'{user.uuid}')
34
40
  redis_client.save()
@@ -33,16 +33,19 @@ def get_enabled_users():
33
33
  for driver in enabled_drivers():
34
34
  try:
35
35
  for u, v in driver.get_enabled_users().items():
36
+ # print(u, "enabled", v, driver)
36
37
  if not v:
37
38
  continue
38
39
  d[u] += 1
39
40
  total += 1
40
41
  except Exception as e:
42
+ print(driver)
41
43
  hiddify.error(f'ERROR! {driver.__class__.__name__} has error in get_enabled users')
42
-
44
+ # print(d, total)
43
45
  res = defaultdict(bool)
44
46
  for u, v in d.items():
45
- res[u] = v >= total # ignore singbox
47
+ # res[u] = v >= total # ignore singbox
48
+ res[u] = v >= 1
46
49
  return res
47
50
 
48
51
 
@@ -2,6 +2,7 @@ import xtlsapi
2
2
  from hiddifypanel.models import *
3
3
  from .abstract_driver import DriverABS
4
4
  from collections import defaultdict
5
+ from hiddifypanel.cache import cache
5
6
 
6
7
 
7
8
  class XrayApi(DriverABS):
@@ -19,7 +20,18 @@ class XrayApi(DriverABS):
19
20
  if "user>>>" not in use.name:
20
21
  continue
21
22
  uuid = use.name.split(">>>")[1].split("@")[0]
22
- res[uuid] = 1
23
+ try:
24
+ t = "xtls"
25
+ protocol = "vless"
26
+ xray_client.add_client(t, f'{uuid}', f'{uuid}@hiddify.com', protocol=protocol, flow='xtls-rprx-vision', alter_id=0, cipher='chacha20_poly1305')
27
+ xray_client.remove_client(t, f'{uuid}@hiddify.com')
28
+ res[uuid] = 0
29
+ except xtlsapi.xtlsapi.exceptions.EmailAlreadyExists as e:
30
+ res[uuid] = 1
31
+ except Exception as e:
32
+ print(f"error {e}")
33
+ res[uuid] = 0
34
+
23
35
  return res
24
36
 
25
37
  # xray_client = self.get_xray_client()
@@ -40,10 +52,11 @@ class XrayApi(DriverABS):
40
52
  # enabled[uuid] = e
41
53
  # return enabled
42
54
 
55
+ # @cache.cache(ttl=300)
43
56
  def get_inbound_tags(self):
44
57
  try:
45
58
  xray_client = self.get_xray_client()
46
- inbounds = [inb.name.split(">>>")[1] for inb in xray_client.stats_query('inbound')]
59
+ inbounds = [inb.name.split(">>>")[1] for inb in xray_client.stats_query('')]
47
60
  print(f"Success in get inbound tags {inbounds}")
48
61
  except Exception as e:
49
62
  print(f"error in get inbound tags {e}")
@@ -65,6 +78,7 @@ class XrayApi(DriverABS):
65
78
  'v2ray': 'shadowsocks',
66
79
  'kcp': 'vless',
67
80
  'dispatcher': 'trojan',
81
+ 'reality': 'vless'
68
82
  }
69
83
 
70
84
  def proto(t):
@@ -82,7 +82,7 @@ def to_link(proxy: dict) -> str | dict:
82
82
  return "ShadowTLS is Not Supported for this platform"
83
83
  # return f'{baseurl}?plugin=v2ray-plugin&path={proxy["proxy_path"]}&host={proxy["fakedomain"]}&udp-over-tcp=true#{name_link}'
84
84
  if proxy['proto'] == 'v2ray':
85
- return f'{baseurl}?plugin=v2ray-plugin&mode=websocket&path={proxy["proxy_path"]}&host={proxy["fakedomain"]}&tls&udp-over-tcp=true#{name_link}'
85
+ return f'{baseurl}?plugin=v2ray-plugin&mode=websocket&path={proxy["proxy_path"]}&host={proxy["sni"]}&tls&udp-over-tcp=true#{name_link}'
86
86
 
87
87
  if proxy['proto'] == 'tuic':
88
88
  baseurl = f'tuic://{proxy["uuid"]}:{proxy["uuid"]}@{proxy["server"]}:{proxy["port"]}?congestion_control=cubic&udp_relay_mode=native&sni={proxy["sni"]}&alpn=h3'
@@ -8,7 +8,7 @@ from hiddifypanel.models import hconfig, ConfigEnum
8
8
  from .xray import is_muxable_agent, OUTBOUND_LEVEL
9
9
 
10
10
 
11
- def configs_as_json(domains: list[Domain], user: User, expire_days: int, remarks: str) -> str:
11
+ def configs_as_json(domains: list[Domain], user: User, expire_days: int, remarks: str) -> str:
12
12
  '''Returns xray configs as json'''
13
13
  all_configs = []
14
14
 
@@ -35,50 +35,48 @@ def configs_as_json(domains: list[Domain], user: User, expire_days: int, remark
35
35
  )
36
36
  # endregion
37
37
 
38
- # region show status (active/disable)
39
- active = True
40
38
  if not user.is_active:
39
+ # region show status (active/disable)
41
40
  tag = '✖ ' + (hutils.encode.url_encode('بسته شما به پایان رسید') if hconfig(ConfigEnum.lang) == 'fa' else 'Package Ended')
42
41
  # add user status
43
42
  all_configs.append(
44
43
  null_config(tag)
45
44
  )
46
- active = False
47
- # endregion
48
-
49
- if active:
45
+ # endregion
46
+ else:
50
47
  # TODO: seperate codes to small functions
51
48
  # TODO: check what are unsupported protocols in other apps
52
- unsupported_protos = []
53
- unsupported_transport = []
49
+ unsupported_protos = {}
50
+ unsupported_transport = {}
54
51
  if g.user_agent.get('is_v2rayng'):
55
52
  # TODO: ensure which protocols are not supported in v2rayng
56
- unsupported_protos = [ProxyProto.wireguard, ProxyProto.hysteria, ProxyProto.hysteria2, ProxyProto.tuic, ProxyProto.ss, ProxyProto.ssr, ProxyProto.ssh]
53
+ unsupported_protos = {ProxyProto.wireguard, ProxyProto.hysteria, ProxyProto.hysteria2,
54
+ ProxyProto.tuic, ProxyProto.ss, ProxyProto.ssr, ProxyProto.ssh}
57
55
  if not hutils.flask.is_client_version(hutils.flask.ClientVersion.v2ryang, 1, 8, 18):
58
- unsupported_transport = [ProxyTransport.httpupgrade]
56
+ unsupported_transport = {ProxyTransport.httpupgrade}
59
57
 
60
58
  # multiple outbounds needs multiple whole base config not just one with multiple outbounds (at least for v2rayng)
61
59
  # https://github.com/2dust/v2rayNG/pull/2827#issue-2127534078
62
60
  outbounds = []
63
61
  for proxy in hutils.proxy.get_valid_proxies(domains):
64
- if unsupported_protos and proxy['proto'] in unsupported_protos:
62
+ if proxy['proto'] in unsupported_protos:
65
63
  continue
66
- if unsupported_transport and proxy['transport'] in unsupported_transport:
64
+ if proxy['transport'] in unsupported_transport:
67
65
  continue
68
66
  outbound = to_xray(proxy)
69
- if 'msg' not in outbound:
70
- outbounds.append(outbound)
67
+ outbounds.append(outbound)
71
68
 
72
69
  base_config = json.loads(render_template('base_xray_config.json.j2', remarks=remarks))
73
70
  if len(outbounds) > 1:
74
71
  for out in outbounds:
75
- base_config['remarks'] = out['tag']
76
- base_config['outbounds'].insert(0, out)
77
- if all_configs:
78
- all_configs.insert(0, copy.deepcopy(base_config))
79
- else:
80
- all_configs.append(copy.deepcopy(base_config))
81
- del base_config['outbounds'][0]
72
+ base = copy.deepcopy(base_config)
73
+ base['remarks'] = out['tag']
74
+ base['outbounds'].insert(0, out)
75
+ # if all_configs:
76
+ # all_configs.insert(0, copy.deepcopy(base_config))
77
+ # else:
78
+ all_configs.append(base)
79
+
82
80
  else: # single outbound
83
81
  base_config['outbounds'].insert(0, outbounds[0])
84
82
  all_configs = base_config
@@ -114,7 +114,7 @@ class DomainAdmin(AdminLTEModelView):
114
114
  if not dip:
115
115
  dip = hutils.network.resolve_domain_with_api(model.domain)
116
116
  myip = hutils.network.get_ip(4)
117
- if myip == dip and model.mode == DomainType.direct:
117
+ if myip == dip and model.mode in [DomainType.direct, DomainType.sub_link_only]:
118
118
  badge_type = ''
119
119
  elif dip and model.mode != DomainType.direct and myip != dip:
120
120
  badge_type = 'warning'
@@ -10,6 +10,7 @@ from wtforms.validators import NumberRange
10
10
  from flask_babel import lazy_gettext as _
11
11
  from flask import g, request # type: ignore
12
12
  from markupsafe import Markup
13
+ from sqlalchemy import desc
13
14
 
14
15
  from hiddifypanel.hutils.flask import hurl_for
15
16
  from wtforms.validators import Regexp, ValidationError
@@ -148,7 +149,7 @@ class UserAdmin(AdminLTEModelView):
148
149
  href = f'{hiddify.get_account_panel_link(model, request.host, is_https=True)}#{hutils.encode.unicode_slug(model.name)}'
149
150
 
150
151
  link = f"""<a target='_blank' class='share-link btn btn-xs btn-primary' data-copy='{href}' href='{href}'>
151
- <i class='fa-solid fa-arrow-up-right-from-square'></i>
152
+ <i class='fa-solid fa-arrow-up-right-from-square'></i>
152
153
  {_("Current Domain")} </a>"""
153
154
 
154
155
  domains = [d for d in Domain.get_domains() if d.domain != request.host]
@@ -171,7 +172,7 @@ class UserAdmin(AdminLTEModelView):
171
172
  </div>
172
173
  """)
173
174
 
174
- def _expire_formatter(view, context, model, name):
175
+ def _expire_formatter(view, context, model: User, name):
175
176
  remaining = model.remaining_days
176
177
 
177
178
  diff = datetime.timedelta(days=remaining)
@@ -231,7 +232,8 @@ class UserAdmin(AdminLTEModelView):
231
232
  # delattr(form,'disable_user')
232
233
  else:
233
234
  remaining = form._obj.remaining_days # remaining_days(form._obj)
234
- msg = _("Remaining: ") + hutils.convert.format_timedelta(datetime.timedelta(days=remaining))
235
+ relative_remaining = hutils.convert.format_timedelta(datetime.timedelta(days=remaining))
236
+ msg = _("Remaining about %(relative)s, exactly %(days)s days", relative=relative_remaining, days=remaining)
235
237
  form.reset_days.label.text += f" ({msg})"
236
238
  usr_usage = f" ({_('user.home.usage.title')} {round(form._obj.current_usage_GB,3)}GB)"
237
239
  form.reset_usage.label.text += usr_usage
@@ -240,10 +242,18 @@ class UserAdmin(AdminLTEModelView):
240
242
 
241
243
  form.usage_limit.label.text += usr_usage
242
244
 
243
- # if form._obj.mode==UserMode.disable:
244
- # delattr(form,'disable_user')
245
- # form.disable_user.data=form._obj.mode==UserMode.disable
246
- form.package_days.label.text += f" ({msg})"
245
+ # if form._obj.mode==UserMode.disable:
246
+ # delattr(form,'disable_user')
247
+ # form.disable_user.data=form._obj.mode==UserMode.disable
248
+ if form._obj.start_date:
249
+ started = form._obj.start_date - datetime.date.today()
250
+ msg = _("Started from %(relative)s", relative=hutils.convert.format_timedelta(started))
251
+ form.package_days.label.text += f" ({msg})"
252
+ if started.days <= 0:
253
+ exact_start = _("Started %(days)s days ago", days=-started.days)
254
+ else:
255
+ exact_start = _("Will Start in %(days)s days", days=started.days)
256
+ form.package_days.description += f" ({exact_start})"
247
257
 
248
258
  def get_edit_form(self):
249
259
  form = super().get_edit_form()
@@ -295,7 +305,7 @@ class UserAdmin(AdminLTEModelView):
295
305
  def after_model_change(self, form, model, is_created):
296
306
  if hconfig(ConfigEnum.first_setup):
297
307
  set_hconfig(ConfigEnum.first_setup, False)
298
- user = User.query.filter(User.uuid == model.uuid).first()
308
+ user = User.query.filter(User.uuid == model.uuid).first() or abort(404)
299
309
  if user.is_active:
300
310
  user_driver.add_client(model)
301
311
  else:
@@ -314,6 +324,7 @@ class UserAdmin(AdminLTEModelView):
314
324
 
315
325
  def get_list(self, page, sort_column, sort_desc, search, filters, page_size=50, *args, **kwargs):
316
326
  res = None
327
+ self._auto_joins = {}
317
328
  # print('aaa',args, kwargs)
318
329
  if sort_column in ['remaining_days', 'is_active']:
319
330
  query = self.get_query()
@@ -365,14 +376,12 @@ class UserAdmin(AdminLTEModelView):
365
376
  abort(403)
366
377
 
367
378
  query = query.filter(User.added_by.in_(admin.recursive_sub_admins_ids()))
368
-
379
+ query = query.order_by(desc(User.id))
369
380
  return query
370
381
 
371
382
  # Override get_count_query() to include the filter condition in the count query
372
383
  def get_count_query(self):
373
384
  # Get the base count query
374
- query = self.get_query()
375
- from sqlalchemy import func
376
385
 
377
386
  # query = query.session.query(func.count(User.id))
378
387
  query = super().get_count_query()
@@ -50,6 +50,7 @@ def _add_users_usage(users_usage_data: Dict[User, Dict], child_id, sync=False):
50
50
  res = {}
51
51
  have_change = False
52
52
  before_enabled_users = user_driver.get_enabled_users()
53
+
53
54
  daily_usage = {}
54
55
  today = datetime.date.today()
55
56
  for adm in AdminUser.query.all():
@@ -64,15 +65,14 @@ def _add_users_usage(users_usage_data: Dict[User, Dict], child_id, sync=False):
64
65
  userDetails = {p.user_id: p for p in UserDetail.query.filter(UserDetail.child_id == child_id).all()}
65
66
  for user, uinfo in users_usage_data.items():
66
67
  usage_bytes = uinfo['usage']
67
- devices = uinfo['devices']
68
68
 
69
69
  # UserDetails things
70
70
  detail = userDetails.get(user.id)
71
71
  if not detail:
72
72
  detail = UserDetail(user_id=user.id, child_id=child_id)
73
73
  db.session.add(detail)
74
- detail.connected_devices = devices
75
- detail.current_usage_GB = detail.current_usage_GB or 0
74
+ if uinfo['devices'] != detail.connected_devices:
75
+ detail.connected_devices = uinfo['devices']
76
76
 
77
77
  # Enable the user if isn't already
78
78
  if not before_enabled_users[user.uuid] and user.is_active:
@@ -86,22 +86,21 @@ def _add_users_usage(users_usage_data: Dict[User, Dict], child_id, sync=False):
86
86
  res[user.uuid] = "No usage"
87
87
  else:
88
88
  # Set new daily usage of the user
89
- if sync:
90
- if daily_usage.get(user.added_by, daily_usage[1]).usage != usage_bytes:
91
- daily_usage.get(user.added_by, daily_usage[1]).usage = usage_bytes
89
+ if sync and daily_usage.get(user.added_by, daily_usage[1]).usage != usage_bytes:
90
+ daily_usage.get(user.added_by, daily_usage[1]).usage = usage_bytes
92
91
  else:
93
92
  daily_usage.get(user.added_by, daily_usage[1]).usage += usage_bytes
94
93
 
95
- in_gig = (usage_bytes) / to_gig_d
94
+ in_bytes = usage_bytes
96
95
 
97
96
  # Set new current usage of the user
98
- if sync:
99
- if user.current_usage_GB != in_gig:
100
- user.current_usage_GB = in_gig
97
+ if sync and user.current_usage != in_bytes:
98
+ user.current_usage = in_bytes
101
99
  # detail.current_usage_GB = in_gig
102
100
  else:
103
- user.current_usage_GB += in_gig
104
- detail.current_usage_GB += in_gig
101
+ user.current_usage += in_bytes
102
+ detail.current_usage = detail.current_usage or 0
103
+ detail.current_usage += in_bytes
105
104
 
106
105
  # Change last online time of the user
107
106
  user.last_online = datetime.datetime.now()
@@ -111,9 +110,10 @@ def _add_users_usage(users_usage_data: Dict[User, Dict], child_id, sync=False):
111
110
  if user.start_date is None:
112
111
  user.start_date = datetime.date.today()
113
112
 
114
- res[user.uuid] = in_gig
113
+ res[user.uuid] = f'{in_bytes/1000000:0.3f}MB'
115
114
 
116
115
  # Remove user from drivers(singbox, xray, wireguard etc.) if they're inactive
116
+ # print(before_enabled_users[user.uuid], user.is_active)
117
117
  if before_enabled_users[user.uuid] and not user.is_active:
118
118
  print(f"Removing enabled client {user.uuid} ")
119
119
  user_driver.remove_client(user)
@@ -122,14 +122,25 @@ def _add_users_usage(users_usage_data: Dict[User, Dict], child_id, sync=False):
122
122
 
123
123
  db.session.commit() # type: ignore
124
124
 
125
+ # Remove invalid users
126
+ for uuid in before_enabled_users:
127
+ if uuid in res:
128
+ continue
129
+
130
+ user = User.query.filter(User.uuid == uuid).first()
131
+ if not user:
132
+ user_driver.remove_client(User(uuid=uuid))
133
+ elif not user.is_active:
134
+ user_driver.remove_client(user)
135
+
136
+ # print("------------------", res)
125
137
  # Apply the changes to the drivers
126
138
  if have_change:
127
139
  hiddify.quick_apply_users()
128
140
 
129
141
  # Sync the new data with the parent node if the data has not been set by the parent node itself and the current panel is a child panel
130
- if not sync:
131
- if hutils.node.is_child():
132
- hutils.node.child.sync_users_usage_with_parent()
142
+ if not sync and hutils.node.is_child():
143
+ hutils.node.child.sync_users_usage_with_parent()
133
144
 
134
145
  return {"status": 'success', "comments": res}
135
146