diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml deleted file mode 100644 index 94e973d..0000000 --- a/.github/workflows/update.yml +++ /dev/null @@ -1,550 +0,0 @@ -name: Update Clash Config - -on: - schedule: - - cron: '*/30 * * * *' - workflow_dispatch: - -permissions: - contents: write - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install dependencies - run: pip install requests PyYAML - - - name: Run Updater - run: | - cat > updater.py << 'EOF' - import requests - import yaml - import socket - import concurrent.futures - from urllib.parse import urlparse, parse_qs - import base64 - import json - import re - import sys - from datetime import datetime, timezone - - SOURCE_URL = "https://git.darkmode.duckdns.org/sirmrak/Goida26-Clash/src/branch/main/servers.json" - OUTPUT_FILE = "clash.yaml" - TIMEOUT = 3 - MAX_WORKERS = 50 - - def parse_vmess(uri): - try: - b64_part = uri[8:].strip() - padding = 4 - (len(b64_part) % 4) - if padding != 4: - b64_part += '=' * padding - decoded = base64.b64decode(b64_part).decode('utf-8') - vmess_json = json.loads(decoded) - - proxy = { - "name": vmess_json.get("ps", "VMess"), - "type": "vmess", - "server": vmess_json.get("add", ""), - "port": int(vmess_json.get("port", 0)), - "uuid": vmess_json.get("id", ""), - "alterId": int(vmess_json.get("aid", 0)), - "cipher": vmess_json.get("scy", "auto"), - "network": vmess_json.get("net", "tcp"), - "udp": True - } - - net = vmess_json.get("net", "tcp") - if net == "ws": - proxy["ws-opts"] = { - "path": vmess_json.get("path", "/"), - "headers": {"Host": vmess_json.get("host", vmess_json.get("add", ""))} - } - elif net == "grpc": - proxy["grpc-opts"] = {"grpc-service-name": vmess_json.get("path", "")} - - if vmess_json.get("tls") == "tls": - proxy["tls"] = True - proxy["skip-cert-verify"] = True - proxy["sni"] = vmess_json.get("sni", vmess_json.get("add", "")) - - return proxy - except: - return None - - def parse_trojan(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Trojan" - - proxy = { - "name": name, - "type": "trojan", - "server": parsed.hostname, - "port": parsed.port or 443, - "password": parsed.username or "", - "udp": True, - "skip-cert-verify": True - } - - if parsed.query: - query = parse_qs(parsed.query) - if "sni" in query: - proxy["sni"] = query["sni"][0] - if "type" in query: - net_type = query["type"][0] - if net_type == "ws": - proxy["network"] = "ws" - proxy["ws-opts"] = { - "path": query.get("path", ["/"])[0], - "headers": {"Host": query.get("host", [parsed.hostname])[0]} - } - elif net_type == "grpc": - proxy["network"] = "grpc" - proxy["grpc-opts"] = {"grpc-service-name": query.get("serviceName", [""])[0]} - - return proxy - except: - return None - - def parse_shadowsocks(uri): - try: - if "#" in uri: - uri_part, name_part = uri.split("#", 1) - name = requests.utils.unquote(name_part) - else: - uri_part = uri - name = "Shadowsocks" - - uri_part = uri_part[5:] - - if '@' in uri_part: - userinfo, server_port = uri_part.split('@', 1) - try: - padding = 4 - (len(userinfo) % 4) - if padding != 4: - userinfo += '=' * padding - decoded = base64.b64decode(userinfo).decode('utf-8') - cipher, password = decoded.split(':', 1) - except: - if ':' in userinfo: - cipher, password = userinfo.split(':', 1) - else: - cipher, password = "aes-256-gcm", userinfo - else: - return None - - server, port = server_port.rsplit(':', 1) - - proxy = { - "name": name, - "type": "ss", - "server": server, - "port": int(port), - "cipher": cipher, - "password": password, - "udp": True - } - return proxy - except: - return None - - def parse_vless(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "VLESS" - - proxy = { - "name": name, - "type": "vless", - "server": parsed.hostname, - "port": parsed.port or 443, - "uuid": parsed.username or "", - "udp": True, - "skip-cert-verify": True, - "encryption": "none" - } - - if parsed.query: - query = parse_qs(parsed.query) - if "security" in query and query["security"][0] == "tls": - proxy["tls"] = True - if "sni" in query: - proxy["sni"] = query["sni"][0] - if "type" in query: - net_type = query["type"][0] - if net_type == "ws": - proxy["network"] = "ws" - proxy["ws-opts"] = { - "path": query.get("path", ["/"])[0], - "headers": {"Host": query.get("host", [parsed.hostname])[0]} - } - elif net_type == "grpc": - proxy["network"] = "grpc" - proxy["grpc-opts"] = {"grpc-service-name": query.get("serviceName", [""])[0]} - - return proxy - except: - return None - - def parse_socks5(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "SOCKS5" - - proxy = { - "name": name, - "type": "socks5", - "server": parsed.hostname, - "port": parsed.port or 1080, - "udp": True - } - - if parsed.username and parsed.password: - proxy["username"] = parsed.username - proxy["password"] = parsed.password - - return proxy - except: - return None - - def parse_http(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "HTTP" - - proxy = { - "name": name, - "type": "http", - "server": parsed.hostname, - "port": parsed.port or 8080, - } - - if parsed.username and parsed.password: - proxy["username"] = parsed.username - proxy["password"] = parsed.password - - if parsed.scheme == "https": - proxy["tls"] = True - proxy["skip-cert-verify"] = True - - return proxy - except: - return None - - def parse_hysteria(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Hysteria" - - proxy = { - "name": name, - "type": "hysteria", - "server": parsed.hostname, - "port": parsed.port or 443, - "up": 100, - "down": 100, - "skip-cert-verify": True - } - - if parsed.query: - query = parse_qs(parsed.query) - if "auth" in query: - proxy["auth-str"] = query["auth"][0] - if "peer" in query: - proxy["sni"] = query["peer"][0] - if "upmbps" in query: - proxy["up"] = int(query["upmbps"][0]) - if "downmbps" in query: - proxy["down"] = int(query["downmbps"][0]) - - return proxy - except: - return None - - def parse_hysteria2(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Hysteria2" - - password = parsed.username or "" - - proxy = { - "name": name, - "type": "hysteria2", - "server": parsed.hostname, - "port": parsed.port or 443, - "password": password, - "skip-cert-verify": True - } - - if parsed.query: - query = parse_qs(parsed.query) - if "auth" in query and not password: - proxy["password"] = query["auth"][0] - if "sni" in query: - proxy["sni"] = query["sni"][0] - - return proxy - except: - return None - - def parse_tuic(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "Tuic" - - uuid = "" - password = "" - if parsed.username and ':' in parsed.username: - uuid, password = parsed.username.split(':', 1) - else: - uuid = parsed.username or "" - - proxy = { - "name": name, - "type": "tuic", - "server": parsed.hostname, - "port": parsed.port or 443, - "uuid": uuid, - "password": password, - "udp": True, - "skip-cert-verify": True - } - - if parsed.query: - query = parse_qs(parsed.query) - if "sni" in query: - proxy["sni"] = query["sni"][0] - - return proxy - except: - return None - - def parse_wireguard(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "WireGuard" - - proxy = { - "name": name, - "type": "wireguard", - "server": parsed.hostname, - "port": parsed.port or 51820, - "private-key": parsed.username or "", - "udp": True - } - - if parsed.query: - query = parse_qs(parsed.query) - if "publicKey" in query: - proxy["public-key"] = query["publicKey"][0] - if "address" in query: - addresses = query["address"][0].split(',') - proxy["ip"] = addresses[0] if addresses else None - if "mtu" in query: - proxy["mtu"] = int(query["mtu"][0]) - - return proxy - except: - return None - - def parse_shadowtls(uri): - try: - parsed = urlparse(uri) - name = requests.utils.unquote(parsed.fragment) if parsed.fragment else "ShadowTLS" - - proxy = { - "name": name, - "type": "shadowtls", - "server": parsed.hostname, - "port": parsed.port or 443, - "version": 3, - "skip-cert-verify": True - } - - if parsed.query: - query = parse_qs(parsed.query) - if "password" in query: - proxy["password"] = query["password"][0] - if "sni" in query: - proxy["sni"] = query["sni"][0] - - return proxy - except: - return None - - def parse_proxies_from_text(text): - proxies = [] - lines = text.strip().split('\n') - - parsers = { - 'vmess://': parse_vmess, - 'trojan://': parse_trojan, - 'ss://': parse_shadowsocks, - 'vless://': parse_vless, - 'socks5://': parse_socks5, - 'socks://': parse_socks5, - 'http://': parse_http, - 'https://': parse_http, - 'hysteria://': parse_hysteria, - 'hysteria2://': parse_hysteria2, - 'hy2://': parse_hysteria2, - 'tuic://': parse_tuic, - 'wireguard://': parse_wireguard, - 'wg://': parse_wireguard, - 'shadowtls://': parse_shadowtls, - } - - for line in lines: - line = line.strip() - if not line or line.startswith('#'): - continue - - for prefix, parser in parsers.items(): - if line.startswith(prefix): - proxy = parser(line) - if proxy: - proxies.append(proxy) - break - - return proxies - - def fix_duplicate_names(proxies): - name_counts = {} - fixed_proxies = [] - - for proxy in proxies: - original_name = proxy["name"] - name = original_name - - if name in name_counts: - name_counts[name] += 1 - new_name = f"{name} ({name_counts[name]})" - while new_name in name_counts: - name_counts[name] += 1 - new_name = f"{name} ({name_counts[name]})" - proxy["name"] = new_name - name = new_name - else: - name_counts[name] = 1 - - fixed_proxies.append(proxy) - - return fixed_proxies - - def check_tcp_port(proxy): - try: - if proxy.get("type") == "wireguard": - return proxy - - server = proxy.get("server") - port = proxy.get("port") - if not server or not port: - return None - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(TIMEOUT) - result = sock.connect_ex((server, port)) - sock.close() - - if result == 0: - return proxy - else: - return None - except: - return None - - def filter_alive_proxies(proxies): - alive = [] - print(f"🔄 Проверяем {len(proxies)} серверов...") - - with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor: - futures = {executor.submit(check_tcp_port, p): p for p in proxies} - for future in concurrent.futures.as_completed(futures): - result = future.result() - if result: - alive.append(result) - - print(f"✅ Найдено живых: {len(alive)} из {len(proxies)}") - return alive - - def generate_clash_config(proxies, total_parsed, alive_count): - config = { - "proxies": proxies, - "proxy-groups": [ - { - "name": "🚀 Авто-выбор", - "type": "url-test", - "proxies": [p["name"] for p in proxies], - "url": "http://www.gstatic.com/generate_204", - "interval": 300 - }, - { - "name": "🌍 Прокси", - "type": "select", - "proxies": ["🚀 Авто-выбор"] + [p["name"] for p in proxies] - } - ], - "rules": [ - "MATCH,🌍 Прокси" - ] - } - return config - - def main(): - print("🚀 Загрузка списка прокси...") - try: - response = requests.get(SOURCE_URL, timeout=30) - response.raise_for_status() - content = response.text - except Exception as e: - print(f"❌ Ошибка загрузки TXT: {e}") - sys.exit(1) - - print("🔍 Парсинг всех форматов...") - all_proxies = parse_proxies_from_text(content) - total_parsed = len(all_proxies) - print(f"📦 Распознано прокси: {total_parsed}") - - print("🩺 Проверка доступности...") - alive_proxies = filter_alive_proxies(all_proxies) - alive_count = len(alive_proxies) - - print("🔧 Исправление дубликатов имён...") - fixed_proxies = fix_duplicate_names(alive_proxies) - - print("📝 Генерация YAML...") - config = generate_clash_config(fixed_proxies, total_parsed, alive_count) - - # Сохраняем с мета-комментарием в начале файла - now_utc = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC') - header = f"# Обновлено: {now_utc}\n# Всего в источнике: {total_parsed}\n# Живых серверов: {alive_count}\n# Источник: 26.txt\n\n" - - with open(OUTPUT_FILE, 'w', encoding='utf-8') as f: - f.write(header) - yaml.dump(config, f, allow_unicode=True, default_flow_style=False, sort_keys=False) - - print(f"✅ Готово! Сохранено в {OUTPUT_FILE}") - - if __name__ == "__main__": - main() - EOF - python updater.py - - - name: Commit and Push changes - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add clash.yaml - # Коммитим только если файл реально изменился - git diff --quiet && git diff --staged --quiet || (git commit -m "🔄 Auto-update config [skip ci]" && git push)