A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
341 lines
10 KiB
Go
341 lines
10 KiB
Go
package jitsi
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// ========================================
|
|
// Breakpilot Drive Game Meeting Types
|
|
// ========================================
|
|
|
|
// GameMeetingMode represents different game video call modes
|
|
type GameMeetingMode string
|
|
|
|
const (
|
|
GameMeetingCoop GameMeetingMode = "coop" // Co-Op voice/video
|
|
GameMeetingChallenge GameMeetingMode = "challenge" // 1v1 face-off
|
|
GameMeetingClassRace GameMeetingMode = "class_race" // Teacher supervises
|
|
GameMeetingTeamHuddle GameMeetingMode = "team_huddle" // Quick team sync
|
|
)
|
|
|
|
// GameMeetingConfig holds configuration for game video meetings
|
|
type GameMeetingConfig struct {
|
|
SessionID string `json:"session_id"`
|
|
Mode GameMeetingMode `json:"mode"`
|
|
HostID string `json:"host_id"`
|
|
HostName string `json:"host_name"`
|
|
Players []GamePlayer `json:"players"`
|
|
EnableVideo bool `json:"enable_video"`
|
|
EnableVoice bool `json:"enable_voice"`
|
|
TeacherID string `json:"teacher_id,omitempty"`
|
|
TeacherName string `json:"teacher_name,omitempty"`
|
|
ClassName string `json:"class_name,omitempty"`
|
|
}
|
|
|
|
// GamePlayer represents a player in the meeting
|
|
type GamePlayer struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
IsModerator bool `json:"is_moderator,omitempty"`
|
|
}
|
|
|
|
// GameMeetingLink extends MeetingLink with game-specific info
|
|
type GameMeetingLink struct {
|
|
*MeetingLink
|
|
SessionID string `json:"session_id"`
|
|
Mode GameMeetingMode `json:"mode"`
|
|
Players []string `json:"players"`
|
|
}
|
|
|
|
// ========================================
|
|
// Game Meeting Creation
|
|
// ========================================
|
|
|
|
// CreateCoopMeeting creates a video call for Co-Op gameplay (2-4 players)
|
|
func (s *JitsiService) CreateCoopMeeting(ctx context.Context, config GameMeetingConfig) (*GameMeetingLink, error) {
|
|
roomName := fmt.Sprintf("bp-coop-%s", config.SessionID[:8])
|
|
|
|
meeting := Meeting{
|
|
RoomName: roomName,
|
|
DisplayName: config.HostName,
|
|
Subject: "Breakpilot Drive - Co-Op Session",
|
|
Moderator: true,
|
|
Config: &MeetingConfig{
|
|
StartWithAudioMuted: !config.EnableVoice,
|
|
StartWithVideoMuted: !config.EnableVideo,
|
|
RequireDisplayName: true,
|
|
EnableLobby: false, // Direct join for co-op
|
|
DisableDeepLinking: true,
|
|
},
|
|
}
|
|
|
|
link, err := s.CreateMeetingLink(ctx, meeting)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create co-op meeting: %w", err)
|
|
}
|
|
|
|
playerIDs := make([]string, len(config.Players))
|
|
for i, p := range config.Players {
|
|
playerIDs[i] = p.ID
|
|
}
|
|
|
|
return &GameMeetingLink{
|
|
MeetingLink: link,
|
|
SessionID: config.SessionID,
|
|
Mode: GameMeetingCoop,
|
|
Players: playerIDs,
|
|
}, nil
|
|
}
|
|
|
|
// CreateChallengeMeeting creates a 1v1 video call for challenges
|
|
func (s *JitsiService) CreateChallengeMeeting(ctx context.Context, config GameMeetingConfig, challengerName string, opponentName string) (*GameMeetingLink, error) {
|
|
roomName := fmt.Sprintf("bp-challenge-%s", config.SessionID[:8])
|
|
|
|
meeting := Meeting{
|
|
RoomName: roomName,
|
|
DisplayName: challengerName,
|
|
Subject: fmt.Sprintf("Challenge: %s vs %s", challengerName, opponentName),
|
|
Moderator: false, // Both players are equal
|
|
Config: &MeetingConfig{
|
|
StartWithAudioMuted: false, // Voice enabled for trash talk
|
|
StartWithVideoMuted: !config.EnableVideo,
|
|
RequireDisplayName: true,
|
|
EnableLobby: false,
|
|
DisableDeepLinking: true,
|
|
},
|
|
}
|
|
|
|
link, err := s.CreateMeetingLink(ctx, meeting)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create challenge meeting: %w", err)
|
|
}
|
|
|
|
return &GameMeetingLink{
|
|
MeetingLink: link,
|
|
SessionID: config.SessionID,
|
|
Mode: GameMeetingChallenge,
|
|
Players: []string{config.HostID},
|
|
}, nil
|
|
}
|
|
|
|
// CreateClassRaceMeeting creates a video call for teacher-supervised class races
|
|
func (s *JitsiService) CreateClassRaceMeeting(ctx context.Context, config GameMeetingConfig) (*GameMeetingLink, error) {
|
|
roomName := fmt.Sprintf("bp-klasse-%s-%s",
|
|
s.sanitizeRoomName(config.ClassName),
|
|
time.Now().Format("150405"))
|
|
|
|
// Teacher is moderator
|
|
meeting := Meeting{
|
|
RoomName: roomName,
|
|
DisplayName: config.TeacherName,
|
|
Subject: fmt.Sprintf("Klassenrennen: %s", config.ClassName),
|
|
Moderator: true,
|
|
Config: &MeetingConfig{
|
|
StartWithAudioMuted: true, // Students muted by default
|
|
StartWithVideoMuted: true, // Video off for performance
|
|
RequireDisplayName: true,
|
|
EnableLobby: true, // Teacher admits students
|
|
EnableRecording: false, // No recording for minors
|
|
DisableDeepLinking: true,
|
|
},
|
|
Features: &MeetingFeatures{
|
|
Recording: false,
|
|
Transcription: false,
|
|
},
|
|
}
|
|
|
|
link, err := s.CreateMeetingLink(ctx, meeting)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create class race meeting: %w", err)
|
|
}
|
|
|
|
playerIDs := make([]string, len(config.Players))
|
|
for i, p := range config.Players {
|
|
playerIDs[i] = p.ID
|
|
}
|
|
|
|
return &GameMeetingLink{
|
|
MeetingLink: link,
|
|
SessionID: config.SessionID,
|
|
Mode: GameMeetingClassRace,
|
|
Players: playerIDs,
|
|
}, nil
|
|
}
|
|
|
|
// CreateTeamHuddleMeeting creates a quick sync meeting for teams
|
|
func (s *JitsiService) CreateTeamHuddleMeeting(ctx context.Context, config GameMeetingConfig, teamName string) (*GameMeetingLink, error) {
|
|
roomName := fmt.Sprintf("bp-team-%s-%s",
|
|
s.sanitizeRoomName(teamName),
|
|
config.SessionID[:8])
|
|
|
|
meeting := Meeting{
|
|
RoomName: roomName,
|
|
DisplayName: config.HostName,
|
|
Subject: fmt.Sprintf("Team %s - Huddle", teamName),
|
|
Duration: 5, // Short 5-minute huddles
|
|
Moderator: true,
|
|
Config: &MeetingConfig{
|
|
StartWithAudioMuted: false, // Voice on for quick sync
|
|
StartWithVideoMuted: true, // Video optional
|
|
RequireDisplayName: true,
|
|
EnableLobby: false,
|
|
DisableDeepLinking: true,
|
|
},
|
|
}
|
|
|
|
link, err := s.CreateMeetingLink(ctx, meeting)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create team huddle: %w", err)
|
|
}
|
|
|
|
playerIDs := make([]string, len(config.Players))
|
|
for i, p := range config.Players {
|
|
playerIDs[i] = p.ID
|
|
}
|
|
|
|
return &GameMeetingLink{
|
|
MeetingLink: link,
|
|
SessionID: config.SessionID,
|
|
Mode: GameMeetingTeamHuddle,
|
|
Players: playerIDs,
|
|
}, nil
|
|
}
|
|
|
|
// ========================================
|
|
// Game-Specific Meeting Configurations
|
|
// ========================================
|
|
|
|
// GetGameEmbedConfig returns optimized config for embedding in Unity WebGL
|
|
func (s *JitsiService) GetGameEmbedConfig(enableVideo bool, enableVoice bool) *MeetingConfig {
|
|
return &MeetingConfig{
|
|
StartWithAudioMuted: !enableVoice,
|
|
StartWithVideoMuted: !enableVideo,
|
|
RequireDisplayName: true,
|
|
EnableLobby: false,
|
|
DisableDeepLinking: true, // Important for iframe embedding
|
|
}
|
|
}
|
|
|
|
// BuildGameEmbedURL creates a URL optimized for Unity WebGL embedding
|
|
func (s *JitsiService) BuildGameEmbedURL(roomName string, playerName string, enableVideo bool, enableVoice bool) string {
|
|
config := s.GetGameEmbedConfig(enableVideo, enableVoice)
|
|
return s.BuildEmbedURL(roomName, playerName, config)
|
|
}
|
|
|
|
// BuildUnityIFrameParams returns parameters for Unity's WebGL iframe
|
|
func (s *JitsiService) BuildUnityIFrameParams(link *GameMeetingLink, playerName string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
"domain": s.extractDomain(),
|
|
"roomName": link.RoomName,
|
|
"displayName": playerName,
|
|
"jwt": link.JWT,
|
|
"configOverwrite": map[string]interface{}{
|
|
"startWithAudioMuted": false,
|
|
"startWithVideoMuted": true,
|
|
"disableDeepLinking": true,
|
|
"prejoinPageEnabled": false,
|
|
"enableWelcomePage": false,
|
|
"enableClosePage": false,
|
|
"disableInviteFunctions": true,
|
|
},
|
|
"interfaceConfigOverwrite": map[string]interface{}{
|
|
"DISABLE_JOIN_LEAVE_NOTIFICATIONS": true,
|
|
"MOBILE_APP_PROMO": false,
|
|
"SHOW_CHROME_EXTENSION_BANNER": false,
|
|
"TOOLBAR_BUTTONS": []string{
|
|
"microphone", "camera", "hangup", "chat",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Spectator Mode (for teachers/parents)
|
|
// ========================================
|
|
|
|
// CreateSpectatorLink creates a view-only link for observers
|
|
func (s *JitsiService) CreateSpectatorLink(ctx context.Context, roomName string, spectatorName string) (*MeetingLink, error) {
|
|
meeting := Meeting{
|
|
RoomName: roomName,
|
|
DisplayName: fmt.Sprintf("[Zuschauer] %s", spectatorName),
|
|
Moderator: false,
|
|
Config: &MeetingConfig{
|
|
StartWithAudioMuted: true,
|
|
StartWithVideoMuted: true,
|
|
DisableDeepLinking: true,
|
|
},
|
|
}
|
|
|
|
return s.CreateMeetingLink(ctx, meeting)
|
|
}
|
|
|
|
// ========================================
|
|
// Helper Functions
|
|
// ========================================
|
|
|
|
// extractDomain extracts the domain from baseURL
|
|
func (s *JitsiService) extractDomain() string {
|
|
// Remove protocol prefix
|
|
domain := s.baseURL
|
|
if len(domain) > 8 && domain[:8] == "https://" {
|
|
domain = domain[8:]
|
|
} else if len(domain) > 7 && domain[:7] == "http://" {
|
|
domain = domain[7:]
|
|
}
|
|
// Remove port if present
|
|
for i, c := range domain {
|
|
if c == ':' || c == '/' {
|
|
domain = domain[:i]
|
|
break
|
|
}
|
|
}
|
|
return domain
|
|
}
|
|
|
|
// ValidateGameMeetingConfig validates configuration before creating meeting
|
|
func ValidateGameMeetingConfig(config GameMeetingConfig) error {
|
|
if config.SessionID == "" {
|
|
return fmt.Errorf("session_id is required")
|
|
}
|
|
|
|
if config.Mode == "" {
|
|
return fmt.Errorf("mode is required")
|
|
}
|
|
|
|
if config.HostID == "" {
|
|
return fmt.Errorf("host_id is required")
|
|
}
|
|
|
|
if config.HostName == "" {
|
|
return fmt.Errorf("host_name is required")
|
|
}
|
|
|
|
switch config.Mode {
|
|
case GameMeetingCoop:
|
|
if len(config.Players) < 2 || len(config.Players) > 4 {
|
|
return fmt.Errorf("co-op mode requires 2-4 players")
|
|
}
|
|
case GameMeetingChallenge:
|
|
if len(config.Players) != 2 {
|
|
return fmt.Errorf("challenge mode requires exactly 2 players")
|
|
}
|
|
case GameMeetingClassRace:
|
|
if config.TeacherID == "" || config.TeacherName == "" {
|
|
return fmt.Errorf("class race mode requires teacher info")
|
|
}
|
|
if config.ClassName == "" {
|
|
return fmt.Errorf("class race mode requires class name")
|
|
}
|
|
case GameMeetingTeamHuddle:
|
|
if len(config.Players) < 2 {
|
|
return fmt.Errorf("team huddle requires at least 2 players")
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown game meeting mode: %s", config.Mode)
|
|
}
|
|
|
|
return nil
|
|
}
|