fix: share Happ generation and correct protocol labels
Move the reusable generator into commons, add Netherlands display metadata, and label Hysteria links by protocol without changing URI parameters. Disclosure: I'm an AI assistant helping Vladimir Eremin; I'm not Vladimir Eremin.
This commit is contained in:
1 parent
65fef9a6b0
commit
99ff1f1433
5 files changed
+248
No files matched your search
@@ -0,0 +1,7 @@
|
||||
FROM python:3.11-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY generator.py .
|
||||
|
||||
ENTRYPOINT ["python", "/app/generator.py"]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Happ subscriptions
|
||||
|
||||
Run the shared action from a node checkout:
|
||||
|
||||
```sh
|
||||
./commons/apex utils/generate-happ-subscriptions /private/subscriptions.json /private/output
|
||||
```
|
||||
|
||||
The action builds the bundled Docker image and generates one file per user. Keep
|
||||
input credentials and output subscription links outside Git. Regenerate into a
|
||||
private staging directory and verify the files before replacing served outputs.
|
||||
|
||||
The input contains `routing`, `templates` and `users`. Templates provide `scheme`,
|
||||
`host`, `port`, URI `params`, and display metadata (`country`, `nodes`). Each user's
|
||||
`configs` entries select a `template` and merge user-specific values such as `id`
|
||||
and `params`; `psub` selects the output filename. URI parameters are preserved.
|
||||
|
||||
Labels use the country, transport, node chain and user name. `NL` renders as
|
||||
`🇳🇱 Netherlands`. Schemes `hysteria2` and `hy2` display `[hysteria]`; other schemes
|
||||
use `params.type`, defaulting to `tcp`. This changes display text only.
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HAPP subscription generator.
|
||||
|
||||
Produces one .psub file per user. Each file contains:
|
||||
line 1 : happ://routing/onadd/<base64> (routing profile, unchanged behaviour)
|
||||
line N : a share link for every config the user has
|
||||
|
||||
Design
|
||||
------
|
||||
The config is written 1:1 against the HAPP / Xray share-link format. A template
|
||||
is just the literal pieces of a URI:
|
||||
|
||||
scheme -> vless:// , hy2:// , trojan:// , ss:// , socks:// , vmess://
|
||||
host -> the @host part
|
||||
port -> the :port part
|
||||
id -> the userinfo before @ (uuid / password / auth)
|
||||
params -> the ?a=b&c=d query, keys are the REAL link keys
|
||||
(type, security, sni, fp, alpn, flow, encryption,
|
||||
pbk, sid, spx, mode, path, host, serviceName,
|
||||
headerType, mux, obfs, ... whatever the link supports)
|
||||
|
||||
Nothing is translated or guessed. Whatever you put in `params` is what goes in
|
||||
the link, url-encoded. This keeps the generator dumb and the config honest.
|
||||
|
||||
Per-user `configs` entries deep-merge onto the named template, so a user only
|
||||
supplies the bits that differ (their `id`, their `params.sid`, etc.).
|
||||
|
||||
The `#remark` is built from template metadata (country / nodes) + the user name.
|
||||
"""
|
||||
|
||||
import json
|
||||
import base64
|
||||
import sys
|
||||
import os
|
||||
import copy
|
||||
from urllib.parse import urlencode, quote
|
||||
|
||||
# ISO 3166-1 alpha-2 -> (flag, display name). Extend as needed.
|
||||
COUNTRY_MAP = {
|
||||
"NL": ("\U0001F1F3\U0001F1F1", "Netherlands"),
|
||||
"LV": ("\U0001F1F1\U0001F1FB", "Latvia"),
|
||||
"RU": ("\U0001F1F7\U0001F1FA", "Russia"),
|
||||
"KZ": ("\U0001F1F0\U0001F1FF", "Kazakhstan"),
|
||||
"BY": ("\U0001F1E7\U0001F1FE", "Belarus"),
|
||||
}
|
||||
|
||||
|
||||
def deep_merge(base, override):
|
||||
"""Recursively merge `override` into a copy of `base`. Lists/scalars replace."""
|
||||
out = copy.deepcopy(base)
|
||||
for key, value in override.items():
|
||||
if isinstance(out.get(key), dict) and isinstance(value, dict):
|
||||
out[key] = deep_merge(out[key], value)
|
||||
else:
|
||||
out[key] = copy.deepcopy(value)
|
||||
return out
|
||||
|
||||
|
||||
def routing_link(routing):
|
||||
"""happ://routing/onadd/<base64(json)>"""
|
||||
payload = json.dumps(routing, separators=(",", ":")).encode()
|
||||
return "happ://routing/onadd/" + base64.b64encode(payload).decode()
|
||||
|
||||
|
||||
def build_remark(node, user_name):
|
||||
code = node.get("country", "")
|
||||
flag, name = COUNTRY_MAP.get(code, ("\U0001F310", code or "??"))
|
||||
transport = ("hysteria" if node.get("scheme") in ("hysteria2", "hy2")
|
||||
else node.get("params", {}).get("type", "tcp"))
|
||||
chain = " > ".join(node.get("nodes", ["unknown"]))
|
||||
return f"{flag} {name} [{transport}] | {chain} | {user_name}"
|
||||
|
||||
|
||||
def build_link(node, user_name):
|
||||
"""Assemble one share link from a fully-merged node dict."""
|
||||
scheme = node["scheme"]
|
||||
host = node["host"]
|
||||
port = node["port"]
|
||||
userinfo = node.get("id", "")
|
||||
|
||||
# Drop empty params so the query stays clean; preserve insertion order.
|
||||
params = {k: v for k, v in node.get("params", {}).items()
|
||||
if v is not None and v != ""}
|
||||
|
||||
# Lists (e.g. alpn) -> comma-joined; everything else stringified.
|
||||
flat = {}
|
||||
for k, v in params.items():
|
||||
flat[k] = ",".join(map(str, v)) if isinstance(v, list) else str(v)
|
||||
|
||||
query = urlencode(flat, safe="/:")
|
||||
remark = quote(build_remark(node, user_name))
|
||||
|
||||
base = f"{scheme}://{userinfo}@{host}:{port}"
|
||||
return f"{base}?{query}#{remark}" if query else f"{base}#{remark}"
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: generator.py <input_json> <output_dir>")
|
||||
sys.exit(1)
|
||||
|
||||
input_path, output_dir = sys.argv[1], sys.argv[2]
|
||||
|
||||
with open(input_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
routing = routing_link(data.get("routing", {}))
|
||||
templates = data.get("templates", {})
|
||||
users = data.get("users", [])
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
for user in users:
|
||||
name = user["name"]
|
||||
out_path = os.path.join(output_dir, user.get("psub", name))
|
||||
|
||||
lines = [routing]
|
||||
for entry in user.get("configs", []):
|
||||
tmpl = entry.get("template")
|
||||
node = deep_merge(templates.get(tmpl, {}), entry) if tmpl else entry
|
||||
lines.append(build_link(node, name))
|
||||
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
print(f"Generated: {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user