package license import ( "encoding/json" "fmt" "io" "net" "net/http" "os" "runtime" "strings" "time" "github.com/google/uuid" "github.com/qingwuyingxing/douyin-live-fetcher/internal/db" ) const ( DefaultServer = "http://42.193.21.75:7777" DefaultProductCode = "douyin-live-fetcher" ) const ( keyServerURL = "license_server_url" keyAccountToken = "license_account_token" keyCustomerCode = "license_customer_code" keyProductCode = "license_product_code" keySessionToken = "license_session_token" keyLastCheckAt = "license_last_check_at" keyLastAllowed = "license_last_allowed" keyLastStatus = "license_last_status" keyCustomerName = "license_customer_name" keyProductName = "license_product_name" keyStartAt = "license_start_at" keyEndAt = "license_end_at" keyPaymentOpts = "license_payment_options" keyDeviceID = "device_id" ) const checkInterval = 300 func LicenseConfig() map[string]string { return map[string]string{ "server_url": strings.TrimRight(funcWithDefault(keyServerURL, DefaultServer), "/"), "account_token": strings.TrimSpace(funcWithDefault(keyAccountToken, "")), "customer_code": strings.TrimSpace(funcWithDefault(keyCustomerCode, "")), "product_code": strings.TrimSpace(funcWithDefault(keyProductCode, DefaultProductCode)), "session_token": strings.TrimSpace(funcWithDefault(keySessionToken, "")), "device_id": DeviceID(), } } func SaveLicenseConfig(server, customerCode, productCode, accountToken string) { db.SetSetting(keyServerURL, strings.TrimRight(server, "/")) db.SetSetting(keyCustomerCode, strings.TrimSpace(customerCode)) db.SetSetting(keyProductCode, strings.TrimSpace(productCode)) db.SetSetting(keyAccountToken, strings.TrimSpace(accountToken)) } func ClearLicenseLogin() { for _, k := range []string{ keyAccountToken, keyCustomerCode, keySessionToken, keyLastCheckAt, keyLastAllowed, keyLastStatus, keyCustomerName, keyProductName, keyStartAt, keyEndAt, keyPaymentOpts, } { db.SetSetting(k, "") } } func LoginAccount(server, login, password string) (map[string]interface{}, error) { base := strings.TrimRight(server, "/") payload, _ := json.Marshal(map[string]string{"login": login, "password": password}) resp, err := http.Post(base+"/api/yser/public/auth/login", "application/json", io.NopCloser(strings.NewReader(string(payload)))) if err != nil { return nil, fmt.Errorf("login request failed: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var result map[string]interface{} json.Unmarshal(body, &result) if code, _ := result["code"].(float64); code != 0 { return nil, fmt.Errorf("%v", result["message"]) } data, _ := result["data"].(map[string]interface{}) token, _ := data["api_token"].(string) if token == "" { return nil, fmt.Errorf("login success but no token returned") } customer, _ := data["customer"].(map[string]interface{}) customerCode, _ := customer["code"].(string) if customerCode == "" { if vc, ok := data["customer_code"].(string); ok { customerCode = vc } } if customerCode == "" { return nil, fmt.Errorf("login success but no customer code returned") } cfg := LicenseConfig() SaveLicenseConfig(base, customerCode, cfg["product_code"], token) return data, nil } func CheckCloudLicense(force bool) map[string]interface{} { cfg := LicenseConfig() if cfg["account_token"] == "" && cfg["customer_code"] == "" { return map[string]interface{}{"configured": false, "allowed": true, "state": "unconfigured", "note": "登录订阅账号以解锁全部功能"} } now := time.Now().Unix() lastAt := int64(0) if v := funcWithDefault(keyLastCheckAt, "0"); v != "" { fmt.Sscanf(v, "%d", &lastAt) } cached := buildCached(cfg, lastAt) if !force && lastAt > 0 && now-lastAt < checkInterval { return cached } endpoint := "/api/yser/license/check" payload := map[string]interface{}{ "product_code": cfg["product_code"], "device_id": cfg["device_id"], } if cfg["account_token"] != "" { endpoint = "/api/yser/account/license/check" payload["account_token"] = cfg["account_token"] payload["device_name"] = getHostname() payload["fingerprint"] = runtime.GOOS + "/" + runtime.GOARCH payload["session_token"] = cfg["session_token"] } else { payload["customer_code"] = cfg["customer_code"] payload["hostname"] = getHostname() payload["platform"] = runtime.GOOS + "/" + runtime.GOARCH } body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", cfg["server_url"]+endpoint, strings.NewReader(string(body))) req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "DouyinLiveFetcher/1.0.0") client := &http.Client{Timeout: 8 * time.Second} resp, err := client.Do(req) if err != nil { endTS := parseTS(cached["end_at"].(string)) if cached["allowed"].(bool) && (endTS == 0 || endTS > now) { cached["note"] = "暂时无法连接授权服务器,使用上次有效授权" cached["offline"] = true return cached } cached["allowed"] = false cached["state"] = "offline" cached["note"] = "授权服务器连接失�? " + err.Error() return cached } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) var r map[string]interface{} json.Unmarshal(respBody, &r) info, _ := r["data"].(map[string]interface{}) allowed := info["allowed"] == true || info["allowed"] == 1 stateVal, _ := info["status"].(string) if stateVal == "" { if allowed { stateVal = "active" } else { stateVal = "suspended" } } db.SetSetting(keyLastCheckAt, fmt.Sprintf("%d", now)) db.SetSetting(keyLastAllowed, fmt.Sprintf("%d", btoi(allowed))) db.SetSetting(keyLastStatus, stateVal) if v, ok := info["customer_name"].(string); ok { db.SetSetting(keyCustomerName, v) } if v, ok := info["product_name"].(string); ok { db.SetSetting(keyProductName, v) } if v, ok := info["start_at"].(string); ok { db.SetSetting(keyStartAt, v) } if v, ok := info["end_at"].(string); ok { db.SetSetting(keyEndAt, v) } if opts, ok := info["payment_options"].([]interface{}); ok { ob, _ := json.Marshal(opts) db.SetSetting(keyPaymentOpts, string(ob)) } if v, ok := info["session_token"].(string); ok && v != "" { db.SetSetting(keySessionToken, v) } cached["allowed"] = allowed cached["state"] = stateVal cached["checked_at"] = now if v, ok := info["customer_name"].(string); ok { cached["customer_name"] = v } if v, ok := info["product_name"].(string); ok { cached["product_name"] = v } if v, ok := info["start_at"].(string); ok { cached["start_at"] = v } if v, ok := info["end_at"].(string); ok { cached["end_at"] = v } if opts, ok := info["payment_options"].([]interface{}); ok { cached["payment_options"] = opts } if allowed { cached["note"] = "云授权有效" } else { if reason, ok := info["reason"].(string); ok { cached["note"] = reason } else { cached["note"] = "云授权不可用: " + stateVal } } return cached } func AppLicenseState(force bool) map[string]interface{} { cloud := CheckCloudLicense(force) ts := trialState() if configured, _ := cloud["configured"].(bool); configured { if allowed, _ := cloud["allowed"].(bool); allowed { cloud["state"] = "active" cloud["days_left"] = nil return cloud } cloud["days_left"] = 0 return cloud } ts["configured"] = false if ts["state"] == "unconfigured" || (ts["state"] == "expired" && ts["days_left"] == 0) { ts["allowed"] = true } else { ts["allowed"] = ts["state"] != "expired" } ts["server_url"] = DefaultServer ts["product_code"] = DefaultProductCode ts["device_id"] = DeviceID() return ts } func LicenseAllowed(force bool) bool { s := AppLicenseState(force) allowed, _ := s["allowed"].(bool) return allowed } func DeviceID() string { did := funcWithDefault(keyDeviceID, "") if did != "" { return did } mac := getFirstMac() raw := fmt.Sprintf("%s|%s|%s", mac, getHostname(), runtime.GOOS+"/"+runtime.GOARCH) did = uuid.NewSHA1(uuid.NameSpaceDNS, []byte(raw)).String() db.SetSetting(keyDeviceID, did) return did } func buildCached(cfg map[string]string, lastAt int64) map[string]interface{} { popts := []interface{}{} if raw := funcWithDefault(keyPaymentOpts, "[]"); raw != "[]" { json.Unmarshal([]byte(raw), &popts) } return map[string]interface{}{ "configured": true, "allowed": funcWithDefault(keyLastAllowed, "0") == "1", "state": funcWithDefault(keyLastStatus, "unknown"), "account_token": cfg["account_token"], "customer_code": cfg["customer_code"], "product_code": cfg["product_code"], "customer_name": funcWithDefault(keyCustomerName, ""), "product_name": funcWithDefault(keyProductName, ""), "start_at": funcWithDefault(keyStartAt, ""), "end_at": funcWithDefault(keyEndAt, ""), "server_url": cfg["server_url"], "device_id": cfg["device_id"], "checked_at": lastAt, "payment_options": popts, } } func trialState() map[string]interface{} { if funcWithDefault("license", "") == "activated" { return map[string]interface{}{"state": "activated", "days_left": nil, "configured": false, "allowed": true} } return map[string]interface{}{"state": "unconfigured", "days_left": nil} } func getFirstMac() string { ifaces, err := net.Interfaces() if err != nil { return "" } for _, iface := range ifaces { if iface.HardwareAddr != nil && len(iface.HardwareAddr) >= 6 { return strings.ReplaceAll(iface.HardwareAddr.String(), ":", "") } } return "" } func getHostname() string { h, _ := os.Hostname() return h } func parseTS(s string) int64 { if s == "" { return 0 } t, err := time.Parse(time.RFC3339, s[:19]) if err != nil { return 0 } return t.Unix() } func btoi(b bool) int { if b { return 1 } return 0 } func strconvParseInt(s string) int64 { var n int64 fmt.Sscanf(s, "%d", &n) return n } func funcWithDefault(key, def string) string { return db.GetSetting(key) }