Show subscription inbounds as Remnawave-style host cards.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+82
-36
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -220,58 +221,103 @@ ORDER BY i.sort_order ASC, i.tag ASC`)
|
||||
return list, rows.Err()
|
||||
}
|
||||
|
||||
// ClientSubscriptionLinks builds basic share links using online nodes that have the inbound enabled.
|
||||
func ClientSubscriptionLinks(db *sql.DB, client *models.Client) ([]string, error) {
|
||||
if client == nil || len(client.InboundIDs) == 0 {
|
||||
// ClientSubscriptionHosts returns Remnawave-style hosts: inbound × node combinations.
|
||||
func ClientSubscriptionHosts(db *sql.DB, client *models.Client) ([]models.SubHost, error) {
|
||||
if client == nil {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := db.Query(`
|
||||
SELECT DISTINCT i.tag, p.code, i.port, i.network, i.security, n.host, n.name
|
||||
SELECT i.id, i.tag, i.remark, p.code, p.name, i.port, i.network, i.security,
|
||||
COALESCE(n.host, ''), COALESCE(n.name, ''),
|
||||
COALESCE(n.status = 'online', FALSE),
|
||||
COALESCE(ni.enabled AND n.status = 'online', FALSE)
|
||||
FROM client_inbounds ci
|
||||
JOIN inbounds i ON i.id = ci.inbound_id AND i.enabled = TRUE
|
||||
JOIN protocols p ON p.id = i.protocol_id
|
||||
JOIN node_inbounds ni ON ni.inbound_id = i.id AND ni.enabled = TRUE
|
||||
JOIN nodes n ON n.id = ni.node_id AND n.status = 'online' AND n.profile_id = i.profile_id
|
||||
LEFT JOIN node_inbounds ni ON ni.inbound_id = i.id AND ni.enabled = TRUE
|
||||
LEFT JOIN nodes n ON n.id = ni.node_id AND n.profile_id = i.profile_id
|
||||
WHERE ci.client_id = $1
|
||||
ORDER BY n.name, i.tag`, client.ID)
|
||||
ORDER BY i.sort_order ASC, i.tag ASC, n.name ASC NULLS LAST`, client.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var links []string
|
||||
uid := client.UUID.String()
|
||||
var hosts []models.SubHost
|
||||
seen := map[string]bool{}
|
||||
for rows.Next() {
|
||||
var tag, code, network, security, host, nodeName string
|
||||
var port int
|
||||
if err := rows.Scan(&tag, &code, &port, &network, &security, &host, &nodeName); err != nil {
|
||||
var h models.SubHost
|
||||
if err := rows.Scan(
|
||||
&h.InboundID, &h.Tag, &h.Remark, &h.ProtocolCode, &h.ProtocolName,
|
||||
&h.Port, &h.Network, &h.Security, &h.Address, &h.NodeName,
|
||||
&h.NodeOnline, &h.Available,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := client.Username + "-" + tag + "@" + nodeName
|
||||
switch code {
|
||||
case "vless":
|
||||
links = append(links, fmt.Sprintf(
|
||||
"vless://%s@%s:%d?encryption=none&type=%s&security=%s#%s",
|
||||
uid, host, port, network, security, name,
|
||||
))
|
||||
case "vmess":
|
||||
links = append(links, fmt.Sprintf(
|
||||
"vmess://%s@%s:%d?type=%s&security=%s#%s",
|
||||
uid, host, port, network, security, name,
|
||||
))
|
||||
case "trojan":
|
||||
links = append(links, fmt.Sprintf(
|
||||
"trojan://%s@%s:%d?type=%s&security=%s#%s",
|
||||
uid, host, port, network, security, name,
|
||||
))
|
||||
case "shadowsocks":
|
||||
links = append(links, fmt.Sprintf(
|
||||
"ss://%s@%s:%d#%s",
|
||||
uid, host, port, name,
|
||||
))
|
||||
default:
|
||||
links = append(links, fmt.Sprintf("%s://%s@%s:%d?type=%s&security=%s#%s", code, uid, host, port, network, security, name))
|
||||
// Deduplicate: same inbound without any node → one offline card.
|
||||
key := h.InboundID.String() + "|" + h.Address + "|" + h.NodeName
|
||||
if h.Address == "" {
|
||||
key = h.InboundID.String() + "|none"
|
||||
}
|
||||
if seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
|
||||
if h.Available && h.Address != "" {
|
||||
h.Link = buildShareLink(h.ProtocolCode, uid, h.Address, h.Port, h.Network, h.Security, client.Username+"-"+h.Tag)
|
||||
}
|
||||
hosts = append(hosts, h)
|
||||
}
|
||||
return hosts, rows.Err()
|
||||
}
|
||||
|
||||
func buildShareLink(code, uid, host string, port int, network, security, name string) string {
|
||||
name = url.QueryEscape(name)
|
||||
switch code {
|
||||
case "vless":
|
||||
return fmt.Sprintf("vless://%s@%s:%d?encryption=none&type=%s&security=%s#%s", uid, host, port, network, security, name)
|
||||
case "vmess":
|
||||
return fmt.Sprintf("vmess://%s@%s:%d?type=%s&security=%s#%s", uid, host, port, network, security, name)
|
||||
case "trojan":
|
||||
return fmt.Sprintf("trojan://%s@%s:%d?type=%s&security=%s#%s", uid, host, port, network, security, name)
|
||||
case "shadowsocks":
|
||||
return fmt.Sprintf("ss://%s@%s:%d#%s", uid, host, port, name)
|
||||
default:
|
||||
return fmt.Sprintf("%s://%s@%s:%d?type=%s&security=%s#%s", code, uid, host, port, network, security, name)
|
||||
}
|
||||
}
|
||||
|
||||
// ClientSubscriptionLinks builds share links for available hosts only.
|
||||
func ClientSubscriptionLinks(db *sql.DB, client *models.Client) ([]string, error) {
|
||||
hosts, err := ClientSubscriptionHosts(db, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var links []string
|
||||
for _, h := range hosts {
|
||||
if h.Link != "" {
|
||||
links = append(links, h.Link)
|
||||
}
|
||||
}
|
||||
return links, rows.Err()
|
||||
return links, nil
|
||||
}
|
||||
|
||||
func BuildSubscriptionInfo(db *sql.DB, client *models.Client) (*models.SubscriptionInfo, error) {
|
||||
hosts, err := ClientSubscriptionHosts(db, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var links []string
|
||||
for _, h := range hosts {
|
||||
if h.Link != "" {
|
||||
links = append(links, h.Link)
|
||||
}
|
||||
}
|
||||
info := &models.SubscriptionInfo{Client: *client, Hosts: hosts, Links: links}
|
||||
if client.ExpireAt != nil {
|
||||
info.ExpireUnix = client.ExpireAt.Unix()
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user