package weblive import ( "encoding/json" "strings" "sync" "time" "unicode/utf8" "yh_web/server/handlers" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) const maxDanmakuRunes = 120 var allowedGifts = map[string]struct{}{ "rocket": {}, "sports_car": {}, "plane": {}, "carnival": {}, "rose": {}, "heart": {}, "star": {}, "clap": {}, "cake": {}, "crown": {}, "fireworks": {}, "gift_box": {}, "beer": {}, "mic": {}, } var ( danmakuClientsMu sync.Mutex danmakuClients = make(map[*websocket.Conn]string) ) func writeDanmakuJSON(ws *websocket.Conn, v any) error { b, err := json.Marshal(v) if err != nil { return err } return ws.WriteMessage(websocket.TextMessage, b) } func clipDanmakuText(t string) string { t = strings.TrimSpace(t) if t == "" { return "" } if utf8.RuneCountInString(t) <= maxDanmakuRunes { return t } runes := []rune(t) return string(runes[:maxDanmakuRunes]) } // handleDanmakuWS 弹幕 / 礼物:未带有效 token 仅可收广播。礼物 JSON {"type":"gift","gift":"rocket"};弹幕 {"text":"..."} 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) if err != nil { return } ws.SetReadLimit(4096) clientIP := c.ClientIP() sessionID := RegisterOnlineSession("danmaku", clientIP, fullUsername) danmakuClientsMu.Lock() danmakuClients[ws] = clientIP danmakuClientsMu.Unlock() defer func() { UnregisterOnlineSession(sessionID) danmakuClientsMu.Lock() delete(danmakuClients, ws) danmakuClientsMu.Unlock() _ = ws.Close() }() for { mt, payload, err := ws.ReadMessage() if err != nil { return } if mt != websocket.TextMessage { continue } TouchOnlineSession(sessionID) if IsMutedForSend(clientIP, fullUsername) { _ = writeDanmakuJSON(ws, map[string]interface{}{ "type": "error", "code": "muted", "message": "您已被禁言,暂时无法发弹幕或送礼物", }) continue } if !AllowSendByIP(clientIP) { _ = writeDanmakuJSON(ws, map[string]interface{}{ "type": "error", "code": "rate_limited", "message": "同 IP 发送过快,请稍后再试", }) continue } if !canSend { _ = writeDanmakuJSON(ws, map[string]interface{}{ "type": "error", "code": "login_required", "message": "请先登录或注册后再发弹幕或礼物", }) continue } var envelope struct { Type string `json:"type"` Text string `json:"text"` Gift string `json:"gift"` } if err := json.Unmarshal(payload, &envelope); err != nil { continue } if strings.EqualFold(strings.TrimSpace(envelope.Type), "gift") { gid := strings.TrimSpace(envelope.Gift) if _, ok := allowedGifts[gid]; !ok { _ = writeDanmakuJSON(ws, map[string]interface{}{ "type": "error", "code": "bad_gift", "message": "无效的礼物", }) continue } out, err := json.Marshal(map[string]interface{}{ "type": "gift", "gift": gid, "from": fromDisplay, "ts": time.Now().UnixMilli(), }) if err != nil { continue } danmakuBroadcast(out) continue } text := clipDanmakuText(envelope.Text) if text == "" { continue } out, err := json.Marshal(map[string]interface{}{ "type": "dm", "text": text, "from": fromDisplay, "ts": time.Now().UnixMilli(), }) if err != nil { continue } danmakuBroadcast(out) } } func danmakuBroadcast(b []byte) { danmakuClientsMu.Lock() defer danmakuClientsMu.Unlock() dead := make([]*websocket.Conn, 0) for conn := range danmakuClients { _ = conn.SetWriteDeadline(time.Now().Add(8 * time.Second)) if err := conn.WriteMessage(websocket.TextMessage, b); err != nil { dead = append(dead, conn) } } for _, conn := range dead { delete(danmakuClients, conn) _ = conn.Close() } }