软件功能基本开发完成,内测BUG等待修复
This commit is contained in:
@@ -157,6 +157,7 @@ func getAllConfigMappings() gin.H {
|
||||
"default_remind_traffic": service.MustGetBool("default_remind_traffic", true),
|
||||
"subscribe_path": service.MustGetString("subscribe_path", "s"),
|
||||
},
|
||||
"subscribe_template": service.GetAllSubscribeTemplates(),
|
||||
"frontend": gin.H{
|
||||
"frontend_theme": service.MustGetString("frontend_theme", "Xboard"),
|
||||
"frontend_theme_sidebar": service.MustGetString("frontend_theme_sidebar", "light"),
|
||||
@@ -202,7 +203,7 @@ func getAllConfigMappings() gin.H {
|
||||
"nebula_welcome_target": service.MustGetString("nebula_welcome_target", ""),
|
||||
"nebula_register_title": service.MustGetString("nebula_register_title", ""),
|
||||
"nebula_background_url": service.MustGetString("nebula_background_url", ""),
|
||||
"nebula_metrics_base_url": service.MustGetString("nebula_metrics_base_url", ""),
|
||||
"nebula_metrics_base_url": service.MustGetString("nebula_metrics_base_url", ""),
|
||||
"nebula_default_theme_mode": service.MustGetString("nebula_default_theme_mode", "system"),
|
||||
"nebula_light_logo_url": service.MustGetString("nebula_light_logo_url", ""),
|
||||
"nebula_dark_logo_url": service.MustGetString("nebula_dark_logo_url", ""),
|
||||
@@ -320,6 +321,9 @@ func settingGroupName(name string) string {
|
||||
"renew_order_event_id", "change_order_event_id", "show_info_to_server_enable",
|
||||
"show_protocol_to_server_enable", "default_remind_expire", "default_remind_traffic", "subscribe_path":
|
||||
return "subscribe"
|
||||
case "subscribe_template_singbox", "subscribe_template_clash", "subscribe_template_clashmeta",
|
||||
"subscribe_template_stash", "subscribe_template_surge", "subscribe_template_surfboard":
|
||||
return "subscribe_template"
|
||||
case "frontend_theme", "frontend_theme_sidebar", "frontend_theme_header", "frontend_theme_color", "frontend_background_url":
|
||||
return "frontend"
|
||||
case "server_token", "server_pull_interval", "server_push_interval", "device_limit_mode", "server_ws_enable", "server_ws_url":
|
||||
|
||||
@@ -579,8 +579,13 @@ func AdminUsersFetch(c *gin.Context) {
|
||||
page := parsePositiveInt(firstString(params["page"], params["current"]), 1)
|
||||
perPage := parsePositiveInt(firstString(params["per_page"], params["pageSize"]), 50)
|
||||
keyword := strings.TrimSpace(params["keyword"])
|
||||
suffix := service.GetPluginConfigString(service.PluginUserAddIPv6, "email_suffix", "-ipv6")
|
||||
shadowPattern := "%" + suffix + "@%"
|
||||
|
||||
query := database.DB.Model(&model.User{}).Preload("Plan").Order("id DESC")
|
||||
query := database.DB.Model(&model.User{}).
|
||||
Preload("Plan").
|
||||
Where("email NOT LIKE ?", shadowPattern).
|
||||
Order("id DESC")
|
||||
if keyword != "" {
|
||||
query = query.Where("email LIKE ? OR CAST(id AS CHAR) = ?", "%"+keyword+"%", keyword)
|
||||
}
|
||||
|
||||
@@ -39,16 +39,37 @@ func FulfillSubscription(c *gin.Context, user *model.User) {
|
||||
|
||||
ua := c.GetHeader("User-Agent")
|
||||
flag := c.Query("flag")
|
||||
uaLower := strings.ToLower(ua)
|
||||
|
||||
// 2. Handle specialized configs
|
||||
if strings.Contains(ua, "Clash") || flag == "clash" {
|
||||
config, _ := protocol.GenerateClash(servers, *user)
|
||||
if strings.Contains(uaLower, "stash") || flag == "stash" {
|
||||
config, _ := protocol.GenerateClashWithTemplate("stash", servers, *user)
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.String(http.StatusOK, config)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(ua), "sing-box") || flag == "sing-box" {
|
||||
if strings.Contains(uaLower, "clashmeta") ||
|
||||
strings.Contains(uaLower, "clash meta") ||
|
||||
strings.Contains(uaLower, "metacubex") ||
|
||||
strings.Contains(uaLower, "verge") ||
|
||||
strings.Contains(uaLower, "flclash") ||
|
||||
strings.Contains(uaLower, "nekobox") ||
|
||||
flag == "clashmeta" || flag == "clash-meta" {
|
||||
config, _ := protocol.GenerateClashWithTemplate("clashmeta", servers, *user)
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.String(http.StatusOK, config)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(ua, "Clash") || flag == "clash" {
|
||||
config, _ := protocol.GenerateClashWithTemplate("clash", servers, *user)
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.String(http.StatusOK, config)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(uaLower, "sing-box") || flag == "sing-box" {
|
||||
config, _ := protocol.GenerateSingBox(servers, *user)
|
||||
c.Header("Content-Type", "application/json; charset=utf-8")
|
||||
c.String(http.StatusOK, config)
|
||||
@@ -99,7 +120,7 @@ func filterServers(servers []model.Server, types, filter string) []model.Server
|
||||
|
||||
func generateInfoNodes(user *model.User) []string {
|
||||
var nodes []string
|
||||
|
||||
|
||||
// Expire Info
|
||||
expireDate := "长期有效"
|
||||
if user.ExpiredAt != nil {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
"xboard-go/internal/database"
|
||||
@@ -11,6 +13,31 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ipv6StatusPresentation(status string, allowed bool) (string, string, string) {
|
||||
status = strings.TrimSpace(strings.ToLower(status))
|
||||
switch status {
|
||||
case "active":
|
||||
return "active", "IPv6 enabled", ""
|
||||
case "eligible":
|
||||
return "eligible", "Ready to enable", ""
|
||||
case "", "not_allowed", "not_eligible", "disallowed":
|
||||
if allowed {
|
||||
return "eligible", "Ready to enable", ""
|
||||
}
|
||||
return "not_allowed", "Not eligible", "Current plan or group is not allowed to enable IPv6"
|
||||
default:
|
||||
label := strings.ReplaceAll(strings.Title(strings.ReplaceAll(status, "_", " ")), "Ipv6", "IPv6")
|
||||
if strings.EqualFold(label, "Not Allowed") {
|
||||
label = "Not eligible"
|
||||
}
|
||||
reason := ""
|
||||
if !allowed && (status == "not_allowed" || status == "not_eligible") {
|
||||
reason = "Current plan or group is not allowed to enable IPv6"
|
||||
}
|
||||
return status, label, reason
|
||||
}
|
||||
}
|
||||
|
||||
func PluginUserOnlineDevicesUsers(c *gin.Context) {
|
||||
|
||||
page := parsePositiveInt(c.DefaultQuery("page", "1"), 1)
|
||||
@@ -165,7 +192,7 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
||||
shadowByParentID := make(map[int]model.User, len(userIDs))
|
||||
if len(userIDs) > 0 {
|
||||
var shadowUsers []model.User
|
||||
if err := database.DB.Where("parent_id IN ?", userIDs).Find(&shadowUsers).Error; err == nil {
|
||||
if err := database.DB.Preload("Plan").Where("parent_id IN ?", userIDs).Find(&shadowUsers).Error; err == nil {
|
||||
for _, shadow := range shadowUsers {
|
||||
if shadow.ParentID != nil {
|
||||
shadowByParentID[*shadow.ParentID] = shadow
|
||||
@@ -173,6 +200,17 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
shadowPlanID := parsePositiveInt(
|
||||
service.GetPluginConfigString(service.PluginUserAddIPv6, "ipv6_plan_id", "0"),
|
||||
0,
|
||||
)
|
||||
planNames := make(map[int]string)
|
||||
var plans []model.Plan
|
||||
if err := database.DB.Select("id", "name").Find(&plans).Error; err == nil {
|
||||
for _, plan := range plans {
|
||||
planNames[plan.ID] = plan.Name
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]gin.H, 0, len(users))
|
||||
for _, user := range users {
|
||||
@@ -191,32 +229,30 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
||||
}
|
||||
allowed := service.PluginUserAllowed(&user, user.Plan)
|
||||
status := "not_allowed"
|
||||
statusLabel := "Not eligible"
|
||||
if allowed {
|
||||
status = "eligible"
|
||||
statusLabel = "Ready to enable"
|
||||
planID := shadowPlanID
|
||||
planNameValue := planNames[shadowPlanID]
|
||||
if hasShadowUser {
|
||||
planID = intFromPointer(shadowUser.PlanID)
|
||||
planNameValue = planName(shadowUser.Plan)
|
||||
}
|
||||
shadowUserID := 0
|
||||
shadowUpdatedAt := int64(0)
|
||||
if hasSubscription {
|
||||
status = firstString(subscription.Status, status)
|
||||
statusLabel = "IPv6 enabled"
|
||||
if subscription.Status != "active" && subscription.Status != "" {
|
||||
statusLabel = strings.ReplaceAll(strings.Title(strings.ReplaceAll(subscription.Status, "_", " ")), "Ipv6", "IPv6")
|
||||
}
|
||||
if subscription.ShadowUserID != nil {
|
||||
shadowUserID = *subscription.ShadowUserID
|
||||
}
|
||||
shadowUpdatedAt = subscription.UpdatedAt
|
||||
} else if allowed {
|
||||
statusLabel = "Ready to enable"
|
||||
}
|
||||
effectiveAllowed := allowed || hasSubscription && subscription.Allowed
|
||||
status, statusLabel, _ := ipv6StatusPresentation(status, effectiveAllowed)
|
||||
|
||||
list = append(list, gin.H{
|
||||
"id": user.ID,
|
||||
"email": user.Email,
|
||||
"plan_name": planName(user.Plan),
|
||||
"allowed": allowed || hasSubscription && subscription.Allowed,
|
||||
"plan_id": planID,
|
||||
"plan_name": firstString(planNameValue, "-"),
|
||||
"allowed": effectiveAllowed,
|
||||
"is_active": hasSubscription && subscription.Status == "active",
|
||||
"status": status,
|
||||
"status_label": statusLabel,
|
||||
@@ -238,6 +274,71 @@ func AdminIPv6SubscriptionUsers(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func AdminIPv6SubscriptionConfigFetch(c *gin.Context) {
|
||||
cfg := service.GetPluginConfig(service.PluginUserAddIPv6)
|
||||
Success(c, gin.H{
|
||||
"ipv6_plan_id": parsePositiveInt(service.GetPluginConfigString(service.PluginUserAddIPv6, "ipv6_plan_id", "0"), 0),
|
||||
"allowed_plans": service.GetPluginConfigIntList(service.PluginUserAddIPv6, "allowed_plans"),
|
||||
"allowed_groups": service.GetPluginConfigIntList(service.PluginUserAddIPv6, "allowed_groups"),
|
||||
"email_suffix": firstString(stringFromAny(cfg["email_suffix"]), "-ipv6"),
|
||||
"reference_flag": firstString(stringFromAny(cfg["reference_flag"]), "ipv6"),
|
||||
})
|
||||
}
|
||||
|
||||
func AdminIPv6SubscriptionConfigSave(c *gin.Context) {
|
||||
var payload struct {
|
||||
IPv6PlanID int `json:"ipv6_plan_id"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
Fail(c, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
var plugin model.Plugin
|
||||
err := database.DB.Where("code = ?", service.PluginUserAddIPv6).First(&plugin).Error
|
||||
if err != nil {
|
||||
plugin = model.Plugin{
|
||||
Code: service.PluginUserAddIPv6,
|
||||
Name: "IPv6 Subscription",
|
||||
IsEnabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
cfg := service.GetPluginConfig(service.PluginUserAddIPv6)
|
||||
if payload.IPv6PlanID > 0 {
|
||||
cfg["ipv6_plan_id"] = payload.IPv6PlanID
|
||||
} else {
|
||||
delete(cfg, "ipv6_plan_id")
|
||||
}
|
||||
|
||||
raw, marshalErr := json.Marshal(cfg)
|
||||
if marshalErr != nil {
|
||||
Fail(c, http.StatusInternalServerError, "failed to encode plugin config")
|
||||
return
|
||||
}
|
||||
configText := string(raw)
|
||||
plugin.Config = &configText
|
||||
|
||||
if plugin.ID > 0 {
|
||||
if updateErr := database.DB.Model(&model.Plugin{}).Where("id = ?", plugin.ID).Updates(map[string]any{
|
||||
"config": plugin.Config,
|
||||
"is_enabled": plugin.IsEnabled,
|
||||
}).Error; updateErr != nil {
|
||||
Fail(c, http.StatusInternalServerError, "failed to save plugin config")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if createErr := database.DB.Create(&plugin).Error; createErr != nil {
|
||||
Fail(c, http.StatusInternalServerError, "failed to create plugin config")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Success(c, gin.H{
|
||||
"ipv6_plan_id": payload.IPv6PlanID,
|
||||
})
|
||||
}
|
||||
|
||||
func AdminIPv6SubscriptionEnable(c *gin.Context) {
|
||||
userID := parsePositiveInt(c.Param("userId"), 0)
|
||||
if userID == 0 {
|
||||
@@ -346,24 +447,14 @@ func PluginUserAddIPv6Check(c *gin.Context) {
|
||||
hasSubscription := database.DB.Where("user_id = ?", user.ID).First(&subscription).Error == nil
|
||||
allowed := service.PluginUserAllowed(user, plan)
|
||||
status := "not_allowed"
|
||||
statusLabel := "Not eligible"
|
||||
reason := "Current plan or group is not allowed to enable IPv6"
|
||||
if allowed {
|
||||
status = "eligible"
|
||||
statusLabel = "Ready to enable"
|
||||
reason = ""
|
||||
}
|
||||
if hasSubscription {
|
||||
status = firstString(subscription.Status, "active")
|
||||
statusLabel = "IPv6 enabled"
|
||||
reason = ""
|
||||
if subscription.Status != "active" && subscription.Status != "" {
|
||||
statusLabel = strings.ReplaceAll(strings.Title(strings.ReplaceAll(subscription.Status, "_", " ")), "Ipv6", "IPv6")
|
||||
}
|
||||
}
|
||||
effectiveAllowed := allowed || hasSubscription && subscription.Allowed
|
||||
status, statusLabel, reason := ipv6StatusPresentation(status, effectiveAllowed)
|
||||
|
||||
Success(c, gin.H{
|
||||
"allowed": allowed || hasSubscription && subscription.Allowed,
|
||||
"allowed": effectiveAllowed,
|
||||
"is_active": hasSubscription && subscription.Status == "active",
|
||||
"status": status,
|
||||
"status_label": statusLabel,
|
||||
|
||||
@@ -5,37 +5,195 @@ import (
|
||||
"strings"
|
||||
"xboard-go/internal/model"
|
||||
"xboard-go/internal/service"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
func GenerateClash(servers []model.Server, user model.User) (string, error) {
|
||||
return GenerateClashWithTemplate("clash", servers, user)
|
||||
}
|
||||
|
||||
func GenerateClashWithTemplate(templateName string, servers []model.Server, user model.User) (string, error) {
|
||||
template := strings.TrimSpace(service.GetSubscribeTemplate(templateName))
|
||||
if template == "" {
|
||||
return generateClashFallback(servers, user), nil
|
||||
}
|
||||
|
||||
var config map[string]any
|
||||
if err := yaml.Unmarshal([]byte(template), &config); err != nil {
|
||||
return generateClashFallback(servers, user), nil
|
||||
}
|
||||
|
||||
proxies := make([]any, 0, len(servers))
|
||||
proxyNames := make([]string, 0, len(servers))
|
||||
for _, s := range servers {
|
||||
conf := service.BuildNodeConfig(&s)
|
||||
proxy := buildClashProxy(conf, user)
|
||||
if proxy == nil {
|
||||
continue
|
||||
}
|
||||
proxies = append(proxies, proxy)
|
||||
proxyNames = append(proxyNames, conf.Name)
|
||||
}
|
||||
|
||||
config["proxies"] = append(anySlice(config["proxies"]), proxies...)
|
||||
config["proxy-groups"] = mergeClashProxyGroups(anySlice(config["proxy-groups"]), proxyNames)
|
||||
|
||||
if _, ok := config["rules"]; !ok {
|
||||
config["rules"] = []any{"MATCH,Proxy"}
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := strings.ReplaceAll(string(data), "$app_name", service.MustGetString("app_name", "XBoard"))
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func buildClashProxy(conf service.NodeServerConfig, user model.User) map[string]any {
|
||||
switch conf.Protocol {
|
||||
case "shadowsocks":
|
||||
cipher, _ := conf.Cipher.(string)
|
||||
return map[string]any{
|
||||
"name": conf.Name,
|
||||
"type": "ss",
|
||||
"server": conf.RawHost,
|
||||
"port": conf.ServerPort,
|
||||
"cipher": cipher,
|
||||
"password": user.UUID,
|
||||
}
|
||||
case "vmess":
|
||||
return map[string]any{
|
||||
"name": conf.Name,
|
||||
"type": "vmess",
|
||||
"server": conf.RawHost,
|
||||
"port": conf.ServerPort,
|
||||
"uuid": user.UUID,
|
||||
"alterId": 0,
|
||||
"cipher": "auto",
|
||||
"udp": true,
|
||||
}
|
||||
case "trojan":
|
||||
return map[string]any{
|
||||
"name": conf.Name,
|
||||
"type": "trojan",
|
||||
"server": conf.RawHost,
|
||||
"port": conf.ServerPort,
|
||||
"password": user.UUID,
|
||||
"udp": true,
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func mergeClashProxyGroups(groups []any, proxyNames []string) []any {
|
||||
if len(groups) == 0 {
|
||||
return []any{
|
||||
map[string]any{
|
||||
"name": "Proxy",
|
||||
"type": "select",
|
||||
"proxies": append([]any{"DIRECT"}, stringsToAny(proxyNames)...),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
for index, item := range groups {
|
||||
group, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
group["proxies"] = appendUniqueAny(anySlice(group["proxies"]), stringsToAny(proxyNames)...)
|
||||
groups[index] = group
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func anySlice(value any) []any {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return []any{}
|
||||
case []any:
|
||||
return append([]any{}, typed...)
|
||||
case []string:
|
||||
result := make([]any, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return []any{}
|
||||
}
|
||||
}
|
||||
|
||||
func stringsToAny(values []string) []any {
|
||||
result := make([]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func appendUniqueAny(base []any, values ...any) []any {
|
||||
existing := make(map[string]struct{}, len(base))
|
||||
for _, item := range base {
|
||||
existing[fmt.Sprint(item)] = struct{}{}
|
||||
}
|
||||
|
||||
for _, item := range values {
|
||||
key := fmt.Sprint(item)
|
||||
if _, ok := existing[key]; ok {
|
||||
continue
|
||||
}
|
||||
base = append(base, item)
|
||||
existing[key] = struct{}{}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func generateClashFallback(servers []model.Server, user model.User) string {
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString("proxies:\n")
|
||||
var proxyNames []string
|
||||
for _, s := range servers {
|
||||
conf := service.BuildNodeConfig(&s)
|
||||
proxy := ""
|
||||
|
||||
switch conf.Protocol {
|
||||
case "shadowsocks":
|
||||
cipher := ""
|
||||
if c, ok := conf.Cipher.(string); ok { cipher = c }
|
||||
proxy = fmt.Sprintf(" - name: \"%s\"\n type: ss\n server: %s\n port: %d\n cipher: %s\n password: %s\n",
|
||||
conf.Name, conf.RawHost, conf.ServerPort, cipher, user.UUID)
|
||||
case "vmess":
|
||||
proxy = fmt.Sprintf(" - name: \"%s\"\n type: vmess\n server: %s\n port: %d\n uuid: %s\n alterId: 0\n cipher: auto\n udp: true\n",
|
||||
conf.Name, conf.RawHost, conf.ServerPort, user.UUID)
|
||||
case "trojan":
|
||||
proxy = fmt.Sprintf(" - name: \"%s\"\n type: trojan\n server: %s\n port: %d\n password: %s\n udp: true\n",
|
||||
conf.Name, conf.RawHost, conf.ServerPort, user.UUID)
|
||||
default:
|
||||
proxy := buildClashProxy(conf, user)
|
||||
if proxy == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if proxy != "" {
|
||||
builder.WriteString(proxy)
|
||||
proxyNames = append(proxyNames, fmt.Sprintf("\"%s\"", conf.Name))
|
||||
switch proxy["type"] {
|
||||
case "ss":
|
||||
builder.WriteString(fmt.Sprintf(
|
||||
" - name: \"%s\"\n type: ss\n server: %s\n port: %d\n cipher: %s\n password: %s\n",
|
||||
conf.Name,
|
||||
conf.RawHost,
|
||||
conf.ServerPort,
|
||||
fmt.Sprint(proxy["cipher"]),
|
||||
user.UUID,
|
||||
))
|
||||
case "vmess":
|
||||
builder.WriteString(fmt.Sprintf(
|
||||
" - name: \"%s\"\n type: vmess\n server: %s\n port: %d\n uuid: %s\n alterId: 0\n cipher: auto\n udp: true\n",
|
||||
conf.Name,
|
||||
conf.RawHost,
|
||||
conf.ServerPort,
|
||||
user.UUID,
|
||||
))
|
||||
case "trojan":
|
||||
builder.WriteString(fmt.Sprintf(
|
||||
" - name: \"%s\"\n type: trojan\n server: %s\n port: %d\n password: %s\n udp: true\n",
|
||||
conf.Name,
|
||||
conf.RawHost,
|
||||
conf.ServerPort,
|
||||
user.UUID,
|
||||
))
|
||||
}
|
||||
proxyNames = append(proxyNames, fmt.Sprintf("\"%s\"", conf.Name))
|
||||
}
|
||||
|
||||
builder.WriteString("\nproxy-groups:\n")
|
||||
@@ -47,5 +205,5 @@ func GenerateClash(servers []model.Server, user model.User) (string, error) {
|
||||
builder.WriteString("\nrules:\n")
|
||||
builder.WriteString(" - MATCH,Proxy\n")
|
||||
|
||||
return builder.String(), nil
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
@@ -11,6 +11,81 @@ type SingBoxConfig struct {
|
||||
}
|
||||
|
||||
func GenerateSingBox(servers []model.Server, user model.User) (string, error) {
|
||||
template := service.GetSubscribeTemplate("singbox")
|
||||
if template == "" {
|
||||
return generateSingBoxFallback(servers, user)
|
||||
}
|
||||
|
||||
var config map[string]any
|
||||
if err := json.Unmarshal([]byte(template), &config); err != nil {
|
||||
return generateSingBoxFallback(servers, user)
|
||||
}
|
||||
|
||||
outbounds := make([]any, 0, len(servers))
|
||||
proxyTags := make([]string, 0, len(servers))
|
||||
for _, s := range servers {
|
||||
conf := service.BuildNodeConfig(&s)
|
||||
outbound := buildSingBoxOutbound(conf, user)
|
||||
if outbound == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
outbounds = append(outbounds, outbound)
|
||||
proxyTags = append(proxyTags, conf.Name)
|
||||
}
|
||||
|
||||
existingOutbounds := anySlice(config["outbounds"])
|
||||
for index, item := range existingOutbounds {
|
||||
outbound, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
outboundType, _ := outbound["type"].(string)
|
||||
if outboundType != "selector" && outboundType != "urltest" {
|
||||
continue
|
||||
}
|
||||
|
||||
outbound["outbounds"] = appendUniqueAny(anySlice(outbound["outbounds"]), stringsToAny(proxyTags)...)
|
||||
existingOutbounds[index] = outbound
|
||||
}
|
||||
|
||||
config["outbounds"] = append(existingOutbounds, outbounds...)
|
||||
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func buildSingBoxOutbound(conf service.NodeServerConfig, user model.User) map[string]any {
|
||||
outbound := map[string]any{
|
||||
"tag": conf.Name,
|
||||
"server": conf.RawHost,
|
||||
"server_port": conf.ServerPort,
|
||||
}
|
||||
|
||||
switch conf.Protocol {
|
||||
case "shadowsocks":
|
||||
outbound["type"] = "shadowsocks"
|
||||
outbound["method"] = conf.Cipher
|
||||
outbound["password"] = user.UUID
|
||||
case "vmess":
|
||||
outbound["type"] = "vmess"
|
||||
outbound["uuid"] = user.UUID
|
||||
outbound["security"] = "auto"
|
||||
case "trojan":
|
||||
outbound["type"] = "trojan"
|
||||
outbound["password"] = user.UUID
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return outbound
|
||||
}
|
||||
|
||||
func generateSingBoxFallback(servers []model.Server, user model.User) (string, error) {
|
||||
outbounds := []map[string]interface{}{}
|
||||
proxyTags := []string{}
|
||||
|
||||
@@ -42,7 +117,6 @@ func GenerateSingBox(servers []model.Server, user model.User) (string, error) {
|
||||
proxyTags = append(proxyTags, conf.Name)
|
||||
}
|
||||
|
||||
// Add selector
|
||||
selector := map[string]interface{}{
|
||||
"type": "selector",
|
||||
"tag": "Proxy",
|
||||
|
||||
56
internal/service/subscribe_templates.go
Normal file
56
internal/service/subscribe_templates.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var subscribeTemplateFiles = map[string]string{
|
||||
"singbox": filepath.Join("submodule", "singbox.json"),
|
||||
"clash": filepath.Join("submodule", "clash.yaml"),
|
||||
"clashmeta": filepath.Join("submodule", "clash-meta.yaml"),
|
||||
"stash": filepath.Join("submodule", "stash.yaml"),
|
||||
"surge": filepath.Join("submodule", "surge.toml"),
|
||||
"surfboard": filepath.Join("submodule", "surfboard.toml"),
|
||||
}
|
||||
|
||||
func SubscribeTemplateSettingKey(name string) string {
|
||||
return "subscribe_template_" + strings.TrimSpace(strings.ToLower(name))
|
||||
}
|
||||
|
||||
func DefaultSubscribeTemplate(name string) string {
|
||||
path, ok := subscribeTemplateFiles[strings.TrimSpace(strings.ToLower(name))]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(content)
|
||||
}
|
||||
|
||||
func GetSubscribeTemplate(name string) string {
|
||||
key := SubscribeTemplateSettingKey(name)
|
||||
if value, ok := GetSetting(key); ok && strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
return DefaultSubscribeTemplate(name)
|
||||
}
|
||||
|
||||
func GetAllSubscribeTemplates() map[string]string {
|
||||
keys := make([]string, 0, len(subscribeTemplateFiles))
|
||||
for key := range subscribeTemplateFiles {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
result := make(map[string]string, len(keys))
|
||||
for _, key := range keys {
|
||||
result[SubscribeTemplateSettingKey(key)] = GetSubscribeTemplate(key)
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user