Files
voice-cat/clients/windows/VoiceCat.App/Forms/ConnectDialog.cs
Talon bba605401d
Some checks failed
Build Linux Binaries / linux/amd64 (push) Has been cancelled
Build Linux Binaries / linux/arm64 (push) Has been cancelled
chore: comment cleanup pass ahead of open-sourcing
Removes leftover debug scaffolding (stray Console.WriteLine/NSLog traces,
dead nick_buf_ptr, a no-op --print-config flag now implemented for real),
fixes stale/misleading comments (channel passwords are no longer a "future
M5+" feature, a wrong cross-reference, a stale TlsContext::close() mention,
an incomplete BanRecord::subject_type doc, and a smoke test pointing at a
build/m1-dev preset that no longer exists), strips internal M1-M5 milestone
jargon from comments now that the roadmap is done, trims comments that just
restated the following line, and consolidates a few "why" explanations that
were duplicated 2-3 times in the same file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 10:20:18 +01:00

263 lines
9.0 KiB
C#

using VoiceCat.App.Models;
using VoiceCat.Interop;
namespace VoiceCat.App.Forms;
/// <summary>
/// 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).
/// </summary>
public partial class ConnectDialog : Form
{
private readonly List<SavedServer> _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);
}
}