rinova-proxy-sdk 1.0.0

Proxy subscription to Clash config converter β€” SDK
Documentation
use crate::types::{ClashConfig, ProxyGroup, ProxyNode, RuleMode};

const BUILTIN_RULES: &[&str] = &[
    "DOMAIN-SUFFIX,ad.com,πŸ›‘ εΉΏε‘Šζ‹¦ζˆͺ",
    "DOMAIN-SUFFIX,doubleclick.net,πŸ›‘ εΉΏε‘Šζ‹¦ζˆͺ",
    "DOMAIN-SUFFIX,google.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,youtube.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,github.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,twitter.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,x.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,telegram.org,🌍 国倖网站",
    "DOMAIN-SUFFIX,steampowered.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,openai.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,anthropic.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,claude.ai,🌍 国倖网站",
    "DOMAIN-SUFFIX,cursor.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,cloudflare.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,netflix.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,spotify.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,disneyplus.com,🌍 国倖网站",
    "DOMAIN-SUFFIX,cn,πŸ‡¨πŸ‡³ 国内网站",
    "DOMAIN-SUFFIX,baidu.com,πŸ‡¨πŸ‡³ 国内网站",
    "DOMAIN-SUFFIX,qq.com,πŸ‡¨πŸ‡³ 国内网站",
    "DOMAIN-SUFFIX,weixin.com,πŸ‡¨πŸ‡³ 国内网站",
    "DOMAIN-SUFFIX,alipay.com,πŸ‡¨πŸ‡³ 国内网站",
    "GEOIP,CN,🎯 η›΄θΏž",
    "MATCH,🌍 国倖网站",
];

const EXT_RULES: &[&str] = &[
    "RULE-SET,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/BanEasyAD.list,πŸ›‘ εΉΏε‘Šζ‹¦ζˆͺ",
    "RULE-SET,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ProxyMedia.list,🌍 国倖网站",
    "RULE-SET,https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/Clash/ChinaDomain.list,πŸ‡¨πŸ‡³ 国内网站",
    "GEOIP,CN,🎯 η›΄θΏž",
    "MATCH,🌍 国倖网站",
];

pub fn build_config(nodes: &[ProxyNode], rule_mode: RuleMode) -> ClashConfig {
    let names: Vec<String> = nodes.iter().map(|n| n.name.clone()).collect();
    let mut foreign_proxies = vec![
        "πŸš€ θŠ‚η‚Ήι€‰ζ‹©".to_string(),
        "♻️ θ‡ͺεŠ¨ι€‰ζ‹©".to_string(),
        "🎯 η›΄θΏž".to_string(),
        "DIRECT".to_string(),
    ];
    foreign_proxies.extend(names.clone());

    let rules = match rule_mode {
        RuleMode::External => EXT_RULES.iter().map(|s| (*s).to_string()).collect(),
        RuleMode::Builtin => BUILTIN_RULES.iter().map(|s| (*s).to_string()).collect(),
    };

    ClashConfig {
        port: 7890,
        socks_port: 7891,
        allow_lan: true,
        mode: "Rule".to_string(),
        log_level: "info".to_string(),
        external_controller: "127.0.0.1:9090".to_string(),
        proxies: nodes.to_vec(),
        proxy_groups: vec![
            ProxyGroup {
                name: "πŸš€ θŠ‚η‚Ήι€‰ζ‹©".to_string(),
                group_type: "select".to_string(),
                proxies: {
                    let mut p = vec![
                        "♻️ θ‡ͺεŠ¨ι€‰ζ‹©".to_string(),
                        "🎯 η›΄θΏž".to_string(),
                        "DIRECT".to_string(),
                    ];
                    p.extend(names.clone());
                    p
                },
                url: None,
                interval: None,
            },
            ProxyGroup {
                name: "♻️ θ‡ͺεŠ¨ι€‰ζ‹©".to_string(),
                group_type: "url-test".to_string(),
                proxies: names.clone(),
                url: Some("http://www.gstatic.com/generate_204".to_string()),
                interval: Some(300),
            },
            ProxyGroup {
                name: "🎯 η›΄θΏž".to_string(),
                group_type: "select".to_string(),
                proxies: {
                    let mut p = vec!["DIRECT".to_string()];
                    p.extend(names.clone());
                    p
                },
                url: None,
                interval: None,
            },
            ProxyGroup {
                name: "πŸ›‘ εΉΏε‘Šζ‹¦ζˆͺ".to_string(),
                group_type: "select".to_string(),
                proxies: {
                    let mut p = vec!["REJECT".to_string(), "DIRECT".to_string()];
                    p.extend(names.clone());
                    p
                },
                url: None,
                interval: None,
            },
            ProxyGroup {
                name: "🌍 国倖网站".to_string(),
                group_type: "select".to_string(),
                proxies: foreign_proxies,
                url: None,
                interval: None,
            },
            ProxyGroup {
                name: "πŸ‡¨πŸ‡³ 国内网站".to_string(),
                group_type: "select".to_string(),
                proxies: {
                    let mut p = vec!["🎯 η›΄θΏž".to_string()];
                    p.extend(names);
                    p
                },
                url: None,
                interval: None,
            },
        ],
        rules,
    }
}

pub fn to_yaml(config: &ClashConfig) -> crate::Result<String> {
    Ok(serde_yaml::to_string(config)?)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::ProxyType;

    fn sample_nodes() -> Vec<ProxyNode> {
        vec![
            ProxyNode::new("θŠ‚η‚Ή-A".into(), ProxyType::Ss, "1.2.3.4".into(), 443),
            ProxyNode::new("θŠ‚η‚Ή-B".into(), ProxyType::Ss, "5.6.7.8".into(), 443),
        ]
    }

    #[test]
    fn foreign_group_follows_select() {
        let config = build_config(&sample_nodes(), RuleMode::Builtin);
        let foreign = config
            .proxy_groups
            .iter()
            .find(|g| g.name == "🌍 国倖网站")
            .unwrap();
        assert_eq!(foreign.proxies[0], "πŸš€ θŠ‚η‚Ήι€‰ζ‹©");
    }

    #[test]
    fn select_group_contains_all_nodes() {
        let config = build_config(&sample_nodes(), RuleMode::Builtin);
        let select = config
            .proxy_groups
            .iter()
            .find(|g| g.name == "πŸš€ θŠ‚η‚Ήι€‰ζ‹©")
            .unwrap();
        assert!(select.proxies.contains(&"θŠ‚η‚Ή-A".to_string()));
        assert!(select.proxies.contains(&"θŠ‚η‚Ή-B".to_string()));
    }

    #[test]
    fn external_rules_use_rule_set() {
        let config = build_config(&sample_nodes(), RuleMode::External);
        assert!(config.rules[0].starts_with("RULE-SET"));
    }
}