Summary Update.
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
namespace RelayShared.Services;
|
||||
namespace RelayShared.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Drives both rendering (sidebar icon, message view vs RTC view) and server-side routing
|
||||
/// (file mirror destination must be ChannelType.File, RTC join only on Voice/Stage).
|
||||
/// </summary>
|
||||
public enum ChannelType
|
||||
{
|
||||
Text, //Default channel type, handles text, links, files*, all in a linear live chat format
|
||||
Voice, //Used for general voice and video calls, utilizes WebRTC in its intended use
|
||||
File, //File browser for connected text channels, used for browsing files rather than scrolling through text channel
|
||||
Forum, //Specific forum posts, meant to keep conversations grouped and on topic while keeping all in an easy to find place
|
||||
Stage //Used for announcements and presentations, voice/video call utilizing a modified WebRTC protocol through server
|
||||
}
|
||||
/// <summary>Default. Linear chat: text, markdown, embeds, attachments. Sidebar prefix "#".</summary>
|
||||
Text,
|
||||
|
||||
/// <summary>WebRTC voice/video. Sidebar prefix 🔊. Selecting auto-swaps to the RTC view.</summary>
|
||||
Voice,
|
||||
|
||||
/// <summary>File browser. Receives auto-mirrored attachments from any Text channel that points here via LinkedFileChannelId. Sidebar prefix 📁.</summary>
|
||||
File,
|
||||
|
||||
/// <summary>Forum-style threaded posts. Sidebar prefix 📋. Currently a placeholder type.</summary>
|
||||
Forum,
|
||||
|
||||
/// <summary>Announcement-style voice. Modified WebRTC where most participants are listeners. Sidebar prefix 🎤. Placeholder.</summary>
|
||||
Stage
|
||||
}
|
||||
|
||||
@@ -1,19 +1,44 @@
|
||||
namespace RelayShared.Services;
|
||||
|
||||
/// <summary>
|
||||
/// One row in the sidebar channel list. The server computes the permission-derived fields
|
||||
/// (CanPost, CanManage) per-user so the client never has to evaluate permissions itself.
|
||||
/// </summary>
|
||||
public sealed class ChannelItem
|
||||
{
|
||||
/// <summary>Surreal record id (e.g. "channels:abc").</summary>
|
||||
public string ChannelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Sidebar display name ("general", "welcome", etc.).</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Drives icon and behavior: Text/Voice/File/Forum/Stage.</summary>
|
||||
public ChannelType Type { get; set; }
|
||||
|
||||
/// <summary>Sidebar category label (e.g. "General"). Empty groups fall under a default "Channels" header.</summary>
|
||||
public string Group { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Creation timestamp. Drives sidebar sort order (oldest → newest).</summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>True if the channel is announcement-style (welcome, files). Drives the 🔒 suffix in the sidebar.</summary>
|
||||
public bool IsReadOnly { get; set; }
|
||||
|
||||
/// <summary>Permission-resolved: can the receiving user send messages here. Drives input enable/disable.</summary>
|
||||
public bool CanPost { get; set; }
|
||||
|
||||
/// <summary>Permission-resolved: can the receiving user edit/delete this channel. Drives context-menu visibility.</summary>
|
||||
public bool CanManage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-to-client channel list. Sent in response to WsAction.GetChannels and broadcast
|
||||
/// to all sessions after every channel create / delete.
|
||||
/// </summary>
|
||||
public sealed class SocketChannelList
|
||||
{
|
||||
public SignalType Type { get; set; } = SignalType.ChannelList;
|
||||
|
||||
/// <summary>Channels the receiving user is allowed to view. Permission filtering happens server-side.</summary>
|
||||
public List<ChannelItem> Channels { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -1,14 +1,43 @@
|
||||
namespace RelayShared.Services;
|
||||
|
||||
/// <summary>
|
||||
/// The plaintext payload of a chat message before E2E encryption is applied.
|
||||
///
|
||||
/// Lifecycle of a message:
|
||||
/// 1. Client builds a ChatMessageContent (text + optional reply/attachment/mentions).
|
||||
/// 2. Client JSON-serialises it, encrypts with the server's public key (RSA wrapping an
|
||||
/// AES-GCM key), and sends the encrypted blob wrapped in a SocketEncryptedMessage.
|
||||
/// 3. Server decrypts with its private key, re-encrypts with the channel DB key, stores it.
|
||||
/// 4. For each recipient, server decrypts from DB key and re-encrypts with that recipient's
|
||||
/// public key, then delivers via SocketEncryptedMessage.
|
||||
/// 5. Recipient decrypts with their private key and JSON-deserialises back to ChatMessageContent.
|
||||
///
|
||||
/// This type is intentionally shared by RelayClient and RelayServer so both ends agree on the
|
||||
/// JSON shape. Adding a field here lights up the whole pipeline automatically.
|
||||
/// </summary>
|
||||
public sealed class ChatMessageContent
|
||||
{
|
||||
/// <summary>The raw message body, including Markdown syntax and @mentions.</summary>
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>When set, this message is a reply. Carries the Surreal record id of the message being replied to.</summary>
|
||||
public string? ReplyToId { get; set; }
|
||||
|
||||
/// <summary>Display name of the user being replied to. Lets the client render the quote bar without a lookup.</summary>
|
||||
public string? ReplyToSenderUsername { get; set; }
|
||||
|
||||
/// <summary>Trimmed preview of the replied-to text (≤100 chars). Captured at send time so the server never has to look it up.</summary>
|
||||
public string? ReplyPreview { get; set; }
|
||||
|
||||
/// <summary>Extracted usernames + special tokens ("everyone", "here"). Drives the ping-badge in the sidebar.</summary>
|
||||
public List<string>? Mentions { get; set; }
|
||||
|
||||
/// <summary>Base64-encoded attachment bytes. Null when there's no attachment.</summary>
|
||||
public string? AttachmentBase64 { get; set; }
|
||||
|
||||
/// <summary>MIME type of the attachment (e.g. "image/png"). Used to choose between BuildBase64ImageEmbed and BuildFileCard.</summary>
|
||||
public string? AttachmentMimeType { get; set; }
|
||||
|
||||
/// <summary>Original filename as chosen by the sender. Shown as the file card label and used for the download path.</summary>
|
||||
public string? AttachmentFileName { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,71 +2,159 @@ namespace RelayShared.Services;
|
||||
|
||||
//TODO: review name of file, potentially rename for Encryption services rather than sockets
|
||||
|
||||
/// <summary>
|
||||
/// The "data plane" wire types for the WebSocket protocol.
|
||||
///
|
||||
/// Every type here carries a SignalType discriminator so a generic JsonDocument peek
|
||||
/// can identify the variant. The server dispatches on SignalType in ChatSocketBehavior.OnMessage;
|
||||
/// the client dispatches on it in RelaySocketClient.OnMessage.
|
||||
///
|
||||
/// Encrypted payloads share a uniform 4-tuple shape: (CipherText, Nonce, Tag, EncryptedKey).
|
||||
/// That tuple is hybrid RSA+AES-GCM: EncryptedKey is the per-message AES key wrapped with the
|
||||
/// recipient's RSA public key; CipherText/Nonce/Tag are the AES-GCM ciphertext, nonce, and
|
||||
/// authentication tag for the actual JSON-serialised ChatMessageContent.
|
||||
/// </summary>
|
||||
public sealed class SocketRtcSignalMessage
|
||||
{
|
||||
/// <summary>Always SignalType.EncryptedSignal in flight.</summary>
|
||||
public SignalType Type { get; set; }
|
||||
|
||||
/// <summary>Username of the user generating the SDP/ICE signal.</summary>
|
||||
public string SenderUsername { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The voice channel this signal belongs to.</summary>
|
||||
public string ChannelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 AES-GCM ciphertext of the JSON-serialised RtcSignalMessage.</summary>
|
||||
public string CipherText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 AES-GCM 96-bit nonce.</summary>
|
||||
public string Nonce { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 AES-GCM 128-bit authentication tag.</summary>
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 RSA-OAEP-encrypted AES key (encrypted with recipient's public key).</summary>
|
||||
public string EncryptedKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The workhorse envelope for chat messages and message lifecycle events.
|
||||
/// Used for both directions and for new sends / edits / delete tombstones.
|
||||
/// </summary>
|
||||
public sealed class SocketEncryptedMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// EncryptedChat (server→client), ClientEncryptedChat (client→server new message),
|
||||
/// ClientEditMessage / ClientDeleteMessage (client→server lifecycle), MessageEdited (server→client).
|
||||
/// </summary>
|
||||
public SignalType Type { get; set; } = SignalType.EncryptedChat;
|
||||
|
||||
/// <summary>Surreal record id (e.g. "channel_messages:abc"). Populated by the server on outbound delivery.</summary>
|
||||
public string MessageId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Who wrote the message.</summary>
|
||||
public string SenderUsername { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Who this specific delivery is encrypted for. Different per recipient on the same logical message.</summary>
|
||||
public string RecipientUsername { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>The channel the message belongs to.</summary>
|
||||
public string ChannelId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 AES-GCM ciphertext of the JSON-serialised ChatMessageContent. Empty on tombstone deliveries.</summary>
|
||||
public string CipherText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 AES-GCM 96-bit nonce.</summary>
|
||||
public string Nonce { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 AES-GCM 128-bit authentication tag.</summary>
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 RSA-OAEP-encrypted AES key (encrypted with recipient's public key on outbound, server's on inbound).</summary>
|
||||
public string EncryptedKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>True when this message has been edited at least once. Drives the (edited) footer in the bubble.</summary>
|
||||
public bool IsEdited { get; set; }
|
||||
|
||||
/// <summary>True for tombstone deliveries (history only). Client renders a placeholder; no decryption is attempted.</summary>
|
||||
public bool IsDeleted { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-broadcast tombstone fired the moment a message is deleted. Carries no content —
|
||||
/// recipients use MessageId to find the existing bubble and swap it to a "deleted" placeholder.
|
||||
/// </summary>
|
||||
public sealed class SocketMessageDeletedEvent
|
||||
{
|
||||
public SignalType Type { get; set; } = SignalType.MessageDeleted;
|
||||
|
||||
/// <summary>The message being tombstoned.</summary>
|
||||
public string MessageId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Channel scope — clients that aren't viewing this channel can defer the bubble update.</summary>
|
||||
public string ChannelId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "{Username} is typing…" hint. Server forwards to every connected member except the sender.
|
||||
/// Client auto-clears the indicator 3 seconds after the last such event.
|
||||
/// </summary>
|
||||
public sealed class SocketTypingEvent
|
||||
{
|
||||
public SignalType Type { get; set; } = SignalType.TypingIndicator;
|
||||
|
||||
/// <summary>Who is typing.</summary>
|
||||
public string Username { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Which channel they're typing in. Clients ignore events for channels they're not viewing.</summary>
|
||||
public string ChannelId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>One historical version of an edited message, re-encrypted for the requester.</summary>
|
||||
public sealed class SocketEditHistoryEntry
|
||||
{
|
||||
/// <summary>Base64 AES-GCM ciphertext of the JSON-serialised previous ChatMessageContent.</summary>
|
||||
public string CipherText { get; set; } = string.Empty;
|
||||
|
||||
public string Nonce { get; set; } = string.Empty;
|
||||
public string Tag { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Base64 RSA-OAEP-encrypted AES key (encrypted with requester's public key).</summary>
|
||||
public string EncryptedKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>When this version was the current text (i.e. when it was replaced).</summary>
|
||||
public DateTime EditedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Server reply to a GetEditHistory request. Entries are ordered oldest→newest.</summary>
|
||||
public sealed class SocketEditHistoryResponse
|
||||
{
|
||||
public SignalType Type { get; set; } = SignalType.EditHistory;
|
||||
|
||||
/// <summary>Which message this history is for.</summary>
|
||||
public string MessageId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Every previous version of the message. Empty if the message has never been edited.</summary>
|
||||
public List<SocketEditHistoryEntry> Entries { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-to-client delivery of the server's public RSA key. Sent once per session in
|
||||
/// response to WsAction.GetServerKey. Clients cache this for all outbound encryption.
|
||||
/// </summary>
|
||||
public sealed class ServerPublicKeyMessage
|
||||
{
|
||||
public SignalType Type { get; set; } = SignalType.ServerPublicKey;
|
||||
|
||||
/// <summary>Base64 SubjectPublicKeyInfo (DER) of the server's RSA public key.</summary>
|
||||
public string PublicKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>The wire discriminator for every data-plane Socket*Message.</summary>
|
||||
public enum SignalType
|
||||
{
|
||||
// RTC SDP/ICE wire types (used by the WebView RTC engine, not handled directly here)
|
||||
Offer,
|
||||
Answer,
|
||||
Candidate,
|
||||
@@ -74,15 +162,37 @@ public enum SignalType
|
||||
AnswerUpdated,
|
||||
CandidateAdded,
|
||||
CallLeft,
|
||||
|
||||
/// <summary>Server→client: paginated channel list (SocketChannelList).</summary>
|
||||
ChannelList,
|
||||
|
||||
/// <summary>Server→client: ServerPublicKeyMessage delivery.</summary>
|
||||
ServerPublicKey,
|
||||
|
||||
/// <summary>Bidirectional: encrypted RTC SDP/ICE signal (SocketRtcSignalMessage).</summary>
|
||||
EncryptedSignal,
|
||||
|
||||
/// <summary>Server→client: delivered chat message (SocketEncryptedMessage).</summary>
|
||||
EncryptedChat,
|
||||
|
||||
/// <summary>Client→server: new chat message send (SocketEncryptedMessage).</summary>
|
||||
ClientEncryptedChat,
|
||||
|
||||
/// <summary>Client→server: request to edit own message (SocketEncryptedMessage with new content).</summary>
|
||||
ClientEditMessage,
|
||||
|
||||
/// <summary>Client→server: request to delete own message (SocketEncryptedMessage with only MessageId).</summary>
|
||||
ClientDeleteMessage,
|
||||
|
||||
/// <summary>Server→clients: edit broadcast carrying re-encrypted new content (SocketEncryptedMessage).</summary>
|
||||
MessageEdited,
|
||||
|
||||
/// <summary>Server→clients: deletion tombstone (SocketMessageDeletedEvent).</summary>
|
||||
MessageDeleted,
|
||||
|
||||
/// <summary>Server→peers: typing indicator (SocketTypingEvent).</summary>
|
||||
TypingIndicator,
|
||||
|
||||
/// <summary>Server→requester: edit-history response (SocketEditHistoryResponse).</summary>
|
||||
EditHistory
|
||||
}
|
||||
|
||||
@@ -1,42 +1,111 @@
|
||||
namespace RelayShared.Services;
|
||||
|
||||
/// <summary>
|
||||
/// JSON-dispatch contract for the WebSocket "control plane" (non-encrypted,
|
||||
/// non-realtime requests like auth, key registration, channel CRUD, history fetches).
|
||||
///
|
||||
/// The server's ChatSocketBehavior.OnMessage looks at the first JSON property of every
|
||||
/// incoming text frame:
|
||||
/// - "Action" present → deserialise into WsControlMessage and dispatch on WsAction.
|
||||
/// - "Type" present → deserialise into SocketEncryptedMessage/SocketRtcSignalMessage
|
||||
/// and dispatch on SignalType (the "data plane" — chat messages,
|
||||
/// RTC signals, edit/delete requests).
|
||||
///
|
||||
/// Responses come back as either WsEventMessage (for acks/errors) or one of the
|
||||
/// Socket*Message types (for streaming data).
|
||||
/// </summary>
|
||||
public enum WsAction
|
||||
{
|
||||
/// <summary>Verify a Core-issued user token. Fields used: Username, Token.</summary>
|
||||
Authenticate,
|
||||
|
||||
/// <summary>Register/update the client's RSA public key. Fields used: Username, PublicKey.</summary>
|
||||
RegisterKey,
|
||||
|
||||
/// <summary>Request the server's public RSA key for outbound encryption. No fields.</summary>
|
||||
GetServerKey,
|
||||
|
||||
/// <summary>Request the full channel list for this user. No fields.</summary>
|
||||
GetChannels,
|
||||
|
||||
/// <summary>Request decrypted message history for a channel. Fields used: Username, ChannelId.</summary>
|
||||
GetHistory,
|
||||
|
||||
/// <summary>Join a voice channel (presence tracking). Fields used: Username, ChannelId.</summary>
|
||||
RtcJoin,
|
||||
|
||||
/// <summary>Leave a voice channel. Fields used: Username, ChannelId.</summary>
|
||||
RtcLeave,
|
||||
|
||||
/// <summary>Broadcast "user is typing" to channel peers. Fields used: ChannelId.</summary>
|
||||
SendTyping,
|
||||
|
||||
/// <summary>Request the edit-history chain for a specific message. Fields used: Username, MessageId, ChannelId.</summary>
|
||||
GetEditHistory,
|
||||
|
||||
/// <summary>Create a new channel (permission-gated). Fields used: ChannelName, ChannelType, ChannelGroup.</summary>
|
||||
CreateChannel,
|
||||
|
||||
/// <summary>Soft-delete a channel (permission-gated). Fields used: ChannelId.</summary>
|
||||
DeleteChannel
|
||||
}
|
||||
|
||||
/// <summary>Server-to-client event types for acks and errors.</summary>
|
||||
public enum WsEvent
|
||||
{
|
||||
/// <summary>Reply to Authenticate. Detail = username.</summary>
|
||||
Authenticated,
|
||||
|
||||
/// <summary>Reply to RegisterKey. Detail = username.</summary>
|
||||
KeyRegistered,
|
||||
|
||||
/// <summary>Generic error. Detail = human-readable reason shown to the user.</summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Control-plane envelope. All fields are nullable because each action only uses a subset
|
||||
/// of them. Serialised as JSON; identified by the presence of the "Action" property.
|
||||
/// </summary>
|
||||
public sealed class WsControlMessage
|
||||
{
|
||||
/// <summary>The action to perform. Server dispatches on this.</summary>
|
||||
public WsAction Action { get; set; }
|
||||
|
||||
/// <summary>Mixed-case username as the user typed it on sign-in. Server preserves casing for display.</summary>
|
||||
public string? Username { get; set; }
|
||||
|
||||
/// <summary>Core-issued auth token. Only set on Authenticate.</summary>
|
||||
public string? Token { get; set; }
|
||||
|
||||
/// <summary>Base64-encoded RSA public key. Only set on RegisterKey.</summary>
|
||||
public string? PublicKey { get; set; }
|
||||
|
||||
/// <summary>Surreal record id of a channel (e.g. "channels:xyz"). Used by most channel-scoped actions.</summary>
|
||||
public string? ChannelId { get; set; }
|
||||
|
||||
/// <summary>Surreal record id of a message. Used by GetEditHistory.</summary>
|
||||
public string? MessageId { get; set; }
|
||||
|
||||
/// <summary>Channel name on create (e.g. "memes"). Server normalises to lowercase-dashes.</summary>
|
||||
public string? ChannelName { get; set; }
|
||||
public int ChannelType { get; set; } // cast to ChannelType enum
|
||||
|
||||
/// <summary>Integer cast of ChannelType enum (Text=0, Voice=1, …). Used on CreateChannel.</summary>
|
||||
public int ChannelType { get; set; }
|
||||
|
||||
/// <summary>Group/category label shown in the sidebar (e.g. "General"). Optional on CreateChannel.</summary>
|
||||
public string? ChannelGroup { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-to-client ack envelope. Identified by the "Event" JSON property
|
||||
/// (vs WsControlMessage's "Action" or Socket*Message's "Type").
|
||||
/// </summary>
|
||||
public sealed class WsEventMessage
|
||||
{
|
||||
/// <summary>Which event this is acknowledging.</summary>
|
||||
public WsEvent Event { get; set; }
|
||||
|
||||
/// <summary>Human-readable context (username on success, error message on Error).</summary>
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user