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) { if (lstServers.SelectedItem is not SavedServer server) return; 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)!; Directory.CreateDirectory(tofuDir); _client = new VoiceCatClient("VoiceCat-Windows", VoiceCatClient.VersionString, VcLogLevel.Info, ServerListStore.TofuStorePath); _client.EventReceived += OnEvent; _identityDialogShown = false; _pumpTimer.Start(); var connectResult = _client.Connect(server.Host, server.Port); 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; _client.AuthenticateGuest(Nickname); } 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 ?? ""; _client.AuthenticateUser(server.SavedUsername ?? "", password); } } private void OnEvent(VoiceCatEvent 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) { if (_identityDialogShown) return; // one decision per connect attempt if (status == VcTofuStatus.Matched) { // Silent success path — no dialog. See ServerIdentityDialog's doc comment. _client!.ConfirmServerIdentity(true); return; } _identityDialogShown = true; using var dlg = new ServerIdentityDialog(status, certFingerprintHex, _client!.GetServerIdentityDisplay()); bool accept = dlg.ShowDialog(this) == DialogResult.OK; _client.ConfirmServerIdentity(accept); 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); } }