This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/consent-service/internal/services/jitsi/game_meetings.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

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
}