package httpapi import ( "context" "encoding/json" "errors" "net/http" "strings" "synctv/server/internal/room" "synctv/server/internal/store" ) type Rooms interface { Create(ctx context.Context) (room.Room, error) Get(ctx context.Context, code string) (room.Room, error) SetSource(ctx context.Context, code, deviceID string, src room.Source) (room.Room, error) } type Server struct { rooms Rooms hub http.Handler proxy http.Handler } func New(rooms Rooms, hub http.Handler, proxy http.Handler) *Server { return &Server{rooms: rooms, hub: hub, proxy: proxy} } func (s *Server) Routes() http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /health", s.health) mux.HandleFunc("POST /api/rooms", s.createRoom) mux.HandleFunc("GET /api/rooms/{code}", s.getRoom) mux.HandleFunc("POST /api/rooms/{code}/source", s.setSource) mux.Handle("GET /api/rooms/{code}/stream", s.proxy) mux.Handle("GET /ws", s.hub) return withCORS(mux) } func (s *Server) health(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (s *Server) createRoom(w http.ResponseWriter, r *http.Request) { rm, err := s.rooms.Create(r.Context()) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusCreated, map[string]any{"room": rm}) } func (s *Server) getRoom(w http.ResponseWriter, r *http.Request) { code := r.PathValue("code") deviceID := r.URL.Query().Get("deviceId") rm, err := s.rooms.Get(r.Context(), code) if err != nil { status := http.StatusInternalServerError if errors.Is(err, store.ErrNotFound) { status = http.StatusNotFound } writeError(w, status, err.Error()) return } writeJSON(w, http.StatusOK, room.Snapshot{ Room: rm.PublicFor(deviceID), IsOwner: deviceID != "" && rm.OwnerDeviceID == deviceID, DeviceID: deviceID, ServerNow: room.NowMS(), }) } func (s *Server) setSource(w http.ResponseWriter, r *http.Request) { code := r.PathValue("code") deviceID := r.Header.Get("X-Device-Id") if strings.TrimSpace(deviceID) == "" { writeError(w, http.StatusBadRequest, "X-Device-Id is required") return } var src room.Source if err := json.NewDecoder(r.Body).Decode(&src); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } rm, err := s.rooms.SetSource(r.Context(), code, deviceID, src) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, room.Snapshot{ Room: rm.PublicFor(deviceID), IsOwner: rm.OwnerDeviceID == deviceID, DeviceID: deviceID, ServerNow: room.NowMS(), }) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]any{"error": message}) } func withCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-Device-Id") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) }