feat: 抖音直播间弹幕监控 - Go+Wails 实现
后端: - SQLite 数据层 (rooms/users/chat_logs/settings/error_logs) - 抖音 WebSocket 抓取器 (proto 解析, 心跳, watchdog) - __ac_signature 算法 (纯 Go 移植) - a_bogus/sign.js 签名 (goja JS 引擎) - Echo HTTP API 服务器 (端口 5000) - SSE 事件广播 - 自动保活 + 授权守护 - PAYS 云授权对接 前端: - 顶栏: 品牌 + 日志/调试/客户名单/授权/文档/房间控制/主题切换 - 添加直播间 (链接解析 + 关键词) - Dashboard (卡片/列表视图, 公开/匿名房筛选, 分页) - 详情页 (多 tab 消息流, 右侧用户面板, 颜色标记, 精准客户, 已看标记) - 精准客户名单 (跨房间汇总) - 订阅授权页面 (登录/注册/购买) - 使用帮助 + 系统调试 - 白昼/黑夜主题切换 (CSS 变量 + iconfont) - 底部版权栏
This commit is contained in:
343
internal/license/license.go
Normal file
343
internal/license/license.go
Normal file
@@ -0,0 +1,343 @@
|
||||
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"] = "授权服务器连接失<E68EA5>? " + 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)
|
||||
}
|
||||
Reference in New Issue
Block a user