From d894fea89e2a36df6cf085d1ede5206bc9d99257 Mon Sep 17 00:00:00 2001 From: nitezs Date: Tue, 12 Sep 2023 18:40:24 +0800 Subject: [PATCH] init --- .gitignore | 4 + README.md | 37 ++ api/controller/clash.go | 35 ++ api/controller/default.go | 51 ++ api/controller/meta.go | 36 ++ api/route.go | 19 + config/config.go | 16 + go.mod | 32 ++ go.sum | 84 +++ main.go | 83 +++ model/contryCode.go | 1034 +++++++++++++++++++++++++++++++++++++ model/proxy.go | 83 +++ model/sub.go | 32 ++ parser/base64.go | 13 + parser/ss.go | 67 +++ parser/ssr.go | 47 ++ parser/trojan.go | 53 ++ parser/vless.go | 75 +++ parser/vmess.go | 68 +++ utils/get.go | 23 + utils/proxy.go | 105 ++++ utils/rule.go | 54 ++ utils/sub.go | 85 +++ utils/template.go | 36 ++ validator/sub.go | 7 + 25 files changed, 2179 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 api/controller/clash.go create mode 100644 api/controller/default.go create mode 100644 api/controller/meta.go create mode 100644 api/route.go create mode 100644 config/config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 model/contryCode.go create mode 100644 model/proxy.go create mode 100644 model/sub.go create mode 100644 parser/base64.go create mode 100644 parser/ss.go create mode 100644 parser/ssr.go create mode 100644 parser/trojan.go create mode 100644 parser/vless.go create mode 100644 parser/vmess.go create mode 100644 utils/get.go create mode 100644 utils/proxy.go create mode 100644 utils/rule.go create mode 100644 utils/sub.go create mode 100644 utils/template.go create mode 100644 validator/sub.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..614f10a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +dist +subs +templates \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..99df446 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# sub2clash + +将订阅链接转换为 Clash、Clash.Meta 配置 + +## 特性 + +- 开箱即用的规则、策略组配置 +- 自动根据节点名称按国家划分策略组 +- 支持协议 + - [x] Shadowsocks + - [x] ShadowsocksR + - [x] Vmess + - [x] Vless + - [x] Trojan + - [ ] Hysteria + - [ ] TUIC + - [ ] WireGuard + +## API + +### /clash + +获取 Clash 配置链接 + +| Query 参数 | 类型 | 说明 | +| ---------- | ------ | --------------------------------- | +| sub | string | 订阅链接 | +| refresh | bool | 强制获取新配置(默认缓存 5 分钟) | + +### /meta + +获取 Meta 配置链接 + +| Query 参数 | 类型 | 说明 | +| ---------- | ------ | --------------------------------- | +| sub | string | 订阅链接 | +| refresh | bool | 强制获取新配置(默认缓存 5 分钟) | diff --git a/api/controller/clash.go b/api/controller/clash.go new file mode 100644 index 0000000..f474d57 --- /dev/null +++ b/api/controller/clash.go @@ -0,0 +1,35 @@ +package controller + +import ( + "net/http" + "net/url" + "sub/config" + "sub/validator" + + "github.com/gin-gonic/gin" + "gopkg.in/yaml.v3" +) + +func SubmodHandler(c *gin.Context) { + // 从请求中获取参数 + var query validator.SubQuery + if err := c.ShouldBind(&query); err != nil { + c.String(http.StatusBadRequest, "参数错误: "+err.Error()) + return + } + query.Sub, _ = url.QueryUnescape(query.Sub) + // 混合订阅和模板节点 + sub, err := MixinSubTemp(query, config.Default.ClashTemplate) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + // 添加自定义节点、规则 + // 输出 + bytes, err := yaml.Marshal(sub) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + c.String(http.StatusOK, string(bytes)) +} diff --git a/api/controller/default.go b/api/controller/default.go new file mode 100644 index 0000000..f9f9e89 --- /dev/null +++ b/api/controller/default.go @@ -0,0 +1,51 @@ +package controller + +import ( + "errors" + "gopkg.in/yaml.v3" + "strings" + "sub/model" + "sub/parser" + "sub/utils" + "sub/validator" +) + +func MixinSubTemp(query validator.SubQuery, template string) (*model.Subscription, error) { + // 定义变量 + var temp *model.Subscription + var sub *model.Subscription + // 加载模板 + template, err := utils.LoadTemplate(template) + if err != nil { + return nil, errors.New("加载模板失败: " + err.Error()) + } + // 解析模板 + err = yaml.Unmarshal([]byte(template), &temp) + if err != nil { + return nil, errors.New("解析模板失败: " + err.Error()) + } + // 加载订阅 + data, err := utils.LoadSubscription( + query.Sub, + query.Refresh, + ) + if err != nil { + return nil, errors.New("加载订阅失败: " + err.Error()) + } + // 解析订阅 + var proxyList []model.Proxy + err = yaml.Unmarshal(data, &sub) + if err != nil { + // 如果无法直接解析,尝试Base64解码 + base64, err := parser.DecodeBase64(string(data)) + if err != nil { + return nil, errors.New("加载订阅失败: " + err.Error()) + } + proxyList = utils.ParseProxy(strings.Split(base64, "\n")...) + } else { + proxyList = sub.Proxies + } + // 添加节点 + utils.AddProxy(temp, proxyList...) + return temp, nil +} diff --git a/api/controller/meta.go b/api/controller/meta.go new file mode 100644 index 0000000..f39abc2 --- /dev/null +++ b/api/controller/meta.go @@ -0,0 +1,36 @@ +package controller + +import ( + _ "embed" + "net/http" + "net/url" + "sub/config" + "sub/validator" + + "github.com/gin-gonic/gin" + "gopkg.in/yaml.v3" +) + +func SubHandler(c *gin.Context) { + // 从请求中获取参数 + var query validator.SubQuery + if err := c.ShouldBind(&query); err != nil { + c.String(http.StatusBadRequest, "参数错误: "+err.Error()) + return + } + query.Sub, _ = url.QueryUnescape(query.Sub) + // 混合订阅和模板节点 + sub, err := MixinSubTemp(query, config.Default.MetaTemplate) + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + // 添加自定义节点、规则 + // 输出 + marshal, err := yaml.Marshal(sub) + if err != nil { + c.String(http.StatusInternalServerError, "YAML序列化失败: "+err.Error()) + return + } + c.String(http.StatusOK, string(marshal)) +} diff --git a/api/route.go b/api/route.go new file mode 100644 index 0000000..8cbb259 --- /dev/null +++ b/api/route.go @@ -0,0 +1,19 @@ +package api + +import ( + "github.com/gin-gonic/gin" + "sub/api/controller" +) + +func SetRoute(r *gin.Engine) { + r.GET( + "/clash", func(c *gin.Context) { + controller.SubmodHandler(c) + }, + ) + r.GET( + "/meta", func(c *gin.Context) { + controller.SubHandler(c) + }, + ) +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..889b881 --- /dev/null +++ b/config/config.go @@ -0,0 +1,16 @@ +package config + +type Config struct { + Port int //TODO: 使用自定义端口 + MetaTemplate string + ClashTemplate string +} + +var Default *Config + +func init() { + Default = &Config{ + MetaTemplate: "template-meta.yaml", + ClashTemplate: "template-clash.yaml", + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d0267f2 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module sub + +go 1.21 + +require ( + github.com/bytedance/sonic v1.10.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.15.3 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.5.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ba35269 --- /dev/null +++ b/go.sum @@ -0,0 +1,84 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.0 h1:qtNZduETEIWJVIyDl01BeNxur2rW9OwTQ/yBqFRkKEk= +github.com/bytedance/sonic v1.10.0/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo= +github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= +golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a3ae0ad --- /dev/null +++ b/main.go @@ -0,0 +1,83 @@ +package main + +import ( + _ "embed" + "fmt" + "github.com/gin-gonic/gin" + "os" + "path/filepath" + "sub/api" + "sub/config" +) + +//go:embed templates/template-meta.yaml +var templateMeta string + +//go:embed templates/template-clash.yaml +var templateClash string + +func writeTemplate(path string, template string) error { + tPath := filepath.Join( + "templates", path, + ) + if _, err := os.Stat(tPath); os.IsNotExist(err) { + file, err := os.Create(tPath) + if err != nil { + fmt.Println(err) + return err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + fmt.Println(err) + } + }(file) + _, err = file.WriteString(template) + if err != nil { + fmt.Println(err) + return err + } + } + return nil +} + +func mkDir(dir string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, os.ModePerm) + if err != nil { + fmt.Println(err) + return err + } + } + return nil +} + +func init() { + if err := mkDir("subs"); err != nil { + os.Exit(1) + } + if err := mkDir("templates"); err != nil { + os.Exit(1) + } + if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil { + os.Exit(1) + } + if err := writeTemplate(config.Default.ClashTemplate, templateClash); err != nil { + os.Exit(1) + } +} + +func main() { + // 设置运行模式 + gin.SetMode(gin.ReleaseMode) + // 创建路由 + r := gin.Default() + // 设置路由 + api.SetRoute(r) + fmt.Println("Server is running at 8011") + err := r.Run(":8011") + if err != nil { + fmt.Println(err) + return + } +} diff --git a/model/contryCode.go b/model/contryCode.go new file mode 100644 index 0000000..6df094a --- /dev/null +++ b/model/contryCode.go @@ -0,0 +1,1034 @@ +package model + +// CountryKeywords 国家关键词表 +// https://zh.wikipedia.org/wiki/%E5%8C%BA%E5%9F%9F%E6%8C%87%E7%A4%BA%E7%AC%A6 +// https://zh.wikipedia.org/zh-sg/ISO_3166-1%E4%BA%8C%E4%BD%8D%E5%AD%97%E6%AF%8D%E4%BB%A3%E7%A0%81 +var CountryKeywords = map[string]string{ + "🇦🇨": "阿森松岛(AC)", + "AC": "阿森松岛(AC)", + "阿森松岛": "阿森松岛(AC)", + "🇦🇩": "安道尔(AD)", + "AD": "安道尔(AD)", + "安道尔": "安道尔(AD)", + "🇦🇪": "阿联酋(AE)", + "AE": "阿联酋(AE)", + "阿联酋": "阿联酋(AE)", + "🇦🇫": "阿富汗(AF)", + "AF": "阿富汗(AF)", + "阿富汗": "阿富汗(AF)", + "🇦🇬": "安提瓜和巴布达(AG)", + "AG": "安提瓜和巴布达(AG)", + "安提瓜和巴布达": "安提瓜和巴布达(AG)", + "🇦🇮": "安圭拉(AI)", + "AI": "安圭拉(AI)", + "安圭拉": "安圭拉(AI)", + "🇦🇱": "阿尔巴尼亚(AL)", + "AL": "阿尔巴尼亚(AL)", + "阿尔巴尼亚": "阿尔巴尼亚(AL)", + "🇦🇲": "亚美尼亚(AM)", + "AM": "亚美尼亚(AM)", + "亚美尼亚": "亚美尼亚(AM)", + "🇦🇴": "安哥拉(AO)", + "AO": "安哥拉(AO)", + "安哥拉": "安哥拉(AO)", + "🇦🇶": "南极洲(AQ)", + "AQ": "南极洲(AQ)", + "南极洲": "南极洲(AQ)", + "🇦🇷": "阿根廷(AR)", + "AR": "阿根廷(AR)", + "阿根廷": "阿根廷(AR)", + "🇦🇸": "美属萨摩亚(AS)", + "AS": "美属萨摩亚(AS)", + "美属萨摩亚": "美属萨摩亚(AS)", + "🇦🇹": "奥地利(AT)", + "AT": "奥地利(AT)", + "奥地利": "奥地利(AT)", + "🇦🇺": "澳大利亚(AU)", + "AU": "澳大利亚(AU)", + "澳大利亚": "澳大利亚(AU)", + "🇦🇼": "阿鲁巴(AW)", + "AW": "阿鲁巴(AW)", + "阿鲁巴": "阿鲁巴(AW)", + "🇦🇽": "奥兰(AX)", + "AX": "奥兰(AX)", + "奥兰": "奥兰(AX)", + "🇦🇿": "阿塞拜疆(AZ)", + "AZ": "阿塞拜疆(AZ)", + "阿塞拜疆": "阿塞拜疆(AZ)", + "🇧🇦": "波黑(BA)", + "BA": "波黑(BA)", + "波黑": "波黑(BA)", + "🇧🇧": "巴巴多斯(BB)", + "BB": "巴巴多斯(BB)", + "巴巴多斯": "巴巴多斯(BB)", + "🇧🇩": "孟加拉国(BD)", + "BD": "孟加拉国(BD)", + "孟加拉国": "孟加拉国(BD)", + "🇧🇪": "比利时(BE)", + "BE": "比利时(BE)", + "比利时": "比利时(BE)", + "🇧🇫": "布基纳法索(BF)", + "BF": "布基纳法索(BF)", + "布基纳法索": "布基纳法索(BF)", + "🇧🇬": "保加利亚(BG)", + "BG": "保加利亚(BG)", + "保加利亚": "保加利亚(BG)", + "🇧🇭": "巴林(BH)", + "BH": "巴林(BH)", + "巴林": "巴林(BH)", + "🇧🇮": "布隆迪(BI)", + "BI": "布隆迪(BI)", + "布隆迪": "布隆迪(BI)", + "🇧🇯": "贝宁(BJ)", + "BJ": "贝宁(BJ)", + "贝宁": "贝宁(BJ)", + "🇧🇱": "圣巴泰勒米(BL)", + "BL": "圣巴泰勒米(BL)", + "圣巴泰勒米": "圣巴泰勒米(BL)", + "🇧🇲": "百慕大(BM)", + "BM": "百慕大(BM)", + "百慕大": "百慕大(BM)", + "🇧🇳": "文莱(BN)", + "BN": "文莱(BN)", + "文莱": "文莱(BN)", + "🇧🇴": "玻利维亚(BO)", + "BO": "玻利维亚(BO)", + "玻利维亚": "玻利维亚(BO)", + "🇧🇶": "荷兰加勒比区(BQ)", + "BQ": "荷兰加勒比区(BQ)", + "荷兰加勒比区": "荷兰加勒比区(BQ)", + "🇧🇷": "巴西(BR)", + "BR": "巴西(BR)", + "巴西": "巴西(BR)", + "🇧🇸": "巴哈马(BS)", + "BS": "巴哈马(BS)", + "巴哈马": "巴哈马(BS)", + "🇧🇹": "不丹(BT)", + "BT": "不丹(BT)", + "不丹": "不丹(BT)", + "🇧🇻": "布韦岛(BV)", + "BV": "布韦岛(BV)", + "布韦岛": "布韦岛(BV)", + "🇧🇼": "博茨瓦纳(BW)", + "BW": "博茨瓦纳(BW)", + "博茨瓦纳": "博茨瓦纳(BW)", + "🇧🇾": "白俄罗斯(BY)", + "BY": "白俄罗斯(BY)", + "白俄罗斯": "白俄罗斯(BY)", + "🇧🇿": "伯利兹(BZ)", + "BZ": "伯利兹(BZ)", + "伯利兹": "伯利兹(BZ)", + "🇨🇦": "加拿大(CA)", + "CA": "加拿大(CA)", + "加拿大": "加拿大(CA)", + "🇨🇨": "科科斯(基林)群岛(CC)", + "CC": "科科斯(基林)群岛(CC)", + "科科斯(基林)群岛": "科科斯(基林)群岛(CC)", + "🇨🇩": "刚果民主共和国(CD)", + "CD": "刚果民主共和国(CD)", + "刚果民主共和国": "刚果民主共和国(CD)", + "🇨🇫": "中非共和国|中非(CF)", + "CF": "中非共和国|中非(CF)", + "中非共和国|中非": "中非共和国|中非(CF)", + "🇨🇬": "刚果共和国(CG)", + "CG": "刚果共和国(CG)", + "刚果共和国": "刚果共和国(CG)", + "🇨🇭": "瑞士(CH)", + "CH": "瑞士(CH)", + "瑞士": "瑞士(CH)", + "🇨🇮": "科特迪瓦(CI)", + "CI": "科特迪瓦(CI)", + "科特迪瓦": "科特迪瓦(CI)", + "🇨🇰": "库克群岛(CK)", + "CK": "库克群岛(CK)", + "库克群岛": "库克群岛(CK)", + "🇨🇱": "智利(CL)", + "CL": "智利(CL)", + "智利": "智利(CL)", + "🇨🇲": "喀麦隆(CM)", + "CM": "喀麦隆(CM)", + "喀麦隆": "喀麦隆(CM)", + "🇨🇳": "中国(CN)", + "CN": "中国(CN)", + "中国": "中国(CN)", + "🇨🇴": "哥伦比亚(CO)", + "CO": "哥伦比亚(CO)", + "哥伦比亚": "哥伦比亚(CO)", + "🇨🇵": "克利珀顿岛(CP)", + "CP": "克利珀顿岛(CP)", + "克利珀顿岛": "克利珀顿岛(CP)", + "🇨🇷": "哥斯达黎加(CR)", + "CR": "哥斯达黎加(CR)", + "哥斯达黎加": "哥斯达黎加(CR)", + "🇨🇺": "古巴(CU)", + "CU": "古巴(CU)", + "古巴": "古巴(CU)", + "🇨🇻": "佛得角(CV)", + "CV": "佛得角(CV)", + "佛得角": "佛得角(CV)", + "🇨🇼": "库拉索(CW)", + "CW": "库拉索(CW)", + "库拉索": "库拉索(CW)", + "🇨🇽": "圣诞岛(CX)", + "CX": "圣诞岛(CX)", + "圣诞岛": "圣诞岛(CX)", + "🇨🇾": "塞浦路斯(CY)", + "CY": "塞浦路斯(CY)", + "塞浦路斯": "塞浦路斯(CY)", + "🇨🇿": "捷克(CZ)", + "CZ": "捷克(CZ)", + "捷克": "捷克(CZ)", + "🇩🇪": "德国(DE)", + "DE": "德国(DE)", + "德国": "德国(DE)", + "🇩🇬": "迪戈加西亚岛(DG)", + "DG": "迪戈加西亚岛(DG)", + "迪戈加西亚岛": "迪戈加西亚岛(DG)", + "🇩🇯": "吉布提(DJ)", + "DJ": "吉布提(DJ)", + "吉布提": "吉布提(DJ)", + "🇩🇰": "丹麦(DK)", + "DK": "丹麦(DK)", + "丹麦": "丹麦(DK)", + "🇩🇲": "多米尼克(DM)", + "DM": "多米尼克(DM)", + "多米尼克": "多米尼克(DM)", + "🇩🇴": "多米尼加(DO)", + "DO": "多米尼加(DO)", + "多米尼加": "多米尼加(DO)", + "🇩🇿": "阿尔及利亚(DZ)", + "DZ": "阿尔及利亚(DZ)", + "阿尔及利亚": "阿尔及利亚(DZ)", + "🇪🇦": "休达(EA)", + "EA": "休达(EA)", + "休达": "休达(EA)", + "🇪🇨": "厄瓜多尔(EC)", + "EC": "厄瓜多尔(EC)", + "厄瓜多尔": "厄瓜多尔(EC)", + "🇪🇪": "爱沙尼亚(EE)", + "EE": "爱沙尼亚(EE)", + "爱沙尼亚": "爱沙尼亚(EE)", + "🇪🇬": "埃及(EG)", + "EG": "埃及(EG)", + "埃及": "埃及(EG)", + "🇪🇭": "西撒哈拉(EH)", + "EH": "西撒哈拉(EH)", + "西撒哈拉": "西撒哈拉(EH)", + "🇪🇷": "厄立特里亚(ER)", + "ER": "厄立特里亚(ER)", + "厄立特里亚": "厄立特里亚(ER)", + "🇪🇸": "西班牙(ES)", + "ES": "西班牙(ES)", + "西班牙": "西班牙(ES)", + "🇪🇹": "埃塞俄比亚(ET)", + "ET": "埃塞俄比亚(ET)", + "埃塞俄比亚": "埃塞俄比亚(ET)", + "🇪🇺": "欧盟(EU)", + "EU": "欧盟(EU)", + "欧盟": "欧盟(EU)", + "🇫🇮": "芬兰(FI)", + "FI": "芬兰(FI)", + "芬兰": "芬兰(FI)", + "🇫🇯": "斐济(FJ)", + "FJ": "斐济(FJ)", + "斐济": "斐济(FJ)", + "🇫🇰": "福克兰群岛(FK)", + "FK": "福克兰群岛(FK)", + "福克兰群岛": "福克兰群岛(FK)", + "🇫🇲": "密克罗尼西亚联邦(FM)", + "FM": "密克罗尼西亚联邦(FM)", + "密克罗尼西亚联邦": "密克罗尼西亚联邦(FM)", + "🇫🇴": "法罗群岛(FO)", + "FO": "法罗群岛(FO)", + "法罗群岛": "法罗群岛(FO)", + "🇫🇷": "法国(FR)", + "FR": "法国(FR)", + "法国": "法国(FR)", + "🇬🇦": "加蓬(GA)", + "GA": "加蓬(GA)", + "加蓬": "加蓬(GA)", + "🇬🇧": "英国(GB)", + "GB": "英国(GB)", + "英国": "英国(GB)", + "🇬🇩": "格林纳达(GD)", + "GD": "格林纳达(GD)", + "格林纳达": "格林纳达(GD)", + "🇬🇪": "格鲁吉亚(GE)", + "GE": "格鲁吉亚(GE)", + "格鲁吉亚": "格鲁吉亚(GE)", + "🇬🇫": "法属圭亚那(GF)", + "GF": "法属圭亚那(GF)", + "法属圭亚那": "法属圭亚那(GF)", + "🇬🇬": "根西(GG)", + "GG": "根西(GG)", + "根西": "根西(GG)", + "🇬🇭": "加纳(GH)", + "GH": "加纳(GH)", + "加纳": "加纳(GH)", + "🇬🇮": "直布罗陀(GI)", + "GI": "直布罗陀(GI)", + "直布罗陀": "直布罗陀(GI)", + "🇬🇱": "格陵兰(GL)", + "GL": "格陵兰(GL)", + "格陵兰": "格陵兰(GL)", + "🇬🇲": "冈比亚(GM)", + "GM": "冈比亚(GM)", + "冈比亚": "冈比亚(GM)", + "🇬🇳": "几内亚(GN)", + "GN": "几内亚(GN)", + "几内亚": "几内亚(GN)", + "🇬🇵": "瓜德罗普(GP)", + "GP": "瓜德罗普(GP)", + "瓜德罗普": "瓜德罗普(GP)", + "🇬🇶": "赤道几内亚(GQ)", + "GQ": "赤道几内亚(GQ)", + "赤道几内亚": "赤道几内亚(GQ)", + "🇬🇷": "希腊(GR)", + "GR": "希腊(GR)", + "希腊": "希腊(GR)", + "🇬🇸": "南乔治亚和南桑威奇群岛(GS)", + "GS": "南乔治亚和南桑威奇群岛(GS)", + "南乔治亚和南桑威奇群岛": "南乔治亚和南桑威奇群岛(GS)", + "🇬🇹": "危地马拉(GT)", + "GT": "危地马拉(GT)", + "危地马拉": "危地马拉(GT)", + "🇬🇺": "关岛(GU)", + "GU": "关岛(GU)", + "关岛": "关岛(GU)", + "🇬🇼": "几内亚比绍(GW)", + "GW": "几内亚比绍(GW)", + "几内亚比绍": "几内亚比绍(GW)", + "🇬🇾": "圭亚那(GY)", + "GY": "圭亚那(GY)", + "圭亚那": "圭亚那(GY)", + "🇭🇰": "香港(HK)", + "HK": "香港(HK)", + "香港": "香港(HK)", + "🇭🇲": "赫德岛和麦克唐纳群岛(HM)", + "HM": "赫德岛和麦克唐纳群岛(HM)", + "赫德岛和麦克唐纳群岛": "赫德岛和麦克唐纳群岛(HM)", + "🇭🇳": "洪都拉斯(HN)", + "HN": "洪都拉斯(HN)", + "洪都拉斯": "洪都拉斯(HN)", + "🇭🇷": "克罗地亚(HR)", + "HR": "克罗地亚(HR)", + "克罗地亚": "克罗地亚(HR)", + "🇭🇹": "海地(HT)", + "HT": "海地(HT)", + "海地": "海地(HT)", + "🇭🇺": "匈牙利(HU)", + "HU": "匈牙利(HU)", + "匈牙利": "匈牙利(HU)", + "🇮🇨": "加纳利群岛(IC)", + "IC": "加纳利群岛(IC)", + "加纳利群岛": "加纳利群岛(IC)", + "🇮🇩": "印尼(ID)", + "ID": "印尼(ID)", + "印尼": "印尼(ID)", + "🇮🇪": "爱尔兰(IE)", + "IE": "爱尔兰(IE)", + "爱尔兰": "爱尔兰(IE)", + "🇮🇱": "以色列(IL)", + "IL": "以色列(IL)", + "以色列": "以色列(IL)", + "🇮🇲": "马恩岛(IM)", + "IM": "马恩岛(IM)", + "马恩岛": "马恩岛(IM)", + "🇮🇳": "印度(IN)", + "IN": "印度(IN)", + "印度": "印度(IN)", + "🇮🇴": "英属印度洋领地(IO)", + "IO": "英属印度洋领地(IO)", + "英属印度洋领地": "英属印度洋领地(IO)", + "🇮🇶": "伊拉克(IQ)", + "IQ": "伊拉克(IQ)", + "伊拉克": "伊拉克(IQ)", + "🇮🇷": "伊朗(IR)", + "IR": "伊朗(IR)", + "伊朗": "伊朗(IR)", + "🇮🇸": "冰岛(IS)", + "IS": "冰岛(IS)", + "冰岛": "冰岛(IS)", + "🇮🇹": "意大利(IT)", + "IT": "意大利(IT)", + "意大利": "意大利(IT)", + "🇯🇪": "泽西(JE)", + "JE": "泽西(JE)", + "泽西": "泽西(JE)", + "🇯🇲": "牙买加(JM)", + "JM": "牙买加(JM)", + "牙买加": "牙买加(JM)", + "🇯🇴": "约旦(JO)", + "JO": "约旦(JO)", + "约旦": "约旦(JO)", + "🇯🇵": "日本(JP)", + "JP": "日本(JP)", + "日本": "日本(JP)", + "🇰🇪": "肯尼亚(KE)", + "KE": "肯尼亚(KE)", + "肯尼亚": "肯尼亚(KE)", + "🇰🇬": "吉尔吉斯斯坦(KG)", + "KG": "吉尔吉斯斯坦(KG)", + "吉尔吉斯斯坦": "吉尔吉斯斯坦(KG)", + "🇰🇭": "柬埔寨(KH)", + "KH": "柬埔寨(KH)", + "柬埔寨": "柬埔寨(KH)", + "🇰🇮": "基里巴斯(KI)", + "KI": "基里巴斯(KI)", + "基里巴斯": "基里巴斯(KI)", + "🇰🇲": "科摩罗(KM)", + "KM": "科摩罗(KM)", + "科摩罗": "科摩罗(KM)", + "🇰🇳": "圣基茨和尼维斯(KN)", + "KN": "圣基茨和尼维斯(KN)", + "圣基茨和尼维斯": "圣基茨和尼维斯(KN)", + "🇰🇵": "朝鲜民主主义人民共和国|朝鲜(KP)", + "KP": "朝鲜民主主义人民共和国|朝鲜(KP)", + "朝鲜民主主义人民共和国|朝鲜": "朝鲜民主主义人民共和国|朝鲜(KP)", + "🇰🇷": "大韩民国|南韩(KR)", + "KR": "大韩民国|南韩(KR)", + "大韩民国|南韩": "大韩民国|南韩(KR)", + "🇰🇼": "科威特(KW)", + "KW": "科威特(KW)", + "科威特": "科威特(KW)", + "🇰🇾": "开曼群岛(KY)", + "KY": "开曼群岛(KY)", + "开曼群岛": "开曼群岛(KY)", + "🇰🇿": "哈萨克斯坦(KZ)", + "KZ": "哈萨克斯坦(KZ)", + "哈萨克斯坦": "哈萨克斯坦(KZ)", + "🇱🇦": "老挝(LA)", + "LA": "老挝(LA)", + "老挝": "老挝(LA)", + "🇱🇧": "黎巴嫩(LB)", + "LB": "黎巴嫩(LB)", + "黎巴嫩": "黎巴嫩(LB)", + "🇱🇨": "圣卢西亚(LC)", + "LC": "圣卢西亚(LC)", + "圣卢西亚": "圣卢西亚(LC)", + "🇱🇮": "列支敦士登(LI)", + "LI": "列支敦士登(LI)", + "列支敦士登": "列支敦士登(LI)", + "🇱🇰": "斯里兰卡(LK)", + "LK": "斯里兰卡(LK)", + "斯里兰卡": "斯里兰卡(LK)", + "🇱🇷": "利比里亚(LR)", + "LR": "利比里亚(LR)", + "利比里亚": "利比里亚(LR)", + "🇱🇸": "莱索托(LS)", + "LS": "莱索托(LS)", + "莱索托": "莱索托(LS)", + "🇱🇹": "立陶宛(LT)", + "LT": "立陶宛(LT)", + "立陶宛": "立陶宛(LT)", + "🇱🇺": "卢森堡(LU)", + "LU": "卢森堡(LU)", + "卢森堡": "卢森堡(LU)", + "🇱🇻": "拉脱维亚(LV)", + "LV": "拉脱维亚(LV)", + "拉脱维亚": "拉脱维亚(LV)", + "🇱🇾": "利比亚(LY)", + "LY": "利比亚(LY)", + "利比亚": "利比亚(LY)", + "🇲🇦": "摩洛哥(MA)", + "MA": "摩洛哥(MA)", + "摩洛哥": "摩洛哥(MA)", + "🇲🇨": "摩纳哥(MC)", + "MC": "摩纳哥(MC)", + "摩纳哥": "摩纳哥(MC)", + "🇲🇩": "摩尔多瓦(MD)", + "MD": "摩尔多瓦(MD)", + "摩尔多瓦": "摩尔多瓦(MD)", + "🇲🇪": "黑山(ME)", + "ME": "黑山(ME)", + "黑山": "黑山(ME)", + "🇲🇫": "法属圣马丁(MF)", + "MF": "法属圣马丁(MF)", + "法属圣马丁": "法属圣马丁(MF)", + "🇲🇬": "马达加斯加(MG)", + "MG": "马达加斯加(MG)", + "马达加斯加": "马达加斯加(MG)", + "🇲🇭": "马绍尔群岛(MH)", + "MH": "马绍尔群岛(MH)", + "马绍尔群岛": "马绍尔群岛(MH)", + "🇲🇰": "北马其顿(MK)", + "MK": "北马其顿(MK)", + "北马其顿": "北马其顿(MK)", + "🇲🇱": "马里(ML)", + "ML": "马里(ML)", + "马里": "马里(ML)", + "🇲🇲": "缅甸(MM)", + "MM": "缅甸(MM)", + "缅甸": "缅甸(MM)", + "🇲🇳": "蒙古国(MN)", + "MN": "蒙古国(MN)", + "蒙古国": "蒙古国(MN)", + "🇲🇴": "澳门(MO)", + "MO": "澳门(MO)", + "澳门": "澳门(MO)", + "🇲🇵": "北马里亚纳群岛(MP)", + "MP": "北马里亚纳群岛(MP)", + "北马里亚纳群岛": "北马里亚纳群岛(MP)", + "🇲🇶": "马提尼克(MQ)", + "MQ": "马提尼克(MQ)", + "马提尼克": "马提尼克(MQ)", + "🇲🇷": "毛里塔尼亚(MR)", + "MR": "毛里塔尼亚(MR)", + "毛里塔尼亚": "毛里塔尼亚(MR)", + "🇲🇸": "蒙特塞拉特(MS)", + "MS": "蒙特塞拉特(MS)", + "蒙特塞拉特": "蒙特塞拉特(MS)", + "🇲🇹": "马耳他(MT)", + "MT": "马耳他(MT)", + "马耳他": "马耳他(MT)", + "🇲🇺": "毛里求斯(MU)", + "MU": "毛里求斯(MU)", + "毛里求斯": "毛里求斯(MU)", + "🇲🇻": "马尔代夫(MV)", + "MV": "马尔代夫(MV)", + "马尔代夫": "马尔代夫(MV)", + "🇲🇼": "马拉维(MW)", + "MW": "马拉维(MW)", + "马拉维": "马拉维(MW)", + "🇲🇽": "墨西哥(MX)", + "MX": "墨西哥(MX)", + "墨西哥": "墨西哥(MX)", + "🇲🇾": "马来西亚(MY)", + "MY": "马来西亚(MY)", + "马来西亚": "马来西亚(MY)", + "🇲🇿": "莫桑比克(MZ)", + "MZ": "莫桑比克(MZ)", + "莫桑比克": "莫桑比克(MZ)", + "🇳🇦": "纳米比亚(NA)", + "NA": "纳米比亚(NA)", + "纳米比亚": "纳米比亚(NA)", + "🇳🇨": "新喀里多尼亚(NC)", + "NC": "新喀里多尼亚(NC)", + "新喀里多尼亚": "新喀里多尼亚(NC)", + "🇳🇪": "尼日尔(NE)", + "NE": "尼日尔(NE)", + "尼日尔": "尼日尔(NE)", + "🇳🇫": "诺福克岛(NF)", + "NF": "诺福克岛(NF)", + "诺福克岛": "诺福克岛(NF)", + "🇳🇬": "尼日利亚(NG)", + "NG": "尼日利亚(NG)", + "尼日利亚": "尼日利亚(NG)", + "🇳🇮": "尼加拉瓜(NI)", + "NI": "尼加拉瓜(NI)", + "尼加拉瓜": "尼加拉瓜(NI)", + "🇳🇱": "荷兰(NL)", + "NL": "荷兰(NL)", + "荷兰": "荷兰(NL)", + "🇳🇴": "挪威(NO)", + "NO": "挪威(NO)", + "挪威": "挪威(NO)", + "🇳🇵": "尼泊尔(NP)", + "NP": "尼泊尔(NP)", + "尼泊尔": "尼泊尔(NP)", + "🇳🇷": "瑙鲁(NR)", + "NR": "瑙鲁(NR)", + "瑙鲁": "瑙鲁(NR)", + "🇳🇺": "纽埃(NU)", + "NU": "纽埃(NU)", + "纽埃": "纽埃(NU)", + "🇳🇿": "新西兰(NZ)", + "NZ": "新西兰(NZ)", + "新西兰": "新西兰(NZ)", + "🇴🇲": "阿曼(OM)", + "OM": "阿曼(OM)", + "阿曼": "阿曼(OM)", + "🇵🇦": "巴拿马(PA)", + "PA": "巴拿马(PA)", + "巴拿马": "巴拿马(PA)", + "🇵🇪": "秘鲁(PE)", + "PE": "秘鲁(PE)", + "秘鲁": "秘鲁(PE)", + "🇵🇫": "法属波利尼西亚(PF)", + "PF": "法属波利尼西亚(PF)", + "法属波利尼西亚": "法属波利尼西亚(PF)", + "🇵🇬": "巴布亚新几内亚(PG)", + "PG": "巴布亚新几内亚(PG)", + "巴布亚新几内亚": "巴布亚新几内亚(PG)", + "🇵🇭": "菲律宾(PH)", + "PH": "菲律宾(PH)", + "菲律宾": "菲律宾(PH)", + "🇵🇰": "巴基斯坦(PK)", + "PK": "巴基斯坦(PK)", + "巴基斯坦": "巴基斯坦(PK)", + "🇵🇱": "波兰(PL)", + "PL": "波兰(PL)", + "波兰": "波兰(PL)", + "🇵🇲": "圣皮埃尔和密克隆(PM)", + "PM": "圣皮埃尔和密克隆(PM)", + "圣皮埃尔和密克隆": "圣皮埃尔和密克隆(PM)", + "🇵🇳": "皮特凯恩群岛(PN)", + "PN": "皮特凯恩群岛(PN)", + "皮特凯恩群岛": "皮特凯恩群岛(PN)", + "🇵🇷": "波多黎各(PR)", + "PR": "波多黎各(PR)", + "波多黎各": "波多黎各(PR)", + "🇵🇸": "巴勒斯坦国|巴勒斯坦(PS)", + "PS": "巴勒斯坦国|巴勒斯坦(PS)", + "巴勒斯坦国|巴勒斯坦": "巴勒斯坦国|巴勒斯坦(PS)", + "🇵🇹": "葡萄牙(PT)", + "PT": "葡萄牙(PT)", + "葡萄牙": "葡萄牙(PT)", + "🇵🇼": "帕劳(PW)", + "PW": "帕劳(PW)", + "帕劳": "帕劳(PW)", + "🇵🇾": "巴拉圭(PY)", + "PY": "巴拉圭(PY)", + "巴拉圭": "巴拉圭(PY)", + "🇶🇦": "卡塔尔(QA)", + "QA": "卡塔尔(QA)", + "卡塔尔": "卡塔尔(QA)", + "🇷🇪": "留尼旺(RE)", + "RE": "留尼旺(RE)", + "留尼旺": "留尼旺(RE)", + "🇷🇴": "罗马尼亚(RO)", + "RO": "罗马尼亚(RO)", + "罗马尼亚": "罗马尼亚(RO)", + "🇷🇸": "塞尔维亚(RS)", + "RS": "塞尔维亚(RS)", + "塞尔维亚": "塞尔维亚(RS)", + "🇷🇺": "俄罗斯(RU)", + "RU": "俄罗斯(RU)", + "俄罗斯": "俄罗斯(RU)", + "🇷🇼": "卢旺达(RW)", + "RW": "卢旺达(RW)", + "卢旺达": "卢旺达(RW)", + "🇸🇦": "沙特阿拉伯(SA)", + "SA": "沙特阿拉伯(SA)", + "沙特阿拉伯": "沙特阿拉伯(SA)", + "🇸🇧": "所罗门群岛(SB)", + "SB": "所罗门群岛(SB)", + "所罗门群岛": "所罗门群岛(SB)", + "🇸🇨": "塞舌尔(SC)", + "SC": "塞舌尔(SC)", + "塞舌尔": "塞舌尔(SC)", + "🇸🇩": "苏丹(SD)", + "SD": "苏丹(SD)", + "苏丹": "苏丹(SD)", + "🇸🇪": "瑞典(SE)", + "SE": "瑞典(SE)", + "瑞典": "瑞典(SE)", + "🇸🇬": "新加坡(SG)", + "SG": "新加坡(SG)", + "新加坡": "新加坡(SG)", + "🇸🇭": "圣赫勒拿(SH)", + "SH": "圣赫勒拿(SH)", + "圣赫勒拿": "圣赫勒拿(SH)", + "🇸🇮": "斯洛文尼亚(SI)", + "SI": "斯洛文尼亚(SI)", + "斯洛文尼亚": "斯洛文尼亚(SI)", + "🇸🇯": "斯瓦尔巴和扬马延(SJ)", + "SJ": "斯瓦尔巴和扬马延(SJ)", + "斯瓦尔巴和扬马延": "斯瓦尔巴和扬马延(SJ)", + "🇸🇰": "斯洛伐克(SK)", + "SK": "斯洛伐克(SK)", + "斯洛伐克": "斯洛伐克(SK)", + "🇸🇱": "塞拉利昂(SL)", + "SL": "塞拉利昂(SL)", + "塞拉利昂": "塞拉利昂(SL)", + "🇸🇲": "圣马力诺(SM)", + "SM": "圣马力诺(SM)", + "圣马力诺": "圣马力诺(SM)", + "🇸🇳": "塞内加尔(SN)", + "SN": "塞内加尔(SN)", + "塞内加尔": "塞内加尔(SN)", + "🇸🇴": "索马里(SO)", + "SO": "索马里(SO)", + "索马里": "索马里(SO)", + "🇸🇷": "苏里南(SR)", + "SR": "苏里南(SR)", + "苏里南": "苏里南(SR)", + "🇸🇸": "南苏丹(SS)", + "SS": "南苏丹(SS)", + "南苏丹": "南苏丹(SS)", + "🇸🇹": "圣多美和普林西比(ST)", + "ST": "圣多美和普林西比(ST)", + "圣多美和普林西比": "圣多美和普林西比(ST)", + "🇸🇻": "萨尔瓦多(SV)", + "SV": "萨尔瓦多(SV)", + "萨尔瓦多": "萨尔瓦多(SV)", + "🇸🇽": "荷属圣马丁(SX)", + "SX": "荷属圣马丁(SX)", + "荷属圣马丁": "荷属圣马丁(SX)", + "🇸🇾": "叙利亚(SY)", + "SY": "叙利亚(SY)", + "叙利亚": "叙利亚(SY)", + "🇸🇿": "斯威士兰(SZ)", + "SZ": "斯威士兰(SZ)", + "斯威士兰": "斯威士兰(SZ)", + "🇹🇦": "特里斯坦-达库尼亚(TA)", + "TA": "特里斯坦-达库尼亚(TA)", + "特里斯坦-达库尼亚": "特里斯坦-达库尼亚(TA)", + "🇹🇨": "特克斯和凯科斯群岛(TC)", + "TC": "特克斯和凯科斯群岛(TC)", + "特克斯和凯科斯群岛": "特克斯和凯科斯群岛(TC)", + "🇹🇩": "乍得(TD)", + "TD": "乍得(TD)", + "乍得": "乍得(TD)", + "🇹🇫": "法属南部和南极领地(TF)", + "TF": "法属南部和南极领地(TF)", + "法属南部和南极领地": "法属南部和南极领地(TF)", + "🇹🇬": "多哥(TG)", + "TG": "多哥(TG)", + "多哥": "多哥(TG)", + "🇹🇭": "泰国(TH)", + "TH": "泰国(TH)", + "泰国": "泰国(TH)", + "🇹🇯": "塔吉克斯坦(TJ)", + "TJ": "塔吉克斯坦(TJ)", + "塔吉克斯坦": "塔吉克斯坦(TJ)", + "🇹🇰": "托克劳(TK)", + "TK": "托克劳(TK)", + "托克劳": "托克劳(TK)", + "🇹🇱": "东帝汶(TL)", + "TL": "东帝汶(TL)", + "东帝汶": "东帝汶(TL)", + "🇹🇲": "土库曼斯坦(TM)", + "TM": "土库曼斯坦(TM)", + "土库曼斯坦": "土库曼斯坦(TM)", + "🇹🇳": "突尼斯(TN)", + "TN": "突尼斯(TN)", + "突尼斯": "突尼斯(TN)", + "🇹🇴": "汤加(TO)", + "TO": "汤加(TO)", + "汤加": "汤加(TO)", + "🇹🇷": "土耳其(TR)", + "TR": "土耳其(TR)", + "土耳其": "土耳其(TR)", + "🇹🇹": "特立尼达和多巴哥(TT)", + "TT": "特立尼达和多巴哥(TT)", + "特立尼达和多巴哥": "特立尼达和多巴哥(TT)", + "🇹🇻": "图瓦卢(TV)", + "TV": "图瓦卢(TV)", + "图瓦卢": "图瓦卢(TV)", + "🇹🇼": "台湾(TW)", + "TW": "台湾(TW)", + "台湾": "台湾(TW)", + "🇹🇿": "坦桑尼亚(TZ)", + "TZ": "坦桑尼亚(TZ)", + "坦桑尼亚": "坦桑尼亚(TZ)", + "🇺🇦": "乌克兰(UA)", + "UA": "乌克兰(UA)", + "乌克兰": "乌克兰(UA)", + "🇺🇬": "乌干达(UG)", + "UG": "乌干达(UG)", + "乌干达": "乌干达(UG)", + "🇺🇲": "美国本土外小岛屿(UM)", + "UM": "美国本土外小岛屿(UM)", + "美国本土外小岛屿": "美国本土外小岛屿(UM)", + "🇺🇳": "联合国(UN)", + "UN": "联合国(UN)", + "联合国": "联合国(UN)", + "🇺🇸": "美国(US)", + "US": "美国(US)", + "美国": "美国(US)", + "🇺🇾": "乌拉圭(UY)", + "UY": "乌拉圭(UY)", + "乌拉圭": "乌拉圭(UY)", + "🇺🇿": "乌兹别克斯坦(UZ)", + "UZ": "乌兹别克斯坦(UZ)", + "乌兹别克斯坦": "乌兹别克斯坦(UZ)", + "🇻🇦": "梵蒂冈(VA)", + "VA": "梵蒂冈(VA)", + "梵蒂冈": "梵蒂冈(VA)", + "🇻🇨": "圣文森特和格林纳丁斯(VC)", + "VC": "圣文森特和格林纳丁斯(VC)", + "圣文森特和格林纳丁斯": "圣文森特和格林纳丁斯(VC)", + "🇻🇪": "委内瑞拉(VE)", + "VE": "委内瑞拉(VE)", + "委内瑞拉": "委内瑞拉(VE)", + "🇻🇬": "英属维尔京群岛(VG)", + "VG": "英属维尔京群岛(VG)", + "英属维尔京群岛": "英属维尔京群岛(VG)", + "🇻🇮": "美属维尔京群岛(VI)", + "VI": "美属维尔京群岛(VI)", + "美属维尔京群岛": "美属维尔京群岛(VI)", + "🇻🇳": "越南(VN)", + "VN": "越南(VN)", + "越南": "越南(VN)", + "🇻🇺": "瓦努阿图(VU)", + "VU": "瓦努阿图(VU)", + "瓦努阿图": "瓦努阿图(VU)", + "🇼🇫": "瓦利斯和富图纳(WF)", + "WF": "瓦利斯和富图纳(WF)", + "瓦利斯和富图纳": "瓦利斯和富图纳(WF)", + "🇼🇸": "萨摩亚(WS)", + "WS": "萨摩亚(WS)", + "萨摩亚": "萨摩亚(WS)", + "🇽🇰": "科索沃(XK)", + "XK": "科索沃(XK)", + "科索沃": "科索沃(XK)", + "🇾🇪": "也门(YE)", + "YE": "也门(YE)", + "也门": "也门(YE)", + "🇾🇹": "马约特(YT)", + "YT": "马约特(YT)", + "马约特": "马约特(YT)", + "🇿🇦": "南非(ZA)", + "ZA": "南非(ZA)", + "南非": "南非(ZA)", + "🇿🇲": "赞比亚(ZM)", + "ZM": "赞比亚(ZM)", + "赞比亚": "赞比亚(ZM)", + "🇿🇼": "津巴布韦(ZW)", + "ZW": "津巴布韦(ZW)", + "津巴布韦": "津巴布韦(ZW)", + "Andorra": "安道尔(AD)", + "United Arab Emirates": "阿联酋(AE)", + "Afghanistan": "阿富汗(AF)", + "Antigua and Barbuda": "安提瓜和巴布达(AG)", + "Anguilla": "安圭拉(AI)", + "Albania": "阿尔巴尼亚(AL)", + "Armenia": "亚美尼亚(AM)", + "Angola": "安哥拉(AO)", + "Antarctica": "南极洲(AQ)", + "Argentina": "阿根廷(AR)", + "American Samoa": "美属萨摩亚(AS)", + "Austria": "奥地利(AT)", + "Australia": "澳大利亚(AU)", + "Aruba": "阿鲁巴(AW)", + "Åland Islands": "奥兰(AX)", + "Azerbaijan": "阿塞拜疆(AZ)", + "Bosnia and Herzegovina": "波黑(BA)", + "Barbados": "巴巴多斯(BB)", + "Bangladesh": "孟加拉(BD)", + "Belgium": "比利时(BE)", + "Burkina Faso": "布基纳法索(BF)", + "Bulgaria": "保加利亚(BG)", + "Bahrain": "巴林(BH)", + "Burundi": "布隆迪(BI)", + "Benin": "贝宁(BJ)", + "Saint Barthélemy": "圣巴泰勒米(BL)", + "Bermuda": "百慕大(BM)", + "Brunei Darussalam": "汶莱(BN)", + "Bolivia": "玻利维亚(BO)", + "Plurinational State of": "玻利维亚(BO)", + "Bonaire": "荷兰加勒比区(BQ)", + "Sint Eustatius and Saba": "荷兰加勒比区(BQ)", + "Brazil": "巴西(BR)", + "Bahamas": "巴哈马(BS)", + "Bhutan": "不丹(BT)", + "Bouvet Island": "布韦岛(BV)", + "Botswana": "博茨瓦纳(BW)", + "Belarus": "白俄罗斯(BY)", + "Belize": "伯利兹(BZ)", + "Canada": "加拿大(CA)", + "Cocos (Keeling) Islands": "科科斯(基林)群岛(CC)", + "DR Congo": "刚果民主共和国(CD)", + "the Democratic Republic of the": "刚果民主共和国(CD)", + "Central African Republic": "中非(CF)", + "Congo": "刚果共和国(CG)", + "Switzerland": "瑞士(CH)", + "Côte d'Ivoire": "象牙海岸(CI)", + "Cook Islands": "库克群岛(CK)", + "Chile": "智利(CL)", + "Cameroon": "喀麦隆(CM)", + "China": "中国(CN)", + "Colombia": "哥伦比亚(CO)", + "Costa Rica": "哥斯达黎加(CR)", + "Cuba": "古巴(CU)", + "Cabo Verde": "佛得角(CV)", + "Curaçao": "库拉索(CW)", + "Christmas Island": "圣诞岛(CX)", + "Cyprus": "塞浦路斯(CY)", + "Czechia": "捷克(CZ)", + "Germany": "德国(DE)", + "Djibouti": "吉布提(DJ)", + "Denmark": "丹麦(DK)", + "Dominica": "多米尼克(DM)", + "Dominican Republic": "多米尼加(DO)", + "Algeria": "阿尔及利亚(DZ)", + "Ecuador": "厄瓜多尔(EC)", + "Estonia": "爱沙尼亚(EE)", + "Egypt": "埃及(EG)", + "Western Sahara": "西撒哈拉(EH)", + "Eritrea": "厄立特里亚(ER)", + "Spain": "西班牙(ES)", + "Ethiopia": "埃塞俄比亚(ET)", + "Finland": "芬兰(FI)", + "Fiji": "斐济(FJ)", + "Falkland Islands (Malvinas)": "福克兰群岛(FK)", + "Micronesia": "密克罗尼西亚联邦(FM)", + "Federated States of": "密克罗尼西亚联邦(FM)", + "Faroe Islands": "法罗群岛(FO)", + "France": "法国(FR)", + "Gabon": "加蓬(GA)", + "United Kingdom of Great Britain and Northern Ireland": "英国(GB)", + "Grenada": "格林纳达(GD)", + "Georgia": "格鲁吉亚(GE)", + "French Guiana": "法属圭亚那(GF)", + "Guernsey": "根西(GG)", + "Ghana": "加纳(GH)", + "Gibraltar": "直布罗陀(GI)", + "Greenland": "格陵兰(GL)", + "Gambia": "冈比亚(GM)", + "Guinea": "几内亚(GN)", + "Guadeloupe": "瓜德罗普(GP)", + "Equatorial Guinea": "赤道几内亚(GQ)", + "Greece": "希腊(GR)", + "South Georgia and the South Sandwich Islands": "南乔治亚和南桑威奇群岛(GS)", + "Guatemala": "危地马拉(GT)", + "Guam": "关岛(GU)", + "Guinea-Bissau": "几内亚比绍(GW)", + "Guyana": "圭亚那(GY)", + "Hong Kong": "香港(HK)", + "Heard Island and McDonald Islands": "赫德岛和麦克唐纳群岛(HM)", + "Honduras": "洪都拉斯(HN)", + "Croatia": "克罗地亚(HR)", + "Haiti": "海地(HT)", + "Hungary": "匈牙利(HU)", + "Indonesia": "印度尼西亚(ID)", + "Ireland": "爱尔兰(IE)", + "Israel": "以色列(IL)", + "Isle of Man": "马恩岛(IM)", + "India": "印度(IN)", + "British Indian Ocean Territory": "英属印度洋领地(IO)", + "Iraq": "伊拉克(IQ)", + "Iran": "伊朗(IR)", + "Iceland": "冰岛(IS)", + "Italy": "意大利(IT)", + "Jersey": "泽西(JE)", + "Jamaica": "牙买加(JM)", + "Jordan": "约旦(JO)", + "Japan": "日本(JP)", + "Kenya": "肯尼亚(KE)", + "Kyrgyzstan": "吉尔吉斯斯坦(KG)", + "Cambodia": "柬埔寨(KH)", + "Kiribati": "基里巴斯(KI)", + "Comoros": "科摩罗(KM)", + "Saint Kitts and Nevis": "圣基茨和尼维斯(KN)", + "North Korea": "北韩(KP)", + "South Korea": "南韩(KR)", + "Kuwait": "科威特(KW)", + "Cayman Islands": "开曼群岛(KY)", + "Kazakhstan": "哈萨克斯坦(KZ)", + "Lao People's Democratic Republic": "寮国(LA)", + "Lebanon": "黎巴嫩(LB)", + "Saint Lucia": "圣卢西亚(LC)", + "Liechtenstein": "列支敦士登(LI)", + "Sri Lanka": "斯里兰卡(LK)", + "Liberia": "利比里亚(LR)", + "Lesotho": "莱索托(LS)", + "Lithuania": "立陶宛(LT)", + "Luxembourg": "卢森堡(LU)", + "Latvia": "拉脱维亚(LV)", + "Libya": "利比亚(LY)", + "Morocco": "摩洛哥(MA)", + "Monaco": "摩纳哥(MC)", + "Moldova, Republic of": "摩尔多瓦(MD)", + "Montenegro": "黑山(ME)", + "Saint Martin (French part)": "法属圣马丁(MF)", + "Madagascar": "马达加斯加(MG)", + "Marshall Islands": "马绍尔群岛(MH)", + "North Macedonia": "北马其顿(MK)", + "Mali": "马里(ML)", + "Myanmar": "缅甸(MM)", + "Mongolia": "蒙古(MN)", + "Macao": "澳门(MO)", + "Northern Mariana Islands": "北马里亚纳群岛(MP)", + "Martinique": "马提尼克(MQ)", + "Mauritania": "毛里塔尼亚(MR)", + "Montserrat": "蒙特塞拉特(MS)", + "Malta": "马耳他(MT)", + "Mauritius": "毛里求斯(MU)", + "Maldives": "马尔代夫(MV)", + "Malawi": "马拉维(MW)", + "Mexico": "墨西哥(MX)", + "Malaysia": "马来西亚(MY)", + "Mozambique": "莫桑比克(MZ)", + "Namibia": "那米比亚(NA)", + "New Caledonia": "新喀里多尼亚(NC)", + "Niger": "尼日尔(NE)", + "Norfolk Island": "诺福克岛(NF)", + "Nigeria": "尼日利亚(NG)", + "Nicaragua": "尼加拉瓜(NI)", + "Netherlands": "荷兰(NL)", + "Norway": "挪威(NO)", + "Nepal": "尼泊尔(NP)", + "Nauru": "瑙鲁(NR)", + "Niue": "纽埃(NU)", + "New Zealand": "新西兰(NZ)", + "Oman": "阿曼(OM)", + "Panama": "巴拿马(PA)", + "Peru": "秘鲁(PE)", + "French Polynesia": "法属波利尼西亚(PF)", + "Papua New Guinea": "巴布亚新几内亚(PG)", + "Philippines": "菲律宾(PH)", + "Pakistan": "巴基斯坦(PK)", + "Poland": "波兰(PL)", + "Saint Pierre and Miquelon": "圣皮埃尔和密克隆(PM)", + "Pitcairn": "皮特凯恩群岛(PN)", + "Puerto Rico": "波多黎各(PR)", + "Palestine": "巴勒斯坦(PS)", + "Portugal": "葡萄牙(PT)", + "Palau": "帕劳(PW)", + "Paraguay": "巴拉圭(PY)", + "Qatar": "卡塔尔(QA)", + "Réunion": "留尼汪(RE)", + "Romania": "罗马尼亚(RO)", + "Serbia": "塞尔维亚(RS)", + "Russian Federation": "俄罗斯(RU)", + "Rwanda": "卢旺达(RW)", + "Saudi Arabia": "沙特阿拉伯(SA)", + "Solomon Islands": "所罗门群岛(SB)", + "Seychelles": "塞舌尔(SC)", + "Sudan": "苏丹(SD)", + "Sweden": "瑞典(SE)", + "Singapore": "新加坡(SG)", + "Saint Helena": "圣赫勒拿、阿森松和特里斯坦-达库尼亚(SH)", + "Slovenia": "斯洛文尼亚(SI)", + "Svalbard and Jan Mayen": "斯瓦尔巴和扬马延(SJ)", + "Slovakia": "斯洛伐克(SK)", + "Sierra Leone": "塞拉利昂(SL)", + "San Marino": "圣马力诺(SM)", + "Senegal": "塞内加尔(SN)", + "Somalia": "索马里(SO)", + "Suriname": "苏里南(SR)", + "South Sudan": "南苏丹(SS)", + "Sao Tome and Principe": "圣多美和普林西比(ST)", + "El Salvador": "萨尔瓦多(SV)", + "Sint Maarten (Dutch part)": "荷属圣马丁(SX)", + "Syrian Arab Republic": "叙利亚(SY)", + "Eswatini": "斯威士兰(SZ)", + "Turks and Caicos Islands": "特克斯和凯科斯群岛(TC)", + "Chad": "乍得(TD)", + "French Southern Territories": "法属南部和南极领地(TF)", + "Togo": "多哥(TG)", + "Thailand": "泰国(TH)", + "Tajikistan": "塔吉克斯坦(TJ)", + "Tokelau": "托克劳(TK)", + "Timor-Leste": "东帝汶(TL)", + "Turkmenistan": "土库曼斯坦(TM)", + "Tunisia": "突尼斯(TN)", + "Tonga": "汤加(TO)", + "Turkey": "土耳其(TR)", + "Trinidad and Tobago": "特立尼达和多巴哥(TT)", + "Tuvalu": "图瓦卢(TV)", + "Taiwan": "台湾(TW)", + "Tanzania": "坦桑尼亚(TZ)", + "Ukraine": "乌克兰(UA)", + "Uganda": "乌干达(UG)", + "United States Minor Outlying Islands": "美国本土外小岛屿(UM)", + "United States of America": "美国(US)", + "Uruguay": "乌拉圭(UY)", + "Uzbekistan": "乌兹别克斯坦(UZ)", + "Holy See": "梵蒂冈(VA)", + "Saint Vincent and the Grenadines": "圣文森特和格林纳丁斯(VC)", + "Venezuela": "委内瑞拉(VE)", + "Virgin Islands, British": "英属维尔京群岛(VG)", + "Virgin Islands, U.S.": "美属维尔京群岛(VI)", + "Viet Nam": "越南(VN)", + "Vanuatu": "瓦努阿图(VU)", + "Wallis and Futuna": "瓦利斯和富图纳(WF)", + "Samoa": "萨摩亚(WS)", + "Yemen": "也门(YE)", + "Mayotte": "马约特(YT)", + "South Africa": "南非(ZA)", + "Zambia": "赞比亚(ZM)", + "Zimbabwe": "津巴布韦(ZW)", +} diff --git a/model/proxy.go b/model/proxy.go new file mode 100644 index 0000000..17e5b20 --- /dev/null +++ b/model/proxy.go @@ -0,0 +1,83 @@ +package model + +type PluginOptsStruct struct { + Mode string `yaml:"mode"` +} + +type SmuxStruct struct { + Enabled bool `yaml:"enable"` +} + +type HeaderStruct struct { + Host string `yaml:"Host"` +} + +type WSOptsStruct struct { + Path string `yaml:"path,omitempty"` + Headers HeaderStruct `yaml:"headers,omitempty"` + MaxEarlyData int `yaml:"max-early-data,omitempty"` + EarlyDataHeaderName string `yaml:"early-data-header-name,omitempty"` +} + +type Vmess struct { + V string `json:"v"` + Ps string `json:"ps"` + Add string `json:"add"` + Port string `json:"port"` + Id string `json:"id"` + Aid string `json:"aid"` + Scy string `json:"scy"` + Net string `json:"net"` + Type string `json:"type"` + Host string `json:"host"` + Path string `json:"path"` + Tls string `json:"tls"` + Sni string `json:"sni"` + Alpn string `json:"alpn"` + Fp string `json:"fp"` +} + +type GRPCOptsStruct struct { + GRPCServiceName string `yaml:"grpc-service-name,omitempty"` +} + +type RealityOptsStruct struct { + PublicKey string `yaml:"public-key,omitempty"` + ShortId string `yaml:"short-id,omitempty"` +} + +type Proxy struct { + Name string `yaml:"name,omitempty"` + Server string `yaml:"server,omitempty"` + Port int `yaml:"port,omitempty"` + Type string `yaml:"type,omitempty"` + Cipher string `yaml:"cipher,omitempty"` + Password string `yaml:"password,omitempty"` + UDP bool `yaml:"udp,omitempty"` + UUID string `yaml:"uuid,omitempty"` + Network string `yaml:"network,omitempty"` + Flow string `yaml:"flow,omitempty"` + TLS bool `yaml:"tls,omitempty"` + ClientFingerprint string `yaml:"client-fingerprint,omitempty"` + UdpOverTcp bool `yaml:"udp-over-tcp,omitempty"` + UdpOverTcpVersion string `yaml:"udp-over-tcp-version,omitempty"` + Plugin string `yaml:"plugin,omitempty"` + PluginOpts PluginOptsStruct `yaml:"plugin-opts,omitempty"` + Smux SmuxStruct `yaml:"smux,omitempty"` + Sni string `yaml:"sni,omitempty"` + AllowInsecure bool `yaml:"allow-insecure,omitempty"` + Fingerprint string `yaml:"fingerprint,omitempty"` + SkipCertVerify bool `yaml:"skip-cert-verify,omitempty"` + Alpn []string `yaml:"alpn,omitempty"` + XUDP bool `yaml:"xudp,omitempty"` + Servername string `yaml:"servername,omitempty"` + WSOpts WSOptsStruct `yaml:"ws-opts,omitempty"` + AlterID string `yaml:"alterId,omitempty"` + GRPCOpts GRPCOptsStruct `yaml:"grpc-opts,omitempty"` + RealityOpts RealityOptsStruct `yaml:"reality-opts,omitempty"` + Protocol string `yaml:"protocol,omitempty"` + Obfs string `yaml:"obfs,omitempty"` + ObfsParam string `yaml:"obfs-param,omitempty"` + ProtocolParam string `yaml:"protocol-param,omitempty"` + Remarks []string `yaml:"remarks,omitempty"` +} diff --git a/model/sub.go b/model/sub.go new file mode 100644 index 0000000..9308b62 --- /dev/null +++ b/model/sub.go @@ -0,0 +1,32 @@ +package model + +type Subscription struct { + Port int `yaml:"port,omitempty"` + SocksPort int `yaml:"socks-port,omitempty"` + AllowLan bool `yaml:"allow-lan,omitempty"` + Mode string `yaml:"mode,omitempty"` + LogLevel string `yaml:"log-level,omitempty"` + ExternalController string `yaml:"external-controller,omitempty"` + Proxies []Proxy `yaml:"proxies,omitempty"` + ProxyGroups []ProxyGroup `yaml:"proxy-groups,omitempty"` + Rules []string `yaml:"rules,omitempty"` + RuleProviders map[string]RuleProvider `yaml:"rule-providers,omitempty,omitempty"` +} + +type ProxyGroup struct { + Name string `yaml:"name,omitempty"` + Type string `yaml:"type,omitempty"` + Proxies []string `yaml:"proxies,omitempty"` +} + +type RuleProvider struct { + Type string `yaml:"type,omitempty"` + Behavior string `yaml:"behavior,omitempty"` + URL string `yaml:"url,omitempty"` + Path string `yaml:"path,omitempty"` + Interval int `yaml:"interval,omitempty"` +} + +type Payload struct { + Rules []string `yaml:"payload,omitempty"` +} diff --git a/parser/base64.go b/parser/base64.go new file mode 100644 index 0000000..4909d04 --- /dev/null +++ b/parser/base64.go @@ -0,0 +1,13 @@ +package parser + +import ( + "encoding/base64" +) + +func DecodeBase64(s string) (string, error) { + decodeStr, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return "", err + } + return string(decodeStr), nil +} diff --git a/parser/ss.go b/parser/ss.go new file mode 100644 index 0000000..15c34f2 --- /dev/null +++ b/parser/ss.go @@ -0,0 +1,67 @@ +package parser + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "sub/model" +) + +// ParseSS 解析 SS(Shadowsocks)URL +func ParseSS(proxy string) (model.Proxy, error) { + // 判断是否以 ss:// 开头 + if !strings.HasPrefix(proxy, "ss://") { + return model.Proxy{}, fmt.Errorf("无效的 ss URL") + } + // 分割 + parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) + if len(parts) != 2 { + return model.Proxy{}, fmt.Errorf("无效的 ss URL") + } + if !strings.Contains(parts[0], ":") { + // 解码 + decoded, err := DecodeBase64(parts[0]) + if err != nil { + return model.Proxy{}, err + } + parts[0] = decoded + } + credentials := strings.SplitN(parts[0], ":", 2) + if len(credentials) != 2 { + return model.Proxy{}, fmt.Errorf("无效的 ss 凭证") + } + // 分割 + serverInfo := strings.SplitN(parts[1], "#", 2) + serverAndPort := strings.SplitN(serverInfo[0], ":", 2) + if len(serverAndPort) != 2 { + return model.Proxy{}, fmt.Errorf("无效的 ss 服务器和端口") + } + // 转换端口字符串为数字 + port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) + if err != nil { + return model.Proxy{}, err + } + // 返回结果 + result := model.Proxy{ + Type: "ss", + Cipher: strings.TrimSpace(credentials[0]), + Password: strings.TrimSpace(credentials[1]), + Server: strings.TrimSpace(serverAndPort[0]), + Port: port, + UDP: true, + Name: serverAndPort[0], + } + // 如果有节点名称 + if len(serverInfo) == 2 { + unescape, err := url.QueryUnescape(serverInfo[1]) + if err != nil { + return model.Proxy{}, err + } + result.Name = strings.TrimSpace(unescape) + } else { + result.Name = strings.TrimSpace(serverAndPort[0]) + } + + return result, nil +} diff --git a/parser/ssr.go b/parser/ssr.go new file mode 100644 index 0000000..3312677 --- /dev/null +++ b/parser/ssr.go @@ -0,0 +1,47 @@ +package parser + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "sub/model" +) + +func ParseShadowsocksR(proxy string) (model.Proxy, error) { + // 判断是否以 ssr:// 开头 + if !strings.HasPrefix(proxy, "ssr://") { + return model.Proxy{}, fmt.Errorf("无效的 ssr URL") + } + var err error + if !strings.Contains(proxy, ":") { + proxy, err = DecodeBase64(strings.TrimPrefix(proxy, "ssr://")) + if err != nil { + return model.Proxy{}, err + } + } + // 分割 + detailsAndParams := strings.SplitN(strings.TrimPrefix(proxy, "ssr://"), "/?", 2) + parts := strings.Split(detailsAndParams[0], ":") + params, err := url.ParseQuery(detailsAndParams[1]) + if err != nil { + return model.Proxy{}, err + } + // 处理端口 + port, err := strconv.Atoi(parts[1]) + if err != nil { + return model.Proxy{}, err + } + result := model.Proxy{ + Type: "ssr", + Server: parts[0], + Port: port, + Protocol: parts[2], + Cipher: parts[3], + Obfs: parts[4], + Password: parts[5], + ObfsParam: params.Get("obfsparam"), + ProtocolParam: params.Get("protoparam"), + } + return result, nil +} diff --git a/parser/trojan.go b/parser/trojan.go new file mode 100644 index 0000000..e1ebec3 --- /dev/null +++ b/parser/trojan.go @@ -0,0 +1,53 @@ +package parser + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "sub/model" +) + +func ParseTrojan(proxy string) (model.Proxy, error) { + // 判断是否以 trojan:// 开头 + if !strings.HasPrefix(proxy, "trojan://") { + return model.Proxy{}, fmt.Errorf("无效的 trojan URL") + } + // 分割 + parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) + if len(parts) != 2 { + return model.Proxy{}, fmt.Errorf("无效的 trojan URL") + } + // 分割 + serverInfo := strings.SplitN(parts[1], "#", 2) + serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + params, err := url.ParseQuery(serverAndPortAndParams[1]) + if err != nil { + return model.Proxy{}, err + } + if len(serverAndPort) != 2 { + return model.Proxy{}, fmt.Errorf("无效的 trojan 服务器和端口") + } + // 处理端口 + port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) + if err != nil { + return model.Proxy{}, err + } + // 返回结果 + result := model.Proxy{ + Type: "trojan", + Server: strings.TrimSpace(serverAndPort[0]), + Port: port, + UDP: true, + Password: strings.TrimSpace(parts[0]), + Sni: params.Get("sni"), + } + // 如果有节点名称 + if len(serverInfo) == 2 { + result.Name, _ = url.QueryUnescape(strings.TrimSpace(serverInfo[1])) + } else { + result.Name = serverAndPort[0] + } + return result, nil +} diff --git a/parser/vless.go b/parser/vless.go new file mode 100644 index 0000000..2163a28 --- /dev/null +++ b/parser/vless.go @@ -0,0 +1,75 @@ +package parser + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "sub/model" +) + +func ParseVless(proxy string) (model.Proxy, error) { + // 判断是否以 vless:// 开头 + if !strings.HasPrefix(proxy, "vless://") { + return model.Proxy{}, fmt.Errorf("无效的 vless URL") + } + // 分割 + parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) + if len(parts) != 2 { + return model.Proxy{}, fmt.Errorf("无效的 vless URL") + } + // 分割 + serverInfo := strings.SplitN(parts[1], "#", 2) + serverAndPortAndParams := strings.SplitN(serverInfo[0], "?", 2) + serverAndPort := strings.SplitN(serverAndPortAndParams[0], ":", 2) + params, err := url.ParseQuery(serverAndPortAndParams[1]) + if err != nil { + return model.Proxy{}, err + } + if len(serverAndPort) != 2 { + return model.Proxy{}, fmt.Errorf("无效的 vless 服务器和端口") + } + // 处理端口 + port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) + if err != nil { + return model.Proxy{}, err + } + // 返回结果 + result := model.Proxy{ + Type: "vless", + Server: strings.TrimSpace(serverAndPort[0]), + Port: port, + UUID: strings.TrimSpace(parts[0]), + UDP: true, + Sni: params.Get("sni"), + Network: params.Get("type"), + TLS: params.Get("security") == "tls", + Flow: params.Get("flow"), + Fingerprint: params.Get("fp"), + Alpn: strings.Split(params.Get("alpn"), ","), + Servername: params.Get("sni"), + WSOpts: model.WSOptsStruct{ + Path: params.Get("path"), + Headers: model.HeaderStruct{ + Host: params.Get("host"), + }, + }, + GRPCOpts: model.GRPCOptsStruct{ + GRPCServiceName: params.Get("serviceName"), + }, + RealityOpts: model.RealityOptsStruct{ + PublicKey: params.Get("pbk"), + }, + } + // 如果有节点名称 + if len(serverInfo) == 2 { + if strings.Contains(serverInfo[1], "|") { + result.Name = strings.SplitN(serverInfo[1], "|", 2)[1] + } else { + result.Name = serverInfo[1] + } + } else { + result.Name = serverAndPort[0] + } + return result, nil +} diff --git a/parser/vmess.go b/parser/vmess.go new file mode 100644 index 0000000..2d58bfe --- /dev/null +++ b/parser/vmess.go @@ -0,0 +1,68 @@ +package parser + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + "sub/model" +) + +// vmess://eyJ2IjoiMiIsInBzIjoiXHU4MmYxXHU1NmZkLVx1NGYxOFx1NTMxNjMiLCJhZGQiOiJ0LmNjY2FvLmN5b3UiLCJwb3J0IjoiMTY2NDUiLCJpZCI6ImVmNmNiMGY0LTcwZWYtNDY2ZS04NGUwLWRiNDQwMWRmNmZhZiIsImFpZCI6IjAiLCJuZXQiOiJ3cyIsInR5cGUiOiJub25lIiwiaG9zdCI6IiIsInBhdGgiOiIiLCJ0bHMiOiIifQ== + +func ParseVmess(proxy string) (model.Proxy, error) { + // 判断是否以 vmess:// 开头 + if !strings.HasPrefix(proxy, "vmess://") { + return model.Proxy{}, fmt.Errorf("无效的 vmess URL") + } + // 解码 + base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) + if err != nil { + return model.Proxy{}, errors.New("无效的 vmess URL") + } + // 解析 + var vmess model.Vmess + err = json.Unmarshal([]byte(base64), &vmess) + if err != nil { + return model.Proxy{}, errors.New("无效的 vmess URL") + } + // 处理端口 + port, err := strconv.Atoi(strings.TrimSpace(vmess.Port)) + if err != nil { + return model.Proxy{}, errors.New("无效的 vmess URL") + } + if vmess.Scy == "" { + vmess.Scy = "auto" + } + if vmess.Net == "ws" && vmess.Path == "" { + vmess.Path = "/" + } + if vmess.Net == "ws" && vmess.Host == "" { + vmess.Host = vmess.Add + } + // 返回结果 + result := model.Proxy{ + Name: vmess.Ps, + Type: "vmess", + Server: vmess.Add, + Port: port, + UUID: vmess.Id, + AlterID: vmess.Aid, + Cipher: vmess.Scy, + UDP: true, + TLS: vmess.Tls == "tls", + Fingerprint: vmess.Fp, + ClientFingerprint: "chrome", + SkipCertVerify: true, + Servername: vmess.Add, + Network: vmess.Net, + WSOpts: model.WSOptsStruct{ + Path: vmess.Path, + Headers: model.HeaderStruct{ + Host: vmess.Host, + }, + }, + } + return result, nil +} diff --git a/utils/get.go b/utils/get.go new file mode 100644 index 0000000..73dc5cd --- /dev/null +++ b/utils/get.go @@ -0,0 +1,23 @@ +package utils + +import ( + "net/http" + "time" +) + +func GetWithRetry(url string) (resp *http.Response, err error) { + retryTimes := 3 + haveTried := 0 + retryDelay := time.Second // 延迟1秒再重试 + for haveTried < retryTimes { + get, err := http.Get(url) + if err != nil { + haveTried++ + time.Sleep(retryDelay) + continue + } else { + return get, nil + } + } + return nil, err +} diff --git a/utils/proxy.go b/utils/proxy.go new file mode 100644 index 0000000..6975155 --- /dev/null +++ b/utils/proxy.go @@ -0,0 +1,105 @@ +package utils + +import ( + "sort" + "strings" + "sub/model" + "sub/parser" +) + +func GetContryCode(proxy model.Proxy) string { + keys := make([]string, 0, len(model.CountryKeywords)) + for k := range model.CountryKeywords { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + if strings.Contains(strings.ToLower(proxy.Name), strings.ToLower(k)) { + return model.CountryKeywords[k] + } + } + return "其他地区" +} + +var skipGroups = map[string]bool{ + "手动切换": true, + "全球直连": true, + "广告拦截": true, + "应用进化": true, +} + +func AddProxy(sub *model.Subscription, proxies ...model.Proxy) { + newContryNames := make([]string, 0, len(proxies)) + for p := range proxies { + proxy := proxies[p] + sub.Proxies = append(sub.Proxies, proxy) + haveProxyGroup := false + for i := range sub.ProxyGroups { + group := &sub.ProxyGroups[i] + groupName := []rune(group.Name) + proxyName := []rune(proxy.Name) + + if string(groupName[:2]) == string(proxyName[:2]) || GetContryCode(proxy) == group.Name { + group.Proxies = append(group.Proxies, proxy.Name) + haveProxyGroup = true + } + + if group.Name == "手动切换" { + group.Proxies = append(group.Proxies, proxy.Name) + } + } + if !haveProxyGroup { + contryCode := GetContryCode(proxy) + newGroup := model.ProxyGroup{ + Name: contryCode, + Type: "select", + Proxies: []string{proxy.Name}, + } + newContryNames = append(newContryNames, contryCode) + sub.ProxyGroups = append(sub.ProxyGroups, newGroup) + } + } + newContryNamesMap := make(map[string]bool) + for _, n := range newContryNames { + newContryNamesMap[n] = true + } + for i := range sub.ProxyGroups { + if !skipGroups[sub.ProxyGroups[i].Name] && !newContryNamesMap[sub.ProxyGroups[i].Name] { + newProxies := make( + []string, len(newContryNames), len(newContryNames)+len(sub.ProxyGroups[i].Proxies), + ) + copy(newProxies, newContryNames) + sub.ProxyGroups[i].Proxies = append(newProxies, sub.ProxyGroups[i].Proxies...) + } + } +} + +func ParseProxy(proxies ...string) []model.Proxy { + var result []model.Proxy + for _, proxy := range proxies { + if proxy != "" { + var proxyItem model.Proxy + var err error + // 解析节点 + if strings.HasPrefix(proxy, "ss://") { + proxyItem, err = parser.ParseSS(proxy) + } + if strings.HasPrefix(proxy, "trojan://") { + proxyItem, err = parser.ParseTrojan(proxy) + } + if strings.HasPrefix(proxy, "vmess://") { + proxyItem, err = parser.ParseVmess(proxy) + } + if strings.HasPrefix(proxy, "vless://") { + proxyItem, err = parser.ParseVless(proxy) + } + if strings.HasPrefix(proxy, "ssr://") { + proxyItem, err = parser.ParseShadowsocksR(proxy) + } + if err == nil { + result = append(result, proxyItem) + } + } + } + return result +} diff --git a/utils/rule.go b/utils/rule.go new file mode 100644 index 0000000..9e9f42b --- /dev/null +++ b/utils/rule.go @@ -0,0 +1,54 @@ +package utils + +import ( + "fmt" + "gopkg.in/yaml.v3" + "io" + "sub/model" +) + +func AddRulesByUrl(sub *model.Subscription, url string, proxy string) { + get, err := GetWithRetry(url) + if err != nil { + fmt.Println(err) + return + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + fmt.Println(err) + } + }(get.Body) + bytes, err := io.ReadAll(get.Body) + if err != nil { + fmt.Println(err) + return + } + var payload model.Payload + err = yaml.Unmarshal(bytes, &payload) + if err != nil { + fmt.Println(err) + return + } + for i := range payload.Rules { + payload.Rules[i] = payload.Rules[i] + "," + proxy + } + AddRules(sub, payload.Rules...) +} + +func AddRuleProvider( + sub *model.Subscription, providerName string, proxy string, provider model.RuleProvider, +) { + if sub.RuleProviders == nil { + sub.RuleProviders = make(map[string]model.RuleProvider) + } + sub.RuleProviders[providerName] = provider + AddRules( + sub, + fmt.Sprintf("RULE-SET,%s,%s", providerName, proxy), + ) +} + +func AddRules(sub *model.Subscription, rules ...string) { + sub.Rules = append(rules, sub.Rules...) +} diff --git a/utils/sub.go b/utils/sub.go new file mode 100644 index 0000000..15eb3d3 --- /dev/null +++ b/utils/sub.go @@ -0,0 +1,85 @@ +package utils + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "os" + "path/filepath" + "time" +) + +var subsDir = "subs" + +func LoadSubscription(url string, refresh bool) ([]byte, error) { + if refresh { + return FetchSubscriptionFromAPI(url) + } + hash := md5.Sum([]byte(url)) + fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:])+".yaml") + const refreshInterval = 5 * 60 // 5分钟 + stat, err := os.Stat(fileName) + if err != nil { + if !os.IsNotExist(err) { + return nil, err + } + return FetchSubscriptionFromAPI(url) + } + lastGetTime := stat.ModTime().Unix() // 单位是秒 + if lastGetTime+refreshInterval > time.Now().Unix() { + file, err := os.Open(fileName) + if err != nil { + return nil, err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + fmt.Println(err) + } + }(file) + subContent, err := io.ReadAll(file) + if err != nil { + return nil, err + } + return subContent, nil + } + return FetchSubscriptionFromAPI(url) +} + +func FetchSubscriptionFromAPI(url string) ([]byte, error) { + hash := md5.Sum([]byte(url)) + fileName := filepath.Join(subsDir, hex.EncodeToString(hash[:])+".yaml") + resp, err := GetWithRetry(url) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + fmt.Println(err) + } + }(resp.Body) + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + file, err := os.Create(fileName) + if err != nil { + return nil, err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + fmt.Println(err) + } + }(file) + _, err = file.Write(data) + if err != nil { + return nil, fmt.Errorf("failed to write to sub.yaml: %w", err) + } + if err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml: %w", err) + } + return data, nil +} diff --git a/utils/template.go b/utils/template.go new file mode 100644 index 0000000..20e53c0 --- /dev/null +++ b/utils/template.go @@ -0,0 +1,36 @@ +package utils + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" +) + +// LoadTemplate 加载模板 +// template 模板文件名 +func LoadTemplate(template string) (string, error) { + tPath := filepath.Join("templates", template) + if _, err := os.Stat(tPath); err == nil { + file, err := os.Open(tPath) + if err != nil { + return "", err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + fmt.Println(err) + } + }(file) + result, err := io.ReadAll(file) + if err != nil { + return "", err + } + if err != nil { + return "", err + } + return string(result), nil + } + return "", errors.New("模板文件不存在") +} diff --git a/validator/sub.go b/validator/sub.go new file mode 100644 index 0000000..71d0219 --- /dev/null +++ b/validator/sub.go @@ -0,0 +1,7 @@ +package validator + +type SubQuery struct { + Sub string `form:"sub" json:"name" binding:"required"` + Mix bool `form:"mix,default=false" json:"email" binding:""` + Refresh bool `form:"refresh,default=false" json:"age" binding:""` +}