hiddifypanel 10.80.0__py3-none-any.whl → 10.80.0.dev8__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 +1 -1
- hiddifypanel/VERSION.py +1 -1
- hiddifypanel/base.py +129 -43
- hiddifypanel/cache.py +1 -1
- hiddifypanel/database.py +0 -7
- hiddifypanel/hutils/flask.py +3 -3
- hiddifypanel/hutils/proxy/__init__.py +0 -1
- hiddifypanel/hutils/proxy/clash.py +2 -2
- hiddifypanel/hutils/proxy/shared.py +6 -6
- hiddifypanel/hutils/proxy/singbox.py +1 -1
- hiddifypanel/hutils/proxy/xray.py +2 -2
- hiddifypanel/hutils/proxy/xrayjson.py +7 -10
- hiddifypanel/models/config.py +1 -4
- hiddifypanel/models/config_enum.py +2 -2
- hiddifypanel/models/proxy.py +1 -1
- hiddifypanel/panel/__init__.py +8 -8
- hiddifypanel/panel/admin/AdminstratorAdmin.py +8 -7
- hiddifypanel/panel/admin/DomainAdmin.py +98 -132
- hiddifypanel/panel/admin/QuickSetup.py +8 -8
- hiddifypanel/panel/admin/UserAdmin.py +33 -58
- hiddifypanel/panel/admin/templates/index.html +4 -6
- hiddifypanel/panel/admin/templates/model/user_list.html +3 -11
- hiddifypanel/panel/cli.py +2 -11
- hiddifypanel/panel/commercial/restapi/v1/tgbot.py +1 -19
- hiddifypanel/panel/commercial/restapi/v2/admin/system_actions.py +1 -5
- hiddifypanel/panel/commercial/restapi/v2/admin/user_api.py +1 -2
- hiddifypanel/panel/common.py +1 -1
- hiddifypanel/panel/init_db.py +16 -19
- hiddifypanel/panel/usage.py +3 -13
- hiddifypanel/panel/user/user.py +18 -12
- hiddifypanel/templates/fake.html +320 -0
- hiddifypanel/translations/en/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/en/LC_MESSAGES/messages.po +13 -82
- hiddifypanel/translations/fa/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/fa/LC_MESSAGES/messages.po +12 -81
- hiddifypanel/translations/pt/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/pt/LC_MESSAGES/messages.po +4 -73
- hiddifypanel/translations/ru/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/ru/LC_MESSAGES/messages.po +9 -79
- hiddifypanel/translations/zh/LC_MESSAGES/messages.mo +0 -0
- hiddifypanel/translations/zh/LC_MESSAGES/messages.po +4 -73
- hiddifypanel/translations.i18n/en.json +7 -50
- hiddifypanel/translations.i18n/fa.json +6 -49
- hiddifypanel/translations.i18n/fr.json +2 -2
- hiddifypanel/translations.i18n/my.json +2 -2
- hiddifypanel/translations.i18n/pt.json +4 -47
- hiddifypanel/translations.i18n/ru.json +7 -50
- hiddifypanel/translations.i18n/zh.json +4 -47
- {hiddifypanel-10.80.0.dist-info → hiddifypanel-10.80.0.dev8.dist-info}/METADATA +6 -13
- {hiddifypanel-10.80.0.dist-info → hiddifypanel-10.80.0.dev8.dist-info}/RECORD +53 -69
- hiddifypanel/apps/__init__.py +0 -0
- hiddifypanel/apps/asgi_app.py +0 -7
- hiddifypanel/apps/celery_app.py +0 -3
- hiddifypanel/apps/wsgi_app.py +0 -5
- hiddifypanel/base_setup.py +0 -82
- hiddifypanel/celery.py +0 -45
- hiddifypanel/hutils/proxy/wireguard.py +0 -34
- hiddifypanel/panel/hlogger.py +0 -32
- hiddifypanel/panel/node/__init__.py +0 -9
- hiddifypanel/panel/node/a.py +0 -14
- hiddifypanel/panel/node/hello.py +0 -14
- hiddifypanel/panel/node/test.proto +0 -13
- hiddifypanel/panel/node/test_grpc.py +0 -40
- hiddifypanel/panel/node/test_pb2.py +0 -40
- hiddifypanel/panel/node/test_pb2.pyi +0 -17
- hiddifypanel/panel/node/test_pb2_grpc.py +0 -97
- {hiddifypanel-10.80.0.dist-info → hiddifypanel-10.80.0.dev8.dist-info}/LICENSE.md +0 -0
- {hiddifypanel-10.80.0.dist-info → hiddifypanel-10.80.0.dev8.dist-info}/WHEEL +0 -0
- {hiddifypanel-10.80.0.dist-info → hiddifypanel-10.80.0.dev8.dist-info}/entry_points.txt +0 -0
@@ -16,7 +16,6 @@ from hiddifypanel.panel import hiddify, custom_widgets
|
|
16
16
|
from .adminlte import AdminLTEModelView
|
17
17
|
from hiddifypanel import hutils
|
18
18
|
|
19
|
-
from loguru import logger
|
20
19
|
from flask import current_app
|
21
20
|
# Define a custom field type for the related domains
|
22
21
|
|
@@ -153,165 +152,132 @@ class DomainAdmin(AdminLTEModelView):
|
|
153
152
|
|
154
153
|
# TODO: refactor this function
|
155
154
|
def on_model_change(self, form, model, is_created):
|
156
|
-
|
157
|
-
model.domain = (model.domain or '').lower().strip()
|
158
|
-
|
159
|
-
# Basic validation
|
155
|
+
model.domain = (model.domain or '').lower()
|
160
156
|
if model.domain == '' and model.mode != DomainType.fake:
|
161
157
|
raise ValidationError(_("domain.empty.allowed_for_fake_only"))
|
158
|
+
configs = get_hconfigs()
|
159
|
+
for c in configs:
|
160
|
+
if "domain" in c and c not in [ConfigEnum.decoy_domain, ConfigEnum.reality_fallback_domain] and c.category != 'hidden':
|
161
|
+
if model.domain == configs[c]:
|
162
|
+
raise ValidationError(_("You have used this domain in: ") + _(f"config.{c}.label"))
|
163
|
+
|
164
|
+
for td in Domain.query.filter(Domain.mode == DomainType.reality, Domain.domain != model.domain).all():
|
165
|
+
# print(td)
|
166
|
+
if td.servernames and (model.domain in td.servernames.split(",")):
|
167
|
+
raise ValidationError(_("You have used this domain in: ") + _(f"config.reality_server_names.label") + td.domain)
|
168
|
+
|
169
|
+
if is_created and Domain.query.filter(Domain.domain == model.domain, Domain.child_id == model.child_id).count() > 1:
|
170
|
+
raise ValidationError(_("You have used this domain in: "))
|
162
171
|
|
163
|
-
self._validate_not_used_before(model,is_created)
|
164
172
|
ipv4_list = hutils.network.get_ips(4)
|
165
173
|
ipv6_list = hutils.network.get_ips(6)
|
166
|
-
server_ips = [*ipv4_list, *ipv6_list]
|
167
174
|
|
168
|
-
if not
|
175
|
+
if not ipv4_list and not ipv6_list:
|
169
176
|
raise ValidationError(_("Couldn't find your ip addresses"))
|
170
177
|
|
171
|
-
# Validate domain based on mode
|
172
178
|
if "*" in model.domain and model.mode not in [DomainType.cdn, DomainType.auto_cdn_ip]:
|
173
179
|
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
|
174
180
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
self._validate_domain_ips(model, server_ips)
|
179
|
-
|
180
|
-
# Handle CDN IP settings
|
181
|
-
if model.mode == DomainType.direct and model.cdn_ip:
|
182
|
-
model.cdn_ip = ""
|
183
|
-
raise ValidationError(_("Specifying CDN IP is only valid for CDN mode"))
|
184
|
-
|
185
|
-
if model.mode == DomainType.fake and not model.cdn_ip:
|
186
|
-
model.cdn_ip = str(server_ips[0])
|
187
|
-
|
188
|
-
if model.cdn_ip:
|
189
|
-
try:
|
190
|
-
hutils.network.auto_ip_selector.get_clean_ip(str(model.cdn_ip))
|
191
|
-
except Exception:
|
192
|
-
raise ValidationError(_("Error in auto cdn format"))
|
193
|
-
|
194
|
-
# Update show domains
|
195
|
-
if len(model.show_domains) == Domain.query.count():
|
196
|
-
model.show_domains = []
|
197
|
-
|
198
|
-
# Handle mode-specific settings
|
199
|
-
if model.mode == DomainType.old_xtls_direct and not hconfig(ConfigEnum.xtls_enable):
|
200
|
-
set_hconfig(ConfigEnum.xtls_enable, True)
|
201
|
-
hutils.proxy.get_proxies().invalidate_all()
|
202
|
-
elif model.mode == DomainType.reality:
|
203
|
-
self._validate_reality_settings(model, server_ips)
|
204
|
-
|
205
|
-
# Signal config update if needed
|
206
|
-
old_db_domain = Domain.by_domain(model.domain)
|
207
|
-
if is_created or not old_db_domain or old_db_domain.mode != model.mode:
|
208
|
-
# return hiddify.reinstall_action(complete_install=False, domain_changed=True)
|
209
|
-
hutils.flask.flash_config_success(restart_mode=ApplyMode.apply_config, domain_changed=True)
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
def _update_cloudflare(self, model, ipv4_list,ipv6_list):
|
181
|
+
skip_check = "*" in model.domain or model.domain == ""
|
214
182
|
if hconfig(ConfigEnum.cloudflare) and model.mode not in [DomainType.fake, DomainType.relay, DomainType.reality]:
|
215
183
|
try:
|
216
184
|
proxied = model.mode in [DomainType.cdn, DomainType.auto_cdn_ip]
|
217
|
-
|
218
|
-
hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv4_list[0]), "A", proxied=proxied)
|
185
|
+
hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv4_list[0]), "A", proxied=proxied)
|
219
186
|
if ipv6_list:
|
220
187
|
hutils.network.cf_api.add_or_update_dns_record(model.domain, str(ipv6_list[0]), "AAAA", proxied=proxied)
|
221
|
-
|
188
|
+
|
189
|
+
skip_check = True
|
222
190
|
except Exception as e:
|
223
191
|
raise ValidationError(__("cloudflare.error") + f' {e}')
|
224
|
-
|
192
|
+
# elif model.mode==DomainType.auto_cdn_ip:
|
193
|
+
# if model.alias and not model.alias.replace("_", "").isalnum():
|
194
|
+
# hutils.flask.flash(__("Using alias with special charachters may cause problem in some clients like FairVPN."), 'warning')
|
195
|
+
# raise ValidationError(_("You have to add your cloudflare api key to use this feature: "))
|
225
196
|
|
226
|
-
|
227
|
-
|
228
|
-
if
|
229
|
-
|
230
|
-
|
197
|
+
dips = hutils.network.get_domain_ips(model.domain)
|
198
|
+
server_ips = [*ipv4_list, *ipv6_list]
|
199
|
+
if model.sub_link_only:
|
200
|
+
if not dips:
|
201
|
+
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain")) # type: ignore
|
202
|
+
elif not skip_check:
|
203
|
+
if not dips:
|
204
|
+
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain")) # type: ignore
|
205
|
+
|
206
|
+
domain_ip_is_same_as_panel = False
|
207
|
+
|
208
|
+
for mip in server_ips:
|
209
|
+
domain_ip_is_same_as_panel |= mip in dips
|
210
|
+
server_ips_str = ', '.join(list(map(str, server_ips)))
|
211
|
+
dips_str = ', '.join(list(map(str, dips)))
|
212
|
+
|
213
|
+
if model.mode == DomainType.direct and not domain_ip_is_same_as_panel:
|
214
|
+
# hutils.flask.flash(__(f"Domain IP={dip} is not matched with your ip={', '.join(list(map(str, ipv4_list)))} which is required in direct mode"),category='error')
|
215
|
+
raise ValidationError(
|
216
|
+
__("Domain IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode", server_ip=server_ips_str, domain_ip=dips_str)) # type: ignore
|
217
|
+
|
218
|
+
if domain_ip_is_same_as_panel and model.mode in [DomainType.cdn, DomainType.relay, DomainType.fake, DomainType.auto_cdn_ip]:
|
219
|
+
# # hutils.flask.flash(__(f"In CDN mode, Domain IP={dip} should be different to your ip={', '.join(list(map(str, ipv4_list)))}"), 'warning')
|
220
|
+
raise ValidationError(__("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
|
221
|
+
server_ip=server_ips_str, domain_ip=dips_str)) # type: ignore
|
222
|
+
|
223
|
+
# if model.mode in [DomainType.ss_faketls, DomainType.telegram_faketls]:
|
224
|
+
# if len(Domain.query.filter(Domain.mode==model.mode and Domain.id!=model.id).all())>0:
|
225
|
+
# ValidationError(f"another {model.mode} is exist")
|
226
|
+
|
227
|
+
model.domain = model.domain.lower()
|
228
|
+
if model.mode == DomainType.direct and model.cdn_ip:
|
229
|
+
model.cdn_ip = ""
|
230
|
+
raise ValidationError(f"Specifying CDN IP is only valid for CDN mode")
|
231
231
|
|
232
|
-
model.
|
233
|
-
|
234
|
-
for v in [model.domain, model.servernames]:
|
235
|
-
domains_to_check.update(d.strip() for d in v.split(",") if d.strip())
|
232
|
+
if model.mode == DomainType.fake and not model.cdn_ip:
|
233
|
+
model.cdn_ip = str(server_ips[0])
|
236
234
|
|
237
|
-
|
238
|
-
|
239
|
-
if not hutils.network.is_domain_reality_friendly(d):
|
240
|
-
raise ValidationError(_("Domain is not REALITY friendly!") + f' {d}')
|
235
|
+
# if model.mode==DomainType.fake and model.cdn_ip!=myip:
|
236
|
+
# raise ValidationError(f"Specifying CDN IP is only valid for CDN mode")
|
241
237
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
238
|
+
# # Update the many-to-many relationship
|
239
|
+
if len(model.show_domains) == Domain.query.count():
|
240
|
+
model.show_domains = []
|
241
|
+
if model.mode == DomainType.old_xtls_direct:
|
242
|
+
if not hconfig(ConfigEnum.xtls_enable):
|
243
|
+
set_hconfig(ConfigEnum.xtls_enable, True)
|
244
|
+
hutils.proxy.get_proxies().invalidate_all()
|
245
|
+
elif model.mode == DomainType.reality:
|
246
|
+
if not hconfig(ConfigEnum.reality_enable):
|
247
|
+
set_hconfig(ConfigEnum.reality_enable, True)
|
248
|
+
hutils.proxy.get_proxies().invalidate_all()
|
249
|
+
model.servernames = (model.servernames or model.domain).lower()
|
250
|
+
for v in set([model.domain, model.servernames]):
|
251
|
+
for d in v.split(","):
|
252
|
+
if not d:
|
253
|
+
continue
|
254
|
+
if not hutils.network.is_domain_reality_friendly(d): # the minimum requirement for the REALITY protocol is to have tls1.3 and h2
|
255
|
+
raise ValidationError(_("Domain is not REALITY friendly!") + f' {d}')
|
256
|
+
|
257
|
+
if not hutils.network.is_in_same_asn(d, server_ips[0]):
|
258
|
+
dip = next(iter(dips))
|
247
259
|
server_asn = hutils.network.get_ip_asn(server_ips[0])
|
248
|
-
domain_asn = hutils.network.get_ip_asn(dip)
|
249
|
-
msg = _("domain.reality.asn_issue")
|
250
|
-
|
251
|
-
msg += f"<br> Server ASN={server_asn}<br>{d}_ASN={domain_asn}"
|
260
|
+
domain_asn = hutils.network.get_ip_asn(dip) # type: ignore
|
261
|
+
msg = _("domain.reality.asn_issue") + \
|
262
|
+
(f"<br> Server ASN={server_asn}<br>{d}_ASN={domain_asn}" if server_asn or domain_asn else "")
|
252
263
|
hutils.flask.flash(msg, 'warning')
|
253
|
-
except Exception as e:
|
254
|
-
logger.warning(f"ASN check failed for domain {d}: {str(e)}")
|
255
|
-
|
256
|
-
# Check fallback compatibility
|
257
|
-
for d in model.servernames.split(","):
|
258
|
-
if d.strip() and not hutils.network.fallback_domain_compatible_with_servernames(model.domain, d):
|
259
|
-
msg = _("REALITY Fallback domain is not compatible with server names!") + f' {d} != {model.domain}'
|
260
|
-
hutils.flask.flash(msg, 'warning')
|
261
264
|
|
265
|
+
for d in model.servernames.split(","):
|
266
|
+
if not hutils.network.fallback_domain_compatible_with_servernames(model.domain, d):
|
267
|
+
msg = _("REALITY Fallback domain is not compaitble with server names!") + f' {d} != {model.domain}'
|
268
|
+
hutils.flask.flash(msg, 'warning')
|
262
269
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
raise ValidationError(_("You have used this domain in: ") + _(f"config.{c}.label"))
|
269
|
-
|
270
|
-
for td in Domain.query.filter(Domain.mode == DomainType.reality, Domain.domain != model.domain).all():
|
271
|
-
# print(td)
|
272
|
-
if td.servernames and (model.domain in td.servernames.split(",")):
|
273
|
-
raise ValidationError(_("You have used this domain in: ") + _(f"config.reality_server_names.label") + td.domain)
|
270
|
+
if (model.cdn_ip):
|
271
|
+
try:
|
272
|
+
hutils.network.auto_ip_selector.get_clean_ip(str(model.cdn_ip))
|
273
|
+
except BaseException:
|
274
|
+
raise ValidationError(_("Error in auto cdn format"))
|
274
275
|
|
275
|
-
|
276
|
-
|
276
|
+
old_db_domain = Domain.by_domain(model.domain)
|
277
|
+
if is_created or not old_db_domain or old_db_domain.mode != model.mode:
|
278
|
+
# return hiddify.reinstall_action(complete_install=False, domain_changed=True)
|
279
|
+
hutils.flask.flash_config_success(restart_mode=ApplyMode.apply_config, domain_changed=True)
|
277
280
|
|
278
|
-
def _validate_domain_ips(self, model, server_ips):
|
279
|
-
"""Validate domain IP resolution and matching"""
|
280
|
-
|
281
|
-
# Skip validation for wildcard or empty domains
|
282
|
-
if (model.domain.startswith('*') or not model.domain) and model.mode not in [DomainType.direct]:
|
283
|
-
return True
|
284
|
-
if model.mode in [DomainType.fake, DomainType.reality, DomainType.relay]:
|
285
|
-
return True
|
286
|
-
# Resolve domain IPs with timeout
|
287
|
-
try:
|
288
|
-
dips = hutils.network.get_domain_ips(model.domain)
|
289
|
-
except Exception as e:
|
290
|
-
logger.error(f"Error resolving domain {model.domain}: {str(e)}")
|
291
|
-
raise ValidationError(_("Domain cannot be resolved! Please check DNS settings"))
|
292
|
-
|
293
|
-
# Validate resolution success
|
294
|
-
if not dips:
|
295
|
-
raise ValidationError(_("Domain cannot be resolved! Please check DNS settings"))
|
296
|
-
|
297
|
-
# Check IP matching based on mode
|
298
|
-
domain_ip_matches_server = any(ip in dips for ip in server_ips)
|
299
|
-
server_ips_str = ', '.join(map(str, server_ips))
|
300
|
-
dips_str = ', '.join(map(str, dips))
|
301
|
-
|
302
|
-
if not domain_ip_matches_server and model.mode in [DomainType.direct]:
|
303
|
-
raise ValidationError(
|
304
|
-
__("Domain IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode",
|
305
|
-
server_ip=server_ips_str, domain_ip=dips_str))
|
306
|
-
|
307
|
-
if domain_ip_matches_server and model.mode in [DomainType.cdn, DomainType.relay, DomainType.fake, DomainType.auto_cdn_ip]:
|
308
|
-
raise ValidationError(
|
309
|
-
__("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
|
310
|
-
server_ip=server_ips_str, domain_ip=dips_str))
|
311
|
-
|
312
|
-
return True
|
313
|
-
|
314
|
-
|
315
281
|
# def after_model_change(self,form, model, is_created):
|
316
282
|
# if model.show_domains.count==0:
|
317
283
|
# db.session.bulk_save_objects(ShowDomain(model.id,model.id))
|
@@ -72,7 +72,7 @@ def get_lang_form(empty=False):
|
|
72
72
|
default=hconfig(ConfigEnum.admin_lang))
|
73
73
|
# lang=wtf.SelectField(_("config.lang.label"),choices=[("en",_("lang.en")),("fa",_("lang.fa"))],description=_("config.lang.description"),default=hconfig(ConfigEnum.lang))
|
74
74
|
country = wtf.SelectField(
|
75
|
-
_("config.country.label"), choices=[("ir", _("Iran")), ("zh", _("China")), ("
|
75
|
+
_("config.country.label"), choices=[("ir", _("Iran")), ("zh", _("China")), ("other", "Others")],
|
76
76
|
description=_("config.country.description"),
|
77
77
|
default=hconfig(ConfigEnum.country))
|
78
78
|
lang_submit = wtf.SubmitField(_('Submit'))
|
@@ -251,11 +251,11 @@ def validate_domain(form, field):
|
|
251
251
|
if dip is None:
|
252
252
|
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
|
253
253
|
|
254
|
-
|
255
|
-
|
256
|
-
if dip not
|
254
|
+
myip = hutils.network.get_ip(4)
|
255
|
+
myip6 = hutils.network.get_ip(4)
|
256
|
+
if dip and myip != dip and (not myip6 or myip6 != dip):
|
257
257
|
raise ValidationError(_("Domain (%(domain)s)-> IP=%(domain_ip)s is not matched with your ip=%(server_ip)s which is required in direct mode",
|
258
|
-
server_ip=
|
258
|
+
server_ip=myip, domain_ip=dip, domain=domain))
|
259
259
|
|
260
260
|
|
261
261
|
def validate_domain_cdn(form, field):
|
@@ -266,10 +266,10 @@ def validate_domain_cdn(form, field):
|
|
266
266
|
if dip is None:
|
267
267
|
raise ValidationError(_("Domain can not be resolved! there is a problem in your domain"))
|
268
268
|
|
269
|
-
|
270
|
-
if
|
269
|
+
myip = hutils.network.get_ip(4)
|
270
|
+
if myip == dip:
|
271
271
|
raise ValidationError(_("In CDN mode, Domain IP=%(domain_ip)s should be different to your ip=%(server_ip)s",
|
272
|
-
server_ip=
|
272
|
+
server_ip=myip, domain_ip=dip, domain=domain))
|
273
273
|
|
274
274
|
|
275
275
|
def admin_link():
|
@@ -27,7 +27,7 @@ from hiddifypanel import hutils
|
|
27
27
|
class UserAdmin(AdminLTEModelView):
|
28
28
|
column_default_sort = ('id', False) # Sort by username in ascending order
|
29
29
|
|
30
|
-
column_sortable_list = ["is_active", "name", "current_usage", 'mode', "remaining_days",
|
30
|
+
column_sortable_list = ["is_active", "name", "current_usage", 'mode', "remaining_days","max_ips", "comment", 'last_online', "uuid", 'remaining_days']
|
31
31
|
column_searchable_list = ["uuid", "name"]
|
32
32
|
column_list = ["is_active", "name", "UserLinks", "current_usage", "remaining_days", "comment", 'last_online', 'mode', 'admin', "uuid"]
|
33
33
|
column_editable_list = ["comment", "name", "uuid"]
|
@@ -131,7 +131,7 @@ class UserAdmin(AdminLTEModelView):
|
|
131
131
|
if model.is_active:
|
132
132
|
link = '<i class="fa-solid fa-circle-check text-success"></i> '
|
133
133
|
elif len(model.devices):
|
134
|
-
link =
|
134
|
+
link = '<i class="fa-solid fa-users-slash text-danger" title="{_("Too many Connected IPs")}"></i>'
|
135
135
|
else:
|
136
136
|
link = '<i class="fa-solid fa-circle-xmark text-danger"></i> '
|
137
137
|
|
@@ -161,8 +161,8 @@ class UserAdmin(AdminLTEModelView):
|
|
161
161
|
|
162
162
|
def _usage_formatter(view, context, model, name):
|
163
163
|
u = round(model.current_usage_GB, 3)
|
164
|
-
t =
|
165
|
-
rate =
|
164
|
+
t = round(model.usage_limit_GB, 3)
|
165
|
+
rate = round(u * 100 / (t + 0.000001))
|
166
166
|
state = "danger" if u >= t else ('warning' if rate > 80 else 'success')
|
167
167
|
color = "#ff7e7e" if u >= t else ('#ffc107' if rate > 80 else '#9ee150')
|
168
168
|
return Markup(f"""
|
@@ -225,45 +225,38 @@ class UserAdmin(AdminLTEModelView):
|
|
225
225
|
|
226
226
|
def on_form_prefill(self, form, id=None):
|
227
227
|
# print("================",form._obj.start_date)
|
228
|
-
if form._obj is None:
|
229
|
-
return
|
230
|
-
|
231
|
-
if id is None or form._obj.start_date is None or form._obj.current_usage==0:
|
228
|
+
if id is None or form._obj is None or form._obj.start_date is None or form._obj.current_usage==0:
|
232
229
|
msg = _("Package not started yet.")
|
233
230
|
# form.reset['class']="d-none"
|
234
|
-
|
235
|
-
if hasattr(form, 'reset_days'):
|
231
|
+
if form._obj.start_date is None:
|
236
232
|
delattr(form, 'reset_days')
|
233
|
+
if form._obj.current_usage==0:
|
234
|
+
delattr(form, 'reset_usage')
|
235
|
+
# delattr(form,'disable_user')
|
237
236
|
else:
|
238
|
-
remaining = form._obj.remaining_days
|
237
|
+
remaining = form._obj.remaining_days # remaining_days(form._obj)
|
239
238
|
relative_remaining = hutils.convert.format_timedelta(datetime.timedelta(days=remaining))
|
240
239
|
msg = _("Remaining about %(relative)s, exactly %(days)s days", relative=relative_remaining, days=remaining)
|
241
240
|
form.reset_days.label.text += f" ({msg})"
|
241
|
+
usr_usage = f" ({_('user.home.usage.title')} {round(form._obj.current_usage_GB,3)}GB)"
|
242
|
+
form.reset_usage.label.text += usr_usage
|
243
|
+
form.reset_usage.data = False
|
242
244
|
form.reset_days.data = False
|
243
245
|
|
244
|
-
|
245
|
-
|
246
|
-
if
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
form.
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
if form._obj.start_date and hasattr(form, 'package_days'):
|
259
|
-
started = form._obj.start_date - datetime.date.today()
|
260
|
-
msg = _("Started from %(relative)s", relative=hutils.convert.format_timedelta(started))
|
261
|
-
form.package_days.label.text += f" ({msg})"
|
262
|
-
if started.days <= 0:
|
263
|
-
exact_start = _("Started %(days)s days ago", days=-started.days)
|
264
|
-
else:
|
265
|
-
exact_start = _("Will Start in %(days)s days", days=started.days)
|
266
|
-
form.package_days.description += f" ({exact_start})"
|
246
|
+
form.usage_limit.label.text += usr_usage
|
247
|
+
|
248
|
+
# if form._obj.mode==UserMode.disable:
|
249
|
+
# delattr(form,'disable_user')
|
250
|
+
# form.disable_user.data=form._obj.mode==UserMode.disable
|
251
|
+
if form._obj.start_date:
|
252
|
+
started = form._obj.start_date - datetime.date.today()
|
253
|
+
msg = _("Started from %(relative)s", relative=hutils.convert.format_timedelta(started))
|
254
|
+
form.package_days.label.text += f" ({msg})"
|
255
|
+
if started.days <= 0:
|
256
|
+
exact_start = _("Started %(days)s days ago", days=-started.days)
|
257
|
+
else:
|
258
|
+
exact_start = _("Will Start in %(days)s days", days=started.days)
|
259
|
+
form.package_days.description += f" ({exact_start})"
|
267
260
|
|
268
261
|
def get_edit_form(self):
|
269
262
|
form = super().get_edit_form()
|
@@ -274,44 +267,26 @@ class UserAdmin(AdminLTEModelView):
|
|
274
267
|
return form
|
275
268
|
|
276
269
|
def on_model_change(self, form, model, is_created):
|
277
|
-
|
278
|
-
try:
|
279
|
-
model.max_ips = max(3, min(int(model.max_ips or 10000), 10000))
|
280
|
-
except (ValueError, TypeError):
|
281
|
-
model.max_ips = 1000
|
282
|
-
|
283
|
-
# Show donation message
|
270
|
+
model.max_ips = max(3, model.max_ips or 10000)
|
284
271
|
if len(User.query.all()) % 4 == 0:
|
285
272
|
hutils.flask.flash(('<div id="show-modal-donation"></div>'), ' d-none')
|
286
|
-
|
287
|
-
# Validate UUID
|
288
273
|
if not re.match("^[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}$", model.uuid):
|
289
274
|
raise ValidationError('Invalid UUID e.g.,' + str(uuid.uuid4()))
|
290
|
-
|
291
|
-
# Handle reset flags
|
292
275
|
if hasattr(form, 'reset_usage') and form.reset_usage.data:
|
293
276
|
model.current_usage_GB = 0
|
294
|
-
|
277
|
+
# if model.telegram_id and model.telegram_id != '0' and not re.match(r"^[1-9]\d*$", model.telegram_id):
|
278
|
+
# raise ValidationError('Invalid Telegram ID')
|
279
|
+
# if form.disable_user.data:
|
280
|
+
# model.mode=UserMode.disable
|
295
281
|
if hasattr(form, 'reset_days') and form.reset_days.data:
|
296
282
|
model.start_date = None
|
297
|
-
|
298
|
-
# Validate package days
|
299
|
-
try:
|
300
|
-
model.package_days = min(int(model.package_days), 10000)
|
301
|
-
except (ValueError, TypeError):
|
302
|
-
model.package_days = 10000
|
303
|
-
|
304
|
-
# Handle user ownership
|
283
|
+
model.package_days = min(model.package_days, 10000)
|
305
284
|
old_user = User.by_id(model.id)
|
306
285
|
if not model.added_by or model.added_by == 1:
|
307
286
|
model.added_by = g.account.id
|
308
|
-
|
309
|
-
# Validate user limits
|
310
287
|
if not g.account.can_have_more_users():
|
311
288
|
raise ValidationError(_('You have too much users! You can have only %(active)s active users and %(total)s users',
|
312
289
|
active=g.account.max_active_users, total=g.account.max_users))
|
313
|
-
|
314
|
-
# Handle UUID changes
|
315
290
|
if old_user and old_user.uuid != model.uuid:
|
316
291
|
user_driver.remove_client(old_user)
|
317
292
|
|
@@ -192,12 +192,10 @@
|
|
192
192
|
}
|
193
193
|
|
194
194
|
function update_from_json(data) {
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
const stats = data['stats'];
|
200
|
-
|
195
|
+
usage_history = data['usage_history']
|
196
|
+
onlines = usage_history['m5']['online']
|
197
|
+
total_users = usage_history['total']['users']
|
198
|
+
stats = data['stats']
|
201
199
|
info_box("today", "fa-solid fa-calendar", "Today Usage",
|
202
200
|
((usage_history['today']['usage'] / Math.pow(1024, 3)).toFixed(1)) + " GB",
|
203
201
|
usage_history['today']['online'] / Math.max(1, total_users) * 100,
|
@@ -121,17 +121,9 @@
|
|
121
121
|
$(document).Toasts('create', {
|
122
122
|
class: 'bg-danger',
|
123
123
|
position: "{{'topRight' if get_locale()=='fa' else 'topLeft'}}",
|
124
|
-
title:
|
125
|
-
body:
|
126
|
-
|
127
|
-
delay: 5000
|
128
|
-
});
|
129
|
-
|
130
|
-
console.error('API Error:', {
|
131
|
-
status: status,
|
132
|
-
error: error,
|
133
|
-
details: jqx
|
134
|
-
});
|
124
|
+
title: status + " " + error,
|
125
|
+
body: JSON.stringify(jqx)
|
126
|
+
})
|
135
127
|
},
|
136
128
|
success: function (data) {
|
137
129
|
// dialog.modal('hide');
|
hiddifypanel/panel/cli.py
CHANGED
@@ -13,7 +13,6 @@ from hiddifypanel.panel import hiddify, usage
|
|
13
13
|
from hiddifypanel.database import db
|
14
14
|
from hiddifypanel.panel.init_db import init_db
|
15
15
|
|
16
|
-
from loguru import logger
|
17
16
|
|
18
17
|
def drop_db():
|
19
18
|
"""Cleans database"""
|
@@ -30,12 +29,7 @@ def downgrade():
|
|
30
29
|
os.rename("/opt/hiddify-manager/hiddify-panel/hiddifypanel.db.old", "/opt/hiddify-manager/hiddify-panel/hiddifypanel.db")
|
31
30
|
|
32
31
|
|
33
|
-
from celery import shared_task
|
34
32
|
def backup():
|
35
|
-
backup_task()
|
36
|
-
|
37
|
-
@shared_task(ignore_result=False)
|
38
|
-
def backup_task():
|
39
33
|
dbdict = hiddify.dump_db_to_dict()
|
40
34
|
os.makedirs('backup', exist_ok=True)
|
41
35
|
dst = f'backup/{datetime.datetime.now().strftime("%Y_%m_%d__%H_%M_%S")}.json'
|
@@ -48,12 +42,9 @@ def backup_task():
|
|
48
42
|
register_bot(True)
|
49
43
|
|
50
44
|
with open(dst, 'rb') as document:
|
51
|
-
for admin in AdminUser.query.filter(AdminUser.mode == AdminMode.super_admin, AdminUser.telegram_id is not None
|
45
|
+
for admin in AdminUser.query.filter(AdminUser.mode == AdminMode.super_admin, AdminUser.telegram_id is not None).all():
|
52
46
|
caption = ("Backup \n" + admin_links())
|
53
|
-
|
54
|
-
bot.send_document(admin.telegram_id, document, visible_file_name=dst.replace("backup/", ""), caption=caption[:1000])
|
55
|
-
except Exception as e:
|
56
|
-
logger.exception(e)
|
47
|
+
bot.send_document(admin.telegram_id, document, visible_file_name=dst.replace("backup/", ""), caption=caption[:min(len(caption), 1000)])
|
57
48
|
|
58
49
|
|
59
50
|
def all_configs():
|
@@ -2,7 +2,6 @@ import telebot
|
|
2
2
|
from flask import request
|
3
3
|
from apiflask import abort
|
4
4
|
from flask_restful import Resource
|
5
|
-
import time
|
6
5
|
|
7
6
|
from hiddifypanel.models import *
|
8
7
|
from hiddifypanel import Events
|
@@ -12,24 +11,7 @@ logger = telebot.logger
|
|
12
11
|
|
13
12
|
class ExceptionHandler(telebot.ExceptionHandler):
|
14
13
|
def handle(self, exception):
|
15
|
-
|
16
|
-
error_msg = str(exception)
|
17
|
-
logger.error(f"Telegram bot error: {error_msg}")
|
18
|
-
|
19
|
-
try:
|
20
|
-
# Attempt recovery based on error type
|
21
|
-
if "webhook" in error_msg.lower():
|
22
|
-
if hasattr(bot, 'remove_webhook'):
|
23
|
-
bot.remove_webhook()
|
24
|
-
logger.info("Removed webhook due to error")
|
25
|
-
elif "connection" in error_msg.lower():
|
26
|
-
# Wait and retry for connection issues
|
27
|
-
time.sleep(5)
|
28
|
-
return True # Indicates retry
|
29
|
-
except Exception as e:
|
30
|
-
logger.error(f"Error during recovery attempt: {str(e)}")
|
31
|
-
|
32
|
-
return False # Don't retry for unknown errors
|
14
|
+
logger.error(exception)
|
33
15
|
|
34
16
|
|
35
17
|
bot = telebot.TeleBot("1:2", parse_mode="HTML", threaded=False, exception_handler=ExceptionHandler())
|
@@ -1,5 +1,3 @@
|
|
1
|
-
import asyncio
|
2
|
-
import time
|
3
1
|
from flask import current_app as app, request
|
4
2
|
from flask import g
|
5
3
|
from flask.views import MethodView
|
@@ -18,9 +16,7 @@ class UpdateUserUsageApi(MethodView):
|
|
18
16
|
|
19
17
|
def get(self):
|
20
18
|
"""System: Update User Usage"""
|
21
|
-
|
22
|
-
|
23
|
-
return json.dumps(usage.update_local_usage_not_lock(), indent=2)
|
19
|
+
return json.dumps(usage.update_local_usage(), indent=2)
|
24
20
|
|
25
21
|
|
26
22
|
class AllConfigsApi(MethodView):
|
@@ -6,8 +6,7 @@ from hiddifypanel.auth import login_required
|
|
6
6
|
from hiddifypanel.models import *
|
7
7
|
from hiddifypanel.panel import hiddify
|
8
8
|
from hiddifypanel.drivers import user_driver
|
9
|
-
|
10
|
-
|
9
|
+
from hiddifypanel.panel import hiddify
|
11
10
|
|
12
11
|
from . import has_permission
|
13
12
|
from .schema import UserSchema, PostUserSchema, PatchUserSchema, SuccessfulSchema
|
hiddifypanel/panel/common.py
CHANGED
@@ -65,7 +65,7 @@ def init_app(app: APIFlask):
|
|
65
65
|
'version': hiddifypanel.__version__,
|
66
66
|
}), 500
|
67
67
|
|
68
|
-
trace = traceback.format_exc()
|
68
|
+
trace = traceback.format_exc(e)
|
69
69
|
|
70
70
|
# Create github issue link
|
71
71
|
issue_link = hutils.github_issue.generate_github_issue_link_for_500_error(e, trace)
|