ultisuite-backend/internal/api/meet/handlers.go
R3D347HR4Y 1d063237b9
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(transcription): integrate Faster Whisper for Jitsi transcriptions
- Added support for Faster Whisper transcription via Jigasi and Skynet.
- Updated .env.example to include new environment variables for transcription settings.
- Enhanced Jitsi Docker Compose configuration to include Skynet and Jigasi services.
- Introduced new API endpoints for managing organizational folders in the drive service.
- Updated Nextcloud initialization script to enable external file mounting.
- Improved error handling and response structures in the drive API.
- Added new properties for organization settings related to transcription and agenda management.
2026-06-12 19:10:18 +02:00

185 lines
5.2 KiB
Go

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})
}