package voice import ( "database/sql" "encoding/json" "net/http" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions" "github.com/go-chi/chi/v5" "github.com/livekit/protocol/livekit" ) type Handler struct { db *sql.DB client *Client hub *gateway.Hub checker *permissions.Checker } func NewHandler(db *sql.DB, client *Client, hub *gateway.Hub) *Handler { return &Handler{ db: db, client: client, hub: hub, checker: permissions.NewChecker(db), } } func (h *Handler) RegisterRoutes(r chi.Router) { r.Use(middleware.RequireAuth) r.Post("/token", h.GetToken) r.Get("/rooms/{roomID}/participants", h.GetParticipants) r.Post("/rooms/{roomID}/participants/{userID}/mute", h.MuteParticipant) } type tokenRequest struct { RoomName string `json:"room_name"` } type tokenResponse struct { Token string `json:"token"` LiveKitURL string `json:"livekit_url"` } // @Summary Get voice token // @Description Get a LiveKit access token for a voice room // @Tags voice // @Accept json // @Produce json // @Security SessionAuth // @Param body body tokenRequest true "Room data" // @Success 200 {object} tokenResponse // @Failure 400 {object} map[string]string // @Failure 401 {object} map[string]string // @Failure 404 {object} map[string]string // @Router /voice/token [post] func (h *Handler) GetToken(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } var req tokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if req.RoomName == "" { http.Error(w, `{"error":"missing room_name"}`, http.StatusBadRequest) return } // Get username for the identity var username string err := h.db.QueryRowContext(r.Context(), `SELECT username FROM users WHERE id = $1`, userID).Scan(&username) if err != nil { http.Error(w, `{"error":"user not found"}`, http.StatusNotFound) return } // Create room if it doesn't exist _, err = h.client.CreateRoom(req.RoomName) if err != nil { // Room might already exist, that's fine } token, err := h.client.GenerateToken(req.RoomName, userID, username) if err != nil { http.Error(w, `{"error":"token generation failed"}`, http.StatusInternalServerError) return } // Broadcast voice join event h.hub.BroadcastEvent(gateway.Event{ Type: "VOICE_JOIN", Data: map[string]interface{}{ "room_name": req.RoomName, "user_id": userID, "username": username, }, }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(tokenResponse{ Token: token, LiveKitURL: h.client.url, }) } // @Summary Get voice room participants // @Description Get participants in a voice room // @Tags voice // @Produce json // @Param roomID path string true "Room ID" // @Success 200 {array} object // @Failure 400 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /voice/rooms/{roomID}/participants [get] func (h *Handler) GetParticipants(w http.ResponseWriter, r *http.Request) { roomID := chi.URLParam(r, "roomID") if roomID == "" { http.Error(w, `{"error":"missing room ID"}`, http.StatusBadRequest) return } participants, err := h.client.ListParticipants(roomID) if err != nil { http.Error(w, `{"error":"failed to list participants"}`, http.StatusInternalServerError) return } type participantInfo struct { Identity string `json:"identity"` State string `json:"state"` } result := make([]participantInfo, 0, len(participants)) for _, p := range participants { state := "connected" if p.State == livekit.ParticipantInfo_DISCONNECTED { state = "disconnected" } result = append(result, participantInfo{ Identity: p.Identity, State: state, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // @Summary Mute a participant // @Description Mute a participant's tracks in a voice room (requires MUTE_MEMBERS permission) // @Tags voice // @Security SessionAuth // @Param roomID path string true "Room ID" // @Param userID path string true "User ID (Identity)" // @Success 200 {object} map[string]string // @Router /voice/rooms/{roomID}/participants/{userID}/mute [post] func (h *Handler) MuteParticipant(w http.ResponseWriter, r *http.Request) { roomID := chi.URLParam(r, "roomID") targetIdentity := chi.URLParam(r, "userID") userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } // Find the server_id for this channel to check permissions. var serverID string err := h.db.QueryRowContext(r.Context(), `SELECT server_id FROM channels WHERE id = $1`, roomID).Scan(&serverID) if err != nil { if err == sql.ErrNoRows { http.Error(w, `{"error":"room not found or not a server channel"}`, http.StatusNotFound) } else { http.Error(w, `{"error":"database error"}`, http.StatusInternalServerError) } return } hasPerm, err := h.checker.CheckChannelPermission(r.Context(), serverID, userID, roomID, permissions.MUTE_MEMBERS) if err != nil { http.Error(w, `{"error":"failed to check permissions"}`, http.StatusInternalServerError) return } if !hasPerm { http.Error(w, `{"error":"forbidden: missing MUTE_MEMBERS permission"}`, http.StatusForbidden) return } if err := h.client.MuteParticipant(roomID, targetIdentity); err != nil { http.Error(w, `{"error":"failed to mute participant"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"status": "success"}) }