@ -1,3 +1,3 @@
|
|||||||
🚀 feat: Improve Hysteria2 IP detection with fallbacks
|
🚀 feat: Add version check and notification new releases
|
||||||
|
🛠️ refactor: Improve utils package structure with cleaner imports
|
||||||
⚙️ Enhance: SNI entry handling in CONFIG_ENV file
|
🐛 fix: IPv6 validation pattern
|
||||||
|
|||||||
25
core/cli.py
25
core/cli.py
@ -485,6 +485,31 @@ def get_services_status():
|
|||||||
click.echo('Error: Services status not available.')
|
click.echo('Error: Services status not available.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
click.echo(f'{e}', err=True)
|
click.echo(f'{e}', err=True)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command('show-version')
|
||||||
|
def show_version():
|
||||||
|
"""Displays the currently installed version of the panel."""
|
||||||
|
try:
|
||||||
|
if version_info := cli_api.show_version():
|
||||||
|
click.echo(version_info)
|
||||||
|
else:
|
||||||
|
click.echo("Error retrieving version")
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command('check-version')
|
||||||
|
def check_version():
|
||||||
|
"""Checks if the current version is up-to-date and displays changelog if not."""
|
||||||
|
try:
|
||||||
|
if version_info := cli_api.check_version():
|
||||||
|
click.echo(version_info)
|
||||||
|
else:
|
||||||
|
click.echo("Error retrieving version")
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"An unexpected error occurred: {e}", err=True)
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,7 @@ class Command(Enum):
|
|||||||
CONFIGURE_WARP = os.path.join(SCRIPT_DIR, 'warp', 'configure.sh')
|
CONFIGURE_WARP = os.path.join(SCRIPT_DIR, 'warp', 'configure.sh')
|
||||||
STATUS_WARP = os.path.join(SCRIPT_DIR, 'warp', 'status.sh')
|
STATUS_WARP = os.path.join(SCRIPT_DIR, 'warp', 'status.sh')
|
||||||
SERVICES_STATUS = os.path.join(SCRIPT_DIR, 'services_status.sh')
|
SERVICES_STATUS = os.path.join(SCRIPT_DIR, 'services_status.sh')
|
||||||
|
VERSION = os.path.join(SCRIPT_DIR, 'hysteria2', 'version.py')
|
||||||
|
|
||||||
# region Custom Exceptions
|
# region Custom Exceptions
|
||||||
|
|
||||||
@ -500,5 +501,15 @@ def get_services_status() -> dict[str, bool] | None:
|
|||||||
'''Gets the status of all project services.'''
|
'''Gets the status of all project services.'''
|
||||||
if res := run_cmd(['bash', Command.SERVICES_STATUS.value]):
|
if res := run_cmd(['bash', Command.SERVICES_STATUS.value]):
|
||||||
return json.loads(res)
|
return json.loads(res)
|
||||||
|
|
||||||
|
def show_version() -> str | None:
|
||||||
|
"""Displays the currently installed version of the panel."""
|
||||||
|
return run_cmd(['python3', Command.VERSION.value, 'show-version'])
|
||||||
|
|
||||||
|
|
||||||
|
def check_version() -> str | None:
|
||||||
|
"""Checks if the current version is up-to-date and displays changelog if not."""
|
||||||
|
return run_cmd(['python3', Command.VERSION.value, 'check-version'])
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
75
core/scripts/hysteria2/version.py
Normal file
75
core/scripts/hysteria2/version.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
LOCALVERSION = "/etc/hysteria/VERSION"
|
||||||
|
LATESTVERSION = "https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/VERSION"
|
||||||
|
LASTESTCHANGE = "https://raw.githubusercontent.com/ReturnFI/Hysteria2/main/changelog"
|
||||||
|
|
||||||
|
def version_greater_equal(version1, version2):
|
||||||
|
version1_parts = [int(part) for part in version1.strip().split('.')]
|
||||||
|
version2_parts = [int(part) for part in version2.strip().split('.')]
|
||||||
|
|
||||||
|
max_length = max(len(version1_parts), len(version2_parts))
|
||||||
|
version1_parts.extend([0] * (max_length - len(version1_parts)))
|
||||||
|
version2_parts.extend([0] * (max_length - len(version2_parts)))
|
||||||
|
|
||||||
|
for i in range(max_length):
|
||||||
|
if version1_parts[i] > version2_parts[i]:
|
||||||
|
return True
|
||||||
|
elif version1_parts[i] < version2_parts[i]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# If we get here, they're equal
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_version():
|
||||||
|
try:
|
||||||
|
with open(LOCALVERSION, 'r') as f:
|
||||||
|
local_version = f.read().strip()
|
||||||
|
|
||||||
|
latest_version = requests.get(LATESTVERSION).text.strip()
|
||||||
|
latest_changelog = requests.get(LASTESTCHANGE).text
|
||||||
|
|
||||||
|
print(f"Panel Version: {local_version}")
|
||||||
|
|
||||||
|
if not version_greater_equal(local_version, latest_version):
|
||||||
|
print(f"Latest Version: {latest_version}")
|
||||||
|
print(f"{latest_version} Version Change Log:")
|
||||||
|
print(latest_changelog)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking version: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def show_version():
|
||||||
|
try:
|
||||||
|
with open(LOCALVERSION, 'r') as f:
|
||||||
|
local_version = f.read().strip()
|
||||||
|
|
||||||
|
print(f"Panel Version: {local_version}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error showing version: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print(f"Usage: {sys.argv[0]} [check-version|show-version]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
command = sys.argv[1]
|
||||||
|
|
||||||
|
if command == "check-version":
|
||||||
|
check_version()
|
||||||
|
elif command == "show-version":
|
||||||
|
show_version()
|
||||||
|
else:
|
||||||
|
print(f"Usage: {sys.argv[0]} [check-version|show-version]", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,13 +1,5 @@
|
|||||||
from telebot import types
|
from telebot import types
|
||||||
from utils.common import create_main_markup
|
from utils import *
|
||||||
from utils.adduser import *
|
|
||||||
from utils.backup import *
|
|
||||||
from utils.command import *
|
|
||||||
from utils.deleteuser import *
|
|
||||||
from utils.edituser import *
|
|
||||||
from utils.search import *
|
|
||||||
from utils.serverinfo import *
|
|
||||||
from utils.cpu import *
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@ -27,4 +19,6 @@ def monitoring_thread():
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
monitor_thread = threading.Thread(target=monitoring_thread, daemon=True)
|
monitor_thread = threading.Thread(target=monitoring_thread, daemon=True)
|
||||||
monitor_thread.start()
|
monitor_thread.start()
|
||||||
|
version_thread = threading.Thread(target=version_monitoring, daemon=True)
|
||||||
|
version_thread.start()
|
||||||
bot.polling(none_stop=True)
|
bot.polling(none_stop=True)
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
from .common import *
|
||||||
|
from .adduser import *
|
||||||
|
from .backup import *
|
||||||
|
from .command import *
|
||||||
|
from .deleteuser import *
|
||||||
|
from .edituser import *
|
||||||
|
from .search import *
|
||||||
|
from .serverinfo import *
|
||||||
|
from .cpu import *
|
||||||
|
from .check_version import *
|
||||||
|
|||||||
34
core/scripts/telegrambot/utils/check_version.py
Normal file
34
core/scripts/telegrambot/utils/check_version.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import telebot
|
||||||
|
import subprocess
|
||||||
|
import shlex
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from utils.command import *
|
||||||
|
|
||||||
|
def check_version():
|
||||||
|
command = f"python3 {CLI_PATH} check-version"
|
||||||
|
try:
|
||||||
|
args = shlex.split(command)
|
||||||
|
result = subprocess.check_output(args, stderr=subprocess.STDOUT).decode("utf-8").strip()
|
||||||
|
panel_version = re.search(r'Panel Version: (\d+\.\d+\.\d+)', result)
|
||||||
|
latest_version = re.search(r'Latest Version: (\d+\.\d+\.\d+)', result)
|
||||||
|
|
||||||
|
if panel_version and latest_version and panel_version.group(1) != latest_version.group(1):
|
||||||
|
notify_admins(f"🔔 New version available!\n\n{result}")
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
error_message = f"Error checking version: {e.output.decode('utf-8')}"
|
||||||
|
print(f"Error checking version: {e.output.decode('utf-8')}")
|
||||||
|
notify_admins(error_message)
|
||||||
|
|
||||||
|
def notify_admins(message):
|
||||||
|
for admin_id in ADMIN_USER_IDS:
|
||||||
|
try:
|
||||||
|
bot.send_message(admin_id, message)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to notify admin {admin_id}: {str(e)}")
|
||||||
|
|
||||||
|
def version_monitoring():
|
||||||
|
while True:
|
||||||
|
check_version()
|
||||||
|
time.sleep(86400)
|
||||||
@ -23,3 +23,13 @@ class ServerServicesStatusResponse(BaseModel):
|
|||||||
hysteria_normal_sub: bool
|
hysteria_normal_sub: bool
|
||||||
hysteria_telegram_bot: bool
|
hysteria_telegram_bot: bool
|
||||||
hysteria_warp: bool
|
hysteria_warp: bool
|
||||||
|
|
||||||
|
class VersionInfoResponse(BaseModel):
|
||||||
|
current_version: str
|
||||||
|
|
||||||
|
|
||||||
|
class VersionCheckResponse(BaseModel):
|
||||||
|
is_latest: bool
|
||||||
|
current_version: str
|
||||||
|
latest_version: str
|
||||||
|
changelog: str
|
||||||
@ -1,6 +1,6 @@
|
|||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
import cli_api
|
import cli_api
|
||||||
from .schema.server import ServerStatusResponse, ServerServicesStatusResponse
|
from .schema.server import ServerStatusResponse, ServerServicesStatusResponse, VersionCheckResponse, VersionInfoResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -144,3 +144,40 @@ def __parse_services_status(services_status: dict[str, bool]) -> ServerServicesS
|
|||||||
elif 'wg-quick' in service:
|
elif 'wg-quick' in service:
|
||||||
parsed_services_status['hysteria_warp'] = status
|
parsed_services_status['hysteria_warp'] = status
|
||||||
return ServerServicesStatusResponse(**parsed_services_status)
|
return ServerServicesStatusResponse(**parsed_services_status)
|
||||||
|
|
||||||
|
@router.get('/version', response_model=VersionInfoResponse)
|
||||||
|
async def get_version_info():
|
||||||
|
"""Retrieves the current version of the panel."""
|
||||||
|
try:
|
||||||
|
version_output = cli_api.show_version()
|
||||||
|
if version_output:
|
||||||
|
current_version = version_output.split(": ")[1].strip()
|
||||||
|
return VersionInfoResponse(current_version=current_version)
|
||||||
|
raise HTTPException(status_code=404, detail="Version information not found")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/version/check', response_model=VersionCheckResponse)
|
||||||
|
async def check_version_info():
|
||||||
|
"""Checks for updates and retrieves version information."""
|
||||||
|
try:
|
||||||
|
check_output = cli_api.check_version()
|
||||||
|
if check_output:
|
||||||
|
lines = check_output.splitlines()
|
||||||
|
current_version = lines[0].split(": ")[1].strip()
|
||||||
|
|
||||||
|
if len(lines) > 1 and "Latest Version" in lines[1]:
|
||||||
|
latest_version = lines[1].split(": ")[1].strip()
|
||||||
|
is_latest = current_version == latest_version
|
||||||
|
changelog_start_index = 3
|
||||||
|
changelog = "\n".join(lines[changelog_start_index:]).strip()
|
||||||
|
return VersionCheckResponse(is_latest=is_latest, current_version=current_version,
|
||||||
|
latest_version=latest_version, changelog=changelog)
|
||||||
|
else:
|
||||||
|
return VersionCheckResponse(is_latest=True, current_version=current_version)
|
||||||
|
|
||||||
|
raise HTTPException(status_code=404, detail="Version information not found")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@ -15,6 +15,8 @@
|
|||||||
<!-- Theme style -->
|
<!-- Theme style -->
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.2.0/css/adminlte.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.2.0/css/adminlte.min.css">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/icheck-bootstrap@3.0.1/icheck-bootstrap.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/icheck-bootstrap@3.0.1/icheck-bootstrap.min.css">
|
||||||
|
<!-- SweetAlert2 -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
|
||||||
{% block stylesheets %}{% endblock %}
|
{% block stylesheets %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@ -114,11 +116,28 @@
|
|||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="main-footer">
|
<footer class="main-footer">
|
||||||
<strong>Copyright © 2023 <a href="https://github.com/ReturnFI/Hysteria2">Return-Hysteria2</a>.</strong>
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
</footer>
|
<div>
|
||||||
|
<a href="https://github.com/ReturnFI/Hysteria2" target="_blank" class="text-decoration-none">
|
||||||
|
<span class="badge badge-secondary" style="font-size: 0.9rem;">
|
||||||
|
<i class="fab fa-github"></i> GitHub
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/hysteria2_panel" target="_blank" class="text-decoration-none ml-2 mr-2">
|
||||||
|
<span class="badge bg-primary" style="font-size: 0.9rem;">
|
||||||
|
<i class="fab fa-telegram"></i> Telegram
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ReturnFI/Hysteria2/releases" target="_blank" class="text-decoration-none ml-2 mr-2">
|
||||||
|
<span class="badge badge-success" style="font-size: 0.9rem;">
|
||||||
|
<i class="fas fa-code-branch"></i> <span id="panel-version"></span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<!-- REQUIRED SCRIPTS -->
|
<!-- REQUIRED SCRIPTS -->
|
||||||
<!-- jQuery -->
|
<!-- jQuery -->
|
||||||
@ -127,6 +146,8 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<!-- AdminLTE App -->
|
<!-- AdminLTE App -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.2.0/js/adminlte.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.2.0/js/adminlte.min.js"></script>
|
||||||
|
<!-- SweetAlert2 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
@ -158,10 +179,48 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateIcon(enabled) {
|
function updateIcon(enabled) {
|
||||||
darkModeIcon
|
darkModeIcon.removeClass("fa-moon fa-sun")
|
||||||
.removeClass("fa-moon fa-sun")
|
|
||||||
.addClass(enabled ? "fa-sun" : "fa-moon");
|
.addClass(enabled ? "fa-sun" : "fa-moon");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const versionUrl = "{{ url_for('get_version_info') }}";
|
||||||
|
$.ajax({
|
||||||
|
url: versionUrl,
|
||||||
|
type: 'GET',
|
||||||
|
success: function (response) {
|
||||||
|
$('#panel-version').text(`Version: ${response.current_version}`);
|
||||||
|
},
|
||||||
|
error: function (error) {
|
||||||
|
console.error("Error fetching version:", error);
|
||||||
|
$('#panel-version').text('Version: Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkVersionUrl = "{{ url_for('check_version_info') }}";
|
||||||
|
$.ajax({
|
||||||
|
url: checkVersionUrl,
|
||||||
|
type: 'GET',
|
||||||
|
success: function (response) {
|
||||||
|
if (!response.is_latest) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Update Available!',
|
||||||
|
html: `A new version of the panel is available: <b>${response.latest_version}</b><br><br>${response.changelog.replace(/\n/g, '<br>')}`,
|
||||||
|
icon: 'info',
|
||||||
|
showCancelButton: false,
|
||||||
|
confirmButtonText: 'OK'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error("Error checking for updates:", error);
|
||||||
|
if (xhr.responseJSON && xhr.responseJSON.detail) {
|
||||||
|
console.error("Server detail:", xhr.responseJSON.detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
$('.main-footer').addClass('d-flex justify-content-between');
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% block javascripts %}{% endblock %}
|
{% block javascripts %}{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user