package main import ( "bufio" "context" "flag" "fmt" "os" "strconv" "strings" "time" "github.com/sorti/openspeak/internal/client" pb "github.com/sorti/openspeak/pkg/api/openspeak/v1" ) // ClientApp represents the CLI client application type ClientApp struct { grpcClient *client.GRPCClient currentUser string currentChannelID string channels []*pb.Channel selectedChannelIdx int micMuted bool speakerMuted bool running bool } func main() { host := flag.String("host", "localhost", "gRPC server host") port := flag.Int("port", 50051, "gRPC server port") flag.Parse() app := &ClientApp{ running: true, } fmt.Println("╔════════════════════════════════════════╗") fmt.Println("║ OpenSpeak - CLI Client ║") fmt.Println("║ Voice Communication Platform ║") fmt.Println("╚════════════════════════════════════════╝") fmt.Println() // Login if !app.login(host, port) { fmt.Println("Failed to login. Exiting.") return } fmt.Printf("✓ Logged in as: %s\n", app.currentUser) // Load channels if !app.loadChannels() { fmt.Println("Failed to load channels.") return } // Main REPL loop app.mainLoop() // Cleanup if app.grpcClient != nil { app.grpcClient.Close() } } // login connects to the server and authenticates func (app *ClientApp) login(host *string, port *int) bool { reader := bufio.NewReader(os.Stdin) // Get token from user fmt.Print("Enter admin token: ") token, _ := reader.ReadString('\n') token = strings.TrimSpace(token) if token == "" { fmt.Println("Token cannot be empty") return false } // Connect to gRPC server fmt.Printf("Connecting to %s:%d...\n", *host, *port) grpcClient, err := client.NewGRPCClient(*host, *port) if err != nil { fmt.Printf("Connection failed: %v\n", err) return false } // Authenticate ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() loginResp, err := grpcClient.Login(ctx, token) if err != nil { fmt.Printf("Authentication failed: %v\n", err) grpcClient.Close() return false } app.grpcClient = grpcClient app.currentUser = loginResp.UserId return true } // loadChannels fetches the channel list from the server func (app *ClientApp) loadChannels() bool { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := app.grpcClient.ListChannels(ctx) if err != nil { fmt.Printf("Failed to load channels: %v\n", err) return false } app.channels = resp.Channels fmt.Printf("✓ Loaded %d channels\n", len(app.channels)) return true } // mainLoop runs the interactive command loop func (app *ClientApp) mainLoop() { reader := bufio.NewReader(os.Stdin) fmt.Println() fmt.Println("Commands: list, select, join, leave, members, mute, status, help, quit") fmt.Println() for app.running { fmt.Print("> ") input, _ := reader.ReadString('\n') input = strings.TrimSpace(input) if input == "" { continue } parts := strings.Fields(input) cmd := strings.ToLower(parts[0]) switch cmd { case "list": app.listChannels() case "select": if len(parts) > 1 { app.selectChannel(parts[1]) } else { fmt.Println("Usage: select ") } case "join": app.joinChannel() case "leave": app.leaveChannel() case "members": app.listMembers() case "mute": app.toggleMute() case "status": app.showStatus() case "help": app.showHelp() case "quit", "exit": app.running = false default: fmt.Printf("Unknown command: %s. Type 'help' for available commands.\n", cmd) } } } // listChannels displays available channels func (app *ClientApp) listChannels() { if len(app.channels) == 0 { fmt.Println("No channels available") return } fmt.Println("\nAvailable Channels:") fmt.Println("─────────────────────────────────────────") for i, ch := range app.channels { marker := " " if i == app.selectedChannelIdx { marker = "►" } fmt.Printf("%s [%d] %s (%d members)\n", marker, i, ch.Name, len(ch.MemberIds)) } fmt.Println() } // selectChannel selects a channel for operations func (app *ClientApp) selectChannel(indexStr string) { index, err := strconv.Atoi(indexStr) if err != nil || index < 0 || index >= len(app.channels) { fmt.Println("Invalid channel index") return } app.selectedChannelIdx = index ch := app.channels[index] fmt.Printf("✓ Selected channel: %s\n", ch.Name) } // joinChannel joins the selected channel func (app *ClientApp) joinChannel() { if app.selectedChannelIdx < 0 || app.selectedChannelIdx >= len(app.channels) { fmt.Println("No channel selected. Use 'select' command first.") return } ch := app.channels[app.selectedChannelIdx] ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := app.grpcClient.JoinChannel(ctx, ch.Id) if err != nil { fmt.Printf("✗ Failed to join channel: %v\n", err) return } app.currentChannelID = ch.Id fmt.Printf("✓ Joined channel: %s\n", ch.Name) } // leaveChannel leaves the current channel func (app *ClientApp) leaveChannel() { if app.currentChannelID == "" { fmt.Println("Not in any channel") return } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := app.grpcClient.LeaveChannel(ctx, app.currentChannelID) if err != nil { fmt.Printf("✗ Failed to leave channel: %v\n", err) return } channelName := "" for _, ch := range app.channels { if ch.Id == app.currentChannelID { channelName = ch.Name break } } app.currentChannelID = "" fmt.Printf("✓ Left channel: %s\n", channelName) } // listMembers displays members of the selected channel func (app *ClientApp) listMembers() { if app.selectedChannelIdx < 0 || app.selectedChannelIdx >= len(app.channels) { fmt.Println("No channel selected. Use 'select' command first.") return } ch := app.channels[app.selectedChannelIdx] ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() resp, err := app.grpcClient.ListMembers(ctx, ch.Id) if err != nil { fmt.Printf("✗ Failed to load members: %v\n", err) return } fmt.Printf("\nMembers of '%s':\n", ch.Name) fmt.Println("─────────────────────────────────────────") if len(resp.MemberIds) == 0 { fmt.Println("No members") } else { for i, member := range resp.MemberIds { fmt.Printf("%d. %s\n", i+1, member) } } fmt.Println() } // toggleMute toggles microphone mute status func (app *ClientApp) toggleMute() { app.micMuted = !app.micMuted ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := app.grpcClient.SetMuteStatus(ctx, app.micMuted, app.speakerMuted) if err != nil { fmt.Printf("✗ Failed to set mute status: %v\n", err) return } status := "unmuted" if app.micMuted { status = "muted" } fmt.Printf("✓ Microphone %s\n", status) } // showStatus shows the current connection status func (app *ClientApp) showStatus() { fmt.Println("\nCurrent Status:") fmt.Println("─────────────────────────────────────────") fmt.Printf("User: %s\n", app.currentUser) if app.currentChannelID != "" { for _, ch := range app.channels { if ch.Id == app.currentChannelID { fmt.Printf("Current Channel: %s\n", ch.Name) break } } } else { fmt.Println("Current Channel: None") } micStatus := "🎤 Unmuted" if app.micMuted { micStatus = "🔇 Muted" } fmt.Printf("Microphone: %s\n", micStatus) speakerStatus := "🔊 On" if app.speakerMuted { speakerStatus = "🔇 Off" } fmt.Printf("Speaker: %s\n", speakerStatus) fmt.Println() } // showHelp displays available commands func (app *ClientApp) showHelp() { fmt.Println("\nAvailable Commands:") fmt.Println("─────────────────────────────────────────") fmt.Println("list - Show all available channels") fmt.Println("select - Select a channel (e.g., 'select 0')") fmt.Println("join - Join the selected channel") fmt.Println("leave - Leave the current channel") fmt.Println("members - List members of selected channel") fmt.Println("mute - Toggle microphone mute") fmt.Println("status - Show current connection status") fmt.Println("help - Show this help message") fmt.Println("quit/exit - Exit the application") fmt.Println() } // Helper to access selected channel func (app *ClientApp) selectedChannel() *pb.Channel { if app.selectedChannelIdx >= 0 && app.selectedChannelIdx < len(app.channels) { return app.channels[app.selectedChannelIdx] } return nil }