diff --git a/.gitignore b/.gitignore
index 358b61c..3043593 100644
--- a/.gitignore
+++ b/.gitignore
@@ -160,4 +160,5 @@ cython_debug/
#.idea/
hysteria2_venv/
-.vscode/
+# .vscode/
+# !.vscode/settings.json
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..b19fff9
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,172 @@
+{
+ "workbench.colorTheme": "Default Dark+",
+ "workbench.colorCustomizations": {
+ "editor.background": "#252b3d",
+ "editor.foreground": "#dde1eb",
+ "activityBar.background": "#2f3648",
+ "activityBar.foreground": "#FF6B35",
+ "activityBar.activeBorder": "#FF6B35",
+ "activityBar.inactiveForeground": "#a8afc0",
+ "sideBar.background": "#2f3648",
+ "sideBar.foreground": "#dde1eb",
+ "sideBar.border": "#3a4158",
+ "sideBarTitle.foreground": "#FFA500",
+ "statusBar.background": "#2f3648",
+ "statusBar.foreground": "#58C4DC",
+ "statusBar.noFolderBackground": "#2f3648",
+ "statusBar.debuggingBackground": "#FF6347",
+ "statusBar.debuggingForeground": "#ffffff",
+ "titleBar.activeBackground": "#252b3d",
+ "titleBar.activeForeground": "#dde1eb",
+ "titleBar.inactiveBackground": "#252b3d",
+ "titleBar.inactiveForeground": "#a8afc0",
+ "titleBar.border": "#3a4158",
+ "editorGroupHeader.tabsBackground": "#252b3d",
+ "editorGroupHeader.tabsBorder": "#3a4158",
+ "tab.activeBackground": "#2f3648",
+ "tab.activeForeground": "#FFA500",
+ "tab.inactiveBackground": "#252b3d",
+ "tab.inactiveForeground": "#a8afc0",
+ "tab.border": "#3a4158",
+ "tab.activeBorder": "#FF6B35",
+ "tab.activeBorderTop": "#FF6B35",
+ "editorLineNumber.foreground": "#8891a4",
+ "editorLineNumber.activeForeground": "#58C4DC",
+ "editorCursor.foreground": "#FFA500",
+ "editor.selectionBackground": "#4a6589",
+ "editor.selectionHighlightBackground": "#4a658960",
+ "editor.lineHighlightBackground": "#2f364860",
+ "editorBracketMatch.background": "#4a658960",
+ "editorBracketMatch.border": "#FFA500",
+ "editorWidget.background": "#2f3648",
+ "editorSuggestWidget.background": "#2f3648",
+ "editorSuggestWidget.selectedBackground": "#4a6589",
+ "editorSuggestWidget.highlightForeground": "#FFA500",
+ "input.background": "#252b3d",
+ "input.border": "#3a4158",
+ "input.foreground": "#dde1eb",
+ "inputOption.activeBorder": "#58C4DC",
+ "dropdown.background": "#2f3648",
+ "dropdown.border": "#3a4158",
+ "button.background": "#3FB950",
+ "button.foreground": "#ffffff",
+ "button.hoverBackground": "#4ec55e",
+ "list.activeSelectionBackground": "#4a6589",
+ "list.activeSelectionForeground": "#ffffff",
+ "list.inactiveSelectionBackground": "#4a658960",
+ "list.hoverBackground": "#4a658950",
+ "list.focusBackground": "#4a6589",
+ "terminal.foreground": "#dde1eb",
+ "terminal.ansiGreen": "#3FB950",
+ "terminal.ansiYellow": "#D29922",
+ "terminal.ansiBlue": "#58A6FF",
+ "terminal.ansiMagenta": "#BC8CFF",
+ "terminal.ansiCyan": "#39C5CF",
+ "terminal.ansiRed": "#FF6347",
+ "gitDecoration.modifiedResourceForeground": "#D29922",
+ "gitDecoration.addedResourceForeground": "#3FB950",
+ "gitDecoration.deletedResourceForeground": "#FF6347",
+ "gitDecoration.untrackedResourceForeground": "#39C5CF",
+ "badge.background": "#FF6B35",
+ "badge.foreground": "#ffffff",
+ "notifications.background": "#2f3648",
+ "notifications.foreground": "#dde1eb",
+ "notifications.border": "#3a4158",
+ "notificationLink.foreground": "#58A6FF",
+ "panel.background": "#252b3d",
+ "panel.border": "#3a4158",
+ "panelTitle.activeForeground": "#FFA500",
+ "panelTitle.inactiveForeground": "#a8afc0",
+ "panelTitle.activeBorder": "#FF6B35"
+ },
+ "editor.tokenColorCustomizations": {
+ "textMateRules": [
+ {
+ "scope": "comment",
+ "settings": {
+ "foreground": "#8891a4",
+ "fontStyle": "italic"
+ }
+ },
+ {
+ "scope": ["keyword", "storage.type", "storage.modifier"],
+ "settings": {
+ "foreground": "#FF6347"
+ }
+ },
+ {
+ "scope": ["string", "string.quoted"],
+ "settings": {
+ "foreground": "#3FB950"
+ }
+ },
+ {
+ "scope": ["constant.numeric", "constant.language"],
+ "settings": {
+ "foreground": "#BC8CFF"
+ }
+ },
+ {
+ "scope": ["variable", "support.variable"],
+ "settings": {
+ "foreground": "#58A6FF"
+ }
+ },
+ {
+ "scope": ["entity.name.function", "support.function"],
+ "settings": {
+ "foreground": "#D29922"
+ }
+ },
+ {
+ "scope": ["entity.name.type", "entity.name.class", "support.class"],
+ "settings": {
+ "foreground": "#39C5CF"
+ }
+ },
+ {
+ "scope": ["entity.name.tag"],
+ "settings": {
+ "foreground": "#3FB950"
+ }
+ },
+ {
+ "scope": ["entity.other.attribute-name"],
+ "settings": {
+ "foreground": "#39C5CF"
+ }
+ },
+ {
+ "scope": ["support.type.property-name"],
+ "settings": {
+ "foreground": "#58A6FF"
+ }
+ },
+ {
+ "scope": ["meta.import", "meta.export"],
+ "settings": {
+ "foreground": "#BC8CFF"
+ }
+ },
+ {
+ "scope": ["punctuation.definition.tag"],
+ "settings": {
+ "foreground": "#a8afc0"
+ }
+ }
+ ]
+ },
+ "editor.fontFamily": "'JetBrains Mono', 'Fira Code', 'Cascadia Code', Consolas, 'Courier New', monospace",
+ "editor.fontSize": 13.5,
+ "editor.lineHeight": 1.6,
+ "editor.fontLigatures": true,
+ "editor.fontWeight": "400",
+ "editor.letterSpacing": 0.5,
+ "editor.cursorBlinking": "expand",
+ "editor.cursorSmoothCaretAnimation": "on",
+ "editor.smoothScrolling": true,
+ "workbench.list.smoothScrolling": true,
+ "editor.minimap.enabled": false,
+ "editor.bracketPairColorization.enabled": true,
+ "editor.guides.bracketPairs": "active"
+}
\ No newline at end of file
diff --git a/VERSION b/VERSION
index fad066f..4fd0fe3 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.5.0
\ No newline at end of file
+2.5.1
\ No newline at end of file
diff --git a/changelog b/changelog
index 5440a8a..14d1760 100644
--- a/changelog
+++ b/changelog
@@ -1,79 +1,36 @@
-# π¦ ** Release 2.5.0 β ACL & WARP fixes, safer password generation, service controls π**
+# π ** release(2.5.1): Telegram UX improvements, web panel access & outbound enhancements π **
-*Released: 2025-12-19*
+*Released: 2026-01-02*
## β¨ New Features
-### π§ **Core & Service Management**
+* π€ **Telegram Bot**: Added settings menu
+* π **Web Panel URL**:
-* π Added **Hysteria2 core version detection** and validation
-* π Display **core version** directly in the web panel
-* π Added **Restart Hysteria2** button in the web UI with proper fetch handling
-* π New API endpoint to **restart Hysteria2 service** programmatically
+ * Added service status check before retrieving web panel URL
+ * New handlers to retrieve and display web panel URL
+* π₯οΈ **CLI**: Added option to display *only* the web panel URL
+* π **Outbounds**: Introduced new `select` outbound type(SingBox)
+* π₯ **Users**: Added option to display up to **1000 users** in selection dropdown
+* β³ **Expiration**: Added validation for expiration days and improved UI
-### π **Geo & ACL Rule Management**
+### π Fixes
-* πΊοΈ Enhanced **Geo file update process** with intelligent ACL rule handling
-* π§© Added **geo rule detection & auto-update** for ACL configurations
-* π§Ή Removed redundant `geoip` / `geosite` rejection rules
-* π« Removed speedtest-related rejection from ACL rules
+* π‘οΈ Prevent enabling **Masquerade** when **OBFS** is active
+* βοΈ Conditionally generate **sing-box OBFS** configuration(SingBox)
-### π‘οΈ **Security & Password Handling**
+### β»οΈ Refactors & Improvements
-* π Replaced `pwgen` with **Python `secrets` module** for secure password generation
-* π Improved password generation for:
+* π§ Introduced settings submenu structure in Telegram bot
+* π Removed username from URI fragments
+* πΎ Streamlined upgrade backup process and removed geo data download
- * add-user
- * bulk users
- * internal password utilities
-* π§ͺ Removed implicit password auto-generation where not required
+### π§ Dependencies
-### π§° **CLI Improvements**
+* β¬οΈ **FastAPI** bumped to `0.128.0`
+* β¬οΈ **psutil** bumped to `7.2.1`
+* β¬οΈ **pillow** bumped to `12.1.0`
-* π€ Added `run_cmd_and_stream` for **real-time command output streaming**
-* βοΈ Improved error messages during **Hysteria2 installation**
-* π Updated install scripts to return meaningful output messages
-* π Replaced `pwgen` with **openssl** for:
-
- * obfs password generation
- * normalsub path generation
-
-### π **Network & IP Detection**
-
-* π Added **ip.sb fallback** for public IP detection to improve reliability
-
----
-
-## π Fixes & Improvements
-
-### π€ **User & Username Handling**
-
-* β
Allowed **underscores (`_`) in usernames**
-* π§Ό Improved validation regex and error handling
-* π§ Updated `remove-user` command syntax
-* π§ Improved user handler robustness
-
-### π **Masquerade & OBFS**
-
-* π« Prevented **OBFS usage when masquerade is enabled** to avoid conflicts
-
-### π§Ή **Scheduler & System Stability**
-
-* π Suppressed noisy `systemctl` output during scheduler setup
-* π§― Improved rule-checking logic for WARP configuration status
-
-### π§Ό **Uninstall & Cleanup**
-
-* β»οΈ Improved WARP uninstall flow:
-
- * Better config loading & error handling
- * Automatic **ACL rule reset** after uninstall
-
----
-
-## π¦ Dependency Updates
-
-* β¬οΈ **FastAPI** β 0.124.4
-* β¬οΈ **Certbot** β 5.2.2
-* β¬οΈ **python-multipart** β 0.0.21
+### β€οΈ Credits
+Special thanks to **@MiliAxe** and **@SeyedHashtag** for their valuable contributions π
diff --git a/core/cli.py b/core/cli.py
index 8518039..5c39552 100644
--- a/core/cli.py
+++ b/core/cli.py
@@ -670,10 +670,14 @@ def stop_webpanel_decoy():
click.echo(f'{e}', err=True)
@cli.command('get-webpanel-url')
-def get_web_panel_url():
+@click.option('--url-only', is_flag=True, help='Only display the URL without additional text')
+def get_web_panel_url(url_only: bool):
try:
url = cli_api.get_webpanel_url()
- click.echo(f'Hysteria web panel is now running. The service is accessible on: {url}')
+ if url_only:
+ click.echo(url)
+ else:
+ click.echo(f'Hysteria web panel is now running. The service is accessible on: {url}')
except Exception as e:
click.echo(f'{e}', err=True)
diff --git a/core/scripts/hysteria2/show_user_uri.py b/core/scripts/hysteria2/show_user_uri.py
index 5981369..f361bf6 100644
--- a/core/scripts/hysteria2/show_user_uri.py
+++ b/core/scripts/hysteria2/show_user_uri.py
@@ -160,13 +160,13 @@ def show_uri(args: argparse.Namespace) -> None:
if args.all or args.ip_version == 4:
if ip4 and ip4 != "None":
uri = generate_uri(args.username, auth_password, ip4, local_port,
- local_obfs_password, local_sha256, local_sni, 4, local_insecure, f"{args.username}-IPv4")
+ local_obfs_password, local_sha256, local_sni, 4, local_insecure, "IPv4")
display_uri_and_qr(uri, "IPv4", args, terminal_width)
if args.all or args.ip_version == 6:
if ip6 and ip6 != "None":
uri = generate_uri(args.username, auth_password, ip6, local_port,
- local_obfs_password, local_sha256, local_sni, 6, local_insecure, f"{args.username}-IPv6")
+ local_obfs_password, local_sha256, local_sni, 6, local_insecure, "IPv6")
display_uri_and_qr(uri, "IPv6", args, terminal_width)
for node in nodes:
@@ -194,14 +194,14 @@ def show_uri(args: argparse.Namespace) -> None:
sni=node_sni,
ip_version=ip_v,
insecure=node_insecure,
- fragment_tag=f"{args.username}-{node_name}"
+ fragment_tag=node_name
)
display_uri_and_qr(uri, f"Node: {node_name} (IPv{ip_v})", args, terminal_width)
if args.singbox and is_service_active("hysteria-singbox.service"):
domain, port = get_singbox_domain_and_port()
if domain and port:
- print(f"\nSingbox Sublink:\nhttps://{domain}:{port}/sub/singbox/{args.username}/{args.ip_version}#{args.username}\n")
+ print(f"\nSingbox Sublink:\nhttps://{domain}:{port}/sub/singbox/{args.username}/{args.ip_version}#Hysteria2\n")
if args.normalsub and is_service_active("hysteria-normal-sub.service"):
domain, port, subpath = get_normalsub_domain_and_port()
diff --git a/core/scripts/hysteria2/wrapper_uri.py b/core/scripts/hysteria2/wrapper_uri.py
index d0246d3..0c5c65a 100644
--- a/core/scripts/hysteria2/wrapper_uri.py
+++ b/core/scripts/hysteria2/wrapper_uri.py
@@ -87,9 +87,9 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]:
user_output = {"username": username, "ipv4": None, "ipv6": None, "nodes": [], "normal_sub": None}
if ip4 and ip4 != "None":
- user_output["ipv4"] = generate_uri(username, auth_password, ip4, default_port, base_uri_params, 4, f"{username}-IPv4")
+ user_output["ipv4"] = generate_uri(username, auth_password, ip4, default_port, base_uri_params, 4, "IPv4")
if ip6 and ip6 != "None":
- user_output["ipv6"] = generate_uri(username, auth_password, ip6, default_port, base_uri_params, 6, f"{username}-IPv6")
+ user_output["ipv6"] = generate_uri(username, auth_password, ip6, default_port, base_uri_params, 6, "IPv6")
for node in nodes:
node_name = node.get("name")
@@ -98,7 +98,7 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]:
continue
ip_v = 6 if ':' in node_ip else 4
- tag = f"{username}-{node_name}"
+ tag = node_name
node_port = str(node.get("port", default_port))
node_sni = node.get("sni", default_sni)
@@ -117,7 +117,7 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]:
user_output["nodes"].append({"name": node_name, "uri": uri})
if ns_domain and ns_port and ns_subpath:
- user_output["normal_sub"] = f"https://{ns_domain}:{ns_port}/{ns_subpath}/{auth_password}#{username}"
+ user_output["normal_sub"] = f"https://{ns_domain}:{ns_port}/{ns_subpath}/{auth_password}#Hysteria2"
results.append(user_output)
diff --git a/core/scripts/normalsub/normalsub.py b/core/scripts/normalsub/normalsub.py
index af4906b..aa963e5 100644
--- a/core/scripts/normalsub/normalsub.py
+++ b/core/scripts/normalsub/normalsub.py
@@ -308,15 +308,11 @@ class SingboxConfigGenerator:
print(f"Error during Singbox config generation from URI: {e}, URI: {uri}")
return None
- return {
+ outbound_config = {
"type": "hysteria2",
"tag": unquote(parsed_url.fragment),
"server": server,
"server_port": server_port,
- "obfs": {
- "type": "salamander",
- "password": obfs_password
- },
"password": final_password,
"tls": {
"enabled": True,
@@ -325,6 +321,14 @@ class SingboxConfigGenerator:
}
}
+ if obfs_password:
+ outbound_config["obfs"] = {
+ "type": "salamander",
+ "password": obfs_password
+ }
+
+ return outbound_config
+
def combine_configs(self, all_uris: List[str], username: str, fragment: str) -> Optional[Dict[str, Any]]:
if not all_uris:
return None
diff --git a/core/scripts/normalsub/singbox.json b/core/scripts/normalsub/singbox.json
index cbcebd1..da9789f 100644
--- a/core/scripts/normalsub/singbox.json
+++ b/core/scripts/normalsub/singbox.json
@@ -70,11 +70,19 @@
{
"outbounds": [
"auto",
- "direct"
+ "direct",
+ "select"
],
"tag": "proxy",
"type": "selector"
},
+ {
+ "type": "selector",
+ "tag": "select",
+
+ "outbounds": [],
+ "interrupt_exist_connections": false
+ },
{
"interval": "10m",
"outbounds": [],
diff --git a/core/scripts/telegrambot/utils/__init__.py b/core/scripts/telegrambot/utils/__init__.py
index 49f6c6c..4ec52d0 100644
--- a/core/scripts/telegrambot/utils/__init__.py
+++ b/core/scripts/telegrambot/utils/__init__.py
@@ -8,3 +8,5 @@ from .search import *
from .serverinfo import *
from .cpu import *
from .check_version import *
+from .weburl import *
+from .settings import *
\ No newline at end of file
diff --git a/core/scripts/telegrambot/utils/common.py b/core/scripts/telegrambot/utils/common.py
index edf9c3c..963b885 100644
--- a/core/scripts/telegrambot/utils/common.py
+++ b/core/scripts/telegrambot/utils/common.py
@@ -4,5 +4,11 @@ def create_main_markup():
markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
markup.row('β Add User', 'π Show User')
markup.row('ποΈ Delete User', 'π₯οΈ Server Info')
- markup.row('πΎ Backup Server')
+ markup.row('πΎ Backup Server', 'βοΈ Settings')
return markup
+
+def create_settings_markup():
+ markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
+ markup.row('π Get Webpanel URL')
+ markup.row('β¬
οΈ Back')
+ return markup
\ No newline at end of file
diff --git a/core/scripts/telegrambot/utils/settings.py b/core/scripts/telegrambot/utils/settings.py
new file mode 100644
index 0000000..0976044
--- /dev/null
+++ b/core/scripts/telegrambot/utils/settings.py
@@ -0,0 +1,10 @@
+from utils.command import *
+from utils.common import create_main_markup, create_settings_markup
+
+@bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'βοΈ Settings')
+def settings_menu_handler(message):
+ bot.send_message(message.chat.id, "βοΈ Settings Menu:", reply_markup=create_settings_markup())
+
+@bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'β¬
οΈ Back')
+def back_to_main_menu_handler(message):
+ bot.send_message(message.chat.id, "β¬
οΈ Returning to Main Menu...", reply_markup=create_main_markup())
\ No newline at end of file
diff --git a/core/scripts/telegrambot/utils/weburl.py b/core/scripts/telegrambot/utils/weburl.py
new file mode 100644
index 0000000..33df1bb
--- /dev/null
+++ b/core/scripts/telegrambot/utils/weburl.py
@@ -0,0 +1,15 @@
+from utils.command import *
+
+@bot.message_handler(func=lambda message: is_admin(message.from_user.id) and message.text == 'π Get Webpanel URL')
+def get_webpanel_url_handler(message):
+ status_command = f"python3 {CLI_PATH} get-webpanel-services-status"
+ status_result = run_cli_command(status_command)
+
+ if "hysteria-webpanel.service: Inactive" in status_result:
+ bot.reply_to(message, "β οΈ The Webpanel service is currently inactive.")
+ return
+
+ command = f"python3 {CLI_PATH} get-webpanel-url --url-only"
+ result = run_cli_command(command)
+ bot.send_chat_action(message.chat.id, 'typing')
+ bot.reply_to(message, "π Webpanel URL:\n" + result)
\ No newline at end of file
diff --git a/core/scripts/webpanel/templates/users.html b/core/scripts/webpanel/templates/users.html
index c0c3da6..4699e19 100644
--- a/core/scripts/webpanel/templates/users.html
+++ b/core/scripts/webpanel/templates/users.html
@@ -161,6 +161,7 @@
+
diff --git a/requirements.txt b/requirements.txt
index 8433a81..184a85a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,7 +6,7 @@ aiofiles==25.1.0
click==8.3.1
# Utilities
-psutil==7.1.3
+psutil==7.2.1
python-dotenv==1.2.1
schedule==1.2.2
requests==2.32.5
@@ -16,7 +16,7 @@ typing_extensions==4.15.0
pyTelegramBotAPI==4.29.1
qrcode==8.2
pypng==0.20220715.0
-pillow==12.0.0
+pillow==12.1.0
# Cache / misc
propcache==0.4.1
@@ -28,7 +28,7 @@ pymongo==4.15.5
hysteria2-api==0.1.3
# Web panel (FastAPI stack)
-fastapi==0.124.4
+fastapi==0.128.0
Jinja2==3.1.6
python-multipart==0.0.21
hypercorn==0.18.0