package weblive import ( "fmt" "sort" "sync" "sync/atomic" "time" ) var ( modMu sync.RWMutex muteAll bool mutedIP = make(map[string]bool) ipWindow = make(map[string][]int64) // 每个 IP 最近窗口内的发送时间戳(ms) onlineMap = make(map[string]*onlineSession) seq uint64 ) type ModerationSnapshot struct { MuteAll bool `json:"mute_all"` MutedIPs []string `json:"muted_ips"` OnlineIPs []IPOnlineItem `json:"online_ips"` OnlineUsers []OnlineUserItem `json:"online_users"` RateLimit struct { WindowMs int `json:"window_ms"` MaxHits int `json:"max_hits"` } `json:"rate_limit"` } type IPOnlineItem struct { IP string `json:"ip"` Count int `json:"count"` Muted bool `json:"muted"` } type onlineSession struct { ID string IP string Username string Channel string Connected time.Time LastAt time.Time } type OnlineUserItem struct { ID string `json:"id"` IP string `json:"ip"` Username string `json:"username"` Channel string `json:"channel"` ConnectedAt string `json:"connected_at"` OnlineSec int64 `json:"online_sec"` IdleSec int64 `json:"idle_sec"` Muted bool `json:"muted"` } func SetMuteAll(enabled bool) { modMu.Lock() muteAll = enabled modMu.Unlock() } func SetIPMuted(ip string, enabled bool) { if ip == "" { return } modMu.Lock() if enabled { mutedIP[ip] = true } else { delete(mutedIP, ip) } modMu.Unlock() } const ( ipSendWindowMs = 3000 ipSendMaxHits = 10 ) func IsIPMuted(ip string) bool { modMu.RLock() defer modMu.RUnlock() return mutedIP[ip] } func IsMutedForIP(ip string) bool { modMu.RLock() defer modMu.RUnlock() if muteAll { return true } return mutedIP[ip] } func ModerationStateSnapshot() ModerationSnapshot { modMu.RLock() muteAllNow := muteAll muted := make([]string, 0, len(mutedIP)) for ip := range mutedIP { muted = append(muted, ip) } modMu.RUnlock() sort.Strings(muted) counts := onlineIPCountsLocked() online := make([]IPOnlineItem, 0, len(counts)) for ip, n := range counts { online = append(online, IPOnlineItem{IP: ip, Count: n, Muted: IsIPMuted(ip)}) } sort.Slice(online, func(i, j int) bool { if online[i].Count == online[j].Count { return online[i].IP < online[j].IP } return online[i].Count > online[j].Count }) users := onlineUsersLocked() out := ModerationSnapshot{MuteAll: muteAllNow, MutedIPs: muted, OnlineIPs: online, OnlineUsers: users} out.RateLimit.WindowMs = ipSendWindowMs out.RateLimit.MaxHits = ipSendMaxHits return out } // AllowSendByIP 本地内存限频(同 IP 先本地判定,避免刷爆) func AllowSendByIP(ip string) bool { if ip == "" { return true } now := time.Now().UnixMilli() cut := now - ipSendWindowMs modMu.Lock() defer modMu.Unlock() arr := ipWindow[ip] if len(arr) > 0 { k := 0 for _, ts := range arr { if ts >= cut { arr[k] = ts k++ } } arr = arr[:k] } if len(arr) >= ipSendMaxHits { ipWindow[ip] = arr return false } arr = append(arr, now) if len(arr) > ipSendMaxHits*4 { arr = arr[len(arr)-ipSendMaxHits*2:] } ipWindow[ip] = arr return true } func RegisterOnlineSession(channel, ip, username string) string { now := time.Now() id := fmt.Sprintf("%s-%d", channel, atomic.AddUint64(&seq, 1)) modMu.Lock() onlineMap[id] = &onlineSession{ ID: id, IP: ip, Username: username, Channel: channel, Connected: now, LastAt: now, } modMu.Unlock() return id } func TouchOnlineSession(id string) { if id == "" { return } modMu.Lock() if s := onlineMap[id]; s != nil { s.LastAt = time.Now() } modMu.Unlock() } func UnregisterOnlineSession(id string) { if id == "" { return } modMu.Lock() delete(onlineMap, id) modMu.Unlock() } func onlineIPCountsLocked() map[string]int { out := make(map[string]int) for _, s := range onlineMap { if s == nil || s.IP == "" { continue } out[s.IP]++ } return out } func onlineUsersLocked() []OnlineUserItem { now := time.Now() out := make([]OnlineUserItem, 0, len(onlineMap)) for _, s := range onlineMap { if s == nil { continue } out = append(out, OnlineUserItem{ ID: s.ID, IP: s.IP, Username: s.Username, Channel: s.Channel, ConnectedAt: s.Connected.Format(time.RFC3339), OnlineSec: int64(now.Sub(s.Connected).Seconds()), IdleSec: int64(now.Sub(s.LastAt).Seconds()), Muted: mutedIP[s.IP] || muteAll, }) } sort.Slice(out, func(i, j int) bool { return out[i].OnlineSec > out[j].OnlineSec }) return out }