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
User: -
👇 Select a channel to continue
Status
Current User: -
Channel: -
Microphone: 🎤 Unmuted
Speaker: 🔊 On
`