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>
This commit is contained in:
@@ -61,7 +61,7 @@ public enum VoiceCatConnectionState: UInt32, Sendable, Equatable {
|
||||
case tlsHandshake = 2
|
||||
case authenticating = 3
|
||||
case connected = 4
|
||||
/// M4: handshake succeeded, waiting on `confirmServerIdentity()`.
|
||||
/// Handshake succeeded, waiting on `confirmServerIdentity()`.
|
||||
case verifyingIdentity = 5
|
||||
|
||||
public init(_ cValue: vc_connection_state) {
|
||||
@@ -125,13 +125,13 @@ public enum VoiceCatEventType: UInt32, Sendable, Equatable {
|
||||
case talkState = 9
|
||||
case error = 10
|
||||
case disconnected = 11
|
||||
/// M4: reply to `joinChannel()` — see `VoiceCatEvent.result` / `.channelId`.
|
||||
/// Reply to `joinChannel()` — see `VoiceCatEvent.result` / `.channelId`.
|
||||
case joinResult = 12
|
||||
/// M4: the TOFU server-identity gate — see `VoiceCatEvent.tofuStatus` / `.text`.
|
||||
/// The TOFU server-identity gate — see `VoiceCatEvent.tofuStatus` / `.text`.
|
||||
case serverIdentity = 13
|
||||
/// M5: async result for moderation/admin/channel operations.
|
||||
/// Async result for moderation/admin/channel operations.
|
||||
case genericResult = 14
|
||||
/// M5: reply to `requestAccountList()` — call `listAccounts()` to read.
|
||||
/// Reply to `requestAccountList()` — call `listAccounts()` to read.
|
||||
case accountList = 15
|
||||
/// Voice-plane subscription state. `u32a` = 1 (subscribed) or 0 (unsubscribed).
|
||||
case voiceState = 16
|
||||
|
||||
@@ -75,7 +75,7 @@ public struct User: Sendable, Equatable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Permission bitset — mirrors `vc_permissions` (M5).
|
||||
/// Permission bitset — mirrors `vc_permissions`.
|
||||
public struct Permissions: Sendable, Equatable {
|
||||
public let canCreateTempChannel: Bool
|
||||
public let canKick: Bool
|
||||
@@ -91,7 +91,7 @@ public struct Permissions: Sendable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Account entry — mirrors `vc_account` (M5, reply to `listAccounts()`).
|
||||
/// Account entry — mirrors `vc_account` (reply to `listAccounts()`).
|
||||
public struct Account: Sendable, Equatable {
|
||||
public let username: String
|
||||
public let isAdmin: Bool
|
||||
|
||||
@@ -219,7 +219,7 @@ public final class VoiceCatClient {
|
||||
VoiceCatResult(vc_authenticate_user(handle, username, password))
|
||||
}
|
||||
|
||||
// MARK: - TOFU server-identity gate (M4)
|
||||
// MARK: - TOFU server-identity gate
|
||||
|
||||
/// Accept or reject the pending server-identity check. Call after a `.serverIdentity`
|
||||
/// event. `accept=true` on firstConnect/mismatch updates the pin file and proceeds;
|
||||
@@ -413,7 +413,7 @@ public final class VoiceCatClient {
|
||||
|
||||
/// Global playback volume applied after mixing all remote streams. gain 0.0 = silent,
|
||||
/// 1.0 = unity (default), >1.0 amplifies. Always LOCAL — no protocol traffic. Mirrors the
|
||||
/// Windows client's `SetOutputVolume` and the C ABI `vc_set_output_volume` added in M5.
|
||||
/// Windows client's `SetOutputVolume` and the C ABI `vc_set_output_volume`.
|
||||
@discardableResult
|
||||
public func setOutputVolume(_ gain: Float) -> VoiceCatResult {
|
||||
VoiceCatResult(vc_set_output_volume(handle, gain < 0 ? 0 : gain))
|
||||
@@ -500,7 +500,7 @@ public final class VoiceCatClient {
|
||||
return Marshaling.devices(&native)
|
||||
}
|
||||
|
||||
// MARK: - M5: Moderation
|
||||
// MARK: - Moderation
|
||||
|
||||
@discardableResult
|
||||
public func kickUser(_ userId: UInt32, reason: String? = nil) -> VoiceCatResult {
|
||||
@@ -535,7 +535,7 @@ public final class VoiceCatClient {
|
||||
VoiceCatResult(vc_move_user(handle, userId, channelId))
|
||||
}
|
||||
|
||||
// MARK: - M5: Channel admin
|
||||
// MARK: - Channel admin
|
||||
|
||||
@discardableResult
|
||||
public func createChannel(_ info: ChannelEdit) -> VoiceCatResult {
|
||||
@@ -558,7 +558,7 @@ public final class VoiceCatClient {
|
||||
VoiceCatResult(vc_delete_channel(handle, channelId))
|
||||
}
|
||||
|
||||
// MARK: - M5: Account admin
|
||||
// MARK: - Account admin
|
||||
|
||||
@discardableResult
|
||||
public func createAccount(_ username: String, password: String) -> VoiceCatResult {
|
||||
|
||||
@@ -64,7 +64,7 @@ private final class ServerHarness {
|
||||
}
|
||||
self.port = port
|
||||
|
||||
// Provision a known admin account for moderation/admin tests (M5).
|
||||
// Provision a known admin account for moderation/admin tests.
|
||||
let adminURL = URL(fileURLWithPath: repoRoot)
|
||||
.appendingPathComponent("build/dev/bin/voicecat-admin")
|
||||
guard FileManager.default.isExecutableFile(atPath: adminURL.path) else {
|
||||
@@ -252,12 +252,12 @@ final class VoiceCatClientSmokeTests: XCTestCase {
|
||||
XCTAssertTrue(channels.contains { $0.id == 1 && $0.name == "Lobby" },
|
||||
"expected Lobby (channel 1) in \(channels.map { $0.name })")
|
||||
|
||||
// M5: permissions getter round-trip.
|
||||
// Permissions getter round-trip.
|
||||
let perms = client.getPermissions()
|
||||
XCTAssertFalse(perms.isAdmin)
|
||||
XCTAssertFalse(perms.canKick)
|
||||
|
||||
// M5: guest ListAccounts is rejected by the server with a GenericResult — proves the
|
||||
// Guest ListAccounts is rejected by the server with a GenericResult — proves the
|
||||
// moderation wrapper path works end-to-end through the Swift interop layer.
|
||||
events.removeAll()
|
||||
XCTAssertEqual(client.requestAccountList(), .ok)
|
||||
|
||||
@@ -116,16 +116,6 @@ final class AppState {
|
||||
ServerListStore.shared.save(servers)
|
||||
}
|
||||
|
||||
// MARK: - Teardown
|
||||
|
||||
/// `AppState` is the @Observable app root owned by the SwiftUI `App`; it lives for the
|
||||
/// whole app process and is torn down only on process exit, at which point OS cleanup
|
||||
/// suffices. Reconnect state (the in-flight `Task` and the `NWPathMonitor`) is cancelled
|
||||
/// via `cancelReconnect()` driven by `disconnect()`/`cancelConnect()` and on successful
|
||||
/// restore — those run on user-initiated teardown, which is the only path that matters.
|
||||
/// (The `Task` captures `[weak self]` and guards on `nil`/`userInitiatedDisconnect`, so a
|
||||
/// stray task left running when AppState is gone is a no-op; the monitor similarly guards.)
|
||||
|
||||
// MARK: - Connect flow
|
||||
|
||||
/// Public connect entry. Always starts a fresh session (no restore).
|
||||
@@ -245,6 +235,13 @@ final class AppState {
|
||||
/// Cancel any in-flight reconnect task and stop the path monitor. Safe to call when nothing
|
||||
/// is armed (no-op). Does NOT touch `userInitiatedDisconnect` or `lastSession` — callers set
|
||||
/// those as needed (disconnect/cancelConnect clear them; scheduleReconnect keeps them).
|
||||
///
|
||||
/// `AppState` is the @Observable app root owned by the SwiftUI `App`; it lives for the
|
||||
/// whole app process and is torn down only on process exit, at which point OS cleanup
|
||||
/// suffices. This method is driven by `disconnect()`/`cancelConnect()` and on successful
|
||||
/// restore — those run on user-initiated teardown, which is the only path that matters.
|
||||
/// (The reconnect `Task` captures `[weak self]` and guards on `nil`/`userInitiatedDisconnect`,
|
||||
/// so a stray task left running when AppState is gone is a no-op; the monitor similarly guards.)
|
||||
private func cancelReconnect() {
|
||||
reconnectTask?.cancel()
|
||||
reconnectTask = nil
|
||||
@@ -310,15 +307,12 @@ final class AppState {
|
||||
if prevSig == nil { return }
|
||||
|
||||
if self.session != nil {
|
||||
// Connected — tear down + reconnect on a meaningful path change.
|
||||
// `.unsatisfied` (all radios off) OR a primary-interface change (Wi-Fi↔cellular)
|
||||
// almost always breaks the live TCP connection; reconnecting proactively beats
|
||||
// waiting for the C core's keepalive/reaper timeout.
|
||||
// See this method's doc comment for why these conditions trigger a
|
||||
// proactive reconnect.
|
||||
if path.status != .satisfied || sig != prevSig {
|
||||
self.proactiveReconnect()
|
||||
}
|
||||
} else if self.lastSession != nil {
|
||||
// Mid-reconnect — a path is available again; fast-fresh the next attempt.
|
||||
if path.status == .satisfied {
|
||||
self.reconnectAttempt = 0
|
||||
self.scheduleReconnect()
|
||||
|
||||
@@ -163,13 +163,8 @@ final class IOSAudioRouter: ObservableObject {
|
||||
/// burns CPU and cycles the audio session on/off (the "glitching" bug).
|
||||
private var isApplyingConfiguration = false
|
||||
|
||||
/// Last `overrideOutputAudioPort` value we successfully applied (`.none` or `.speaker`),
|
||||
/// so `applyA2dpSpeakerFallback` can skip a redundant `overrideOutputAudioPort` call.
|
||||
/// That call fires a `.override` route-change notification on every invocation, and on
|
||||
/// an AirPods disconnect the fallback is invoked once per `recoverAudio()` — which
|
||||
/// itself fires on every non-skipped route change — so without this guard the override
|
||||
/// call and the route-change handler ping-pong: the AirPods-disconnect reinitialize
|
||||
/// loop (each iteration also rebuilds the AVAudioEngine via reconfigure()).
|
||||
/// Last `overrideOutputAudioPort` value we successfully applied (`.none` or `.speaker`).
|
||||
/// See `applyA2dpSpeakerFallback`'s doc comment for why this cache exists.
|
||||
/// `nil` = "unknown / assume not applied" — reset at the top of `applyConfiguration()`
|
||||
/// because `setCategory` can reset the override out from under us, and on first run.
|
||||
private var lastAppliedOutputOverride: AVAudioSession.PortOverride?
|
||||
@@ -382,13 +377,8 @@ final class IOSAudioRouter: ObservableObject {
|
||||
|
||||
// 3. Input & mic-capsule configuration.
|
||||
if captureChannels == .stereo {
|
||||
// Stereo: enable the built-in mic's .stereo polar pattern AND anchor the input
|
||||
// route explicitly via setPreferredInput + setInputDataSource. With HFP disabled
|
||||
// the system routes input to the built-in mic, but without the explicit
|
||||
// preferred-input anchor the route can collapse during the mode switch
|
||||
// (.voiceChat → .default) and the output dies. The channel count is carried by the
|
||||
// engine's mic tap + vc_set_capture_channels, NOT via
|
||||
// setPreferredInputNumberOfChannels(2) — that call collapses the A2DP output route.
|
||||
// See configureStereoCapture's doc comment for the full stereo-capture recipe
|
||||
// and why each step is necessary.
|
||||
configureStereoCapture(session: session)
|
||||
} else if let portId = selectedInputPortId, !portId.isEmpty,
|
||||
let port = session.availableInputs?.first(where: { $0.uid == portId }) {
|
||||
@@ -434,8 +424,6 @@ final class IOSAudioRouter: ObservableObject {
|
||||
do {
|
||||
try builtIn.setPreferredDataSource(stereoSource)
|
||||
try stereoSource.setPreferredPolarPattern(.stereo)
|
||||
// Anchor the input route explicitly; without it the route can collapse during
|
||||
// the mode switch (.voiceChat → .default) and the A2DP output dies.
|
||||
try session.setPreferredInput(builtIn)
|
||||
// Commit the data source at the session level. setPreferredDataSource alone only
|
||||
// sets the port-level preference; setInputDataSource makes it the active source.
|
||||
@@ -697,8 +685,6 @@ final class IOSAudioRouter: ObservableObject {
|
||||
}
|
||||
let desired: AVAudioSession.PortOverride = hasExternalOutput ? .none : .speaker
|
||||
if desired == lastAppliedOutputOverride {
|
||||
// Already in the desired state — calling overrideOutputAudioPort again would just
|
||||
// fire a redundant `.override` route-change notification (the loop driver).
|
||||
logger.debug("A2DP fallback — desired=\(self.overrideLabel(desired)) already applied, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ final class SessionState {
|
||||
var accounts: [Account] = []
|
||||
var devices: [Device] = []
|
||||
|
||||
/// Back-reference to the app state. Once `SessionState.init` overwrites `client.onEvent`
|
||||
/// (`SessionState.swift:87`), `AppState.handleConnectEvent` no longer receives per-session
|
||||
/// events — so the `.disconnected` event for a LIVE session arrives here in `handleEvent`,
|
||||
/// not in AppState. This weak ref lets us hand the disconnect back to AppState (which owns
|
||||
/// the reconnect state machine) so the auto-reconnect fires. Set by AppState on auth success.
|
||||
/// Back-reference to the app state. Once `SessionState.init` overwrites `client.onEvent`,
|
||||
/// `AppState.handleConnectEvent` no longer receives per-session events — so the
|
||||
/// `.disconnected` event for a LIVE session arrives here in `handleEvent`, not in AppState.
|
||||
/// This weak ref lets us hand the disconnect back to AppState (which owns the reconnect
|
||||
/// state machine) so the auto-reconnect fires. Set by AppState on auth success.
|
||||
weak var appState: AppState?
|
||||
|
||||
// MARK: - Reconnect restore state
|
||||
@@ -260,17 +260,18 @@ final class SessionState {
|
||||
// MARK: - Self-channel / server-mute sync
|
||||
|
||||
/// Sync currentChannelId from the self user's channelId in the user list. Mirrors macOS
|
||||
/// MainWindowController.swift:461,491,522. The server auto-places every authed user into
|
||||
/// the Lobby (channel 1) on connect, but without this sync currentChannelId stays 0 and
|
||||
/// the mic button (gated on currentChannelId == 0) stays permanently dimmed.
|
||||
/// MainWindowController's bootstrap/event-handling sync. The server auto-places every
|
||||
/// authed user into the Lobby (channel 1) on connect, but without this sync
|
||||
/// currentChannelId stays 0 and the mic button (gated on currentChannelId == 0) stays
|
||||
/// permanently dimmed.
|
||||
private func syncSelfChannel() {
|
||||
if let me = users.first(where: { $0.id == selfUserId }) {
|
||||
currentChannelId = me.channelId
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply server-side mute/deafen state — mirrors macOS MainWindowController.swift:693-700.
|
||||
/// iOS was previously ignoring server mute/deafen entirely.
|
||||
/// Apply server-side mute/deafen state — mirrors macOS MainWindowController's handling
|
||||
/// of UserEvent.UPDATED for the self user.
|
||||
private func applyServerMuteState(muted: Bool, deafened: Bool) {
|
||||
if muted && !voiceState.serverMuted { addActivity("You have been server-muted") }
|
||||
if deafened && !voiceState.serverDeafened { addActivity("You have been server-deafened") }
|
||||
|
||||
@@ -48,7 +48,6 @@ struct PerUserTuningView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Load from first stream if available
|
||||
let streams = streamsForUser
|
||||
if let first = streams.first {
|
||||
let (_, state) = session.client.getRemoteStream(userId: user.id, streamId: first.id)
|
||||
|
||||
@@ -44,7 +44,6 @@ final class ConnectWindowController: NSWindowController, NSWindowDelegate {
|
||||
private func buildUI() {
|
||||
guard let contentView = window?.contentView else { return }
|
||||
|
||||
// Server list
|
||||
let col = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("server"))
|
||||
col.title = "Saved Servers"
|
||||
serverTableView.addTableColumn(col)
|
||||
@@ -61,7 +60,6 @@ final class ConnectWindowController: NSWindowController, NSWindowDelegate {
|
||||
serverScrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(serverScrollView)
|
||||
|
||||
// Buttons row
|
||||
configureButton(addButton, title: "Add…", action: #selector(addClicked))
|
||||
configureButton(editButton, title: "Edit…", action: #selector(editClicked))
|
||||
configureButton(removeButton, title: "Remove", action: #selector(removeClicked))
|
||||
@@ -72,13 +70,11 @@ final class ConnectWindowController: NSWindowController, NSWindowDelegate {
|
||||
buttonStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(buttonStack)
|
||||
|
||||
// Status
|
||||
statusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
statusLabel.textColor = .secondaryLabelColor
|
||||
statusLabel.setAccessibilityLabel("Connection status")
|
||||
contentView.addSubview(statusLabel)
|
||||
|
||||
// Connect button
|
||||
connectButton.title = "Connect"
|
||||
connectButton.bezelStyle = .rounded
|
||||
connectButton.keyEquivalent = "\r"
|
||||
@@ -376,7 +372,3 @@ extension ConnectWindowController: NSTableViewDataSource, NSTableViewDelegate {
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper
|
||||
|
||||
|
||||
|
||||
@@ -185,9 +185,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
|
||||
|
||||
required init?(coder: NSCoder) { fatalError() }
|
||||
|
||||
deinit {
|
||||
NSLog("[VoiceCatMac] MainWindowController deinit — client and event handlers are gone")
|
||||
}
|
||||
deinit {}
|
||||
|
||||
// MARK: - UI construction
|
||||
|
||||
@@ -455,10 +453,6 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
|
||||
}
|
||||
ownPermissions = client.getPermissions()
|
||||
|
||||
NSLog("[VoiceCatMac] bootstrap: channels=%d users=%d perms{admin=%d kick=%d} currentChannelId=%u",
|
||||
channels.count, allUsers.count,
|
||||
ownPermissions.isAdmin, ownPermissions.canKick, currentChannelId)
|
||||
|
||||
// Apply initial output volume (default 80% — matches Windows client)
|
||||
client.setOutputVolume(0.8)
|
||||
|
||||
@@ -477,9 +471,6 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
|
||||
// MARK: - Event handling
|
||||
|
||||
private func handleEvent(_ event: VoiceCatEvent) {
|
||||
NSLog("[VoiceCatMac] event type=%d result=%d userId=%u channelId=%u streamId=%u text=%@",
|
||||
event.type.rawValue, event.result.rawValue, event.userId, event.channelId,
|
||||
event.streamId, event.text ?? "(nil)")
|
||||
switch event.type {
|
||||
case .channelList:
|
||||
channels = client.listChannels()
|
||||
@@ -1067,7 +1058,7 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
|
||||
composeField.stringValue = ""
|
||||
}
|
||||
|
||||
// MARK: - M5: Moderation helpers
|
||||
// MARK: - Moderation helpers
|
||||
|
||||
private func moveUser(_ user: User) {
|
||||
let sheet = MoveUserSheet(channels: channels, currentChannelId: user.channelId)
|
||||
@@ -1241,7 +1232,6 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
|
||||
presentSheet(sheet)
|
||||
}
|
||||
|
||||
/// Open a PM window from the user context menu.
|
||||
private func openPmWindow(_ user: User) {
|
||||
getOrOpenPmWindow(user.id)
|
||||
}
|
||||
@@ -1288,17 +1278,13 @@ final class MainWindowController: NSWindowController, NSWindowDelegate {
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
if let mon = pttMonitor { NSEvent.removeMonitor(mon) }
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
// Close all PM windows
|
||||
for (_, pmWin) in pmWindows { pmWin.close() }
|
||||
pmWindows.removeAll()
|
||||
// Close settings window
|
||||
settingsWindowController?.close()
|
||||
settingsWindowController = nil
|
||||
// Remove app menus we added
|
||||
if let item = voiceMenuItem { NSApp.mainMenu?.removeItem(item) }
|
||||
if let item = messagesMenuItem { NSApp.mainMenu?.removeItem(item) }
|
||||
if let item = adminMenuItem { NSApp.mainMenu?.removeItem(item) }
|
||||
// Remove Settings menu item + separator from app menu
|
||||
if let appMenu = NSApp.mainMenu?.item(at: 0)?.submenu {
|
||||
if let item = settingsMenuItem { appMenu.removeItem(item) }
|
||||
// Remove the separator we inserted before Quit
|
||||
|
||||
@@ -105,7 +105,6 @@ public sealed class ProcessAudioMixer : IDisposable
|
||||
for (int i = 0; i < frameLen; i++)
|
||||
{
|
||||
int sum = mix[i] + frame[i];
|
||||
// Saturating clamp
|
||||
mix[i] = (short)Math.Clamp(sum, short.MinValue, short.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,13 +92,8 @@ public partial class ConnectDialog : Form
|
||||
|
||||
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);
|
||||
@@ -121,21 +116,15 @@ public partial class ConnectDialog : Form
|
||||
: 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}";
|
||||
@@ -146,9 +135,7 @@ public partial class ConnectDialog : Form
|
||||
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}");
|
||||
_client.AuthenticateGuest(Nickname);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -169,15 +156,12 @@ public partial class ConnectDialog : Form
|
||||
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}");
|
||||
_client.AuthenticateUser(server.SavedUsername ?? "", password);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEvent(VoiceCatEvent ev)
|
||||
{
|
||||
Console.WriteLine($"[ConnectDialog] event: {ev}");
|
||||
switch (ev.Type)
|
||||
{
|
||||
case VcEventType.ConnectionState:
|
||||
@@ -225,24 +209,18 @@ public partial class ConnectDialog : Form
|
||||
|
||||
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}");
|
||||
bool accept = dlg.ShowDialog(this) == DialogResult.OK;
|
||||
_client.ConfirmServerIdentity(accept);
|
||||
if (!accept) lblStatus.Text = "Server identity rejected.";
|
||||
}
|
||||
|
||||
|
||||
@@ -1114,7 +1114,7 @@ public partial class MainForm : Form
|
||||
OpenPmWindow(dlg.SelectedUserId);
|
||||
}
|
||||
|
||||
// ── M5: Moderation helpers ────────────────────────────────────────────────
|
||||
// ── Moderation helpers ─────────────────────────────────────────────────────
|
||||
|
||||
private void UpdateSelfServerMuteState(bool muted, bool deafened)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>Small modal for "type a password right now" — used when a saved server's
|
||||
/// password wasn't remembered, and (Phase E) for password-protected channel joins.</summary>
|
||||
/// password wasn't remembered, and for password-protected channel joins.</summary>
|
||||
public partial class PasswordPromptDialog : Form
|
||||
{
|
||||
public string Password => txtPassword.Text;
|
||||
|
||||
@@ -3,7 +3,7 @@ using VoiceCat.Interop;
|
||||
namespace VoiceCat.App.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// TOFU server-identity confirmation (M4). Shown only for VcTofuStatus.FirstConnect/Mismatch
|
||||
/// TOFU server-identity confirmation. Shown only for VcTofuStatus.FirstConnect/Mismatch
|
||||
/// — never Matched (that's the silent-success "subsequent connects verify the pin" path
|
||||
/// docs/security.md describes; showing a dialog on every routine reconnect would be exactly
|
||||
/// the "overly chatty" experience this project avoids elsewhere too).
|
||||
|
||||
@@ -7,17 +7,17 @@ namespace VoiceCat.App.Notifications;
|
||||
/// </summary>
|
||||
public enum SoundEvent
|
||||
{
|
||||
ChannelJoin, // another user joined my channel
|
||||
ChannelLeave, // another user left my channel
|
||||
ChannelJoin,
|
||||
ChannelLeave,
|
||||
ChannelRecv, // channel text message from someone else
|
||||
ChannelSent, // channel text message I sent
|
||||
PmRecv, // private message received
|
||||
PmSent, // private message I sent
|
||||
Login, // connected / authenticated
|
||||
Logout, // clean disconnect
|
||||
ConnectionLost, // unexpected disconnect
|
||||
VoiceOn, // my microphone stream started
|
||||
VoiceOff, // my microphone stream stopped
|
||||
PmRecv,
|
||||
PmSent,
|
||||
Login,
|
||||
Logout,
|
||||
ConnectionLost,
|
||||
VoiceOn,
|
||||
VoiceOff,
|
||||
VaStart, // my voice-activity began (off by default)
|
||||
VaStop, // my voice-activity ended (off by default)
|
||||
Ptt, // push-to-talk engaged (off by default)
|
||||
|
||||
@@ -7,30 +7,21 @@ internal static class Program
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
// Diagnostic-logging-only for now (manual debugging session) — every exception that
|
||||
// would otherwise be silently caught by WinForms' default message-loop handling (or
|
||||
// crash with no visible cause) gets printed to stdout/stderr first.
|
||||
// Surface exceptions that WinForms' default message-loop handling would otherwise
|
||||
// swallow silently (or crash with no visible cause).
|
||||
Application.ThreadException += (_, e) =>
|
||||
Console.Error.WriteLine($"[UNHANDLED ThreadException] {e.Exception}");
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
|
||||
Console.Error.WriteLine($"[UNHANDLED AppDomain exception] {e.ExceptionObject}");
|
||||
|
||||
Console.WriteLine("VoiceCat.App starting...");
|
||||
ApplicationConfiguration.Initialize();
|
||||
|
||||
using var connectDialog = new ConnectDialog();
|
||||
Console.WriteLine("Showing ConnectDialog...");
|
||||
var result = connectDialog.ShowDialog();
|
||||
Console.WriteLine($"ConnectDialog closed with DialogResult={result}, ConnectedClient={(connectDialog.ConnectedClient is null ? "null" : "set")}");
|
||||
if (result != DialogResult.OK || connectDialog.ConnectedClient is null)
|
||||
{
|
||||
Console.WriteLine("Exiting (cancelled or no connected client).");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Launching MainForm...");
|
||||
Application.Run(new MainForm(connectDialog.ConnectedClient, connectDialog.SelfUserId,
|
||||
connectDialog.Nickname, connectDialog.ServerName));
|
||||
Console.WriteLine("MainForm closed. Exiting.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,10 @@ public sealed class VoiceCatClientSmokeTests : IDisposable
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), "vc_csharp_smoke_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
|
||||
string serverExe = Path.Combine(FindRepoRoot(), "build", "m1-dev", "bin", "voicecat-server.exe");
|
||||
string serverExe = Path.Combine(FindRepoRoot(), "build", "dev", "bin", "voicecat-server.exe");
|
||||
Assert.True(File.Exists(serverExe),
|
||||
$"voicecat-server.exe not found at '{serverExe}' — build the m1-dev preset first " +
|
||||
"(cmake --preset m1-dev && cmake --build --preset m1-dev).");
|
||||
$"voicecat-server.exe not found at '{serverExe}' — build the dev preset first " +
|
||||
"(cmake --preset dev && cmake --build --preset dev).");
|
||||
|
||||
var psi = new ProcessStartInfo(serverExe)
|
||||
{
|
||||
@@ -56,9 +56,9 @@ public sealed class VoiceCatClientSmokeTests : IDisposable
|
||||
Assert.True(port is not null, "voicecat-server.exe did not report a bound TCP port within 10s.");
|
||||
_port = port!.Value;
|
||||
|
||||
// M5: provision a known admin account so we can exercise moderation wrappers end-to-end.
|
||||
string adminExe = Path.Combine(FindRepoRoot(), "build", "m1-dev", "bin", "voicecat-admin.exe");
|
||||
Assert.True(File.Exists(adminExe), "voicecat-admin.exe not found — build the m1-dev preset.");
|
||||
// Provision a known admin account so we can exercise moderation wrappers end-to-end.
|
||||
string adminExe = Path.Combine(FindRepoRoot(), "build", "dev", "bin", "voicecat-admin.exe");
|
||||
Assert.True(File.Exists(adminExe), "voicecat-admin.exe not found — build the dev preset.");
|
||||
var adminPsi = new ProcessStartInfo(adminExe)
|
||||
{
|
||||
Arguments = $"--data-dir \"{_tempDir}\" account add admin2 --admin --password testpassword123",
|
||||
@@ -140,14 +140,14 @@ public sealed class VoiceCatClientSmokeTests : IDisposable
|
||||
var channels = client.ListChannels();
|
||||
Assert.Contains(channels, c => c.Id == 1 && c.Name == "Lobby");
|
||||
|
||||
// M5: permissions getter round-trip.
|
||||
// Permissions getter round-trip.
|
||||
var perms = client.GetPermissions();
|
||||
Assert.False(perms.IsAdmin);
|
||||
Assert.False(perms.CanKick);
|
||||
|
||||
// M5: moderation request wrappers queue without error. As a guest, account listing
|
||||
// Moderation request wrappers queue without error. As a guest, account listing
|
||||
// is rejected by the server with a GenericResult, which proves the wrapper path works
|
||||
// end-to-end and that the new event type is delivered through P/Invoke.
|
||||
// end-to-end and that the event type is delivered through P/Invoke.
|
||||
Assert.Equal(VcResult.Ok, client.RequestAccountList());
|
||||
Assert.True(PumpUntil(client,
|
||||
() => events.Any(e => e.Type == VcEventType.GenericResult), 3000),
|
||||
|
||||
@@ -37,7 +37,7 @@ public enum VcConnectionState
|
||||
TlsHandshake = 2,
|
||||
Authenticating = 3,
|
||||
Connected = 4,
|
||||
/// <summary>M4: handshake succeeded, waiting on vc_confirm_server_identity().</summary>
|
||||
/// <summary>Handshake succeeded, waiting on vc_confirm_server_identity().</summary>
|
||||
VerifyingIdentity = 5,
|
||||
}
|
||||
|
||||
@@ -84,13 +84,13 @@ public enum VcEventType
|
||||
TalkState = 9,
|
||||
Error = 10,
|
||||
Disconnected = 11,
|
||||
/// <summary>M4: reply to VoiceCatClient.JoinChannelAsync's underlying vc_join_channel.</summary>
|
||||
/// <summary>Reply to VoiceCatClient.JoinChannelAsync's underlying vc_join_channel.</summary>
|
||||
JoinResult = 12,
|
||||
/// <summary>M4: the TOFU server-identity gate — see VcTofuStatus.</summary>
|
||||
/// <summary>The TOFU server-identity gate — see VcTofuStatus.</summary>
|
||||
ServerIdentity = 13,
|
||||
/// <summary>M5: async result for moderation/admin/channel operations.</summary>
|
||||
/// <summary>Async result for moderation/admin/channel operations.</summary>
|
||||
GenericResult = 14,
|
||||
/// <summary>M5: reply to VoiceCatClient.RequestAccountList — call ListAccounts() to read.</summary>
|
||||
/// <summary>Reply to VoiceCatClient.RequestAccountList — call ListAccounts() to read.</summary>
|
||||
AccountList = 15,
|
||||
/// <summary>Voice-plane subscription state. u32a = 1 (subscribed) or 0 (unsubscribed).</summary>
|
||||
VoiceState = 16,
|
||||
|
||||
@@ -148,7 +148,7 @@ internal static partial class NativeMethods
|
||||
[LibraryImport(LibName)]
|
||||
internal static partial void vc_free_device_list(ref VcDeviceListNative list);
|
||||
|
||||
// ── M4: channel / user / stream snapshot getters ────────────────────────────────────────
|
||||
// ── Channel / user / stream snapshot getters ────────────────────────────────────────────
|
||||
[LibraryImport(LibName)]
|
||||
internal static partial VcResult vc_list_channels(nint c, out VcChannelListNative outList);
|
||||
|
||||
@@ -168,7 +168,7 @@ internal static partial class NativeMethods
|
||||
[LibraryImport(LibName)]
|
||||
internal static partial void vc_free_stream_summary_list(ref VcStreamSummaryListNative list);
|
||||
|
||||
// ── M4: TOFU server-identity gate ───────────────────────────────────────────────────────
|
||||
// ── TOFU server-identity gate ───────────────────────────────────────────────────────────
|
||||
[LibraryImport(LibName)]
|
||||
internal static partial VcResult vc_confirm_server_identity(nint c, int accept);
|
||||
|
||||
@@ -176,7 +176,7 @@ internal static partial class NativeMethods
|
||||
internal static partial VcResult vc_get_server_identity_display(nint c, nint outBuf,
|
||||
nuint bufCap, out nuint outLen);
|
||||
|
||||
// ── M5: Moderation & admin ─────────────────────────────────────────────────────────────
|
||||
// ── Moderation & admin ───────────────────────────────────────────────────────────────────
|
||||
[LibraryImport(LibName, StringMarshalling = StringMarshalling.Utf8)]
|
||||
internal static partial VcResult vc_kick_user(nint c, uint userId, string? reason);
|
||||
|
||||
|
||||
@@ -95,10 +95,6 @@ public sealed class VoiceCatClient : IDisposable
|
||||
|
||||
internal void EnqueueEvent(VoiceCatEvent ev)
|
||||
{
|
||||
// Temporary diagnostic (manual debugging session) — confirms the native callback
|
||||
// chain (UnmanagedCallersOnly -> GCHandle resolve -> here) actually fires, independent
|
||||
// of whether the UI-thread drain (PumpEvents) ever sees it.
|
||||
Console.WriteLine($"[VoiceCatClient] EnqueueEvent (native thread): {ev}");
|
||||
_events.Writer.TryWrite(ev);
|
||||
}
|
||||
internal void EnqueueLevel(uint streamId, float rms) => _latestLevels[streamId] = rms;
|
||||
@@ -116,7 +112,7 @@ public sealed class VoiceCatClient : IDisposable
|
||||
public VcResult AuthenticateUser(string username, string password) =>
|
||||
NativeMethods.vc_authenticate_user(_handle.DangerousGetHandle(), username, password);
|
||||
|
||||
// ── TOFU server-identity gate (M4) ──────────────────────────────────────────────────────
|
||||
// ── TOFU server-identity gate ───────────────────────────────────────────────────────────
|
||||
public VcResult ConfirmServerIdentity(bool accept) =>
|
||||
NativeMethods.vc_confirm_server_identity(_handle.DangerousGetHandle(), accept ? 1 : 0);
|
||||
|
||||
@@ -143,9 +139,7 @@ public sealed class VoiceCatClient : IDisposable
|
||||
|
||||
// ── Channels ─────────────────────────────────────────────────────────────────────────
|
||||
/// <summary>Result arrives as a VcEventType.JoinResult event, not via this return value
|
||||
/// (which only reflects "request queued" — see voicecat.h's vc_join_channel doc comment).
|
||||
/// NOTE: no in-tree channel has a server-side password to check yet (M5+ feature) — this
|
||||
/// path is wired but not yet exercisable end-to-end.</summary>
|
||||
/// (which only reflects "request queued" — see voicecat.h's vc_join_channel doc comment).</summary>
|
||||
public VcResult JoinChannel(uint channelId, string? password = null) =>
|
||||
NativeMethods.vc_join_channel(_handle.DangerousGetHandle(), channelId, password);
|
||||
|
||||
@@ -290,7 +284,7 @@ public sealed class VoiceCatClient : IDisposable
|
||||
public VcResult SetPcmSink(nint cb, IntPtr user) =>
|
||||
NativeMethods.vc_set_pcm_sink(_handle.DangerousGetHandle(), cb, user);
|
||||
|
||||
// ── M5: Moderation & admin ───────────────────────────────────────────────────────────
|
||||
// ── Moderation & admin ───────────────────────────────────────────────────────────────
|
||||
public VcResult KickUser(uint userId, string? reason = null) =>
|
||||
NativeMethods.vc_kick_user(_handle.DangerousGetHandle(), userId, reason);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user