mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
Server:
- Split monolithic voice_handlers.go into voice_join/leave/controls/broadcast
- Add metrics endpoint (admin-IP-restricted /api/v1/metrics)
- Add orphaned attachment cleanup in maintenance loop
- Add sentinel errors (db/errors.go, ws/errors.go)
- Add ring buffer for event replay on reconnect
- Add heartbeat monitoring with stale connection sweep
- Improve hub with panic recovery, graceful shutdown, seq tracking
- Typed message structs replace raw map[string]interface{}
Client:
- Decompose MainPage into ChatArea + SidebarArea controllers
- Add disposable.ts lifecycle management pattern
- Add member list right-click context menu (kick/ban/role)
- Tighten CSP (media-src, font-src, object-src, base-uri)
- Improve store with shallowEqual, 500-msg cap, batch updates
- Add search API endpoint wiring
- Fix LiveKit session cleanup and reconnection
Docs:
- Add CODEMAPS for architecture, backend, frontend, data, deps
- Add protocol-schema.json (machine-readable, 36 message types)
- Add platform research report
- Update PROTOCOL.md with seq/replay fields
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
package ws
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/owncord/server/permissions"
|
|
)
|
|
|
|
// handleVoiceMute processes a voice_mute message.
|
|
// 1. Parses muted bool.
|
|
// 2. Updates DB.
|
|
// 3. Broadcasts voice_state update to channel.
|
|
func (h *Hub) handleVoiceMute(c *Client, payload json.RawMessage) {
|
|
var p struct {
|
|
Muted bool `json:"muted"`
|
|
}
|
|
if err := json.Unmarshal(payload, &p); err != nil {
|
|
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, "invalid voice_mute payload"))
|
|
return
|
|
}
|
|
|
|
if err := h.db.UpdateVoiceMute(c.userID, p.Muted); err != nil {
|
|
slog.Error("ws handleVoiceMute UpdateVoiceMute", "err", err, "user_id", c.userID)
|
|
c.sendMsg(buildErrorMsg(ErrCodeInternal, "failed to update mute state"))
|
|
return
|
|
}
|
|
slog.Debug("voice mute changed", "user_id", c.userID, "muted", p.Muted)
|
|
|
|
h.broadcastVoiceStateUpdate(c)
|
|
}
|
|
|
|
// handleVoiceDeafen processes a voice_deafen message.
|
|
// 1. Parses deafened bool.
|
|
// 2. Updates DB.
|
|
// 3. Broadcasts voice_state update to channel.
|
|
func (h *Hub) handleVoiceDeafen(c *Client, payload json.RawMessage) {
|
|
var p struct {
|
|
Deafened bool `json:"deafened"`
|
|
}
|
|
if err := json.Unmarshal(payload, &p); err != nil {
|
|
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, "invalid voice_deafen payload"))
|
|
return
|
|
}
|
|
|
|
if err := h.db.UpdateVoiceDeafen(c.userID, p.Deafened); err != nil {
|
|
slog.Error("ws handleVoiceDeafen UpdateVoiceDeafen", "err", err, "user_id", c.userID)
|
|
c.sendMsg(buildErrorMsg(ErrCodeInternal, "failed to update deafen state"))
|
|
return
|
|
}
|
|
slog.Debug("voice deafen changed", "user_id", c.userID, "deafened", p.Deafened)
|
|
|
|
h.broadcastVoiceStateUpdate(c)
|
|
}
|
|
|
|
// handleVoiceCamera processes a voice_camera message.
|
|
// 1. Rate limits at 2/sec per user.
|
|
// 2. Checks USE_VIDEO permission.
|
|
// 3. Parses enabled bool.
|
|
// 4. Enforces MaxVideo limit via LiveKit.
|
|
// 5. Updates DB.
|
|
// 6. Broadcasts voice_state update to channel.
|
|
func (h *Hub) handleVoiceCamera(c *Client, payload json.RawMessage) {
|
|
ratKey := fmt.Sprintf("voice_camera:%d", c.userID)
|
|
if !h.limiter.Allow(ratKey, voiceCameraRateLimit, voiceCameraWindow) {
|
|
c.sendMsg(buildRateLimitError("too many camera toggles", voiceCameraWindow.Seconds()))
|
|
return
|
|
}
|
|
|
|
voiceChID := c.getVoiceChID()
|
|
if voiceChID == 0 {
|
|
c.sendMsg(buildErrorMsg(ErrCodeVoiceError, "not in a voice channel"))
|
|
return
|
|
}
|
|
|
|
if !h.requireChannelPerm(c, voiceChID, permissions.UseVideo, "USE_VIDEO") {
|
|
return
|
|
}
|
|
|
|
var p struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
if err := json.Unmarshal(payload, &p); err != nil {
|
|
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, "invalid voice_camera payload"))
|
|
return
|
|
}
|
|
|
|
// Enforce MaxVideo limit when enabling camera.
|
|
if p.Enabled {
|
|
ch, chErr := h.db.GetChannel(voiceChID)
|
|
if chErr == nil && ch != nil && ch.VoiceMaxVideo > 0 && h.livekit != nil {
|
|
videoCount, countErr := h.livekit.CountVideoTracks(voiceChID)
|
|
if countErr != nil {
|
|
slog.Error("handleVoiceCamera CountVideoTracks", "err", countErr, "channel_id", voiceChID)
|
|
} else if videoCount >= ch.VoiceMaxVideo {
|
|
c.sendMsg(buildErrorMsg(ErrCodeVideoLimit,
|
|
fmt.Sprintf("maximum %d video streams reached", ch.VoiceMaxVideo)))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := h.db.UpdateVoiceCamera(c.userID, p.Enabled); err != nil {
|
|
slog.Error("ws handleVoiceCamera UpdateVoiceCamera", "err", err, "user_id", c.userID)
|
|
c.sendMsg(buildErrorMsg(ErrCodeInternal, "failed to update camera state"))
|
|
return
|
|
}
|
|
slog.Debug("voice camera changed", "user_id", c.userID, "enabled", p.Enabled)
|
|
|
|
h.broadcastVoiceStateUpdate(c)
|
|
}
|
|
|
|
// handleVoiceScreenshare processes a voice_screenshare message.
|
|
// 1. Rate limits at 2/sec per user.
|
|
// 2. Checks SHARE_SCREEN permission.
|
|
// 3. Parses enabled bool.
|
|
// 4. Updates DB.
|
|
// 5. Broadcasts voice_state update to channel.
|
|
func (h *Hub) handleVoiceScreenshare(c *Client, payload json.RawMessage) {
|
|
ratKey := fmt.Sprintf("voice_screenshare:%d", c.userID)
|
|
if !h.limiter.Allow(ratKey, voiceScreenshareRateLimit, voiceScreenshareWindow) {
|
|
c.sendMsg(buildRateLimitError("too many screenshare toggles", voiceScreenshareWindow.Seconds()))
|
|
return
|
|
}
|
|
|
|
voiceChID := c.getVoiceChID()
|
|
if voiceChID == 0 {
|
|
c.sendMsg(buildErrorMsg(ErrCodeVoiceError, "not in a voice channel"))
|
|
return
|
|
}
|
|
|
|
if !h.requireChannelPerm(c, voiceChID, permissions.ShareScreen, "SHARE_SCREEN") {
|
|
return
|
|
}
|
|
|
|
var p struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
if err := json.Unmarshal(payload, &p); err != nil {
|
|
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, "invalid voice_screenshare payload"))
|
|
return
|
|
}
|
|
|
|
if err := h.db.UpdateVoiceScreenshare(c.userID, p.Enabled); err != nil {
|
|
slog.Error("ws handleVoiceScreenshare UpdateVoiceScreenshare", "err", err, "user_id", c.userID)
|
|
c.sendMsg(buildErrorMsg(ErrCodeInternal, "failed to update screenshare state"))
|
|
return
|
|
}
|
|
slog.Debug("voice screenshare changed", "user_id", c.userID, "enabled", p.Enabled)
|
|
|
|
h.broadcastVoiceStateUpdate(c)
|
|
}
|