226 lines
4.5 KiB
Go
226 lines
4.5 KiB
Go
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
|
|
}
|