diff --git a/RelayClient/MainPage.xaml.cs b/RelayClient/MainPage.xaml.cs index 000e97d..202b4dc 100644 --- a/RelayClient/MainPage.xaml.cs +++ b/RelayClient/MainPage.xaml.cs @@ -1,18 +1,17 @@ -using RelayClient.Crypto; -using WebSocketSharp; -using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using RelayClient.Crypto; +using RelayClient.Services; using RelayShared.Rtc; using RelayShared.Services; namespace RelayClient; - public partial class MainPage : ContentPage { private readonly string _username; - private readonly WebSocket _wsc; - private string? _serverPublicKey; + private readonly RelaySocketClient _socket; + private readonly RtcBridgeService _rtc; + private string? _currentChannelId; private string? _currentChannelName; @@ -33,19 +32,30 @@ public partial class MainPage : ContentPage KeyStorage.SavePublicKey(_username, keys.publicKey); } - _wsc = new WebSocket("ws://localhost:1337/"); - - _wsc.OnMessage += WscOnMessage; - _wsc.Connect(); - - var publicKey = KeyStorage.LoadPublicKey(_username); - _wsc.Send($"REGISTER_KEY|{_username}|{publicKey}"); - _wsc.Send("GET_SERVER_KEY"); - _wsc.Send("GET_CHANNELS"); - hybridWebView.SetInvokeJavaScriptTarget(this); ServerAPI.setupClient(); + _socket = new RelaySocketClient(_username); + _rtc = new RtcBridgeService( + _username, + _socket, + hybridWebView, + () => _currentChannelId, + SafeSendRawToWebView + ); + + _socket.Log += Console.WriteLine; + _socket.ChannelListReceived += HandleChannelList; + _socket.EncryptedChatReceived += HandleEncryptedChat; + _socket.EncryptedRtcSignalReceived += payload => + { + MainThread.BeginInvokeOnMainThread(async () => + { + await _rtc.HandleIncomingRtcSignalAsync(payload); + }); + }; + + _socket.Connect(); } private void SendButton_OnClicked(object? sender, EventArgs e) @@ -65,7 +75,7 @@ public partial class MainPage : ContentPage if (string.IsNullOrWhiteSpace(text)) return; - if (string.IsNullOrWhiteSpace(_serverPublicKey)) + if (string.IsNullOrWhiteSpace(_socket.ServerPublicKey)) { Console.WriteLine("Server public key not loaded yet."); return; @@ -77,7 +87,7 @@ public partial class MainPage : ContentPage return; } - var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey); + var encrypted = E2EeHelper.EncryptForRecipient(text, _socket.ServerPublicKey); var payload = new SocketEncryptedMessage { @@ -90,8 +100,7 @@ public partial class MainPage : ContentPage EncryptedKey = encrypted.EncryptedKey }; - var json = JsonSerializer.Serialize(payload); - _wsc.Send(json); + _socket.SendJson(payload); Console.WriteLine($"[{_username}] sent encrypted message."); @@ -99,179 +108,83 @@ public partial class MainPage : ContentPage MessageEntry.Focus(); } - private void WscOnMessage(object? sender, MessageEventArgs e) + private void HandleChannelList(SocketChannelList channelList) { - if (e.Data.StartsWith("SERVER:REGISTERED_KEY:")) + _channels.Clear(); + _channels.AddRange(channelList.Channels.OrderBy(c => c.CreatedAt)); + + var defaultChannel = _channels + .Where(c => c.Name.Equals("welcome", StringComparison.OrdinalIgnoreCase)) + .OrderBy(c => c.CreatedAt) + .FirstOrDefault() + ?? _channels.OrderBy(c => c.CreatedAt).FirstOrDefault(); + + if (defaultChannel is null) return; + + _currentChannelId = defaultChannel.ChannelId; + _currentChannelName = defaultChannel.Name; + + MainThread.BeginInvokeOnMainThread(async () => { - Console.WriteLine(e.Data); + ChannelLabel.Text = $"#{_currentChannelName}"; + RenderChannelList(); + await _rtc.PushRtcContextToJsAsync(); + }); + + _socket.SendRaw($"GET_HISTORY|{_username}|{_currentChannelId}"); + } + + private void HandleEncryptedChat(SocketEncryptedMessage payload) { + if (payload.RecipientUsername != _username) return; - } - // SafeSendRawToWebView($"[{_username}] RAW WS DATA: {e.Data}"); - - Console.WriteLine($"[{_username}] RAW WS DATA: {e.Data}"); + string decryptedText; try { - using var doc = JsonDocument.Parse(e.Data); - var root = doc.RootElement; + var privateKey = KeyStorage.LoadPrivateKey(_username); - if (!root.TryGetProperty("Type", out var typeElement)) - return; - - var type = (SignalType)typeElement.GetInt32(); - - if (type == SignalType.ChannelList) - { - var channelList = JsonSerializer.Deserialize(e.Data); - if (channelList is null) - return; - - _channels.Clear(); - _channels.AddRange(channelList.Channels.OrderBy(c => c.CreatedAt)); - - var defaultChannel = _channels - .Where(c => c.Name.Equals("welcome", StringComparison.OrdinalIgnoreCase)) - .OrderBy(c => c.CreatedAt) - .FirstOrDefault() - ?? _channels.OrderBy(c => c.CreatedAt).FirstOrDefault(); - - if (defaultChannel is not null) - { - _currentChannelId = defaultChannel.ChannelId; - _currentChannelName = defaultChannel.Name; - - MainThread.BeginInvokeOnMainThread(async () => - { - ChannelLabel.Text = $"#{_currentChannelName}"; - RenderChannelList(); - await PushRtcContextToJsAsync(); - }); - - _wsc.Send($"GET_HISTORY|{_username}|{_currentChannelId}"); - } - - return; - } - - if (type == SignalType.ServerPublicKey) - { - var serverKeyMessage = JsonSerializer.Deserialize(e.Data); - if (serverKeyMessage is not null) - { - _serverPublicKey = serverKeyMessage.PublicKey; - Console.WriteLine($"[{_username}] loaded server public key."); - } - - return; - } - - if (type == SignalType.EncryptedSignal) - { - var payload = JsonSerializer.Deserialize(e.Data); - if (payload is null) - return; - - if (payload.ChannelId != _currentChannelId) - return; - - if (payload.SenderUsername == _username) - return; - - var privateKey = KeyStorage.LoadPrivateKey(_username); - - var decryptedJson = E2EeHelper.DecryptForRecipient( - new EncryptedPayload - { - CipherText = payload.CipherText, - Nonce = payload.Nonce, - Tag = payload.Tag, - EncryptedKey = payload.EncryptedKey - }, - privateKey - ); - - var rtcSignal = JsonSerializer.Deserialize(decryptedJson); - - if (rtcSignal is null) - return; - - if (!string.IsNullOrWhiteSpace(rtcSignal.To) && - !string.Equals(rtcSignal.To, _username, StringComparison.OrdinalIgnoreCase)) - { - SafeSendRawToWebView($"Ignoring RTC signal meant for {rtcSignal.To}"); - return; - } - - SafeSendRawToWebView("Received encrypted RTC signal: " + decryptedJson); - - MainThread.BeginInvokeOnMainThread(async () => - { - await SendRtcSignalToJsAsync(decryptedJson); - }); - - return; - } - - if (type != SignalType.EncryptedChat) - return; - - var pyload = JsonSerializer.Deserialize(e.Data); - if (pyload is null) - return; - - if (pyload.RecipientUsername != _username) - return; - - Console.WriteLine($"[{_username}] received encrypted payload for {pyload.RecipientUsername}"); - - var privKey = KeyStorage.LoadPrivateKey(_username); - - var decryptedText = E2EeHelper.DecryptForRecipient( + decryptedText = E2EeHelper.DecryptForRecipient( new EncryptedPayload { - CipherText = pyload.CipherText, - Nonce = pyload.Nonce, - Tag = pyload.Tag, - EncryptedKey = pyload.EncryptedKey + CipherText = payload.CipherText, + Nonce = payload.Nonce, + Tag = payload.Tag, + EncryptedKey = payload.EncryptedKey }, - privKey - ); - - Console.WriteLine($"[{_username}] decrypted message from {pyload.SenderUsername}: {decryptedText}"); - - var message = new ChatMessage - { - SenderUsername = pyload.SenderUsername, - Text = decryptedText, - Timestamp = DateTime.Now - }; - - if (!_messagesByChannel.ContainsKey(pyload.ChannelId)) - { - _messagesByChannel[pyload.ChannelId] = []; - } - - _messagesByChannel[pyload.ChannelId].Add(message); - - if (pyload.ChannelId == _currentChannelId) - { - MainThread.BeginInvokeOnMainThread(() => - { - RenderSingleMessage(message); - }); - } + privateKey + ); } catch (Exception ex) { - Console.WriteLine($"[{_username}] failed to process websocket message: {ex.Message}"); + Console.WriteLine($"[{_username}] failed to decrypt chat: {ex.Message}"); + return; + } + + var message = new ChatMessage + { + SenderUsername = payload.SenderUsername, + Text = decryptedText, + Timestamp = DateTime.Now + }; + + if (!_messagesByChannel.ContainsKey(payload.ChannelId)) + _messagesByChannel[payload.ChannelId] = []; + + _messagesByChannel[payload.ChannelId].Add(message); + + if (payload.ChannelId == _currentChannelId) + { + MainThread.BeginInvokeOnMainThread(() => + { + RenderSingleMessage(message); + }); } } protected override void OnDisappearing() { - _wsc.OnMessage -= WscOnMessage; - _wsc.Close(); + _socket.Disconnect(); base.OnDisappearing(); } @@ -295,11 +208,14 @@ public partial class MainPage : ContentPage MainThread.BeginInvokeOnMainThread(async () => { - await PushRtcContextToJsAsync(); + await _rtc.PushRtcContextToJsAsync(); + if (channel.Type == ChannelType.Voice) { - SwapView(); - _ = JoinRtcChannel(); //TODO: Join voice calls when clicking channel rather than a separate button + if (!RtcView.IsVisible) + SwapView(); + + _ = _rtc.JoinRtcChannel(); } }); @@ -307,9 +223,7 @@ public partial class MainPage : ContentPage RenderCurrentChannelMessages(); if (!_messagesByChannel.ContainsKey(channel.ChannelId)) - { - _wsc.Send($"GET_HISTORY|{_username}|{channel.ChannelId}"); - } + _socket.SendRaw($"GET_HISTORY|{_username}|{channel.ChannelId}"); }; SidebarList.Children.Add(button); @@ -334,7 +248,7 @@ public partial class MainPage : ContentPage private async void RenderSingleMessage(ChatMessage message) { - bool isOwnMessage = message.SenderUsername == _username; + var isOwnMessage = message.SenderUsername == _username; var bubble = new Border { @@ -369,7 +283,6 @@ public partial class MainPage : ContentPage MessagesScrollView.IsVisible = true; RtcView.IsVisible = false; ViewSwapped.Text = "Swap to Web View"; - } else { @@ -384,154 +297,37 @@ public partial class MainPage : ContentPage SwapView(); } - #region RTC Functions - public Task JoinRtcChannel() { - if (string.IsNullOrWhiteSpace(_currentChannelId)) - return Task.CompletedTask; - - _wsc.Send($"RTC_JOIN_CHANNEL|{_username}|{_currentChannelId}"); - return Task.CompletedTask; + return _rtc.JoinRtcChannel(); } public void LeaveRtcChannel() { - if (string.IsNullOrWhiteSpace(_currentChannelId)) - return; - - _wsc.Send($"RTC_LEAVE_CHANNEL|{_username}|{_currentChannelId}"); + _rtc.LeaveRtcChannel(); } public void SendRtcSignal(string json) { - if (string.IsNullOrWhiteSpace(_serverPublicKey)) - { - SafeSendRawToWebView("SendRtcSignal failed: server public key not loaded."); - return; - } - - RtcSignalMessage? rtcSignal; - - try - { - rtcSignal = JsonSerializer.Deserialize(json); - } - catch (Exception ex) - { - SafeSendRawToWebView("SendRtcSignal failed to parse RTC signal: " + ex.Message); - return; - } - - if (rtcSignal is null) - return; - - if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId)) - rtcSignal.ChannelId = _currentChannelId; - - if (string.IsNullOrWhiteSpace(rtcSignal.From)) - rtcSignal.From = _username; - - if (string.IsNullOrWhiteSpace(rtcSignal.ChannelId)) - { - SafeSendRawToWebView("SendRtcSignal failed: missing channel id."); - return; - } - - var outgoingJson = JsonSerializer.Serialize(rtcSignal); - - try - { - var encrypted = E2EeHelper.EncryptForRecipient(outgoingJson, _serverPublicKey); - - var payload = new SocketRtcSignalMessage - { - Type = SignalType.EncryptedSignal, - SenderUsername = _username, - ChannelId = rtcSignal.ChannelId, - CipherText = encrypted.CipherText, - Nonce = encrypted.Nonce, - Tag = encrypted.Tag, - EncryptedKey = encrypted.EncryptedKey - }; - - _wsc.Send(JsonSerializer.Serialize(payload)); - - SafeSendRawToWebView($"SendRtcSignal sent: {rtcSignal.Type} -> {rtcSignal.To}"); - } - catch (Exception ex) - { - SafeSendRawToWebView("SendRtcSignal failed: " + ex.Message); - } + _rtc.SendRtcSignal(json); } - public async Task GetRtcParticipants() + public Task GetRtcParticipants() { - if (string.IsNullOrWhiteSpace(_currentChannelId)) - return "[]"; - - var participants = await ServerAPI.GetRtcParticipantsAsync(_currentChannelId); - return JsonSerializer.Serialize(participants ?? []); + return _rtc.GetRtcParticipants(); } - private Task SendRtcSignalToJsAsync(string rawJson) - { - MainThread.BeginInvokeOnMainThread(async () => - { - try - { - var jsArg = JsonSerializer.Serialize(rawJson); - - await hybridWebView.EvaluateJavaScriptAsync($@" - try {{ - window.HybridWebView.SendRawMessage('C# eval entered'); - - if (!window.RelaySocket) {{ - window.HybridWebView.SendRawMessage('window.RelaySocket missing'); - }} else if (typeof window.RelaySocket.receiveRtcSignal !== 'function') {{ - window.HybridWebView.SendRawMessage('RelaySocket.receiveRtcSignal missing'); - }} else {{ - window.HybridWebView.SendRawMessage('Calling RelaySocket.receiveRtcSignal'); - window.RelaySocket.receiveRtcSignal({jsArg}); - }} - }} catch (err) {{ - window.HybridWebView.SendRawMessage('RTC JS dispatch failed: ' + err); - }} - "); - } - catch (Exception ex) - { - SafeSendRawToWebView("SendRtcSignalToJsAsync failed: " + ex.Message); - } - }); - - return Task.CompletedTask; - } - - private async Task PushRtcContextToJsAsync() - { - var usernameJson = JsonSerializer.Serialize(_username); - var channelIdJson = JsonSerializer.Serialize(_currentChannelId); - - await hybridWebView.EvaluateJavaScriptAsync($"window.setUsername({usernameJson})"); - await hybridWebView.EvaluateJavaScriptAsync($"window.setChannelId({channelIdJson})"); - - Console.WriteLine($"[{_username}] pushed RTC context into HybridWebView."); - } - - #endregion - private async void OnHybridWebViewRawMessageReceived(object sender, HybridWebViewRawMessageReceivedEventArgs e) { if (e.Message == "rtc_page_ready") { - await PushRtcContextToJsAsync(); + await _rtc.PushRtcContextToJsAsync(); return; } SafeSendRawToWebView($"JS RAW -> C#: {e.Message}"); } - + private void SafeSendRawToWebView(string message) { MainThread.BeginInvokeOnMainThread(() => @@ -550,9 +346,9 @@ public partial class MainPage : ContentPage public class ChannelButton : Button { public ChannelType Type { get; set; } - public string Group { get; set; } + public string Group { get; set; } = string.Empty; } - + [JsonSourceGenerationOptions(WriteIndented = false)] [JsonSerializable(typeof(RtcDescription))] [JsonSerializable(typeof(List))] @@ -561,8 +357,5 @@ public partial class MainPage : ContentPage [JsonSerializable(typeof(string))] internal partial class HybridJSType : JsonSerializerContext { - // This type's attributes specify JSON serialization info to preserve type structure - // for trimmed builds. } - } \ No newline at end of file