Skip to content
This repository was archived by the owner on Jul 8, 2026. It is now read-only.

Commit a8314ee

Browse files
committed
Feat: Refactor subscription page templates
1 parent ed1ce55 commit a8314ee

5 files changed

Lines changed: 438 additions & 524 deletions

File tree

core/scripts/normalsub/normalsub.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from aiohttp import web
1414
from aiohttp.web_middlewares import middleware
15-
from urllib.parse import unquote, parse_qs, urlparse, urljoin
15+
from urllib.parse import unquote, parse_qs, urlparse, urljoin, quote
1616
from dotenv import load_dotenv
1717
import qrcode
1818
from jinja2 import Environment, FileSystemLoader
@@ -127,9 +127,14 @@ class TemplateContext:
127127
expiration_date: str
128128
sublink_qrcode: str
129129
sub_link: str
130+
sub_link_encoded: str
130131
blocked: bool = False
131132
local_uris: List[NodeURI] = field(default_factory=list)
132133
node_uris: List[NodeURI] = field(default_factory=list)
134+
singbox_qrcode: Optional[str] = None
135+
hiddify_qrcode: Optional[str] = None
136+
streisand_qrcode: Optional[str] = None
137+
nekobox_qrcode: Optional[str] = None
133138

134139

135140
class Utils:
@@ -398,14 +403,14 @@ def get_normal_subscription(self, username: str, user_agent: str) -> str:
398403
f"total={user_info.max_download_bytes}; "
399404
f"expire={user_info.expiration_timestamp}\n"
400405
)
401-
profile_lines = f"//profile-title: {username}-Hysteria2 🚀\n//profile-update-interval: 1\n"
406+
profile_lines = f"//profile-title: {username}-Blitz ⚡\n//profile-update-interval: 1\n"
402407
return profile_lines + subscription_info + "\n".join(all_processed_uris)
403408

404409

405410
class TemplateRenderer:
406411
def __init__(self, template_dir: str, config: AppConfig):
407412
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
408-
self.html_template = self.env.get_template('template.html')
413+
self.html_template = self.env.get_template('index.html')
409414
self.config = config
410415

411416
def render(self, context: TemplateContext) -> str:
@@ -430,6 +435,8 @@ def __init__(self):
430435
safe_subpath = self.validate_and_escape_subpath(self.config.subpath)
431436

432437
base_path = f'/{safe_subpath}'
438+
self.app.router.add_get(f'{base_path}/sub/normal/style.css', self.handle_style)
439+
self.app.router.add_get(f'{base_path}/sub/normal/script.js', self.handle_script)
433440
self.app.router.add_get(f'{base_path}/sub/normal/{{password_token}}', self.handle)
434441
self.app.router.add_get(f'{base_path}/robots.txt', self.robots_handler)
435442
self.app.router.add_route('*', f'{base_path}/{{tail:.*}}', self.handle_404_subpath)
@@ -452,7 +459,7 @@ def _load_config(self) -> AppConfig:
452459
extra_config_path = '/etc/hysteria/extra.json'
453460
rate_limit = 100
454461
rate_limit_window = 60
455-
template_dir = os.path.dirname(__file__)
462+
template_dir = os.path.join(os.path.dirname(__file__), 'template')
456463

457464
sni = self._load_sni_from_env(sni_file)
458465
return AppConfig(domain=domain, external_port=external_port,
@@ -565,6 +572,7 @@ def _get_blocked_template_context(self, fake_uri: str, user_info: UserInfo) -> T
565572
expiration_date=user_info.expiration_date,
566573
sublink_qrcode="",
567574
sub_link="#blocked",
575+
sub_link_encoded="",
568576
blocked=True,
569577
local_uris=[
570578
NodeURI(
@@ -603,8 +611,14 @@ async def _get_template_context(self, username: str, user_info: UserInfo) -> Tem
603611
print(f"Warning: Constructed base URL '{base_url}' might be invalid. Check domain and port config.")
604612

605613
sub_link = f"{base_url}/{self.config.subpath}/sub/normal/{user_info.password}"
614+
sub_link_encoded = quote(sub_link, safe='')
606615
sublink_qrcode = Utils.generate_qrcode_base64(sub_link)
607616

617+
singbox_qrcode = Utils.generate_qrcode_base64(f"sing-box://import-remote-profile?url={sub_link_encoded}")
618+
hiddify_qrcode = Utils.generate_qrcode_base64(f"hiddify://import/{sub_link_encoded}")
619+
streisand_qrcode = Utils.generate_qrcode_base64(f"streisand://import/sub?url={sub_link_encoded}")
620+
nekobox_qrcode = Utils.generate_qrcode_base64(f"nekobox://import?url={sub_link_encoded}")
621+
608622
local_uris = []
609623
node_uris = []
610624

@@ -626,9 +640,14 @@ async def _get_template_context(self, username: str, user_info: UserInfo) -> Tem
626640
expiration_date=user_info.expiration_date,
627641
sublink_qrcode=sublink_qrcode,
628642
sub_link=sub_link,
643+
sub_link_encoded=sub_link_encoded,
629644
blocked=user_info.blocked,
630645
local_uris=local_uris,
631-
node_uris=node_uris
646+
node_uris=node_uris,
647+
singbox_qrcode=singbox_qrcode,
648+
hiddify_qrcode=hiddify_qrcode,
649+
streisand_qrcode=streisand_qrcode,
650+
nekobox_qrcode=nekobox_qrcode
632651
)
633652

634653
async def robots_handler(self, request: web.Request) -> web.Response:
@@ -637,6 +656,12 @@ async def robots_handler(self, request: web.Request) -> web.Response:
637656
async def handle_404_subpath(self, request: web.Request) -> web.Response:
638657
print(f"404 Not Found (within subpath, unhandled by specific routes): {request.path}")
639658
return web.Response(status=404, text="Not Found within Subpath")
659+
660+
async def handle_style(self, request: web.Request) -> web.Response:
661+
return web.FileResponse(os.path.join(self.config.template_dir, 'style.css'))
662+
663+
async def handle_script(self, request: web.Request) -> web.Response:
664+
return web.FileResponse(os.path.join(self.config.template_dir, 'script.js'))
640665

641666
def run(self):
642667
print(f"Starting Hysteria Normalsub server on {self.config.aiohttp_listen_address}:{self.config.aiohttp_listen_port}")

0 commit comments

Comments
 (0)