feat(live): 按用户名禁言、后台右侧管控栏、前台抖音式布局与禁言 Toast
Made-with: Cursor
This commit is contained in:
@@ -62,8 +62,10 @@ func handleDanmakuWS(c *gin.Context) {
|
||||
claims, tokenOK := handlers.ParseSiteClaims(c.Query("token"))
|
||||
canSend := tokenOK
|
||||
fromDisplay := "***"
|
||||
fullUsername := ""
|
||||
if tokenOK && claims != nil {
|
||||
fromDisplay = handlers.MaskSiteUsernameForDanmaku(claims.Username)
|
||||
fullUsername = strings.TrimSpace(claims.Username)
|
||||
}
|
||||
|
||||
ws, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
@@ -72,7 +74,7 @@ func handleDanmakuWS(c *gin.Context) {
|
||||
}
|
||||
ws.SetReadLimit(4096)
|
||||
clientIP := c.ClientIP()
|
||||
sessionID := RegisterOnlineSession("danmaku", clientIP, fromDisplay)
|
||||
sessionID := RegisterOnlineSession("danmaku", clientIP, fullUsername)
|
||||
|
||||
danmakuClientsMu.Lock()
|
||||
danmakuClients[ws] = clientIP
|
||||
@@ -95,11 +97,11 @@ func handleDanmakuWS(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
TouchOnlineSession(sessionID)
|
||||
if IsMutedForIP(clientIP) {
|
||||
if IsMutedForSend(clientIP, fullUsername) {
|
||||
_ = writeDanmakuJSON(ws, map[string]interface{}{
|
||||
"type": "error",
|
||||
"code": "muted",
|
||||
"message": "当前已被禁言",
|
||||
"message": "您已被禁言,暂时无法发弹幕或送礼物",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package weblive
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -10,18 +11,20 @@ import (
|
||||
|
||||
var (
|
||||
modMu sync.RWMutex
|
||||
muteAll bool
|
||||
mutedIP = make(map[string]bool)
|
||||
ipWindow = make(map[string][]int64) // 每个 IP 最近窗口内的发送时间戳(ms)
|
||||
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"`
|
||||
OnlineIPs []IPOnlineItem `json:"online_ips"`
|
||||
OnlineUsers []OnlineUserItem `json:"online_users"`
|
||||
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"`
|
||||
@@ -73,6 +76,25 @@ func SetIPMuted(ip string, enabled bool) {
|
||||
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
|
||||
@@ -93,6 +115,22 @@ func IsMutedForIP(ip string) bool {
|
||||
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
|
||||
@@ -100,8 +138,13 @@ func ModerationStateSnapshot() ModerationSnapshot {
|
||||
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))
|
||||
@@ -215,7 +258,7 @@ func onlineUsersLocked() []OnlineUserItem {
|
||||
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,
|
||||
Muted: IsMutedForSend(s.IP, s.Username),
|
||||
})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
|
||||
@@ -40,3 +40,21 @@ func PutLiveMuteIP(c *gin.Context) {
|
||||
SetIPMuted(ip, body.Enabled)
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func PutLiveMuteUser(c *gin.Context) {
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数无效"})
|
||||
return
|
||||
}
|
||||
u := strings.TrimSpace(body.Username)
|
||||
if u == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名不能为空"})
|
||||
return
|
||||
}
|
||||
SetUserMuted(u, body.Enabled)
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ func handleViewerWS(c *gin.Context, h *Hub) {
|
||||
}
|
||||
vid := uuid.New().String()
|
||||
vs := &viewerSession{id: vid, ws: ws}
|
||||
sessionID := RegisterOnlineSession("viewer", c.ClientIP(), "***")
|
||||
sessionID := RegisterOnlineSession("viewer", c.ClientIP(), "")
|
||||
|
||||
h.mu.Lock()
|
||||
h.viewers[vid] = vs
|
||||
|
||||
Reference in New Issue
Block a user