Skip to content

Commit

Permalink
ci: generate sing-box ruleset
Browse files Browse the repository at this point in the history
  • Loading branch information
wouiSB committed Jul 16, 2024
1 parent 8af34cb commit 38cd6f3
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 0 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/sing-box-rulesets.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Generate and Update sing-box rulesets

on:
push:

permissions:
contents: write

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Set up sing-box
run: |
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc
sudo chmod a+r /etc/apt/keyrings/sagernet.asc
echo "deb [arch=`dpkg --print-architecture` signed-by=/etc/apt/keyrings/sagernet.asc] https://deb.sagernet.org/ * *" | sudo tee /etc/apt/sources.list.d/sagernet.list > /dev/null
sudo apt-get update
sudo apt-get install sing-box
- name: Check out code
uses: actions/checkout@v4

- name: Generate source and compile rulesets
run: python3 update_sing_box.py

- name: Update sing-box branch
run: |
mv sing-box /tmp/
git config --global user.name "GitHub Actions"
git config --global user.email "[email protected]"
git checkout --orphan sing-box
git reset --hard
mv /tmp/sing-box/* ./
git add -A
git commit -m "Update sing-box rulesets"
git push -f origin sing-box
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ build/

### VS Studio ###
.vs/

__pycache__
112 changes: 112 additions & 0 deletions update_sing_box.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env python3

from dataclasses import dataclass
import os
import typing
import json

class EnhancedJSONEncoder(json.JSONEncoder):
def default(self, o):
if hasattr(o, 'dict'):
return o.dict()
return json.JSONEncoder.default(self, o)


@dataclass
class HeadlessRule:
# query_type: list of int or str
query_type: typing.List[typing.Union[int, str]] = None
network: list[str] = None
domain: list[str] = None
domain_suffix: list[str] = None
domain_keyword: list[str] = None
domain_regex: list[str] = None
source_ip_cidr: list[str] = None
ip_cidr: list[str] = None
source_port: list[int] = None
source_port_range: list[str] = None
port: list[int] = None
port_range: list[str] = None
process_name: list[str] = None
process_path: list[str] = None
package_name: list[str] = None
wifi_ssid: list[str] = None
wifi_bssid: list[str] = None
invert: bool = None

def append(self, key: str, value: str):
if getattr(self, key) is None:
setattr(self, key, [])
getattr(self, key).append(value)

def dict(self):
return {k: v for k, v in self.__dict__.items() if v is not None}

@dataclass
class Rule:
version: int = 1
rules: list[HeadlessRule] = None

def dict(self):
return {k: v for k, v in self.__dict__.items() if v is not None}

def json(self):
return json.dumps(self.dict(), indent=2, cls=EnhancedJSONEncoder)

IGNORE = (
("Clash", "config"),
("Clash", "Providers")
)
IGNORE = [ os.path.join(*_ignore) for _ignore in IGNORE ]

for root, dirs, files in os.walk("Clash"):
dirs[:] = [d for d in dirs if os.path.join(root, d) not in IGNORE]
for file in files:
if file.endswith(".list"):
print(os.path.join(root, file))
path_l = os.path.join(root, file).split(os.sep)

headless_rule = HeadlessRule()
with open(os.path.join(root, file), encoding="utf-8") as f_in:
lines = f_in.readlines()
write_path = os.path.join("sing-box", *path_l[1:])
write_path = os.path.splitext(write_path)[0] + ".json"
# if dir not exists, create it (recursively)
if not os.path.exists(os.path.dirname(write_path)):
os.makedirs(os.path.dirname(write_path))
for line in lines:
line = line.strip()
if not line:
continue
if line.startswith("#"):
continue
if line.split(",")[0] == "DOMAIN":
headless_rule.append("domain", line.split(",")[1])
elif line.split(",")[0] == "DOMAIN-SUFFIX":
headless_rule.append("domain_suffix", line.split(",")[1])
elif line.split(",")[0] == "DOMAIN-KEYWORD":
headless_rule.append("domain_keyword", line.split(",")[1])
elif line.split(",")[0] == "DOMAIN-CIDR":
headless_rule.append("ip_cidr", line.split(",")[1])
elif line.split(",")[0] == "PROCESS-NAME":
headless_rule.append("process_name", line.split(",")[1])

rule = Rule(rules=[headless_rule])
# write source
with open(write_path, "w", encoding="utf-8") as f_out:
f_out.write(rule.json())
# compile `sing-box rule-set compile [--output <file-name>.srs] <file-name>.json` and delete json
os.system(f"sing-box rule-set compile {write_path}")
os.remove(write_path)

# remove those not exist outside `Providers`
for root, dirs, files in os.walk("Clash"):
for file in files:
if file.endswith(".yaml"):
path_l = os.path.join(root, file).split(os.sep)
# eg: `Clash/Providers/Ruleset/58.yaml` -> `Clash/Ruleset/58.list`
read_path = os.path.join(*path_l[:1], *path_l[2:])
read_path = os.path.splitext(read_path)[0] + ".list"
if not os.path.exists(read_path):
print(f"remove: {os.path.join(root, file)}")
os.remove(os.path.join(root, file))

0 comments on commit 38cd6f3

Please sign in to comment.