269 lines
5.6 KiB
Go
269 lines
5.6 KiB
Go
package weblive
|
||
|
||
import (
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
)
|
||
|
||
var (
|
||
modMu sync.RWMutex
|
||
muteAll bool
|
||
mutedIP = make(map[string]bool)
|
||
mutedUsers = make(map[string]bool) // key: normMuteUsername
|
||
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"`
|
||
MutedUsernames []string `json:"muted_usernames"`
|
||
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()
|
||
}
|
||
|
||
func normMuteUsername(u string) string {
|
||
return strings.ToLower(strings.TrimSpace(u))
|
||
}
|
||
|
||
// SetUserMuted 按登录用户名禁言(弹幕/礼物);与 IP 禁言、全体禁言叠加。
|
||
func SetUserMuted(username string, enabled bool) {
|
||
key := normMuteUsername(username)
|
||
if key == "" {
|
||
return
|
||
}
|
||
modMu.Lock()
|
||
if enabled {
|
||
mutedUsers[key] = true
|
||
} else {
|
||
delete(mutedUsers, key)
|
||
}
|
||
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]
|
||
}
|
||
|
||
// IsMutedForSend 发弹幕/礼物前:全体禁言、IP 禁言、或已登录用户名被禁。
|
||
func IsMutedForSend(ip, username string) bool {
|
||
modMu.RLock()
|
||
defer modMu.RUnlock()
|
||
if muteAll {
|
||
return true
|
||
}
|
||
if mutedIP[ip] {
|
||
return true
|
||
}
|
||
if k := normMuteUsername(username); k != "" && mutedUsers[k] {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func ModerationStateSnapshot() ModerationSnapshot {
|
||
modMu.RLock()
|
||
muteAllNow := muteAll
|
||
muted := make([]string, 0, len(mutedIP))
|
||
for ip := range mutedIP {
|
||
muted = append(muted, ip)
|
||
}
|
||
mutedNames := make([]string, 0, len(mutedUsers))
|
||
for u := range mutedUsers {
|
||
mutedNames = append(mutedNames, u)
|
||
}
|
||
modMu.RUnlock()
|
||
sort.Strings(muted)
|
||
sort.Strings(mutedNames)
|
||
|
||
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: IsMutedForSend(s.IP, s.Username),
|
||
})
|
||
}
|
||
sort.Slice(out, func(i, j int) bool {
|
||
return out[i].OnlineSec > out[j].OnlineSec
|
||
})
|
||
return out
|
||
}
|