package meet import ( "log/slog" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgxpool" "github.com/ultisuite/ulti-backend/internal/api/apiresponse" "github.com/ultisuite/ulti-backend/internal/api/apivalidate" "github.com/ultisuite/ulti-backend/internal/api/middleware" "github.com/ultisuite/ulti-backend/internal/auth" meetpkg "github.com/ultisuite/ulti-backend/internal/meet" "github.com/ultisuite/ulti-backend/internal/nextcloud" "github.com/ultisuite/ulti-backend/internal/orgpolicy" ) type Handler struct { svc *Service enabled bool publicURL string policy *orgpolicy.Loader transcripts *TranscriptProcessor transcriptSecret string logger *slog.Logger } func NewHandler( meetCfg *meetpkg.Config, enabled bool, publicURL string, policy *orgpolicy.Loader, db *pgxpool.Pool, nc *nextcloud.Client, transcriptSecret string, ) *Handler { return &Handler{ svc: NewService(meetCfg, policy), enabled: enabled && meetCfg != nil, publicURL: strings.TrimRight(strings.TrimSpace(publicURL), "/"), policy: policy, transcripts: NewTranscriptProcessor(db, nc, policy), transcriptSecret: strings.TrimSpace(transcriptSecret), logger: slog.Default().With("component", "meet-api"), } } func (h *Handler) Routes() chi.Router { r := chi.NewRouter() r.Get("/config", h.GetConfig) r.Post("/transcripts", h.ReceiveTranscript) r.Group(func(r chi.Router) { r.Use(middleware.RequireFullAccount) r.Post("/rooms", h.CreateRoom) r.Post("/rooms/{roomID}/token", h.GetToken) }) return r } func (h *Handler) GetConfig(w http.ResponseWriter, r *http.Request) { resp := map[string]any{ "enabled": h.enabled, "public_url": h.publicURL, "brand_name": "UltiMeet", } if h.policy != nil { if pub, err := h.policy.PublicMeetPolicy(r.Context()); err == nil { resp["transcription_enabled"] = pub.TranscriptionEnabled resp["transcription_mode"] = pub.TranscriptionMode resp["auto_start_transcription"] = pub.AutoStartTranscription } } apiresponse.WriteJSON(w, http.StatusOK, resp) } func meetUser(claims *auth.Claims) *meetpkg.UserInfo { return &meetpkg.UserInfo{ ID: claims.Sub, Name: claims.Name, Email: claims.Email, } } func (h *Handler) CreateRoom(w http.ResponseWriter, r *http.Request) { if !h.enabled { apiresponse.WriteError(w, r, http.StatusConflict, "meet_disabled", "meet is disabled", nil) return } claims := middleware.ClaimsFromContext(r.Context()) var req createRoomRequest if r.ContentLength != 0 { if err := apivalidate.DecodeJSON(w, r, maxRequestBody, &req); err != nil { return } } if verr := validateCreateRoom(&req); verr != nil { apivalidate.WriteValidationError(w, r, verr) return } user := meetUser(claims) user.IsMod = true token, err := h.svc.CreateRoom(r.Context(), req.Name, user) if err != nil { h.logger.Error("create room token", "error", err) apivalidate.WriteInternal(w, r) return } apiresponse.WriteJSON(w, http.StatusCreated, token) } func (h *Handler) GetToken(w http.ResponseWriter, r *http.Request) { if !h.enabled { apiresponse.WriteError(w, r, http.StatusConflict, "meet_disabled", "meet is disabled", nil) return } claims := middleware.ClaimsFromContext(r.Context()) roomID := chi.URLParam(r, "roomID") if verr := validateRoomID(roomID); verr != nil { apivalidate.WriteValidationError(w, r, verr) return } user := meetUser(claims) user.IsMod = false token, err := h.svc.GetToken(r.Context(), roomID, user) if err != nil { h.logger.Error("get room token", "error", err) apivalidate.WriteInternal(w, r) return } apiresponse.WriteJSON(w, http.StatusOK, token) } func (h *Handler) ReceiveTranscript(w http.ResponseWriter, r *http.Request) { if h.transcriptSecret != "" { authHeader := strings.TrimSpace(r.Header.Get("Authorization")) if authHeader != "Bearer "+h.transcriptSecret { apiresponse.WriteError(w, r, http.StatusUnauthorized, "auth.unauthorized", "unauthorized", nil) return } } var req transcriptRequest if err := apivalidate.DecodeJSON(w, r, 2<<20, &req); err != nil { return } if verr := validateTranscriptRequest(&req); verr != nil { apivalidate.WriteValidationError(w, r, verr) return } claims := middleware.ClaimsFromContext(r.Context()) organizerUserID := strings.TrimSpace(req.OrganizerUserID) organizerEmail := strings.TrimSpace(req.OrganizerEmail) if claims != nil { if organizerUserID == "" { organizerUserID = claims.Sub } if organizerEmail == "" { organizerEmail = claims.Email } } err := h.transcripts.Handle(r.Context(), transcriptJobInput{ RoomID: strings.TrimSpace(req.RoomID), OrganizerUserID: organizerUserID, OrganizerEmail: organizerEmail, ParticipantEmails: req.ParticipantEmails, RawTranscript: req.Transcript, Mode: strings.TrimSpace(req.Mode), QueuedAudioURL: strings.TrimSpace(req.QueuedAudioURL), }) if err != nil { h.logger.Error("process transcript", "error", err) apiresponse.WriteError(w, r, http.StatusBadRequest, "transcript_failed", err.Error(), nil) return } apiresponse.WriteJSON(w, http.StatusAccepted, map[string]any{"ok": true}) }