Initial commit: breakpilot-core - Shared Infrastructure
Docker Compose with 24+ services: - PostgreSQL (PostGIS), Valkey, MinIO, Qdrant - Vault (PKI/TLS), Nginx (Reverse Proxy) - Backend Core API, Consent Service, Billing Service - RAG Service, Embedding Service - Gitea, Woodpecker CI/CD - Night Scheduler, Health Aggregator - Jitsi (Web/XMPP/JVB/Jicofo), Mailpit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
511
consent-service/internal/handlers/communication_handlers.go
Normal file
511
consent-service/internal/handlers/communication_handlers.go
Normal file
@@ -0,0 +1,511 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user