Summary Update.
This commit is contained in:
@@ -5,24 +5,69 @@ using WebSocketSharp;
|
||||
|
||||
namespace RelayClient.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The client-side WebSocket transport. Mirrors ChatSocketBehavior on the server.
|
||||
///
|
||||
/// Sending: typed helpers (SendGetHistory, SendRtcJoinChannel, SendEditMessage, …) build the
|
||||
/// appropriate WsControlMessage or SocketEncryptedMessage and route through SendRaw. SendRaw
|
||||
/// always uses synchronous _socket.Send because WebSocketSharp's SendAsync calls
|
||||
/// Action.BeginInvoke internally, which throws PlatformNotSupportedException on .NET 5+.
|
||||
/// Callers that need non-blocking sends (e.g. MainPage.SendMessage for image attachments)
|
||||
/// wrap the call in Task.Run.
|
||||
///
|
||||
/// Receiving: OnMessage peeks the JSON. If it has an "Event" property → WsEventMessage (acks).
|
||||
/// If it has a "Type" property → SignalType discriminator, deserialise into the right Socket*
|
||||
/// type, fire the matching C# event. MainPage subscribes to these events.
|
||||
///
|
||||
/// Connect order matters: the first frame after the handshake is Authenticate (so the server
|
||||
/// can verify the Core-issued token), then RegisterKey (so the server has our public key
|
||||
/// before any encrypted message arrives), then GetServerKey + GetChannels.
|
||||
/// </summary>
|
||||
public sealed class RelaySocketClient
|
||||
{
|
||||
/// <summary>Username this socket is authenticated as. Captured at construction.</summary>
|
||||
private readonly string _username;
|
||||
|
||||
/// <summary>The underlying WebSocketSharp client. Owned (constructed) by this class.</summary>
|
||||
private readonly WebSocket _socket;
|
||||
|
||||
/// <summary>
|
||||
/// The server's RSA public key, cached after the first GetServerKey response.
|
||||
/// MainPage reads this to encrypt outbound chat payloads.
|
||||
/// </summary>
|
||||
public string? ServerPublicKey { get; private set; }
|
||||
|
||||
/// <summary>Fires for every raw incoming text frame. Mostly used for debug logging.</summary>
|
||||
public event Action<string>? RawMessageReceived;
|
||||
|
||||
/// <summary>Fires when the server pushes a fresh channel list (initial connect or after CRUD).</summary>
|
||||
public event Action<SocketChannelList>? ChannelListReceived;
|
||||
|
||||
/// <summary>Fires for newly-arrived chat messages (SignalType.EncryptedChat).</summary>
|
||||
public event Action<SocketEncryptedMessage>? EncryptedChatReceived;
|
||||
|
||||
/// <summary>Fires when an existing message is edited by its author (SignalType.MessageEdited).</summary>
|
||||
public event Action<SocketEncryptedMessage>? MessageEdited;
|
||||
|
||||
/// <summary>Fires when a message is deleted (SignalType.MessageDeleted).</summary>
|
||||
public event Action<SocketMessageDeletedEvent>? MessageDeleted;
|
||||
|
||||
/// <summary>Fires when another user is typing in a channel.</summary>
|
||||
public event Action<SocketTypingEvent>? TypingReceived;
|
||||
|
||||
/// <summary>Fires in response to a SendGetEditHistory request.</summary>
|
||||
public event Action<SocketEditHistoryResponse>? EditHistoryReceived;
|
||||
|
||||
/// <summary>Fires for encrypted RTC SDP/ICE signals — RtcBridgeService forwards into the JS engine.</summary>
|
||||
public event Action<SocketRtcSignalMessage>? EncryptedRtcSignalReceived;
|
||||
|
||||
/// <summary>Fires once when the server's public key arrives. Mainly used by tests; production reads ServerPublicKey directly.</summary>
|
||||
public event Action<string>? ServerPublicKeyReceived;
|
||||
|
||||
/// <summary>Diagnostic logger. MainPage subscribes Console.WriteLine here.</summary>
|
||||
public event Action<string>? Log;
|
||||
|
||||
/// <summary>Default URL points at localhost dev server. Production passes a remote URL.</summary>
|
||||
public RelaySocketClient(string username, string url = "ws://127.0.0.1:5001/")
|
||||
{
|
||||
_username = username;
|
||||
@@ -30,6 +75,12 @@ public sealed class RelaySocketClient
|
||||
_socket.OnMessage += OnMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the WebSocket and fires the four-step boot handshake IN ORDER:
|
||||
/// Authenticate → RegisterKey → GetServerKey → GetChannels. Order matters because the
|
||||
/// server uses RegisterKey to populate its session→username map (needed for permission
|
||||
/// checks on subsequent messages).
|
||||
/// </summary>
|
||||
public void Connect()
|
||||
{
|
||||
_socket.Connect();
|
||||
@@ -42,6 +93,7 @@ public sealed class RelaySocketClient
|
||||
SendControlMessage(new WsControlMessage { Action = WsAction.GetChannels });
|
||||
}
|
||||
|
||||
/// <summary>Detaches the message handler and closes the socket. Called from MainPage.OnDisappearing.</summary>
|
||||
public void Disconnect()
|
||||
{
|
||||
_socket.OnMessage -= OnMessage;
|
||||
@@ -49,24 +101,31 @@ public sealed class RelaySocketClient
|
||||
_socket.Close();
|
||||
}
|
||||
|
||||
/// <summary>Generic control-plane send. Serialises the WsControlMessage to JSON and ships it.</summary>
|
||||
public void SendControlMessage(WsControlMessage message) =>
|
||||
SendRaw(JsonSerializer.Serialize(message));
|
||||
|
||||
/// <summary>Request the message history for a channel. Server streams it back as individual EncryptedChat frames.</summary>
|
||||
public void SendGetHistory(string channelId) =>
|
||||
SendControlMessage(new WsControlMessage { Action = WsAction.GetHistory, Username = _username, ChannelId = channelId });
|
||||
|
||||
/// <summary>Tell the server we've joined a voice channel. Fires Speak permission check server-side.</summary>
|
||||
public void SendRtcJoinChannel(string channelId) =>
|
||||
SendControlMessage(new WsControlMessage { Action = WsAction.RtcJoin, Username = _username, ChannelId = channelId });
|
||||
|
||||
/// <summary>Tell the server we've left the voice channel. Idempotent server-side.</summary>
|
||||
public void SendRtcLeaveChannel(string channelId) =>
|
||||
SendControlMessage(new WsControlMessage { Action = WsAction.RtcLeave, Username = _username, ChannelId = channelId });
|
||||
|
||||
/// <summary>Notify channel peers that we're typing. Server broadcasts a SocketTypingEvent to everyone but us.</summary>
|
||||
public void SendTyping(string channelId) =>
|
||||
SendControlMessage(new WsControlMessage { Action = WsAction.SendTyping, Username = _username, ChannelId = channelId });
|
||||
|
||||
/// <summary>Request all historical versions of a message. Server replies with SocketEditHistoryResponse.</summary>
|
||||
public void SendGetEditHistory(string messageId, string channelId) =>
|
||||
SendControlMessage(new WsControlMessage { Action = WsAction.GetEditHistory, Username = _username, MessageId = messageId, ChannelId = channelId });
|
||||
|
||||
/// <summary>Create a new channel. Permission-gated server-side; on success the server broadcasts a fresh channel list.</summary>
|
||||
public void SendCreateChannel(string name, ChannelType type, string group = "") =>
|
||||
SendControlMessage(new WsControlMessage
|
||||
{
|
||||
@@ -76,9 +135,14 @@ public sealed class RelaySocketClient
|
||||
ChannelGroup = group
|
||||
});
|
||||
|
||||
/// <summary>Soft-delete a channel. Permission-gated server-side.</summary>
|
||||
public void SendDeleteChannel(string channelId) =>
|
||||
SendControlMessage(new WsControlMessage { Action = WsAction.DeleteChannel, ChannelId = channelId });
|
||||
|
||||
/// <summary>
|
||||
/// Send an edit for an existing message. Caller is responsible for encrypting the new
|
||||
/// content (with the server's public key) before calling — same encryption shape as a new send.
|
||||
/// </summary>
|
||||
public void SendEditMessage(string messageId, string channelId, EncryptedPayload encrypted) =>
|
||||
SendJson(new SocketEncryptedMessage
|
||||
{
|
||||
@@ -88,6 +152,7 @@ public sealed class RelaySocketClient
|
||||
Tag = encrypted.Tag, EncryptedKey = encrypted.EncryptedKey
|
||||
});
|
||||
|
||||
/// <summary>Request soft-delete of one of our own messages. Server checks ownership before honoring.</summary>
|
||||
public void SendDeleteMessage(string messageId, string channelId) =>
|
||||
SendJson(new SocketEncryptedMessage
|
||||
{
|
||||
@@ -95,6 +160,11 @@ public sealed class RelaySocketClient
|
||||
SenderUsername = _username, ChannelId = channelId
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// The single send pinch point. Synchronous (WebSocketSharp's SendAsync is broken on .NET 5+
|
||||
/// due to Action.BeginInvoke). All exceptions are logged AND rethrown so the calling
|
||||
/// Task.Run can surface them to the user via DisplayAlert.
|
||||
/// </summary>
|
||||
public void SendRaw(string message)
|
||||
{
|
||||
if (_socket.ReadyState != WebSocketState.Open)
|
||||
@@ -114,8 +184,15 @@ public sealed class RelaySocketClient
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Convenience: JSON-serialise any payload and ship it. Used for all SocketEncryptedMessage and WsControlMessage sends.</summary>
|
||||
public void SendJson<T>(T payload) => SendRaw(JsonSerializer.Serialize(payload));
|
||||
|
||||
/// <summary>
|
||||
/// WebSocketSharp callback for every incoming text frame. Peeks the JSON to decide whether
|
||||
/// it's a control-plane ack (Event property) or data-plane message (Type property), then
|
||||
/// fires the matching public C# event. Exceptions are caught locally so a malformed frame
|
||||
/// can't drop the connection.
|
||||
/// </summary>
|
||||
private void OnMessage(object? sender, MessageEventArgs e)
|
||||
{
|
||||
RawMessageReceived?.Invoke(e.Data);
|
||||
|
||||
@@ -6,14 +6,39 @@ using RelayShared.Services;
|
||||
|
||||
namespace RelayClient.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The bridge between the C# WebSocket pipe and the JavaScript WebRTC engine
|
||||
/// running inside the HybridWebView (which is shown when a Voice channel is open).
|
||||
///
|
||||
/// Outbound (JS → C# → server): the WebView JS calls into C# via SendRtcSignal(json).
|
||||
/// We deserialise to RtcSignalMessage, encrypt with the server's public key, wrap in
|
||||
/// SocketRtcSignalMessage, and send through the WebSocket.
|
||||
///
|
||||
/// Inbound (server → C# → JS): the WebSocket fires EncryptedRtcSignalReceived. MainPage
|
||||
/// hands it to HandleIncomingRtcSignalAsync, which decrypts with the user's private key
|
||||
/// and calls back into JS via hybridWebView.InvokeJavaScriptAsync("testIndex", …).
|
||||
///
|
||||
/// JoinRtcChannel / LeaveRtcChannel just send WsAction control messages; presence tracking
|
||||
/// happens server-side in RtcChannelPresenceService.
|
||||
/// </summary>
|
||||
public sealed class RtcBridgeService
|
||||
{
|
||||
/// <summary>The currently-signed-in username. Stamped onto outgoing RTC signals.</summary>
|
||||
private readonly string _username;
|
||||
|
||||
/// <summary>The shared WebSocket to RelayServer. Outbound RTC signals ride on this.</summary>
|
||||
private readonly RelaySocketClient _socket;
|
||||
|
||||
/// <summary>The MAUI HybridWebView that hosts the JS WebRTC engine. We push JS calls into it.</summary>
|
||||
private readonly HybridWebView _hybridWebView;
|
||||
|
||||
/// <summary>Lazy view into MainPage._currentChannelId so we always have the current voice channel.</summary>
|
||||
private readonly Func<string?> _getCurrentChannelId;
|
||||
|
||||
/// <summary>Diagnostic logger that surfaces messages back to the WebView UI. Used for status/error reporting.</summary>
|
||||
private readonly Action<string> _sendRawToWebView;
|
||||
|
||||
/// <summary>Captures collaborators. MainPage constructs this once and never replaces it.</summary>
|
||||
public RtcBridgeService(string username, RelaySocketClient socket, HybridWebView hybridWebView,
|
||||
Func<string?> getCurrentChannelId, Action<string> sendRawToWebView)
|
||||
{
|
||||
@@ -24,6 +49,7 @@ public sealed class RtcBridgeService
|
||||
_sendRawToWebView = sendRawToWebView;
|
||||
}
|
||||
|
||||
/// <summary>Sends RtcJoin for the currently-selected channel. Server-side, this triggers the Speak permission check and presence registration.</summary>
|
||||
public Task JoinRtcChannel()
|
||||
{
|
||||
var channelId = _getCurrentChannelId();
|
||||
@@ -35,6 +61,7 @@ public sealed class RtcBridgeService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Sends RtcLeave for the currently-selected channel. Clears server-side voice presence so peers stop seeing us.</summary>
|
||||
public void LeaveRtcChannel()
|
||||
{
|
||||
var channelId = _getCurrentChannelId();
|
||||
@@ -45,6 +72,13 @@ public sealed class RtcBridgeService
|
||||
_socket.SendRtcLeaveChannel(channelId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called from JavaScript (via the HybridWebView bridge) when the WebRTC engine wants to
|
||||
/// send an SDP offer/answer or ICE candidate to other peers. Parses the JSON, fills in
|
||||
/// missing ChannelId/From, encrypts with the server's public key, ships as
|
||||
/// SocketRtcSignalMessage. The server then forwards it (re-encrypted per-recipient) to
|
||||
/// every other session in the same voice channel.
|
||||
/// </summary>
|
||||
public void SendRtcSignal(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_socket.ServerPublicKey))
|
||||
@@ -105,6 +139,7 @@ public sealed class RtcBridgeService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>JS bridge: returns the current voice-channel roster as JSON. Hits ServerAPI's REST endpoint, not the WebSocket.</summary>
|
||||
public async Task<string> GetRtcParticipants()
|
||||
{
|
||||
var channelId = _getCurrentChannelId();
|
||||
@@ -116,6 +151,11 @@ public sealed class RtcBridgeService
|
||||
return JsonSerializer.Serialize(participants ?? []);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MainPage hands incoming SocketRtcSignalMessage frames here. Filters out our own
|
||||
/// frames, validates the channel scope, decrypts with the user's private key, parses to
|
||||
/// RtcSignalMessage, then pushes into the JS RTC engine via SendRtcSignalToJsAsync.
|
||||
/// </summary>
|
||||
public async Task HandleIncomingRtcSignalAsync(SocketRtcSignalMessage payload)
|
||||
{
|
||||
// _sendRawToWebView("HandleIncomingRtcSignal called");
|
||||
@@ -187,6 +227,10 @@ public sealed class RtcBridgeService
|
||||
await SendRtcSignalToJsAsync(rtcSignal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the current username and channelId into JS globals (window.setUsername, window.setChannelId).
|
||||
/// Called whenever the user switches voice channels OR the JS engine reports rtc_page_ready.
|
||||
/// </summary>
|
||||
public Task PushRtcContextToJsAsync()
|
||||
{
|
||||
MainThread.BeginInvokeOnMainThread(async () =>
|
||||
@@ -201,6 +245,11 @@ public sealed class RtcBridgeService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Final hop: hands a decrypted RtcSignalMessage off to the JS engine via
|
||||
/// hybridWebView.InvokeJavaScriptAsync("testIndex", …). SDP strings have their newlines
|
||||
/// escaped as "(rn)" because the JSON marshalling otherwise breaks them.
|
||||
/// </summary>
|
||||
private Task SendRtcSignalToJsAsync(RtcSignalMessage data)
|
||||
{
|
||||
if (data.Type == "rtc_offer" || data.Type == "rtc_answer")
|
||||
|
||||
Reference in New Issue
Block a user