From caf020c393cdd9edb4ab150ccbb02ae23416558b Mon Sep 17 00:00:00 2001 From: RuKira Date: Sun, 22 Mar 2026 01:54:52 -0400 Subject: [PATCH] Update: Mutli Channel Support --- RelayClient/MainPage.xaml | 48 ++++- RelayClient/MainPage.xaml.cs | 183 ++++++++++++++----- RelayClient/Models/ChannelItem.cs | 8 + RelayClient/Models/SocketChannelList.cs | 7 + RelayClient/Models/SocketEncryptedMessage.cs | 1 + RelayServer/Models/SocketChannelInfo.cs | 8 + RelayServer/Models/SocketChannelList.cs | 7 + RelayServer/Models/SocketEncryptedMessage.cs | 3 +- RelayServer/Program.cs | 17 +- RelayServer/Services/ChatTest.cs | 70 ++++++- 10 files changed, 283 insertions(+), 69 deletions(-) create mode 100644 RelayClient/Models/ChannelItem.cs create mode 100644 RelayClient/Models/SocketChannelList.cs create mode 100644 RelayServer/Models/SocketChannelInfo.cs create mode 100644 RelayServer/Models/SocketChannelList.cs diff --git a/RelayClient/MainPage.xaml b/RelayClient/MainPage.xaml index 18006dc..ee2be68 100644 --- a/RelayClient/MainPage.xaml +++ b/RelayClient/MainPage.xaml @@ -5,26 +5,60 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" Title="Relay Client"> - + - + + - - - + + + + + - + + + + + + + + + > _messagesByChannel = new(); + private readonly List _channels = new(); public MainPage(string username) { @@ -25,15 +30,15 @@ public partial class MainPage : ContentPage KeyStorage.SavePublicKey(_username, keys.publicKey); } - wsc = new WebSocket("ws://localhost:1337/"); + _wsc = new WebSocket("ws://localhost:1337/"); - wsc.OnMessage += WscOnMessage; - wsc.Connect(); + _wsc.OnMessage += WscOnMessage; + _wsc.Connect(); var publicKey = KeyStorage.LoadPublicKey(_username); - wsc.Send($"REGISTER_KEY|{_username}|{publicKey}"); - wsc.Send("GET_SERVER_KEY"); - wsc.Send($"GET_HISTORY|{_username}"); + _wsc.Send($"REGISTER_KEY|{_username}|{publicKey}"); + _wsc.Send("GET_SERVER_KEY"); + _wsc.Send("GET_CHANNELS"); } private void SendButton_OnClicked(object? sender, EventArgs e) @@ -63,6 +68,7 @@ public partial class MainPage : ContentPage var payload = new SocketEncryptedMessage { + ChannelId = _currentChannelId!, Type = "client_encrypted_chat", SenderUsername = _username, CipherText = encrypted.CipherText, @@ -72,7 +78,7 @@ public partial class MainPage : ContentPage }; var json = JsonSerializer.Serialize(payload); - wsc.Send(json); + _wsc.Send(json); Console.WriteLine($"[{_username}] sent encrypted message."); @@ -100,6 +106,38 @@ public partial class MainPage : ContentPage var type = typeElement.GetString(); + if (type == "channel_list") + { + 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(() => + { + ChannelLabel.Text = $"#{_currentChannelName}"; + RenderChannelList(); + }); + + _wsc.Send($"GET_HISTORY|{_username}|{_currentChannelId}"); + } + + return; + } + if (type == "server_public_key") { var serverKeyMessage = JsonSerializer.Deserialize(e.Data); @@ -146,48 +184,20 @@ public partial class MainPage : ContentPage Timestamp = DateTime.Now }; - MainThread.BeginInvokeOnMainThread(async () => + if (!_messagesByChannel.ContainsKey(payload.ChannelId)) { - bool isOwnMessage = message.SenderUsername == _username; + _messagesByChannel[payload.ChannelId] = []; + } - var bubble = new Border + _messagesByChannel[payload.ChannelId].Add(message); + + if (payload.ChannelId == _currentChannelId) + { + MainThread.BeginInvokeOnMainThread(() => { - StrokeThickness = 1, - Padding = 10, - Margin = isOwnMessage - ? new Thickness(40, 0, 0, 0) - : new Thickness(0, 0, 40, 0), - HorizontalOptions = isOwnMessage - ? LayoutOptions.End - : LayoutOptions.Start, - Content = new VerticalStackLayout - { - Spacing = 2, - Children = - { - new Label - { - Text = message.SenderUsername, - FontAttributes = FontAttributes.Bold, - FontSize = 12 - }, - new Label - { - Text = message.Text, - FontSize = 14 - }, - new Label - { - Text = message.Timestamp.ToString("h:mm tt"), - FontSize = 10 - } - } - } - }; - - MessagesLayout.Children.Add(bubble); - await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true); - }); + RenderSingleMessage(message); + }); + } } catch (Exception ex) { @@ -197,8 +207,83 @@ public partial class MainPage : ContentPage protected override void OnDisappearing() { - wsc.OnMessage -= WscOnMessage; - wsc.Close(); + _wsc.OnMessage -= WscOnMessage; + _wsc.Close(); base.OnDisappearing(); } + + private void RenderChannelList() + { + SidebarList.Children.Clear(); + + foreach (var channel in _channels.OrderBy(c => c.CreatedAt)) + { + var button = new Button + { + Text = $"#{channel.Name}" + }; + + button.Clicked += (_, _) => + { + _currentChannelId = channel.ChannelId; + _currentChannelName = channel.Name; + + ChannelLabel.Text = $"#{_currentChannelName}"; + RenderCurrentChannelMessages(); + + if (!_messagesByChannel.ContainsKey(channel.ChannelId)) + { + _wsc.Send($"GET_HISTORY|{_username}|{channel.ChannelId}"); + } + }; + + SidebarList.Children.Add(button); + } + } + + private void RenderCurrentChannelMessages() + { + MessagesLayout.Children.Clear(); + + if (_currentChannelId is null) + return; + + if (!_messagesByChannel.TryGetValue(_currentChannelId, out var messages)) + return; + + foreach (var message in messages.OrderBy(m => m.Timestamp)) + { + RenderSingleMessage(message); + } + } + + private async void RenderSingleMessage(ChatMessage message) + { + bool isOwnMessage = message.SenderUsername == _username; + + var bubble = new Border + { + StrokeThickness = 1, + Padding = 10, + Margin = isOwnMessage + ? new Thickness(40, 0, 0, 0) + : new Thickness(0, 0, 40, 0), + HorizontalOptions = isOwnMessage + ? LayoutOptions.End + : LayoutOptions.Start, + Content = new VerticalStackLayout + { + Spacing = 2, + Children = + { + new Label { Text = message.SenderUsername, FontAttributes = FontAttributes.Bold, FontSize = 12 }, + new Label { Text = message.Text, FontSize = 14 }, + new Label { Text = message.Timestamp.ToString("h:mm tt"), FontSize = 10 } + } + } + }; + + MessagesLayout.Children.Add(bubble); + await MessagesScrollView.ScrollToAsync(MessagesLayout, ScrollToPosition.End, true); + } } \ No newline at end of file diff --git a/RelayClient/Models/ChannelItem.cs b/RelayClient/Models/ChannelItem.cs new file mode 100644 index 0000000..be3ddac --- /dev/null +++ b/RelayClient/Models/ChannelItem.cs @@ -0,0 +1,8 @@ +namespace RelayClient.Models; + +public class ChannelItem +{ + public required string ChannelId { get; set; } + public required string Name { get; set; } + public required DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/RelayClient/Models/SocketChannelList.cs b/RelayClient/Models/SocketChannelList.cs new file mode 100644 index 0000000..f9d149d --- /dev/null +++ b/RelayClient/Models/SocketChannelList.cs @@ -0,0 +1,7 @@ +namespace RelayClient.Models; + +public class SocketChannelList +{ + public required string Type { get; set; } + public required List Channels { get; set; } +} \ No newline at end of file diff --git a/RelayClient/Models/SocketEncryptedMessage.cs b/RelayClient/Models/SocketEncryptedMessage.cs index f36863f..1e158c2 100644 --- a/RelayClient/Models/SocketEncryptedMessage.cs +++ b/RelayClient/Models/SocketEncryptedMessage.cs @@ -5,6 +5,7 @@ public class SocketEncryptedMessage public required string Type { get; set; } public required string SenderUsername { get; set; } public string? RecipientUsername { get; set; } + public required string ChannelId { get; set; } public required string CipherText { get; set; } public required string Nonce { get; set; } public required string Tag { get; set; } diff --git a/RelayServer/Models/SocketChannelInfo.cs b/RelayServer/Models/SocketChannelInfo.cs new file mode 100644 index 0000000..71a827f --- /dev/null +++ b/RelayServer/Models/SocketChannelInfo.cs @@ -0,0 +1,8 @@ +namespace RelayServer.Models; + +public class SocketChannelInfo +{ + public required string ChannelId { get; set; } + public required string Name { get; set; } + public required DateTime CreatedAt { get; set; } +} \ No newline at end of file diff --git a/RelayServer/Models/SocketChannelList.cs b/RelayServer/Models/SocketChannelList.cs new file mode 100644 index 0000000..f0f3635 --- /dev/null +++ b/RelayServer/Models/SocketChannelList.cs @@ -0,0 +1,7 @@ +namespace RelayServer.Models; + +public class SocketChannelList +{ + public required string Type { get; set; } + public required List Channels { get; set; } +} \ No newline at end of file diff --git a/RelayServer/Models/SocketEncryptedMessage.cs b/RelayServer/Models/SocketEncryptedMessage.cs index 472e82c..05896f1 100644 --- a/RelayServer/Models/SocketEncryptedMessage.cs +++ b/RelayServer/Models/SocketEncryptedMessage.cs @@ -4,7 +4,8 @@ public class SocketEncryptedMessage { public required string Type { get; set; } public required string SenderUsername { get; set; } - public required string RecipientUsername { get; set; } + public string? RecipientUsername { get; set; } + public required string ChannelId { get; set; } public required string CipherText { get; set; } public required string Nonce { get; set; } public required string Tag { get; set; } diff --git a/RelayServer/Program.cs b/RelayServer/Program.cs index ce40f98..cbdf928 100644 --- a/RelayServer/Program.cs +++ b/RelayServer/Program.cs @@ -75,18 +75,29 @@ var channel = await db.Create("channels", new Channels CreatedAt = DateTime.UtcNow }); -var channel2 = await db.Create("files", new Channels +var channel2 = await db.Create("channels", new Channels { Name = "files", - CreatedAt = DateTime.UtcNow.Subtract(new TimeSpan(0, 4, 0,0)) + CreatedAt = DateTime.UtcNow.Subtract(new TimeSpan(0, 4, 0, 0)) }); +var channel3 = await db.Create("channels", new Channels +{ + Name = "welcome", + CreatedAt = DateTime.UtcNow.Subtract(new TimeSpan(1, 4, 4, 4)) +}); +Console.WriteLine($"Channel created: {ToJsonString(channel)}"); Console.WriteLine($"Channel created: {ToJsonString(channel2)}"); +Console.WriteLine($"Channel created: {ToJsonString(channel3)}"); var channelId = GetRecordId(channel.Id); +var channelId2 = GetRecordId(channel2.Id); +var channelId3 = GetRecordId(channel3.Id); + Console.WriteLine($"Resolved channelId: {channelId}"); -ChatTest.DefaultChannelId = channelId; +Console.WriteLine($"Resolved channelId: {channelId2}"); +Console.WriteLine($"Resolved channelId: {channelId3}"); var keyBase64 = cryptoService.GenerateKey(); var serverKeys = E2EeHelper.GenerateRsaKeyPair(); diff --git a/RelayServer/Services/ChatTest.cs b/RelayServer/Services/ChatTest.cs index 39e5bf2..e31f1b9 100644 --- a/RelayServer/Services/ChatTest.cs +++ b/RelayServer/Services/ChatTest.cs @@ -12,7 +12,6 @@ public class ChatTest : WebSocketBehavior public static string? ServerPrivateKey { get; set; } public static string? ChannelDbKey { get; set; } public static SurrealDb.Net.SurrealDbClient? Db { get; set; } - public static string? DefaultChannelId { get; set; } protected override void OnMessage(MessageEventArgs e) { @@ -31,6 +30,12 @@ public class ChatTest : WebSocketBehavior return; } + if (msg == "GET_CHANNELS") + { + HandleGetChannels(); + return; + } + if (msg.StartsWith("GET_HISTORY|")) { HandleGetHistory(msg); @@ -76,6 +81,35 @@ public class ChatTest : WebSocketBehavior Send($"SERVER:REGISTERED_KEY:{username}"); } + private void HandleGetChannels() + { + if (Db is null) + { + Console.WriteLine("Db is not initialized."); + return; + } + + var channels = Task.Run(async () => await Db.Select("channels")) + .GetAwaiter() + .GetResult() + .OrderBy(c => c.CreatedAt) + .Select(c => new SocketChannelInfo + { + ChannelId = GetRecordId(c.Id), + Name = c.Name, + CreatedAt = c.CreatedAt + }) + .ToList(); + + var payload = new SocketChannelList + { + Type = "channel_list", + Channels = channels + }; + + Send(JsonSerializer.Serialize(payload)); + } + private void HandleGetServerKey() { if (string.IsNullOrWhiteSpace(ServerPublicKey)) @@ -113,8 +147,7 @@ public class ChatTest : WebSocketBehavior if (ClientKeyService is null || Db is null || string.IsNullOrWhiteSpace(ServerPrivateKey) || - string.IsNullOrWhiteSpace(ChannelDbKey) || - string.IsNullOrWhiteSpace(DefaultChannelId)) + string.IsNullOrWhiteSpace(ChannelDbKey)) { Console.WriteLine("Server crypto/database dependencies are not initialized."); return; @@ -150,7 +183,7 @@ public class ChatTest : WebSocketBehavior var savedMessage = Task.Run(async () => await Db.Create("channel_messages", new ChannelMessages { - ChannelId = DefaultChannelId, + ChannelId = clientPayload.ChannelId, SenderUserId = $"users:{clientPayload.SenderUsername.ToLower()}", CipherText = dbEncrypted.cipherText, Nonce = dbEncrypted.nonce, @@ -182,6 +215,7 @@ public class ChatTest : WebSocketBehavior Type = "encrypted_chat", SenderUsername = clientPayload.SenderUsername, RecipientUsername = client.Username, + ChannelId = clientPayload.ChannelId, CipherText = encrypted.CipherText, Nonce = encrypted.Nonce, Tag = encrypted.Tag, @@ -194,20 +228,20 @@ public class ChatTest : WebSocketBehavior private void HandleGetHistory(string msg) { - var parts = msg.Split('|', 2); + var parts = msg.Split('|', 3); - if (parts.Length < 2) + if (parts.Length < 3) { Console.WriteLine("Invalid GET_HISTORY payload."); return; } var username = parts[1]; + var channelId = parts[2]; if (ClientKeyService is null || Db is null || - string.IsNullOrWhiteSpace(ChannelDbKey) || - string.IsNullOrWhiteSpace(DefaultChannelId)) + string.IsNullOrWhiteSpace(ChannelDbKey)) { Console.WriteLine("History dependencies are not initialized."); return; @@ -228,7 +262,7 @@ public class ChatTest : WebSocketBehavior .GetResult(); var channelMessages = allMessages - .Where(m => m.ChannelId == DefaultChannelId) + .Where(m => m.ChannelId == channelId) .OrderBy(m => m.CreatedAt) .ToList(); @@ -262,6 +296,7 @@ public class ChatTest : WebSocketBehavior Type = "encrypted_chat", SenderUsername = ExtractUsernameFromUserId(dbMessage.SenderUserId), RecipientUsername = username, + ChannelId = channelId, CipherText = encrypted.CipherText, Nonce = encrypted.Nonce, Tag = encrypted.Tag, @@ -271,4 +306,21 @@ public class ChatTest : WebSocketBehavior Send(JsonSerializer.Serialize(outbound)); } } + + private static string GetRecordId(object? id) + { + if (id is null) + return string.Empty; + + var json = JsonSerializer.Serialize(id); + + using var doc = JsonDocument.Parse(json); + + var root = doc.RootElement; + + var recordId = root.GetProperty("Id").GetString() ?? string.Empty; + var table = root.GetProperty("Table").GetString() ?? string.Empty; + + return $"{table}:{recordId}"; + } } \ No newline at end of file