using VoiceCat.App.Models; using VoiceCat.Interop; namespace VoiceCat.App.Forms; /// /// Saved-server list + connect flow. On success, ConnectedClient/SelfUserId/Nickname are set /// and DialogResult == OK; the caller (Program.cs) takes ownership of ConnectedClient (does /// NOT dispose it here — MainForm owns its lifetime from that point on). /// public partial class ConnectDialog : Form { private readonly List _servers; private readonly System.Windows.Forms.Timer _pumpTimer = new() { Interval = 30 }; private VoiceCatClient? _client; private bool _identityDialogShown; public VoiceCatClient? ConnectedClient { get; private set; } public uint SelfUserId { get; private set; } public string Nickname { get; private set; } = ""; public string ServerName { get; private set; } = ""; public ConnectDialog() { InitializeComponent(); _servers = ServerListStore.Load(); RefreshServerList(); _pumpTimer.Tick += (_, _) => { try { _client?.PumpEvents(); } catch (Exception ex) { Console.Error.WriteLine($"[ConnectDialog] EXCEPTION in PumpEvents/event handler: {ex}"); } }; btnAddNew.Click += BtnAddNew_Click; btnEdit.Click += BtnEdit_Click; btnRemove.Click += BtnRemove_Click; btnConnect.Click += BtnConnect_Click; lstServers.DoubleClick += BtnConnect_Click; } private void RefreshServerList() { object? previouslySelected = lstServers.SelectedItem; lstServers.Items.Clear(); foreach (var s in _servers) lstServers.Items.Add(s); if (previouslySelected is not null && _servers.Contains(previouslySelected)) lstServers.SelectedItem = previouslySelected; else if (lstServers.Items.Count > 0) lstServers.SelectedIndex = 0; UpdateButtonsEnabled(); } private void UpdateButtonsEnabled() { bool hasSelection = lstServers.SelectedItem is not null; btnConnect.Enabled = hasSelection; btnEdit.Enabled = hasSelection; btnRemove.Enabled = hasSelection; } private void BtnAddNew_Click(object? sender, EventArgs e) { using var dlg = new AddServerDialog(); if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return; _servers.Add(dlg.Result); ServerListStore.Save(_servers); RefreshServerList(); lstServers.SelectedItem = dlg.Result; } private void BtnEdit_Click(object? sender, EventArgs e) { if (lstServers.SelectedItem is not SavedServer existing) return; using var dlg = new AddServerDialog(existing); if (dlg.ShowDialog(this) != DialogResult.OK) return; ServerListStore.Save(_servers); RefreshServerList(); } private void BtnRemove_Click(object? sender, EventArgs e) { if (lstServers.SelectedItem is not SavedServer existing) return; var confirm = MessageBox.Show(this, $"Remove '{existing}' from the saved-server list?", "VoiceCat", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (confirm != DialogResult.Yes) return; _servers.Remove(existing); ServerListStore.Save(_servers); RefreshServerList(); } private void BtnConnect_Click(object? sender, EventArgs e) { Console.WriteLine("[ConnectDialog] BtnConnect_Click fired"); if (lstServers.SelectedItem is not SavedServer server) { Console.WriteLine("[ConnectDialog] no SavedServer selected — ignoring click"); return; } Console.WriteLine($"[ConnectDialog] selected server: Host={server.Host} Port={server.Port} AuthMode={server.AuthMode}"); try { StartConnect(server); } catch (Exception ex) { Console.Error.WriteLine($"[ConnectDialog] EXCEPTION in StartConnect: {ex}"); lblStatus.Text = $"Internal error: {ex.Message}"; CleanupFailedAttempt(); } } private void StartConnect(SavedServer server) { SetBusy(true); lblStatus.Text = "Connecting..."; ServerName = string.IsNullOrWhiteSpace(server.DisplayName) ? $"{server.Host}:{server.Port}" : server.DisplayName; string tofuDir = Path.GetDirectoryName(ServerListStore.TofuStorePath)!; Console.WriteLine($"[ConnectDialog] tofu store dir: {tofuDir}"); Directory.CreateDirectory(tofuDir); Console.WriteLine("[ConnectDialog] creating VoiceCatClient..."); _client = new VoiceCatClient("VoiceCat-Windows", VoiceCatClient.VersionString, VcLogLevel.Info, ServerListStore.TofuStorePath); Console.WriteLine("[ConnectDialog] VoiceCatClient created OK"); _client.EventReceived += OnEvent; _identityDialogShown = false; _pumpTimer.Start(); Console.WriteLine($"[ConnectDialog] pump timer started, Enabled={_pumpTimer.Enabled}, Interval={_pumpTimer.Interval}"); Console.WriteLine($"[ConnectDialog] calling Connect({server.Host}, {server.Port})..."); var connectResult = _client.Connect(server.Host, server.Port); Console.WriteLine($"[ConnectDialog] Connect() returned {connectResult}"); if (connectResult != VcResult.Ok) { lblStatus.Text = $"Connect failed: {connectResult}"; CleanupFailedAttempt(); return; } if (server.AuthMode == AuthMode.Guest) { Nickname = string.IsNullOrWhiteSpace(server.LastNickname) ? Environment.UserName : server.LastNickname; Console.WriteLine($"[ConnectDialog] calling AuthenticateGuest({Nickname})..."); var authResult = _client.AuthenticateGuest(Nickname); Console.WriteLine($"[ConnectDialog] AuthenticateGuest() returned {authResult}"); } else { string password; if (server.ProtectedPasswordBase64 is not null) { password = PasswordProtector.Unprotect(server.ProtectedPasswordBase64); } else { using var pwDlg = new PasswordPromptDialog($"Password for {server.SavedUsername}@{server.Host}:"); if (pwDlg.ShowDialog(this) != DialogResult.OK) { lblStatus.Text = "Cancelled."; CleanupFailedAttempt(); return; } password = pwDlg.Password; } Nickname = server.SavedUsername ?? ""; Console.WriteLine($"[ConnectDialog] calling AuthenticateUser({Nickname})..."); var authResult = _client.AuthenticateUser(server.SavedUsername ?? "", password); Console.WriteLine($"[ConnectDialog] AuthenticateUser() returned {authResult}"); } } private void OnEvent(VoiceCatEvent ev) { Console.WriteLine($"[ConnectDialog] event: {ev}"); switch (ev.Type) { case VcEventType.ConnectionState: lblStatus.Text = ev.ConnectionState switch { VcConnectionState.Connecting => "Connecting...", VcConnectionState.TlsHandshake => "TLS handshake...", VcConnectionState.VerifyingIdentity => "Verifying server identity...", VcConnectionState.Authenticating => "Authenticating...", VcConnectionState.Connected => "Connected.", _ => lblStatus.Text, }; break; case VcEventType.ServerIdentity: HandleServerIdentity((VcTofuStatus)ev.U32a, ev.Text ?? ""); break; case VcEventType.AuthResult: if (ev.Result == VcResult.Ok) { SelfUserId = ev.UserId; ConnectedClient = _client; _pumpTimer.Stop(); _client!.EventReceived -= OnEvent; DialogResult = DialogResult.OK; Close(); } else { lblStatus.Text = $"Authentication failed: {ev.Text}"; CleanupFailedAttempt(); } break; case VcEventType.Disconnected: if (ConnectedClient is null) { lblStatus.Text = string.IsNullOrEmpty(ev.Text) ? "Disconnected." : $"Disconnected: {ev.Text}"; CleanupFailedAttempt(); } break; } } private void HandleServerIdentity(VcTofuStatus status, string certFingerprintHex) { Console.WriteLine($"[ConnectDialog] HandleServerIdentity status={status} fp={certFingerprintHex} alreadyShown={_identityDialogShown}"); if (_identityDialogShown) return; // one decision per connect attempt if (status == VcTofuStatus.Matched) { // Silent success path — no dialog. See ServerIdentityDialog's doc comment. Console.WriteLine("[ConnectDialog] status=Matched -> auto-confirming, no dialog"); _client!.ConfirmServerIdentity(true); return; } _identityDialogShown = true; Console.WriteLine("[ConnectDialog] showing ServerIdentityDialog..."); using var dlg = new ServerIdentityDialog(status, certFingerprintHex, _client!.GetServerIdentityDisplay()); var dlgResult = dlg.ShowDialog(this); Console.WriteLine($"[ConnectDialog] ServerIdentityDialog closed with {dlgResult}"); bool accept = dlgResult == DialogResult.OK; var confirmResult = _client.ConfirmServerIdentity(accept); Console.WriteLine($"[ConnectDialog] ConfirmServerIdentity({accept}) returned {confirmResult}"); if (!accept) lblStatus.Text = "Server identity rejected."; } private void CleanupFailedAttempt() { _pumpTimer.Stop(); if (_client is not null) { _client.EventReceived -= OnEvent; _client.Dispose(); _client = null; } SetBusy(false); } private void SetBusy(bool busy) { lstServers.Enabled = !busy; btnConnect.Enabled = !busy && lstServers.SelectedItem is not null; btnAddNew.Enabled = !busy; btnEdit.Enabled = !busy && lstServers.SelectedItem is not null; btnRemove.Enabled = !busy && lstServers.SelectedItem is not null; } protected override void OnFormClosing(FormClosingEventArgs e) { if (DialogResult != DialogResult.OK) { _pumpTimer.Stop(); if (_client is not null) { _client.EventReceived -= OnEvent; _client.Dispose(); _client = null; } } base.OnFormClosing(e); } }