package handlers import ( "net/http" "time" "github.com/breakpilot/consent-service/internal/services/jitsi" "github.com/breakpilot/consent-service/internal/services/matrix" "github.com/gin-gonic/gin" ) // CommunicationHandlers handles Matrix and Jitsi API endpoints type CommunicationHandlers struct { matrixService *matrix.MatrixService jitsiService *jitsi.JitsiService } // NewCommunicationHandlers creates new communication handlers func NewCommunicationHandlers(matrixSvc *matrix.MatrixService, jitsiSvc *jitsi.JitsiService) *CommunicationHandlers { return &CommunicationHandlers{ matrixService: matrixSvc, jitsiService: jitsiSvc, } } // ======================================== // Health & Status Endpoints // ======================================== // GetCommunicationStatus returns status of Matrix and Jitsi services func (h *CommunicationHandlers) GetCommunicationStatus(c *gin.Context) { ctx := c.Request.Context() status := gin.H{ "timestamp": time.Now().UTC().Format(time.RFC3339), } // Check Matrix if h.matrixService != nil { matrixErr := h.matrixService.HealthCheck(ctx) status["matrix"] = gin.H{ "enabled": true, "healthy": matrixErr == nil, "server_name": h.matrixService.GetServerName(), "error": errToString(matrixErr), } } else { status["matrix"] = gin.H{ "enabled": false, "healthy": false, } } // Check Jitsi if h.jitsiService != nil { jitsiErr := h.jitsiService.HealthCheck(ctx) serverInfo := h.jitsiService.GetServerInfo() status["jitsi"] = gin.H{ "enabled": true, "healthy": jitsiErr == nil, "base_url": serverInfo["base_url"], "auth_enabled": serverInfo["auth_enabled"], "error": errToString(jitsiErr), } } else { status["jitsi"] = gin.H{ "enabled": false, "healthy": false, } } c.JSON(http.StatusOK, status) } // ======================================== // Matrix Room Endpoints // ======================================== // CreateRoomRequest for creating Matrix rooms type CreateRoomRequest struct { Type string `json:"type" binding:"required"` // "class_info", "student_dm", "parent_rep" ClassName string `json:"class_name"` SchoolName string `json:"school_name"` StudentName string `json:"student_name,omitempty"` TeacherIDs []string `json:"teacher_ids"` ParentIDs []string `json:"parent_ids,omitempty"` ParentRepIDs []string `json:"parent_rep_ids,omitempty"` } // CreateRoom creates a new Matrix room based on type func (h *CommunicationHandlers) CreateRoom(c *gin.Context) { if h.matrixService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Matrix service not configured"}) return } var req CreateRoomRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() var resp *matrix.CreateRoomResponse var err error switch req.Type { case "class_info": resp, err = h.matrixService.CreateClassInfoRoom(ctx, req.ClassName, req.SchoolName, req.TeacherIDs) case "student_dm": resp, err = h.matrixService.CreateStudentDMRoom(ctx, req.StudentName, req.ClassName, req.TeacherIDs, req.ParentIDs) case "parent_rep": resp, err = h.matrixService.CreateParentRepRoom(ctx, req.ClassName, req.TeacherIDs, req.ParentRepIDs) default: c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid room type. Use: class_info, student_dm, parent_rep"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "room_id": resp.RoomID, "type": req.Type, }) } // InviteUserRequest for inviting users to rooms type InviteUserRequest struct { RoomID string `json:"room_id" binding:"required"` UserID string `json:"user_id" binding:"required"` } // InviteUser invites a user to a Matrix room func (h *CommunicationHandlers) InviteUser(c *gin.Context) { if h.matrixService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Matrix service not configured"}) return } var req InviteUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() if err := h.matrixService.InviteUser(ctx, req.RoomID, req.UserID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true}) } // SendMessageRequest for sending messages type SendMessageRequest struct { RoomID string `json:"room_id" binding:"required"` Message string `json:"message" binding:"required"` HTML string `json:"html,omitempty"` } // SendMessage sends a message to a Matrix room func (h *CommunicationHandlers) SendMessage(c *gin.Context) { if h.matrixService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Matrix service not configured"}) return } var req SendMessageRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() var err error if req.HTML != "" { err = h.matrixService.SendHTMLMessage(ctx, req.RoomID, req.Message, req.HTML) } else { err = h.matrixService.SendMessage(ctx, req.RoomID, req.Message) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true}) } // SendNotificationRequest for sending school notifications type SendNotificationRequest struct { RoomID string `json:"room_id" binding:"required"` Type string `json:"type" binding:"required"` // "absence", "grade", "announcement" StudentName string `json:"student_name,omitempty"` Date string `json:"date,omitempty"` Lesson int `json:"lesson,omitempty"` Subject string `json:"subject,omitempty"` GradeType string `json:"grade_type,omitempty"` Grade float64 `json:"grade,omitempty"` Title string `json:"title,omitempty"` Content string `json:"content,omitempty"` TeacherName string `json:"teacher_name,omitempty"` } // SendNotification sends a typed notification (absence, grade, announcement) func (h *CommunicationHandlers) SendNotification(c *gin.Context) { if h.matrixService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Matrix service not configured"}) return } var req SendNotificationRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() var err error switch req.Type { case "absence": err = h.matrixService.SendAbsenceNotification(ctx, req.RoomID, req.StudentName, req.Date, req.Lesson) case "grade": err = h.matrixService.SendGradeNotification(ctx, req.RoomID, req.StudentName, req.Subject, req.GradeType, req.Grade) case "announcement": err = h.matrixService.SendClassAnnouncement(ctx, req.RoomID, req.Title, req.Content, req.TeacherName) default: c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid notification type. Use: absence, grade, announcement"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true}) } // RegisterUserRequest for user registration type RegisterUserRequest struct { Username string `json:"username" binding:"required"` DisplayName string `json:"display_name"` } // RegisterMatrixUser registers a new Matrix user func (h *CommunicationHandlers) RegisterMatrixUser(c *gin.Context) { if h.matrixService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Matrix service not configured"}) return } var req RegisterUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() resp, err := h.matrixService.RegisterUser(ctx, req.Username, req.DisplayName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "user_id": resp.UserID, }) } // ======================================== // Jitsi Video Conference Endpoints // ======================================== // CreateMeetingRequest for creating Jitsi meetings type CreateMeetingRequest struct { Type string `json:"type" binding:"required"` // "quick", "training", "parent_teacher", "class" Title string `json:"title,omitempty"` DisplayName string `json:"display_name"` Email string `json:"email,omitempty"` Duration int `json:"duration,omitempty"` // minutes ClassName string `json:"class_name,omitempty"` ParentName string `json:"parent_name,omitempty"` StudentName string `json:"student_name,omitempty"` Subject string `json:"subject,omitempty"` StartTime time.Time `json:"start_time,omitempty"` } // CreateMeeting creates a new Jitsi meeting func (h *CommunicationHandlers) CreateMeeting(c *gin.Context) { if h.jitsiService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Jitsi service not configured"}) return } var req CreateMeetingRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() var link *jitsi.MeetingLink var err error switch req.Type { case "quick": link, err = h.jitsiService.CreateQuickMeeting(ctx, req.DisplayName) case "training": link, err = h.jitsiService.CreateTrainingSession(ctx, req.Title, req.DisplayName, req.Email, req.Duration) case "parent_teacher": link, err = h.jitsiService.CreateParentTeacherMeeting(ctx, req.DisplayName, req.ParentName, req.StudentName, req.StartTime) case "class": link, err = h.jitsiService.CreateClassMeeting(ctx, req.ClassName, req.DisplayName, req.Subject) default: c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid meeting type. Use: quick, training, parent_teacher, class"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, gin.H{ "room_name": link.RoomName, "url": link.URL, "join_url": link.JoinURL, "moderator_url": link.ModeratorURL, "password": link.Password, "expires_at": link.ExpiresAt, }) } // GetEmbedURLRequest for embedding Jitsi type GetEmbedURLRequest struct { RoomName string `json:"room_name" binding:"required"` DisplayName string `json:"display_name"` AudioMuted bool `json:"audio_muted"` VideoMuted bool `json:"video_muted"` } // GetEmbedURL returns an embeddable Jitsi URL func (h *CommunicationHandlers) GetEmbedURL(c *gin.Context) { if h.jitsiService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Jitsi service not configured"}) return } var req GetEmbedURLRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } config := &jitsi.MeetingConfig{ StartWithAudioMuted: req.AudioMuted, StartWithVideoMuted: req.VideoMuted, DisableDeepLinking: true, } embedURL := h.jitsiService.BuildEmbedURL(req.RoomName, req.DisplayName, config) iframeCode := h.jitsiService.BuildIFrameCode(req.RoomName, 800, 600) c.JSON(http.StatusOK, gin.H{ "embed_url": embedURL, "iframe_code": iframeCode, }) } // GetJitsiInfo returns Jitsi server information func (h *CommunicationHandlers) GetJitsiInfo(c *gin.Context) { if h.jitsiService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "Jitsi service not configured"}) return } info := h.jitsiService.GetServerInfo() c.JSON(http.StatusOK, info) } // ======================================== // Admin Statistics Endpoints (for Admin Panel) // ======================================== // CommunicationStats holds communication service statistics type CommunicationStats struct { Matrix MatrixStats `json:"matrix"` Jitsi JitsiStats `json:"jitsi"` } // MatrixStats holds Matrix-specific statistics type MatrixStats struct { Enabled bool `json:"enabled"` Healthy bool `json:"healthy"` ServerName string `json:"server_name"` // TODO: Add real stats from Matrix Synapse Admin API TotalUsers int `json:"total_users"` TotalRooms int `json:"total_rooms"` ActiveToday int `json:"active_today"` MessagesToday int `json:"messages_today"` } // JitsiStats holds Jitsi-specific statistics type JitsiStats struct { Enabled bool `json:"enabled"` Healthy bool `json:"healthy"` BaseURL string `json:"base_url"` AuthEnabled bool `json:"auth_enabled"` // TODO: Add real stats from Jitsi SRTP API or Jicofo ActiveMeetings int `json:"active_meetings"` TotalParticipants int `json:"total_participants"` MeetingsToday int `json:"meetings_today"` AvgDurationMin int `json:"avg_duration_min"` } // GetAdminStats returns admin statistics for Matrix and Jitsi func (h *CommunicationHandlers) GetAdminStats(c *gin.Context) { ctx := c.Request.Context() stats := CommunicationStats{} // Matrix Stats if h.matrixService != nil { matrixErr := h.matrixService.HealthCheck(ctx) stats.Matrix = MatrixStats{ Enabled: true, Healthy: matrixErr == nil, ServerName: h.matrixService.GetServerName(), // Placeholder stats - in production these would come from Synapse Admin API TotalUsers: 0, TotalRooms: 0, ActiveToday: 0, MessagesToday: 0, } } else { stats.Matrix = MatrixStats{Enabled: false} } // Jitsi Stats if h.jitsiService != nil { jitsiErr := h.jitsiService.HealthCheck(ctx) serverInfo := h.jitsiService.GetServerInfo() stats.Jitsi = JitsiStats{ Enabled: true, Healthy: jitsiErr == nil, BaseURL: serverInfo["base_url"], AuthEnabled: serverInfo["auth_enabled"] == "true", // Placeholder stats - in production these would come from Jicofo/JVB stats ActiveMeetings: 0, TotalParticipants: 0, MeetingsToday: 0, AvgDurationMin: 0, } } else { stats.Jitsi = JitsiStats{Enabled: false} } c.JSON(http.StatusOK, stats) } // ======================================== // Helper Functions // ======================================== func errToString(err error) string { if err == nil { return "" } return err.Error() } // RegisterRoutes registers all communication routes func (h *CommunicationHandlers) RegisterRoutes(router *gin.RouterGroup, jwtSecret string, authMiddleware gin.HandlerFunc) { comm := router.Group("/communication") { // Public health check comm.GET("/status", h.GetCommunicationStatus) // Protected routes protected := comm.Group("") protected.Use(authMiddleware) { // Matrix protected.POST("/rooms", h.CreateRoom) protected.POST("/rooms/invite", h.InviteUser) protected.POST("/messages", h.SendMessage) protected.POST("/notifications", h.SendNotification) // Jitsi protected.POST("/meetings", h.CreateMeeting) protected.POST("/meetings/embed", h.GetEmbedURL) protected.GET("/jitsi/info", h.GetJitsiInfo) } // Admin routes (for Matrix user registration and stats) admin := comm.Group("/admin") admin.Use(authMiddleware) // TODO: Add AdminOnly middleware { admin.POST("/matrix/users", h.RegisterMatrixUser) admin.GET("/stats", h.GetAdminStats) } } }