package main import ( "context" "encoding/json" "flag" "fmt" "log" "net/http" "sync" "time" "github.com/sorti/openspeak/internal/client" pb "github.com/sorti/openspeak/pkg/api/openspeak/v1" ) // GUIApp represents the GUI application state type GUIApp struct { grpcClient *client.GRPCClient currentUser string token string channels []*pb.Channel selectedChannelID string micMuted bool speakerMuted bool mu sync.RWMutex } var guiApp *GUIApp func main() { port := flag.Int("port", 8080, "HTTP server port") flag.Parse() guiApp = &GUIApp{} // Serve static files and API endpoints http.HandleFunc("/", serveHTML) http.HandleFunc("/api/login", handleLogin) http.HandleFunc("/api/channels", handleChannels) http.HandleFunc("/api/join", handleJoin) http.HandleFunc("/api/leave", handleLeave) http.HandleFunc("/api/mute", handleMute) http.HandleFunc("/api/members", handleMembers) http.HandleFunc("/api/status", handleStatus) addr := fmt.Sprintf(":%d", *port) log.Printf("OpenSpeak GUI Server starting on http://localhost:%d", *port) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatal(err) } } // serveHTML serves the main HTML page func serveHTML(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, htmlContent) } // handleLogin authenticates with the server func handleLogin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { Host string `json:"host"` Port int `json:"port"` Token string `json:"token"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } // Connect to gRPC server grpcClient, err := client.NewGRPCClient(req.Host, req.Port) if err != nil { json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("Connection failed: %v", err)}) return } // Authenticate ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() loginResp, err := grpcClient.Login(ctx, req.Token) if err != nil { grpcClient.Close() json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("Authentication failed: %v", err)}) return } // Store client, user info, and token guiApp.mu.Lock() guiApp.grpcClient = grpcClient guiApp.currentUser = loginResp.UserId guiApp.token = req.Token guiApp.mu.Unlock() // Load channels (token is already stored in grpcClient and will be added by ListChannels) ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := grpcClient.ListChannels(ctx) if err != nil { json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("Failed to load channels: %v", err)}) return } guiApp.mu.Lock() guiApp.channels = resp.Channels guiApp.mu.Unlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "user": guiApp.currentUser, "channels": len(resp.Channels), }) } // handleChannels returns the list of channels func handleChannels(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } guiApp.mu.RLock() channels := guiApp.channels guiApp.mu.RUnlock() if channels == nil { http.Error(w, "Not logged in", http.StatusUnauthorized) return } type ChannelInfo struct { ID string `json:"id"` Name string `json:"name"` Members int `json:"members"` IsCurrent bool `json:"isCurrent"` } guiApp.mu.RLock() currentChannelID := guiApp.selectedChannelID guiApp.mu.RUnlock() var result []ChannelInfo for _, ch := range channels { result = append(result, ChannelInfo{ ID: ch.Id, Name: ch.Name, Members: len(ch.MemberIds), IsCurrent: ch.Id == currentChannelID, }) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) } // handleJoin joins a channel func handleJoin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { ChannelID string `json:"channelId"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } guiApp.mu.Lock() grpcClient := guiApp.grpcClient guiApp.mu.Unlock() if grpcClient == nil { http.Error(w, "Not logged in", http.StatusUnauthorized) return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := grpcClient.JoinChannel(ctx, req.ChannelID) if err != nil { json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("Failed to join: %v", err)}) return } guiApp.mu.Lock() guiApp.selectedChannelID = req.ChannelID guiApp.mu.Unlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"success": true}) } // handleLeave leaves a channel func handleLeave(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } guiApp.mu.Lock() grpcClient := guiApp.grpcClient channelID := guiApp.selectedChannelID guiApp.mu.Unlock() if grpcClient == nil || channelID == "" { http.Error(w, "Not in a channel", http.StatusBadRequest) return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := grpcClient.LeaveChannel(ctx, channelID) if err != nil { json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("Failed to leave: %v", err)}) return } guiApp.mu.Lock() guiApp.selectedChannelID = "" guiApp.mu.Unlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"success": true}) } // handleMute toggles microphone mute func handleMute(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } guiApp.mu.Lock() grpcClient := guiApp.grpcClient guiApp.micMuted = !guiApp.micMuted micMuted := guiApp.micMuted speakerMuted := guiApp.speakerMuted guiApp.mu.Unlock() if grpcClient == nil { http.Error(w, "Not logged in", http.StatusUnauthorized) return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := grpcClient.SetMuteStatus(ctx, micMuted, speakerMuted) if err != nil { json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("Failed to set mute: %v", err)}) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "micMuted": micMuted, "speakerMuted": speakerMuted, }) } // handleMembers returns members of a channel func handleMembers(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } channelID := r.URL.Query().Get("id") if channelID == "" { http.Error(w, "Missing channel id", http.StatusBadRequest) return } guiApp.mu.RLock() grpcClient := guiApp.grpcClient guiApp.mu.RUnlock() if grpcClient == nil { http.Error(w, "Not logged in", http.StatusUnauthorized) return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := grpcClient.ListMembers(ctx, channelID) if err != nil { json.NewEncoder(w).Encode(map[string]interface{}{"error": fmt.Sprintf("Failed to load members: %v", err)}) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "members": resp.MemberIds, }) } // handleStatus returns current status func handleStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } guiApp.mu.RLock() defer guiApp.mu.RUnlock() if guiApp.grpcClient == nil { http.Error(w, "Not logged in", http.StatusUnauthorized) return } var currentChannelName string for _, ch := range guiApp.channels { if ch.Id == guiApp.selectedChannelID { currentChannelName = ch.Name break } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "user": guiApp.currentUser, "currentChannel": currentChannelName, "micMuted": guiApp.micMuted, "speakerMuted": guiApp.speakerMuted, }) } const htmlContent = ` OpenSpeak - Voice Communication Platform

🎤 OpenSpeak

Voice Communication Platform

🎤 OpenSpeak

User: -
👇 Select a channel to continue
`