Files
web/server/pkg/weblive/moderation.go

269 lines
5.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}