Release 3.9.0+2: tolerant subscription import, full Remnawave server list, subscription info card (expiry / traffic / devices).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+97
-25
@@ -26,14 +26,14 @@ import (
|
||||
|
||||
// Manager orchestrates protocol engines and Windows system proxy.
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
cfgPath string
|
||||
cfg *config.Config
|
||||
engine Engine
|
||||
sys sysproxy.Controller
|
||||
profile *config.Profile
|
||||
stderr io.Writer
|
||||
binDir string
|
||||
mu sync.Mutex
|
||||
cfgPath string
|
||||
cfg *config.Config
|
||||
engine Engine
|
||||
sys sysproxy.Controller
|
||||
profile *config.Profile
|
||||
stderr io.Writer
|
||||
binDir string
|
||||
}
|
||||
|
||||
func NewManager(cfgPath string, cfg *config.Config, stderr io.Writer) (*Manager, error) {
|
||||
@@ -439,7 +439,14 @@ func (m *Manager) SaveRemnawaveSettings(s remnawave.Settings) error {
|
||||
return config.Save(m.cfgPath, *m.cfg)
|
||||
}
|
||||
|
||||
func (m *Manager) ImportSubscription(rawURL string) (int, error) {
|
||||
// ImportResult reports how a subscription import went.
|
||||
type ImportResult struct {
|
||||
Imported int `json:"imported"`
|
||||
Skipped int `json:"skipped"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
func (m *Manager) ImportSubscription(rawURL string) (ImportResult, error) {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
@@ -452,22 +459,22 @@ func (m *Manager) ImportSubscription(rawURL string) (int, error) {
|
||||
Token: m.RemnawaveSettings().Token,
|
||||
CaddyToken: m.RemnawaveSettings().CaddyToken,
|
||||
}
|
||||
items, used, err := remnawave.Fetch(ctx, s)
|
||||
res, used, err := remnawave.Fetch(ctx, s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return ImportResult{}, err
|
||||
}
|
||||
return m.applySubscriptionItems(items, used, &s)
|
||||
return m.applySubscription(ctx, res, used, &s)
|
||||
}
|
||||
|
||||
items, err := subscription.Fetch(ctx, rawURL)
|
||||
res, err := subscription.Fetch(ctx, rawURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return ImportResult{}, err
|
||||
}
|
||||
return m.applySubscriptionItems(items, rawURL, nil)
|
||||
return m.applySubscription(ctx, res, rawURL, nil)
|
||||
}
|
||||
|
||||
// ImportRemnawave fetches configs using saved / provided panel settings.
|
||||
func (m *Manager) ImportRemnawave(s remnawave.Settings) (int, error) {
|
||||
func (m *Manager) ImportRemnawave(s remnawave.Settings) (ImportResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
||||
defer cancel()
|
||||
if strings.TrimSpace(s.BaseURL) == "" || strings.TrimSpace(s.ShortUUID) == "" {
|
||||
@@ -485,16 +492,55 @@ func (m *Manager) ImportRemnawave(s remnawave.Settings) (int, error) {
|
||||
s.CaddyToken = cur.CaddyToken
|
||||
}
|
||||
}
|
||||
items, used, err := remnawave.Fetch(ctx, s)
|
||||
res, used, err := remnawave.Fetch(ctx, s)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return ImportResult{}, err
|
||||
}
|
||||
return m.applySubscriptionItems(items, used, &s)
|
||||
return m.applySubscription(ctx, res, used, &s)
|
||||
}
|
||||
|
||||
func (m *Manager) applySubscriptionItems(items []subscription.Item, usedURL string, rw *remnawave.Settings) (int, error) {
|
||||
if len(items) == 0 {
|
||||
return 0, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2 / awg / vless / vmess / trojan)")
|
||||
func (m *Manager) applySubscription(ctx context.Context, res *subscription.Result, usedURL string, rw *remnawave.Settings) (ImportResult, error) {
|
||||
if res == nil || len(res.Items) == 0 {
|
||||
if res != nil && res.Skipped > 0 {
|
||||
return ImportResult{Skipped: res.Skipped, Warnings: res.Warnings},
|
||||
fmt.Errorf("нет валидных серверов: %d записей пропущено (%s)", res.Skipped, strings.Join(res.Warnings, "; "))
|
||||
}
|
||||
return ImportResult{}, fmt.Errorf("в подписке нет поддерживаемых ссылок (naive / hysteria2 / awg / vless / vmess / trojan)")
|
||||
}
|
||||
|
||||
// Subscription usage info: header data first, enriched via Remnawave API
|
||||
// (devices / limits) when an API token is configured. Best-effort.
|
||||
subInfo := buildSubInfo(res.Info)
|
||||
{
|
||||
s := m.RemnawaveSettings()
|
||||
if rw != nil {
|
||||
s = *rw
|
||||
if strings.TrimSpace(s.Token) == "" {
|
||||
s.Token = m.RemnawaveSettings().Token
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(s.Token) != "" && strings.TrimSpace(s.BaseURL) != "" && strings.TrimSpace(s.ShortUUID) != "" {
|
||||
if ui, err := remnawave.FetchUserInfo(ctx, s); err == nil && ui != nil {
|
||||
if subInfo == nil {
|
||||
subInfo = &config.SubscriptionInfo{}
|
||||
}
|
||||
if subInfo.Upload == 0 && subInfo.Download == 0 && ui.UsedTraffic > 0 {
|
||||
subInfo.Download = ui.UsedTraffic
|
||||
}
|
||||
if subInfo.Total == 0 && ui.TrafficLimit > 0 {
|
||||
subInfo.Total = ui.TrafficLimit
|
||||
}
|
||||
if subInfo.Expire == 0 && ui.ExpireAt > 0 {
|
||||
subInfo.Expire = ui.ExpireAt
|
||||
}
|
||||
subInfo.DevicesKnown = ui.DevicesKnown
|
||||
subInfo.Devices = ui.Devices
|
||||
subInfo.DeviceLimit = ui.DeviceLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
if subInfo != nil {
|
||||
subInfo.UpdatedAt = time.Now().Unix()
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
@@ -512,7 +558,10 @@ func (m *Manager) applySubscriptionItems(items []subscription.Item, usedURL stri
|
||||
m.cfg.RemnawaveCaddyToken = t
|
||||
}
|
||||
}
|
||||
for _, it := range items {
|
||||
if subInfo != nil {
|
||||
m.cfg.SubInfo = subInfo
|
||||
}
|
||||
for _, it := range res.Items {
|
||||
_ = m.cfg.UpsertProfileWithProtocol(it.Name, it.URI, it.Protocol)
|
||||
for i := range m.cfg.Profiles {
|
||||
if m.cfg.Profiles[i].Name == it.Name {
|
||||
@@ -522,9 +571,32 @@ func (m *Manager) applySubscriptionItems(items []subscription.Item, usedURL stri
|
||||
}
|
||||
}
|
||||
if err := config.Save(m.cfgPath, *m.cfg); err != nil {
|
||||
return 0, err
|
||||
return ImportResult{}, err
|
||||
}
|
||||
return len(items), nil
|
||||
return ImportResult{Imported: len(res.Items), Skipped: res.Skipped, Warnings: res.Warnings}, nil
|
||||
}
|
||||
|
||||
func buildSubInfo(info *subscription.Info) *config.SubscriptionInfo {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
return &config.SubscriptionInfo{
|
||||
Upload: info.Upload,
|
||||
Download: info.Download,
|
||||
Total: info.Total,
|
||||
Expire: info.Expire,
|
||||
}
|
||||
}
|
||||
|
||||
// SubscriptionInfo returns the cached subscription usage info (may be nil).
|
||||
func (m *Manager) SubscriptionInfo() *config.SubscriptionInfo {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.cfg.SubInfo == nil {
|
||||
return nil
|
||||
}
|
||||
cp := *m.cfg.SubInfo
|
||||
return &cp
|
||||
}
|
||||
|
||||
func (m *Manager) SaveConfig() error {
|
||||
|
||||
Reference in New Issue
Block a user