Files
Relay/RelayClient/MainPage.xaml.cs
2026-03-22 04:43:43 -04:00

360 lines
11 KiB
C#

using RelayClient.Crypto;
using RelayClient.Models;
using WebSocketSharp;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.MixedReality.WebRTC;
using System.IO;
using System.Collections.Generic;
namespace RelayClient;
public partial class MainPage : ContentPage
{
private readonly string _username;
private readonly WebSocket _wsc;
private string? _serverPublicKey;
private string? _currentChannelId;
private string? _currentChannelName;
private readonly Dictionary<string, List<ChatMessage>> _messagesByChannel = new();
private readonly List<ChannelItem> _channels = new();
public MainPage(string username)
{
InitializeComponent();
_username = username;
UserLabel.Text = $"Logged in as: {_username}";
if (!KeyStorage.HasKeys(_username))
{
var keys = E2EeHelper.GenerateRsaKeyPair();
KeyStorage.SavePrivateKey(_username, keys.privateKey);
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");
webRTCTest();
}
private void SendButton_OnClicked(object? sender, EventArgs e)
{
SendMessage();
}
private void MessageEntry_OnCompleted(object? sender, EventArgs e)
{
SendMessage();
}
private void SendMessage()
{
var text = MessageEntry.Text?.Trim();
if (string.IsNullOrWhiteSpace(text))
return;
if (string.IsNullOrWhiteSpace(_serverPublicKey))
{
Console.WriteLine("Server public key not loaded yet.");
return;
}
var encrypted = E2EeHelper.EncryptForRecipient(text, _serverPublicKey);
var payload = new SocketEncryptedMessage
{
ChannelId = _currentChannelId!,
Type = "client_encrypted_chat",
SenderUsername = _username,
CipherText = encrypted.CipherText,
Nonce = encrypted.Nonce,
Tag = encrypted.Tag,
EncryptedKey = encrypted.EncryptedKey
};
var json = JsonSerializer.Serialize(payload);
_wsc.Send(json);
Console.WriteLine($"[{_username}] sent encrypted message.");
MessageEntry.Text = string.Empty;
MessageEntry.Focus();
}
private void WscOnMessage(object? sender, MessageEventArgs e)
{
if (e.Data.StartsWith("SERVER:REGISTERED_KEY:"))
{
Console.WriteLine(e.Data);
return;
}
Console.WriteLine($"[{_username}] RAW WS DATA: {e.Data}");
try
{
using var doc = JsonDocument.Parse(e.Data);
var root = doc.RootElement;
if (!root.TryGetProperty("Type", out var typeElement))
return;
var type = typeElement.GetString();
if (type == "channel_list")
{
var channelList = JsonSerializer.Deserialize<SocketChannelList>(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<ServerPublicKeyMessage>(e.Data);
if (serverKeyMessage is not null)
{
_serverPublicKey = serverKeyMessage.PublicKey;
Console.WriteLine($"[{_username}] loaded server public key.");
}
return;
}
if (type != "encrypted_chat")
return;
var payload = JsonSerializer.Deserialize<SocketEncryptedMessage>(e.Data);
if (payload is null)
return;
if (payload.RecipientUsername != _username)
return;
Console.WriteLine($"[{_username}] received encrypted payload for {payload.RecipientUsername}");
var privateKey = KeyStorage.LoadPrivateKey(_username);
var decryptedText = E2EeHelper.DecryptForRecipient(
new EncryptedPayload
{
CipherText = payload.CipherText,
Nonce = payload.Nonce,
Tag = payload.Tag,
EncryptedKey = payload.EncryptedKey
},
privateKey
);
Console.WriteLine($"[{_username}] decrypted message from {payload.SenderUsername}: {decryptedText}");
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);
});
}
}
catch (Exception ex)
{
Console.WriteLine($"[{_username}] failed to process websocket message: {ex.Message}");
}
}
protected override void OnDisappearing()
{
_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);
}
private async Task webRTCTest()
{
// var path = "D:\\DDI\\Relay\\RelayChat\\RelayCore\\RelayClient\\bin\\debug.txt";
// AudioTrackSource microphoneSource = null;
// VideoTrackSource webcamSource = null;
// Transceiver audioTransceiver = null;
// Transceiver videoTransceiver = null;
// LocalAudioTrack localAudioTrack = null;
// LocalVideoTrack localVideoTrack = null;
try
{
var deviceList = await DeviceVideoTrackSource.GetCaptureDevicesAsync();
string devices = "";
foreach (var device in deviceList)
{
devices += $"Found device: {device.name} (id: {device.id})\n";
Console.WriteLine($"Found device: {device.name} (id: {device.id})");
}
File.WriteAllText(path, devices);
}
catch (Exception e)
{
File.WriteAllText(path, e.Message);
Console.WriteLine(e);
throw;
}
using var pc = new PeerConnection();
var config = new PeerConnectionConfiguration
{
IceServers = new List<IceServer>
{
new IceServer { Urls = { "stun:stun.l.google.com:19302" } }
}
};
await pc.InitializeAsync(config);
// webcamSource = await DeviceVideoTrackSource.CreateAsync();
// var videoTrackConfig = new LocalVideoTrackInitConfig
// {
// trackName = "webcam_track"
// };
// localVideoTrack = LocalVideoTrack.CreateFromSource(webcamSource, videoTrackConfig);
// microphoneSource = await DeviceAudioTrackSource.CreateAsync();
// var audioTrackConfig = new LocalAudioTrackInitConfig
// {
// trackName = "microphone_track"
// };
// localAudioTrack = LocalAudioTrack.CreateFromSource(microphoneSource, audioTrackConfig);
// videoTransceiver = pc.AddTransceiver(MediaKind.Video);
// videoTransceiver.LocalVideoTrack = localVideoTrack;
// videoTransceiver.DesiredDirection = Transceiver.Direction.SendReceive;
//
// audioTransceiver = pc.AddTransceiver(MediaKind.Audio);
// audioTransceiver.LocalAudioTrack = localAudioTrack;
// audioTransceiver.DesiredDirection = Transceiver.Direction.SendReceive;
//
//
//
// localAudioTrack?.Dispose();
// localVideoTrack?.Dispose();
// microphoneSource?.Dispose();
// webcamSource?.Dispose();
}
}