mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-05 23:34:09 +02:00
deploy: 0105f798e0
This commit is contained in:
@@ -8,12 +8,23 @@ public static class HubConstants
|
||||
```
|
||||
|
||||
|
||||
HubConstants is a static container for global constants used by the chat hub to configure limits, paths, and feature boundaries. It provides values such as the hub path, default channel, and various size and constraint limits, ensuring consistent behavior across components and avoiding scattered magic numbers.
|
||||
HubConstants acts as the single source of truth for the chat hub’s configurable limits and defaults. It groups static, compile-time constants that govern where the hub is exposed, how sessions are identified (including the IRC gateway prefix), and the upper bounds for messages, attachments, avatars, and embeds, providing a centralized reference that other components consult for validation and formatting.
|
||||
|
||||
## Remarks
|
||||
HubConstants centralizes cross-cutting, tunable values so changes propagate consistently across messaging validation, content embedding, and endpoint configuration. Because these are compile-time constants, they are not sourced from runtime configuration; if you need different behavior per deployment, introduce a separate configuration mechanism rather than altering these constants at runtime.
|
||||
HubConstants isolates cross-cutting numerical constraints from business logic, ensuring all parts of the EchoHub system enforce the same rules. It enables tuning by operators—e.g., increasing `MaxMessageLength` or `MaxAttachmentsPerMessage`—without altering core workflows, while the IRC connection-id prefix helps the presence tracker distinguish IRC-based clients from native ones. The constants also centralize embed sizing and fetch behavior to maintain predictable link previews and resource usage across gateways and clients.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Validate message length against hub-wide limit
|
||||
if (message.Text.Length > HubConstants.MaxMessageLength)
|
||||
{
|
||||
// handle too long
|
||||
}
|
||||
|
||||
// Build the path for the chat hub
|
||||
var hubPath = HubConstants.ChatHubPath;
|
||||
```
|
||||
|
||||
## Notes
|
||||
- The distinction between MaxMessageNewlines (30) and MaxConsecutiveNewlines (1) matters: the first limits overall newline usage, the second limits consecutive newline runs.
|
||||
- Size limits are per-file (e.g., MaxImageSizeBytes, MaxAudioFileSizeBytes, MaxFileSizeBytes) and guide validation and storage decisions; never assume a single cap covers all attachment types.
|
||||
- IrcConnectionIdPrefix is used by the presence tracker to distinguish IRC gateway connections from native SignalR clients; ensure prefix checks rather than simple contains checks to avoid misclassification.
|
||||
- They are compile-time constants (const) and thus require a recompilation to change; runtime configuration is not supported.
|
||||
- Changes to these values reflect architectural expectations across components (UI, gateway, presence tracker, and embeds) and should be coordinated to avoid breaking client assumptions.
|
||||
@@ -8,23 +8,18 @@ public static class MessageConventions
|
||||
```
|
||||
|
||||
|
||||
Cross-protocol message conventions are centralized in this static helper. It provides formatting and parsing for IRC CTCP ACTION-style messages, so /me-like actions render consistently across clients. Action messages are stored as the CTCP framing: 0x01 + "ACTION " + text + 0x01; MessageConventions.FormatAction(text) wraps a plain text string in that payload, and TryParseAction(content, out actionText) extracts the inner text when the content matches the framing. In end-to-end encrypted rooms the action marker travels with the text, preserving semantics.
|
||||
Cross-protocol message conventions for action messages. Action messages (the /me style) are stored using the IRC CTCP ACTION wire format: a 0x01 prefix, the literal string `ACTION `, the text, and a trailing 0x01 suffix. This class exposes the constants `ActionPrefix` and `ActionSuffix`, plus helpers `FormatAction` and `TryParseAction` to wrap and unwrap the action text, ensuring consistent storage, rendering, and encryption behavior.
|
||||
|
||||
## Remarks
|
||||
- This abstraction prevents scattering the CTCP ACTION framing constants across the codebase and offers a single source of truth for how action messages are stored and read.
|
||||
- It isolates the low-level framing from higher-level message handling, making testing and future changes safer and easier.
|
||||
- The parsing path uses ordinal string comparisons and explicitly requires both the proper prefix and suffix, plus non-empty inner text, to succeed.
|
||||
ActionConventions centralize the wire-format markers so changes in one place don't ripple through callers, and to provide a clear boundary between encoding and decoding of action messages. `FormatAction` encapsulates the exact wrapper, while `TryParseAction` validates the pattern and extracts the inner text without exposing the wire markers to callers. This avoids scattering the CTCP formatting details throughout the codebase and keeps rendering logic aligned with storage format.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var action = MessageConventions.FormatAction("waves");
|
||||
if (MessageConventions.TryParseAction(action, out var text))
|
||||
{
|
||||
// text == "waves"
|
||||
}
|
||||
string content = MessageConventions.FormatAction("waves");
|
||||
bool ok = MessageConventions.TryParseAction(content, out var actionText);
|
||||
// ok == true, actionText == "waves"
|
||||
```
|
||||
|
||||
## Notes
|
||||
- TryParseAction(content, out actionText) returns true only if the content starts with ActionPrefix, ends with ActionSuffix, and the extracted inner text has length > 0; otherwise actionText is null and the method returns false.
|
||||
- The behavior relies on ordinal comparisons to avoid culture-related differences in prefix/suffix checks.
|
||||
- The inner action text can contain arbitrary characters; the method only enforces the framing and non-emptiness of the payload.
|
||||
- `TryParseAction` requires the content to start with `ActionPrefix`, end with `ActionSuffix`, and have non-empty inner text; otherwise it returns false and sets `actionText` to null.
|
||||
- The implementation uses ordinal comparisons to check the markers for performance and culture-invariant behavior.
|
||||
@@ -8,13 +8,11 @@ public static partial class ValidationConstants
|
||||
```
|
||||
|
||||
|
||||
ValidationConstants is a centralized, static container for validation constraints used throughout the EchoHub.Core domain. It defines reusable patterns for usernames, channel names, and hex color codes, as well as a set of length limits governing passwords, display names, bios, statuses, channel topics, and chat history. The included GeneratedRegex methods expose precompiled Regex instances derived from those patterns, enabling fast, consistent validation without incurring per-call regex compilation.
|
||||
ValidationConstants is a centralized repository of validation rules used across the codebase. It defines the canonical pattern strings for usernames, channel names, and hex colors, together with numeric bounds for various user-facing fields. Specifically, it exposes the strings `UsernamePattern`, `ChannelNamePattern`, `HexColorPattern`, and several limit constants such as `MaxPasswordLength`, `MinChannelPasswordLength`, `MaxDisplayNameLength`, `MaxBioLength`, `MaxStatusMessageLength`, `MaxChannelTopicLength`, and `MaxHistoryCount`. In addition, it provides precompiled Regex accessors via the `GeneratedRegex`-decorated methods `UsernameRegex()`, `ChannelNameRegex()`, and `HexColorRegex()`, enabling fast, centralized validation without scattering literal patterns across call sites.
|
||||
|
||||
## Remarks
|
||||
ValidationConstants provides a single source of truth for input validation. By offloading regex compilation to source generation, it avoids runtime overhead while keeping the validation rules easily discoverable and consistent across the codebase.
|
||||
|
||||
The class is static and partial, so callers simply reference ValidationConstants.UsernameRegex(), ValidationConstants.ChannelNameRegex(), and ValidationConstants.HexColorRegex() to obtain ready-to-use Regex instances.
|
||||
By centralizing these constraints, `ValidationConstants` minimizes drift in validation rules across features (sign-up, profile updates, channel creation, etc.) and makes it easy to update rules in one place. The `UsernameRegex()`, `ChannelNameRegex()`, and `HexColorRegex()` methods are generated at compile time by the `GeneratedRegex` attribute, which yields ready-to-use, presumably cached `Regex` instances, reducing runtime regex compilation overhead at validation points.
|
||||
|
||||
## Notes
|
||||
- GeneratedRegex provides compile-time-compiled Regex instances, which improves performance by avoiding repeated regex compilation at runtime.
|
||||
- Updating any constraint here propagates the change to all validation sites, ensuring consistency; do not duplicate rules elsewhere.
|
||||
- GeneratedRegex-based accessors rely on C# source generation; ensure your project enables source generators and targets a compatible framework, otherwise these methods may not be produced.
|
||||
- The constants define the canonical validation boundaries pharmacologically used by the system; changing them updates all consumers that reference these values.
|
||||
|
||||
@@ -7,16 +7,24 @@
|
||||
```mermaid
|
||||
%%{init: {'theme':'base','themeVariables':{'background':'#faf7ef','primaryColor':'#f0e2c2','primaryTextColor':'#1f2840','primaryBorderColor':'#8a7548','secondaryColor':'#d9efec','secondaryBorderColor':'#1d8a80','secondaryTextColor':'#1f2840','tertiaryColor':'#f2ebd8','tertiaryBorderColor':'#8a7548','tertiaryTextColor':'#1f2840','lineColor':'#1d8a80','titleColor':'#1f2840','fontSize':'14px','edgeLabelBackground':'#faf7ef','clusterBkg':'#f2ebd8','clusterBorder':'#8a7548','actorBkg':'#f0e2c2','actorBorder':'#8a7548','actorTextColor':'#1f2840','actorLineColor':'#8a7548','signalColor':'#1d8a80','signalTextColor':'#1f2840','activationBkgColor':'#d9efec','activationBorderColor':'#1d8a80','noteBkgColor':'#f2ebd8','noteBorderColor':'#8a7548','noteTextColor':'#1f2840','labelBoxBkgColor':'#f0e2c2','labelBoxBorderColor':'#8a7548','labelTextColor':'#1f2840','transitionColor':'#1d8a80','transitionLabelColor':'#1f2840','stateLabelColor':'#1f2840','altBackground':'#f2ebd8'}}}%%
|
||||
flowchart TB
|
||||
IChannelService["IChannelService: entry"]
|
||||
IChannelService -->|"GetChannelsAsync"| PaginatedResponse["PaginatedResponse<ChannelDto>"]
|
||||
PaginatedResponse -->|"items"| ChannelDto["ChannelDto"]
|
||||
IChannelService -->|"GetChannelByNameAsync"| ChannelDto
|
||||
IChannelService -->|"CreateChannelAsync / UpdateTopicAsync / SetChannelPasswordAsync / RekeyChannelAsync / DeleteChannelAsync"| ChannelOperationResult["ChannelOperationResult"]
|
||||
IChannelService -->|"GetChannelListAsync"| ChannelListItem["List<ChannelListItem>"]
|
||||
IChannelService -->|"GetChannelMetaAsync"| ChannelMetaDto["ChannelMetaDto"]
|
||||
IChannelService -->|"GetChannelCryptoAsync / GetChannelKeyEnvelopeAsync"| ChannelCryptoDto["ChannelCryptoDto"]
|
||||
IChannelService -->|"EnsureSystemChannelAsync"| Channel["Ensure or create server-managed Channel"]
|
||||
Channel -->|"returns"| ChannelDto
|
||||
IChannelService["IChannelService: entry"] -->|"GetChannelsAsync(userId, offset, limit)"| PaginatedResponse["Build PaginatedResponse of ChannelDto"]
|
||||
PaginatedResponse -->|"items: ChannelDto"| ChannelDto["Map DB rows to ChannelDto"]
|
||||
IChannelService -->|"CreateChannelAsync(creatorUserId, name, topic, isPublic, password?, encryptionSalt?, wrappedRoomKey?)"| Channel["Create Channel record"]
|
||||
Channel -->|"return"| ChannelOperationResult["ChannelOperationResult (success/error)"]
|
||||
IChannelService -->|"UpdateTopicAsync(callerUserId, channelName, topic?)"| ChannelOperationResult
|
||||
IChannelService -->|"SetChannelPasswordAsync(callerUserId, channelName, password?)"| ChannelOperationResult
|
||||
IChannelService -->|"RekeyChannelAsync(callerUserId, channelName, oldPassword, newPassword, newEncryptionSalt, newWrappedRoomKey)"| ChannelCryptoDto["Update encryptionSalt and wrappedRoomKey"]
|
||||
ChannelCryptoDto -->|"return"| ChannelOperationResult
|
||||
IChannelService -->|"DeleteChannelAsync(callerUserId, channelName)"| ChannelOperationResult
|
||||
IChannelService -->|"GetChannelByNameAsync(channelName)"| ChannelDto
|
||||
IChannelService -->|"GetChannelMetaAsync(channelName)"| ChannelMetaDto
|
||||
IChannelService -->|"GetChannelCryptoAsync(channelName)"| ChannelCryptoDto
|
||||
IChannelService -->|"GetChannelKeyEnvelopeAsync(channelName) -> (EncryptionSalt, WrappedRoomKey)"| ChannelCryptoDto
|
||||
IChannelService -->|"GetChannelTopicAsync(channelName) -> (Topic, Exists)"| ChannelMetaDto
|
||||
IChannelService -->|"GetChannelListAsync()"| ChannelListItem["Return list of ChannelListItem"]
|
||||
IChannelService -->|"EnsureChannelMembershipAsync(userId, channelName, password?) -> (Success, Error, PasswordRequired)"| ChannelOperationResult
|
||||
IChannelService -->|"EnsureSystemChannelAsync(channelName, topic?)"| Channel["Create or reclaim system Channel"]
|
||||
Channel -->|"return ChannelDto"| ChannelDto
|
||||
```
|
||||
|
||||
## Contents
|
||||
@@ -35,45 +43,32 @@ public interface IChannelService
|
||||
```
|
||||
|
||||
|
||||
Provides an asynchronous API for creating, updating, deleting and querying chat channels, managing membership, and exposing channel encryption metadata. Implement this interface to centralize channel lifecycle, access control and crypto-envelope access rather than manipulating persistence or membership directly.
|
||||
Provides the canonical server-side API for creating, querying, updating, and deleting chat channels and for enforcing membership and channel-level security. Use `IChannelService` when implementing application logic that needs to manage channel lifecycle (CRUD), inspect channel metadata or crypto information, handle membership checks (including password-protected rooms), or ensure server-owned system channels exist and cannot be hijacked by user-created channels.
|
||||
|
||||
## Remarks
|
||||
The interface groups CRUD operations, read/query methods, membership checks, and crypto-related lookups so callers can depend on a single abstraction for channel business rules. Mutating methods return ChannelOperationResult (which carries IsSuccess and factory helpers) to make success/failure handling explicit; query methods return lightweight DTOs or tuples for simple lookups. EnsureSystemChannelAsync is a server-managed path that ensures required system channels exist and prevents server content from being written into user-owned rooms.
|
||||
`IChannelService` centralizes channel-related policy and state so higher-level features (e.g. connection/auth layers, hub message routing, admin tools) can treat channel management as a single abstraction. It separates responsibilities: CRUD and topic/password operations return a [`ChannelOperationResult`](../DTOs/CommonDtos.cs.md) that callers must inspect (via `ChannelOperationResult.IsSuccess`) while read-only queries (e.g. [`GetChannelByNameAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `GetChannelMetaAsync`, `GetChannelCryptoAsync`) let callers obtain DTO representations. Crypto and key-envelope methods (`GetChannelCryptoAsync`, [`GetChannelKeyEnvelopeAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`) keep cryptographic metadata operations colocated with channel lifecycle logic. The [`EnsureSystemChannelAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) method is intentionally server-managed: it creates missing system channels and reclaims any same-named user-owned channels so server content is never stored in a user-controlled room.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Create a public channel and inspect the operation result
|
||||
var createResult = await channelService.CreateChannelAsync(creatorUserId, "general", "General discussion", true);
|
||||
if (createResult.IsSuccess)
|
||||
// create a public channel and then fetch its DTO if creation succeeded
|
||||
var result = await channelService.CreateChannelAsync(creatorUserId, "general", "General chat", isPublic: true);
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
var created = createResult; // ChannelOperationResult.Success contains the created ChannelDto
|
||||
var channel = await channelService.GetChannelByNameAsync("general");
|
||||
// use 'channel' (type: ChannelDto) for further operations
|
||||
}
|
||||
else
|
||||
{
|
||||
// handle failure
|
||||
}
|
||||
|
||||
// Ensure membership for a user (third parameter is the optional password/credential)
|
||||
var membership = await channelService.EnsureChannelMembershipAsync(userId, "general", null);
|
||||
if (membership.Success)
|
||||
{
|
||||
// user is a member or was added
|
||||
}
|
||||
else if (membership.PasswordRequired)
|
||||
{
|
||||
// prompt for password and retry
|
||||
}
|
||||
else
|
||||
{
|
||||
// membership failed; membership.Error contains a message
|
||||
// handle failure (inspect result for details provided by the implementation)
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Always check ChannelOperationResult.IsSuccess before assuming a mutating operation succeeded; use the provided factory helpers on ChannelOperationResult to construct success/failure values.
|
||||
- Methods that return encryption metadata (encryption salt, wrapped room key) expose envelopes, not raw symmetric keys; treat any secrets derived from these values securely.
|
||||
- The source contains redacted/truncated text in some method signatures (CreateChannelAsync and EnsureChannelMembershipAsync). Verify the real parameter names and optional overloads in the codebase before calling those methods.
|
||||
|
||||
- Methods that return [`ChannelOperationResult`](../DTOs/CommonDtos.cs.md) (for example `CreateChannelAsync`, [`UpdateTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), [`SetChannelPasswordAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`, `DeleteChannelAsync`) must have their `ChannelOperationResult.IsSuccess` checked before assuming the operation succeeded. Do not assume a returned DTO exists unless the operation reports success.
|
||||
- Several parameters are nullable (`topic`, `password`, `encryptionSalt`, `wrappedRoomKey`); callers should explicitly pass `null` when no value is intended and be prepared for implementations to treat `null` as "no value" or as an instruction to remove/clear a setting (verify service semantics for your deployment).
|
||||
- [`GetChannelTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) returns `(string? Topic, bool Exists)` — a `null` `Topic` can mean either an empty topic or that no topic was set; check `Exists` to distinguish a non-existent channel from a channel with a `null` topic.
|
||||
- [`EnsureChannelMembershipAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) returns a tuple including `PasswordRequired`; if `PasswordRequired` is `true`, callers should prompt for and supply a password on subsequent calls. The `Error` element may contain implementation-specific failure information.
|
||||
- `GetChannelsAsync` accepts `offset` and `limit` for pagination; callers are responsible for passing sensible bounds and handling potentially large result sets incrementally.
|
||||
|
||||
---
|
||||
|
||||
@@ -96,26 +91,18 @@ public record ChannelListItem(string Name, string? Topic, int OnlineCount, bool
|
||||
| `IsProtected` | `bool` | `false` |
|
||||
|
||||
|
||||
ChannelListItem is an immutable value that represents a single entry in a channel list. It carries the core metadata needed to display or transport channel information: the channel Name, an optional Topic, the current OnlineCount, and two visibility flags (IsPublic and IsProtected) which default to true and false respectively. Use this type whenever you need a concise, stable descriptor of a channel for UI lists, payloads, or comparisons, rather than a mutable or richer domain model.
|
||||
ChannelListItem is an immutable value object that describes a single channel in a channel list. It carries the channel's display name (`Name`), an optional topic (`Topic`), the number of online users (`OnlineCount`), and visibility flags (`IsPublic` and `IsProtected`). As a `record`, it provides value-based equality and straightforward construction for transport or UI scenarios, with `IsPublic` defaulting to true and `IsProtected` defaulting to false.
|
||||
|
||||
## Remarks
|
||||
ChannelListItem benefits from value-based equality inherent to records, so two items with identical fields compare as equal, which helps with list diffs, caching, and deduplication. The Topic is nullable to accommodate channels without a topic. Defaults (IsPublic = true, IsProtected = false) reflect common expectations for channels unless stated otherwise. Because this is a record, instances are immutable; to reflect changes (for example, a rising OnlineCount), create a new instance via a with-expression.
|
||||
The use of a `record` signals that this is a lightweight value object intended for transport and comparison across boundaries. It models channel metadata as a single, cohesive unit, aiding deduplication and consistent rendering in lists or API responses.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Basic construction with defaults for visibility flags
|
||||
var item = new ChannelListItem("general", "Public channel for announcements", 12);
|
||||
|
||||
// Or using named arguments for clarity
|
||||
var itemNamed = new ChannelListItem(Name: "general", Topic: "Public channel for announcements", OnlineCount: 12);
|
||||
|
||||
// Immutability in action: create a modified copy with an updated OnlineCount
|
||||
var updated = item with { OnlineCount = 13 };
|
||||
var item = new ChannelListItem("general", "General discussion", 12);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Topic is nullable; pass null if the channel has no topic.
|
||||
- To reflect a change in OnlineCount or other fields, use the with-expression since ChannelListItem is immutable.
|
||||
|
||||
- Topic may be null to indicate no topic is set.
|
||||
- IsPublic defaults to true and IsProtected defaults to false; pass explicit values to override.
|
||||
|
||||
---
|
||||
@@ -8,28 +8,12 @@ public interface IChatBroadcaster
|
||||
```
|
||||
|
||||
|
||||
An abstraction for broadcasting chat-related events and notifications to connected clients. Implementations deliver channel messages, presence updates, moderation events and connection-specific errors or disconnects to the appropriate recipients; use this interface when you want hub/transport-agnostic broadcasting logic (for example to decouple business logic from SignalR or another realtime transport).
|
||||
A transport-agnostic abstraction for broadcasting chat events and presence changes to connected clients. Use `IChatBroadcaster` whenever server-side code (for example a hub, worker, or command handler) needs to notify one or more clients about messages, presence updates, channel lifecycle events, moderation actions, or errors without depending on a specific delivery mechanism.
|
||||
|
||||
## Remarks
|
||||
This interface centralizes all outbound chat notifications the server emits: channel messages, user join/leave/presence events, channel lifecycle events (updated, deleted, nuked), moderation notifications (kicked, banned), message deletions, error messages to a particular connection, and forced disconnects. It exists to keep broadcasting responsibilities in one place so higher-level code can invoke intent ("send this message to the channel" or "force-disconnect these connections") without knowing how connections are routed or how the underlying transport addresses individual connections or groups.
|
||||
|
||||
Implementations must honor the documented routing hints (for example, do not echo a message back to an excluded connection when excludeConnectionId is supplied). Use the channelName and connectionId parameters to determine recipients; SendErrorAsync targets a single connection, while ForceDisconnectUserAsync targets a set of connection ids.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// typical usage from server-side chat logic
|
||||
// (messageDto and presenceDto are prepared elsewhere)
|
||||
await broadcaster.SendMessageToChannelAsync("#general", messageDto, excludeConnectionId: currentConnectionId);
|
||||
await broadcaster.SendUserJoinedAsync("#general", "alice", presenceDto, excludeConnectionId: currentConnectionId);
|
||||
|
||||
// send an error to a single connection
|
||||
await broadcaster.SendErrorAsync(connectionId, "You are not authorized to perform that action.");
|
||||
|
||||
// force-disconnect multiple connections for a user session cleanup
|
||||
await broadcaster.ForceDisconnectUserAsync(new List<string> { connA, connB }, "Session revoked");
|
||||
```
|
||||
`IChatBroadcaster` centralizes all outgoing chat-related notifications so callers do not need to know or implement the delivery/fan-out semantics. Each method maps to a well-defined event type: `SendMessageToChannelAsync` for chat messages, `SendUserJoinedAsync` / `SendUserLeftAsync` for presence changes, `SendChannelUpdatedAsync` / `SendChannelDeletedAsync` / `SendChannelNukedAsync` for channel lifecycle, moderation actions via `SendUserKickedAsync` / `SendUserBannedAsync`, and utility operations such as `SendMessageDeletedAsync`, `SendUserStatusChangedAsync`, `SendErrorAsync`, and `ForceDisconnectUserAsync` for forced disconnects. The interface is asynchronous (`Task`-based) so implementations can perform non-blocking I/O, retries, batching, or use different transports (for example SignalR, WebSockets, or a message bus) without changing callers. The `excludeConnectionId` parameter on message/presence methods encodes the common IRC convention of not echoing a message back to the originating connection while still delivering it to other connections belonging to the same user.
|
||||
|
||||
## Notes
|
||||
- excludeConnectionId is documented for SendMessageToChannelAsync to avoid echoing the origin connection; other methods that lack an exclude parameter (for example SendUserLeftAsync) will be delivered to all intended recipients unless an implementation-specific filter is applied.
|
||||
- SendUserStatusChangedAsync accepts a list of channel names so presence updates can be routed only to relevant channels; callers should pass the minimal set of channels that need the update to reduce unnecessary traffic.
|
||||
- Implementations should be asynchronous and non-blocking; broadcasting to many recipients may be best-effort and not transactional across multiple method calls.
|
||||
- `excludeConnectionId` prevents delivery only to the specified connection; other connections for the same user still receive the event. Callers should pass the sending connection id to avoid echoing to that connection but should not rely on it to suppress notifications to other sessions of the same user.
|
||||
- `SendChannelUpdatedAsync` includes an optional `channelName` parameter in addition to the [`ChannelDto`](../DTOs/ChatDtos.cs.md). The intent of the optional `channelName` (for example: target channel selection vs. previous name) is not obvious from the signature and should be clarified by the implementation or caller to avoid mismatched behavior.
|
||||
- All methods return `Task` and must be awaited or otherwise observed by callers to ensure errors in the broadcasting layer are surfaced; implementations may perform I/O and should handle transient failures internally or propagate meaningful exceptions to callers.
|
||||
@@ -3,9 +3,76 @@
|
||||
> **File:** `src/EchoHub.Core/Contracts/IChatService.cs`
|
||||
> **Kind:** interface
|
||||
|
||||
*Figure: How IChatService works.*
|
||||
|
||||
```mermaid
|
||||
%%{init: {'theme':'base','themeVariables':{'background':'#faf7ef','primaryColor':'#f0e2c2','primaryTextColor':'#1f2840','primaryBorderColor':'#8a7548','secondaryColor':'#d9efec','secondaryBorderColor':'#1d8a80','secondaryTextColor':'#1f2840','tertiaryColor':'#f2ebd8','tertiaryBorderColor':'#8a7548','tertiaryTextColor':'#1f2840','lineColor':'#1d8a80','titleColor':'#1f2840','fontSize':'14px','edgeLabelBackground':'#faf7ef','clusterBkg':'#f2ebd8','clusterBorder':'#8a7548','actorBkg':'#f0e2c2','actorBorder':'#8a7548','actorTextColor':'#1f2840','actorLineColor':'#8a7548','signalColor':'#1d8a80','signalTextColor':'#1f2840','activationBkgColor':'#d9efec','activationBorderColor':'#1d8a80','noteBkgColor':'#f2ebd8','noteBorderColor':'#8a7548','noteTextColor':'#1f2840','labelBoxBkgColor':'#f0e2c2','labelBoxBorderColor':'#8a7548','labelTextColor':'#1f2840','transitionColor':'#1d8a80','transitionLabelColor':'#1f2840','stateLabelColor':'#1f2840','altBackground':'#f2ebd8'}}}%%
|
||||
flowchart TB
|
||||
Start["Start"]
|
||||
Start --> Conn["IChatService: UserConnectedAsync(connectionId, userId, username) registers connection"]
|
||||
Conn --> Join["IChatService: JoinChannelAsync(connectionId, userId, username, channelName, password?) returns (History, Error, PasswordRequired)"]
|
||||
Join --> CheckPwd{"Channel requires password?"}
|
||||
CheckPwd -->|"yes"| RequirePwd["Return (History=null, Error='Password required', PasswordRequired=true)"]
|
||||
CheckPwd -->|"no"| Joined["Return (History=List of MessageDto, Error=null, PasswordRequired=false)"]
|
||||
Joined --> History["MessageDto: history items provided by GetChannelHistoryAsync(channelName, count, offset)"]
|
||||
|
||||
Conn --> Send["IChatService: SendMessageAsync(userId, username, channelName, content, originConnectionId?, replyToMessageId?)"]
|
||||
Send --> CheckReply{"replyToMessageId != null?"}
|
||||
CheckReply -->|"yes"| ValidateReply{"reply exists and is in same channel?"}
|
||||
ValidateReply -->|"no"| RejectReply["Return Error (invalid reply target)"]
|
||||
ValidateReply -->|"yes"| CreateMsg["Create MessageDto with content, sender, replyToMessageId"]
|
||||
CheckReply -->|"no"| CreateMsg
|
||||
CreateMsg --> Broadcast["IChatService: BroadcastMessageAsync(channelName, MessageDto) avoids echo to originConnectionId"]
|
||||
Broadcast --> Channel["Channel: deliver message to channel members' connections"]
|
||||
|
||||
Conn --> Update["IChatService: UpdateStatusAsync(userId, username, UserStatus, statusMessage) returns optional string"]
|
||||
Update --> PresenceList["IChatService: GetOnlineUsersAsync(channelName) returns list of UserPresenceDto"]
|
||||
|
||||
Broadcast --> ChannelUpdated["IChatService: BroadcastChannelUpdatedAsync(ChannelDto channel, channelName?)"]
|
||||
ChannelUpdated --> ChannelDto["ChannelDto: channel metadata"]
|
||||
ChannelUpdated --> ChannelDeleted["IChatService: BroadcastChannelDeletedAsync(channelName)"]
|
||||
|
||||
Conn --> Query["IChatService: GetChannelsForUserAsync(username) returns List of channels"]
|
||||
```
|
||||
|
||||
```csharp
|
||||
public interface IChatService
|
||||
```
|
||||
|
||||
|
||||
I have submitted the narrative documentation for IChatService and raised a critical flag about the malformed/redacted parameter in JoinChannelAsync. The documentation includes description, remarks, an example usage, and notes that point out the signature issue and nullable-return semantics for callers to verify against the concrete implementation.
|
||||
Provides chat-layer operations for connection lifecycle, channel membership, messaging, presence and cross-process broadcasting. Reach for `IChatService` when implementing or calling the application-level chat logic (for example from controllers, real-time hubs or an IRC gateway) rather than manipulating lower-level transport or persistence APIs directly.
|
||||
|
||||
## Remarks
|
||||
`IChatService` centralizes the domain operations needed by the real-time chat surface: tracking connections ([`UserConnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), joining and leaving channels ([`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`LeaveChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), sending and retrieving messages ([`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), presence ([`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetOnlineUsersAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), and broadcasting channel or message events to other processes ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface is designed for use by controllers and gateway components (the code comments indicate the IRC gateway uses several methods), so it intentionally mixes request/response operations (join, send) with one-way broadcast methods used to propagate state across processes.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Typical happy-path usage from a controller or hub
|
||||
var (history, joinError, passwordRequired) = await chatService.JoinChannelAsync(connectionId, userId, username, "general");
|
||||
if (joinError != null) {
|
||||
// handle join failure (implementation-specific semantics)
|
||||
return;
|
||||
}
|
||||
// Show the returned history to the user
|
||||
foreach (var item in history) {
|
||||
// item is a [`MessageDto`](../DTOs/ChatDtos.cs.md)
|
||||
}
|
||||
|
||||
// Send a message; the returned nullable string has implementation-dependent meaning
|
||||
var sendResult = await chatService.SendMessageAsync(userId, username, "general", "Hello everyone!");
|
||||
if (sendResult != null) {
|
||||
// react to non-null result per the concrete implementation
|
||||
}
|
||||
|
||||
// Broadcast a message instance (e.g. from background processing or another gateway)
|
||||
// `message` here is a [`MessageDto`](../DTOs/ChatDtos.cs.md) obtained from persistence or constructed by the implementation
|
||||
// await chatService.BroadcastMessageAsync("general", message);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Several methods return `Task<string?>` (for example [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface does not document the exact semantics of a non-null string (error message vs. identifier vs. other). Consumers must consult the concrete implementation or its docs to interpret these values correctly.
|
||||
- The `originConnectionId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) is used to avoid echoing a broadcast back to the originating connection (IRC-like behavior). Other sessions owned by the same user still receive the message.
|
||||
- The `replyToMessageId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) must reference a message that exists in the same channel; implementations should validate this constraint.
|
||||
- [`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md) returns a tuple containing `History`, `Error`, and `PasswordRequired`. Callers should handle the `Error` and `PasswordRequired` flags before assuming `History` contains usable data.
|
||||
- [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md) supports simple pagination via `count` and `offset`; callers should choose `count` and `offset` to limit load and avoid returning excessively large histories in a single call.
|
||||
- Broadcasting methods ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)) are intentionally one-way primitives used to notify other processes; they do not return operation results and callers should not rely on them for synchronous guarantees.
|
||||
@@ -8,53 +8,93 @@ public interface IEchoHubClient
|
||||
```
|
||||
|
||||
|
||||
Represents the set of callbacks the server can invoke on a connected client. Implement this interface on client-side code that subscribes to the server's real-time hub so the client can react to server-initiated events such as incoming messages, presence updates, channel changes, and administrative actions.
|
||||
Represents the callback contract for notifications and control messages the server can invoke on connected clients. Implement this interface on the client side (or provide a test double) when you need a strongly-typed set of server-to-client RPCs for events such as new messages, presence changes, channel updates, moderation actions, and error or disconnect notifications.
|
||||
|
||||
## Remarks
|
||||
This interface defines a stable, strongly-typed surface for server-to-client notifications. Each method corresponds to a distinct event the server may raise (message delivery, user presence changes, channel lifecycle events, errors, and forced disconnects). Implementations keep client-side handling decoupled from the transport layer and allow the server to call back into client logic without embedding client behavior in server code.
|
||||
This interface centralizes all server-originated client callbacks into a single, versioned surface so the server can address connected clients with a known set of operations. Each method returns a `Task` to allow asynchronous client implementations (IO, UI dispatching, persistence) and to make the callbacks composable for test harnesses and runtime adapters. The nullable annotations on parameters (for example the `UserPresenceDto?` in `UserJoined` and `string?` in `UserKicked`) indicate which values the server may omit; implementations must handle those cases.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Minimal client-side implementation that logs events; real handlers should avoid long-running work.
|
||||
public class EchoClientHandler : IEchoHubClient
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
public class ConsoleEchoClient : IEchoHubClient
|
||||
{
|
||||
public Task ReceiveMessage(MessageDto message)
|
||||
{
|
||||
Console.WriteLine($"Received message: {message}");
|
||||
Console.WriteLine($"[{message.Channel}] {message.Sender}: {message.Text}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UserJoined(string channelName, string username, UserPresenceDto? presence)
|
||||
{
|
||||
Console.WriteLine($"{username} joined {channelName}");
|
||||
Console.WriteLine($"User joined {channelName}: {username}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UserLeft(string channelName, string username)
|
||||
{
|
||||
Console.WriteLine($"{username} left {channelName}");
|
||||
Console.WriteLine($"User left {channelName}: {username}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChannelUpdated(ChannelDto channel)
|
||||
{
|
||||
Console.WriteLine($"Channel updated: {channel.Name}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UserStatusChanged(UserPresenceDto presence)
|
||||
{
|
||||
Console.WriteLine($"Status changed: {presence.Username} -> {presence.Status}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UserKicked(string channelName, string username, string? reason)
|
||||
{
|
||||
Console.WriteLine($"User kicked from {channelName}: {username} Reason: {reason ?? "(none)"}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UserBanned(string username, string? reason)
|
||||
{
|
||||
Console.WriteLine($"User banned: {username} Reason: {reason ?? "(none)"}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task MessageDeleted(string channelName, Guid messageId)
|
||||
{
|
||||
Console.WriteLine($"Message deleted in {channelName}: {messageId}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChannelDeleted(string channelName)
|
||||
{
|
||||
Console.WriteLine($"Channel deleted: {channelName}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ChannelNuked(string channelName)
|
||||
{
|
||||
Console.WriteLine($"Channel nuked: {channelName}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task ForceDisconnect(string reason)
|
||||
{
|
||||
Console.WriteLine($"Force disconnect: {reason}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Other members can be implemented similarly; keep handlers quick and non-blocking.
|
||||
public Task ChannelUpdated(ChannelDto channel) => Task.CompletedTask;
|
||||
public Task UserStatusChanged(UserPresenceDto presence) => Task.CompletedTask;
|
||||
public Task UserKicked(string channelName, string username, string? reason) => Task.CompletedTask;
|
||||
public Task UserBanned(string username, string? reason) => Task.CompletedTask;
|
||||
public Task MessageDeleted(string channelName, Guid messageId) => Task.CompletedTask;
|
||||
public Task ChannelDeleted(string channelName) => Task.CompletedTask;
|
||||
public Task ChannelNuked(string channelName) => Task.CompletedTask;
|
||||
public Task ForceDisconnect(string reason) => Task.CompletedTask;
|
||||
public Task Error(string message)
|
||||
{
|
||||
Console.Error.WriteLine(message);
|
||||
Console.WriteLine($"Error from server: {message}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Handlers are asynchronous (return Task): keep implementations short and non-blocking to avoid delaying the server's invocation path.
|
||||
- Nullable parameters (e.g. UserPresenceDto? and string?) may be null; check before accessing members.
|
||||
- Server-driven callbacks can occur concurrently; ensure any shared client state mutated by these methods is accessed in a thread-safe manner.
|
||||
- Catch and handle exceptions inside handlers — unhandled exceptions may affect the connection or be observable by the server depending on the transport behavior.
|
||||
- Respect nullability: parameters annotated with `?` (for example `UserPresenceDto?` and `string?`) may be `null` and callers should handle those cases gracefully.
|
||||
- All methods return `Task`: implementations should avoid long-running synchronous work on the calling thread (use `async`/`await` or schedule work) to prevent blocking the runtime that invokes these callbacks.
|
||||
- Implementations should avoid throwing exceptions from these methods where possible; unhandled exceptions may surface to the caller or the hosting infrastructure depending on how the callbacks are invoked.
|
||||
@@ -8,28 +8,27 @@ public interface IMessageEncryptionService
|
||||
```
|
||||
|
||||
|
||||
IMessageEncryptionService defines a pluggable contract for encrypting and decrypting string data, using a distinctive prefix to mark encrypted content so callers can distinguish ciphertext from plain text and pass through non-encrypted values safely. It also exposes EncryptDatabaseEnabled to reflect the server setting for encrypting database content at rest, and provides nullable variants to handle optional fields without extra null checks.
|
||||
The `IMessageEncryptionService` interface defines a centralized contract for encrypting and decrypting messages used in transit and at rest. It exposes a straightforward API to convert plaintext into ciphertext and back, while the `CiphertextPrefix` marks encrypted payloads so the implementation can transparently pass through values that are not encrypted. The `EncryptDatabaseEnabled` flag surfaces the server-side setting that indicates whether data stored in the database should be encrypted at rest, enabling callers to adapt their behavior to policy.
|
||||
|
||||
## Remarks
|
||||
This interface acts as a thin abstraction that isolates encryption concerns from business logic, enabling swap-in of different algorithms or key-management strategies without touching call sites. The public CiphertextPrefix and the Decrypt pass-through behavior for non-encrypted values provide a simple, deterministic convention for distinguishing encrypted payloads. The nullable variants help preserve nullability semantics in data-transfer surfaces while still enabling encryption when a value is present.
|
||||
|
||||
This abstraction minimizes scattered crypto logic by presenting a single, testable surface for encryption decisions. The pass-through behavior for content that does not begin with the `CiphertextPrefix` helps prevent double-encrypting and keeps compatibility with data already in plaintext. By providing nullable-aware methods (`EncryptNullable` and `DecryptNullable`), it cleanly handles optional values without forcing callers to perform boilerplate null checks at call sites.
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
// Given an IMessageEncryptionService implementation (injected or resolved via DI)
|
||||
IMessageEncryptionService service = ...;
|
||||
// Assume you have an instance of IMessageEncryptionService named `service`
|
||||
string ciphertext = service.Encrypt("TopSecret");
|
||||
string plaintext = service.Decrypt(ciphertext); // "TopSecret"
|
||||
|
||||
string plain = "customer-secret";
|
||||
string cipher = service.Encrypt(plain);
|
||||
string decrypted = service.Decrypt(cipher); // == plain
|
||||
// Decrypting non-encrypted content yields the original value (pass-through)
|
||||
string passthrough = service.Decrypt("plain-text"); // "plain-text"
|
||||
|
||||
string? nullablePlain = null;
|
||||
string? nullableCipher = service.EncryptNullable(nullablePlain); // null
|
||||
string? nullableDecrypted = service.DecryptNullable(nullableCipher); // null
|
||||
|
||||
bool atRest = service.EncryptDatabaseEnabled;
|
||||
string? nullableValue = null;
|
||||
string? encNullable = service.EncryptNullable(nullableValue); // null
|
||||
string? decNullable = service.DecryptNullable(encNullable); // null
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Decrypt will pass through values that do not start with the CiphertextPrefix.
|
||||
- EncryptNullable/DecryptNullable gracefully handle nulls by returning null.
|
||||
- EncryptDatabaseEnabled indicates whether server-side encrypt-at-rest is active; use it to guide storage strategies.
|
||||
- The `CiphertextPrefix` ("$ENC$v1$") is a marker used to identify encrypted data. Decrypt will return the input unchanged if it does not start with this prefix.
|
||||
- `EncryptDatabaseEnabled` reflects a server policy. It indicates whether data should be encrypted at rest, but callers must still invoke `Encrypt`/`EncryptNullable` before storage to ensure encryption occurs per policy.
|
||||
|
||||
@@ -8,30 +8,29 @@ public interface IUserService
|
||||
```
|
||||
|
||||
|
||||
IUserService defines a contract for asynchronous user-management operations within EchoHub.Core. It exposes methods to register and authenticate users, retrieve profiles by username or by ID, update profile details, and set a user's avatar. Implementations of this interface serve as the single logical boundary for user lifecycle concerns, allowing REST endpoints and the IRC gateway to funnel through a consistent surface and enabling easier testing and swapping of storage or identity providers. The RegisterUserAsync method acknowledges server configuration: when Server:Registration is set to "invite", an inviteCode is required; when set to "closed", new accounts are rejected; all such flows funnel through this service.
|
||||
`IUserService` is the asynchronous contract for common user-account operations: registration, authentication, and profile access. Implementations may back these calls with REST, an IRC gateway, or other transports, but callers interact with this interface to perform login, account creation, and profile queries without coupling to a specific transport.
|
||||
|
||||
## Remarks
|
||||
By centralizing these operations behind IUserService, the rest of the system depends on a stable, testable contract rather than concrete data stores or authentication mechanisms. It coordinates with the UserOperationResult wrapper to communicate success or failure and, for retrieval operations, to surface user data returned on success, keeping error handling consistent across the application.
|
||||
|
||||
By returning `Task<UserOperationResult>` for mutating operations and `Task<UserProfileDto?>` for profile queries, the interface cleanly models success/failure and optional data. The [`UserOperationResult`](../DTOs/CommonDtos.cs.md) type provides `Success(UserProfileDto user)` and `Fail(UserError error, string message)` helpers, enabling implementations to construct consistent outcomes. The `RegisterUserAsync` method carries a server-policies cue in its comment: when `Server:Registration = "invite"`, an `inviteCode` is required; in `"closed"` mode, new accounts are refused. This centralizes registration policy at the service boundary and avoids scattering policy checks across call sites.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Example usage of the IUserService contract
|
||||
var result = await userService.RegisterUserAsync("alice", "P@ssw0rd", displayName: "Alice", inviteCode: "INV-123");
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
// registration succeeded; you can proceed with login or profile fetch
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
var profile = await userService.GetUserProfileAsync("alice");
|
||||
if (profile != null)
|
||||
// Example usage of IUserService
|
||||
public async Task DemoAsync(IUserService userService)
|
||||
{
|
||||
// use profile data
|
||||
var reg = await userService.RegisterUserAsync("alice", "Secret123", inviteCode: "INVITE-42");
|
||||
if (reg.IsSuccess)
|
||||
{
|
||||
var profile = await userService.GetUserProfileAsync("alice");
|
||||
// Use profile as needed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- If you call UpdateProfileAsync with all arguments as null, the operation may be a no-op; only pass the fields you intend to update.
|
||||
- GetUserByIdAsync returns a UserProfileDto?; handle the null case when the user does not exist.
|
||||
- For registration, ensure your server's registration policy (invite vs closed) is aligned with your inviteCode usage; otherwise registration may fail.
|
||||
|
||||
- The `inviteCode` parameter is context-sensitive and should be supplied when the server is configured with `Server:Registration = "invite"`; otherwise it may be omitted.
|
||||
- All methods are asynchronous; callers should `await` the results and branch on `UserOperationResult.IsSuccess` as appropriate.
|
||||
- [`GetUserProfileAsync`](../../EchoHub.Client/Services/ApiClient.cs.md) and `GetUserByIdAsync` return `UserProfileDto?`, reflecting the possibility that a user profile may not be found or accessible in certain contexts.
|
||||
|
||||
@@ -26,20 +26,14 @@ public record DeleteAccountRequest(string Password)
|
||||
| `Password` | `string` | — |
|
||||
|
||||
|
||||
This record models the password-confirmation payload required when a user initiates destructive self-service account actions (such as deleting their account). It captures the password as a single field to prove the user’s intent before the action is executed.
|
||||
Represents a request payload that carries the user's `Password` to re-confirm destructive self-service actions on the account. This separate `DeleteAccountRequest` DTO isolates credential input from other account data and is intended for use in flows that require explicit user re-authentication before irreversible operations (e.g., account deletion).
|
||||
|
||||
## Remarks
|
||||
DeleteAccountRequest encapsulates a sensitive credential within a lightweight boundary object to keep password handling explicit in the delete workflow. By isolating the password in a dedicated payload, the system can perform authentication checks, auditing, and policy enforcement at the appropriate boundary. The record is immutable and minimal (a single Password property), which simplifies model binding and reduces the surface area for accidental data exposure.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// When initiating a delete flow, supply the password for re-confirmation.
|
||||
var request = new DeleteAccountRequest("P@ssw0rd!");
|
||||
```
|
||||
Isolates sensitive credential input into a minimal, purpose-built payload, enabling focused validation and auditing of destructive actions. It complements authentication state by forcing an explicit password re-entry rather than relying on session state alone, which helps mitigate accidental or unauthorized deletions. This pattern supports clearer separation of concerns between domain models and security-critical request data.
|
||||
|
||||
## Notes
|
||||
- Treat the Password as sensitive; avoid logging or exposing it in responses.
|
||||
- Use this payload only in the delete flow; ensure that the password validation is performed server-side before performing the destructive action.
|
||||
- Do not log or persist the `Password` value in plaintext; keep it transient and ensure redaction in any logs.
|
||||
- Ensure transport security (`TLS`) when transmitting this payload; avoid storing passwords in memory longer than needed; clear the value after usage if possible.
|
||||
|
||||
---
|
||||
|
||||
@@ -69,28 +63,16 @@ public record ExportedAttachmentDto(
|
||||
| `SentAt` | `DateTimeOffset` | — |
|
||||
|
||||
|
||||
Represents the metadata of an attachment that has been exported from a channel. It groups the file name, a URL to access the file, the file size in bytes, a textual kind descriptor, the originating channel name, and the timestamp when it was sent. Use this DTO when returning or transmitting export results to clients or cross-system boundaries to ensure a stable, serializable shape that is decoupled from internal domain models.
|
||||
Represents the metadata of an attachment that has been exported, carrying the essential details needed to access and display it—`FileName`, `Url`, `FileSize`, `Kind`, `ChannelName`, and `SentAt`. It serves as a transport contract between the export logic and clients or downstream services rather than exposing internal domain entities.
|
||||
|
||||
## Remarks
|
||||
- Being a record, instances are immutable and equality is value-based, making it ideal for transport across layers or for caching export results. It serves as a clean contract between the export process and API or consumer layers.
|
||||
- It acts as a boundary object, decoupling presentation/API concerns from domain entities while preserving the essential attachment metadata needed by clients (name, access URL, size, kind, origin channel, and timestamp).
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var attachment = new ExportedAttachmentDto(
|
||||
FileName: "invoice.pdf",
|
||||
Url: "https://cdn.example.com/exports/invoice.pdf",
|
||||
FileSize: 254000,
|
||||
Kind: "document",
|
||||
ChannelName: "billing",
|
||||
SentAt: DateTimeOffset.UtcNow
|
||||
);
|
||||
```
|
||||
`ExportedAttachmentDto` acts as a boundary-crossing contract: it decouples the external payload from the internal attachment representation and exposes only the data consumers require. The inclusion of a `Url` implies a downloadable resource that may be protected or time-limited, so callers should treat access as potentially ephemeral and handle expiration appropriately. Because this is a `record`, instances are immutable by default, which helps preserve the integrity of the export snapshot across layers.
|
||||
|
||||
## Notes
|
||||
- The Kind property is a free-form string; if there is a known finite set of kinds, consider introducing a dedicated enum later to avoid inconsistent values.
|
||||
- FileSize is a long and should be non-negative; implement validation at boundaries if negative values could be produced by upstream systems.
|
||||
- Ensure the Url is appropriate for client access (consider expiration, authentication, and CORS as needed) since this DTO surfaces a direct link to the exported attachment.
|
||||
|
||||
- The `Url` is often a signed or temporary link; do not assume long-lived access and design clients to handle expiration (e.g., 404 or 403 responses).
|
||||
- This DTO is strictly a data carrier; avoid embedding business logic in the payload and prefer mapping from domain models to this shape when exporting data.
|
||||
|
||||
---
|
||||
|
||||
@@ -118,26 +100,10 @@ public record ExportedMessageDto(
|
||||
| `ReplyToMessageId` | `Guid?` | — |
|
||||
|
||||
|
||||
ExportedMessageDto is an immutable data transfer object that captures the essential details of a message exported from a channel: its identity (Id), the channel it came from (ChannelName), when it was sent (SentAt), the message content (Content), and an optional reference to the message it replies to (ReplyToMessageId). It serves as a serialization-friendly payload used by export or archival pipelines, decoupled from the in-memory domain model.
|
||||
ExportedMessageDto is an immutable data transfer object (record) that captures the essential data of a single exported message: the message `Id`, the `ChannelName` it was sent in, the `SentAt` timestamp, the `Content`, and an optional `ReplyToMessageId` if the message is a reply. It provides a stable, serializable contract for exporting messages to external systems or archives, decoupled from domain behavior so consumers can rely on a consistent shape without depending on domain entities.
|
||||
|
||||
## Remarks
|
||||
ExportedMessageDto provides a stable contract for export pipelines by decoupling serialized data from the internal domain entities. Being a record, it benefits from value-based equality and immutability, which simplifies de-duplication and testing of exported payloads. The nullable ReplyToMessageId models the optional threading relationship: null means the message has no parent. Use ChannelName and SentAt as lightweight contextual metadata when reconstructing conversations in external systems.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var message = new ExportedMessageDto(
|
||||
Id: Guid.NewGuid(),
|
||||
ChannelName: "general",
|
||||
SentAt: DateTimeOffset.UtcNow,
|
||||
Content: "Hello world",
|
||||
ReplyToMessageId: null
|
||||
);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- The ReplyToMessageId is nullable; null indicates no parent message.
|
||||
- As a record, equality is based on all properties; two messages with identical data compare as equal.
|
||||
- If you need to derive a modified copy without mutating the original, use the with-expression (e.g., var updated = message with { Content = "Updated" };).
|
||||
As a `record`, `ExportedMessageDto` provides value-like semantics and a predictable equality contract, which is helpful when comparing exported records or caching results during export pipelines. It also separates export concerns from the rest of the domain, making it easier to evolve the internal models without breaking external consumers.
|
||||
|
||||
---
|
||||
|
||||
@@ -165,14 +131,12 @@ public record UserDataExportDto(
|
||||
| `Attachments` | `List<ExportedAttachmentDto>` | — |
|
||||
|
||||
|
||||
Represents a persisted snapshot of a user's data as stored by the server, intended for data export or portability. It consolidates the export timestamp, the server identity, the user's profile, and the exported messages and attachments; in end-to-end encrypted rooms the message contents are ciphertext, since the server never has access to plaintext.
|
||||
`UserDataExportDto` is a `record` that represents a complete snapshot of the server's stored data for a given user, produced when exporting user data for portability or archival. It contains the export timestamp (`ExportedAt`), the originating server name ([`ServerName`](../../EchoHub.Server.Irc/IrcCommandHandler.cs.md)), the user's profile (`Profile`), and the exported content items: messages (`Messages`) and attachments (`Attachments`). In end-to-end encrypted rooms, the message payload is preserved as ciphertext, since the server cannot provide plaintext it never possessed.
|
||||
|
||||
## Remarks
|
||||
|
||||
UserDataExportDto is an immutable data transfer object that anchors the export pipeline to the server's stored representation. By pairing profile, messages, and attachments into a single artifact, it simplifies serialization, auditing, and versioning while guarding the boundaries between storage concerns and export logic.
|
||||
This DTO acts as the stable envelope for user data exports, keeping metadata, profile, and content items together for portability and archival use. It decouples export semantics from how data is stored, permitting changes to storage without breaking export contracts. Note that for end-to-end encrypted rooms, the `Messages` are ciphertext as stored; no plaintext is accessible to the server.
|
||||
|
||||
## Notes
|
||||
|
||||
- The Messages collection contains ciphertext for end-to-end encrypted rooms; do not decrypt on the server. Decryption and user presentation must happen client-side with proper keys.
|
||||
- Large exports can be memory-intensive; plan for streaming or chunked delivery in exporters.
|
||||
|
||||
---
|
||||
@@ -27,22 +27,13 @@ public record LoginRequest(string Username, string Password)
|
||||
| `Password` | `string` | — |
|
||||
|
||||
|
||||
Represents the credentials needed to log a user in. This immutable record carries a Username and Password and is intended to be used as a data transfer object when submitting login data to authentication endpoints.
|
||||
Represents the credentials payload for a login operation as an immutable data transfer object. It carries the two required fields, `Username` and `Password`, and is intended to be sent to the authentication boundary to perform sign-in. Use `LoginRequest` when you need to pass user credentials through service boundaries in a strongly-typed, single payload rather than as separate arguments.
|
||||
|
||||
## Remarks
|
||||
|
||||
As a positional record, LoginRequest provides value-based equality and deconstruction. It is immutable, with init-only properties, which helps prevent accidental mutation of credential data as it travels across system boundaries. Treat Password as sensitive data: avoid logging or displaying it, and ensure transport security when sending this DTO.
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
var request = new LoginRequest("alice", "P@ssw0rd!");
|
||||
```
|
||||
Because `LoginRequest` is a `record`, it provides value-based equality and immutability, which makes it a natural data carrier across application layers. This abstraction helps decouple transport concerns from domain logic by centralizing credentials into a single, typed payload.
|
||||
|
||||
## Notes
|
||||
|
||||
- Password is sensitive data; avoid logging or displaying it; mask when emitted in logs or error messages.
|
||||
- This is a simple data-transfer object; it contains no business logic.
|
||||
- Do not log or serialize the `Password` value; treat `LoginRequest` as sensitive data and ensure transport uses TLS.
|
||||
|
||||
---
|
||||
|
||||
@@ -72,27 +63,25 @@ public record LoginResponse(
|
||||
| `NicknameColor` | `string?` | — |
|
||||
|
||||
|
||||
LoginResponse is a data transfer object that represents the server's response to a successful login. It bundles the authentication tokens (Token and RefreshToken), the token expiration moment (ExpiresAt), and the authenticated user's identity (Username), along with optional personalization fields (DisplayName and NicknameColor). This object is intended for consumption by clients to establish authenticated sessions, attach the access token to requests, refresh tokens when needed, and present user information in the UI.
|
||||
`LoginResponse` represents the result of a login attempt, carrying the [`Token`](../../EchoHub.Client/Services/ApiClient.cs.md), `RefreshToken`, `ExpiresAt`, and user identity data like `Username`, with optional `DisplayName` and `NicknameColor` for UI personalization. As a `record`, it is immutable and uses value-based equality, making it a convenient, transportable payload for authentication flows.
|
||||
|
||||
## Remarks
|
||||
LoginResponse is an immutable value object (a record) whose identity is defined by its content. It cleanly separates transport concerns from domain logic, acting as a simple contract that different layers can rely on without side effects. The optional DisplayName and NicknameColor fields model user-facing personalization; callers must handle potential nulls when those fields are not provided.
|
||||
Immutability and value-based equality make `LoginResponse` easy to compare, cache, and pattern-match in authentication workflows. It groups all login-related data in one cohesive container, reducing the risk of mismatched fields across layers. The optional `DisplayName` and `NicknameColor` allow UI layers to present user-friendly details without forcing these values for every login.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var response = new LoginResponse(
|
||||
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
RefreshToken: "def123-refresh",
|
||||
RefreshToken: "defghijklmnopqrstuvwxyz",
|
||||
ExpiresAt: DateTimeOffset.UtcNow.AddHours(1),
|
||||
Username: "alex",
|
||||
DisplayName: "Alex Doe",
|
||||
NicknameColor: "#FF6A00"
|
||||
Username: "alice",
|
||||
DisplayName: "Alice",
|
||||
NicknameColor: "#1E90FF"
|
||||
);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- DisplayName and NicknameColor may be null if the server omits them.
|
||||
- Treat this type as data-only; avoid adding behavior such as validation or mutation.
|
||||
- Token values are sensitive; avoid logging them and consider secure storage/handling in the client.
|
||||
- Token and RefreshToken are sensitive; avoid logging them or exposing them in UI or analytics outputs. Treat these values as secrets and secure any transport or storage paths that handle them.
|
||||
|
||||
---
|
||||
|
||||
@@ -111,21 +100,14 @@ public record RefreshRequest(string RefreshToken)
|
||||
| `RefreshToken` | `string` | — |
|
||||
|
||||
|
||||
RefreshRequest is a small, immutable data transfer object (a C# record) that carries a single value: the RefreshToken. It is used when a client requests a new access token from the authentication service, typically by posting this payload to the refresh endpoint.
|
||||
An immutable data container representing the payload of a token refresh request. It exposes a single property, `RefreshToken`, which the authentication workflow uses to obtain new access tokens.
|
||||
|
||||
## Remarks
|
||||
By representing the refresh payload as a dedicated type, the API boundary gains a clear, strongly-typed contract that can be validated and logged consistently. The use of a record ensures value-based equality and immutable semantics, which helps prevent accidental mutation during transport or handling and makes it straightforward to pattern-match or deconstruct if needed in higher layers. In the overall authentication flow, this DTO sits alongside other EchoHub authentication DTOs and forms the low-level transport shape for refresh token exchanges.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var request = new RefreshRequest("sample-refresh-token");
|
||||
```
|
||||
Because this is a `record` with a single value, it provides value-based equality and straightforward deconstruction, making it ideal as a data-transfer object (DTO) across API boundaries. It decouples transport concerns from token-issuance logic, enabling the controller to receive and forward the refresh token without embedding behavior.
|
||||
|
||||
## Notes
|
||||
- Do not log or expose the RefreshToken; avoid writing it to logs or UI.
|
||||
- Ensure the token is transmitted over HTTPS and handled only in the request body, not in URLs.
|
||||
- Validate that the token is non-empty before sending to the refresh endpoint; handle nulls gracefully.
|
||||
|
||||
- `RefreshToken` is sensitive data; avoid logging it or exposing it in error payloads.
|
||||
- This type is a plain DTO with no validation or side effects; validation should occur in the service layer.
|
||||
|
||||
---
|
||||
|
||||
@@ -147,22 +129,13 @@ public record RegisterRequest(string Username, string Password, string? DisplayN
|
||||
| [`InviteCode`](../Models/InviteCode.cs.md) | `string?` | `null` |
|
||||
|
||||
|
||||
RegisterRequest is a compact data-transfer object used to convey the information necessary to register a new user. It requires a Username and Password, and optionally accepts a DisplayName and an InviteCode. Implemented as a C# positional record, it is immutable and uses value-based equality, making it ideal for transport across API boundaries and for straightforward comparisons in tests. This DTO is typically produced by a client during registration and consumed by server-side authentication logic. The DisplayName parameter is nullable with a default of null, allowing clients to omit it; InviteCode is also nullable and used only when the onboarding flow supports invitation codes.
|
||||
RegisterRequest is a data-transfer object that captures the input for a user registration operation. It encapsulates the required `Username` and `Password` and includes optional `DisplayName` and [`InviteCode`](../Models/InviteCode.cs.md) so callers can supply additional metadata in a single payload to the authentication endpoint.
|
||||
|
||||
## Remarks
|
||||
This symbol acts as a stable contract for the registration flow: it encapsulates the required credentials and optional metadata in a single, immutable object. By using a record, equality and deconstruction align with value semantics, making it easy to compare requests and to pass them through layers without mutation. Because DisplayName and InviteCode are optional, validation often happens elsewhere, enabling flexible client behavior while preserving a clear API boundary.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Typical usage with all fields
|
||||
var full = new RegisterRequest("jdoe", "P@ssw0rd", "John Doe", "INVITE-42");
|
||||
|
||||
// Minimal usage: only required fields
|
||||
var minimal = new RegisterRequest("jdoe", "P@ssw0rd");
|
||||
```
|
||||
As a simple DTO, `RegisterRequest` acts as a stable contract between the public API surface and the authentication logic. It isolates the registration input structure from internal domain models, enabling independent evolution and simpler testing while the underlying registration workflow evolves.
|
||||
|
||||
## Notes
|
||||
- Do not log or leak the Password value; treat it as sensitive data and rely on secure transport and proper logging practices.
|
||||
- Optional fields may be null; server-side validation should enforce any business rules regarding DisplayName or InviteCode as appropriate.
|
||||
- Do not log or serialize the `Password` field in logs or telemetry; treat it as sensitive data and rely on transport security.
|
||||
- The optional fields `DisplayName` and [`InviteCode`](../Models/InviteCode.cs.md) may be `null`; downstream code should handle nulls gracefully and only include them when provided.
|
||||
|
||||
---
|
||||
@@ -45,25 +45,10 @@ public record AttachmentDto(
|
||||
| `AsciiPreview` | `string?` | `null` |
|
||||
|
||||
|
||||
Represents a file attachment attached to a chat message. It carries the attachment kind, a URL to access the resource, the original file name, the size of the file, and an optional ASCII preview used for color-tag art when available. This DTO is used when composing or processing message payloads that include attachments, or when consuming message data that contains attachment metadata. In end-to-end encrypted channels the content behind the URL and the preview may be ciphertext that the server cannot read.
|
||||
A file attached to a message is represented by `AttachmentDto`. It carries the attachment's kind ([`AttachmentKind`](../Models/AttachmentKind.cs.md)), a URL to retrieve the content (`Url`), the original file name (`FileName`), and the file size in bytes (`FileSize`). If available, `AsciiPreview` holds color-tag ASCII art for images; in end-to-end encrypted channels the data behind `Url` and the preview is ciphertext the server cannot read.
|
||||
|
||||
## Remarks
|
||||
AttachmentDto serves as a compact, immutable value object that consolidates attachment metadata for transport, storage, and rendering across UI and API boundaries. Being a record provides value-based equality, which simplifies deduplication and caching scenarios, and makes it natural to compare attachments without inspecting the entire payload. It decouples attachment handling from the message body, enabling consistent rendering and processing of attachments regardless of how the message content is structured.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Example: construct an attachment DTO for a file attachment
|
||||
var attachment = new AttachmentDto(
|
||||
default(AttachmentKind),
|
||||
"https://cdn.example.com/files/document.pdf",
|
||||
"document.pdf",
|
||||
204800,
|
||||
null);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- AsciiPreview is optional; when present, it provides a text-based preview but is not guaranteed to render a full image. Clients should gracefully fall back to the URL or file name if the preview is absent.
|
||||
- AttachmentDto is a record, so instances are immutable and compare by value. This supports straightforward caching and deduplication strategies across layers.
|
||||
Because `AttachmentDto` is a record, it provides value-based equality and immutability, making it a stable transport object across layers. It decouples the attachment metadata from the message payload, enabling clients to render previews or retrieve content on demand without embedding binary data in the message. The `AsciiPreview` field offers a lightweight preview for image attachments, while `Url` points to the resource whose handling may be encrypted in transit.
|
||||
|
||||
---
|
||||
|
||||
@@ -83,15 +68,10 @@ public record ChannelCryptoDto(bool IsEncrypted, string? EncryptionSalt)
|
||||
| `EncryptionSalt` | `string?` | — |
|
||||
|
||||
|
||||
ChannelCryptoDto carries the public cryptographic metadata required by a client to derive its join credential from a passphrase. It should be used by clients during the channel join flow to determine if a passphrase-based derivation is necessary and to access the salt used for key derivation, without ever handling the wrapped room key.
|
||||
ChannelCryptoDto is a small data container that exposes the channel's cryptographic policy: whether encryption is enabled (`IsEncrypted`) and the salt used to derive a join credential from a passphrase (`EncryptionSalt`). Use it when you need to pass this metadata across system boundaries without exposing the wrapped room key.
|
||||
|
||||
## Remarks
|
||||
This DTO isolates derivation parameters from actual keys, enabling authentication-related components to reason about how a credential is derived without touching or exposing key material. The IsEncrypted flag indicates whether a passphrase-based join is applicable, and EncryptionSalt provides the salt used in the derivation when encryption is in effect. When IsEncrypted is false, EncryptionSalt may be null, reflecting that no passphrase-based derivation is required.
|
||||
|
||||
## Notes
|
||||
- If IsEncrypted is true, EncryptionSalt should be non-null to derive the join credential; when false, the salt may be null.
|
||||
- This is a simple data transfer object intended to convey derivation parameters safely; never serialize or expose wrapped key material.
|
||||
|
||||
Consolidating `IsEncrypted` and `EncryptionSalt` into a single value object reduces coupling between channel-joining logic and cryptographic operations. It makes intent explicit at call sites that must decide how to derive credentials from a passphrase. Importantly, the actual wrapped room key remains outside this DTO, preserving the security boundary that keys are only handled by the cryptographic subsystem. The nullable `EncryptionSalt` communicates that a salt is omitted when encryption is disabled.
|
||||
|
||||
---
|
||||
|
||||
@@ -127,30 +107,25 @@ public record ChannelDto(
|
||||
| `IsSystem` | `bool` | `false` |
|
||||
|
||||
|
||||
ChannelDto is an immutable data transfer object that encapsulates the core metadata of a chat channel. It groups the channel’s unique identifier, display name, an optional topic, visibility, message count, and creation timestamp, together with flags that describe its characteristics (protected, encrypted, and system channels). This object is commonly produced by the server when retrieving or creating channel data and is consumed by clients and services that need a stable snapshot of a channel’s state. As a record, ChannelDto provides value-based equality and supports convenient cloning via with-expressions without mutating the original instance.
|
||||
ChannelDto is an immutable data transfer object that carries the essential metadata of a chat channel: `Id`, `Name`, `Topic`, `IsPublic`, `MessageCount`, `CreatedAt`, and the optional flags `IsProtected`, `IsEncrypted`, and `IsSystem`. As a `record`, it provides value-based equality and a straightforward bundle of properties suitable for transport across layers or API boundaries without exposing domain entities. Use it when returning channel summaries, listings, or lightweight channel representations to clients or other services, rather than leaking internal domain models.
|
||||
|
||||
## Remarks
|
||||
ChannelDto serves as a transport-friendly abstraction that decouples channel metadata from domain models. The boolean flags encode common channel semantics: IsPublic indicates whether the channel is publicly discoverable, IsProtected denotes restricted access, IsEncrypted signals encryption usage, and IsSystem marks built-in, system-managed channels. CreatedAt represents the creation-time snapshot and should be treated as immutable; for updates, create a new ChannelDto instance (e.g., with a with-expression) rather than mutating the existing one.
|
||||
ChannelDto exists to decouple transport contracts from domain models; by consolidating channel metadata into a single, serializable shape, it enables stable APIs and easier versioning. The `IsSystem` flag allows distinguishing system channels (like announcements) from user-created ones, while `CreatedAt` helps clients sort or display recency.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var channel = new ChannelDto(
|
||||
Id: Guid.NewGuid(),
|
||||
Name: "general",
|
||||
Topic: "General discussion",
|
||||
IsPublic: true,
|
||||
MessageCount: 482,
|
||||
CreatedAt: DateTimeOffset.UtcNow,
|
||||
IsProtected: false,
|
||||
IsEncrypted: true,
|
||||
IsSystem: false
|
||||
Guid.NewGuid(),
|
||||
"general",
|
||||
"General discussion",
|
||||
true,
|
||||
128,
|
||||
DateTimeOffset.UtcNow
|
||||
);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Topic may be null; consumers should handle absence of a topic gracefully.
|
||||
- ChannelDto is immutable; to derive a modified version use the with expression (e.g., channel with { Name = "new-name" }).
|
||||
- Boolean flags default to false when omitted, so explicit values should reflect the actual channel semantics.
|
||||
- Topic is nullable; consumers should handle `null` before displaying a topic, or provide a fallback.
|
||||
|
||||
---
|
||||
|
||||
@@ -186,14 +161,13 @@ public record ChannelMetaDto(
|
||||
| `CreatedAt` | `DateTimeOffset` | — |
|
||||
|
||||
|
||||
ChannelMetaDto is a data transfer object that captures human-facing metadata for a chat channel as surfaced by the /meta command. It exposes the channel's identity (Id), presentation (Name), optional description (Topic), security properties (IsEncrypted, IsProtected), participation metrics (MessageCount, UniqueUserCount), a best-effort size estimate of content (EstimatedSizeBytes), and the creation timestamp (CreatedAt). For encrypted channels, the server retains counts, timestamps, and blob sizes but cannot read the content itself; EstimatedSizeBytes is the sum of stored attachment blob sizes plus message text length, so it is an estimate rather than an exact on-disk total.
|
||||
ChannelMetaDto is an immutable data transfer object that presents a concise, human-facing snapshot of a channel's metadata (the `/meta` command) to clients. It exposes the channel's identity (`Id`, `Name`), optional `Topic`, security/status flags (`IsEncrypted`, `IsProtected`), audience metrics (`MessageCount`, `UniqueUserCount`), and an estimated on-disk footprint (`EstimatedSizeBytes`), which is the sum of stored attachment blob sizes plus message text length and thus an estimate rather than an exact total. For encrypted channels the server still knows these figures — counts, timestamps, and stored blob sizes — even though it cannot read the content itself. The `CreatedAt` field records when the channel was created.
|
||||
|
||||
## Remarks
|
||||
This immutable record serves as a stable, client-facing contract that decouples internal storage from UI rendering. By aggregating these fields, it enables lightweight channel listings and meta views without exposing message content, while still providing enough information to gauge activity and scope.
|
||||
ChannelMetaDto serves as a stable, read-only contract between server and clients for channel overviews. As an immutable `record`, it guarantees value-based equality and prevents accidental mutation, which simplifies caching and change detection in UI layers. The metadata it carries—identity, topic, security flags, counts, and size—supports efficient rendering of channel lists and summaries without exposing the channel contents.
|
||||
|
||||
## Notes
|
||||
- Topic may be null; clients should handle absence gracefully when rendering.
|
||||
- EstimatedSizeBytes is an approximation; the value may drift as new messages or attachments are added.
|
||||
- The `EstimatedSizeBytes` is an estimate (sum of stored attachment blob sizes and message text length); it is not an exact on-disk size and can drift as content changes.
|
||||
|
||||
---
|
||||
|
||||
@@ -206,7 +180,7 @@ public record CreateChannelRequest(
|
||||
string Name,
|
||||
string? Topic = null,
|
||||
bool IsPublic = true,
|
||||
string? [REDACTED:CONNECTION_STRING_PASSWORD]
|
||||
string? Password = null,
|
||||
string? EncryptionSalt = null,
|
||||
string? WrappedRoomKey = null)
|
||||
```
|
||||
@@ -218,20 +192,20 @@ public record CreateChannelRequest(
|
||||
| `Name` | `string` | — |
|
||||
| `Topic` | `string?` | `null` |
|
||||
| `IsPublic` | `bool` | `true` |
|
||||
| `EncryptionSalt` | `string? [REDACTED:CONNECTION_STRING_PASSWORD]
|
||||
string?` | `null` |
|
||||
| `Password` | `string?` | `null` |
|
||||
| `EncryptionSalt` | `string?` | `null` |
|
||||
| `WrappedRoomKey` | `string?` | `null` |
|
||||
|
||||
|
||||
Represents the payload for creating a new chat channel. It encapsulates the channel name, an optional topic, a visibility flag, and optional cryptographic data used to secure channel communications. A redacted credentials field stands in for a sensitive connection password and should be supplied securely at runtime rather than stored or logged.
|
||||
The `CreateChannelRequest` is an immutable data transfer object that encapsulates all parameters needed to create a new chat channel. It requires a `Name` and exposes optional settings including `Topic`, whether the channel is public via `IsPublic` (default true), and optional security fields such as `Password`, `EncryptionSalt`, and `WrappedRoomKey` used for encrypted channel setup. Use this record when issuing a channel creation operation so that all related options are passed as a single, strongly-typed payload rather than a loose collection of parameters.
|
||||
|
||||
## Remarks
|
||||
This record is an immutable value object intended to be used as a single payload passed from client to API for channel creation. It coalesces related creation parameters in one place, facilitating validation and transport across layers while remaining independent of any particular persistence or network protocol. The redacted password field highlights a security concern: avoid exposing credentials in logs or UI surfaces; handle it through secure channels only.
|
||||
By collecting channel creation options into a single `CreateChannelRequest`, the boundary between API inputs and domain logic is cleanly expressed. The defaults on `IsPublic` and the optional nature of the other fields enable flexible requests while preserving a stable, serializable contract across process boundaries. This abstraction also makes future extension safer: new optional settings can be added without altering existing call sites.
|
||||
|
||||
## Notes
|
||||
- Name is required; Topic, IsPublic, EncryptionSalt, WrappedRoomKey are optional with sensible defaults (Topic = null, IsPublic = true, EncryptionSalt = null, WrappedRoomKey = null).
|
||||
- IsPublic defaults to true; set to false to create a private channel.
|
||||
- Sensitive fields (the redacted password) must be handled securely; avoid logging or exposing the value in logs or UI.
|
||||
- Do not log sensitive fields: avoid writing `Password`, `EncryptionSalt`, or `WrappedRoomKey` to logs or telemetry.
|
||||
- Nullable fields imply validation; ensure meaningful values before persisting or acting on them.
|
||||
- If `IsPublic` is false, consider validating that a `Password` is provided for access control; enforce this at the API or domain layer if required.
|
||||
|
||||
---
|
||||
|
||||
@@ -261,25 +235,28 @@ public record EmbedDto(
|
||||
| `ThemeColor` | `string?` | `null` |
|
||||
|
||||
|
||||
EmbedDto is a lightweight, immutable data carrier for the metadata needed to render a rich embed in chat messages. As a C# record, it provides value-based equality and convenient construction, making it ideal for transporting embed information across layers without mutating state. It carries optional metadata fields (SiteName, Title, Description, ImageAscii, ThemeColor) and requires a Url that points to the embed resource.
|
||||
EmbedDto is an immutable data container used to carry the metadata needed to render a rich embed, such as in chat messages or UI panels. It groups the surface data for an embed: `SiteName`, `Title`, `Description`, `ImageAscii`, `Url`, and an optional `ThemeColor`, so callers can supply a complete embed definition in a single object.
|
||||
|
||||
## Remarks
|
||||
This abstraction centralizes all embed-related data into a single contract, decoupling embedding details from other message payloads. By using a record, it gains structural equality and easy pattern matching, which simplifies testing and usage in render pipelines. The optional ThemeColor guides UI theming, while ImageAscii allows lightweight, ASCII-based previews when a graphical asset is unavailable.
|
||||
As a `record`, `EmbedDto` provides value-based equality and supports deconstruction, making it straightforward to compare embeddings or pattern-match in rendering logic. It serves as a clean boundary between data authors and renderers: producers populate an `EmbedDto`, consumers render an embed from its fields without needing to understand surrounding domain.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var embed = new EmbedDto(
|
||||
SiteName: "Aurora Gallery",
|
||||
Title: "Landscape Preview",
|
||||
Description: "A sample landscape embed",
|
||||
ImageAscii: "[ASCII_ART]",
|
||||
Url: "https://example.org/embeds/landscape",
|
||||
ThemeColor: "#3366FF");
|
||||
SiteName: "EchoHub",
|
||||
Title: "Welcome",
|
||||
Description: "A friendly hello from EchoHub.",
|
||||
ImageAscii: " ___ \n (o o) \n \_/ ",
|
||||
Url: "https://echohub.example",
|
||||
ThemeColor: "#4B8BBE"
|
||||
);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- All fields except Url are optional, so a minimal EmbedDto can be created with just the Url.
|
||||
- Being a record, EmbedDto is immutable and supports with-expressions to create modified copies without changing the original instance.
|
||||
- `ThemeColor` is optional; omit it to use a default theming.
|
||||
- `Url` is required; ensure it is a valid URL to enable link previews.
|
||||
- Because `EmbedDto` is a `record`, two instances with identical field values compare equal.
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -309,25 +286,7 @@ public record JoinChannelResult(
|
||||
| `WrappedRoomKey` | `string?` | `null` |
|
||||
|
||||
|
||||
JoinChannelResult is a value object that conveys the outcome of attempting to join a chat channel. It exposes whether the operation succeeded, provides the channel's message history for immediate rendering, and carries optional security-related data (password requirement, encryption salt, and wrapped room key) that consumers can act on after the join completes.
|
||||
|
||||
## Remarks
|
||||
JoinChannelResult centralizes all information produced by a join attempt, keeping the caller decoupled from the join logic. By pairing a success flag with the History and optional security fields, it supports both happy-path UI rendering and encrypted or password-protected channels without additional payloads. The inclusion of EncryptionSalt and WrappedRoomKey suggests a workflow where the client may fetch or negotiate encryption material as part of joining, rather than as a separate round-trip.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Successful join with history
|
||||
List<MessageDto> history = new List<MessageDto>();
|
||||
var result = new JoinChannelResult(true, history);
|
||||
|
||||
// Join that requires a password and includes encryption material
|
||||
var secured = new JoinChannelResult(true, history, PasswordRequired: true, EncryptionSalt: \"salt123\", WrappedRoomKey: \"wrappedKey\");
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Error is typically non-null only when Success is false; use it to surface the failure reason to the user.
|
||||
- EncryptionSalt and WrappedRoomKey are meaningful only for encrypted or password-protected channels; they may be null in plain channels.
|
||||
- History should be treated as the initial set of messages to render immediately after a join; it may be empty in failure scenarios or when a channel has no prior messages.
|
||||
Represents the outcome of a join-channel operation as a `JoinChannelResult` type. It exposes a `bool` `Success` flag, a `List<MessageDto>` `History` of messages retrieved for the channel, and optional metadata including a `string?` `Error`, a `bool` `PasswordRequired`, and optional encryption data (`string?` `EncryptionSalt`, `string?` `WrappedRoomKey`).
|
||||
|
||||
---
|
||||
|
||||
@@ -365,15 +324,13 @@ public record MessageDto(
|
||||
| `ReplyTo` | `ReplyRefDto?` | `null` |
|
||||
|
||||
|
||||
MessageDto is an immutable data transfer object that captures the essential details of a chat message as it moves across the EchoHub chat API surface. Implemented as a C# record, it provides value-based equality and straightforward construction for message data, making it ideal for serialization and transport between layers (e.g., API, client, and service boundaries). The object aggregates core message data such as Id, Content, SenderUsername, ChannelName, and SentAt, while also supporting optional enhancements like Attachments and Embeds, a human-friendly SenderDisplayName, and a ReplyTo reference for threaded conversations. This shape keeps message-related concerns contained in a single DTO without leaking domain internals, enabling predictable data contracts for consumers.
|
||||
Represents a chat message as a data contract used by the chat API. It captures the message `Id`, the textual `Content`, and author info (`SenderUsername`, optional `SenderNicknameColor`, optional `SenderDisplayName`), the `ChannelName`, and the `SentAt` timestamp. Optional `Attachments` and `Embeds` support rich content, while `ReplyTo` references a prior message.
|
||||
|
||||
## Remarks
|
||||
This symbol serves as a boundary object that encapsulates a complete chat message payload, including optional media and UI hints. By composing AttachmentDto and EmbedDto, it allows rich messages to travel without forcing callers to depend on internal domain types. The use of a record emphasizes that MessageDto represents a snapshot of message data at a point in time; consumers should treat instances as immutable and, if changes are needed, create new instances. The presence of optional fields (SenderNicknameColor, Attachments, Embeds, SenderDisplayName, ReplyTo) reflects real-world variability in messaging scenarios (e.g., plain text messages, media-enabled messages, or replies).
|
||||
This DTO is designed as a transport-friendly aggregation of message data, suitable for serialization across clients and services. By referencing the dedicated `AttachmentDto` and `EmbedDto` types, it remains extensible for rich content, and its optional fields (`Attachments`, `Embeds`, `ReplyTo`, `SenderNicknameColor`, `SenderDisplayName`) allow the same shape to cover both simple and feature-rich messages.
|
||||
|
||||
## Notes
|
||||
- Attachments and Embeds may be null; downstream code should handle nulls or default to empty collections to avoid null reference errors.
|
||||
- SenderNicknameColor and SenderDisplayName are optional UI hints and may be absent; consumers should gracefully handle missing values.
|
||||
- ReplyTo is optional and only populated for messages that are replies to another message; check for null before accessing related data.
|
||||
- `Attachments` and `Embeds` may be `null`; treat them as empty sequences when rendering or iterating.
|
||||
|
||||
---
|
||||
|
||||
@@ -399,27 +356,13 @@ public record RekeyChannelRequest(
|
||||
| `NewWrappedRoomKey` | `string` | — |
|
||||
|
||||
|
||||
Passphrase change for an encrypted channel: the client proves knowledge of the old passphrase (old auth key), then supplies the re-wrapped room key under the new one.
|
||||
|
||||
This RekeyChannelRequest is a data transfer object used to perform a channel rekey. It carries the old password to prove knowledge of the current key, the new password and its salt, and the re-wrapped room key to be used under the new credentials.
|
||||
The `RekeyChannelRequest` record represents the data the client sends to request a rekey of an encrypted channel. It conveys knowledge of the current passphrase (via `OldPassword`) and the new credentials and wrapped key to apply (via `NewPassword`, `NewEncryptionSalt`, and `NewWrappedRoomKey`).
|
||||
|
||||
## Remarks
|
||||
This type serves as a single payload boundary in the channel rekey workflow, encapsulating all data required to authenticate the existing context and establish a new encryption context for the room. Being a record enforces immutability and provides straightforward value-based equality, which simplifies testing and auditing of rekey requests. It acts as a contract between the client and server for the rotation of the room key tied to a new passphrase.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var request = new RekeyChannelRequest(
|
||||
OldPassword: "old-passphrase",
|
||||
NewPassword: "new-passphrase",
|
||||
NewEncryptionSalt: "salt-42",
|
||||
NewWrappedRoomKey: "BASE64_WRAPPED_ROOM_KEY"
|
||||
);
|
||||
```
|
||||
This DTO enables the server to verify the client's possession of the existing auth key while atomically applying new encryption material in a single operation. It decouples the client's input from the rekeying logic, allowing validation, auditing, and rollback policies to be applied at the server boundary.
|
||||
|
||||
## Notes
|
||||
- Do not log or expose OldPassword, NewPassword, or NewWrappedRoomKey; treat them as highly sensitive and avoid telemetry.
|
||||
- NewEncryptionSalt should be a cryptographically strong, per-operation salt generated by a secure RNG; do not reuse salts.
|
||||
- This object represents a single rekey operation and should not be reused for multiple independent requests.
|
||||
- Do not log `OldPassword` or `NewPassword`; treat these values as ephemeral and ensure transport-layer secrecy.
|
||||
|
||||
---
|
||||
|
||||
@@ -443,23 +386,13 @@ public record ReplyRefDto(
|
||||
| `Content` | `string` | — |
|
||||
|
||||
|
||||
ReplyRefDto is a compact, immutable data transfer object that identifies the message a user is replying to. It carries the target message's ID, the original sender's username, and the Content of that message as transmitted over the network, enabling clients and services to render contextual reply previews and preserve the reply's linkage. Content is treated exactly like ordinary message content on the wire: transport-encrypted, and for end-to-end encrypted rooms it is room ciphertext the client must decrypt (the server truncates only plaintext snippets). If the original message has been deleted, the related MessageDto will be null; the reply reference remains a valid anchor for rendering the reply context.
|
||||
Represents a reference to the message that a reply targets. It carries the target message's identifier (`MessageId`), the original sender's username (`SenderUsername`), and the reply content (`Content`), which is treated exactly like message content on the wire: transport-encrypted, and for end-to-end encrypted rooms it is room ciphertext the client must decrypt (the server truncates only plaintext snippets). Null on a `MessageDto` when the original message no longer exists.
|
||||
|
||||
## Remarks
|
||||
Represents the reply target in chat threads as a minimal reference, decoupling the UI payload from the full MessageDto. It ensures consistent wire-format handling across plaintext and end-to-end encrypted rooms, while allowing clients to display reply context without requiring the entire original payload.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var reference = new ReplyRefDto(
|
||||
MessageId: Guid.Parse("3f2504e0-4f89-11d3-9a0c-0305e82c3301"),
|
||||
SenderUsername: "alice",
|
||||
Content: "Hello world"
|
||||
);
|
||||
```
|
||||
ReplyRefDto acts as a compact pointer that preserves the link between a reply and its target message without duplicating payloads. It separates transport- and encryption-aware handling from display logic, enabling clients to decrypt or render the referenced content while the server retains plaintext-only signals. In threaded chat UX, this symbol supports rendering reply previews and context for the target message.
|
||||
|
||||
## Notes
|
||||
- Content is the exact on-wire representation of the referenced message; it may be ciphertext in encrypted rooms and should be decrypted by the client when applicable.
|
||||
- If the original message has been deleted, the MessageDto may be null, but the ReplyRefDto still anchors the reply context for UI rendering; callers should handle potential missing referenced data gracefully.
|
||||
- Be aware that `Content` might be ciphertext in encrypted rooms and may not be human-readable until decrypted; do not display it as plaintext without decryption.
|
||||
|
||||
---
|
||||
|
||||
@@ -479,17 +412,7 @@ public record SendMessageRequest(string ChannelName, string Content)
|
||||
| `Content` | `string` | — |
|
||||
|
||||
|
||||
SendMessageRequest is an immutable data transfer object that encapsulates the information required to send a message to a specific chat channel. It combines the ChannelName and the Content to be delivered so transport or messaging layers can operate on a single payload. As a record, it provides value-based equality and easy cloning with the with-expression, which helps when constructing variations without mutating existing instances.
|
||||
|
||||
## Remarks
|
||||
|
||||
Acts as a boundary contract between UI/API layers and the messaging service. The record's immutability and structural equality make it reliable for logging, caching, and test assertions. Validation rules or routing decisions should live outside this DTO; this type should not perform domain validation. Its simple two-string shape also makes it friendly to common serialization mechanisms, enabling straightforward transport across boundaries.
|
||||
|
||||
## Notes
|
||||
|
||||
- No validation is performed by the type itself; ensure ChannelName and Content conform to domain rules before sending.
|
||||
- The type is immutable; to modify, create a new instance (or use the with-expression) rather than mutating an existing one.
|
||||
- Suitable for serialization; the plain two-property shape works well with JSON, XML, or other common serializers.
|
||||
SendMessageRequest is a simple, immutable data carrier (record) that encapsulates the channel to which a message should be sent and the message content itself. Use this `SendMessageRequest` when you need to issue a message to a specific chat channel, providing both the `ChannelName` and the `Content` in a single object rather than passing multiple parameters or ad-hoc structures.
|
||||
|
||||
---
|
||||
|
||||
@@ -508,18 +431,18 @@ public record SendUrlRequest(string Url)
|
||||
| `Url` | `string` | — |
|
||||
|
||||
|
||||
SendUrlRequest is a tiny, immutable URL payload represented as a C# record. It’s intended for scenarios where a URL must be passed across boundaries in a strongly-typed way rather than as a raw string, gaining value-based equality and straightforward deconstruction in the process.
|
||||
SendUrlRequest is a minimal value object used to convey a URL as a request payload. As a `record` with a single `string Url` positional parameter, it provides value-based equality and immutability, making it ideal for passing URL data through layers or across API boundaries instead of threading raw `string` values.
|
||||
|
||||
## Remarks
|
||||
Using a record for this DTO ensures immutability, value-based equality, and built-in deconstruction. This makes SendUrlRequest a natural fit for messaging or API surfaces that expect a dedicated URL payload type instead of raw strings, reducing the chance of accidental mutation and enabling pattern-based handling of the URL payload.
|
||||
`SendUrlRequest` serves as a precise contract for operations that require a URL. Its `record` semantics ensure structural equality and allow easy deconstruction; by encapsulating the `Url` property, it clarifies intent and supports serialization as a simple payload.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var request = new SendUrlRequest("https://example.com");
|
||||
var req = new SendUrlRequest("https://example.com");
|
||||
```
|
||||
|
||||
## Notes
|
||||
- No validation is performed inside the type; ensure the URL is valid at the call site or in downstream handlers.
|
||||
- No URL validation is performed by this type; validate the URL in the caller or service layer before processing.
|
||||
|
||||
---
|
||||
|
||||
@@ -538,26 +461,7 @@ public record UpdateTopicRequest(string? Topic)
|
||||
| `Topic` | `string?` | — |
|
||||
|
||||
|
||||
Represents a request to update the topic of a chat or conversation. This immutable record acts as a lightweight DTO that carries an optional Topic value; use it when issuing an update operation—provide a non-null Topic to set a new topic, or pass null to indicate that the topic should be cleared or left unchanged by the API, depending on server semantics.
|
||||
|
||||
## Remarks
|
||||
This abstraction communicates the intent of updating only the topic field, leveraging a nullable Topic to express optionality. The record nature provides value-based equality and simple construction, and you can create modified copies with the with-expression (e.g., updating the Topic while preserving other fields in a derived request).
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Set a new topic
|
||||
var request = new UpdateTopicRequest("New Topic");
|
||||
|
||||
// Clear the topic (behavior depends on the API)
|
||||
var clearRequest = new UpdateTopicRequest(null);
|
||||
|
||||
// Create a modified copy
|
||||
var updated = request with { Topic = "Updated Topic" };
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Topic is nullable; serialization and API behavior may vary—null may mean "no change" or "clear" depending on the endpoint.
|
||||
- Because this is a record, instances are immutable; use the with-expression to derive variations without mutating the original.
|
||||
Represents a request payload to update a topic, encapsulating an optional `Topic` value. As a positional-record, it provides an immutable, lightweight data carrier that callers populate with the new topic string when issuing an update to a chat's topic.
|
||||
|
||||
---
|
||||
|
||||
@@ -587,12 +491,14 @@ public record UserDto(
|
||||
| `LastSeenAt` | `DateTimeOffset` | — |
|
||||
|
||||
|
||||
UserDto is a lightweight, immutable data transfer object that conveys a user's identity and presence-related attributes across boundaries such as API responses or UI bindings. It aggregates the user's unique identifier, login name, optional display name and nickname color, current status, and the last seen timestamp so clients can present a consistent and responsive user summary.
|
||||
`UserDto` is an immutable data transfer object that carries a concise snapshot of a user for chat workflows. It exposes the user’s `Id` (`Guid`), `Username`, optional `DisplayName` and `NicknameColor`, the current `Status` ([`UserStatus`](../Models/UserStatus.cs.md)), and the `LastSeenAt` timestamp (`DateTimeOffset`). Use this DTO when returning or transferring lightweight user data across API boundaries or UI layers instead of exposing full domain entities.
|
||||
|
||||
## Remarks
|
||||
As a record, UserDto benefits from value-based equality and structural immutability, making it easy to compare user summaries and safely pass them around without worrying about accidental mutation. DisplayName and NicknameColor are optional to accommodate scenarios where presentation details are missing. LastSeenAt and Status provide presence information that can drive UI indicators and sorting.
|
||||
Being a `record` with positional parameters, `UserDto` benefits from value-based equality and convenient deconstruction, which is helpful for tests and payload comparisons. The nullable fields `DisplayName` and `NicknameColor` reflect optional user profile data; readers should handle the possibility of missing values gracefully.
|
||||
|
||||
## Notes
|
||||
- DisplayName and NicknameColor are nullable; null should be treated as absent presentation data.
|
||||
- Nullable fields require null checks during consumption.
|
||||
- Being immutable, modifying a `UserDto` requires creating a new instance (e.g., via a `with` expression).
|
||||
- The `LastSeenAt` is a `DateTimeOffset`; ensure consistent time zone handling across systems.
|
||||
|
||||
---
|
||||
@@ -32,14 +32,21 @@ public record ApiResponse(bool Success, string? Message = null, List<string>? Er
|
||||
| `Errors` | `List<string>?` | `null` |
|
||||
|
||||
|
||||
ApiResponse is a lightweight data transfer object used to convey the outcome of an operation. It carries a required Success flag and optional Message and Errors to provide feedback and diagnostics to callers.
|
||||
Represents a standard API outcome as a `record` with a `bool` `Success`, an optional `string?` [`Message`](../Models/Message.cs.md), and an optional `List<string>?` `Errors`. Use `ApiResponse` to package the result of API operations or service methods into a single, strongly-typed object for consistent client consumption instead of scattering boolean flags and messages across code.
|
||||
|
||||
## Remarks
|
||||
Used as a common response shape across service boundaries to avoid ad-hoc return types. The primary purpose is to separate control flow (success/failure) from payload, facilitating simple success messaging and error propagation. Be mindful that Errors is a `List<string>`, which remains mutable if the same instance is shared; convert to a read-only collection or copy before returning to external consumers.
|
||||
By centralizing outcome data in `ApiResponse`, callers can handle success/failure logic in a uniform way and avoid ad-hoc boolean checks scattered through the code. The `Errors` collection is intended for granular, field-level validation messages that the client can display; the [`Message`](../Models/Message.cs.md) offers a concise summary, while `Success` drives flow control.
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
var success = new ApiResponse(true);
|
||||
var failure = new ApiResponse(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" });
|
||||
```
|
||||
|
||||
## Notes
|
||||
- The Errors property is a mutable `List<string>`—wrap or copy it if you intend to preserve a fixed snapshot when returning to consumers.
|
||||
- Message may be null; supply a default user-friendly message or handle nulls in UI/logging.
|
||||
- The `Errors` property is a mutable `List<string>`; external mutation is possible. If you need true immutability, consider using `IReadOnlyList<string>` or an immutable collection.
|
||||
- When `Success` is true, you may omit [`Message`](../Models/Message.cs.md) and `Errors` or set them as appropriate; when `Success` is false, provide a meaningful [`Message`](../Models/Message.cs.md) and optionally populate `Errors` to detail issues.
|
||||
|
||||
---
|
||||
|
||||
@@ -61,27 +68,22 @@ public record ApiResponse<T>(bool Success, string? Message = null, List<string>?
|
||||
| `Data` | `T?` | `default` |
|
||||
|
||||
|
||||
`ApiResponse<T>` is a generic wrapper you return from API methods to convey a successful outcome, an optional human-friendly message, and a payload of type T, along with any per-call errors. Use this pattern when you want a consistent contract for success, messaging, and data across endpoints rather than returning raw data alone.
|
||||
A generic wrapper for operation results that standardizes API responses. It indicates success with `Success` and carries an optional [`Message`](../Models/Message.cs.md), a `List<string>` named `Errors` for validation or processing issues, and an optional `Data` payload of type `T`.
|
||||
|
||||
## Remarks
|
||||
`ApiResponse<T>` is an immutable value type (a record with a primary constructor) that standardizes how results are communicated. It separates the data payload from status information, allowing clients to inspect Success, Message, and Errors independently from Data. Because Message and Errors are optional, responses can remain concise for successful operations while still providing rich error detail when needed.
|
||||
|
||||
This abstraction decouples the shape of a successful response from the actual data, enabling consistent error handling and client-side parsing across services. By returning `ApiResponse<T>` from operations, you centralize how success, messages, and validation details are conveyed, which simplifies cross-cutting concerns like localization and error translation.
|
||||
|
||||
## Example
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
// Successful response with data
|
||||
var success = new ApiResponse<string>(true, "Operation completed", null, "payload-data");
|
||||
|
||||
// success with data
|
||||
var result = new ApiResponse<string>(true, "Operation completed", null, "payload");
|
||||
|
||||
// error with details
|
||||
var failure = new ApiResponse<string>(false, "Validation failed", new List<string> { "Email is invalid" }, null);
|
||||
// Failed response with errors
|
||||
var failure = new ApiResponse<string>(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" }, null);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Message and Errors are nullable; always check Success before relying on these fields, and provide defaults if you need non-null output.
|
||||
- `ApiResponse<T>` is immutable; to modify it, use a with-expression to create a copy (e.g., var updated = result with { Data = newData };).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## ChannelOperationResult
|
||||
@@ -107,14 +109,13 @@ public record ChannelOperationResult(ChannelDto? Channel, ChannelError? Error, s
|
||||
| `ErrorMessage` | `string?` | — |
|
||||
|
||||
|
||||
ChannelOperationResult is a lightweight result wrapper used by channel-creation/lookup operations to return either a ChannelDto on success or an error descriptor on failure. Callers typically inspect IsSuccess and then access Channel or Error/ErrorMessage, using the static factories to produce a well-formed result rather than constructing it directly.
|
||||
ChannelOperationResult is an immutable wrapper that conveys the outcome of a channel-related operation. It either carries a [`ChannelDto`](ChatDtos.cs.md) when the operation succeeds or a `ChannelError` with an `ErrorMessage` when it fails; the static helpers `Success` and `Fail` make the intent explicit when constructing results.
|
||||
|
||||
## Remarks
|
||||
It captures the outcome of channel-oriented operations in a single, immutable value, reducing the need for exception-based control flow. By pairing either a Channel with no error or an Error with a message, it forces consumers to handle both success and failure paths in a uniform way. It complements the ChannelDto and ChannelError types by providing a minimal, self-describing container that can be passed through layers without leaking implementation details.
|
||||
ChannelOperationResult uses a C# `record` to express a simple, value-like outcome. It centralizes success/failure information for channel-oriented operations, enabling uniform error handling and reducing scattered null-checks. Consumers should inspect `IsSuccess` before accessing [`Channel`](../Models/Channel.cs.md); when `IsSuccess` is true, [`Channel`](../Models/Channel.cs.md) is non-null, and when false, `Error` and `ErrorMessage` describe the failure.
|
||||
|
||||
## Notes
|
||||
- Prefer the static factories to create instances to preserve the intended invariant that a result carries either a Channel or an error. The public constructor can produce degenerate states if misused.
|
||||
- The ErrorMessage is optional; provide a descriptive message to aid debugging when using Fail.
|
||||
- Prefer constructing via `ChannelOperationResult.Success(...)` or `ChannelOperationResult.Fail(...)` rather than the primary constructor to preserve the invariant that a successful result has a non-null [`Channel`](../Models/Channel.cs.md) and a failed result has non-null `Error`.
|
||||
|
||||
---
|
||||
|
||||
@@ -134,15 +135,10 @@ public record ErrorResponse(string Error, string? Detail = null)
|
||||
| `Detail` | `string?` | `null` |
|
||||
|
||||
|
||||
ErrorResponse is a small, immutable data transfer object used to convey error information from the server to API clients. Implemented as a C# record with two positional properties, Error and Detail, it carries a concise error identifier or message and optional supplemental details. Use it when standardizing error payloads across API endpoints or error-handling middleware that wants to provide a consistent error shape.
|
||||
Encapsulates a standardized error payload with a mandatory `Error` code and an optional `Detail` string for extra context. As a `record`, it is immutable by design and supports value-based equality and deconstruction, which makes it ideal for returning a single, comparable error object from APIs or services. Use this type to produce consistent, serializable error information across the system.
|
||||
|
||||
## Remarks
|
||||
Using a record provides value-based equality and immutability, making ErrorResponse a stable payload that is easy to compare in tests and to clone with modifications via with-expressions. The Error field represents a short error code or message, while Detail offers optional, human-friendly context. This type is intended to be reused across API boundaries, ensuring clients receive a uniform error shape.
|
||||
|
||||
## Notes
|
||||
- Avoid leaking sensitive internals in Error; prefer stable, client-friendly codes or messages.
|
||||
- Detail is nullable; when null, serialization may omit the property depending on serializer settings.
|
||||
- As a DTO, this record should be produced by a dedicated error-handling path rather than constructed manually in business logic.
|
||||
Centralizes the error payload shape to ensure all error responses share a single contract. The optional `Detail` field provides human-friendly context without breaking clients that only inspect the `Error` code. Because it is a `record`, it naturally supports comparisons and pattern matching when handling error responses.
|
||||
|
||||
---
|
||||
|
||||
@@ -164,24 +160,13 @@ public record PaginatedResponse<T>(List<T> Items, int Total, int Offset, int Lim
|
||||
| `Limit` | `int` | — |
|
||||
|
||||
|
||||
Represents a paginated result set for a collection of items of type T. It bundles the items for the current page together with paging metadata (Total, Offset, and Limit), enabling consumers to render pages and request subsequent pages without fetching the entire dataset. Use `PaginatedResponse<T>` when an API or service returns a slice of a larger collection and you need to convey both the page content and the overall size.
|
||||
Represents a paged result set for a collection of items of type `T`. It bundles the current page of data (`Items`) with paging metadata: the total item count (`Total`), the starting offset (`Offset`), and the page size limit (`Limit`). This shape is used by APIs that support paging to convey both the data and how to fetch additional pages; the use of a `record` provides value-based equality and immutability for API responses.
|
||||
|
||||
## Remarks
|
||||
This generic DTO unifies paging across different endpoints by pairing a page of items with metadata describing the total size of the set and the paging window (Offset and Limit). Consumers can derive the total number of pages and navigate accordingly, without duplicating paging logic.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var page = new PaginatedResponse<int>(
|
||||
Items: new List<int> { 1, 2, 3 },
|
||||
Total: 10,
|
||||
Offset: 0,
|
||||
Limit: 3
|
||||
);
|
||||
```
|
||||
Using a `record` for `PaginatedResponse<T>` gives value-based equality and an immutable data shape, which makes it natural for transporting paging results across boundaries. It centralizes both the data (`Items`) and its paging metadata (`Total`, `Offset`, `Limit`) in a single coherent DTO, reducing the risk of mismatch between data and paging state when consumed by clients or other services.
|
||||
|
||||
## Notes
|
||||
- The Items property is a `List<T>`, which is mutable. Mutating the list after construction will affect the PaginatedResponse instance. If you require immutability of the collection, consider exposing `ReadOnlyCollection<T>` or `IReadOnlyList<T>` instead of `List<T>`, or wrap the list before returning.
|
||||
- Because `PaginatedResponse<T>` is a record, the wrapper itself uses value-based equality, but the `List<T>` contained in Items is compared by reference. Two instances with equal contents but different `List<T>` instances will not compare equal.
|
||||
- The `Items` collection is a `List<T>`, which is mutable. If you require immutability guarantees, wrap it in a read-only collection or clone the list before exposure.
|
||||
|
||||
---
|
||||
|
||||
@@ -208,14 +193,13 @@ public record UserOperationResult(UserProfileDto? User, UserError? Error, string
|
||||
| `ErrorMessage` | `string?` | — |
|
||||
|
||||
|
||||
Represents the outcome of a user-related operation: it either carries a UserProfileDto for success or a UserError and an ErrorMessage for failure. Use IsSuccess to branch on the result and create instances via Success(user) for success or Fail(error, message) for failure.
|
||||
An immutable result wrapper for user-related operations. It encapsulates either a [`UserProfileDto`](ProfileDtos.cs.md) payload via [`User`](../Models/User.cs.md) on success, or a `UserError` and a diagnostic `ErrorMessage` on failure. Use the static factories `Success` and `Fail` to construct consistent results, and check `IsSuccess` to decide how to proceed.
|
||||
|
||||
## Remarks
|
||||
This abstraction uses a record with nullable payload fields to model a simple Result pattern without introducing a separate discriminated union. It provides a single return type across methods that can either yield a user profile or fail with details, enabling concise consumer code that checks IsSuccess first. Because User is nullable when the result is a failure, and because Error and ErrorMessage are null on success, callers should guard access to User unless IsSuccess is true. The helper methods ensure the invariant that a successful result always carries a user while a failure carries an error and message.
|
||||
By encapsulating both success payload and failure details into a single value, this symbol standardizes how user-operation results are communicated. Callers check `IsSuccess` and then access either the [`User`](../Models/User.cs.md) payload or the `Error`/`ErrorMessage` to react. Because it is a `record`, equality is based on its contents, which helps tests and caching rely on value semantics.
|
||||
|
||||
## Notes
|
||||
- Read result.User only after confirming IsSuccess; otherwise the value may be null.
|
||||
- On failure, User will be null; consult Error and ErrorMessage for details.
|
||||
- Directly constructing with a mismatched state (for example, a non-null `Error` but a null or missing `ErrorMessage`) can create inconsistent results; prefer the provided factories to enforce the invariant that success results include a [`User`](../Models/User.cs.md) and no error, while failures include an `Error` and an `ErrorMessage`.
|
||||
|
||||
---
|
||||
|
||||
@@ -235,14 +219,11 @@ public enum ChannelError
|
||||
```
|
||||
|
||||
|
||||
ChannelError enumerates the discrete failure cases that can arise when managing channels in EchoHub. It provides a finite set of error codes so callers can distinguish invalid input, duplicates, missing resources, permission issues, and protected resources without resorting to free-form strings.
|
||||
ChannelError is an enum that enumerates the standard error conditions that may arise when working with channels in the `EchoHub` domain. It provides a typed set of failure reasons—`ValidationFailed`, `AlreadyExists`, `NotFound`, `Forbidden`, and `Protected`—to be returned by channel-related operations, enabling callers to branch on the specific cause and handle it uniformly rather than parsing strings.
|
||||
|
||||
## Remarks
|
||||
This enum lives in the DTO layer to convey precise failure reasons from service or repository operations to API clients. By centralizing channel-related errors, it enables consistent error handling, mapping to user-friendly responses, and easier client-side interpretation across create, update, and lookup workflows. The member names align with common REST/DTO conventions, reducing ambiguity when serializing and documenting API contracts.
|
||||
|
||||
## Notes
|
||||
- Changing the enum's members or their order can impact clients that serialize/deserialize error codes; treat it as a public contract.
|
||||
- If you enable numeric JSON serialization for enums, ensure the API contract documents the expected codes to avoid confusion.
|
||||
`ChannelError` centralizes the failure kinds that can occur during channel-related operations and is intended to be carried by DTOs that report operation results. It enables type-safe error handling, allowing callers to pattern-match on the exact failure (`ValidationFailed`, `AlreadyExists`, `NotFound`, `Forbidden`, `Protected`) and map them to appropriate responses without parsing human-generated messages. This separation of error kind from presentation keeps the API consistent as channel semantics evolve.
|
||||
|
||||
---
|
||||
|
||||
@@ -262,21 +243,12 @@ public enum UserError
|
||||
```
|
||||
|
||||
|
||||
Represents the set of user-related errors that can occur during authentication, registration, lookup, or other user-identity operations in the EchoHub DTO layer. This enum provides a typed, contract-friendly way to communicate failure modes from server to client, enabling centralized handling and consistent feedback without scattering string literals across the codebase.
|
||||
|
||||
Values include:
|
||||
- ValidationFailed: input data failed validation.
|
||||
- AlreadyExists: a resource with the given identifier already exists.
|
||||
- NotFound: the requested user or resource could not be found.
|
||||
- InvalidCredentials: credentials were invalid during authentication.
|
||||
- Banned: the user is banned from the system.
|
||||
The `UserError` enum defines the canonical set of failure conditions related to user accounts that may be surfaced by operations in the core DTO layer. Members include `ValidationFailed`, `AlreadyExists`, `NotFound`, `InvalidCredentials`, and `Banned`, each representing a distinct error scenario that downstream code can pattern-match to drive error responses and user messaging.
|
||||
|
||||
## Remarks
|
||||
By consolidating these common errors into a single enum, this abstraction decouples transport contracts from domain logic and supports uniform error mapping on the client. It simplifies UI messaging, and it allows the server to evolve its error vocabulary without changing method signatures.
|
||||
This enum centralizes common user-domain errors so that authentication, registration, and profile-management flows can share a consistent error-handling strategy. By codifying these cases in a single type, callers can translate domain failures into uniform API responses and UI messages without depending on implementation details.
|
||||
|
||||
## Notes
|
||||
- Be mindful of how the enum is serialized in API responses (numeric vs string); consider standardizing on string representations to avoid client breakage when new values are added.
|
||||
- Adding new values is a contract change; document and version the API accordingly, and ensure clients handle unknown values gracefully.
|
||||
- This enum is a DTO-level error vocabulary; do not encode domain exceptions here.
|
||||
- When mapping these errors to user-facing messages, avoid exposing sensitive internal details and rely on generic messaging driven by the enum value.
|
||||
|
||||
---
|
||||
@@ -25,21 +25,13 @@ public record CreateInviteRequest(int? MaxUses = null, int? ExpiresInHours = nul
|
||||
| `ExpiresInHours` | `int?` | `null` |
|
||||
|
||||
|
||||
This record serves as the payload for creating an invitation. It carries optional constraints that govern the invite: MaxUses limits how many times the invite can be redeemed, and ExpiresInHours determines how long the invite remains valid (in hours). When constructing the request, omit values you don’t want to constrain; null properties indicate the server should apply its defaults.
|
||||
Represents the request payload for creating an invite, carrying optional constraints for the invite. The nullable `MaxUses` and `ExpiresInHours` allow callers to omit constraints. As a `record`, it provides value-based equality and immutability, making it a convenient, typed carrier for API calls.
|
||||
|
||||
## Remarks
|
||||
Because CreateInviteRequest is a C# record, it provides value-based equality and immutable semantics, making it a reliable DTO for API calls and caching. The nullable properties express optional constraints without introducing separate flags, keeping the surface area small and expressive.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var request = new CreateInviteRequest(MaxUses: 5, ExpiresInHours: 24);
|
||||
```
|
||||
This type centralizes the concept of invite constraints and cleanly separates client request construction from business logic. It interoperates with the invite-creation pathway by encoding optional parameters as nullable properties, allowing the API to apply defaults when a field is null.
|
||||
|
||||
## Notes
|
||||
- Null on a property means no constraint; the API defaults apply.
|
||||
- Many serializers omit null fields; if the API requires an explicit indicator for "no constraint," ensure your serializer preserves the field or you configure it accordingly.
|
||||
- If you need to convey zero constraints explicitly, pass 0 (not null) for the respective property; null is not the same as zero.
|
||||
|
||||
- Null values indicate 'not specified' and will be treated as absent by the invite-creation endpoint; set only the fields you intend to constrain.
|
||||
|
||||
---
|
||||
|
||||
@@ -69,26 +61,9 @@ public record InviteDto(
|
||||
| `UseCount` | `int` | — |
|
||||
|
||||
|
||||
InviteDto is a small, transport-oriented representation of an invitation. It encapsulates the invitation code, the creator's username, the moment of creation, an optional expiry, and simple usage counters, making it suitable for API responses and inter-layer data transfers without revealing domain internals.
|
||||
InviteDto is an immutable data transfer object that carries the metadata for an invitation: the `Code`, the creator's username (`CreatedByUsername`), the creation time (`CreatedAt`), an optional expiration (`ExpiresAt`), and usage counters (`MaxUses` and `UseCount`). It is designed for transporting invitation data across application boundaries without behavior, making it easy to serialize, deserialize, and compare by value.
|
||||
|
||||
## Remarks
|
||||
As a record, InviteDto is immutable and uses value-based equality, which makes caching and comparisons straightforward. It decouples transport concerns from domain logic by presenting only the data clients need. The fields map directly to invitation semantics: Code is the token, CreatedByUsername and CreatedAt capture provenance, ExpiresAt denotes expiry (nullable means no expiry), and MaxUses/UseCount express the usage limits and current consumption.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var invite = new InviteDto(
|
||||
Code: "WELCOME-ABC123",
|
||||
CreatedByUsername: "admin",
|
||||
CreatedAt: DateTimeOffset.UtcNow,
|
||||
ExpiresAt: DateTimeOffset.UtcNow.AddDays(7),
|
||||
MaxUses: 5,
|
||||
UseCount: 0
|
||||
);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Null ExpiresAt means the invitation does not expire; ensure your validation logic accounts for that.
|
||||
- InviteDto is immutable; to reflect state changes (e.g., after a use), construct a new instance rather than mutating the existing one.
|
||||
- Use UTC times for CreatedAt/ExpiresAt to avoid timezone ambiguity.
|
||||
Because it is defined as a `record`, `InviteDto` benefits from value-based equality and structural immutability, ensuring that two invitations with the same data compare equal and that the payload remains unchanged after construction. The nullable `ExpiresAt` conveys that an invitation might have no expiration; consumers must treat a null as no expiry. The `MaxUses` together with `UseCount` enables the system to enforce limits at the boundary without embedding logic here. This symbol sits at the boundary between persistence, API contracts, and business logic, keeping the shape of invitation data consistent across layers.
|
||||
|
||||
---
|
||||
@@ -27,14 +27,14 @@ public record AssignRoleRequest(string Username, ServerRole Role)
|
||||
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
|
||||
|
||||
|
||||
AssignRoleRequest is a lightweight, immutable data transfer object that carries the intent to assign a specific server role to a user. It encapsulates just two pieces of information—the target Username and the desired Role—and is intended to be serialized and sent to moderation or authorization services that perform the actual role assignment.
|
||||
AssignRoleRequest is a lightweight, immutable data container (a positional `record`) that carries the target `Username` and the `Role` to be assigned. It serves as the payload for moderation workflows when granting a [`ServerRole`](../Models/ServerRole.cs.md) to a user, enabling consistent transport of this intent across API boundaries without embedding behavior. As a `record`, it uses value-based equality and can be copied with a `with` expression to create variations.
|
||||
|
||||
## Remarks
|
||||
The record type provides value-based equality and immutability, making it a reliable payload for messaging boundaries between UI, services, and backend handlers. By expressing the action as data rather than behavior, it supports clean separation of concerns and straightforward routing in moderation workflows.
|
||||
This symbol acts purely as a data carrier for the moderation flow, separating payload shape from the enforcement logic. It relies on the `Username` and `Role` values to identify the target user and the desired permission, enabling services to validate and enact the change consistently.
|
||||
|
||||
## Notes
|
||||
- Ensure Username conforms to identity rules at the boundary before processing the request.
|
||||
- Because this is an immutable record, callers should create a new instance for every distinct request; do not modify an existing instance.
|
||||
- Ensure `Username` is a valid existing member; the DTO does not enforce existence.
|
||||
- The `Role` must be a valid [`ServerRole`](../Models/ServerRole.cs.md) value; rely on server-side validation to handle invalid roles.
|
||||
|
||||
---
|
||||
|
||||
@@ -53,13 +53,7 @@ public record BanRequest(string? Reason = null)
|
||||
| `Reason` | `string?` | `null` |
|
||||
|
||||
|
||||
BanRequest is a lightweight, immutable data container used when issuing moderation bans. It carries an optional Reason and is designed to be passed as a single object through the moderation pipeline instead of a group of disparate parameters. This structure makes future extension straightforward (e.g., adding additional ban metadata) without changing call sites.
|
||||
|
||||
## Remarks
|
||||
BanRequest acts as a boundary between the transport/presentation layer and the moderation domain. Using a record provides value-based equality and predictable serialization, which aids testing, logging, and caching. The optional Reason supports both silent bans and bans accompanied by rationale, with policy decisions about requiring a reason typically enforced at higher layers.
|
||||
|
||||
## Notes
|
||||
- Reason is nullable; handle nulls gracefully when displaying or persisting data, and apply any policy about requiring a reason at the appropriate layer.
|
||||
BanRequest is a simple data carrier used to submit a moderation ban action, optionally including a rationale. Its only member, `Reason`, is nullable and defaults to null, so callers may omit a reason when none is provided.
|
||||
|
||||
---
|
||||
|
||||
@@ -78,15 +72,19 @@ public record KickRequest(string? Reason = null)
|
||||
| `Reason` | `string?` | `null` |
|
||||
|
||||
|
||||
KickRequest is a lightweight, immutable payload used when performing a moderation kick. It carries an optional Reason describing why the kick occurred. Callers construct this record when issuing a kick action and attach the reason if one is known; if no reason is provided, Reason remains null. The record shape ensures value-based equality and easy serialization across boundaries, making it a convenient transport object for moderation workflows.
|
||||
KickRequest is a minimal, immutable data carrier used to convey a moderation kick action. It carries an optional `Reason` explaining why the kick is issued. Callers instantiate a `KickRequest` when initiating a kick, providing a `Reason` if available; if no reason is supplied, the `Reason` property is `null`.
|
||||
|
||||
## Remarks
|
||||
KickRequest isolates the transport of a kick action from its core moderation logic. This abstraction makes it easy to extend later with additional fields (for example, moderatorId, timestamp, or kick ban duration) without changing the public contract. It also supports consistent logging and audit trails by treating the kick reason as optional metadata.
|
||||
KickRequest being a `record` makes it a value object with structural equality and immutability, which is helpful when routing kick intents through handlers or messaging layers. It encapsulates the kick payload so that higher-level services can work with a single, consistent input type rather than ad-hoc parameters.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var req = new KickRequest("Spamming in chat");
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Reason is optional; validate as needed at the API boundary if your scenario requires a non-null reason.
|
||||
- When serializing, null Reason might be omitted depending on serializer configuration; be explicit if you need to communicate 'no reason'.
|
||||
- This is a simple DTO; do not conflate it with the domain entity for a kick; use it to transport data.
|
||||
- `Reason` is nullable; downstream code should handle `null` and decide whether a reason is required.
|
||||
- Records provide value-based equality; two `KickRequest` instances with the same `Reason` compare equal.
|
||||
|
||||
---
|
||||
|
||||
@@ -106,22 +104,6 @@ public record MuteRequest(string? Reason = null, int? DurationMinutes = null)
|
||||
| `DurationMinutes` | `int?` | `null` |
|
||||
|
||||
|
||||
MuteRequest is a compact, immutable data transfer object used to initiate a moderation mute. It carries two optional fields: Reason and DurationMinutes, allowing you to specify a rationale and a duration when issuing a mute; omitting either field leaves that detail to the receiver's policy.
|
||||
|
||||
## Remarks
|
||||
By grouping the fields into a single record, this abstraction reduces API surface area and provides a consistent payload for mute-related actions across the moderation layer. The record semantics also enable value-based equality and straightforward testing and transport.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Mute for 30 minutes with a reason
|
||||
var request = new MuteRequest("Spamming in chat", 30);
|
||||
|
||||
// Mute without specifying details
|
||||
var request2 = new MuteRequest();
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Reason may contain user-provided content; avoid including it in logs or telemetry unless explicitly permitted.
|
||||
- Because the type is a record with nullable fields, ensure boundary validation and handle nulls gracefully at the call site or in the receiving layer.
|
||||
MuteRequest is a lightweight data transfer object used to specify the parameters of a mute action in moderation flows. It includes two optional values: `Reason`, a `string?` describing why the mute is issued, and `DurationMinutes`, an `int?` indicating how long the mute should last; both default to `null` if not provided. This allows callers to mute with a default duration or provide additional context for auditing and user experience.
|
||||
|
||||
---
|
||||
@@ -27,21 +27,7 @@ public record AvatarUploadResponse(string AvatarAscii)
|
||||
| `AvatarAscii` | `string` | — |
|
||||
|
||||
|
||||
AvatarUploadResponse is a tiny, immutable data container that represents the server’s response to an avatar-upload operation. It carries a single payload, AvatarAscii, which holds the ASCII-art representation of the uploaded avatar. Use this type as a typed contract when returning avatar data from a service or API endpoint, rather than returning a raw string scattered through your responses.
|
||||
|
||||
## Remarks
|
||||
This abstracted DTO isolates the avatar representation behind a named contract, making it easier to evolve the API (e.g., by adding metadata) without breaking call sites. The record semantics ensure value-based equality and straightforward deconstruction, which pairs well with serialization and testing.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var resp = new AvatarUploadResponse("ASCII_ART");
|
||||
Console.WriteLine(resp.AvatarAscii);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- AvatarAscii may contain newline characters; ensure your JSON/HTTP layer preserves them.
|
||||
- Keep the payload size reasonable; extremely large ASCII art can inflate responses.
|
||||
- This type is a pure DTO with no behavior; avoid placing business logic here.
|
||||
AvatarUploadResponse is a lightweight data container that carries the ASCII representation of a user-uploaded avatar. Its sole payload is the `AvatarAscii` string, which downstream clients can render to display the avatar in text form after an upload.
|
||||
|
||||
---
|
||||
|
||||
@@ -65,23 +51,22 @@ public record UpdateProfileRequest(
|
||||
| `NicknameColor` | `string?` | `null` |
|
||||
|
||||
|
||||
UpdateProfileRequest is a data transfer object used when updating a user's profile. All fields are optional, enabling partial updates by supplying only the fields you want to change (DisplayName, Bio, or NicknameColor). This object is typically sent to a profile update endpoint or service, where the provided values are applied while unspecified fields remain unchanged.
|
||||
This `UpdateProfileRequest` is a `record` that carries a partial update payload for a user's profile. By supplying only non-null properties (e.g. `DisplayName`, `Bio`, or `NicknameColor`), callers express which fields should be updated; fields left as `null` indicate no change for that field.
|
||||
|
||||
## Remarks
|
||||
By modeling the payload as a record with nullable properties, this abstraction communicates intent clearly: you're patching specific aspects of a profile rather than replacing it wholesale. It decouples API contract from the underlying domain model and reinforces immutability semantics for the request object. The combination of a concise DTO and nullable members makes it straightforward for clients to express partial updates without constructing separate patch types.
|
||||
|
||||
Using a `record` provides value-based equality and inherent immutability, which makes it ideal for data-carrying DTOs. The ability to set properties to `null` gives a clean contract for partial updates; consumers should treat nulls as 'do not modify' for that field and pass through only the intended changes to the update operation.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Update only the display name
|
||||
var request1 = new UpdateProfileRequest(DisplayName: "Nova");
|
||||
|
||||
// Update multiple fields
|
||||
var request2 = new UpdateProfileRequest(DisplayName: "Nova", Bio: "Software engineer", NicknameColor: "#1E90FF");
|
||||
```csharp
|
||||
var request = new UpdateProfileRequest(DisplayName: "Nova", NicknameColor: "#FFAA00");
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Omitted properties are treated as "no update" by the receiver; a null value may be interpreted differently depending on backend semantics.
|
||||
- If you need to clear a value, verify the server's rules: null may not clear a field unless explicitly supported; you may need to provide an empty string or use a dedicated API path to clear a value.
|
||||
|
||||
- Ensure the update handler interprets nulls as "no change" to avoid overwriting existing values.
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -103,14 +88,15 @@ public record UpdateStatusRequest(
|
||||
| `StatusMessage` | `string?` | `null` |
|
||||
|
||||
|
||||
UpdateStatusRequest is a small, immutable data transfer object used to submit a user's status update. It bundles the new Status and, optionally, an accompanying StatusMessage to be processed by a profile update operation.
|
||||
This `UpdateStatusRequest` record encapsulates the payload required to update a user's profile status. It carries the new [`UserStatus`](../Models/UserStatus.cs.md) and an optional `StatusMessage`, and is intended to be used when issuing a status update to APIs or command handlers where a consistent update payload is expected.
|
||||
|
||||
## Remarks
|
||||
Being a C# 9 record, UpdateStatusRequest is immutable and supports value-based equality, which makes it reliable to pass across process boundaries and into tests. The Status is a required field that identifies the new user state via UserStatus, while StatusMessage provides optional context. This DTO participates in the profile update workflow and is typically serialized as part of requests to the profile service.
|
||||
|
||||
By modeling the input as a dedicated value object, this abstraction centralizes validation and transport concerns at the boundaries between the domain and application layers, ensuring a stable contract for status updates. It also isolates update-related concerns from the rest of the profile payload, making it easier to evolve serialization, auditing, or routing rules without touching domain entities.
|
||||
|
||||
## Notes
|
||||
- StatusMessage is nullable; if the receiver accepts no message, null can be sent and should be handled gracefully.
|
||||
- Because UpdateStatusRequest is a record, you can create modified copies using the with expression, e.g. existing with { Status = newStatus } to preserve other fields.
|
||||
|
||||
- The `StatusMessage` property is nullable. Callers must handle the possibility of a missing message when consuming this payload.
|
||||
|
||||
---
|
||||
|
||||
@@ -142,15 +128,10 @@ public record UserPresenceDto(
|
||||
| `IsIrc` | `bool` | `false` |
|
||||
|
||||
|
||||
Represents a single snapshot of a user's presence in EchoHub. This record aggregates the user's identity (Username and optional DisplayName), their current presence state (Status and optional StatusMessage), and their server role (Role). It also carries UI-related hints such as NicknameColor and an IsIrc flag indicating whether the presence originated from IRC. The type is a C# record with positional parameters, making it an immutable, value-based data object that is ideal for transport across API boundaries and for equality comparisons of presence data.
|
||||
Represents the presence-related data for a user in profile contexts, bundling the `Username`, optional `DisplayName`, optional `NicknameColor`, current `Status`, optional `StatusMessage`, `Role`, and the `IsIrc` flag into a single immutable DTO (with `IsIrc` defaulting to `false`). It is intended to be created and transported as a coherent unit when rendering user cards or updating presence in the UI or API responses, rather than scattering these fields across multiple structures.
|
||||
|
||||
## Remarks
|
||||
Consolidating identity, status, and role into one DTO reduces the number of cross-cutting data transfers required to render a user in a presence list or chat UI. The NicknameColor provides a presentation cue without forcing consumers to derive display styling; the IsIrc flag lets calling code distinguish between sources. As a record, instances compare by their values, enabling straightforward caching, deduplication, and change detection.
|
||||
|
||||
## Notes
|
||||
- Nullable fields (DisplayName, NicknameColor, and StatusMessage) may be null; callers should handle nulls gracefully.
|
||||
- IsIrc defaults to false; set to true when constructing from IRC-origin data.
|
||||
- This is a positional-parameter record; properties are init-only and the object is immutable after construction; create a new instance to represent a changed presence.
|
||||
Acts as a stable boundary for presence data used by profile-related UI and API surfaces, consolidating identity, status, and role information into one payload. The [`UserStatus`](../Models/UserStatus.cs.md) and [`ServerRole`](../Models/ServerRole.cs.md) collaborators encode the allowed presence states and roles, while `NicknameColor` provides a UI cue without forcing a separate domain type. Being a `record`, it relies on value equality to simplify change detection and caching as presence updates propagate.
|
||||
|
||||
---
|
||||
|
||||
@@ -190,13 +171,9 @@ public record UserProfileDto(
|
||||
| `LastSeenAt` | `DateTimeOffset` | — |
|
||||
|
||||
|
||||
Represents a compact, transport-friendly snapshot of a user's profile used across boundaries (e.g., API responses, UI layers). As a C# record, it provides value-based equality and immutability, ensuring a stable contract when serializing user data. It collects identity (Id, Username), optional display attributes (DisplayName, Bio, NicknameColor, AvatarAscii), current status (Status, StatusMessage), role (Role), and timestamp metadata (CreatedAt, LastSeenAt).
|
||||
UserProfileDto is an immutable data transfer object that represents a snapshot of a user's profile for API responses and inter-layer communication. Implemented as a `record`, it carries a stable payload including the user's identity (`Id` of type `Guid`, `Username`), optional display details (`DisplayName`, `Bio`, `NicknameColor`, `AvatarAscii`), presence (`Status` of type [`UserStatus`](../Models/UserStatus.cs.md), `StatusMessage`), role (`Role` of type [`ServerRole`](../Models/ServerRole.cs.md)), and timestamps (`CreatedAt`, `LastSeenAt` of type `DateTimeOffset`).
|
||||
|
||||
## Remarks
|
||||
This DTO exists to decouple internal domain models from the data contract exposed to clients. By using a dedicated record, changes to the underlying domain models won't automatically ripple into API payloads. The explicit nullable fields model optional user attributes, and the timestamp fields communicate when the profile was created and last observed; consumers must handle time values robustly across time zones.
|
||||
|
||||
## Notes
|
||||
- Nullable properties (DisplayName, Bio, NicknameColor, AvatarAscii, StatusMessage) may be null; handle accordingly in consumers.
|
||||
- CreatedAt and LastSeenAt are DateTimeOffset values; when displaying, convert to a user-friendly timezone or use UTC representation as defined by the API contract.
|
||||
By modelling the payload as a `record`, `UserProfileDto` benefits from value-based equality and straightforward serialization for API clients. It serves as a transport contract that decouples external API surfaces from the internal domain model, allowing optional fields to convey partial profile information without mutating server state.
|
||||
|
||||
---
|
||||
@@ -24,14 +24,10 @@ public record EncryptionKeyResponse(string Key)
|
||||
| `Key` | `string` | — |
|
||||
|
||||
|
||||
EncryptionKeyResponse is a tiny, immutable data transfer object that carries a single encryption key via its Key property. Use it whenever a caller must receive an encryption key in a strongly-typed envelope (instead of returning a plain string) to improve clarity and compatibility with serialization and tooling.
|
||||
EncryptionKeyResponse is a minimal, strongly-typed envelope used to return an encryption key from server-side DTOs. It is implemented as a C# `record` with a single property `string Key`, providing value-based equality and convenient deconstruction while keeping the surface area stable for serialization and future extension.
|
||||
|
||||
## Remarks
|
||||
By leveraging a C# record, EncryptionKeyResponse benefits from value-based equality, structural deconstruction, and concise construction. It serves as a semantic wrapper around the raw key, making intent explicit in APIs that issue or relay keys, and aligns with other DTOs in the EchoHub.Core DTOs layer.
|
||||
|
||||
## Notes
|
||||
- The Key contains sensitive material; avoid logging or exposing it in request traces. Ensure transport channels are secure (TLS) and that only authorized callers can obtain the key.
|
||||
- Because it is a simple wrapper, use it when a typed envelope adds value (e.g., API contracts or structured responses) and avoid over-modeling plain, ephemeral keys.
|
||||
Using a one-property `record` as a DTO provides a stable, strongly-typed surface for returning the key, while enabling easy evolution (e.g., adding metadata like algorithm, expiration, or salt) without breaking client contracts. It also leverages `record` semantics to support value-based equality and clean deconstruction when used in responses.
|
||||
|
||||
|
||||
---
|
||||
@@ -60,14 +56,13 @@ public record ServerStatusDto(
|
||||
| `RegistrationMode` | `string` | `"open"` |
|
||||
|
||||
|
||||
ServerStatusDto is an immutable data-transfer object that represents the current status of a server in EchoHub. It exposes the server name, an optional description, the number of online users, the total number of channels, and a registration mode (defaulting to open). As a C# record with a primary constructor, it benefits from value-based equality and convenient deconstruction, making it a natural payload for API responses that describe the server's state.
|
||||
Represents a lightweight, immutable snapshot of a server's status for transport between layers or to clients. It exposes the server's `Name`, optional `Description`, current `OnlineUsers`, total `TotalChannels`, and the `RegistrationMode` (defaulting to `open` when not provided).
|
||||
|
||||
## Remarks
|
||||
A record provides value-based equality and immutability for a simple data carrier, which is exactly what a status payload is. The Description field is optional, so consumers must be prepared to handle null. The shape is designed to be serialized to JSON for API responses and easily deconstructed when mapping to other domain models.
|
||||
Because this is a `record`, it uses value-based equality and immutable properties, making it ideal as a DTO boundary between internal domain models and external consumers. Construct this type from your server state when returning status information to clients, rather than leaking domain entities.
|
||||
|
||||
## Notes
|
||||
- Nullable Description means clients must handle nulls.
|
||||
- RegistrationMode defaults to "open" when not supplied, preserving backward compatibility.
|
||||
- As a record, two instances with identical property values compare equal (value equality).
|
||||
- `Description` is nullable (`string?`). Guard against null or provide a fallback when presenting it to callers.
|
||||
- To derive a modified copy (e.g., update `OnlineUsers`), use the `with` expression since `ServerStatusDto` is immutable.
|
||||
|
||||
---
|
||||
@@ -8,11 +8,14 @@ public class Attachment
|
||||
```
|
||||
|
||||
|
||||
Represents a file attached to a message, such as an image, audio, or document. A message may carry zero or more attachments alongside its text content (Discord-style).
|
||||
Represents a file attached to a message (such as an image, audio, or any file), enabling a message to carry zero or more attachments alongside its text content. The `Attachment` entity associates a downloadable resource with its parent [`Message`](Message.cs.md) via `MessageId` and, optionally, [`Message`](Message.cs.md), while storing the attachment's URL (`Url`), filename (`FileName`), size (`FileSize`), type (`Kind`), and an optional ASCII preview (`AsciiPreview`).
|
||||
|
||||
## Remarks
|
||||
Decouples attachment data from the message to allow independent storage and retrieval while keeping a lightweight reference to the owning message. The Url provides the relative download path (for example, /api/files/{fileId}) and FileName preserves the original filename. FileSize stores the stored blob size in bytes, which corresponds to ciphertext size when database encryption is enabled. AsciiPreview offers a rendered ASCII-art preview for images in color-tag format and is null for non-image attachments; it is stored encrypted-at-rest and, in end-to-end encrypted channels, remains room-encrypted.
|
||||
|
||||
Attachments decouple media from the textual content of a message, allowing the system to manage downloads, permissions, and encryption independently from the message body. The `AsciiPreview` provides a lightweight visual cue for image attachments, and its presence is influenced by how media is encrypted at rest or within channel scopes. The [`AttachmentKind`](AttachmentKind.cs.md) helps callers distinguish among images, audio, and other file types to apply appropriate handling.
|
||||
|
||||
## Notes
|
||||
- AsciiPreview is only populated for image attachments; for other kinds of attachments it is null.
|
||||
- The Message navigation property may be null if the related Message entity isn't loaded; use MessageId for persistence and rely on Message when the relationship is loaded.
|
||||
|
||||
- The `Url` is a relative download path (for example, `/api/files/{fileId}`); clients should prefix it with the API base URL when constructing a full link.
|
||||
- The `AsciiPreview` is null for non-image attachments and is stored encrypted at rest in encrypted channels.
|
||||
- The `FileSize` is the number of bytes stored for the attachment and may reflect ciphertext size when encryption is enabled.
|
||||
@@ -13,27 +13,7 @@ public enum AttachmentKind
|
||||
```
|
||||
|
||||
|
||||
AttachmentKind enumerates the possible types of a message attachment and signals how the client should render it. Use this enum when you know the specific attachment kind (image, audio, or file) so the UI can render an ASCII preview, a playback control, or a download option instead of a generic attachment rendering.
|
||||
AttachmentKind is an enum that encodes how a message attachment should be rendered in the client. It enables rendering logic to pick the appropriate UI: for `Image` attachments, an ASCII image preview is shown; for `Audio`, a play control is exposed; and for `File`, a download line is presented. Use this enum when you need to branch rendering behavior based on the attachment's kind, instead of scattering rendering decisions across the codebase.
|
||||
|
||||
## Remarks
|
||||
This enum centralizes the presentation logic for attachments and serves as a simple discriminator that decouples the attachment data from its rendering. By representing the modality with a single value, components can switch on kind to choose the appropriate UI affordance without inspecting the content payload. It helps maintain a clean separation between the data model (what the attachment is) and the presentation (how it should be shown).
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
AttachmentKind kind = AttachmentKind.Image;
|
||||
switch (kind)
|
||||
{
|
||||
case AttachmentKind.Image:
|
||||
Console.WriteLine("Render as ASCII image preview");
|
||||
break;
|
||||
case AttachmentKind.Audio:
|
||||
Console.WriteLine("Render with audio controls");
|
||||
break;
|
||||
case AttachmentKind.File:
|
||||
Console.WriteLine("Render as downloadable file");
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- If the enum is extended in the future, ensure all switch expressions include a default/fallback to handle unknown values gracefully.
|
||||
This enum centralizes how attachments are presented, decoupling the attachment data from UI rendering code. It helps the rendering layer evolve independently (e.g., swapping ASCII previews or adding new affordances) without changing attachment structures.
|
||||
@@ -8,4 +8,4 @@ public class Channel
|
||||
```
|
||||
|
||||
|
||||
Represents a chat channel (room) within EchoHub's domain model. It stores the channel's identity, metadata for access control, an optional topic, and the collection of messages that belong to the channel, as well as an encryption envelope used for end-to-end security. Use this type to model a distinct conversation space that can be public or restricted, with the possibility of system-managed channels that are auto-created and not user-initiated. The class ties together the channel's identity (Id, Name), its description (Topic), its visibility (IsPublic) and authentication data (PasswordHash), its system-channel semantics (IsSystem), its client-managed encryption data (EncryptionSalt, WrappedRoomKey), creation auditing (CreatedAt, CreatedByUserId), and the message history (Messages).
|
||||
Channel models a chat channel within EchoHub's chat surface. It exposes an identifier `Id` (`Guid`), a required `Name` (`string`), an optional `Topic` (`string?`), and a flag `IsPublic` (`bool`) that defaults to `true`. The model also supports server-managed channels via `IsSystem` (`bool`), which are auto-created and read-only for all roles; users cannot create them. When a channel is password-protected, `PasswordHash` (`string?`) stores the hashed password. For end-to-end encryption, the envelope is represented by `EncryptionSalt` (`string?`) and `WrappedRoomKey` (`string?`), both client-generated so that the server never has access to the room content. Creation metadata is captured by `CreatedAt` (`DateTimeOffset`) and `CreatedByUserId` (`Guid`). The `Messages` collection (`List<Message>`) contains the related [`Message`](Message.cs.md) entities that belong to this channel.
|
||||
@@ -8,21 +8,11 @@ public class ChannelMembership
|
||||
```
|
||||
|
||||
|
||||
ChannelMembership is a lightweight data container that models the association between a user and a channel, recording when the user joined. It is intended for persistence and transport of membership data; instantiate and persist this model when recording channel participation rather than scattering ad-hoc data structures.
|
||||
The `ChannelMembership` class is a simple data container that models the association between a user and a channel within the EchoHub system. It stores the `UserId`, the `ChannelId`, and the time the membership was created (`JoinedAt`), which defaults to the current UTC time if not specified.
|
||||
|
||||
## Remarks
|
||||
ChannelMembership encapsulates the many-to-many relationship between users and channels along with a join timestamp, enabling straightforward CRUD operations, serialization, and display of membership data. As a plain DTO, it contains no behavior beyond storage of UserId, ChannelId, and JoinedAt; it complements User and Channel entities by representing their linkage. The JoinedAt default is DateTimeOffset.UtcNow at construction, which is convenient for new memberships but should be overridden or preserved from storage when loading existing records.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var membership = new ChannelMembership
|
||||
{
|
||||
UserId = Guid.NewGuid(),
|
||||
ChannelId = Guid.NewGuid()
|
||||
// JoinedAt defaults to DateTimeOffset.UtcNow
|
||||
};
|
||||
```
|
||||
Locates a specific user's membership in a channel and records when it happened. It serves as a lightweight linkage between `UserId` and `ChannelId`, with `JoinedAt` providing a timestamp of when the membership was established.
|
||||
|
||||
## Notes
|
||||
- The default JoinedAt value applies only to newly created instances; deserialization from a data store will populate JoinedAt from the stored value.
|
||||
- This class is a plain data holder with no validation or invariants; enforce domain rules at a higher layer when necessary.
|
||||
- `JoinedAt` defaults to `DateTimeOffset.UtcNow` at object creation; when loading from a data store this value may be overridden by stored data, so rely on the persisted timestamp in that case.
|
||||
- There are no invariants enforced here; enforce uniqueness and referential constraints at the database or repository layer.
|
||||
@@ -8,29 +8,4 @@ public class InviteCode
|
||||
```
|
||||
|
||||
|
||||
Represents a registration invitation code used to gate account creation when the server's registration mode is set to invite. An InviteCode captures the unique identifier, the actual code string, who created it, and when it was created, plus optional expiration and per-invite usage constraints. When a new REST or IRC account is created and the system is configured for invite-based registration, the incoming code must match an existing InviteCode that has not expired and that has remaining uses.
|
||||
|
||||
## Remarks
|
||||
InviteCode acts as a persistence-side contract for invitation-based onboarding. It separates the concerns of registration gating from user data and provides a straightforward way to enforce expiration and single-use or limited-use policies at the data layer. The server's registration flow should consult these properties to validate a code before creating a new account and to record each use via UseCount, potentially preventing additional uses after MaxUses is reached.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Example usage: initialize a new invite code that will expire in 7 days and allow up to 5 uses
|
||||
Guid adminUserId = Guid.NewGuid();
|
||||
var invite = new InviteCode
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Code = "INVITE-2026-ACME",
|
||||
CreatedByUserId = adminUserId,
|
||||
CreatedByUsername = "admin",
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7),
|
||||
MaxUses = 5,
|
||||
UseCount = 0
|
||||
};
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Use of 'required' Code property ensures that a code value is provided when constructing instances; compile-time enforcement.
|
||||
- ExpiresAt null means never expires; If ExpiresAt is not set, the code is perpetual.
|
||||
- The class does not implement persistence or concurrency control; UseCount and MaxUses must be enforced by the application or data layer.
|
||||
A data model representing a single registration invitation. When the server is configured with `Server:Registration = "invite"`, new accounts (REST and IRC alike) must present a valid, unexpired, and not-fully-used code to register. The `InviteCode` tracks the invitation's identity and policy: the persistent identifier `Id`, the required invitation value `Code` (marked `required` in the model), who created it (`CreatedByUserId` and `CreatedByUsername`), and when it was created (`CreatedAt`). The invitation may expire via `ExpiresAt` (null meaning it never expires), and its usage is controlled by `MaxUses` with current usage stored in `UseCount`. By default, a new invite is single-use (`MaxUses` = 1) and `CreatedAt` is initialized to the current UTC moment. This class is intended to be stored and consulted by the registration workflow to enforce invite-based onboarding.
|
||||
@@ -8,7 +8,36 @@ public class Message
|
||||
```
|
||||
|
||||
|
||||
Message is the persistence model for a chat message in EchoHub, capturing who sent it, when, where, and what was said. Content is required text (which may be empty if the message carries only attachments), with an optional EmbedJson and a list of Attachments for attached files; SenderUserId/SenderUsername identify the author and ChannelId/Channel locate the conversation. Messages may reply to another message via ReplyToMessageId. It also includes legacy pre-attachments fields (Type, AttachmentUrl, AttachmentFileName, AttachmentFileSize) retained to support a one-time startup migration that folds old single-attachment messages into Attachments; new code never writes these and they are nulled after migration and not exposed in DTOs.
|
||||
Represents a single message in a channel, encapsulating the text payload, sender identity, timestamp, and any attachments. It serves as the core record for conversations and is designed to be persisted by the data layer and consumed by the UI to render threads and channel histories. The message may carry rich content via `EmbedJson` and can reference a previous message through `ReplyToMessageId` to model simple threading. The `Content` property is required, yet a message may legitimately have empty content if it carries attachments.
|
||||
|
||||
## Remarks
|
||||
Architecturally, Message acts as the persistence model for chat messages, combining the modern Attachments collection with legacy fields retained to support a one-time startup data migration. New code never writes the legacy fields; they are nulled after migration and are not exposed in DTOs.
|
||||
Message is the domain aggregate for a chat entry, linking to its [`Channel`](Channel.cs.md) via `ChannelId`/[`Channel`](Channel.cs.md) and to its sender via `SenderUserId`/`SenderUsername`. Attachments are modeled as a separate collection (`Attachments`), enabling a clean separation between textual payloads and media. Legacy fields (`Type`, `AttachmentUrl`, `AttachmentFileName`, `AttachmentFileSize`) exist solely to support a one-time startup data migration into the new attachments model; new writes should use the `Attachments` collection, and these legacy fields are not exposed in DTOs and are nulled after migration. The `ReplyToMessageId` enables basic threading by pointing to the message this one replies to, if any; downstream logic should gracefully handle references to messages that may have been deleted.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var message = new Message
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Content = "Welcome to the channel!",
|
||||
SenderUserId = Guid.NewGuid(),
|
||||
SenderUsername = "system",
|
||||
ChannelId = Guid.NewGuid(),
|
||||
Attachments = new List<Attachment>
|
||||
{
|
||||
new Attachment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
MessageId = Guid.Empty,
|
||||
Url = "https://example.com/file.png",
|
||||
FileName = "file.png",
|
||||
FileSize = 4096
|
||||
}
|
||||
},
|
||||
SentAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Legacy fields are for migration only; do not rely on them for new code.
|
||||
- `SentAt` defaults to `DateTimeOffset.UtcNow` on instantiation; override if you have a specific send time.
|
||||
- Use `EmbedJson` for optional rich content, and handle its absence gracefully in the UI.
|
||||
@@ -14,11 +14,11 @@ public enum MessageType
|
||||
```
|
||||
|
||||
|
||||
Represents the category of a message in EchoHub. MessageType defines the four concrete payload kinds that a message can carry: Text, Image, File, or Audio. Use this enum whenever a component, data model, or API needs to convey which kind of content is attached to a message so consumers can handle, display, or validate it in a type-safe way instead of relying on strings or magic numbers.
|
||||
Represents the category of a message payload within the model, enabling code to distinguish between textual content, images, files, and audio. Use `MessageType` to drive type-specific logic (rendering, validation, or serialization) by switching on the enum values rather than inspecting the payload directly.
|
||||
|
||||
## Remarks
|
||||
Centralizes classification: this enum provides a single source of truth for message content kinds, enabling consistent routing, rendering, and validation across the system. It helps collaborators—models, serializers, and UI layers—make decisions based on content type without duplicating logic for string constants. By using an enum, you get compile-time checks and clearer intent.
|
||||
By centralizing the variety of message payloads behind a single discriminator, `MessageType` makes it easier to extend support for new kinds. Renderers, validators, and serializers can rely on this enum to route behavior without peeking into payload internals, promoting cleaner separation of concerns.
|
||||
|
||||
## Notes
|
||||
- When stored or transferred, the underlying value defaults to int (0-3) in the order shown; changing the sequence or renaming members may break persisted data.
|
||||
- If external systems expect string representations, consider mapping to/from MessageType names to avoid breaking compatibility.
|
||||
- When adding a new member to `MessageType`, update all switch expressions that handle the enum to avoid unhandled values at runtime. Prefer exhaustiveness to catch omissions at compile time.
|
||||
- Do not repurpose existing values; if the meaning changes, introduce a new member to preserve backward compatibility and serialization stability.
|
||||
@@ -8,7 +8,27 @@ public class RefreshToken
|
||||
```
|
||||
|
||||
|
||||
RefreshToken is a persistence model that represents a refresh token tied to a user. It stores a hashed token (TokenHash), the associated user via UserId, and validity information such as ExpiresAt and CreatedAt (which defaults to the current UTC time), plus an optional RevokedAt timestamp. It exposes IsExpired, IsRevoked, and IsActive to quickly assess the token’s state. A developer would create and persist these tokens when issuing refresh tokens in an authentication flow, check IsActive (or IsExpired/IsRevoked) when validating a refresh attempt, and use RevokedAt to mark a token as revoked.
|
||||
Represents a `RefreshToken` that carries the metadata and state needed to sustain a user session via token-based authentication. It encapsulates the token hash, the owning user, expiration, and revocation data, and exposes simple predicates to answer the token's current validity. The `TokenHash` is marked `required`, guaranteeing a hash is provided during initialization; `CreatedAt` records when the token was created (defaulting to `DateTimeOffset.UtcNow`); `ExpiresAt` defines when the token becomes invalid; `RevokedAt` records a revocation timestamp when the token is revoked. The computed properties `IsExpired`, `IsRevoked`, and `IsActive` reflect the token's lifecycle status, so callers can check validity without inspecting each field. The [`User`](User.cs.md) navigation property links the token to its owner for convenient data access in domain services or ORMs.
|
||||
|
||||
## Dependencies
|
||||
- `DateTimeOffset`
|
||||
|
||||
## Remarks
|
||||
This class serves as a persistence-facing token entity with a foreign key to User and a corresponding navigation property, enabling lifecycle management (creation, expiry, revocation) at the data layer while providing simple state checks for business logic.
|
||||
Architecturally, this symbol serves as the boundary for token-based authentication in the domain. It centralizes lifecycle logic (expiry and revocation) into a single place, enabling consistent checks via `IsActive` across services. The presence of [`User`](User.cs.md) further supports straightforward navigation to the owner, which is helpful when presenting token data in dashboards or auditing scenarios.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Example: creating a new `RefreshToken` (TokenHash is required)
|
||||
Guid userId = Guid.NewGuid();
|
||||
var token = new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TokenHash = "sha256-abc123",
|
||||
UserId = userId,
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
|
||||
};
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Be aware that `CreatedAt` is initialized to the current UTC time at construction. If you load an existing token from storage, ensure the stored value for `CreatedAt` is preserved.
|
||||
- `IsActive` depends on both `IsExpired` and `IsRevoked`. If you set `RevokedAt` but forget to update `IsRevoked`, the token might appear active.
|
||||
@@ -14,31 +14,7 @@ public enum ServerRole
|
||||
```
|
||||
|
||||
|
||||
Represents the role assigned to a member within a server context in EchoHub. It defines four distinct levels of authority: Member, Mod (moderator), Admin, and Owner. Use this enum whenever you need to distinguish capabilities, gate UI or actions, or persist role information instead of relying on magic numbers.
|
||||
Represents the role a user holds within a server in EchoHub. It categorizes users into distinct permission tiers: `Member`, `Mod`, `Admin`, and `Owner`, which are used to drive authorization and feature availability without scattering numeric checks throughout the codebase.
|
||||
|
||||
## Remarks
|
||||
By centralizing roles in a single enum, the codebase can map each role to its corresponding permissions in one place, enabling consistent authorization checks across services. The explicit integer values also support stable serialization and interop when persisting or transmitting role data, without forcing string-based representations.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
var role = ServerRole.Admin;
|
||||
switch (role)
|
||||
{
|
||||
case ServerRole.Owner:
|
||||
case ServerRole.Admin:
|
||||
// elevated permissions
|
||||
break;
|
||||
case ServerRole.Mod:
|
||||
// moderation tasks
|
||||
break;
|
||||
case ServerRole.Member:
|
||||
// regular user actions
|
||||
break;
|
||||
}
|
||||
Console.WriteLine($"User role: {role}"); // prints Owner, Admin, Mod, or Member
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Do not treat ServerRole as a Flags enum; do not combine roles with bitwise operators.
|
||||
- Prefer using the named constants in checks; avoid relying on numeric ordering for access decisions.
|
||||
- Changing the underlying values (0–3) can affect serialized data; coordinate evolution across all consumers to preserve compatibility.
|
||||
This enum provides a stable abstraction for role-based access control, allowing components to reason about capabilities (moderation, configuration, ownership) by comparing against `ServerRole` values. Centralizing roles reduces duplication of permission logic and helps ensure consistent authorization across command handlers, UI components, and services. It also offers an extension point: adding a new role or reordering the hierarchy can be localized to this enum and its consumers.
|
||||
@@ -8,13 +8,12 @@ public class ServerStatsReport
|
||||
```
|
||||
|
||||
|
||||
Represents a snapshot of server activity for a single reporting window, produced periodically by the stats-report background job. It captures timing data (PeriodStart, PeriodEnd, WindowHours, GeneratedAt) and per-window metrics (MessagesSent, FilesUploaded, BytesUploaded, NewMembers, ActiveMembers, Connections, Disconnections, Kicks, Bans) as well as end-of-window totals (TotalMembers, OnlineNow, PeakOnline) for persistence as pretty-printed JSON.
|
||||
ServerStatsReport is a snapshot of server activity for a single reporting window, produced periodically by the `stats-report` background job. It records when the report was generated, the start and end of the window, the window length in hours, and a set of per-window activity counters (`MessagesSent`, `FilesUploaded`, `BytesUploaded`, `NewMembers`, `ActiveMembers`, `Connections`, `Disconnections`, `Kicks`, `Bans`) as well as end-of-window totals (`TotalMembers`, `OnlineNow`, `PeakOnline`); the report is serialized as pretty-printed JSON and persisted for historical trend analysis. The window is defined as "since the previous report" (or since startup for the first report).
|
||||
|
||||
## Remarks
|
||||
Serves as a stable, serializable container for periodic server activity, enabling dashboards and trend analyses to compare windows over time. By separating window semantics (start/end, duration) from generation time, it supports reliable aggregation and rhythm-based alerts when metrics diverge.
|
||||
ServerStatsReport serves as the canonical persisted unit for time-bounded server activity, decoupling the reporting job from storage and analytics. It combines both within-window activity and end-of-window aggregates to support dashboards, trend charts, and anomaly detection across multiple windows. As a plain data container, it is populated by the reporting process and then written to the data store; its structure is stable to ensure reliable longitudinal comparisons.
|
||||
|
||||
## Notes
|
||||
- GeneratedAt is intended to equal PeriodEnd; ensure synchronization when populating the model. The default initializer uses DateTimeOffset.UtcNow, which may diverge if PeriodEnd is set to a different value.
|
||||
|
||||
## Dependencies
|
||||
- DateTimeOffset (System) — used for all timestamp properties on the model.
|
||||
- GeneratedAt is intended to reflect the moment the window ended; ensure GeneratedAt is kept in sync with PeriodEnd to avoid confusion (GeneratedAt should effectively equal PeriodEnd when the report is produced).
|
||||
- PeriodEnd should be greater than or equal to PeriodStart; WindowHours should be non-negative.
|
||||
- BytesUploaded uses a 64-bit signed integer; extremely large attachment activity should still stay within `BytesUploaded`'s range to avoid overflow.
|
||||
|
||||
@@ -8,14 +8,25 @@ public class User
|
||||
```
|
||||
|
||||
|
||||
The User class is a domain model that represents a person using EchoHub, encapsulating identity (Id, Username, PasswordHash), profile details (DisplayName, Bio, NicknameColor, AvatarAscii), presence (Status, StatusMessage), role-based access (Role), moderation flags (IsMuted, MutedUntil, IsBanned), and auditing timestamps (CreatedAt, LastSeenAt). Username and PasswordHash are required to create a usable user, while other fields are optional to support rich profiles; defaults establish an online, member-facing user with current timestamps when a new instance is created.
|
||||
Represents a user entity in the EchoHub domain, aggregating identity, profile, presence, and lifecycle data. It is the primary model used when creating, retrieving, and persisting user information, with required credentials enforced at construction via the `required` modifiers on `Username` and `PasswordHash`.
|
||||
|
||||
## Remarks
|
||||
Designed to be a single source of truth for user state, it coordinates authentication, authorization via `Role`, and moderation flags such as `IsMuted` and `IsBanned`. The default values — `Status` set to `UserStatus.Online`, `Role` set to `ServerRole.Member`, and timestamps on creation — provide sensible startup behavior while keeping optional fields available for richer profiles. It serves as the canonical user payload across core services and data stores, reducing duplication and drift between layers.
|
||||
|
||||
This class serves as a central data container used across authentication, user management, presence rendering, and authorization checks. It’s designed to be lightweight and serializable for persistence, while keeping domain concerns cohesive with a single user entity. The defaults for Status and Role, along with the auditing timestamps, provide a sensible initial state for newly created users.
|
||||
## Example
|
||||
```csharp
|
||||
var user = new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Username = "jdoe",
|
||||
PasswordHash = "pbkdf2$...",
|
||||
DisplayName = "Jane Doe",
|
||||
Status = UserStatus.Online,
|
||||
Role = ServerRole.Member
|
||||
};
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The required fields (Username and PasswordHash) enforce that essential credentials are provided when constructing a user instance.
|
||||
- PasswordHash should be treated as sensitive data; avoid exposing it in logs or API responses and ensure the persistence layer handles security appropriately.
|
||||
- If hydrating from storage, ensure CreatedAt and LastSeenAt reflect the persisted values rather than new defaults.
|
||||
- The `required` modifier on `Username` and `PasswordHash` enforces initialization when constructing a `User` via object initializers (compile-time check).
|
||||
- `CreatedAt` and `LastSeenAt` default to the moment of object creation but may be replaced by deserialized data from storage.
|
||||
- `MutedUntil` is meaningful only when `IsMuted` is true; it can be null if not muted.
|
||||
|
||||
@@ -14,4 +14,10 @@ public enum UserStatus
|
||||
```
|
||||
|
||||
|
||||
Represents the current presence state of a user in EchoHub, used by UI presence indicators and presence logic throughout the app. Use Online when the user is connected and active, Away when the user is idle, DoNotDisturb to signal notifications should be minimized, and Invisible when the user should not appear online to others.
|
||||
Represents a user's presence status within the application, guiding UI rendering, presence-based filtering, and notification behavior. The enum exposes four discrete states: `Online`, `Away`, `DoNotDisturb`, and `Invisible` to express typical availability scenarios.
|
||||
|
||||
## Remarks
|
||||
Centralizing presence into `UserStatus` prevents scattered string values or boolean flags across the codebase, promoting consistent semantics for how users are shown and how presence-related logic runs. It also future-proofs the API by allowing new statuses to be added without changing call-sites that consume the type. This enum typically intersects with UI components that render status indicators and with services that filter or route behavior based on a user's current state.
|
||||
|
||||
## Notes
|
||||
- Changing the set of statuses (adding/removing/reordering enum members) is a breaking change that can affect serialization, persistence, and cross-boundary API compatibility; prefer backward-compatible extensions by adding new members rather than reordering existing ones.
|
||||
@@ -8,31 +8,27 @@ public static class RoomCrypto
|
||||
```
|
||||
|
||||
|
||||
Client-side envelope encryption primitives used for end-to-end encrypted channels: derive per-room keys from a passphrase, generate random room content keys (RCKs), and encrypt/decrypt room content using AES-GCM. Use this class when you need a canonical, interoperable way to create room key material, wrap/unlock a room key with a passphrase-derived key, and produce/recognize the wire format used on the server ($RC1$base64(nonce||tag||ciphertext)).
|
||||
Client-side utilities for envelope encryption used by private (end-to-end encrypted) channels. Use `RoomCrypto` when you need a simple, opinionated way to derive keys from a passphrase, generate a random room content key (RCK), and encrypt/decrypt room content in the wire format this project uses (a `$RC1$`-prefixed base64 blob for text and a nonce||tag||ciphertext blob for raw bytes).
|
||||
|
||||
## Remarks
|
||||
This class encapsulates the protocol choices and low-level crypto work so callers don't compose PBKDF2, hex encoding, and AES-GCM themselves. It implements an envelope pattern: the client generates a random 256-bit room content key (RCK) to encrypt room data; the RCK is stored server-side wrapped (AES-GCM) with a key-encryption key (KEK) derived from the user's passphrase. PBKDF2-SHA256 with 210000 iterations produces 64 bytes: the first 32 bytes (returned as lowercase hex) are the auth key used as the join gate, and the final 32 bytes are the KEK (never sent). Re-wrapping the RCK on passphrase change avoids re-encrypting history.
|
||||
`RoomCrypto` implements the client-side half of an envelope-encryption scheme: the client generates a random 256-bit room content key (RCK) that actually encrypts all channel content, and a key-encryption key (KEK) derived from the user's passphrase is used to wrap the RCK before the wrapped RCK is stored on the server. The derivation uses PBKDF2-SHA256 with `Pbkdf2Iterations` (210000) and a `SaltSizeBytes` (16) salt; the resulting 64 bytes are split so the first `KeySizeBytes` (32) bytes are exported as a lowercase hex `AuthKeyHex` (the join credential) and the last `KeySizeBytes` bytes are kept as the `KeyEncryptionKey`. `RoomCrypto` keeps a small, explicit surface: `GenerateSalt`, `GenerateRoomKey`, `DeriveKeys`, `EncryptText`, `TryDecryptText`, `IsRoomCiphertext`, and byte-level `EncryptBytes`/`DecryptBytes` (used internally). The text wire format is the literal `CiphertextPrefix` (`"$RC1$"`) followed by `Convert.ToBase64String(nonce||tag||ciphertext)`; binary APIs return/expect the raw `nonce||tag||ciphertext` blob. The implementation zeroes the slice of derived bytes used for the auth key after converting to hex to reduce exposure of sensitive material.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
// Typical client flow:
|
||||
// 1) Create room: generate salt and room key, derive keys from passphrase, wrap RCK and send auth key + wrapped blob to server.
|
||||
// Typical client flow: derive keys from a passphrase, create a room key, encrypt and decrypt text.
|
||||
var salt = RoomCrypto.GenerateSalt();
|
||||
var roomKey = RoomCrypto.GenerateRoomKey();
|
||||
var derived = RoomCrypto.DeriveKeys("correct horse battery staple", salt);
|
||||
// derived.AuthKeyHex is sent to server as the join credential
|
||||
// derived.KeyEncryptionKey (KEK) is used locally to wrap roomKey with AES-GCM (use EncryptBytes/EncryptText as appropriate)
|
||||
// `derived.AuthKeyHex` is sent to the server as the join credential; `derived.KeyEncryptionKey` stays local.
|
||||
var roomKey = RoomCrypto.GenerateRoomKey();
|
||||
|
||||
// 2) Encrypt/decrypt room content with the room key
|
||||
var plaintext = "hello room";
|
||||
var ct = RoomCrypto.EncryptText(plaintext, roomKey);
|
||||
if (RoomCrypto.IsRoomCiphertext(ct) && RoomCrypto.TryDecryptText(ct, roomKey, out var recovered))
|
||||
var ciphertext = RoomCrypto.EncryptText("hello room", roomKey);
|
||||
if (RoomCrypto.TryDecryptText(ciphertext, roomKey, out var plaintext))
|
||||
{
|
||||
// recovered == "hello room"
|
||||
// plaintext == "hello room"
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- PBKDF2 parameters are fixed: 16-byte salt, 210000 iterations, 64-byte output; the auth key is returned as lowercase hex and the KEK as raw bytes.
|
||||
- AES-GCM parameters are fixed: 12-byte nonce, 16-byte tag, 32-byte key (AES-256). Text wire format is the literal prefix "$RC1$" then base64(nonce||tag||ciphertext).
|
||||
- TryDecryptText returns false for non-room ciphertext or when decryption/authentication fails (malformed base64, wrong key, or tampering). Protect KEK and RCK in memory and avoid persisting raw keys.
|
||||
- `RoomCrypto` expects a `KeySizeBytes`-length key (32 bytes) for its AES-GCM operations; supplying a key of the wrong length will fail when constructing the cipher.
|
||||
- Nonces are randomly generated per-encryption (`NonceSizeBytes` = 12). Do not reuse a `roomKey`/nonce pair for different plaintexts; the implementation already generates random nonces, so avoid reusing the same nonce manually.
|
||||
- `TryDecryptText` returns `false` (and sets `plaintext` to empty) both for non-room content (missing the `CiphertextPrefix`) and for any integrity/format errors (bad base64, authentication failure).
|
||||
@@ -8,18 +8,17 @@ public static class AsciiBannerService
|
||||
```
|
||||
|
||||
|
||||
Renders input text as a 5-row block-character banner (the /banner command). It uses a self-contained, hand-authored font defined in code, with no dependencies or network access, producing plain text content that can be transmitted like any other message; the renderer trims input to the maximum length and skips characters not defined in the font.
|
||||
AsciiBannerService renders a string as a five-row block-character banner using a hand-authored, figlet-style font defined entirely in code. It is self-contained — no dependencies and no network access — and returns plain text suitable for transport or encryption just like any other message. Use `Render` when you need a compact, dependency-free banner for logs, UI previews, or console-like output.
|
||||
|
||||
## Remarks
|
||||
This symbol provides a deterministic, dependency-free banner renderer that can be used anywhere a compact ASCII-art label is desirable. The font is embedded in code as a glyph dictionary, so rendering is purely local and consistent across environments. Input is uppercased to match the glyph keys, glyphs are joined per row with a single space, and ink is rendered by replacing the '#' glyphs with the block character '█' and '.' with spaces; trailing spaces on each line are trimmed to minimize payload.
|
||||
AsciiBannerService provides a centralized, self-contained banner rendering capability that does not rely on external resources. The glyphs are embedded in a private `Font` dictionary, ensuring deterministic rendering across environments. The banner width is bounded by `MaxInputLength` (20 characters) and the height is fixed to `Rows` (5), which keeps banners predictable in size and performance. Input is normalized by converting to uppercase with `ToUpperInvariant()`, and only characters present in `Font` are rendered; unsupported characters are skipped. The final output is assembled with a `StringBuilder`, joining glyph rows horizontally with spaces and replacing `#` (ink) with the solid block character `█` and `.` (blank) with spaces. Trailing spaces on each line are trimmed to minimize payload.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
string? banner = AsciiBannerService.Render("EchoHub");
|
||||
if (banner != null)
|
||||
Console.WriteLine(banner);
|
||||
var banner = AsciiBannerService.Render("TEST");
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Non-renderable input (no supported characters) yields null; callers should handle null results to avoid printing empty banners.
|
||||
- The method trims whitespace and enforces a maximum length of 20 characters; longer input is truncated before rendering.
|
||||
- Returns `null` when the input is empty, whitespace, or contains no renderable characters.
|
||||
- Non-renderable characters are skipped; only characters present in `Font` contribute to the banner.
|
||||
- The input is capped at `MaxInputLength` characters, and the output always consists of exactly `Rows` lines if renderable content exists.
|
||||
|
||||
@@ -8,12 +8,23 @@ public static class FileValidationHelper
|
||||
```
|
||||
|
||||
|
||||
FileValidationHelper centralizes lightweight, stream-based validation for common image formats and audio file names. Its IsValidImage(Stream) method reads the stream header (without changing the stream's position) and recognizes JPEG, PNG, GIF, and WebP by their magic numbers, returning true for known formats and false otherwise. IsAudioFile(string) validates a file name’s extension against a predefined set of audio extensions in a case-insensitive manner. Together, these helpers let callers pre-filter content before attempting to decode or process media data.
|
||||
FileValidationHelper is a compact utility that centralizes quick, non-destructive checks for media file types. It exposes `IsValidImage(Stream)` to determine if the provided stream represents a known image format by peeking at its header bytes, while always restoring the stream's original position. It also exposes `IsAudioFile(string)` to decide whether a file name uses one of the recognized audio extensions. Use these helpers to validate inputs in upload or ingestion paths without loading or parsing full files, and to keep format-detection logic consistent across the codebase.
|
||||
|
||||
## Remarks
|
||||
This symbol provides a single, testable utility to detect supported media formats without pulling in a full decoder. By encapsulating the magic-number checks and the extension-based guard, it reduces duplication and concentrates format-coverage decisions in one place. It favors a fast, low-allocation validation path and leaves actual parsing to dedicated components.
|
||||
By coalescing the magic-byte checks in one place, this abstraction reduces duplication and the risk of inconsistent format handling across components that ingest media. The detection rules cover JPEG, PNG, GIF, and WebP at the header level, with WebP requiring a RIFF header followed by the WebP tag; the private helper `StartsWith` encapsulates the prefix comparison to keep `IsValidImage` focused on intent. The `AudioExtensions` set drives a fast, case-insensitive extension lookup for `IsAudioFile` without touching disk data.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
using System.IO;
|
||||
|
||||
byte[] header = new byte[] { 0xFF, 0xD8, 0xFF };
|
||||
using var ms = new MemoryStream(header);
|
||||
bool isImage = FileValidationHelper.IsValidImage(ms);
|
||||
|
||||
bool isAudio = FileValidationHelper.IsAudioFile("song.MP3");
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Non-seekable streams cause IsValidImage to return false (the check stream.CanSeek is performed up-front).
|
||||
- IsAudioFile relies solely on the file extension and does not inspect file contents.
|
||||
- WebP detection requires a RIFF header followed by a WEBP tag at the expected offsets; malformed headers degrade gracefully to false.
|
||||
- The stream passed to `IsValidImage` must be seekable; non-seekable streams will not have their position reset and may lead to false results.
|
||||
- `IsAudioFile` performs a purely extension-based check and does not inspect file contents.
|
||||
- The image-detection logic recognizes specific headers (JPEG, PNG, GIF, WebP) and is not a full format validator; for strict validation, perform content analysis beyond these checks.
|
||||
@@ -8,21 +8,21 @@ public class ImageToAsciiService
|
||||
```
|
||||
|
||||
|
||||
ImageToAsciiService is a lightweight utility that converts an input image stream into color-aware ASCII art by packing two vertical pixels into a single character cell using half-block characters and per-cell color tags. Use GetDimensions to pick a target resolution and ConvertToAscii when you need a textual, ASCII-only representation of an image for logs, chat, or environments without graphical support.
|
||||
ImageToAsciiService converts an image stream into ASCII art using two vertical pixels per character and half-block characters. The static `GetDimensions` translates a size code (`'s'`, `'m'`, `'l'`) into ASCII art dimensions (40x40, 80x80, 120x120 respectively) and returns the default dimensions from `HubConstants.AsciiArtWidth` and `HubConstants.AsciiArtHeightHalfBlock` for other codes. The instance method `ConvertToAscii` accepts a `Stream` containing an image and returns a string composed of color tokens and block characters. Each character cell encodes two vertical pixels; a foreground color token `{F:RRGGBB}` and a background color token `{B:RRGGBB}` are emitted when colors change, followed by a block character (either `█` or `▀`), with `{X}` used to reset coloring. The output uses only printable ASCII and avoids terminal escape sequences.
|
||||
|
||||
## Remarks
|
||||
The class embodies a small, focused translation between raster images and ASCII art. It emits inline color tokens only when the color changes, preserving color fidelity while keeping the output readable in plain-text environments. The two-pixel vertical mapping (top pixel as the foreground color, bottom pixel as the background) enables higher-density representation than single-character ASCII, while remaining printable and parseable by consumers that understand the {F:...}{B:...}{X} tags. An even-height safeguard ensures the processing loop always handles complete pixel pairs, resizing the image as needed to maintain consistent output.
|
||||
`ImageToAsciiService` encapsulates the image-to-ASCII rendering logic, separating it from image loading and presentation concerns. It centralizes the color-token encoding and block-character strategy so callers can produce text-based previews in environments that cannot render images. By relying on [`HubConstants`](../Constants/HubConstants.cs.md) for defaults, global rendering preferences propagate naturally to this converter.
|
||||
|
||||
## Example
|
||||
```csharp
|
||||
using System.IO;
|
||||
|
||||
var stream = File.OpenRead("path/to/image.png");
|
||||
using var fs = File.OpenRead("path/to/image.png");
|
||||
var service = new ImageToAsciiService();
|
||||
string ascii = service.ConvertToAscii(stream, 80, 40);
|
||||
Console.WriteLine(ascii);
|
||||
string ascii = service.ConvertToAscii(fs);
|
||||
```
|
||||
|
||||
## Notes
|
||||
- The ASCII output relies on the presence of the {F:RRGGBB}{B:RRGGBB}{X} tags and the block characters; ensure your rendering environment understands these tokens, otherwise you will see literal tags.
|
||||
- If a height is provided as an odd number, the implementation advances to an even height, which may slightly alter the aspect ratio of the produced art.
|
||||
- The converter emits color-change tokens only when the foreground or background color differs from the previous pixel pair, which keeps the output compact for large areas of uniform color.
|
||||
- Each ASCII cell represents two vertical pixels; the image is resized to the requested `width` and `height` (defaulting to `HubConstants.AsciiArtWidth` and `HubConstants.AsciiArtHeightHalfBlock` if not specified). This can alter aspect ratio, so choose dimensions with that in mind.
|
||||
- The format relies on the tokenized color syntax (e.g. `{F:RRGGBB}` and `{B:RRGGBB}`) being understood by the consumer; renderers that ignore these tokens will display plain block characters without color.</
|
||||
Reference in New Issue
Block a user