121 lines
3.3 KiB
Go
121 lines
3.3 KiB
Go
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)
|
|
})
|
|
}
|