762 lines
31 KiB
C#
762 lines
31 KiB
C#
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
using RelayServer.Models;
|
|
using RelayServer.Services.Crypto;
|
|
using RelayServer.Services.Data;
|
|
using RelayServer.Services.Rtc;
|
|
using WebSocketSharp;
|
|
using WebSocketSharp.Server;
|
|
using ErrorEventArgs = WebSocketSharp.ErrorEventArgs;
|
|
using RelayShared.Services;
|
|
|
|
namespace RelayServer.Services.Chat;
|
|
|
|
/// <summary>
|
|
/// Handles all WebSocket traffic: authentication, key registration, channel management,
|
|
/// encrypted chat relay, message editing/deletion, typing indicators, and edit history.
|
|
/// </summary>
|
|
public class ChatSocketBehavior : WebSocketBehavior
|
|
{
|
|
public static ClientKeyService? ClientKeyService { get; set; }
|
|
public static PermissionService? PermissionService { get; set; }
|
|
public static string? ServerPublicKey { get; set; }
|
|
public static string? ServerPrivateKey { get; set; }
|
|
public static string? ChannelDbKey { get; set; }
|
|
public static ChannelCryptoService? ChannelCryptoService { get; set; }
|
|
public static SurrealDb.Net.SurrealDbClient? Db { get; set; }
|
|
|
|
protected override void OnMessage(MessageEventArgs e)
|
|
{
|
|
var msg = e.Data;
|
|
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(msg);
|
|
var root = doc.RootElement;
|
|
|
|
if (root.TryGetProperty("Action", out var actionProp))
|
|
{
|
|
var action = (WsAction)actionProp.GetInt32();
|
|
var control = JsonSerializer.Deserialize<WsControlMessage>(msg)!;
|
|
DispatchControl(action, control);
|
|
return;
|
|
}
|
|
|
|
if (root.TryGetProperty("Type", out var typeProp))
|
|
{
|
|
var type = (SignalType)typeProp.GetInt32();
|
|
switch (type)
|
|
{
|
|
case SignalType.EncryptedSignal: HandleEncryptedRtcSignal(msg); return;
|
|
case SignalType.ClientEncryptedChat: HandleEncryptedChatMessage(msg); return;
|
|
case SignalType.ClientEditMessage: HandleEditMessage(msg); return;
|
|
case SignalType.ClientDeleteMessage: HandleDeleteMessage(msg); return;
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"Unrecognised WS message session={ID}: {msg[..Math.Min(120, msg.Length)]}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"WS message error session={ID}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void DispatchControl(WsAction action, WsControlMessage c)
|
|
{
|
|
switch (action)
|
|
{
|
|
case WsAction.Authenticate: HandleAuthenticate(c); break;
|
|
case WsAction.RegisterKey: HandleRegisterKey(c); break;
|
|
case WsAction.GetServerKey: HandleGetServerKey(); break;
|
|
case WsAction.GetChannels: HandleGetChannels(); break;
|
|
case WsAction.GetHistory: HandleGetHistory(c); break;
|
|
case WsAction.RtcJoin: HandleRtcJoinChannel(c); break;
|
|
case WsAction.RtcLeave: HandleRtcLeaveChannel(c); break;
|
|
case WsAction.SendTyping: HandleTyping(c); break;
|
|
case WsAction.GetEditHistory: HandleGetEditHistory(c); break;
|
|
case WsAction.CreateChannel: HandleCreateChannel(c); break;
|
|
case WsAction.DeleteChannel: HandleDeleteChannel(c); break;
|
|
default: Console.WriteLine($"Unknown WsAction {action} session={ID}"); break;
|
|
}
|
|
}
|
|
|
|
private async void HandleAuthenticate(WsControlMessage c)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(c.Username) || string.IsNullOrWhiteSpace(c.Token))
|
|
{
|
|
Console.WriteLine("Invalid Authenticate payload.");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var core = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:1337") };
|
|
core.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
core.DefaultRequestHeaders.Add("User-Agent", "RelayServer");
|
|
|
|
var resp = await core.PostAsJsonAsync("/server/verify/user",
|
|
new AuthUserVerify { Username = c.Username, Token = c.Token });
|
|
|
|
Console.WriteLine($"Auth [{c.Username}]: {await resp.Content.ReadAsStringAsync()}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Auth failed for {c.Username}: {ex.Message}");
|
|
}
|
|
|
|
Send(JsonSerializer.Serialize(new WsEventMessage { Event = WsEvent.Authenticated, Detail = c.Username }));
|
|
}
|
|
|
|
private void HandleRegisterKey(WsControlMessage c)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(c.Username) || string.IsNullOrWhiteSpace(c.PublicKey))
|
|
{
|
|
Console.WriteLine("Invalid RegisterKey payload.");
|
|
return;
|
|
}
|
|
|
|
if (ClientKeyService is null) { Console.WriteLine("ClientKeyService null."); return; }
|
|
|
|
RegisterOrUpdateClientKeySync(c.Username, c.PublicKey);
|
|
ConnectedClientService.Register(ID, c.Username);
|
|
|
|
Console.WriteLine($"Key registered: {c.Username} (session={ID})");
|
|
Send(JsonSerializer.Serialize(new WsEventMessage { Event = WsEvent.KeyRegistered, Detail = c.Username }));
|
|
}
|
|
|
|
private void HandleGetServerKey()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ServerPublicKey)) { Console.WriteLine("Server public key not initialised."); return; }
|
|
Send(JsonSerializer.Serialize(new ServerPublicKeyMessage { Type = SignalType.ServerPublicKey, PublicKey = ServerPublicKey }));
|
|
}
|
|
|
|
private void HandleGetChannels()
|
|
{
|
|
if (Db is null) { Console.WriteLine("Db null."); return; }
|
|
|
|
// Resolve the requesting user so we can compute per-user CanPost for each channel.
|
|
var username = ConnectedClientService.GetUsernameForSession(ID) ?? string.Empty;
|
|
|
|
var channels = BuildChannelListForUser(username);
|
|
Send(JsonSerializer.Serialize(new SocketChannelList { Type = SignalType.ChannelList, Channels = channels }));
|
|
}
|
|
|
|
private void HandleGetHistory(WsControlMessage c)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(c.Username) || string.IsNullOrWhiteSpace(c.ChannelId))
|
|
{
|
|
Console.WriteLine("Invalid GetHistory payload.");
|
|
return;
|
|
}
|
|
|
|
if (!EnsureCoreReady() || ChannelCryptoService is null || string.IsNullOrWhiteSpace(ChannelDbKey)) return;
|
|
|
|
var targetClient = GetClientPublicKeyByUsernameSync(c.Username);
|
|
if (targetClient is null) { Console.WriteLine($"No public key for history user {c.Username}"); return; }
|
|
|
|
var messages = GetChannelMessagesSync()
|
|
.Where(m => m.ChannelId == c.ChannelId)
|
|
.OrderBy(m => m.CreatedAt)
|
|
.ToList();
|
|
|
|
Console.WriteLine($"Sending {messages.Count} history messages to {c.Username}");
|
|
|
|
foreach (var dbMsg in messages)
|
|
{
|
|
var msgId = GetRecordId(dbMsg.Id);
|
|
|
|
if (dbMsg.IsDeleted)
|
|
{
|
|
Send(JsonSerializer.Serialize(new SocketEncryptedMessage
|
|
{
|
|
Type = SignalType.EncryptedChat, MessageId = msgId,
|
|
SenderUsername = ExtractUsernameFromUserId(dbMsg.SenderUserId),
|
|
RecipientUsername = c.Username, ChannelId = c.ChannelId, IsDeleted = true
|
|
}));
|
|
continue;
|
|
}
|
|
|
|
string plainText;
|
|
try { plainText = ChannelCryptoService.Decrypt(dbMsg.CipherText, dbMsg.Nonce, dbMsg.Tag, ChannelDbKey); }
|
|
catch (Exception ex) { Console.WriteLine($"History decrypt failed {dbMsg.Id}: {ex.Message}"); continue; }
|
|
|
|
var encrypted = E2EeHelper.EncryptForRecipient(plainText, targetClient.PublicKey);
|
|
Send(JsonSerializer.Serialize(new SocketEncryptedMessage
|
|
{
|
|
Type = SignalType.EncryptedChat, MessageId = msgId,
|
|
SenderUsername = ExtractUsernameFromUserId(dbMsg.SenderUserId),
|
|
RecipientUsername = c.Username, ChannelId = c.ChannelId,
|
|
CipherText = encrypted.CipherText, Nonce = encrypted.Nonce,
|
|
Tag = encrypted.Tag, EncryptedKey = encrypted.EncryptedKey,
|
|
IsEdited = dbMsg.EditedAt.HasValue
|
|
}));
|
|
}
|
|
}
|
|
|
|
private void HandleRtcJoinChannel(WsControlMessage c)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(c.Username) || string.IsNullOrWhiteSpace(c.ChannelId))
|
|
{
|
|
Console.WriteLine("Invalid RtcJoin payload.");
|
|
return;
|
|
}
|
|
|
|
if (PermissionService is not null &&
|
|
!PermissionService.CanSpeakAsync(c.Username, c.ChannelId).GetAwaiter().GetResult())
|
|
{
|
|
Console.WriteLine($"RTC join denied (no Speak): user={c.Username}, channel={c.ChannelId}");
|
|
Send(JsonSerializer.Serialize(new WsEventMessage { Event = WsEvent.Error, Detail = "You don't have permission to speak in this channel." }));
|
|
return;
|
|
}
|
|
|
|
RtcChannelPresenceService.SetUser(ID, c.Username);
|
|
RtcChannelPresenceService.JoinChannel(ID, c.ChannelId);
|
|
Console.WriteLine($"RTC join: session={ID}, user={c.Username}, channel={c.ChannelId}");
|
|
}
|
|
|
|
private void HandleRtcLeaveChannel(WsControlMessage c)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(c.ChannelId) && RtcChannelPresenceService.IsInChannel(ID, c.ChannelId))
|
|
RtcChannelPresenceService.LeaveChannel(ID);
|
|
Console.WriteLine($"RTC leave: session={ID}, user={c.Username}");
|
|
}
|
|
|
|
private void HandleTyping(WsControlMessage c)
|
|
{
|
|
var senderUsername = ConnectedClientService.GetUsernameForSession(ID);
|
|
if (string.IsNullOrWhiteSpace(senderUsername) || string.IsNullOrWhiteSpace(c.ChannelId)) return;
|
|
|
|
var json = JsonSerializer.Serialize(new SocketTypingEvent
|
|
{
|
|
Type = SignalType.TypingIndicator,
|
|
Username = senderUsername,
|
|
ChannelId = c.ChannelId
|
|
});
|
|
|
|
foreach (var member in GetServerMembersSync())
|
|
{
|
|
var rawUsername = ExtractUsernameFromUserId(member.UserId);
|
|
if (string.Equals(rawUsername, senderUsername, StringComparison.OrdinalIgnoreCase)) continue;
|
|
|
|
foreach (var sid in ConnectedClientService.GetSessionsForUser(rawUsername))
|
|
Sessions.SendTo(json, sid);
|
|
}
|
|
}
|
|
|
|
private void HandleGetEditHistory(WsControlMessage c)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(c.MessageId) || string.IsNullOrWhiteSpace(c.Username)) return;
|
|
if (!EnsureCoreReady() || ChannelCryptoService is null || string.IsNullOrWhiteSpace(ChannelDbKey)) return;
|
|
|
|
var targetClient = GetClientPublicKeyByUsernameSync(c.Username);
|
|
if (targetClient is null) return;
|
|
|
|
var edits = GetChannelMessageEditsSync(c.MessageId)
|
|
.OrderBy(e => e.EditedAt)
|
|
.ToList();
|
|
|
|
var entries = new List<SocketEditHistoryEntry>();
|
|
|
|
foreach (var edit in edits)
|
|
{
|
|
string plainText;
|
|
try { plainText = ChannelCryptoService.Decrypt(edit.CipherText, edit.Nonce, edit.Tag, ChannelDbKey); }
|
|
catch (Exception ex) { Console.WriteLine($"Edit history decrypt failed: {ex.Message}"); continue; }
|
|
|
|
var encrypted = E2EeHelper.EncryptForRecipient(plainText, targetClient.PublicKey);
|
|
entries.Add(new SocketEditHistoryEntry
|
|
{
|
|
CipherText = encrypted.CipherText,
|
|
Nonce = encrypted.Nonce,
|
|
Tag = encrypted.Tag,
|
|
EncryptedKey = encrypted.EncryptedKey,
|
|
EditedAt = edit.EditedAt
|
|
});
|
|
}
|
|
|
|
Send(JsonSerializer.Serialize(new SocketEditHistoryResponse
|
|
{
|
|
Type = SignalType.EditHistory,
|
|
MessageId = c.MessageId,
|
|
Entries = entries
|
|
}));
|
|
}
|
|
|
|
private void HandleCreateChannel(WsControlMessage c)
|
|
{
|
|
var username = ConnectedClientService.GetUsernameForSession(ID);
|
|
if (string.IsNullOrWhiteSpace(username)) return;
|
|
|
|
if (PermissionService is null || !PermissionService.CanManageChannelsAsync(username).GetAwaiter().GetResult())
|
|
{
|
|
Console.WriteLine($"CreateChannel denied for {username}: insufficient permissions.");
|
|
Send(JsonSerializer.Serialize(new WsEventMessage { Event = WsEvent.Error, Detail = "Permission denied." }));
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(c.ChannelName))
|
|
{
|
|
Send(JsonSerializer.Serialize(new WsEventMessage { Event = WsEvent.Error, Detail = "Channel name is required." }));
|
|
return;
|
|
}
|
|
|
|
var type = (ChannelType)c.ChannelType;
|
|
|
|
Task.Run(async () => await Db!.Create("channels", new Channels
|
|
{
|
|
Name = c.ChannelName,
|
|
Type = type,
|
|
Group = c.ChannelGroup ?? string.Empty,
|
|
CreatedAt = DateTime.UtcNow
|
|
})).GetAwaiter().GetResult();
|
|
|
|
Console.WriteLine($"Channel created: {c.ChannelName} ({type}) by {username}");
|
|
BroadcastChannelList();
|
|
}
|
|
|
|
private void HandleDeleteChannel(WsControlMessage c)
|
|
{
|
|
var username = ConnectedClientService.GetUsernameForSession(ID);
|
|
if (string.IsNullOrWhiteSpace(username)) return;
|
|
|
|
if (PermissionService is null || !PermissionService.CanDeleteChannelAsync(username).GetAwaiter().GetResult())
|
|
{
|
|
Send(JsonSerializer.Serialize(new WsEventMessage { Event = WsEvent.Error, Detail = "Permission denied." }));
|
|
return;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(c.ChannelId)) return;
|
|
|
|
var all = GetChannelsSync();
|
|
var target = all.FirstOrDefault(ch => GetRecordId(ch.Id) == c.ChannelId);
|
|
if (target is null) return;
|
|
|
|
target.IsDeleted = true;
|
|
Task.Run(async () => await Db!.Merge<Channels, Channels>(target))
|
|
.GetAwaiter().GetResult();
|
|
|
|
Console.WriteLine($"Channel deleted: {target.Name} by {username}");
|
|
BroadcastChannelList();
|
|
}
|
|
|
|
private void HandleEncryptedRtcSignal(string msg)
|
|
{
|
|
SocketRtcSignalMessage? payload;
|
|
try { payload = JsonSerializer.Deserialize<SocketRtcSignalMessage>(msg); }
|
|
catch { Console.WriteLine("Failed to parse RTC signal."); return; }
|
|
|
|
if (payload is null || string.IsNullOrWhiteSpace(payload.ChannelId)) return;
|
|
|
|
string plainText;
|
|
try
|
|
{
|
|
plainText = E2EeHelper.DecryptForRecipient(
|
|
new EncryptedPayload { CipherText = payload.CipherText, Nonce = payload.Nonce, Tag = payload.Tag, EncryptedKey = payload.EncryptedKey },
|
|
ServerPrivateKey);
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"RTC decrypt failed: {ex.Message}"); return; }
|
|
|
|
foreach (var sid in RtcChannelPresenceService.GetSessionsInChannel(payload.ChannelId))
|
|
{
|
|
if (sid == ID) continue;
|
|
var uname = RtcChannelPresenceService.GetUsernameForSession(sid);
|
|
if (string.IsNullOrWhiteSpace(uname)) continue;
|
|
var key = GetClientPublicKeyByUsernameSync(uname);
|
|
if (key is null) continue;
|
|
var enc = E2EeHelper.EncryptForRecipient(plainText, key.PublicKey);
|
|
Sessions.SendTo(JsonSerializer.Serialize(new SocketRtcSignalMessage
|
|
{
|
|
Type = SignalType.EncryptedSignal, SenderUsername = payload.SenderUsername,
|
|
ChannelId = payload.ChannelId, CipherText = enc.CipherText,
|
|
Nonce = enc.Nonce, Tag = enc.Tag, EncryptedKey = enc.EncryptedKey
|
|
}), sid);
|
|
}
|
|
}
|
|
|
|
private void HandleEncryptedChatMessage(string msg)
|
|
{
|
|
SocketEncryptedMessage? payload;
|
|
try { payload = JsonSerializer.Deserialize<SocketEncryptedMessage>(msg); }
|
|
catch { Console.WriteLine("Failed to parse chat payload."); return; }
|
|
|
|
if (payload is null || payload.Type != SignalType.ClientEncryptedChat) return;
|
|
if (!EnsureCoreReady() || !EnsureCryptoReady()) return;
|
|
|
|
// Permission check.
|
|
var senderUsername = ConnectedClientService.GetUsernameForSession(ID);
|
|
if (string.IsNullOrWhiteSpace(senderUsername)) return;
|
|
|
|
if (PermissionService is not null &&
|
|
!PermissionService.CanSendMessagesAsync(senderUsername, payload.ChannelId).GetAwaiter().GetResult())
|
|
{
|
|
Send(JsonSerializer.Serialize(new WsEventMessage { Event = WsEvent.Error, Detail = "You cannot send messages in this channel." }));
|
|
return;
|
|
}
|
|
|
|
string plainText;
|
|
try
|
|
{
|
|
plainText = E2EeHelper.DecryptForRecipient(
|
|
new EncryptedPayload { CipherText = payload.CipherText, Nonce = payload.Nonce, Tag = payload.Tag, EncryptedKey = payload.EncryptedKey },
|
|
ServerPrivateKey);
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"Chat decrypt failed: {ex.Message}"); return; }
|
|
|
|
Console.WriteLine($"Decrypted chat from {payload.SenderUsername}");
|
|
|
|
string messageId;
|
|
try
|
|
{
|
|
var dbEnc = ChannelCryptoService!.Encrypt(plainText, ChannelDbKey);
|
|
var saved = CreateChannelMessageSync(new ChannelMessages
|
|
{
|
|
ChannelId = payload.ChannelId,
|
|
SenderUserId = $"users:{payload.SenderUsername.ToLower()}",
|
|
CipherText = dbEnc.cipherText,
|
|
Nonce = dbEnc.nonce,
|
|
Tag = dbEnc.tag,
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
messageId = GetRecordId(saved.Id);
|
|
Console.WriteLine($"Message saved: {messageId}");
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"Save failed: {ex.Message}"); return; }
|
|
|
|
DeliverToServerMembers(plainText, payload.SenderUsername, payload.ChannelId,
|
|
messageId, SignalType.EncryptedChat, isEdited: false);
|
|
|
|
MirrorAttachmentIfNeeded(plainText, payload.SenderUsername, payload.ChannelId);
|
|
}
|
|
|
|
private void MirrorAttachmentIfNeeded(string plainText, string senderUsername, string originChannelId)
|
|
{
|
|
ChatMessageContent? content;
|
|
try { content = JsonSerializer.Deserialize<ChatMessageContent>(plainText); }
|
|
catch { return; }
|
|
|
|
if (content is null || string.IsNullOrWhiteSpace(content.AttachmentBase64))
|
|
return;
|
|
|
|
// The user wants images, zips, docs — but not gifs (and links/text aren't attachments anyway).
|
|
var mime = content.AttachmentMimeType ?? string.Empty;
|
|
if (mime.Equals("image/gif", StringComparison.OrdinalIgnoreCase))
|
|
return;
|
|
|
|
var origin = GetChannelsSync().FirstOrDefault(ch => GetRecordId(ch.Id) == originChannelId);
|
|
if (origin?.LinkedFileChannelId is null) return;
|
|
|
|
var fileChannelId = origin.LinkedFileChannelId;
|
|
|
|
if (originChannelId == fileChannelId) return;
|
|
|
|
var mirror = new ChatMessageContent
|
|
{
|
|
Text = $"📎 Shared from #{origin.Name} by {senderUsername}",
|
|
AttachmentBase64 = content.AttachmentBase64,
|
|
AttachmentMimeType = content.AttachmentMimeType,
|
|
AttachmentFileName = content.AttachmentFileName
|
|
};
|
|
|
|
var mirrorPlain = JsonSerializer.Serialize(mirror);
|
|
|
|
string mirrorId;
|
|
try
|
|
{
|
|
var dbEnc = ChannelCryptoService!.Encrypt(mirrorPlain, ChannelDbKey);
|
|
var saved = CreateChannelMessageSync(new ChannelMessages
|
|
{
|
|
ChannelId = fileChannelId,
|
|
SenderUserId = $"users:{senderUsername.ToLower()}",
|
|
CipherText = dbEnc.cipherText,
|
|
Nonce = dbEnc.nonce,
|
|
Tag = dbEnc.tag,
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
mirrorId = GetRecordId(saved.Id);
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"File mirror save failed: {ex.Message}"); return; }
|
|
|
|
DeliverToServerMembers(mirrorPlain, senderUsername, fileChannelId,
|
|
mirrorId, SignalType.EncryptedChat, isEdited: false);
|
|
|
|
Console.WriteLine($"Mirrored attachment from {originChannelId} to file channel {fileChannelId}");
|
|
}
|
|
|
|
private void HandleEditMessage(string msg)
|
|
{
|
|
SocketEncryptedMessage? request;
|
|
try { request = JsonSerializer.Deserialize<SocketEncryptedMessage>(msg); }
|
|
catch { Console.WriteLine("Failed to parse edit request."); return; }
|
|
|
|
if (request is null || string.IsNullOrWhiteSpace(request.MessageId)) return;
|
|
if (!EnsureCoreReady() || !EnsureCryptoReady()) return;
|
|
|
|
var senderUsername = ConnectedClientService.GetUsernameForSession(ID);
|
|
if (string.IsNullOrWhiteSpace(senderUsername)) return;
|
|
|
|
var existing = GetChannelMessageByIdSync(request.MessageId);
|
|
if (existing is null) { Console.WriteLine($"Edit: message {request.MessageId} not found."); return; }
|
|
|
|
if (!string.Equals(ExtractUsernameFromUserId(existing.SenderUserId), senderUsername, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
Console.WriteLine($"Edit denied: {senderUsername} does not own {request.MessageId}.");
|
|
return;
|
|
}
|
|
|
|
string newPlainText;
|
|
try
|
|
{
|
|
newPlainText = E2EeHelper.DecryptForRecipient(
|
|
new EncryptedPayload { CipherText = request.CipherText, Nonce = request.Nonce, Tag = request.Tag, EncryptedKey = request.EncryptedKey },
|
|
ServerPrivateKey);
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"Edit decrypt failed: {ex.Message}"); return; }
|
|
|
|
try
|
|
{
|
|
CreateChannelMessageEditSync(new ChannelMessageEdits
|
|
{
|
|
MessageId = request.MessageId,
|
|
CipherText = existing.CipherText,
|
|
Nonce = existing.Nonce,
|
|
Tag = existing.Tag,
|
|
EditedAt = existing.EditedAt ?? existing.CreatedAt
|
|
});
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"Edit history save failed: {ex.Message}"); }
|
|
|
|
try
|
|
{
|
|
var dbEnc = ChannelCryptoService!.Encrypt(newPlainText, ChannelDbKey);
|
|
existing.CipherText = dbEnc.cipherText;
|
|
existing.Nonce = dbEnc.nonce;
|
|
existing.Tag = dbEnc.tag;
|
|
existing.EditedAt = DateTime.UtcNow;
|
|
UpdateChannelMessageSync(existing);
|
|
Console.WriteLine($"Message {request.MessageId} edited by {senderUsername}.");
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"Edit DB update failed: {ex.Message}"); return; }
|
|
|
|
DeliverToServerMembers(newPlainText, senderUsername, request.ChannelId,
|
|
request.MessageId, SignalType.MessageEdited, isEdited: true);
|
|
}
|
|
|
|
private void HandleDeleteMessage(string msg)
|
|
{
|
|
SocketEncryptedMessage? request;
|
|
try { request = JsonSerializer.Deserialize<SocketEncryptedMessage>(msg); }
|
|
catch { Console.WriteLine("Failed to parse delete request."); return; }
|
|
|
|
if (request is null || string.IsNullOrWhiteSpace(request.MessageId)) return;
|
|
if (!EnsureCoreReady()) return;
|
|
|
|
var senderUsername = ConnectedClientService.GetUsernameForSession(ID);
|
|
if (string.IsNullOrWhiteSpace(senderUsername)) return;
|
|
|
|
var existing = GetChannelMessageByIdSync(request.MessageId);
|
|
if (existing is null) return;
|
|
|
|
bool isOwner = string.Equals(ExtractUsernameFromUserId(existing.SenderUserId), senderUsername, StringComparison.OrdinalIgnoreCase);
|
|
bool canManage = PermissionService?.CanManageMessagesAsync(senderUsername, request.ChannelId).GetAwaiter().GetResult() ?? false;
|
|
|
|
if (!isOwner && !canManage)
|
|
{
|
|
Console.WriteLine($"Delete denied: {senderUsername} does not own {request.MessageId}.");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
existing.IsDeleted = true;
|
|
UpdateChannelMessageSync(existing);
|
|
Console.WriteLine($"Message {request.MessageId} deleted by {senderUsername}.");
|
|
}
|
|
catch (Exception ex) { Console.WriteLine($"Delete DB update failed: {ex.Message}"); return; }
|
|
|
|
var deletedEvent = JsonSerializer.Serialize(new SocketMessageDeletedEvent
|
|
{
|
|
Type = SignalType.MessageDeleted, MessageId = request.MessageId, ChannelId = request.ChannelId
|
|
});
|
|
|
|
foreach (var member in GetServerMembersSync())
|
|
{
|
|
var rawUsername = ExtractUsernameFromUserId(member.UserId);
|
|
foreach (var sid in ConnectedClientService.GetSessionsForUser(rawUsername))
|
|
Sessions.SendTo(deletedEvent, sid);
|
|
}
|
|
}
|
|
|
|
private void DeliverToServerMembers(
|
|
string plainText, string senderUsername, string channelId,
|
|
string messageId, SignalType signalType, bool isEdited)
|
|
{
|
|
foreach (var member in GetServerMembersSync())
|
|
{
|
|
var rawUsername = ExtractUsernameFromUserId(member.UserId);
|
|
var sessionIds = ConnectedClientService.GetSessionsForUser(rawUsername);
|
|
if (sessionIds.Count == 0) continue;
|
|
|
|
var properUsername = sessionIds
|
|
.Select(ConnectedClientService.GetUsernameForSession)
|
|
.FirstOrDefault(u => u is not null) ?? rawUsername;
|
|
|
|
var clientKey = GetClientPublicKeyByUsernameSync(properUsername);
|
|
if (clientKey is null) { Console.WriteLine($"No public key for {properUsername}, skipping."); continue; }
|
|
|
|
var encrypted = E2EeHelper.EncryptForRecipient(plainText, clientKey.PublicKey);
|
|
var json = JsonSerializer.Serialize(new SocketEncryptedMessage
|
|
{
|
|
Type = signalType, MessageId = messageId,
|
|
SenderUsername = senderUsername, RecipientUsername = properUsername, ChannelId = channelId,
|
|
CipherText = encrypted.CipherText, Nonce = encrypted.Nonce,
|
|
Tag = encrypted.Tag, EncryptedKey = encrypted.EncryptedKey,
|
|
IsEdited = isEdited
|
|
});
|
|
|
|
foreach (var sid in sessionIds)
|
|
Sessions.SendTo(json, sid);
|
|
}
|
|
}
|
|
|
|
private void BroadcastChannelList()
|
|
{
|
|
foreach (var member in GetServerMembersSync())
|
|
{
|
|
var rawUsername = ExtractUsernameFromUserId(member.UserId);
|
|
var sessionIds = ConnectedClientService.GetSessionsForUser(rawUsername);
|
|
if (sessionIds.Count == 0) continue;
|
|
|
|
var properUsername = sessionIds
|
|
.Select(ConnectedClientService.GetUsernameForSession)
|
|
.FirstOrDefault(u => u is not null) ?? rawUsername;
|
|
|
|
var channels = BuildChannelListForUser(properUsername);
|
|
var json = JsonSerializer.Serialize(new SocketChannelList { Type = SignalType.ChannelList, Channels = channels });
|
|
|
|
foreach (var sid in sessionIds)
|
|
Sessions.SendTo(json, sid);
|
|
}
|
|
}
|
|
|
|
private List<ChannelItem> BuildChannelListForUser(string username)
|
|
{
|
|
var rawChannels = GetChannelsSync()
|
|
.Where(c => !c.IsDeleted)
|
|
.OrderBy(c => c.CreatedAt)
|
|
.ToList();
|
|
|
|
var items = new List<ChannelItem>();
|
|
|
|
foreach (var c in rawChannels)
|
|
{
|
|
var channelId = GetRecordId(c.Id);
|
|
|
|
// "Visibility" — drop channels this user is not allowed to see.
|
|
if (PermissionService is not null &&
|
|
!PermissionService.CanViewChannelAsync(username, channelId).GetAwaiter().GetResult())
|
|
continue;
|
|
|
|
bool canPost = PermissionService is null
|
|
|| PermissionService.CanSendMessagesAsync(username, channelId).GetAwaiter().GetResult();
|
|
bool canManage = PermissionService is not null &&
|
|
(PermissionService.CanDeleteChannelAsync(username).GetAwaiter().GetResult() ||
|
|
PermissionService.CanEditChannelAsync(username).GetAwaiter().GetResult());
|
|
|
|
items.Add(new ChannelItem
|
|
{
|
|
ChannelId = channelId,
|
|
Name = c.Name,
|
|
Type = c.Type,
|
|
Group = c.Group,
|
|
IsReadOnly = c.IsReadOnly,
|
|
CanPost = canPost,
|
|
CanManage = canManage,
|
|
CreatedAt = c.CreatedAt
|
|
});
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
protected override void OnClose(CloseEventArgs e)
|
|
{
|
|
ConnectedClientService.Unregister(ID);
|
|
RtcChannelPresenceService.RemoveSession(ID);
|
|
Console.WriteLine($"WS closed: session={ID}, code={e.Code}");
|
|
base.OnClose(e);
|
|
}
|
|
|
|
protected override void OnError(ErrorEventArgs e)
|
|
{
|
|
Console.WriteLine($"WS error: session={ID}, message={e.Message}");
|
|
base.OnError(e);
|
|
}
|
|
|
|
private void RegisterOrUpdateClientKeySync(string username, string publicKey) =>
|
|
Task.Run(async () => await ClientKeyService!.RegisterOrUpdateKeyAsync(username, publicKey)).GetAwaiter().GetResult();
|
|
|
|
private List<Channels> GetChannelsSync() =>
|
|
Task.Run(async () => await Db!.Select<Channels>("channels")).GetAwaiter().GetResult().ToList();
|
|
|
|
private ClientPublicKeys? GetClientPublicKeyByUsernameSync(string username) =>
|
|
Task.Run(async () => await ClientKeyService!.GetByUsernameAsync(username)).GetAwaiter().GetResult();
|
|
|
|
private List<ChannelMessages> GetChannelMessagesSync() =>
|
|
Task.Run(async () => await Db!.Select<ChannelMessages>("channel_messages")).GetAwaiter().GetResult().ToList();
|
|
|
|
private ChannelMessages? GetChannelMessageByIdSync(string messageId) =>
|
|
GetChannelMessagesSync().FirstOrDefault(m => GetRecordId(m.Id) == messageId);
|
|
|
|
private ChannelMessages CreateChannelMessageSync(ChannelMessages message) =>
|
|
Task.Run(async () => await Db!.Create("channel_messages", message)).GetAwaiter().GetResult();
|
|
|
|
private void UpdateChannelMessageSync(ChannelMessages message) =>
|
|
Task.Run(async () => await Db!.Merge<ChannelMessages, ChannelMessages>(message)).GetAwaiter().GetResult();
|
|
|
|
private void CreateChannelMessageEditSync(ChannelMessageEdits edit) =>
|
|
Task.Run(async () => await Db!.Create("channel_message_edits", edit)).GetAwaiter().GetResult();
|
|
|
|
private List<ChannelMessageEdits> GetChannelMessageEditsSync(string messageId)
|
|
{
|
|
var all = Task.Run(async () => await Db!.Select<ChannelMessageEdits>("channel_message_edits"))
|
|
.GetAwaiter().GetResult().ToList();
|
|
return all.Where(e => e.MessageId == messageId).ToList();
|
|
}
|
|
|
|
private List<ServerMembers> GetServerMembersSync() =>
|
|
Task.Run(async () => await Db!.Select<ServerMembers>("server_members")).GetAwaiter().GetResult().ToList();
|
|
|
|
private static string ExtractUsernameFromUserId(string senderUserId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(senderUserId)) return "Unknown";
|
|
var parts = senderUserId.Split(':', 2);
|
|
return parts.Length == 2 ? parts[1] : senderUserId;
|
|
}
|
|
|
|
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;
|
|
return $"{root.GetProperty("Table").GetString()}:{root.GetProperty("Id").GetString()}";
|
|
}
|
|
|
|
private bool EnsureCoreReady()
|
|
{
|
|
if (ClientKeyService is null || Db is null) { Console.WriteLine("Core services null."); return false; }
|
|
return true;
|
|
}
|
|
|
|
private bool EnsureCryptoReady()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ServerPrivateKey) || string.IsNullOrWhiteSpace(ChannelDbKey) || ChannelCryptoService is null)
|
|
{
|
|
Console.WriteLine("Crypto keys null.");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|