docs: Update documentation for 145 files

Generated by AurionDocs
Job ID: c99fff50-67a3-4294-b4df-3e73f4f12de9
Source commit: 4dcb480
This commit is contained in:
Hue
2026-07-23 08:10:35 +02:00
parent 4dcb480d1d
commit f8f4e03ddd
145 changed files with 22779 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
# HueByte/EchoHub — Documentation
*Generated from branch `master`*
*Commit: `4dcb480d`*
## Languages
- **C#**: 128 files
## Statistics
- **Files processed:** 128
- **Lines of code:** 22,133
- **Symbols documented:** 598
- **Validation retries:** 77
## Documentation Index
### src/EchoHub.Client
- [AppOrchestrator](src/EchoHub.Client/AppOrchestrator.cs.md)
- [Program](src/EchoHub.Client/Program.cs.md)
### src/EchoHub.Client/Commands
- [CommandHandler](src/EchoHub.Client/Commands/CommandHandler.cs.md)
### src/EchoHub.Client/Config
- [ClientConfig](src/EchoHub.Client/Config/ClientConfig.cs.md)
- [ConfigManager](src/EchoHub.Client/Config/ConfigManager.cs.md)
### src/EchoHub.Client/Services
- [ApiClient](src/EchoHub.Client/Services/ApiClient.cs.md)
- [AsyncRunner](src/EchoHub.Client/Services/AsyncRunner.cs.md)
- [AudioPlaybackService](src/EchoHub.Client/Services/AudioPlaybackService.cs.md)
- [AvatarHelper](src/EchoHub.Client/Services/AvatarHelper.cs.md)
- [ClientEncryptionService](src/EchoHub.Client/Services/ClientEncryptionService.cs.md)
- [ClipboardFiles](src/EchoHub.Client/Services/ClipboardFiles.cs.md)
- [ClipboardImage](src/EchoHub.Client/Services/ClipboardImage.cs.md)
- [ConnectionManager](src/EchoHub.Client/Services/ConnectionManager.cs.md)
- [EchoHubConnection](src/EchoHub.Client/Services/EchoHubConnection.cs.md)
- [NativeFolderPicker](src/EchoHub.Client/Services/NativeFolderPicker.cs.md)
- [NotificationSoundService](src/EchoHub.Client/Services/NotificationSoundService.cs.md)
- [OutgoingAttachment](src/EchoHub.Client/Services/OutgoingAttachment.cs.md)
- [PathSetup](src/EchoHub.Client/Services/PathSetup.cs.md)
- [RoomKeyProtector](src/EchoHub.Client/Services/RoomKeyProtector.cs.md)
- [RoomKeyStore](src/EchoHub.Client/Services/RoomKeyStore.cs.md)
- [UpdateBackupService](src/EchoHub.Client/Services/UpdateBackupService.cs.md)
- [UpdateChecker](src/EchoHub.Client/Services/UpdateChecker.cs.md)
- [UserSession](src/EchoHub.Client/Services/UserSession.cs.md)
### src/EchoHub.Client/Themes
- [Theme](src/EchoHub.Client/Themes/Theme.cs.md)
- [ThemeManager](src/EchoHub.Client/Themes/ThemeManager.cs.md)
### src/EchoHub.Client/UI
- [MainWindow](src/EchoHub.Client/UI/MainWindow.cs.md)
### src/EchoHub.Client/UI/Chat
- [ChatColors](src/EchoHub.Client/UI/Chat/ChatColors.cs.md)
- [ChatLine](src/EchoHub.Client/UI/Chat/ChatLine.cs.md)
- [ChatListSource](src/EchoHub.Client/UI/Chat/ChatListSource.cs.md)
- [ChatMessageManager](src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md)
- [ChatSegment](src/EchoHub.Client/UI/Chat/ChatSegment.cs.md)
- [RenderHelpers](src/EchoHub.Client/UI/Chat/RenderHelpers.cs.md)
- [WelcomeBanner](src/EchoHub.Client/UI/Chat/WelcomeBanner.cs.md)
### src/EchoHub.Client/UI/Dialogs
- [AudioPlayerDialog](src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs.md)
- [ChannelPasswordDialog](src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs.md)
- [ConnectDialog](src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs.md)
- [CreateChannelDialog](src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs.md)
- [ProfileEditDialog](src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs.md)
- [ProfileViewDialog](src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md)
- [SearchDialog](src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md)
- [StatusDialog](src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md)
- [UpdateConfirmDialog](src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs.md)
### src/EchoHub.Client/UI/Helpers
- [DroppedFileParser](src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs.md)
- [EmojiHelper](src/EchoHub.Client/UI/Helpers/EmojiHelper.cs.md)
- [HexColorHelper](src/EchoHub.Client/UI/Helpers/HexColorHelper.cs.md)
- [NickColorHelper](src/EchoHub.Client/UI/Helpers/NickColorHelper.cs.md)
### src/EchoHub.Client/UI/ListSources
- [ChannelListSource](src/EchoHub.Client/UI/ListSources/ChannelListSource.cs.md)
- [SearchListSource](src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md)
- [UserListSource](src/EchoHub.Client/UI/ListSources/UserListSource.cs.md)
### src/EchoHub.Core/Constants
- [HubConstants](src/EchoHub.Core/Constants/HubConstants.cs.md)
- [MessageConventions](src/EchoHub.Core/Constants/MessageConventions.cs.md)
- [ValidationConstants](src/EchoHub.Core/Constants/ValidationConstants.cs.md)
### src/EchoHub.Core/Contracts
- [IChannelService](src/EchoHub.Core/Contracts/IChannelService.cs.md)
- [IChatBroadcaster](src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md)
- [IChatService](src/EchoHub.Core/Contracts/IChatService.cs.md)
- [IEchoHubClient](src/EchoHub.Core/Contracts/IEchoHubClient.cs.md)
- [IMessageEncryptionService](src/EchoHub.Core/Contracts/IMessageEncryptionService.cs.md)
- [IUserService](src/EchoHub.Core/Contracts/IUserService.cs.md)
### src/EchoHub.Core/DTOs
- [AccountDtos](src/EchoHub.Core/DTOs/AccountDtos.cs.md)
- [AuthDtos](src/EchoHub.Core/DTOs/AuthDtos.cs.md)
- [ChatDtos](src/EchoHub.Core/DTOs/ChatDtos.cs.md)
- [CommonDtos](src/EchoHub.Core/DTOs/CommonDtos.cs.md)
- [InviteDtos](src/EchoHub.Core/DTOs/InviteDtos.cs.md)
- [ModerationDtos](src/EchoHub.Core/DTOs/ModerationDtos.cs.md)
- [ProfileDtos](src/EchoHub.Core/DTOs/ProfileDtos.cs.md)
- [ServerDtos](src/EchoHub.Core/DTOs/ServerDtos.cs.md)
### src/EchoHub.Core/Models
- [Attachment](src/EchoHub.Core/Models/Attachment.cs.md)
- [AttachmentKind](src/EchoHub.Core/Models/AttachmentKind.cs.md)
- [Channel](src/EchoHub.Core/Models/Channel.cs.md)
- [ChannelMembership](src/EchoHub.Core/Models/ChannelMembership.cs.md)
- [InviteCode](src/EchoHub.Core/Models/InviteCode.cs.md)
- [Message](src/EchoHub.Core/Models/Message.cs.md)
- [MessageType](src/EchoHub.Core/Models/MessageType.cs.md)
- [RefreshToken](src/EchoHub.Core/Models/RefreshToken.cs.md)
- [ServerRole](src/EchoHub.Core/Models/ServerRole.cs.md)
- [ServerStatsReport](src/EchoHub.Core/Models/ServerStatsReport.cs.md)
- [User](src/EchoHub.Core/Models/User.cs.md)
- [UserStatus](src/EchoHub.Core/Models/UserStatus.cs.md)
### src/EchoHub.Core/Security
- [RoomCrypto](src/EchoHub.Core/Security/RoomCrypto.cs.md)
### src/EchoHub.Core/Services
- [AsciiBannerService](src/EchoHub.Core/Services/AsciiBannerService.cs.md)
- [FileValidationHelper](src/EchoHub.Core/Services/FileValidationHelper.cs.md)
- [ImageToAsciiService](src/EchoHub.Core/Services/ImageToAsciiService.cs.md)
### src/EchoHub.Server
- [Program](src/EchoHub.Server/Program.cs.md)
### src/EchoHub.Server.Irc
- [IrcBroadcaster](src/EchoHub.Server.Irc/IrcBroadcaster.cs.md)
- [IrcClientConnection](src/EchoHub.Server.Irc/IrcClientConnection.cs.md)
- [IrcCommandHandler](src/EchoHub.Server.Irc/IrcCommandHandler.cs.md)
- [IrcGatewayService](src/EchoHub.Server.Irc/IrcGatewayService.cs.md)
- [IrcMessage](src/EchoHub.Server.Irc/IrcMessage.cs.md)
- [IrcMessageFormatter](src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md)
- [IrcNumericReply](src/EchoHub.Server.Irc/IrcNumericReply.cs.md)
- [IrcOptions](src/EchoHub.Server.Irc/IrcOptions.cs.md)
- [IrcServiceExtensions](src/EchoHub.Server.Irc/IrcServiceExtensions.cs.md)
### src/EchoHub.Server/Auth
- [JwtTokenService](src/EchoHub.Server/Auth/JwtTokenService.cs.md)
### src/EchoHub.Server/Config
- [ServerLogsOptions](src/EchoHub.Server/Config/ServerLogsOptions.cs.md)
- [SpamOptions](src/EchoHub.Server/Config/SpamOptions.cs.md)
- [StatsOptions](src/EchoHub.Server/Config/StatsOptions.cs.md)
- [UploadLimits](src/EchoHub.Server/Config/UploadLimits.cs.md)
### src/EchoHub.Server/Controllers
- [AuthController](src/EchoHub.Server/Controllers/AuthController.cs.md)
- [ChannelsController](src/EchoHub.Server/Controllers/ChannelsController.cs.md)
- [FilesController](src/EchoHub.Server/Controllers/FilesController.cs.md)
- [InvitesController](src/EchoHub.Server/Controllers/InvitesController.cs.md)
- [ModerationController](src/EchoHub.Server/Controllers/ModerationController.cs.md)
- [ServerController](src/EchoHub.Server/Controllers/ServerController.cs.md)
- [UsersController](src/EchoHub.Server/Controllers/UsersController.cs.md)
### src/EchoHub.Server/Data
- [EchoHubDbContext](src/EchoHub.Server/Data/EchoHubDbContext.cs.md)
### src/EchoHub.Server/Hubs
- [ChatHub](src/EchoHub.Server/Hubs/ChatHub.cs.md)
### src/EchoHub.Server/Services
- [ChannelService](src/EchoHub.Server/Services/ChannelService.cs.md)
- [ChatService](src/EchoHub.Server/Services/ChatService.cs.md)
- [DirectoryClaimStore](src/EchoHub.Server/Services/DirectoryClaimStore.cs.md)
- [FileCleanupService](src/EchoHub.Server/Services/FileCleanupService.cs.md)
- [FileStorageService](src/EchoHub.Server/Services/FileStorageService.cs.md)
- [LinkEmbedService](src/EchoHub.Server/Services/LinkEmbedService.cs.md)
- [MessageEncryptionService](src/EchoHub.Server/Services/MessageEncryptionService.cs.md)
- [MuteExpirationService](src/EchoHub.Server/Services/MuteExpirationService.cs.md)
- [PresenceTracker](src/EchoHub.Server/Services/PresenceTracker.cs.md)
- [ServerDirectoryService](src/EchoHub.Server/Services/ServerDirectoryService.cs.md)
- [SignalRBroadcaster](src/EchoHub.Server/Services/SignalRBroadcaster.cs.md)
- [SpamGuard](src/EchoHub.Server/Services/SpamGuard.cs.md)
- [UserService](src/EchoHub.Server/Services/UserService.cs.md)
### src/EchoHub.Server/Services/ServerLogs
- [ServerLogsService](src/EchoHub.Server/Services/ServerLogs/ServerLogsService.cs.md)
- [ServerLogsSink](src/EchoHub.Server/Services/ServerLogs/ServerLogsSink.cs.md)
- [ServerLogsStreamService](src/EchoHub.Server/Services/ServerLogs/ServerLogsStreamService.cs.md)
### src/EchoHub.Server/Services/Stats
- [ServerStatsCollector](src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md)
- [ServerStatsReportService](src/EchoHub.Server/Services/Stats/ServerStatsReportService.cs.md)
### src/EchoHub.Server/Setup
- [DataMigrationService](src/EchoHub.Server/Setup/DataMigrationService.cs.md)
- [DatabaseSetup](src/EchoHub.Server/Setup/DatabaseSetup.cs.md)
- [FirstRunSetup](src/EchoHub.Server/Setup/FirstRunSetup.cs.md)
### src/EchoHub.Tests/Irc
- [TestHelpers](src/EchoHub.Tests/Irc/TestHelpers.cs.md)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,124 @@
# ClientConfig.cs
> **Source:** `src/EchoHub.Client/Config/ClientConfig.cs`
## Contents
- [AccountPreset](#accountpreset)
- [ClientConfig](#clientconfig)
- [NotificationConfig](#notificationconfig)
- [SavedServer](#savedserver)
---
## AccountPreset
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
> **Kind:** class
```csharp
public class AccountPreset
```
AccountPreset is a lightweight data container that groups three optional account identity properties—DisplayName, Bio, and NicknameColor—so callers can apply or persist a predefined persona for an account. It is intended for use in client configuration (ClientConfig.cs), enabling a consistent, reusable identity profile to be attached to account-related logic.
## Remarks
AccountPreset exists to keep related identity attributes together, reducing the surface area of APIs that need to accept or propagate persona data. It aligns with a configuration/templating pattern in the client, making it easier to serialize, store, and reuse account personas across components that render or modify user identity.
## Notes
- All properties are nullable; callers must define default behavior when a property is null (e.g., preserve existing values or apply a fallback).
- Null-valued properties may be serialized depending on the chosen serializer; configure to ignore nulls if you prefer a clean configuration payload.
- There is no validation here; enforce constraints instead in the surrounding configuration or UI logic.
---
## ClientConfig
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
> **Kind:** class
```csharp
public class ClientConfig
```
ClientConfig is the central container for a user's preferences and runtime state in the EchoHub client. It aggregates saved servers, the active account preset, the UI theme, notification settings, and attachment-handling options such as the download path and ASCII-rendering size.
## Remarks
It acts as a single source of truth for components that configure server connectivity, UI theming, and how attachments are stored and rendered. Centralizing defaults and user-specific values reduces duplication and helps ensure consistent behavior across sessions and test environments.
## Example
```csharp
var config = new ClientConfig
{
SavedServers = new List<SavedServer>
{
new SavedServer { Name = "Work", Url = "https://work.example", RememberMe = true }
},
DownloadPath = @"C:\Downloads",
DefaultAsciiSize = "m"
};
```
## Notes
- DownloadPath being null means attachments and saved images go to the OS Downloads folder. Ensure the application has write permissions to that location when relying on the default.
- DefaultAsciiSize accepts "s" (40×40), "m" (80×80), or "l" (120×120). This size applies to copy-paste/drag-drop attachments that do not carry a per-file size flag.
---
## NotificationConfig
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
> **Kind:** class
```csharp
public class NotificationConfig
```
NotificationConfig is a lightweight data container used by the EchoHub client to express how notifications should behave. It encapsulates three related knobs: Enabled, Volume, and SoundFile. Developers instantiate this class to configure or override the client's notification behavior when wiring up configuration (for example, within ClientConfig) or when configuring the notifier component. The defaults indicate that notifications are enabled by default, a modest default volume, and no custom sound file unless specified.
## Remarks
By grouping notification-related settings into a single object, NotificationConfig reduces coupling between components that render or play notification sounds and the rest of the configuration. It also provides a clean extension point: new knobs can be added in the future without scattering settings across call sites, since a single configuration object can be passed around.
## Example
```csharp
var config = new NotificationConfig
{
Enabled = true,
Volume = 40,
SoundFile = "assets/notify.wav"
};
```
## Notes
- Volume is stored as a byte (0255). If your UI operates in a 0100 range, map or clamp values appropriately before consumption.
- SoundFile is nullable; when it is null, the consumer should handle the absence of a custom sound (e.g., fall back to a default sound or skip audible notification based on the environment).
---
## SavedServer
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
> **Kind:** class
```csharp
public class SavedServer
```
SavedServer is a client-side representation of a per-server configuration and its associated local state for the EchoHub client. It stores credentials and connection details (Name, Url, Username, RefreshToken), a RememberMe flag, and the last connection timestamp (LastConnected). It also holds per-channel state that remains on the client: ChannelKeys (end-to-end encrypted keys cached per channel), LeftChannels (channels the user explicitly left), and LastReadMessages (per-channel read markers). These keys live only on the user's machine; the server never sees them.
## Remarks
SavedServer acts as the single source of truth for a user's relationship to a particular server within the client. By keeping ChannelKeys and LastReadMessages client-side, the app can decrypt and present channel content and maintain read state even after restarts, without leaking sensitive information to the server. LeftChannels honors user intent by preventing auto-joining of channels the user has consciously left, until they rejoin. This abstraction fits alongside other per-server configuration objects and collates server identity, credentials, and per-channel metadata for efficient session restore and UX.
## Notes
- Sensitive data such as RefreshToken and ChannelKeys should be stored securely at rest; the server never holds these values.
- These collections are mutable; ensure proper synchronization if accessed from multiple threads to avoid data races or inconsistent state.
---
@@ -0,0 +1,21 @@
# ConfigManager
> **File:** `src/EchoHub.Client/Config/ConfigManager.cs`
> **Kind:** class
```csharp
public static class ConfigManager
```
ConfigManager provides a thread-safe, single-point API for loading and persisting the client's configuration to disk. Use Load to read the current ClientConfig and Save/SaveServer/RemoveServer to apply changes from the UI or background tasks (for example, after token refreshes or updating saved servers).
## Remarks
ConfigManager stores the configuration in a JSON file named config.json inside a per-user directory (.echohub) under the current user's profile. All file I/O is serialized with a private lock (FileLock) to prevent concurrent access from UI threads and background tasks. When you call SaveServer, the code locates an existing SavedServer by URL (case-insensitive) and updates it, or appends a new one if none exists; RemoveServer deletes entries by URL. The design uses best-effort error handling—exceptions are swallowed to avoid disrupting the app—but this means persistence failures are not surfaced to callers unless they implement their own checks.
## Notes
- Persistence operations swallow all exceptions, making failures non-fatal but potentially leading to invisible data loss.
- SavedServers are deduplicated by URL using a case-insensitive comparison; updating an existing URL won't create a duplicate.
- ConfigDir uses Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); on systems where a user profile is unavailable or access is restricted, initialization may fall back to a default path.
@@ -0,0 +1,15 @@
# Program
> **File:** `src/EchoHub.Client/Program.cs`
> **Kind:** file
The Program file serves as the application's entry point and startup bootstrap for the EchoHub client. It coordinates early startup tasks such as rollback handling, permission checks, configuration provisioning, logging setup, PATH preparation, post-update cleanup, and UI initialization before handing control to the main orchestrator and theme system.
## Remarks
It functions as a central bootstrap that hides cross-cutting concerns from downstream components. By coordinating UpdateBackupService for rollback support, PathSetup for PATH hygiene, and ThemeManager for theming, it decouples startup sequencing from the rest of the application and ensures the runtime begins in a well-defined state.
## Notes
- Rollback path exits the process after attempting a restore; normal startup does not proceed.
- If appsettings.json is missing, the code seeds it from an embedded example; if the resource isn't available, startup continues with defaults.
- Several operations are best-effort and exceptions are swallowed to avoid stopping startup (e.g., Unix permissions adjustments, cleanup of a leftover .old executable).
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
# AsyncRunner
> **File:** `src/EchoHub.Client/Services/AsyncRunner.cs`
> **Kind:** class
```csharp
public static class AsyncRunner
```
Eliminates repeated Task.Run/try/catch/app.Invoke(ShowError) boilerplate by consolidating the common pattern of running background work and surfacing errors to the UI. It runs the provided async work on a background thread and routes any exceptions to the UI thread for user notification.
## Remarks
AsyncRunner encapsulates a cross-cutting concern: performing asynchronous work without blocking the UI and centralizing error reporting. It uses Task.Run to execute work off the calling thread and app.Invoke to marshal the error surface back to the UI. When an exception occurs, it logs the failure with the provided context (logContext if supplied, otherwise errorPrefix) and shows a UI message using showError prefixed by errorPrefix. Because Run is fire-and-forget (it returns void), callers should not rely on it for completion or exception propagation; choose a different pattern if you need to observe results.
## Notes
- This method is fire-and-forget; exceptions are caught and surfaced but not propagated to the caller.
- The UI update and logging rely on the provided IApplication and showError delegate; ensure they are safe to call from a background thread; app.Invoke is used to marshal to the UI thread.
@@ -0,0 +1,24 @@
# AudioPlaybackService
> **File:** `src/EchoHub.Client/Services/AudioPlaybackService.cs`
> **Kind:** class
```csharp
public class AudioPlaybackService
```
AudioPlaybackService is a thread-safe wrapper around an underlying audio player that exposes asynchronous playback controls and a finished event surface. Use it when you need serialized access to play, pause, resume, or stop audio and a consistent event interface without managing locks and state machines yourself.
## Remarks
To prevent concurrent calls from interfering with playback state, the class serializes all operations using a SemaphoreSlim. When PlayAsync is invoked while something is already playing, it stops the current track before starting the new file; PauseAsync, ResumeAsync, and StopAsync perform their actions only when appropriate states are detected. The PlaybackFinished event is forwarded from the internal player, so callers can react to completion without depending on the concrete implementation of the _player. Exceptions raised by the underlying player are caught and logged with a warning, ensuring playback issues do not crash the application.
## Example
```csharp
// Example usage
var audio = new AudioPlaybackService();
await audio.PlayAsync("path/to/file.mp3");
```
## Notes
- This wrapper serializes calls to avoid race conditions; however, it is not cancellation-aware. If you need to cancel an in-flight operation, extend the class with cancellation support or a dedicated cancellation mechanism.
@@ -0,0 +1,33 @@
# AvatarHelper
> **File:** `src/EchoHub.Client/Services/AvatarHelper.cs`
> **Kind:** class
```csharp
internal static class AvatarHelper
```
AvatarHelper centralizes the shared logic for uploading avatars by accepting either a local file path or an HTTP(S) URL, resolving the input to a stream, and uploading it via ApiClient.UploadAvatarAsync. It returns the ASCII art response from the server, providing a straightforward way to obtain the server-side representation of the uploaded avatar without duplicating local-file or network-handling code.
## Remarks
By supporting both local and remote sources behind a single UploadAsync entry point, AvatarHelper hides the mechanics of data retrieval and stream management from call sites and ensures consistent disposal of the stream. The actual upload is delegated to ApiClient, keeping concerns separated between data acquisition and server interaction. The class is internal, reinforcing its role as a reusable utility within the client layer rather than a public API.
The method propagates errors from file access, HTTP fetch, or the server upload to the caller, which is appropriate for a small, focused helper that prioritizes simplicity over internal retries or resilience policies.
## Example
```csharp
// Example usage within the same assembly
var client = new ApiClient("https://api.example.org");
string? artFromFile = await AvatarHelper.UploadAsync(client, @"C:\avatars\user.png");
string? artFromUrl = await AvatarHelper.UploadAsync(client, "https://example.org/avatars/user.png");
```
## Notes
- Creating a new HttpClient per invocation can lead to socket exhaustion in high-throughput scenarios; consider reusing a shared HttpClient instance or using HttpClientFactory in production code.
- If the local path does not exist, a FileNotFoundException is thrown.
- When targeting a URL, if the URL's file name is missing or lacks an extension, the code defaults to using avatar.png as the upload file name.
- Exceptions from the HTTP request or the server upload propagate to the caller; there is no retry logic within this helper.
@@ -0,0 +1,35 @@
# ClientEncryptionService
> **File:** `src/EchoHub.Client/Services/ClientEncryptionService.cs`
> **Kind:** class
```csharp
public sealed class ClientEncryptionService : IMessageEncryptionService
```
ClientEncryptionService provides client-side encryption for messages by applying AES-256-GCM using a key supplied by the server. It mirrors the servers encryption format so messages are encrypted end-to-end between client and server. When no key has been set, Encrypt is a no-op and returns the plaintext to preserve compatibility with unauthenticated flows; once initialized, Encrypt produces a prefixed, base64-encoded payload containing the nonce and ciphertext+tag, and Decrypt reverses this process. If decryption fails due to a missing or mismatched key or corrupted data, a sentinel message is returned to indicate the failure and prompt re-authentication to refresh the key.
## Remarks
This abstraction isolates cryptography behind a single, testable service that can be swapped or disabled without changing business logic. It enforces a clear security boundary: encryption only happens after a server-provided key is loaded, reducing the risk of leaking plaintext. The pre-key pass-through behavior preserves compatibility with existing flows during login or in environments where the key has not yet been fetched.
## Example
```csharp
// Example: encrypt and decrypt with a server-provided key
var client = new ClientEncryptionService();
// Create a 32-byte key for demonstration (replace with real server-provided key)
var keyBytes = new byte[32];
var base64Key = Convert.ToBase64String(keyBytes);
client.SetKey(base64Key);
string plaintext = "Secret message";
string encrypted = client.Encrypt(plaintext);
string decrypted = client.Decrypt(encrypted);
// decrypted should equal plaintext
```
## Notes
- Encrypt and Decrypt only work after a 32-byte key has been provided via SetKey; otherwise Encrypt returns plaintext and Decrypt returns content unchanged.
- If the encrypted content is tampered with, the key is wrong, or the payload is malformed, Decrypt returns the special placeholder: "[encrypted message — decryption failed, try re-logging to fetch the latest key]".
- The key is held in memory and is not rotated automatically; ensure proper key management and re-fetch after key rotation on the server.
@@ -0,0 +1,32 @@
# ClipboardFiles
> **File:** `src/EchoHub.Client/Services/ClipboardFiles.cs`
> **Kind:** class
```csharp
public static class ClipboardFiles
```
ClipboardFiles reads file paths from the clipboard when the clipboard contains a file-list (such as after copying files in Explorer/Finder). Use TryGetFiles to retrieve those paths so you can attach copied files directly without pasting textual paths; this works on Windows and Linux, while macOS and other platforms do not expose a file-list clipboard.
## Remarks
ClipboardFiles encapsulates platform differences behind a single API. It isolates Windows-specific CF_HDROP handling and Linux's text/uri-list retrieval, performing path existence checks and filtering out non-file entries to return a clean list of existing paths. It returns true only when at least one file is found; otherwise false, letting callers gracefully fall back to other input methods.
## Example
```csharp
if (ClipboardFiles.TryGetFiles(out var files))
{
Console.WriteLine($"Clipboard contains {files.Count} file(s): {string.Join(", ", files)}");
}
else
{
Console.WriteLine("Clipboard does not contain a file-list or contains only non-existent paths.");
}
```
## Notes
- Returns only existing files; non-existent or inaccessible paths are ignored.
- Windows implementation relies on CF_HDROP with a brief retry loop to tolerate clipboard contention.
- Linux implementation uses wl-paste or xclip (one must be available for success).
- macOS and other platforms do not provide file-list clipboard support.
@@ -0,0 +1,29 @@
# ClipboardImage
> **File:** `src/EchoHub.Client/Services/ClipboardImage.cs`
> **Kind:** class
```csharp
public static class ClipboardImage
```
Reads raw image data from the OS clipboard and returns PNG-encoded bytes suitable for saving, embedding, or transmitting. Use this when you need a single, consistent PNG representation of whatever image the user has copied (browser-copied PNGs, screenshots, editor bitmaps) so callers don't need per-OS or per-format handling.
## Remarks
This class normalizes multiple clipboard image formats into PNG. It prefers native clipboard PNG formats when available (preserving transparency) and falls back to platform clipboard bitmaps (CF_DIB on Windows) by wrapping the DIB bytes in a minimal BMP file header and decoding/re-encoding them as PNG. TryGetPng routes to OS-specific helpers and catches/logs errors, returning false on failure rather than throwing.
## Example
```csharp
// Save whatever image is on the clipboard to a file named clipboard.png
if (ClipboardImage.TryGetPng(out var png))
{
System.IO.File.WriteAllBytes("clipboard.png", png);
}
```
## Notes
- DibToPng returns null for malformed or undecodable DIB input; TryGetPng propagates that as a failure (false).
- The implementation prefers registered PNG clipboard formats to preserve alpha; CF_DIB bitmaps are re-encoded and may lose or change metadata.
- Re-encoding a bitmap to PNG allocates memory and does CPU work; callers should avoid doing this in a tight loop.
- TryGetPng checks the platform (Windows/Linux/macOS) and will return false on unsupported platforms; failures are logged rather than thrown.
@@ -0,0 +1,63 @@
# ConnectionManager.cs
> **Source:** `src/EchoHub.Client/Services/ConnectionManager.cs`
## Contents
- [ConnectionManager](#connectionmanager)
- [ConnectResult](#connectresult)
---
## ConnectionManager
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
> **Kind:** class
```csharp
internal sealed class ConnectionManager : IAsyncDisposable
```
Manages the full lifecycle of a live chat connection: authenticating with the server, establishing end-to-end encryption keys, creating and wiring the SignalR (EchoHub) connection, tracking joined channels, and exposing a thin event surface that the UI (AppOrchestrator) can subscribe to. Use this when you want a single, high-level component to own connection state and SignalR event forwarding instead of manipulating ApiClient and EchoHubConnection directly.
## Remarks
This class centralizes the responsibilities that would otherwise be scattered across UI code: authentication and token rotation, attempting to fetch and apply the E2E encryption key, instantiating and wiring an EchoHubConnection, and keeping track of which channels have been joined. It forwards SignalR events as simple .NET events so the UI layer can react without needing to know SignalR details. ConnectionManager also implements IAsyncDisposable so callers can cleanly tear down both the EchoHubConnection and the underlying ApiClient.
## Notes
- ConnectionManager may raise forwarded events from background threads (SignalR callbacks). UI handlers should marshal to the UI thread if required by the UI framework.
- ConnectAsync reports progress via the onStatus callback and will throw on authentication failure; callers are expected to handle expired saved sessions or retry logic.
- Failure to fetch the encryption key is treated as non-fatal: the manager logs a warning and proceeds without message encryption.
- Dispose of the manager (DisposeAsync) when the app shuts down to ensure the hub connection and ApiClient are cleaned up.
---
## ConnectResult
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
> **Kind:** record
```csharp
internal record ConnectResult(
LoginResponse Login,
List<ChannelDto> Channels,
Dictionary<string, List<MessageDto>> Histories)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Login` | [`LoginResponse`](../../EchoHub.Core/DTOs/AuthDtos.cs.md) | — |
| `Channels` | `List<ChannelDto>` | — |
| `Histories` | `Dictionary<string, List<MessageDto>>` | — |
Represents the outcome of a successful connection, returned to AppOrchestrator for UI updates. It bundles the authentication result, the current set of channels, and the initial histories for every auto-joined channel (keyed by channel name and including the default channel). As an immutable record, it serves as a single, self-contained snapshot that the UI can bootstrap from after a connect.
## Remarks
This object centralizes the data needed to render the initial connected state, decoupling the connection logic from the UI orchestration. By passing a single ConnectResult, the AppOrchestrator can immediately populate channel lists and histories without issuing additional fetches, promoting a clean separation between connection handling and presentation concerns.
## Notes
- ConnectResult is immutable; to reflect changes (e.g., new messages or channels), construct and pass a new instance rather than mutating the existing one.
- Histories is a dictionary keyed by channel name that contains the initial per-channel histories; ensure channel names in the dictionary align with the Channels list to avoid inconsistencies.
---
@@ -0,0 +1,158 @@
# EchoHubConnection.cs
> **Source:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
## Contents
- [ChannelPasswordRequiredException](#channelpasswordrequiredexception)
- [EchoHubConnection](#echohubconnection)
- [RoomLockedException](#roomlockedexception)
- [JoinOutcome](#joinoutcome)
---
## ChannelPasswordRequiredException
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
> **Kind:** class
```csharp
public sealed class ChannelPasswordRequiredException : Exception
```
Thrown when joining a channel fails because a password is required or the provided password is incorrect. The UI catches this to prompt the user for credentials and retry the join, using ChannelName to provide channel context.
## Remarks
ChannelPasswordRequiredException provides a precise signal for a password-related join failure. By carrying the ChannelName, it enables the UI to present a meaningful prompt and retry flow without inspecting lower-level errors. This focused exception helps keep join logic cohesive and testable by separating password-entry concerns from generic failure handling.
## Example
```csharp
try
{
// Code that attempts to join a channel and may throw ChannelPasswordRequiredException
}
catch (ChannelPasswordRequiredException ex)
{
Console.WriteLine($"Password is required to join channel '{ex.ChannelName}'.");
// Prompt the user for a password and retry the join using the provided channel name
}
```
## Notes
- Be mindful that ChannelName may be null if constructed with null; guard accordingly before displaying it to users.
---
## EchoHubConnection
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
> **Kind:** class
```csharp
public sealed class EchoHubConnection : IAsyncDisposable
```
A SignalR-backed client wrapper that manages a HubConnection to the Echo chat hub, integrates client-side encryption/room-key lookup, and exposes simple event callbacks for incoming messages, presence and channel events. Reach for EchoHubConnection when you need a higher-level, event-driven connection to the server that automatically handles authentication token provisioning and reconnect behavior while decrypting incoming payloads for the UI.
## Remarks
EchoHubConnection encapsulates the SignalR HubConnection lifecycle and maps server callbacks onto plain .NET events (e.g. OnMessageReceived, OnUserJoined, OnChannelUpdated). It supplies the HubConnectionBuilder with an AccessTokenProvider using the provided ApiClient so calls are authenticated, and it wires automatic-reconnect handlers that surface connection state changes via OnConnectionStateChanged and OnReconnected. Incoming MessageDto instances are passed through the client-side encryption pipeline (ClientEncryptionService and RoomKeyStore) so the UI sees decrypted content or a locked placeholder when a room key is not available.
## Example
```csharp
// Subscribe to events and inspect connection state
var echo = new EchoHubConnection(serverUrl, apiClient, encryptionService, roomKeyStore);
echo.OnMessageReceived += message =>
{
// MessageDto is provided by the library; content may be the LockedMessagePlaceholder
Console.WriteLine($"Message received in {message.ChannelName}: {message.Content}");
};
if (echo.IsConnected)
{
Console.WriteLine("Currently connected to the chat hub.");
}
// Remember to dispose when finished
await echo.DisposeAsync();
```
## Notes
- Events are raised directly from SignalR callbacks; handlers may not run on a UI thread — marshal to the UI thread if required.
- Encrypted messages for channels without a stored key are replaced with LockedMessagePlaceholder; supply the channel passphrase (through the app's key store flow) to see decrypted content.
- Call DisposeAsync to release the underlying HubConnection and related resources to avoid background network activity.
---
## RoomLockedException
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
> **Kind:** class
```csharp
public sealed class RoomLockedException : Exception
```
Thrown to signal a security-sensitive condition when attempting to send a message into an end-to-end encrypted channel whose room key isn't cached. The operation is blocked to prevent sending plaintext; catching this exception lets the UI prompt for the channel's passphrase and unlock the room before retrying.
## Remarks
RoomLockedException acts as a clear boundary between encryption policy and transport logic. By exposing the ChannelName, callers can present a channel-scoped unlock prompt without parsing the error text, and the sealed Exception type communicates a concrete, expected failure mode that downstream code can handle distinctly from generic errors.
## Example
```csharp
try
{
// Simulated scenario: an attempt to send into a locked E2E channel
throw new RoomLockedException("Lobby");
}
catch (RoomLockedException ex)
{
// Use the information to drive the unlock UX
Console.WriteLine(ex.Message);
Console.WriteLine($"Unlock channel: {ex.ChannelName} by entering its passphrase.");
}
```
## Notes
- Do not swallow this as a generic error; catch RoomLockedException to trigger the unlock UX and use ex.ChannelName to identify the affected channel. The displayed message is user-facing and not localized.
---
## JoinOutcome
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
> **Kind:** record
```csharp
public sealed record JoinOutcome(List<MessageDto> History, string? EncryptionSalt, string? WrappedRoomKey)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `History` | `List<MessageDto>` | — |
| `EncryptionSalt` | `string?` | — |
| `WrappedRoomKey` | `string?` | — |
JoinOutcome is a sealed record that represents the result of joining a channel: it includes the decrypted history (History) and, for end-to-end encrypted channels, the key envelope necessary to unlock the room's content key (WrappedRoomKey). EncryptionSalt is the salt used to derive the encryption key when applicable. This type is typically produced by the join logic and consumed by the UI to render messages and initialize decryption if needed.
## Remarks
This abstraction centralizes the outcome of a join into a single, immutable value that downstream components can rely on. The History is always present (even if empty), while EncryptionSalt and WrappedRoomKey are nullable to reflect that some rooms are not end-to-end encrypted or that keys may not be provisioned yet. By grouping history and encryption metadata together, the join logic can separate concerns: rendering chat versus handling cryptographic setup.
## Example
```csharp
using System.Collections.Generic;
List<MessageDto> history = new List<MessageDto>();
var joinResult = new JoinOutcome(history, null, null);
```
## Notes
- EncryptionSalt and WrappedRoomKey can be null; callers should verify non-null before attempting decryption-related steps.
---
@@ -0,0 +1,96 @@
# NativeFolderPicker.cs
> **Source:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
## Contents
- [NativeFolderPicker](#nativefolderpicker)
- [FolderPickResult](#folderpickresult)
- [PickerOutcome](#pickeroutcome)
---
## NativeFolderPicker
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
> **Kind:** class
```csharp
public static class NativeFolderPicker
```
Opens the OS-native folder picker by shelling out to the host OS, keeping the TUI free of GUI toolkit dependencies. It supports Windows, macOS, and Linux by delegating to platform-specific helpers and returns a FolderPickResult that communicates whether a folder was chosen, the dialog was cancelled, or the native picker is unavailable so the caller can fall back to a configured path.
## Remarks
NativeFolderPicker centralizes cross-platform behavior for obtaining a folder path without pulling in a GUI toolkit. It hides OS differences behind a single entry point, PickFolderAsync, and exposes a uniform result type (FolderPickResult with a PickerOutcome) that callers can inspect to either proceed with the chosen path or fall back to defaults. Failures are caught and logged, ensuring graceful degradation rather than exceptions propagating to the UI.
## Notes
- Linux will not attempt a graphical picker if no graphical session is detected (DISPLAY or WAYLAND_DISPLAY are missing); in that case, the method returns Unavailable.
- On Windows, the initial directory is sanitized (apostrophes are doubled) to safely embed the path in the PowerShell script, and PowerShell is invoked via an encoded command to avoid quoting issues.
- If the platform-specific helper cannot be started, the code falls back to returning Unavailable instead of throwing, allowing callers to implement their own fallback strategy.
---
## FolderPickResult
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
> **Kind:** record
```csharp
public sealed record FolderPickResult(PickerOutcome Outcome, string? Path)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Outcome` | `PickerOutcome` | — |
| `Path` | `string?` | — |
FolderPickResult is an immutable data carrier that represents the outcome of a native folder-picking operation and, when successful, the path of the selected folder.
## Remarks
Because FolderPickResult is a record, it benefits from value-based equality and straightforward pattern matching when consumed by calling code. The Path member is nullable to reflect that a folder may not be selected; always check the Outcome before using Path. This abstraction decouples application logic from platform-specific picker implementations, promoting testability and cross-platform compatibility.
## Example
```csharp
var result = new FolderPickResult(PickerOutcome.Success, @"C:\Projects");
if (result.Outcome == PickerOutcome.Success && result.Path is not null)
{
Console.WriteLine(result.Path);
}
```
## Notes
- Path may be null when Outcome indicates cancellation or failure; always verify Outcome before accessing Path.
---
## PickerOutcome
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
> **Kind:** enum
```csharp
public enum PickerOutcome
{
Chosen,
Cancelled,
Unavailable,
}
```
PickerOutcome encodes the result of attempting to display a native folder picker. It defines three mutually exclusive states: Chosen (the user picked a folder and FolderPickResult.Path is set), Cancelled (the native dialog ran but no selection was made), and Unavailable (no native picker is available on the current machine).
Use this enum to drive post-pick logic without scattering platform checks or error handling across call sites.
## Remarks
This enum serves as a lightweight sum type for the outcome of a folder-picking operation. It centralizes decision points and pairs with FolderPickResult to obtain the actual path when Chosen is returned. Consumers can implement a fallback flow for Unavailable and provide a smooth user experience when Cancelled.
## Notes
- Unavailable is not an error; it indicates the absence of a native picker and warrants a fallback strategy (e.g., a non-native picker or manual path entry).
---
@@ -0,0 +1,19 @@
# NotificationSoundService
> **File:** `src/EchoHub.Client/Services/NotificationSoundService.cs`
> **Kind:** class
```csharp
public class NotificationSoundService
```
NotificationSoundService centralizes the playback of the notification sound. It resolves the sound file from configuration (if specified and found) or falls back to a bundled default, then plays the sound at a configurable volume when requested. The service exposes SetEnabled and SetVolume for simple runtime tuning, and PlayAsync for normal operation or PlayTestAsync for QA scenarios where playback should occur regardless of the Enabled flag. Internally it uses a semaphore to serialize concurrent playback, and a 10-second timeout to prevent a stuck caller if the sound does not finish.
## Remarks
The class isolates all concerns around audio playback: path resolution, volume handling, concurrency, and fault tolerance. By hiding these details behind a single service, higher-level notification logic can simply request a sound without worrying about file presence, logging, or synchronization. The design anticipates environments where a sound file might be missing or playback might stall, and it ensures resources are released and the system remains responsive.
## Notes
- Silent fallback if a sound file cannot be found; production environments should ensure the asset exists if audible alerts are required.
- The PlaybackFinished event and the 10-second timeout guard the system against hangs; the lock may be released before the sound finishes, which means subsequent playback requests can start while a prior one is still playing.
- PlayAsync respects the Enabled flag, while PlayTestAsync allows testing the sound regardless of Enabled.
@@ -0,0 +1,46 @@
# OutgoingAttachment
> **File:** `src/EchoHub.Client/Services/OutgoingAttachment.cs`
> **Kind:** record
```csharp
public sealed record OutgoingAttachment(
Stream Stream,
string FileName,
string? DeclaredKind = null,
string? EncryptedPreview = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Stream` | `Stream` | — |
| `FileName` | `string` | — |
| `DeclaredKind` | `string?` | `null` |
| `EncryptedPreview` | `string?` | `null` |
OutgoingAttachment is a transport object that represents a single file to upload as part of a message. It bundles the data Stream and FileName, and optionally carries DeclaredKind and EncryptedPreview for encrypted channels, while non-encrypted channels typically set only Stream and FileName.
## Remarks
OutgoingAttachment serves as a compact, immutable data carrier that travels through the sending pipeline. As a record, it uses value-based equality which helps comparisons and deduplication when attachments are tracked across requests. It also clarifies ownership: the record does not manage the lifetime of the underlying Stream; callers are responsible for opening and disposing streams as appropriate.
## Example
```csharp
using System.IO;
// Normal channel usage: only Stream and FileName are provided
var data = new byte[] { 0x01, 0x02, 0x03 };
var stream = new MemoryStream(data);
var attachment = new OutgoingAttachment(stream, "data.bin");
// End-to-end encrypted channel usage: DeclaredKind and EncryptedPreview are set
var ciphertext = new MemoryStream(new byte[] { 0xAA, 0xBB, 0xCC });
var asciiPreview = @"ASCII_ART_PREVIEW";
var encryptedAttachment = new OutgoingAttachment(ciphertext, "image.png", "image", asciiPreview);
```
## Notes
- The lifetime of the underlying Stream is not managed by OutgoingAttachment; the caller must ensure the stream is disposed when appropriate.
- DeclaredKind and EncryptedPreview are intended for encrypted channels; in normal channels these values are typically null.
@@ -0,0 +1,24 @@
# PathSetup
> **File:** `src/EchoHub.Client/Services/PathSetup.cs`
> **Kind:** class
```csharp
public static class PathSetup
```
PathSetup is a cross-platform helper that ensures the application's directory is present on the system PATH, enabling commands like echohub to be run from any terminal session without specifying the full path. EnsureOnPath checks for the directory and, if missing, updates PATH in a platform-appropriate way: Windows updates the user PATH; Unix-like systems append an export line to common shell profile files.
## Remarks
By centralizing PATH manipulation, this abstraction reduces code duplication and the risk of divergent PATH states across platforms. It uses a lightweight, best-effort approach and logs outcomes to aid diagnostics when PATH updates fail or are skipped. The addition is clearly marked by a PathMarker to avoid duplicating lines in shell profiles.
## Example
```csharp
PathSetup.EnsureOnPath();
```
## Notes
- The method swallows exceptions and logs at debug level, so callers should not rely on exceptions to signal failure.
- Unix updates affect the user's shell environment; new terminal sessions are typically required to observe changes.
- Windows updates are done at the per-user level; system-wide PATH is not modified.
@@ -0,0 +1,30 @@
# RoomKeyProtector
> **File:** `src/EchoHub.Client/Services/RoomKeyProtector.cs`
> **Kind:** class
```csharp
public sealed class RoomKeyProtector
```
Encrypts cached room content keys at rest so the client config never holds them as plain base64. Windows uses DPAPI (current-user scope, format prefix "dp1:"). On other platforms the keys are AES-GCM encrypted with a per-user master key file stored next to the config with 0600 permissions (prefix "k1:") — without an OS keychain that is file-permission-level protection, not zero-knowledge: anyone who can read both the config and the key file can recover the room keys. Values with no recognized prefix are legacy plain-base64 keys from older clients; they load once and are re-encrypted. The room passphrase itself is never stored in any form.
The RoomKeyProtector class provides a single API surface to protect and unprotect per-user room keys across platforms. The Protect method returns a string suitable for storage in the config, automatically selecting the appropriate protection mechanism for the current OS (DPAPI on Windows, file-based AES-GCM on others). TryUnprotect decodes a stored value back into a room key, reporting whether the value was a legacy (unencrypted) entry and whether the decryption succeeded. The implementation intentionally hides platform differences behind a consistent interface, so callers can persist and reload keys without worrying about the underlying cryptosystem.
The constructor accepts a directory that holds the master key file and an optional flag to override the OS-provided protection path (useful for tests). The key file path is derived from the directory by appending the fixed file name roomkeys.key. Key loading is guarded by a small lock and the master key is cached after the first read. The Protect path prefixes the output to indicate how the data is protected ("dp1:" or "k1:").
The class ensures the room passphrase itself is never persisted, and it gracefully tolerates missing or unreadable key material by returning false from TryUnprotect (leaving the caller to prompt the user for action).
````csharp
// Typical usage
var protector = new RoomKeyProtector("/config");
byte[] roomKey = new byte[32]; // obtain from a secure source
string stored = protector.Protect(roomKey);
if (protector.TryUnprotect(stored, out var recovered, out bool wasLegacy))
{
// recovered contains the room key if the value was decryptable
// wasLegacy is true only if the input was a legacy base64 key without a prefix
}
````
@@ -0,0 +1,43 @@
# RoomKeyStore
> **File:** `src/EchoHub.Client/Services/RoomKeyStore.cs`
> **Kind:** class
```csharp
public sealed class RoomKeyStore
```
Holds and manages end-to-end encrypted room keys for a single client instance: it keeps a decrypted, in-memory cache for the active session and a per-server persisted, encrypted copy so users do not have to re-enter passphrases each launch. Use RoomKeyStore when you need a thread-safe local store that provides room keys to the runtime and ensures keys are encrypted at rest via RoomKeyProtector.
## Remarks
RoomKeyStore links transient runtime state with the client's persisted configuration. It binds to a server (LoadForServer), loads that server's saved ChannelKeys (unprotecting them with RoomKeyProtector), and exposes methods to read, add, replace, or remove keys while persisting changes back to the SavedServer entry. It also records which channels are known to be encrypted so callers can avoid emitting plaintext into rooms without a cached key. The class performs a one-way upgrade of legacy unprotected entries to the protected format when possible and logs unreadable entries rather than failing.
## Example
```csharp
var store = new RoomKeyStore();
store.LoadForServer("https://chat.example.com");
// Generate and store a new room key for a channel
byte[] newKey = RoomCrypto.GenerateRoomKey();
store.StoreKey("#team-room", newKey);
// Retrieve a key for sending encrypted messages
if (store.TryGetKey("#team-room", out var key))
{
// Use `key` with RoomCrypto API to encrypt message content
}
// Accept an encrypted envelope and store the unwrapped key only if the KEK opens it
string wrapped = "..."; // envelope string received
byte[] kek = /* key-encryption-key */ new byte[RoomCrypto.KeySizeBytes];
if (store.TryStoreFromEnvelope("#other-room", wrapped, kek))
{
// successfully unwrapped and cached
}
```
## Notes
- Call LoadForServer(serverUrl) before persisting or retrieving server-scoped keys; the store clears and reinitializes its cache when bound to a server.
- Legacy (plain/base64) saved entries are upgraded to the protector-backed format when possible; entries that cannot be unprotected are ignored and logged.
- The class uses an internal lock for basic thread-safety of the in-memory cache; avoid holding returned keys while performing long synchronous work that might race with store mutations.
@@ -0,0 +1,86 @@
# UpdateBackupService.cs
> **Source:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
## Contents
- [BackupJsonContext](#backupjsoncontext)
- [UpdateBackupService](#updatebackupservice)
- [BackupInfo](#backupinfo)
---
## BackupJsonContext
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
> **Kind:** class
```csharp
[System.Text.Json.Serialization.JsonSerializable(typeof(BackupInfo))]
internal partial class BackupJsonContext : System.Text.Json.Serialization.JsonSerializerContext
```
BackupJsonContext is an internal partial class that provides the source-generated JSON serialization metadata for the BackupInfo type. It plugs into System.Text.Jsons source generator, enabling reflection-free serialization of BackupInfo when you configure a JsonSerializerOptions with this context.
## Remarks
This symbol acts as the concrete carrier of serialization metadata for BackupInfo within the JSON pipeline of EchoHubs client. By centralizing the generated type information in a single context, it keeps serialization concerns isolated from business logic and allows the type to evolve without scattering attributes across multiple call sites. The pattern here—one generated context per data contract—supports predictable performance improvements while preserving a clean, minimal public surface.
## Notes
- The symbol is internal; it is intended for use within the containing assembly, not by external callers.
- The class is generated and partial; do not edit it by hand, as changes will be overwritten by the source generator.
- If you modify the BackupInfo shape, you must re-run code generation to keep the context in sync with the data contract.
---
## UpdateBackupService
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
> **Kind:** class
```csharp
public static class UpdateBackupService
```
Manages pre-update backups for the auto-updater and provides rollback support by snapshotting the running application prior to an update. Backups are stored under ~/.echohub/update-backup/ as backup.zip with a companion backup-info.json that records the version, application directory, and UTC timestamp. Use CreateBackup before applying an update; verify presence with BackupExists and inspect metadata with GetBackupInfo to drive a rollback if needed. The IsPostUpdate flag signals that a backup from a recent update exists, allowing startup logic to react accordingly.
---
## BackupInfo
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
> **Kind:** record
```csharp
public record BackupInfo(
string Version,
string AppDirectory,
DateTimeOffset CreatedAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Version` | `string` | — |
| `AppDirectory` | `string` | — |
| `CreatedAt` | `DateTimeOffset` | — |
BackupInfo is a lightweight, value-like record that encapsulates metadata about a created backup. It carries the backup Version, the AppDirectory that was backed up, and the CreatedAt timestamp, enabling complete backup metadata to be passed around as a single unit.
## Remarks
BackupInfo, being a record with positional parameters, is immutable and benefits from value-based equality. This makes it ideal as a canonical data carrier when the UpdateBackupService reports or persists backup information, or when UI/logging layers need to compare or display backup entries.
## Example
```csharp
var backup = new BackupInfo(
Version: "1.2.3",
AppDirectory: "/opt/MyApp",
CreatedAt: DateTimeOffset.UtcNow
);
```
## Notes
- Records provide structural equality; two instances with the same Version, AppDirectory, and CreatedAt compare as equal.
- CreatedAt uses DateTimeOffset to preserve offset information; prefer UTC (DateTimeOffset.UtcNow) when constructing backups to avoid timezone ambiguities.
---
@@ -0,0 +1,36 @@
# UpdateChecker
> **File:** `src/EchoHub.Client/Services/UpdateChecker.cs`
> **Kind:** class
```csharp
public sealed class UpdateChecker : IDisposable
```
Checks for application updates in the background, presents a TUI confirmation dialog when a new version is available, and defers the actual download/extract/restart work until after the terminal UI has been shut down. Use this class when the host application runs a Terminal.Gui main loop and needs a safe way to offer in-place updates without deadlocking the console or performing heavy I/O while the TUI still owns the terminal.
## Remarks
This class encapsulates polling and manual update checks via an internal Updater instance and marshals user interaction back onto the provided IApplication main loop using _app.Invoke. When the user confirms an update, UpdateChecker does not perform the network/download work immediately; instead it sets PendingUpdate to an awaitable callback (ApplyUpdateAsync), stores the selected version, and requests the TUI to stop. The host is expected to call PendingUpdate after the main loop exits and the console has been restored so the update process can safely run headless (the Updater's update flow may restart the process and call Environment.Exit).
## Example
```csharp
// During application startup
var updateChecker = new UpdateChecker(app);
updateChecker.Start(); // starts periodic checks in RELEASE builds
// ... run Terminal.Gui main loop ...
// After the main loop exits and the console is restored, run any pending update
if (updateChecker.PendingUpdate != null)
{
await updateChecker.PendingUpdate();
}
```
## Notes
- Start only activates the background poller in RELEASE builds (the Start method is no-op in non-RELEASE builds).
- PendingUpdate is deliberately set to a Task-returning delegate and intended to be invoked by the host after the TUI has fully stopped; running it while the TUI still owns the console can deadlock the restart flow.
- ApplyUpdateAsync attempts to create a pre-update backup with UpdateBackupService.CreateBackup; backup creation failures are logged and the update continues.
- ApplyUpdateAsync sets Console.OutputEncoding = UTF8 but swallows exceptions (useful when stdout is redirected or non-interactive).
- CurrentVersion reads the assembly version and falls back to "0.0.0" if unavailable.
@@ -0,0 +1,19 @@
# UserSession
> **File:** `src/EchoHub.Client/Services/UserSession.cs`
> **Kind:** class
```csharp
internal sealed class UserSession
```
Stores the current user's session state on the client, including username, online status, and an optional status message. Use this type as a lightweight, centralized container when you need to read or mutate the ephemeral session data for the active user, and call Reset to return all fields to their defaults (empty username, Online status, and no status message).
## Remarks
Internally sealed and non-public, this class keeps the session representation stable within the client service layer and prevents inheritance. It relies on the UserStatus enum from the core models to express the user's current state consistently across the application.
## Notes
- Not thread-safe by default; coordinate concurrent access if used from multiple threads.
- Reset mutates state in place; if you require preserving data, capture it before calling Reset.
- StatusMessage is nullable; null indicates that no message is provided.
@@ -0,0 +1,52 @@
# Theme.cs
> **Source:** `src/EchoHub.Client/Themes/Theme.cs`
## Contents
- [Theme](#theme)
- [ThemeColors](#themecolors)
---
## Theme
> **File:** `src/EchoHub.Client/Themes/Theme.cs`
> **Kind:** class
```csharp
public class Theme
```
Theme is the central container for theming the EchoHub client UI. It holds a Name and four color palettes (Base, Menu, Dialog, Status) used across the main window and its chrome; plus an optional Border palette that can override only the frame borders while the rest remains tied to Base. If Border is null, the border colors fall back to the Base palette, letting themes tone borders down independently from text to achieve effects like glassy translucency. The palettes default to new ThemeColors instances, so a Theme is immediately usable and developers only configure what they need. Border supports hex literals like "#6E6E6E" and named colors, enabling quick tweaks without changing the rest of the palette.
## Remarks
Theme isolates brand identity and UI chrome from layout logic, enabling themes to be swapped at runtime or per user preference. The per-area color groups—Base, Menu, Dialog, and Status—provide visual consistency while allowing targeted overrides; Border offers a focused knob for edge treatment without touching text colors. This composition reduces duplication: a single Theme can render across the chrome, with optional Border overrides to achieve distinctive looks without rewriting color logic.
## Notes
- Name is marked as required; always provide a non-empty value during initialization.
- Border is nullable. If you don't set it, the UI uses Base colors for borders; set Border when you want to tint borders independently.
- Hex codes and named colors: ensure strings you assign are valid color tokens understood by the theming system to avoid fallback or misrendering.
---
## ThemeColors
> **File:** `src/EchoHub.Client/Themes/Theme.cs`
> **Kind:** class
```csharp
public class ThemeColors
```
ThemeColors is a small data container that holds the color choices used by the UI theme. It exposes four properties—Foreground, Background, FocusForeground, and FocusBackground—each with a sensible default (White on Black for normal state, and White on Blue for focused state). This class centralizes theming values so UI components can render consistently and themes can be swapped by supplying a ThemeColors instance rather than scattering color literals throughout rendering code.
## Remarks
- It acts as a cohesive value object for theming, separating concerns between color data and rendering logic.
- It enables swapping themes by replacing one ThemeColors instance rather than modifying rendering code.
- It is mutable, allowing runtime theme adjustments; if a ThemeColors instance is shared across threads, consider synchronization to avoid race conditions.
## Notes
- If you mutate and share ThemeColors across threads, you may encounter race conditions; prefer per-thread copies or proper synchronization when updating values.
---
@@ -0,0 +1,601 @@
# ThemeManager.cs
> **Source:** `src/EchoHub.Client/Themes/ThemeManager.cs`
## Contents
- [ThemeManager](#thememanager)
- [ApplyTheme](#applytheme)
- [BuildColorScheme](#buildcolorscheme)
- [GetAvailableThemes](#getavailablethemes)
- [GetTheme](#gettheme)
- [ParseColor](#parsecolor)
- [SaveTheme](#savetheme)
- [ClassicTheme](#classictheme)
- [DefaultTheme](#defaulttheme)
- [DraculaTheme](#draculatheme)
- [HackerTheme](#hackertheme)
- [HighContrastTheme](#highcontrasttheme)
- [JsonOptions](#jsonoptions)
- [LightTheme](#lighttheme)
- [MonokaiTheme](#monokaitheme)
- [OceanTheme](#oceantheme)
- [SolarizedTheme](#solarizedtheme)
- [ThemeDir](#themedir)
- [TransparentLightTheme](#transparentlighttheme)
- [TransparentTheme](#transparenttheme)
- [BuiltInThemes](#builtinthemes)
- [GruvboxTheme](#gruvboxtheme)
- [NordTheme](#nordtheme)
- [RosePineTheme](#rosepinetheme)
---
## ThemeManager
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** class
```csharp
public static class ThemeManager
```
ThemeManager_overview provides a centralized, static API for discovering, loading, applying, and persisting themes used by the EchoHub client UI. Call GetAvailableThemes to enumerate built-in and user-defined themes, GetTheme to fetch a theme by name, and ApplyTheme to switch the UI to a chosen theme.
## Remarks
Conceptually, ThemeManager acts as the bridge between Theme data (the Theme class) and the runtime UI. It maintains a curated list of built-in themes and exposes logic to load additional themes from a user directory, surfacing them for selection without requiring changes to the runtime code. In addition, BuildColorScheme ensures color assignments for text areas align with the active theme, pinning Editable/ReadOnly roles so that transparent themes render correctly and inputs stay legible. This centralizes theming concerns and keeps theme-related behavior in one place, simplifying maintenance and experimentation with new themes.
## Example
```csharp
var available = ThemeManager.GetAvailableThemes();
var theme = ThemeManager.GetTheme("Default");
ThemeManager.ApplyTheme(theme);
```
## Notes
- SaveTheme is best-effort and silently swallows failures; verify persistence if you rely on saved themes.
- GetAvailableThemes falls back to built-in themes when the theme directory cannot be read.
- ParseColor expects valid color identifiers defined by the theming system; supply colors that exist in the library or your Theme colors.
---
### ApplyTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
public static void ApplyTheme(Theme theme)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `theme` | [`Theme`](Theme.cs.md) | — |
**Returns:** `void`
Applies a Theme by registering color schemes for the core UI areas with SchemeManager. This single call maps the Theme's Base, Menu, Dialog, and optional Border sections to named schemes so the rest of the UI can render consistently according to the active theme.
## Remarks
This method acts as a bridge between the Theme model and SchemeManager's scheme registry. It delegates color construction to BuildColorScheme for each region, ensuring Base, Menu, and Dialog colors stay in sync. The Border scheme uses theme.Border when provided, otherwise it falls back to the Base palette to preserve a coherent frame. By applying all four schemes in one place, ApplyTheme reduces the risk of components diverging toward inconsistent styling.
## Notes
- Repeatedly calling ApplyTheme overwrites the previously registered schemes, so batch theme updates if you want to avoid intermediate flashes.
- The Border palette falls back to Base when Border is not provided; ensure the Base colors reflect the desired frame in that case.
## Dependencies
- SchemeManager
---
### BuildColorScheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
private static Scheme BuildColorScheme(ThemeColors colors)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `colors` | [`ThemeColors`](Theme.cs.md) | — |
**Returns:** `Scheme`
BuildColorScheme converts ThemeColors into a Terminal.Gui Scheme by deriving two Attributes—Normal from Foreground and Background and Focus from FocusForeground and FocusBackground—then applying them to the Scheme's state properties (Normal, Focus, HotNormal, HotFocus, Disabled). It also pins Editable and ReadOnly to Normal to ensure input controls render against the theme background, avoiding opaque boxes in transparent themes.
## Remarks
This method centralizes the theme-to-scheme translation, decoupling ThemeColors from the Scheme used by the UI. By deriving Normal and Focus once and reusing them for all relevant roles, and by tying Editable/ReadOnly to Normal, it guarantees consistent visual behavior for standard controls and editable regions across themes. The method being private static signals that it's an internal detail of the theming pipeline used by ThemeManager to assemble the active color scheme.
## Notes
- If ThemeColors contain invalid color strings, ParseColor may throw; ensure colors are validated before calling BuildColorScheme.
- The returned Scheme is a new object each time; repeated calls may impact allocations.
- Editable and ReadOnly are deliberately mapped to Normal; if you need distinct input backgrounds, adjust the mapping accordingly.
---
### GetAvailableThemes
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
public static List<Theme> GetAvailableThemes()
```
**Returns:** `List<Theme>`
The GetAvailableThemes method returns a list of Theme objects by starting with the built-in themes and augmenting that set with user-defined themes discovered in the ThemeDir directory. It iterates over all *.json files, deserializes each one into a Theme using JsonSerializer with the configured JsonOptions, and, if the resulting theme has a non-empty Name and does not duplicate an existing theme (case-insensitive comparison on Name), appends it to the collection. If ThemeDir does not exist or any IO or JSON parsing error occurs, the method gracefully falls back to returning only the built-in themes.
## Remarks
This function encapsulates the theme-loading strategy: built-in themes establish the default baseline, while external JSON themes extend the collection without mutating the originals. It operates defensively, skipping malformed files and continuing execution in the face of read errors, which yields a predictable return value even under partial failure. De-duplication is driven by Theme.Name using a case-insensitive comparison to prevent accidental duplicates when names differ only by case.
## Notes
- It swallows IO and JSON parsing exceptions, so failures to read or parse individual files do not propagate to the caller.
- Built-in themes take precedence: a user-defined theme with a Name that matches an existing built-in theme is ignored, ensuring stable baseline behavior.
---
### GetTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
public static Theme GetTheme(string name)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `name` | `string` | — |
**Returns:** [`Theme`](Theme.cs.md)
Resolves a Theme by name by searching the collection returned by GetAvailableThemes and returning the first match found when the theme name equals the provided name, ignoring case. It is the right choice when you need to map a user-provided theme name (from UI, config, or input) to a Theme object, with a fallback to DefaultTheme if no match exists.
## Remarks
By centralizing theme resolution in this single method, callers can map a string (for example, user input) to a Theme object without duplicating comparison logic or null checks. The use of ordinal string comparison ensures consistent, culture-invariant matching across locales. The method relies on GetAvailableThemes providing a valid collection and on DefaultTheme representing a concrete theme.
## Notes
- If GetAvailableThemes returns null, the call to Find will throw a NullReferenceException.
- The search is linear in the size of the themes collection; for large catalogs consider caching or indexing to improve lookup performance.
- Name comparison uses OrdinalIgnoreCase; if you need culture-aware matching, replace with a culture-aware comparison or normalize names elsewhere.
---
### ParseColor
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
private static Color ParseColor(string colorName)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `colorName` | `string` | — |
**Returns:** `Color`
Parses a color name into a Color using Color.TryParse. If parsing succeeds, it returns the resulting Color (or White if the parsed color is null). If parsing fails, it returns Color.White. Use this helper when theme code needs to translate a color name string into a Color value, ensuring a valid color is always returned instead of propagating nulls.
## Remarks
Centralizes color-name parsing, reducing duplication and guarding ThemeManager's rendering paths against invalid color inputs. The fallback to White makes the UI predictable but at the risk of hiding misconfigurations; consider logging when a fallback occurs to aid debugging.
## Notes
- Invalid or unknown color names yield Color.White without throwing.
- No exception is thrown; a deterministic Color is always returned.
---
### SaveTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** method
```csharp
public static void SaveTheme(Theme theme)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `theme` | [`Theme`](Theme.cs.md) | — |
**Returns:** `void`
Persists a Theme by serializing it to JSON and writing it to a file named after the theme under ThemeDir. Use SaveTheme to persist a user-selected theme so it can be reloaded on startup; it's a best-effort operation that silently swallows failures, so callers shouldn't rely on it for critical persistence.
## Remarks
This abstraction encapsulates the simple idea of theme persistence: ensure the target directory exists, determine a file path from the Theme.Name, serialize to JSON using JsonOptions, and write the content. It uses Theme.Name as the file name, so two themes with the same name will overwrite each other; an enhanced naming strategy or unique IDs could help. Failures are swallowed, so any persistence failure is invisible to the caller; consider adding logging or a higher-level retry if persistence must be durable. The method depends on JsonOptions for serialization behavior and relies on the standard IO primitives (Directory, Path, JsonSerializer, File).
## Notes
- The catch-all block hides errors; callers cannot detect save failures.
- Using Theme.Name directly as a file name may introduce invalid characters or path traversal risks if Name isn't sanitized.
- Existing theme JSON will be overwritten without backup or versioning.
---
### ClassicTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme ClassicTheme = new()
```
ClassicTheme is a private static readonly Theme that encapsulates the classic visual styling used by the UI. It defines the 'Classic' theme name and assigns color palettes for four UI zones — Base, Menu, Dialog, and Status — so the theming system can render consistent foregrounds, backgrounds, and focus states across the application.
## Remarks
Why this abstraction exists: centralizes the classic color palette in one place, avoiding repetitive literals across components. It also stabilizes the look by exposing a single instance that the ThemeManager can switch to internally to apply the classic aesthetic. In short, ClassicTheme acts as the canonical, versioned styling bundle for the traditional UI appearance.
## Notes
- Potential mutability: If Theme or ThemeColors expose public setters, the colors may be mutated after initialization. Consumers should rely on a stable palette or the code should enforce immutability.
- Accessibility considerations: The palette uses high-contrast combinations (e.g., White foreground on DarkGray/Blue). If your accessibility requirements change, adjust this Theme instance or provide alternative themes.
---
### DefaultTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme DefaultTheme = new()
```
Defines the canonical default theme used by the UI components within the ThemeManager. This private static readonly field initializes a single Theme instance named 'Default' with color settings for each UI region (Base, Menu, Dialog, Status). The nested ThemeColors specify the foreground, background, and focus colors, establishing a consistent look-and-feel across the application unless overridden by other theme configurations. Because it is static and readonly, the instance is created once at type initialization and cannot be reassigned, ensuring all consumers relying on the default palette see the same values.
## Remarks
Centralizes the default visual styling to ensure a single, shared baseline across the UI. It prevents scattering color choices across components and makes it easier to reason about the default appearance of the application. If a different baseline is needed for testing or special scenarios, a separate Theme can be created and applied through the ThemeManager, rather than modifying this field.
## Notes
- The field is private; external code cannot access or mutate DefaultTheme directly.
- Even though the reference is readonly, the nested ThemeColors objects may be mutable if their properties are settable; treat the default palette as effectively immutable at runtime unless you deliberately mutate its contents within ThemeManager.
- The color values are provided as names (e.g., 'Gray', 'White'); ensure the rendering layer recognizes these tokens to avoid unexpected visuals.
---
### DraculaTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme DraculaTheme = new()
```
DraculaTheme is a private static readonly Theme field that represents the Dracula-inspired color palette used by the theme system. It defines distinct color specifications for four UI surfaces—Base, Menu, Dialog, and Status—each with a foreground color, a background color, and explicit focus colors to ensure consistent, high-contrast visuals across the application. This field is intended for internal use by ThemeManager to apply a cohesive dark theme; external code should not rely on it directly.
## Remarks
Having a single DraculaTheme instance centralizes the Dracula look, preventing drift in color choices across components. By keeping it private and readonly, ThemeManager can switch to Dracula without duplicating palettes, while still allowing other themes to be composed similarly. The explicit focus colors help maintain clear keyboard-navigation states even on dark surfaces.
## Notes
- Private visibility prevents external code from referencing DraculaTheme directly.
- It is static readonly and assigned once; runtime mutation is not expected.
- Token names like BrightMagenta and Magenta map to concrete colors in the rendering layer; ensure the color system supports these tokens for accurate rendering.
---
### HackerTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme HackerTheme = new()
```
HackerTheme is a private, static, readonly Theme instance that encodes the color palette used by the Hacker appearance within the UI. It defines the colors for the Base, Menu, Dialog, and Status areas, providing a single source of truth that ThemeManager can apply to render a consistent dark-themed interface.
## Remarks
This field centralizes the Hacker color scheme, ensuring consistent foreground/background pairs across all UI regions and their focus states. Because HackerTheme is private to ThemeManager, external code cannot reference or mutate it directly; changes to the palette must go through ThemeManager's public API or future extensions. The nested ThemeColors per region make it easy to tweak the palette in one place when refining the visual language.
## Notes
- The static readonly modifier means HackerTheme is initialized once and its reference cannot be reassigned, but the contained ThemeColors objects may still be mutable depending on their type.
- External code should not rely on HackerTheme having a public accessor; to reuse the palette publicly, ThemeManager should expose a proper API rather than exposing internal details.
---
### HighContrastTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme HighContrastTheme = new()
```
Defines a private static readonly Theme instance named HighContrastTheme that captures a high-contrast color scheme used by the theming subsystem. It specifies color configurations for the Base, Menu, Dialog, and Status surfaces to maximize legibility and clearly indicate focus against a dark background.
## Remarks
HighContrastTheme centralizes the high-contrast styling to avoid scattering color values throughout the codebase. The ThemeManager can switch to this theme to satisfy accessibility requirements without exposing public API changes.
## Notes
- The nested ThemeColors objects may be mutable; mutating them would undermine the high-contrast guarantee. Treat HighContrastTheme as an internal constant and avoid altering its color properties at runtime.
---
### JsonOptions
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly JsonSerializerOptions JsonOptions = new()
```
JsonOptions is a privately scoped, preconfigured JsonSerializerOptions instance used by ThemeManager to serialize JSON with the projects conventions. It enables indented output and camelCase property naming, ensuring that any JSON emitted while theming is both human-readable and aligned with the API surface.
## Remarks
By using a private static readonly field, ThemeManager avoids repeated allocations and guarantees a single shared configuration for its JSON serialization within the class. Note that JsonSerializerOptions is mutable; while the field reference cannot be reassigned, changing its properties at runtime can lead to subtle, cross-call side effects. Treat this instance as effectively immutable after initialization.
## Example
```csharp
// Within ThemeManager
var data = new { Theme = "Dark", Version = 1 };
string json = JsonSerializer.Serialize(data, JsonOptions);
```
## Notes
- Mutating JsonOptions at runtime can cause inconsistent formatting across serialized outputs; prefer making changes only during initialization.
- This field is internal to ThemeManager; if different parts of the application require alternative formatting, construct and pass their own JsonSerializerOptions instead of reusing JsonOptions.
---
### LightTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme LightTheme = new()
```
LightTheme is a predefined Theme instance that encodes the light-mode color configuration used by the UI. It centralizes the color values for the base surface and for Menu, Dialog, and Status regions so the theming system can apply a consistent light appearance without constructing a new Theme object each time.
## Remarks
By consolidating the light palette in a single static object, LightTheme ensures visual consistency across components that render base surfaces, menus, dialogs, and status bars. It serves as a canonical reference for the light aesthetic within the theming subsystem, enabling ThemeManager to switch to a known, shared configuration. Because the field is private static readonly, it should be treated as a shared, effectively immutable source at runtime; mutating its nested color objects could lead to inconsistent visuals.
## Notes
- It is a static shared instance; mutating its nested ThemeColors at runtime would have global effects; treat as read-only after initialization.
---
### MonokaiTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme MonokaiTheme = new()
```
MonokaiTheme is a private static readonly field that defines the Monokai color palette used by the theme system. It holds a Theme named "Monokai" composed of four color blocks (Base, Menu, Dialog, Status), each described by ThemeColors with specific foreground, background, and focus colors. This single, shared instance provides a consistent color vocabulary for the UI, allowing ThemeManager and related rendering code to apply the Monokai look uniformly without scattering literals across the codebase. Because the field is private, its usage is internal to the class that declares it.
## Remarks
MonokaiTheme serves as a centralized, reusable color configuration for the Monokai look. By grouping color sets into Base, Menu, Dialog, and Status, it expresses distinct chrome regions while keeping a single source of truth for the palette. This abstraction makes it straightforward for ThemeManager and UI components to consistently apply the Monokai styling.
## Notes
- The Theme and ThemeColors instances are mutable; altering their properties would mutate the shared theme at runtime and affect all consumers within the process.
- External code cannot replace MonokaiTheme, but internal code could adjust its nested properties unless immutability is enforced; consider making Theme/ThemeColors immutable if a fixed theme is intended.
---
### OceanTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme OceanTheme = new()
```
OceanTheme is a predefined ocean-inspired color palette represented as a Theme instance. It groups color configurations for four UI regions—Base, Menu, Dialog, and Status—each with foreground, background, and focus colors, enabling a cohesive look across the application. The field is private static readonly, so the same Theme object is created once and reused, preventing accidental reassignment while keeping internal mutability restricted to the defining class.
## Remarks
Centralizes theming decisions and reduces duplication by providing a single, cohesive palette that UI components can rely on. OceanTheme expresses a clear design intent (an ocean-like aesthetic) and is intended to be selected by theming logic to apply a consistent appearance across Base, Menu, Dialog, and Status surfaces. The per-area ThemeColors allow distinct focus and interaction states while preserving a unified visual language.
## Notes
- Access is private to the ThemeManager class, preventing external code from directly reusing or mutating OceanTheme.
- The reference is readonly, so the field cannot be reassigned; internal mutability would require explicit code within the defining class.
- The color tokens (e.g., BrightCyan, DarkBlue, White, DarkCyan) must be valid tokens within the projects visual system for the palette to render correctly.
---
### SolarizedTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme SolarizedTheme = new()
```
SolarizedTheme is a private static readonly Theme instance that encapsulates the Solarized color palette used by the UI. It defines color roles for four UI surfaces—Base, Menu, Dialog, and Status—specifying both normal foreground/background and focused-state foreground/background colors. The field is initialized once at type-load time and is then reused wherever a Solarized look is required, providing a single source of truth for this color scheme and preventing runtime mutations.
## Remarks
This symbol acts as a centralized, immutable specification of the Solarized look. By housing the color tokens in a single Theme, ThemeManager can consistently apply the same palette across menus, dialogs, and status lines without scattering literals throughout the code. The private static readonly pattern communicates intent: SolarizedTheme is a predefined, non-changing theme available to internal consumers of ThemeManager, not something that should be modified at runtime.
## Notes
- The theme uses string color tokens (e.g., "Cyan", "BrightYellow"), which are resolved by the theming subsystem to actual display colors.
- Because the field is readonly, any changes require rebuilding the Theme instance; runtime mutation is prevented.
- The four ThemeColors sections (Base, Menu, Dialog, Status) each specify both normal and focused color states to support focus indication.
---
### ThemeDir
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly string ThemeDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".echohub", "themes")
```
ThemeDir is a private, static readonly string that resolves to the user-specific themes directory by combining the current users profile folder with .echohub/themes. It provides a single, OS-agnostic path for ThemeManager to load and save theme files, avoiding scattered string literals.
## Remarks
Centralizing the location of theme assets decouples theme storage from OS conventions and hard-coded paths, making future relocations or tests simpler. The static readonly nature guarantees a consistent path across all ThemeManager operations, computed at type initialization. If the target directory doesn't exist at runtime, higher-level startup or initialization code should ensure it is created before any read/write of themes.
## Notes
- Directory existence: ensure creation to avoid IO errors when reading or writing themes.
- Hidden folder nuance: .echohub will be hidden on Unix-like systems; consider how this affects user visibility or directory listings in certain UI scenarios.
---
### TransparentLightTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme TransparentLightTheme = new()
```
Represents a canonical light-theme configuration used by the UI to render surfaces on light backgrounds. TransparentLightTheme is a private static readonly Theme instance that bundles a complete color palette for Base, Menu, Dialog, Status, and Border, enabling a consistent light appearance across the UI when a light or transparent background is in use. The defined colors map foregrounds, backgrounds, and focus states to maintain readability and clear focus cues (Blue for focused elements).
## Remarks
By centralizing the light-theme palette in a single internal Theme instance, this symbol reduces drift between UI surfaces and makes it straightforward to derive alternate light variants from a single baseline. Its private visibility signals it's an internal default rather than a public customization point; external code should define and consume their own Theme instances instead of mutating this one.
## Notes
- Border foreground uses #8F8F8F for softer borders on light terminals.
- Background values set to 'None' indicate transparency or reliance on the parent/background, aligning with a transparent-light aesthetic.
---
### TransparentTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme TransparentTheme = new()
```
Defines a single, shared Theme instance named TransparentTheme that implements a glassy, semi-transparent UI aesthetic. Declared private static readonly, it is initialized once and reused by the ThemeManager to apply a cohesive translucent look across Base, Menu, Dialog, Status, and Border color groups (most backgrounds are None to preserve translucency, with White foreground and BrightCyan focus colors; Dialog uses DarkGray to retain legibility; borders use muted grays to complete the glassy look).
## Remarks
This symbol centralizes the glassy appearance so all UI surfaces adopting transparency share a single color model. Being private ensures the theme is an internal implementation detail of ThemeManager and not part of the public theming surface. If a project needs a similar variant publicly, it should be created as a separate, publicly accessible theme instance rather than exposing this private field. The pattern reduces drift between components and simplifies maintenance of the transparent aesthetic.
## Notes
- The field is readonly, but its nested color objects are not guaranteed immutable; mutating their properties at runtime would alter the shared theme for all users. Treat the instance as immutable after initialization to preserve consistency.
---
## BuiltInThemes
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly List<Theme> BuiltInThemes =
[
DefaultTheme,
TransparentTheme,
TransparentLightTheme,
ClassicTheme,
LightTheme,
HackerTheme,
SolarizedTheme,
DraculaTheme,
MonokaiTheme,
NordTheme,
GruvboxTheme,
OceanTheme,
HighContrastTheme,
RosePineTheme
]
```
BuiltInThemes is a private static readonly collection that enumerates the Theme instances shipped as built-in themes. It provides a stable, canonical set of themes (including DefaultTheme, TransparentTheme, TransparentLightTheme, ClassicTheme, LightTheme, HackerTheme, SolarizedTheme, DraculaTheme, MonokaiTheme, NordTheme, GruvboxTheme, OceanTheme, HighContrastTheme, and RosePineTheme) that ThemeManager can iterate over to present theme options and initialize theming state. Because the field is private and readonly, external code cannot modify this collection at runtime; it is intended as an internal baseline that ensures consistent theming behavior across the application.
## Remarks
Centralizes the shipped themes into a single place, guaranteeing a consistent ordering and a single source of truth for what counts as built-in. This reduces duplication and makes it easier to adjust defaults or add new themes by updating the initializer, rather than sprinkling Theme references throughout the code. Because it's private, consumers must rely on public Theme-related APIs or ThemeManager flows to query or apply themes.
## Notes
- The list is constructed from static Theme instances defined elsewhere (the DefaultTheme, TransparentTheme, etc.).
- As a private, readonly field, it cannot be replaced or mutated at runtime; new themes must be added via source changes.
- If you need to expose or customize the built-in set, provide a public API rather than accessing this field directly.
---
## GruvboxTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme GruvboxTheme = new()
```
GruvboxTheme is a private, static, readonly Theme instance that encodes the Gruvbox color palette for the EchoHub client UI. It defines colors for core regions—Base, Menu, Dialog, and Status—each with a Foreground, Background, FocusForeground, and FocusBackground value. This single object acts as the canonical Gruvbox styling source consumed by the theming subsystem to render a consistent look across the application. Because the field is private and readonly, external callers should rely on ThemeManager's public mechanisms to obtain themed resources rather than mutate or reference this field directly.
## Remarks
By centralizing the palette in one immutable object, GruvboxTheme reduces drift between UI regions and simplifies theming changes. The per-region color groups reflect a clean separation of concerns: Base handles the main chrome, Menu for navigation, Dialog for modal surfaces, and Status for status indicators; the consistent focus colors ensure accessible emphasis when keyboard navigation occurs. This pattern makes it straightforward to swap themes by replacing the underlying Theme instance without scattering color literals throughout the code.
## Notes
- The readonly reference prevents re-assignment, but if Theme or ThemeColors are mutable, their values can still be mutated at runtime.
- This field is private; there is no direct public API here—consumers should obtain theme data via ThemeManager's public surface rather than accessing GruvboxTheme directly.
---
## NordTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme NordTheme = new()
```
NordTheme defines the internal, immutable Nord color palette used by ThemeManager to style the UI. It is a single Theme instance configured with per-surface color mappings (Base, Menu, Dialog, Status) so the Nord look is applied consistently without duplicating color definitions throughout the code.
## Remarks
This symbol centralizes the Nord appearance, providing a single source of truth for foreground/background and focus colors across different UI surfaces. It is private to ThemeManager, which means external code should interact with the public theming API rather than reference or mutate this instance. The approach reduces drift between surfaces and makes it easy to switch themes by swapping higher-level theme providers rather than tweaking individual components.
## Notes
- Be aware that the readonly modifier applies to the field reference; nested ThemeColors instances may still be mutable if their properties expose setters. If true immutability is required, consider making Theme and ThemeColors immutable or returning defensive copies.
---
## RosePineTheme
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
> **Kind:** field
```csharp
private static readonly Theme RosePineTheme = new()
```
RosePineTheme is a private static readonly Theme instance that encapsulates the RosePine color palette used by the UI. It defines per-area color configurations for Base, Menu, Dialog, and Status, pairing foreground and background colors with their focused variants. This centralized definition provides a single source of truth for the RosePine look and is consumed by the theming subsystem rather than by external code, helping maintain a cohesive visual style across the application.
## Remarks
Centralizes theme-related color data to ensure visual consistency and to simplify theme swapping or adjustment. Keeping the field private hides implementation details from consumers and enforces usage through the theming infrastructure, reducing the risk of accidental divergence in color usage.
## Notes
- The field is private; external code cannot reference RosePineTheme directly.
- The field is readonly in reference, but its internal properties may be mutable depending on ThemeColors' mutability; if ThemeColors exposes setters, the palette could be modified after initialization.
- Static initialization order and potential side effects: If ThemeManager relies on RosePineTheme during application startup, ensure initialization order is correct.
---
@@ -0,0 +1,19 @@
# ChatColors
> **File:** `src/EchoHub.Client/UI/Chat/ChatColors.cs`
> **Kind:** class
```csharp
public static partial class ChatColors
```
Shared color attributes and a small parsing helper for chat rendering. Use this class when rendering chat UI elements (timestamps, system messages, mentions, channel references, embeds, attachments, etc.) so all parts of the UI use a consistent set of Attribute values. Call SplitMentions when you need to break a message into colored segments so mentions (@user) and channel references (#channel) can be rendered with their accent colors while non-special text uses a supplied default.
## Remarks
ChatColors centralizes the visual styling for chat components and includes a utility to split text into ChatSegment pieces that carry color information. SplitMentions performs a two-pass parse: first it finds @mentions (avoiding emails by requiring no preceding word character) and marks them with MentionTextAttr; then it examines the remaining, non-mention segments to find #channel references (the regex requires at least one letter to avoid matching hex colors or numeric issue references) and marks those with ChannelRefAttr. All Attribute instances are readonly and intended as shared, immutable style tokens that renderers can reuse.
## Notes
- The mention regex uses a negative lookbehind (?<!\w) so strings like "me@domain" are not treated as @mentions.
- The channel regex requires at least one ASCII letter to avoid matching plain hex colors or purely numeric tokens.
- SplitMentions accepts a nullable defaultColor; callers should handle null when rendering (null means "no explicit attribute supplied").
@@ -0,0 +1,120 @@
# ChatLine.cs
> **Source:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
## Contents
- [ChatLine](#chatline)
- [AttachmentActionSpan](#attachmentactionspan)
- [AttachmentAction](#attachmentaction)
---
## ChatLine
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
> **Kind:** class
```csharp
public partial class ChatLine
```
Represents a single rendered chat line made up of colored ChatSegment pieces and associated display metadata. Use ChatLine when preparing or manipulating a line for rendering in the chat view (layout, wrapping, attachment actions, separators, mention/highlight state) rather than working with raw strings or segments directly.
## Remarks
ChatLine is the view-level unit for a message or a rule separator: it aggregates ChatSegment instances (text + color), stores metadata such as MessageId, sender, attachment info and clickable action spans, and exposes logic to break the line into multiple display lines that fit a viewport width. It centralizes presentation concerns (continuation indentation, colored continuation prefixes, non-wrapping rule lines, and unread-marker behavior) so the chat rendering layer can ask a ChatLine to produce the wrapped pieces it needs rather than implementing wrapping and metadata handling itself.
## Example
```csharp
// Construct from plain text
var line = new ChatLine("Hello, world!");
// Optional metadata
line.MessageId = Guid.NewGuid();
line.SenderUsername = "alice";
// Wrap to a viewport width of 40 columns, with a 4-space continuation indent
var wrapped = line.Wrap(40, continuationIndent: 4);
// Construct from explicit segments (preserves per-segment color attributes)
var segments = new List<ChatSegment>
{
new ChatSegment("[alice] ", ChatColors.RailAttr),
new ChatSegment("This is a message", null)
};
var coloredLine = new ChatLine(segments);
```
## Notes
- RuleLabel makes the line a separator rule; such lines are not word-wrapped and are regenerated to the viewport width by the view.
- If ContinuationPrefixSegments is set, it overrides ContinuationIndent: continuation lines use the prefix segments' column width instead of plain-space indentation.
- ActionSpans (when present) are column positions relative to the unwrapped line; only the first wrapped line preserves those spans — subsequent wrapped continuation lines do not.
- Wrapping respects grapheme clusters and column widths (uses GetGraphemes and GetColumns), so wide characters and combining sequences are handled when measuring width. If width <= 0 or the line already fits, Wrap returns the original line in a single-element list.
---
## AttachmentActionSpan
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
> **Kind:** record
```csharp
public readonly record struct AttachmentActionSpan(int StartCol, int EndCol, AttachmentAction Action)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `StartCol` | `int` | — |
| `EndCol` | `int` | — |
| `Action` | `AttachmentAction` | — |
It encodes an inclusive horizontal span on a chat line that maps to an AttachmentAction when clicked. This readonly record struct pairs StartCol and EndCol (both inclusive) with an Action to designate a specific clickable region that triggers an attachment operation.
## Remarks
Because it's a value type with immutable fields, AttachmentActionSpan is cheap to copy and compare, which helps with hit-testing and rendering across frames. It expresses the intent of interactive regions alongside their coordinates and associated action, keeping the UI layer decoupled from how actions are executed. This symbol complements other line-rendering data structures that describe clickable spans, enabling straightforward collection, filtering, and application during rendering.
## Notes
- EndCol is inclusive; ensure range checks treat EndCol as inclusive to avoid off-by-one errors.
- Overlapping spans may require careful resolution logic at render or hit-test time to determine which action should fire.
---
## AttachmentAction
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
> **Kind:** enum
```csharp
public enum AttachmentAction
{
OpenImage,
SaveImage,
}
```
An enum that represents the action a click on an attachment line can trigger in the chat UI. It lets the click handler distinguish between opening the image for viewing and saving the image to disk, promoting explicit, testable logic rather than ad-hoc behavior.
## Remarks
By codifying the possible outcomes as an enum, AttachmentAction defines a clear contract for how attachment clicks should be handled. It decouples the UI event from the concrete actions, making it easy to extend with new options (for example, ShareImage) without changing call sites. This abstraction supports consistent behavior across different chat lines and simplifies testing by allowing mocks or verifications based on the enum value.
## Example
```csharp
AttachmentAction action = /* determined by UI context */;
switch (action)
{
case AttachmentAction.OpenImage:
// Open the image in a viewer
break;
case AttachmentAction.SaveImage:
// Persist the image to disk
break;
}
```
## Notes
- If you later add actions to the enum, remember to handle them in all switch expressions and tests.
- Prefer explicit enum-based logic over string-based representations to avoid misinterpretation.
- Ensure UI-to-action mappings are consistent across chat lines to prevent user confusion.
---
@@ -0,0 +1,19 @@
# ChatListSource
> **File:** `src/EchoHub.Client/UI/Chat/ChatListSource.cs`
> **Kind:** class
```csharp
public class ChatListSource : IListDataSource
```
A list-backed data source for chat messages that implements IListDataSource and performs grapheme-aware rendering with per-segment coloring, mention-background highlighting, and a focus-based full-row highlight. Use this when supplying chat messages to a ListView-like control that expects the data source to manage items, raise collection-change notifications, and draw each row with segment-level attributes and correct column clipping.
## Remarks
ChatListSource maintains an internal `List<ChatLine>`, tracks the longest item via MaxItemLength, and raises a CollectionChanged (Reset) event whenever the collection is modified unless SuspendCollectionChangedEvent is set. Its Render implementation is grapheme-aware (uses GraphemeHelper.GetGraphemes and each grapheme's column width) and applies attributes per ChatSegment: a Focus attribute (when the row is selected and the list has focus) or the segment's color with fallbacks for missing backgrounds. If a ChatLine.IsMention is true the renderer uses ChatColors.MentionHighlightAttr.Background to override segment backgrounds and to fill the remainder of the row.
## Notes
- GetLine returns null for out-of-range indices; callers should validate the index first.
- IsMarked/SetMark are intentionally no-ops in this implementation and Dispose is a no-op — no per-item mark state or unmanaged cleanup is performed.
- MaxItemLength is updated only when lines are added/inserted; mutating a ChatLine.TextLength after insertion will not update MaxItemLength automatically. Use SuspendCollectionChangedEvent to batch updates and suppress the Reset event during bulk changes.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
# ChatSegment
> **File:** `src/EchoHub.Client/UI/Chat/ChatSegment.cs`
> **Kind:** record
```csharp
public record ChatSegment(string Text, Attribute? Color)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Text` | `string` | — |
| `Color` | `Attribute?` | — |
ChatSegment represents a colored fragment of text within a chat line. It pairs the displayed text with an optional color attribute, enabling the UI to render parts of a message with varying styling without altering the textual content. As a record, ChatSegment is immutable and supports value-based equality, making it convenient to compose a full line by aggregating multiple segments in a deterministic way.
## Remarks
ChatSegment exists to separate content from presentation. By modeling a line as a sequence of segments, the rendering layer can apply different colors or styles to each piece while preserving the original order. The record-like semantics also ease comparisons, caching, and deduplication of segments across messages.
## Notes
- Color is stored as a nullable Attribute; a null Color means no special styling is requested for this segment.
- `Attribute` is a general metadata type; downstream renderers interpret it to apply styling. The exact meaning of the Color value depends on the consuming UI.
- Because ChatSegment is a two-property record, equality includes both Text and Color; changes to either produce a distinct segment, which is important when deduplicating or comparing segments.
@@ -0,0 +1,11 @@
# RenderHelpers
> **File:** `src/EchoHub.Client/UI/Chat/RenderHelpers.cs`
> **Kind:** class
```csharp
static class RenderHelpers
```
RenderHelpers is a small, shared utility for rendering IListDataSource content. Its WriteText method writes text to a ListView grapheme-by-grapheme while respecting a maximum width, returning the updated count of drawn columns. It iterates over grapheme clusters obtained from GraphemeHelper.GetGraphemes(text); for each grapheme, it computes the display width with GetColumns() (falling back to 1 if necessary). If adding the grapheme would exceed maxWidth, rendering stops. Otherwise, it appends the grapheme to the ListView via lv.AddStr(grapheme) and increments the drawn count. This centralizes grapheme-aware rendering logic so multiple IListDataSource implementations share consistent width handling and avoid duplicating rendering concerns.
@@ -0,0 +1,24 @@
# WelcomeBanner
> **File:** `src/EchoHub.Client/UI/Chat/WelcomeBanner.cs`
> **Kind:** class
```csharp
internal static class WelcomeBanner
```
Renders a MOTD-style splash in the chat pane when no channel is selected — a gold-gradient ASCII logo accompanied by a version tagline and quick usage hints, evoking classic IRC greetings. Use WelcomeBanner.Build to generate the banner lines for a given viewport width and version string, then feed those lines into the chat UI.
## Remarks
WelcomeBanner encapsulates the presentation of the welcome banner: centering, padding, colorization, and the two-logo strategy are all handled here so the rest of the chat UI can simply render a sequence of lines. It selects between a full-width BigLogo and a compact SmallLogo based on the viewport width, scales the gradient across the chosen logo, and appends a version tagline plus a set of user hints. This keeps branding consistent across sizes and isolates banner-specific formatting from the broader rendering pipeline.
## Example
```csharp
var lines = WelcomeBanner.Build(80, "1.2.3");
// integrate 'lines' into the chat pane
```
## Notes
- The logo variant is chosen based on the provided width; very small panes will display SmallLogo to preserve legibility.
- The color attributes (Attributes on ChatSegment) require UI support in the chat renderer; without color support the banner falls back to plain text.
@@ -0,0 +1,23 @@
# AudioPlayerDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs`
> **Kind:** class
```csharp
public sealed class AudioPlayerDialog
```
AudioPlayerDialog is a sealed class that presents a modal Audio Player UI within the application's terminal UI. It assembles a compact layout with the current file name, a wave-like block visualization, playback status, and simple volume and playback controls, all exposed via a single Show method that binds an IApplication and an AudioPlaybackService to the dialog's lifecycle.
## Remarks
By encapsulating layout, colors, and animation in one place, it provides a reusable, cohesive UX for audio playback that can be dropped into different screens without duplicating UI code. The class relies on themed attributes (e.g. WaveActiveAttr, WaveIdleAttr, FileNameAttr, Status*Attr) to ensure consistent appearance, and uses a timer-driven animation loop to render the wave pattern while playback is active.
## Example
```csharp
AudioPlayerDialog.Show(app, audioService, "/path/to/song.mp3", "song.mp3");
```
## Notes
- The waveform visualization uses Unicode block characters; ensure your terminal font supports these glyphs for correct rendering.
- The dialog starts a background animation timer; dispose the dialog or stop the timer to avoid leaks when closing.
@@ -0,0 +1,23 @@
# ChannelPasswordDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs`
> **Kind:** class
```csharp
public sealed class ChannelPasswordDialog
```
Prompts for a channel password when joining a protected channel and returns the entered password, or null if the user cancels. Use this helper whenever you need a consistent, modal password prompt instead of duplicating dialog boilerplate across join flows.
## Remarks
This class centralizes the user flow for joining password-protected channels. It presents a modal dialog titled Join #<channel>, collects the password, and returns it to the caller, ensuring a single, predictable contract. The UI avoids displaying the actual password text by using a redacted caption and automatically focusing the password field, while the dialog lifecycle is orchestrated through the application (app.Run and app.RequestStop).
## Example
```csharp
string? password = ChannelPasswordDialog.Show(app, "mychannel", "Enter password to join #mychannel.");
```
## Notes
- The method is synchronous and modal; it blocks the caller until the user completes the interaction.
- A null return value indicates the user canceled the operation. If the user submits an empty password, a brief error dialog is shown and the prompt remains active until a non-empty password is provided.
@@ -0,0 +1,80 @@
# ConnectDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
## Contents
- [ConnectDialog](#connectdialog)
- [ConnectDialogResult](#connectdialogresult)
---
## ConnectDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
> **Kind:** class
```csharp
public sealed class ConnectDialog
```
ConnectDialog is a Terminal.Gui-based dialog that collects server connection details and authentication information for the application. When shown, it can display a list of SavedServer entries at the top if any saved servers are provided; in that case a Saved Servers section is rendered with a ListView of display names that indicate whether a session exists (the code appends a [session] marker when a RefreshToken is present). Below (or in place of it, when there are no saved servers), the dialog presents manual entry fields for Server URL (default http://localhost:5000), Username, and Password, along with UI hints such as a hidden password placeholder and a Remember me option. Additional fields include Display Name and, when relevant, an Invite Code for invite-gated registrations. The static Show method returns a ConnectDialogResult when the user completes the dialog, or null if the dialog is cancelled; the dialog height is adjusted depending on whether saved servers are shown.
---
## ConnectDialogResult
> **File:** `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs`
> **Kind:** record
```csharp
public record ConnectDialogResult(
string ServerUrl, string Username, string Password,
bool IsRegister, bool RememberMe, string? SavedRefreshToken,
string? DisplayName = null, string? InviteCode = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `ServerUrl` | `string` | — |
| `Username` | `string` | — |
| `Password` | `string` | — |
| `IsRegister` | `bool` | — |
| `RememberMe` | `bool` | — |
| `SavedRefreshToken` | `string?` | — |
| `DisplayName` | `string?` | `null` |
| [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md) | `string?` | `null` |
ConnectDialogResult encapsulates all user input gathered from the connect dialog as a single, immutable value. It is produced when the dialog completes and is consumed by the rest of the application to initiate a connection flow, passing the server URL, credentials, and onboarding flags as a single, strongly-typed package.
## Remarks
By collecting all related fields into a single record, this abstraction reduces coupling between the UI layer and the connection logic. It clearly expresses the intent of the user's action (login vs register) and whether credentials should be remembered, while allowing optional data (DisplayName, InviteCode) to participate in specialized flows without forcing callers to thread every field separately.
## Example
```csharp
// Common usage: construct a result from values collected in UI
var result = new ConnectDialogResult(
ServerUrl: "https://example.server/api",
Username: "alice",
Password: "P@ssw0rd",
IsRegister: false,
RememberMe: true,
SavedRefreshToken: null,
DisplayName: "Alice",
InviteCode: "INVITE-2024-ABCD"
);
```
## Notes
- DisplayName and InviteCode are nullable; omit them or pass null if not applicable.
- Password should be treated as sensitive data: avoid logging it or persisting it longer than necessary, and ensure proper disposal or clearing after use.
- SavedRefreshToken may be null; handle accordingly in login/refresh flows.
- This record is intended for in-memory transfer between UI and authentication/connection logic; when persisting or transmitting, apply appropriate security measures and avoid leaking confidential fields.
---
@@ -0,0 +1,74 @@
# CreateChannelDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
## Contents
- [CreateChannelDialog](#createchanneldialog)
- [CreateChannelResult](#createchannelresult)
---
## CreateChannelDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
> **Kind:** class
```csharp
public sealed class CreateChannelDialog
```
Displays a modal Create Channel dialog that collects the details needed to create a new channel: a name, an optional topic, a password, and a public visibility setting. The name is trimmed and normalized to lower case; if it is empty, the dialog reports an error and stays open. On Create, it builds a CreateChannelResult containing the name, topic (nullable), isPublic, and the password; on Cancel it returns null. The dialog runs via the provided IApplication instance and returns after the user makes a choice.
## Remarks
Encapsulates all UI logic for channel creation into a single entry point, enabling consistent behavior across the app and isolating rendering from business logic. The class acts as a small, self-contained UX widget that constructs the result object, ensuring callers need only handle the CreateChannelResult or null.
## Notes
- Name validation is minimal in code: the name is trimmed and lowercased, and non-empty; there is no explicit enforcement of length or allowed character patterns at runtime beyond what the UI hints suggest.
- Password handling appears behind-the-scenes (the UI labels redact the password, yet the password value is captured and returned as part of the result); ensure secure handling and minimize exposure of the plaintext password.
- The snippet references passwordField and publicCheckbox, which must exist in the full class scope; if you modify the UI composition, ensure these controls are present and wired consistently with the password retrieval and public visibility logic.
---
## CreateChannelResult
> **File:** `src/EchoHub.Client/UI/Dialogs/CreateChannelDialog.cs`
> **Kind:** record
```csharp
public record CreateChannelResult(string Name, string? Topic, bool IsPublic, string? Password)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Name` | `string` | — |
| `Topic` | `string?` | — |
| `IsPublic` | `bool` | — |
| `Password` | `string?` | — |
CreateChannelResult is an immutable data carrier that represents the outcome of creating a channel in the EchoHub client UI. It carries the channel's Name, an optional Topic, a flag IsPublic indicating whether the channel is public, and an optional Password.
## Remarks
As a record, CreateChannelResult participates in value-based equality, making comparisons straightforward without manual field checks. The positional constructor provides a concise, immutable payload that is easy to pass through layers (UI, services, or view models). You can deconstruct a result into its components, or derive a modified copy with a with-expression if you need a slightly different result without mutating the original. This type is intended to be produced by the channel-creation flow and consumed by UI code and downstream components.
## Example
```csharp
// Common case: create a public channel with a topic and password
var result = new CreateChannelResult("General", "Team discussions", true, "s3cr3t");
// Access fields
string name = result.Name;
string? topic = result.Topic;
bool isPublic = result.IsPublic;
string? password = result.Password;
// Deconstruct for convenience
var (n, t, pub, pwd) = result;
// Create a modified copy
var updated = result with { Topic = "New topic" };
```
---
@@ -0,0 +1,64 @@
# ProfileEditDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
## Contents
- [ProfileEditDialog](#profileeditdialog)
- [ProfileEditResult](#profileeditresult)
---
## ProfileEditDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
> **Kind:** class
```csharp
public sealed class ProfileEditDialog
```
ProfileEditDialog provides a Terminal.Gui dialog for editing the user's profile. Its Show method presents a modal dialog titled "Edit Profile" with fields for Display Name, Bio, Nickname Color (with a hex input and a live color preview) and Avatar selection, plus notification preferences, returning a ProfileEditResult when the user accepts or null if cancelled.
## Remarks
ProfileEditDialog centralizes profile-edit UI in one reusable component, ensuring a consistent look and behavior whenever the user updates their profile. It wires up real-time color previews by updating the color swatch whenever the hex input changes, and it delegates color parsing to HexColorHelper to translate user input into a Color value. The Avatar field demonstrates integration with a file picker (OpenDialog) within a Terminal.Gui workflow, keeping file selection cohesive with the rest of the dialog.
## Notes
- The Show method accepts optional parameters for notificationSoundEnabled and notificationVolume, defaulting to false and 30 respectively.
- If no avatar is selected, avatarField.Text remains empty.
- The return type is ProfileEditResult?; callers should handle null to cover the cancel path.
- This implementation relies on Terminal.Gui primitives (Label, TextField, Button, CheckBox, OpenDialog) and collaborator types (ProfileEditResult, HexColorHelper); ensure these types are available in the consuming project.
---
## ProfileEditResult
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileEditDialog.cs`
> **Kind:** record
```csharp
public record ProfileEditResult(string? DisplayName, string? Bio, string? NicknameColor, string? AvatarPath, bool? NotificationSoundEnabled, byte? NotificationVolume)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `DisplayName` | `string?` | — |
| `Bio` | `string?` | — |
| `NicknameColor` | `string?` | — |
| `AvatarPath` | `string?` | — |
| `NotificationSoundEnabled` | `bool?` | — |
| `NotificationVolume` | `byte?` | — |
Represents the data returned from the profile edit dialog. It encapsulates the user\'s optional inputs for DisplayName, Bio, NicknameColor, AvatarPath, NotificationSoundEnabled, and NotificationVolume so the caller can apply changes in a single operation. Each property is nullable: a null value means no change for that field; a non-null value provides a new value to persist.
## Remarks
ProfileEditResult is an immutable value object used as the dialog\'s return type. Its nullable fields express a delta: non-null values indicate updates, while null indicates no change. As a record, it benefits from value-based equality, making comparisons and tests straightforward, and it cleanly separates UI input from downstream update logic.
## Notes
- Null values indicate no change; apply only non-null fields when updating the profile.
- The type is immutable; to derive modifications, use a with-expression to create a new instance.
---
@@ -0,0 +1,51 @@
# ProfileViewDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
## Contents
- [ProfileViewDialog](#profileviewdialog)
- [ProfileAction](#profileaction)
---
## ProfileViewDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
> **Kind:** class
```csharp
public sealed class ProfileViewDialog
```
ProfileViewDialog renders a dialog to view a user's server profile; when showing the current user's profile it also exposes action buttons (Edit Profile / Set Status) and returns the chosen ProfileAction, while viewing another user yields a read-only presentation.
## Remarks
ProfileViewDialog encapsulates all the layout and formatting decisions for a user profile in a single place. It dynamically switches between a read-only view and an ownership-aware view that surfaces actions, and it applies color theming to the status and nickname fields. By centralizing this UI behavior, the dialog remains consistent across the application and reduces duplication by isolating profile presentation from business logic. The component gracefully handles a missing profile by showing an error message and returning a Close action, which defines a clear contract for callers.
## Notes
- If invoked with a null profile, the dialog shows an error and returns ProfileAction.Close; callers should guard against null input or handle the Close result accordingly.
- The dialog title differentiates ownership with "My Profile" for the current user and "Profile — {username}" for others, and it uses color-coding helpers to reflect status and nickname color for quick visual cues.
---
## ProfileAction
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
> **Kind:** enum
```csharp
public enum ProfileAction
{
Close,
EditProfile,
SetStatus
}
```
ProfileAction defines the set of actions a user can select from their profile dialog: Close, EditProfile, and SetStatus. It provides a typed representation of user intent that downstream UI logic can handle in a deterministic way, rather than relying on magic strings or numeric codes.
## Remarks
ProfileAction represents the users chosen action from the profile dialog, allowing the UI layer to dispatch the appropriate workflow in a type-safe way. By enumerating possible intents, the code can exhaustively handle all cases in a switch or pattern-match, reducing errors from invalid values. The Close action also clarifies that the action is about dialog lifecycle control as opposed to in-dialog tasks such as editing or setting status. If new actions are required in the future, they should be added here with clear naming that maps to corresponding UI behaviors.
---
@@ -0,0 +1,94 @@
# SearchDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
## Contents
- [SearchDialog](#searchdialog)
- [SearchResult](#searchresult)
- [SearchResultType](#searchresulttype)
---
## SearchDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
> **Kind:** class
```csharp
public static class SearchDialog
```
SearchDialog is a command-palette style search dialog used to quickly navigate channels and trigger common app actions from a single, keyboard-driven interface. Use it when you want fast, non-mouse access to channels and actions by filtering a combined list and selecting with Enter.
## Remarks
SearchDialog composes a modal dialog that presents both channel names and a predefined set of actions, merged into a single searchable list via a SearchListSource. It returns the selected SearchResult and signals completion to the hosting application by invoking RequestStop on IApplication, keeping the dialog logic decoupled from the rest of the UI. This abstraction enables a reusable, consistent navigation surface across different parts of the app.
## Example
```csharp
// Example
IApplication app = /* obtain your app instance */;
IReadOnlyList<string> channels = new[] { "general", "engineering" };
var result = SearchDialog.Show(app, channels);
if (result != null)
{
// Handle the selected item (channel or action) here.
}
```
## Notes
- The dialog includes a hint, a text field for filtering, a list of results, and a Cancel button; selection is returned as a SearchResult, or null if cancelled.
- Ctrl+K handling in both the dialog and the search field cancels the operation by requesting stop from the application, so be aware that this combo acts as a cancel gesture rather than an open/search trigger.
- When items exist, the first item is pre-selected; filtering updates the source and may reset the selection.
---
## SearchResult
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
> **Kind:** record
```csharp
public record SearchResult(SearchResultType Type, string Key, string Label)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Type` | `SearchResultType` | — |
| `Key` | `string` | — |
| `Label` | `string` | — |
Represents a single entry in search results, encapsulating the result's category (Type), a key (Key), and a user-facing label (Label). As a positional-record, it is immutable and compared by value, which makes it convenient to pass around and render in the search UI.
## Remarks
Use SearchResult to model a single outcome returned by the search feature. Type communicates the kind of item (as defined by SearchResultType), Key is the stable identifier for navigation or lookup, and Label is the display text shown in the results list. Because it is a deconstructible record, you can conveniently extract its fields with deconstruction or pattern matching, and equality checks are based on the content rather than the instance identity.
## Notes
- Immutability: SearchResult uses a primary constructor; properties are read-only and a modified instance must be created with a with-expression or a new constructor.
- Deconstruction: The positional constructor enables deconstruction: var (t, k, l) = result; or access via result.Type, result.Key, result.Label.
- Type relies on the SearchResultType enum; when consuming code, prefer switching on Type rather than comparing display strings.
---
## SearchResultType
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
> **Kind:** enum
```csharp
public enum SearchResultType
{
Channel,
Action
}
```
Represents the category of a search result in the EchoHub client UI, distinguishing Channel results from Action results. Developers reach for this enum to branch rendering or navigation logic based on the result type, instead of using boolean flags or string comparisons.
## Remarks
Because it is a small discriminant, SearchResultType is typically consumed alongside a broader SearchResult structure. It enables simple pattern matching in switch expressions or if statements, guiding UI decisions such as which view to open or which icon to display when a user selects a result.
---
@@ -0,0 +1,69 @@
# StatusDialog.cs
> **Source:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
## Contents
- [StatusDialog](#statusdialog)
- [StatusDialogResult](#statusdialogresult)
---
## StatusDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
> **Kind:** class
```csharp
public sealed class StatusDialog
```
StatusDialog is a terminal-based UI component that presents a compact dialog for updating the current user's status and an optional status message. Its Show method renders the dialog initialized with the provided current status and message, and returns a StatusDialogResult when the user saves, or null if the user cancels.
The dialog consists of a title 'Set Status', a status option selector pre-populated with the current status, a text field for the status message, and Save/Cancel actions. On Save, the selected status is captured (defaulting to Online if nothing is selected) and the message is trimmed; an empty message becomes null. The method returns a new StatusDialogResult with those values and stops the application loop via app.RequestStop(); Cancel returns null and stops the loop.
Callers use the returned result to apply the updated status and message; otherwise, no changes are made.
## Remarks
StatusDialog encapsulates the presentation logic for updating user status, isolating UI concerns from business logic. It is a small, reusable piece that orchestrates Terminal.Gui controls (Dialog, Label, OptionSelector, TextField, Button) and relies on IApplication to drive the modal flow. The use of a default Online and trimming of the message ensures sane behavior even when fields are left blank.
## Example
```csharp
var result = StatusDialog.Show(app, currentStatus, currentMessage);
if (result != null)
{
// Apply updates to the user's status and message
currentStatus = result.Status;
currentMessage = result.Message;
}
```
## Notes
- A null result indicates the user cancelled the dialog; callers should guard against applying changes in this case.
- If the user leaves the Message field blank or whitespace, the message is stored as null.
- The Save action is wired as the default action (IsDefault = true), and both Save and Cancel terminate the modal interaction by invoking app.RequestStop().
---
## StatusDialogResult
> **File:** `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
> **Kind:** record
```csharp
public record StatusDialogResult(UserStatus Status, string? StatusMessage)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Status` | [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) | — |
| `StatusMessage` | `string?` | — |
StatusDialogResult is a minimal, immutable data carrier returned when the status dialog completes. It groups the chosen user status (Status) with an optional message (StatusMessage) into a single value that downstream logic can consume without inspecting the dialog UI directly. As a C# record, it benefits from value-based equality and straightforward deconstruction.
## Remarks
StatusDialogResult encapsulates the outcome of a UI interaction into a single semantic unit that can be passed through the application flow or stored for auditing. It separates presentation concerns from business logic: callers reason about the user's status and optional message rather than UI details. The nullable StatusMessage signals that extra context is optional; consumer code should handle the absence gracefully, typically by pattern matching on Status and checking for a non-null message. The record type also supports structural equality, making tests and comparisons concise.
---
@@ -0,0 +1,14 @@
# UpdateConfirmDialog
> **File:** `src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs`
> **Kind:** class
```csharp
public sealed class UpdateConfirmDialog
```
UpdateConfirmDialog is a sealed utility with a single static Show method that prompts the user to confirm an available update. It builds a small modal dialog titled “Update Available” showing the current and latest versions and offers two actions: Update (default) and Cancel; it returns true if the user chooses Update and false otherwise. The method runs the provided IApplication until the user makes a choice, using RequestStop to close the dialog and return the result.
## Remarks
It encapsulates the update-confirmation interaction as a reusable, modal prompt that coordinates with the host application's event loop, avoiding duplication of dialog boilerplate across the codebase.
@@ -0,0 +1,28 @@
# DroppedFileParser
> **File:** `src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs`
> **Kind:** class
```csharp
public static class DroppedFileParser
```
DroppedFileParser is a small utility that interprets terminal-dropped input as potential file paths and resolves them to existing files. Use it when you need to convert user-typed or pasted text into concrete file paths without scattering filesystem checks across callers.
## Remarks
This abstraction centralizes the logic for recognizing path-like input and for extracting one or more existing file paths from either a single path or a space-separated list of paths. It exposes a fast pre-check (LooksLikePath) to avoid expensive filesystem calls for clearly non-path input, and a test-friendly parser (TryGetFiles) that can inject a custom file existence predicate. The design favors explicit handling of both Windows (drive letters and UNC) and POSIX-style absolute paths, including quoted components and spaces.
## Example
```csharp
var input = "\"C:\\Temp\\report.pdf\" C:\\Data\\log.txt";
if (DroppedFileParser.TryGetFiles(input, out var files))
{
// files contains: ["C:\\Temp\\report.pdf", "C:\\Data\\log.txt"]
}
```
## Notes
- LookSLikePath may return true for strings that resemble paths (e.g., starting with a quote, a slash, UNC prefix, or a drive letter), so TryGetFiles should be used to confirm actual file existence.
- TryGetFiles enforces that all tokens are fully-qualified paths and that each path exists (via the injectable fileExists predicate, which defaults to File.Exists). This reduces accidental assumptions about the input.
- The tokenization logic respects quoted segments so that spaces within a single path do not split tokens unintentionally.
@@ -0,0 +1,19 @@
# EmojiHelper
> **File:** `src/EchoHub.Client/UI/Helpers/EmojiHelper.cs`
> **Kind:** class
```csharp
public static class EmojiHelper
```
EmojiHelper converts emoji grapheme clusters to text shortcodes for safe TUI rendering. It replaces emoji with fixed-width ASCII shortcodes when available, falling back to a generic [emoji] placeholder for unknown symbols; non-emoji text passes through unchanged.
## Remarks
This utility uses grapheme-aware processing to handle complex emoji sequences (including ZWJ-joined glyphs and modifier-bearing emojis) by iterating over text elements rather than individual code points. It first attempts a full-grapheme shortcode lookup, then falls back to the base emoji (the first rune of the grapheme) if necessary, and finally uses the [emoji] placeholder when no mapping exists. An initial pass quickly determines whether any emoji exist in the input to avoid unnecessary work. The implementation relies on StringBuilder for efficient string construction, StringInfo for grapheme segmentation, and the EmojiShortcodes mapping as the source of truth for replacements.
## Notes
- Unknown or unmapped emoji are replaced with [emoji], which can reduce expressiveness if the shortcode dictionary is incomplete. Ensure EmojiShortcodes covers the emoji you expect to render in your UI.
@@ -0,0 +1,18 @@
# HexColorHelper
> **File:** `src/EchoHub.Client/UI/Helpers/HexColorHelper.cs`
> **Kind:** class
```csharp
public static class HexColorHelper
```
HexColorHelper is a small utility that converts hex color strings into Terminal.Gui coloring primitives. Use ParseHexColor to obtain an Attribute suitable for styling a control's foreground, and ParseHexToColor when you need a Color value with a safe fallback for invalid input.
## Remarks
By centralizing hex parsing, HexColorHelper avoids duplicating color-conversion logic and provides predictable fallbacks for malformed input. It interprets a hex string as an RGB triplet and applies it as the foreground color (with no explicit background). This keeps styling decisions consistent across the UI while keeping the parsing logic isolated in one place.
## Notes
- Invalid input yields null (for ParseHexColor) or the provided fallback (for ParseHexToColor); no exceptions are thrown.
- A 6-digit hex value is required after an optional leading '#'. Non-hex characters or incorrect length return fallback/null.
@@ -0,0 +1,25 @@
# NickColorHelper
> **File:** `src/EchoHub.Client/UI/Helpers/NickColorHelper.cs`
> **Kind:** class
```csharp
public static class NickColorHelper
```
NickColorHelper deterministically assigns a stable color to every nickname, ensuring the same nick always maps to the same palette entry. This mirrors classic IRC behavior and lets busy channels stay readable without per-user configuration.
## Remarks
NickColorHelper isolates color selection from rendering logic by exposing a pure function GetPaletteIndex and GetAttribute. The palette itself is a fixed sequence of medium-saturation colors designed for legibility on both dark and light backgrounds; changing the palette order would re-color every nick and break visual consistency across sessions.
## Example
```csharp
var color = NickColorHelper.GetAttribute("Alice");
// Use `color` when rendering Alice's username in the UI
```
## Notes
- Null nick will throw; ensure non-null before calling GetPaletteIndex.
- The palette order is fixed; reordering or removing entries changes every nickname's color.
- The mapping uses a case-insensitive FNV-1a hash; changing the hash or its normalization will alter which nick gets which color.
@@ -0,0 +1,21 @@
# ChannelListSource
> **File:** `src/EchoHub.Client/UI/ListSources/ChannelListSource.cs`
> **Kind:** class
```csharp
public class ChannelListSource : IListDataSource
```
A colored, list-backed IListDataSource that presents channel names with visual affordances: an active-channel indicator, unread count badges, and markers for protected, private and system channels. Use this when you need a ListView-compatible data source that maintains channel ordering, per-channel unread counts and simple visual state (active, mention, protected/private, system) instead of hand-rendering each row.
## Remarks
ChannelListSource centralizes the channel-list state required by a ListView: the ordered channel names, a per-channel unread count map and several role sets (protected, mention, private and system). It exposes a single Update method that replaces the in-memory collections in one operation and (unless suspended) raises a Reset collection-changed event so consumers can re-layout or refresh. The class also provides MaxItemLength to help the host compute layout and ToList to produce a display-friendly list of channel strings (each prefixed with '#'). Rendering is delegated to the ListView via the Render method; the class supplies attributes (ActiveAttr, UnreadAttr, NormalAttr, BadgeAttr, MentionAttr, SystemAttr) and simple prefix/marker rules so the view paints active items, unread badges and visual separation for system channels.
## Notes
- Update clears and replaces all internal collections; call it with the full desired state rather than trying to patch individual entries.
- The Count/MaxItemLength values are derived from the current channel list. MaxItemLength computes name.Length + 6 (reserved space for prefixes/badges), so layout logic should consider that padding when sizing the list column.
- CollectionChanged will be invoked with a NotifyCollectionChangedAction.Reset at the end of Update unless SuspendCollectionChangedEvent is true. SuspendCollectionChangedEvent is a simple in-memory flag — using it prevents the Reset event from being raised during an Update.
- IsMarked and SetMark are intentionally inert (IsMarked always returns false and SetMark is a no-op), so callers should not rely on marking support from this source.
- Render moves the ListView cursor using Math.Max(col - viewportX, 0) to account for horizontal scrolling (viewportX). Hosts should provide correct viewportX and width values so rendering and clipping behave as intended.
@@ -0,0 +1,22 @@
# SearchListSource
> **File:** `src/EchoHub.Client/UI/ListSources/SearchListSource.cs`
> **Kind:** class
```csharp
public class SearchListSource(List<SearchResult> items) : IListDataSource
```
List data source that feeds a search dialog's ListView: it maintains an original item list, supports case-insensitive filtering by label or key, raises a Reset collection-changed notification when the filter changes (unless suspended), and renders each row with color-coding depending on the SearchResultType.
## Remarks
This class combines two responsibilities commonly needed by a search dialog: fast, in-memory filtering of a fixed set of SearchResult records and rendering of those results into a ListView with per-type coloring. Consumers attach to CollectionChanged to refresh the UI when Filter(string) updates the visible set. Render uses RenderHelpers.WriteText to draw the label and then fills the remainder of the column; it chooses a highlight (selected) attribute from the ListView or a per-result attribute (channel/action) and preserves the list's background when a per-result attribute leaves the background as Color.None.
## Notes
- Filter is case-insensitive and matches either SearchResult.Label or SearchResult.Key.
- When Filter receives a null/whitespace query the visible list is reset to all items and a Reset event is raised (unless SuspendCollectionChangedEvent is true).
- IsMarked and SetMark are no-ops; this data source does not track per-item marks.
- Dispose is a no-op; there are no unmanaged resources to release.
- MaxItemLength returns 0 when there are no filtered items.
- This class does not provide internal synchronization; callers should ensure thread-safety when mutating the source list or calling Filter from multiple threads.
@@ -0,0 +1,21 @@
# UserListSource
> **File:** `src/EchoHub.Client/UI/ListSources/UserListSource.cs`
> **Kind:** class
```csharp
public class UserListSource : IListDataSource
```
A data source implementation for a list view that presents online users with per-user nickname colors. Use this when you need a ready-made IListDataSource that holds tuples of display text, an optional nickname color (Attribute), and the username; it supplies item count, a maximal item width, batch updates via Update, and a Render implementation that paints a status/prefix in the normal role and the username portion in the configured nickname color while respecting selection and a fixed column width.
## Remarks
UserListSource is a UI-focused data source: it couples a small in-memory collection of user display tuples with a Render method tailored for a ListView consumer. It delegates grapheme-aware splitting to GraphemeHelper so prefix characters (status icon and optional role badge) are drawn in the list's normal attribute while the visible username text is drawn in the per-user nickname color unless the item is selected (selection forces the normal/Focus attribute). MaxItemLength is maintained as a convenience for layout calculations and is updated by Update.
## Notes
- Update replaces the entire contents; after calling Update the class raises NotifyCollectionChangedAction.Reset unless SuspendCollectionChangedEvent is true. If you set SuspendCollectionChangedEvent to batch multiple updates you are responsible for raising/triggering an appropriate collection changed notification afterward.
- MaxItemLength is computed using each entry's Text.GetColumns(), so wide characters and grapheme clusters affect reported width — MaxItemLength is a column/terminal-width measure, not a character count.
- Rendering is grapheme-aware and respects the provided width: text drawing stops when the accumulated column width reaches the requested width. This prevents partial grapheme rendering but means long names will be truncated to fit.
- Several IListDataSource members are intentionally trivial: IsMarked and SetMark are no-ops, ToList returns the visible Text values as objects, and Dispose is a no-op. Callers should not rely on any persistent marking or disposal behavior from this class.
- The implementation contains no internal synchronization; it is not inherently thread-safe. Ensure all access (especially Update and Render) is serialized by the caller when used from multiple threads.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,19 @@
# HubConstants
> **File:** `src/EchoHub.Core/Constants/HubConstants.cs`
> **Kind:** class
```csharp
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.
## 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.
## 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.
@@ -0,0 +1,30 @@
# MessageConventions
> **File:** `src/EchoHub.Core/Constants/MessageConventions.cs`
> **Kind:** class
```csharp
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.
## 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.
## Example
```csharp
var action = MessageConventions.FormatAction("waves");
if (MessageConventions.TryParseAction(action, out var text))
{
// text == "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.
@@ -0,0 +1,20 @@
# ValidationConstants
> **File:** `src/EchoHub.Core/Constants/ValidationConstants.cs`
> **Kind:** class
```csharp
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.
## 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.
## 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.
@@ -0,0 +1,121 @@
# IChannelService.cs
> **Source:** `src/EchoHub.Core/Contracts/IChannelService.cs`
*Figure: How IChannelService 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
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
```
## Contents
- [IChannelService](#ichannelservice)
- [ChannelListItem](#channellistitem)
---
## IChannelService
> **File:** `src/EchoHub.Core/Contracts/IChannelService.cs`
> **Kind:** interface
```csharp
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.
## 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.
## Example
```csharp
// Create a public channel and inspect the operation result
var createResult = await channelService.CreateChannelAsync(creatorUserId, "general", "General discussion", true);
if (createResult.IsSuccess)
{
var created = createResult; // ChannelOperationResult.Success contains the created ChannelDto
}
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
}
```
## 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.
---
## ChannelListItem
> **File:** `src/EchoHub.Core/Contracts/IChannelService.cs`
> **Kind:** record
```csharp
public record ChannelListItem(string Name, string? Topic, int OnlineCount, bool IsPublic = true, bool IsProtected = false)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Name` | `string` | — |
| `Topic` | `string?` | — |
| `OnlineCount` | `int` | — |
| `IsPublic` | `bool` | `true` |
| `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.
## 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.
## 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 };
```
## 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.
---
@@ -0,0 +1,35 @@
# IChatBroadcaster
> **File:** `src/EchoHub.Core/Contracts/IChatBroadcaster.cs`
> **Kind:** interface
```csharp
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).
## 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");
```
## 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.
@@ -0,0 +1,11 @@
# IChatService
> **File:** `src/EchoHub.Core/Contracts/IChatService.cs`
> **Kind:** interface
```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.
@@ -0,0 +1,60 @@
# IEchoHubClient
> **File:** `src/EchoHub.Core/Contracts/IEchoHubClient.cs`
> **Kind:** interface
```csharp
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.
## 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.
## Example
```csharp
// Minimal client-side implementation that logs events; real handlers should avoid long-running work.
public class EchoClientHandler : IEchoHubClient
{
public Task ReceiveMessage(MessageDto message)
{
Console.WriteLine($"Received message: {message}");
return Task.CompletedTask;
}
public Task UserJoined(string channelName, string username, UserPresenceDto? presence)
{
Console.WriteLine($"{username} joined {channelName}");
return Task.CompletedTask;
}
public Task UserLeft(string channelName, string username)
{
Console.WriteLine($"{username} left {channelName}");
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);
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.
@@ -0,0 +1,35 @@
# IMessageEncryptionService
> **File:** `src/EchoHub.Core/Contracts/IMessageEncryptionService.cs`
> **Kind:** interface
```csharp
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.
## 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.
## Example
```csharp
// Given an IMessageEncryptionService implementation (injected or resolved via DI)
IMessageEncryptionService service = ...;
string plain = "customer-secret";
string cipher = service.Encrypt(plain);
string decrypted = service.Decrypt(cipher); // == plain
string? nullablePlain = null;
string? nullableCipher = service.EncryptNullable(nullablePlain); // null
string? nullableDecrypted = service.DecryptNullable(nullableCipher); // null
bool atRest = service.EncryptDatabaseEnabled;
```
## 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.
@@ -0,0 +1,37 @@
# IUserService
> **File:** `src/EchoHub.Core/Contracts/IUserService.cs`
> **Kind:** interface
```csharp
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.
## 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.
## 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)
{
// use profile data
}
```
## 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.
@@ -0,0 +1,178 @@
# AccountDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
## Contents
- [DeleteAccountRequest](#deleteaccountrequest)
- [ExportedAttachmentDto](#exportedattachmentdto)
- [ExportedMessageDto](#exportedmessagedto)
- [UserDataExportDto](#userdataexportdto)
---
## DeleteAccountRequest
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
> **Kind:** record
```csharp
public record DeleteAccountRequest(string Password)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `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 users intent before the action is executed.
## 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!");
```
## 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.
---
## ExportedAttachmentDto
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
> **Kind:** record
```csharp
public record ExportedAttachmentDto(
string FileName,
string Url,
long FileSize,
string Kind,
string ChannelName,
DateTimeOffset SentAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `FileName` | `string` | — |
| `Url` | `string` | — |
| `FileSize` | `long` | — |
| `Kind` | `string` | — |
| `ChannelName` | `string` | — |
| `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.
## 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
);
```
## 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.
---
## ExportedMessageDto
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
> **Kind:** record
```csharp
public record ExportedMessageDto(
Guid Id,
string ChannelName,
DateTimeOffset SentAt,
string Content,
Guid? ReplyToMessageId)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `ChannelName` | `string` | — |
| `SentAt` | `DateTimeOffset` | — |
| `Content` | `string` | — |
| `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.
## 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" };).
---
## UserDataExportDto
> **File:** `src/EchoHub.Core/DTOs/AccountDtos.cs`
> **Kind:** record
```csharp
public record UserDataExportDto(
DateTimeOffset ExportedAt,
string ServerName,
UserProfileDto Profile,
List<ExportedMessageDto> Messages,
List<ExportedAttachmentDto> Attachments)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `ExportedAt` | `DateTimeOffset` | — |
| [`ServerName`](../../EchoHub.Server.Irc/IrcCommandHandler.cs.md) | `string` | — |
| `Profile` | [`UserProfileDto`](ProfileDtos.cs.md) | — |
| `Messages` | `List<ExportedMessageDto>` | — |
| `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.
## 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.
## 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.
---
@@ -0,0 +1,168 @@
# AuthDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
## Contents
- [LoginRequest](#loginrequest)
- [LoginResponse](#loginresponse)
- [RefreshRequest](#refreshrequest)
- [RegisterRequest](#registerrequest)
---
## LoginRequest
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
> **Kind:** record
```csharp
public record LoginRequest(string Username, string Password)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Username` | `string` | — |
| `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.
## 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!");
```
## 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.
---
## LoginResponse
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
> **Kind:** record
```csharp
public record LoginResponse(
string Token,
string RefreshToken,
DateTimeOffset ExpiresAt,
string Username,
string? DisplayName,
string? NicknameColor)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| [`Token`](../../EchoHub.Client/Services/ApiClient.cs.md) | `string` | — |
| `RefreshToken` | `string` | — |
| `ExpiresAt` | `DateTimeOffset` | — |
| `Username` | `string` | — |
| `DisplayName` | `string?` | — |
| `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.
## 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.
## Example
```csharp
var response = new LoginResponse(
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
RefreshToken: "def123-refresh",
ExpiresAt: DateTimeOffset.UtcNow.AddHours(1),
Username: "alex",
DisplayName: "Alex Doe",
NicknameColor: "#FF6A00"
);
```
## 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.
---
## RefreshRequest
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
> **Kind:** record
```csharp
public record RefreshRequest(string RefreshToken)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `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.
## 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");
```
## 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.
---
## RegisterRequest
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
> **Kind:** record
```csharp
public record RegisterRequest(string Username, string Password, string? DisplayName = null, string? InviteCode = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Username` | `string` | — |
| `Password` | `string` | — |
| `DisplayName` | `string?` | `null` |
| [`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.
## 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");
```
## 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.
---
@@ -0,0 +1,598 @@
# ChatDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
## Contents
- [AttachmentDto](#attachmentdto)
- [ChannelCryptoDto](#channelcryptodto)
- [ChannelDto](#channeldto)
- [ChannelMetaDto](#channelmetadto)
- [CreateChannelRequest](#createchannelrequest)
- [EmbedDto](#embeddto)
- [JoinChannelResult](#joinchannelresult)
- [MessageDto](#messagedto)
- [RekeyChannelRequest](#rekeychannelrequest)
- [ReplyRefDto](#replyrefdto)
- [SendMessageRequest](#sendmessagerequest)
- [SendUrlRequest](#sendurlrequest)
- [UpdateTopicRequest](#updatetopicrequest)
- [UserDto](#userdto)
---
## AttachmentDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record AttachmentDto(
AttachmentKind Kind,
string Url,
string FileName,
long FileSize,
string? AsciiPreview = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Kind` | [`AttachmentKind`](../Models/AttachmentKind.cs.md) | — |
| `Url` | `string` | — |
| `FileName` | `string` | — |
| `FileSize` | `long` | — |
| `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.
## 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.
---
## ChannelCryptoDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record ChannelCryptoDto(bool IsEncrypted, string? EncryptionSalt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `IsEncrypted` | `bool` | — |
| `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.
## 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.
---
## ChannelDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record ChannelDto(
Guid Id,
string Name,
string? Topic,
bool IsPublic,
int MessageCount,
DateTimeOffset CreatedAt,
bool IsProtected = false,
bool IsEncrypted = false,
bool IsSystem = false)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Name` | `string` | — |
| `Topic` | `string?` | — |
| `IsPublic` | `bool` | — |
| `MessageCount` | `int` | — |
| `CreatedAt` | `DateTimeOffset` | — |
| `IsProtected` | `bool` | `false` |
| `IsEncrypted` | `bool` | `false` |
| `IsSystem` | `bool` | `false` |
ChannelDto is an immutable data transfer object that encapsulates the core metadata of a chat channel. It groups the channels 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 channels state. As a record, ChannelDto provides value-based equality and supports convenient cloning via with-expressions without mutating the original instance.
## 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.
## 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
);
```
## 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.
---
## ChannelMetaDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record ChannelMetaDto(
Guid Id,
string Name,
string? Topic,
bool IsEncrypted,
bool IsProtected,
int MessageCount,
int UniqueUserCount,
long EstimatedSizeBytes,
DateTimeOffset CreatedAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Name` | `string` | — |
| `Topic` | `string?` | — |
| `IsEncrypted` | `bool` | — |
| `IsProtected` | `bool` | — |
| `MessageCount` | `int` | — |
| `UniqueUserCount` | `int` | — |
| `EstimatedSizeBytes` | `long` | — |
| `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.
## 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.
## 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.
---
## CreateChannelRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record CreateChannelRequest(
string Name,
string? Topic = null,
bool IsPublic = true,
string? [REDACTED:CONNECTION_STRING_PASSWORD]
string? EncryptionSalt = null,
string? WrappedRoomKey = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Name` | `string` | — |
| `Topic` | `string?` | `null` |
| `IsPublic` | `bool` | `true` |
| `EncryptionSalt` | `string? [REDACTED:CONNECTION_STRING_PASSWORD]
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.
## 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.
## 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.
---
## EmbedDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record EmbedDto(
string? SiteName,
string? Title,
string? Description,
string? ImageAscii,
string Url,
string? ThemeColor = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `SiteName` | `string?` | — |
| `Title` | `string?` | — |
| `Description` | `string?` | — |
| `ImageAscii` | `string?` | — |
| `Url` | `string` | — |
| `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.
## 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.
## 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");
```
## 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.
---
## JoinChannelResult
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record JoinChannelResult(
bool Success,
List<MessageDto> History,
string? Error = null,
bool PasswordRequired = false,
string? EncryptionSalt = null,
string? WrappedRoomKey = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Success` | `bool` | — |
| `History` | `List<MessageDto>` | — |
| `Error` | `string?` | `null` |
| `PasswordRequired` | `bool` | `false` |
| `EncryptionSalt` | `string?` | `null` |
| `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.
---
## MessageDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record MessageDto(
Guid Id,
string Content,
string SenderUsername,
string? SenderNicknameColor,
string ChannelName,
DateTimeOffset SentAt,
List<AttachmentDto>? Attachments = null,
List<EmbedDto>? Embeds = null,
string? SenderDisplayName = null,
ReplyRefDto? ReplyTo = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Content` | `string` | — |
| `SenderUsername` | `string` | — |
| `SenderNicknameColor` | `string?` | — |
| `ChannelName` | `string` | — |
| `SentAt` | `DateTimeOffset` | — |
| `Attachments` | `List<AttachmentDto>?` | `null` |
| `Embeds` | `List<EmbedDto>?` | `null` |
| `SenderDisplayName` | `string?` | `null` |
| `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.
## 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).
## 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.
---
## RekeyChannelRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record RekeyChannelRequest(
string OldPassword,
string NewPassword,
string NewEncryptionSalt,
string NewWrappedRoomKey)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `OldPassword` | `string` | — |
| `NewPassword` | `string` | — |
| `NewEncryptionSalt` | `string` | — |
| `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.
## 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"
);
```
## 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.
---
## ReplyRefDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record ReplyRefDto(
Guid MessageId,
string SenderUsername,
string Content)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `MessageId` | `Guid` | — |
| `SenderUsername` | `string` | — |
| `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.
## 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"
);
```
## 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.
---
## SendMessageRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record SendMessageRequest(string ChannelName, string Content)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `ChannelName` | `string` | — |
| `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.
---
## SendUrlRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record SendUrlRequest(string Url)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Url` | `string` | — |
SendUrlRequest is a tiny, immutable URL payload represented as a C# record. Its 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.
## 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.
## Example
```csharp
var request = 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.
---
## UpdateTopicRequest
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record UpdateTopicRequest(string? Topic)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `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.
---
## UserDto
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
> **Kind:** record
```csharp
public record UserDto(
Guid Id,
string Username,
string? DisplayName,
string? NicknameColor,
UserStatus Status,
DateTimeOffset LastSeenAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Username` | `string` | — |
| `DisplayName` | `string?` | — |
| `NicknameColor` | `string?` | — |
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
| `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.
## 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.
## Notes
- DisplayName and NicknameColor are nullable; null should be treated as absent presentation data.
---
@@ -0,0 +1,282 @@
# CommonDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
## Contents
- [ApiResponse](#apiresponse)
- [ApiResponse](#apiresponse-1)
- [ChannelOperationResult](#channeloperationresult)
- [ErrorResponse](#errorresponse)
- [PaginatedResponse](#paginatedresponse)
- [UserOperationResult](#useroperationresult)
- [ChannelError](#channelerror)
- [UserError](#usererror)
---
## ApiResponse
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record ApiResponse(bool Success, string? Message = null, List<string>? Errors = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Success` | `bool` | — |
| [`Message`](../Models/Message.cs.md) | `string?` | `null` |
| `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.
## 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.
## 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.
---
## ApiResponse
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record ApiResponse<T>(bool Success, string? Message = null, List<string>? Errors = null, T? Data = default)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Success` | `bool` | — |
| [`Message`](../Models/Message.cs.md) | `string?` | `null` |
| `Errors` | `List<string>?` | `null` |
| `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.
## 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.
## Example
```csharp
using System.Collections.Generic;
// 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);
```
## 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
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record ChannelOperationResult(ChannelDto? Channel, ChannelError? Error, string? ErrorMessage)
{
public bool IsSuccess => Error is null;
public static ChannelOperationResult Success(ChannelDto channel) => new(channel, null, null);
public static ChannelOperationResult Fail(ChannelError error, string message) => new(null, error, message);
}
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| [`Channel`](../Models/Channel.cs.md) | `ChannelDto?` | — |
| `Error` | `ChannelError?` | — |
| `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.
## 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.
## 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.
---
## ErrorResponse
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record ErrorResponse(string Error, string? Detail = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Error` | `string` | — |
| `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.
## 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.
---
## PaginatedResponse
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record PaginatedResponse<T>(List<T> Items, int Total, int Offset, int Limit)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Items` | `List<T>` | — |
| `Total` | `int` | — |
| `Offset` | `int` | — |
| `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.
## 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
);
```
## 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.
---
## UserOperationResult
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** record
```csharp
public record UserOperationResult(UserProfileDto? User, UserError? Error, string? ErrorMessage)
{
public bool IsSuccess => Error is null;
public static UserOperationResult Success(UserProfileDto user) => new(user, null, null);
public static UserOperationResult Fail(UserError error, string message) => new(null, error, message);
}
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| [`User`](../Models/User.cs.md) | `UserProfileDto?` | — |
| `Error` | `UserError?` | — |
| `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.
## 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.
## 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.
---
## ChannelError
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** enum
```csharp
public enum ChannelError
{
ValidationFailed,
AlreadyExists,
NotFound,
Forbidden,
Protected
}
```
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.
## 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.
---
## UserError
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
> **Kind:** enum
```csharp
public enum UserError
{
ValidationFailed,
AlreadyExists,
NotFound,
InvalidCredentials,
Banned
}
```
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.
## 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.
## 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.
---
@@ -0,0 +1,94 @@
# InviteDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
## Contents
- [CreateInviteRequest](#createinviterequest)
- [InviteDto](#invitedto)
---
## CreateInviteRequest
> **File:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
> **Kind:** record
```csharp
public record CreateInviteRequest(int? MaxUses = null, int? ExpiresInHours = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `MaxUses` | `int?` | `null` |
| `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 dont want to constrain; null properties indicate the server should apply its defaults.
## 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);
```
## 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.
---
## InviteDto
> **File:** `src/EchoHub.Core/DTOs/InviteDtos.cs`
> **Kind:** record
```csharp
public record InviteDto(
string Code,
string CreatedByUsername,
DateTimeOffset CreatedAt,
DateTimeOffset? ExpiresAt,
int MaxUses,
int UseCount)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Code` | `string` | — |
| `CreatedByUsername` | `string` | — |
| `CreatedAt` | `DateTimeOffset` | — |
| `ExpiresAt` | `DateTimeOffset?` | — |
| `MaxUses` | `int` | — |
| `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.
## 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.
---
@@ -0,0 +1,127 @@
# ModerationDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
## Contents
- [AssignRoleRequest](#assignrolerequest)
- [BanRequest](#banrequest)
- [KickRequest](#kickrequest)
- [MuteRequest](#muterequest)
---
## AssignRoleRequest
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
> **Kind:** record
```csharp
public record AssignRoleRequest(string Username, ServerRole Role)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Username` | `string` | — |
| `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.
## 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.
## 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.
---
## BanRequest
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
> **Kind:** record
```csharp
public record BanRequest(string? Reason = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `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.
---
## KickRequest
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
> **Kind:** record
```csharp
public record KickRequest(string? Reason = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `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.
## 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.
## 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.
---
## MuteRequest
> **File:** `src/EchoHub.Core/DTOs/ModerationDtos.cs`
> **Kind:** record
```csharp
public record MuteRequest(string? Reason = null, int? DurationMinutes = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Reason` | `string?` | `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.
---
@@ -0,0 +1,202 @@
# ProfileDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
## Contents
- [AvatarUploadResponse](#avataruploadresponse)
- [UpdateProfileRequest](#updateprofilerequest)
- [UpdateStatusRequest](#updatestatusrequest)
- [UserPresenceDto](#userpresencedto)
- [UserProfileDto](#userprofiledto)
---
## AvatarUploadResponse
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record AvatarUploadResponse(string AvatarAscii)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `AvatarAscii` | `string` | — |
AvatarUploadResponse is a tiny, immutable data container that represents the servers 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.
---
## UpdateProfileRequest
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record UpdateProfileRequest(
string? DisplayName = null,
string? Bio = null,
string? NicknameColor = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `DisplayName` | `string?` | `null` |
| `Bio` | `string?` | `null` |
| `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.
## 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.
## 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");
```
## 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.
---
## UpdateStatusRequest
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record UpdateStatusRequest(
UserStatus Status,
string? StatusMessage = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
| `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.
## 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.
## 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.
---
## UserPresenceDto
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record UserPresenceDto(
string Username,
string? DisplayName,
string? NicknameColor,
UserStatus Status,
string? StatusMessage,
ServerRole Role,
bool IsIrc = false)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Username` | `string` | — |
| `DisplayName` | `string?` | — |
| `NicknameColor` | `string?` | — |
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
| `StatusMessage` | `string?` | — |
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
| `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.
## 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.
---
## UserProfileDto
> **File:** `src/EchoHub.Core/DTOs/ProfileDtos.cs`
> **Kind:** record
```csharp
public record UserProfileDto(
Guid Id,
string Username,
string? DisplayName,
string? Bio,
string? NicknameColor,
string? AvatarAscii,
UserStatus Status,
string? StatusMessage,
ServerRole Role,
DateTimeOffset CreatedAt,
DateTimeOffset LastSeenAt)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Id` | `Guid` | — |
| `Username` | `string` | — |
| `DisplayName` | `string?` | — |
| `Bio` | `string?` | — |
| `NicknameColor` | `string?` | — |
| `AvatarAscii` | `string?` | — |
| `Status` | [`UserStatus`](../Models/UserStatus.cs.md) | — |
| `StatusMessage` | `string?` | — |
| `Role` | [`ServerRole`](../Models/ServerRole.cs.md) | — |
| `CreatedAt` | `DateTimeOffset` | — |
| `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).
## 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.
---
@@ -0,0 +1,73 @@
# ServerDtos.cs
> **Source:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
## Contents
- [EncryptionKeyResponse](#encryptionkeyresponse)
- [ServerStatusDto](#serverstatusdto)
---
## EncryptionKeyResponse
> **File:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
> **Kind:** record
```csharp
public record EncryptionKeyResponse(string Key)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `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.
## 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.
---
## ServerStatusDto
> **File:** `src/EchoHub.Core/DTOs/ServerDtos.cs`
> **Kind:** record
```csharp
public record ServerStatusDto(
string Name,
string? Description,
int OnlineUsers,
int TotalChannels,
string RegistrationMode = "open")
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `Name` | `string` | — |
| `Description` | `string?` | — |
| `OnlineUsers` | `int` | — |
| `TotalChannels` | `int` | — |
| `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.
## 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.
## 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).
---
@@ -0,0 +1,18 @@
# Attachment
> **File:** `src/EchoHub.Core/Models/Attachment.cs`
> **Kind:** class
```csharp
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).
## 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.
## 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.
@@ -0,0 +1,39 @@
# AttachmentKind
> **File:** `src/EchoHub.Core/Models/AttachmentKind.cs`
> **Kind:** enum
```csharp
public enum AttachmentKind
{
Image,
Audio,
File
}
```
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.
## 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.
@@ -0,0 +1,11 @@
# Channel
> **File:** `src/EchoHub.Core/Models/Channel.cs`
> **Kind:** class
```csharp
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).
@@ -0,0 +1,28 @@
# ChannelMembership
> **File:** `src/EchoHub.Core/Models/ChannelMembership.cs`
> **Kind:** class
```csharp
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.
## 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
};
```
## 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.
@@ -0,0 +1,36 @@
# InviteCode
> **File:** `src/EchoHub.Core/Models/InviteCode.cs`
> **Kind:** class
```csharp
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.
@@ -0,0 +1,14 @@
# Message
> **File:** `src/EchoHub.Core/Models/Message.cs`
> **Kind:** class
```csharp
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.
## 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.
@@ -0,0 +1,24 @@
# MessageType
> **File:** `src/EchoHub.Core/Models/MessageType.cs`
> **Kind:** enum
```csharp
public enum MessageType
{
Text,
Image,
File,
Audio
}
```
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.
## 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.
## 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.
@@ -0,0 +1,14 @@
# RefreshToken
> **File:** `src/EchoHub.Core/Models/RefreshToken.cs`
> **Kind:** class
```csharp
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 tokens 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.
## 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.
@@ -0,0 +1,44 @@
# ServerRole
> **File:** `src/EchoHub.Core/Models/ServerRole.cs`
> **Kind:** enum
```csharp
public enum ServerRole
{
Member = 0,
Mod = 1,
Admin = 2,
Owner = 3
}
```
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.
## 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 (03) can affect serialized data; coordinate evolution across all consumers to preserve compatibility.
@@ -0,0 +1,20 @@
# ServerStatsReport
> **File:** `src/EchoHub.Core/Models/ServerStatsReport.cs`
> **Kind:** class
```csharp
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.
## 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.
## 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.
@@ -0,0 +1,21 @@
# User
> **File:** `src/EchoHub.Core/Models/User.cs`
> **Kind:** class
```csharp
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.
## Remarks
This class serves as a central data container used across authentication, user management, presence rendering, and authorization checks. Its 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.
## 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.
@@ -0,0 +1,17 @@
# UserStatus
> **File:** `src/EchoHub.Core/Models/UserStatus.cs`
> **Kind:** enum
```csharp
public enum UserStatus
{
Online,
Away,
DoNotDisturb,
Invisible
}
```
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.
@@ -0,0 +1,38 @@
# RoomCrypto
> **File:** `src/EchoHub.Core/Security/RoomCrypto.cs`
> **Kind:** class
```csharp
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)).
## 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.
## 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.
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)
// 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))
{
// recovered == "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.
@@ -0,0 +1,25 @@
# AsciiBannerService
> **File:** `src/EchoHub.Core/Services/AsciiBannerService.cs`
> **Kind:** class
```csharp
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.
## 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.
## Example
```csharp
string? banner = AsciiBannerService.Render("EchoHub");
if (banner != null)
Console.WriteLine(banner);
```
## 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.
@@ -0,0 +1,19 @@
# FileValidationHelper
> **File:** `src/EchoHub.Core/Services/FileValidationHelper.cs`
> **Kind:** class
```csharp
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 names 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.
## 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.
## 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.
@@ -0,0 +1,28 @@
# ImageToAsciiService
> **File:** `src/EchoHub.Core/Services/ImageToAsciiService.cs`
> **Kind:** class
```csharp
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.
## 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.
## Example
```csharp
using System.IO;
var stream = File.OpenRead("path/to/image.png");
var service = new ImageToAsciiService();
string ascii = service.ConvertToAscii(stream, 80, 40);
Console.WriteLine(ascii);
```
## 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.
@@ -0,0 +1,19 @@
# IrcBroadcaster
> **File:** `src/EchoHub.Server.Irc/IrcBroadcaster.cs`
> **Kind:** class
```csharp
public class IrcBroadcaster : IChatBroadcaster
```
Broadcasts chat events to IRC clients by translating application-level messages and room/user events into IRC protocol lines and sending them via the IrcGatewayService. Decrypts transport-layer-encrypted message content so IRC clients (which do not support the application's app-layer encryption) receive readable text; end-to-end room ciphertext markers (e.g. $RC1$) are preserved. Use this class when you need to mirror server chat rooms and user lifecycle events to connected IRC clients.
## Remarks
IrcBroadcaster is the IRC-specific implementation of the IChatBroadcaster contract and acts as the bridge between the chat model and the IRC wire format. It relies on IMessageEncryptionService to remove transport-layer encryption for IRC consumers and on IrcMessageFormatter to produce IRC-compliant lines (including splitting long messages and formatting reply prefixes/embeds). Messages are dispatched by enumerating connections returned from the gateway and sending formatted lines to each connection; the broadcaster also follows IRC conventions such as avoiding echoing a message back to the originating connection.
## Notes
- Message content is decrypted before formatting; E2E room ciphertext (explicit markers like $RC1$) is left unchanged so room-encrypted messages are not exposed.
- The broadcaster avoids echoing by comparing connection IDs (excludeConnectionId) when sending normal messages. Be aware this requires callers to pass the originating connection id to suppress local echoes correctly when appropriate.
- Sends are awaited sequentially per connection/line (each connection's SendAsync is awaited in a loop). Under high fan-out this can introduce latency; consider batching or parallelization at the caller/gateway level if latency becomes an issue.
@@ -0,0 +1,22 @@
# IrcClientConnection
> **File:** `src/EchoHub.Server.Irc/IrcClientConnection.cs`
> **Kind:** class
```csharp
public sealed class IrcClientConnection : IAsyncDisposable
```
Manages a single IRC client TCP connection: wraps a TcpClient/Stream pair, serializes outgoing IRC lines, provides simple registration and channel membership state, and exposes read/write helpers for the IRC protocol. Use this when handling a single connected client in server code so you get consistent CRLF framing, UTF-8 encoding without BOM, and serialized writes.
## Remarks
This type represents the per-connection state and I/O for one IRC client. It centralizes the socket-level StreamReader/StreamWriter setup (UTF-8 without BOM, CRLF line endings, AutoFlush) and enforces serialized writes with an internal SemaphoreSlim. The class keeps mutable registration and presence properties (nickname, username, authentication flags, away message, joined channels) so higher-level command handlers and broadcaster threads can consult or update a single source of truth. Channel membership access is guarded by a private lock and exposed via snapshot methods so broadcaster threads can read without additional synchronization.
## Notes
- ReadLineAsync swallows read exceptions and returns null; treat a null result as "connection closed" or irrecoverable read error.
- SendAsync swallows write exceptions (connection lost) after serializing via an internal semaphore; callers cannot observe write failures directly.
- Registration properties (Nickname, Username, UserId, IsRegistered, etc.) are not synchronized by this class — callers should coordinate concurrent access if needed.
- Channel membership APIs (JoinChannel, LeaveChannel, IsInChannel, GetJoinedChannels) are thread-safe: the implementation takes a lock and GetJoinedChannels returns a snapshot list to avoid callers iterating the internal set directly.
- Hostmask composes Nickname and Username; Username may be null so Hostmask uses Username ?? Nickname in its string formatting.
- DisposeAsync closes the underlying TcpClient and disposes the reader/writer and semaphore; do not use the connection after disposing.
@@ -0,0 +1,933 @@
# IrcCommandHandler.cs
> **Source:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
## Contents
- [IrcCommandHandler](#irccommandhandler)
- [IrcCommandHandler (constructor)](#irccommandhandler-constructor)
- [ServerName](#servername)
- [HandleAuthenticateAsync](#handleauthenticateasync)
- [HandleAwayAsync](#handleawayasync)
- [HandleCapAsync](#handlecapasync)
- [HandleCommandAsync](#handlecommandasync)
- [HandleJoinAsync](#handlejoinasync)
- [HandleListAsync](#handlelistasync)
- [HandleModeAsync](#handlemodeasync)
- [HandleNamesAsync](#handlenamesasync)
- [HandleNickAsync](#handlenickasync)
- [HandlePartAsync](#handlepartasync)
- [HandlePassAsync](#handlepassasync)
- [HandlePingAsync](#handlepingasync)
- [HandlePrivmsgAsync](#handleprivmsgasync)
- [HandleQuitAsync](#handlequitasync)
- [HandleTopicAsync](#handletopicasync)
- [HandleUserAsync](#handleuserasync)
- [HandleWhoAsync](#handlewhoasync)
- [IrcToEchoHubChannel](#irctoechohubchannel)
- [RequireRegisteredAsync](#requireregisteredasync)
- [RunAsync](#runasync)
- [SendChannelTopicAsync](#sendchanneltopicasync)
- [SendModeErrorAsync](#sendmodeerrorasync)
- [SendMotdAsync](#sendmotdasync)
- [SendNamesReplyAsync](#sendnamesreplyasync)
- [SendWelcomeBurstAsync](#sendwelcomeburstasync)
- [TryCompleteRegistrationAsync](#trycompleteregistrationasync)
- [HandleWhoisAsync](#handlewhoisasync)
---
## IrcCommandHandler
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** class
```csharp
public sealed class IrcCommandHandler
```
Acts as the main dispatcher that translates incoming IRC protocol messages from a single IrcClientConnection into actions against the server-side chat, user and channel services. Reach for this class when you need the gateway that accepts an IRC client, performs registration/authentication (SASL or PASS), sends the welcome/MOTD burst, and maps channel/query commands (JOIN, PART, PRIVMSG, NAMES, TOPIC, WHO/WHOIS, LIST, etc.) into the server's chat/channel/user APIs.
## Remarks
This sealed handler centralizes IRC protocol handling for one client connection. It coordinates authentication (including SASL and PASS fallbacks), completes IRC registration (NICK/USER), and then routes post-registration commands to the underlying IChatService, IUserService, and IChannelService. It also enforces gateway-specific policies mentioned in the source comments: encrypted rooms are not readable over IRC (so joins are blocked), system channels are not proxied to IRC, and private channels are omitted from LIST results. Message history replay must be decrypted via IMessageEncryptionService before being sent to the IRC client.
## Notes
- Encrypted rooms are intentionally blocked from JOIN over the IRC gateway: the gateway does not hold room keys and therefore cannot expose encrypted room contents to IRC clients.
- Private channels are hidden from LIST to match the SignalR client's channel visibility; expect LIST to only include public/discoverable channels.
- Registration and authentication have multiple paths (SASL, PASS, or account registration fallback); callers should expect asynchronous authentication flow and that RunAsync accepts a CancellationToken to stop processing.
---
### IrcCommandHandler (constructor)
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** constructor
```csharp
public IrcCommandHandler(
IrcClientConnection conn,
IrcOptions options,
IChatService chatService,
IUserService userService,
IChannelService channelService,
IMessageEncryptionService encryption,
ILogger logger)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `conn` | [`IrcClientConnection`](IrcClientConnection.cs.md) | — |
| `options` | [`IrcOptions`](IrcOptions.cs.md) | — |
| `chatService` | [`IChatService`](../EchoHub.Core/Contracts/IChatService.cs.md) | — |
| `userService` | [`IUserService`](../EchoHub.Core/Contracts/IUserService.cs.md) | — |
| `channelService` | [`IChannelService`](../EchoHub.Core/Contracts/IChannelService.cs.md) | — |
| `encryption` | [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) | — |
| `logger` | `ILogger` | — |
The constructor initializes an IrcCommandHandler by wiring together its required collaborators: IrcClientConnection, IrcOptions, IChatService, IUserService, IChannelService, IMessageEncryptionService, and ILogger. It stores these dependencies in private fields so the command-handling logic can access the IRC connection, configuration, chat and user/channel services, encryption features, and logging throughout command processing. This pattern follows dependency injection, enabling easy testing with mocks and seamless composition by the applications DI container at startup.
## Remarks
By aggregating these collaborators, the constructor centralizes the wiring of core capabilities—network I/O, configuration, domain services for chat, user and channel state, cryptographic operations, and observability—so command processing remains focused on business logic rather than setup. This design promotes testability, consistency, and clear separation of concerns within the IRC subsystem.
## Notes
- The constructor as shown does not perform null checks; ensure the DI container enforces non-null registrations or add guards in production code.
- Be mindful of lifetime management: the handler should typically share lifetimes with its collaborators or be disposed in tandem to avoid resource leaks.
---
### ServerName
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** property
```csharp
private string ServerName => _options.ServerName
```
This private read-only property exposes the server name configured in the handlers options by forwarding to _options.ServerName. It should be used whenever the command handler needs the target IRC server name, offering a single indirection point if the source of that value changes in the future.
## Remarks
By wrapping the access in ServerName, you decouple usage from the underlying options data. This centralization makes future changes (like deriving the server name from a different config source or applying normalization) localized to this property. It also communicates that the server name is a configuration concern and not a computed field of the handler itself.
## Notes
- The property simply forwards to _options.ServerName; it does not perform validation or mutation.
- If _options.ServerName can change at runtime, callers may observe updates on subsequent accesses.
---
### HandleAuthenticateAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleAuthenticateAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Handles SASL authentication for a connected IRC client by processing the SASL-related AUTHENTICATE messages. It supports initiating SASL with PLAIN, aborting SASL, and performing the actual PLAIN payload verification, ultimately authenticating or registering the user, and then updating the connection state and sending appropriate IRC numeric replies.
## Remarks
This method centralizes SASL negotiation within the command handler, bridging the IRC SASL protocol with the application's user store. It redacts the password in logs and relies on the user service to either authenticate or register the user, enabling a smooth first-time login flow. It validates payload structure and wraps the process in a catch block to translate unexpected errors into SASL failure feedback while preserving a consistent connection state.
## Notes
- The SASL payload must decode to a null-delimited string yielding at least three parts; malformed payloads trigger an ERR_SASLFAIL response.
- The code derives the username from parts[1] when present, otherwise parts[0], and normalizes it to lowercase; the actual password is sourced from a redacted variable and is not logged.
- On success, the connection's Nickname and UserId are populated, the connection is marked authenticated, and the client receives both a LOGGEDIN notice and a SASL success reply; failures emit ERR_SASLFAIL and are logged for auditing.
---
### HandleAwayAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleAwayAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
HandleAwayAsync processes a user's away status in response to the IRC AWAY command. It first ensures the caller is registered; if not, it exits early. When a non-empty parameter is supplied, it stores that string as the away message on the connection, updates the user's status to Away via the chat service, and sends a RPL_NOWAWAY reply to the client. If no parameter is provided, it clears the away message, updates the status to Online with a null message, and sends a RPL_UNAWAY reply. The method is asynchronous, so it does not block the command handling path while performing persistence and network communication.
## Remarks
Consolidates away-state handling in a single place so all callers see the same effect on status and client notification. It keeps IrcCommandHandler lean by delegating away management and relies on _chatService to persist user state. It uses a simple, deterministic flow based on whether a message parameter is provided.
## Notes
- Accessing _conn.UserId with the null-forgiving operator assumes RequireRegisteredAsync succeeded; calling this method without a valid registered session could throw a NullReferenceException.
- This method sends numeric replies (RPL_NOWAWAY / RPL_UNAWAY) to the connected client; ensure ServerName and _conn are valid at call time.
---
### HandleCapAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleCapAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
HandleCapAsync processes CAP negotiation commands from the IRC server. It requires at least one parameter; if none are provided, it returns without action. It switches on the upper-cased first parameter to implement the SASL/capability handshake: on LS it requests the sasl capability and marks negotiation as in progress; on REQ it either acknowledges the 'sasl' request and enables SASL, or responds with NAK for the requested capability; on END it ends negotiation and, if identity information is available and the client is not yet registered, triggers a registration attempt via TryCompleteRegistrationAsync.
## Remarks
This method centralizes the CAP negotiation lifecycle for the IRC connection, coordinating with the connection state (_conn) to track whether a CAP negotiation is underway, whether SASL is engaged, and whether registration has completed. By encapsulating the protocol specifics here, it avoids scattering CAP handling logic across multiple handlers and ensures correct sequencing between CAP negotiation, SASL activation, and user registration.
## Notes
- The method short-circuits when there are no parameters, avoiding potential null-reference issues.
- The REQ path treats a missing or non-matching second parameter as a NAK for the requested capability, preserving protocol safety.
- END clears the negotiation flag and only triggers registration if Nickname and Username are non-null and the client is not already registered, preventing premature or repeated registration attempts.
---
### HandleCommandAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private Task HandleCommandAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Dispatches incoming IRC commands by normalizing the command to uppercase and routing to the corresponding per-command asynchronous handler, centralizing the IRC command handling logic (e.g., CAP -> HandleCapAsync, PRIVMSG -> HandlePrivmsgAsync). If the command is unknown, it responds with the ERR_UNKNOWNCOMMAND numeric back to the client.
## Remarks
Centralizes command dispatch behind a single switch expression, mapping command strings to their asynchronous handlers. This design makes it straightforward to extend support for new commands by adding a new case to the switch. It returns a Task to support asynchronous work and relies on the private _conn to send numeric replies back to the client; some branches return Task.CompletedTask to represent no-op work for certain commands (e.g., PONG).
## Example
```csharp
// Example: dispatch flow for a known command
IrcMessage msg = /* ... */;
await HandleCommandAsync(msg); // if msg.Command == "PRIVMSG" this path invokes HandlePrivmsgAsync(msg)
```
## Notes
- Unknown commands trigger an ERR_UNKNOWNCOMMAND reply, authored with the server name and the raw command.
- The PONG path is treated as a no-op by returning Task.CompletedTask, avoiding unnecessary asynchronous work.
---
### HandleJoinAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleJoinAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Handles an IRC JOIN request for a registered user, performing parameter validation, channel-name mapping, and policy checks before joining the user to each requested channel. It delegates to backend services to perform the join, then updates the client with a JOIN confirmation, channel topic, NAMES list, and a decrypted history replay.
## Remarks
This function centralizes the join workflow for the IRC gateway and enforces privacy and policy constraints: end-to-end encrypted channels and server-managed system channels are blocked from IRC joins, ensuring the EchoHub client remains the source of truth for restricted channels. It coordinates with the connection object, channel service, and chat service to validate input, perform joins per channel (supporting RFC 1459-style per-channel keys), and synchronize the IRC client view (JOIN message, topic, NAMES, and history).
## Notes
- Requires the user to be registered; if not, the method exits early and no join is attempted.
- If there are fewer than one parameter, the gateway responds with ERR_NEEDMOREPARAMS to indicate insufficient input.
- For each channel, invalid channel names yield ERR_NOSUCHCHANNEL with an invalid channel notice.
- End-to-end encrypted channels are blocked from IRC joins; use the EchoHub client for such channels.
- System channels are blocked from IRC joins because they stream content over SignalR; use the EchoHub client for access.
- When a channel join requires a password and the provided key is incorrect or missing, the gateway responds with ERR_BADCHANNELKEY.
- History is replayed after joining, with Content and any embedded replies decrypted for proper IRC presentation.
---
### HandleListAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleListAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Implements the IRC LIST command for the server. After verifying the client is registered, it fetches the channel list from the channel service, filters to public channels, and sends one RPL_LIST reply per channel containing the channel name, online user count, and topic. If a channel is protected, a [+k] lock hint is prefixed to the topic. Private channels are intentionally hidden to match what the SignalR client sees. Once all public channels have been reported, it sends RPL_LISTEND to signal completion.
## Remarks
This handler encapsulates the server-side semantics of channel discovery separate from the client protocol encoding. By filtering to IsPublic channels, it keeps private channels from being exposed to clients, preserving privacy where appropriate. The lock indicator (+k) encodes channel protection state in the LIST output, while the Topic is plumbed directly into the listing, enabling clients to present useful metadata without additional requests. The approach keeps channel management in _channelService and I/O in _conn, promoting testability and a clean separation between data retrieval and protocol signaling.
## Notes
- The method requires a registered user; unauthenticated users will cause the method to return early without emitting LIST data due to the initial RequireRegisteredAsync check.
- Private channels are hidden by design via the IsPublic filter; modify the filter only if you intend to expose private channels and ensure client expectations are updated accordingly.
---
### HandleModeAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleModeAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
HandleModeAsync processes incoming MODE commands for channels and queries. It first ensures the caller is registered, then validates parameters and resolves the IRC target. For channel targets, it either returns the current channel mode or applies mode changes (notably +k to set a channel password and -k to clear it), persisting changes through the channel service and signaling results with the appropriate IRC numerics. When the target is not a channel, it responds with the user-mode indicator (+) to indicate no user modes are reported. If the channel cannot be resolved, it returns ERR_NOSUCHCHANNEL. When querying a channel's mode (MODE #channel with no extra parameters), it responds with RPL_CHANNELMODEIS and, if the channel is protected, indicates +k. For mode changes, it handles +k (requiring a key) and -k (clearing the key); unknown modes yield ERR_UNKNOWNMODE. A small, targeted behavior detail is that probing the ban list returns an empty list via RPL_ENDOFBANLIST to mirror common client expectations during join.
## Dependencies
- MODE
- Parameters
- IrcNumericReply
## Dependency APIs (verified signatures)
- property `Parameters` (`src/EchoHub.Server.Irc/IrcMessage.cs`)
- class [`IrcNumericReply`](IrcNumericReply.cs.md) (`src/EchoHub.Server.Irc/IrcNumericReply.cs`)
- field `string RPL_WELCOME`
- field `string RPL_YOURHOST`
- field `string RPL_CREATED`
- field `string RPL_MYINFO`
- field `string RPL_ISUPPORT`
- field `string RPL_MOTDSTART`
- field `string RPL_MOTD`
- field `string RPL_ENDOFMOTD`
- field `string ERR_NOMOTD`
- field `string RPL_NOTOPIC`
- field `string RPL_TOPIC`
- field `string RPL_NAMREPLY`
- …and 35 more member(s) not shown
## Symbol To Document
- Name: HandleModeAsync
- Kind: method
- File: src/EchoHub.Server.Irc/IrcCommandHandler.cs
- Language: csharp
- ID: 7207cb42-b229-42e0-9b0c-126018e8c975
---
### HandleNamesAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleNamesAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
HandleNamesAsync processes an incoming NAMES-like query for the IRC command handler. It first ensures the caller is registered by awaiting RequireRegisteredAsync; if the user is not registered, the method exits early to prevent exposing channel membership information to unauthorized callers. It then requires at least one parameter; if none are provided, it returns without a response. It converts the first parameter to the internal EchoHub channel using IrcToEchoHubChannel; if this mapping yields null, the method again exits. When all preconditions succeed, it issues the names response for the mapped channel by calling SendNamesReplyAsync with that channel.
## Remarks
This method centralizes the NAMES query flow, isolating authentication, input validation, and channel-name resolution from the response formatting logic. It enforces that only authenticated, well-formed requests proceed to produce a response, contributing to predictable and secure command handling.
## Notes
- Silent declines: if preconditions fail (not registered, missing parameters, or invalid channel mapping), the method returns without emitting a response.
- The mapping function (IrcToEchoHubChannel) determines whether an IRC channel reference has a corresponding internal EchoHub channel; a null result means no valid target was found, and no response is produced.
---
### HandleNickAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleNickAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Handles the NICK command from a connected IRC client. It validates that a nickname parameter is supplied, enforces the server's username rules, stores a canonical lowercase nickname on the connection, and, when applicable, advances the registration flow by attempting to complete registration if a username is already present.
## Remarks
Centralizes nickname processing in the command handler to ensure consistent validation, normalization, and state progression. It uses numeric replies to communicate issues back to the client (missing nickname or invalid nickname) and coordinates with the registration logic via TryCompleteRegistrationAsync once the client is partially authenticated. Normalizing to lowercase provides a stable internal identity, independent of the client's casing.
## Notes
- The error text for invalid nicknames lists allowed characters and length; confirm that UsernameRegex() and the user-visible message remain in sync to avoid misleading users.
---
### HandlePartAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandlePartAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Leaves one or more IRC channels as requested by an incoming IrcMessage. It first ensures the caller is registered; if not, it exits early without issuing any IRC traffic. It expects at least one parameter; the first parameter is a comma-separated list of raw channel names, and an optional second parameter carries a part message to be appended after PART. For each channel in the list, the method translates the raw channel into an internal channel name using IrcToEchoHubChannel; if mapping returns null, that channel is skipped. It then tells the chat service to leave the mapped channel, updates the local connection state by calling LeaveChannel, and finally emits the IRC PART command for that channel, including the optional message.
## Remarks
Acts as a coordination boundary between the IRC protocol and the application's connection state. It encapsulates registration verification, channel translation, state mutation, and protocol emission in a single command path. Because it awaits each channel in sequence, multiple PARTs are issued in order rather than in parallel.
## Notes
- Early returns ensure no actions occur if the user is not registered or if no channels are specified.
- Channels that cannot be translated via IrcToEchoHubChannel are skipped without error.
- The emitted PART command uses the hostmask and a '#channel' target, and appends an optional message if provided.
---
### HandlePassAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private Task HandlePassAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
HandlePassAsync is a private helper in the IRC command handling flow that processes the PASS command for a connection. It blocks re-registration by sending ERR_ALREADYREGISTERED when the connection is already registered, and if a password parameter is provided, it routes that parameter to the connections password handling path (the actual value is redacted in this snippet). In cases where neither condition applies, it completes without performing additional work.
## Remarks
This abstraction centralizes PASS command handling within the command handler to ensure consistent protocol error signaling and password processing across the handshake sequence. It delegates state management and messaging to the underlying connection object, which keeps the command dispatch logic focused and testable. The explicit redaction of the password demonstrates a security-conscious approach to handling sensitive data, avoiding exposure in logs or snapshots. By returning a Task, the method remains composable with the asynchronous command pipeline.
## Notes
- If a PASS parameter is provided, ensure proper validation and secure handling of the credential; the actual value is redacted here, so verify correctness in your environment.
- The method relies on external state (_conn.IsRegistered) and may either complete synchronously or proceed asynchronously via SendNumericAsync; callers should await as appropriate to preserve command-ordering guarantees.
- This function does not perform full authentication itself; it coordinates with the connection object for state and output, acting as a gateway in the PASS handling path.
---
### HandlePingAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandlePingAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Responds to IRC PING messages by sending a corresponding PONG back to the server to keep the connection alive. It chooses the token to include in the PONG from the incoming message: if a parameter is present, that token is used; otherwise it falls back to the server name. The response is sent using the underlying connection with the format :<ServerName> PONG <ServerName> :<token>.
## Remarks
Internally, this method serves as the keep-alive handler for the IRC command flow. By basing the PONG on ServerName and the received parameters, it guarantees a consistent reply format and avoids leaking raw protocol details to higher layers. It relies on the ServerName and Parameters dependencies and on the underlying connection to transmit the response.
## Example
```csharp
// Example: a PING with a token results in a PONG containing that token
var token = "12345";
// Assuming ServerName is "irc.example.org"
var response = $":{ServerName} PONG {ServerName} :{token}";
// The actual send occurs via _conn.SendAsync in HandlePingAsync
```
## Notes
- If msg.Parameters is empty, token defaults to ServerName.
- The method is private; usage is internal to the IrcCommandHandler and not exposed publicly.
- Exceptions from SendAsync propagate; callers may need to log or retry as part of larger connection management.
---
### HandlePrivmsgAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandlePrivmsgAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Handles the PRIVMSG command by validating parameters, ensuring the sender is registered, and routing channel-targeted messages to the EchoHub chat service. It rejects private messages (targets that do not start with '#') with an appropriate error and surfaces delivery failures back to the IRC client.
## Remarks
This method acts as a boundary between IRC protocol handling and the EchoHub chat system. It enforces channel-only messaging for PRIVMSG, consolidates parameter validation and error reporting via IRC numeric replies, and delegates the actual delivery to a dedicated chat service. By encapsulating channel-name translation (IrcToEchoHubChannel) and the delivery call (SendMessageAsync), it keeps command handling focused and testable, while remaining resilient to mapping failures and chat-service errors.
## Notes
- Requires the current connection to be registered; otherwise the operation is short-circuited.
- If the PRIVMSG target does not begin with '#', a private-message error is returned: ERR_NOSUCHNICK with a hint to use channels.
- If channel name mapping returns null, the method exits without performing delivery.
- If SendMessageAsync reports an error, the client receives ERR_CANNOTSENDTOCHAN to indicate delivery failure to the channel.
---
### HandleQuitAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleQuitAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Handles a quit event by sending an IRC ERROR line to the active connection that signals the closing of the link. It derives the quit reason from the first parameter of the incoming IrcMessage when provided, otherwise it uses 'Client quit' as a default.
## Remarks
Centralizes the termination messaging for quit scenarios, ensuring a consistent closing notice across paths that terminate a connection. It formats the message with the current nickname and the resolved quit reason, and delegates the actual network transmission to _conn.SendAsync, keeping the higher-level quit flow simple and testable.
## Example
```csharp
// Example usage within the same class (quit with a reason)
var msg = new IrcMessage { Parameters = new List<string> { "Server maintenance" } };
await HandleQuitAsync(msg);
```
## Notes
- This method only sends the closing line; it does not by itself terminate the connection. The caller should close the connection after the message is sent.
- It relies on msg.Parameters[0] as the quit reason; if there are multiple parameters, only the first is used.
- Assumes msg.Parameters is non-null; if it's null, this will throw a NullReferenceException.
---
### HandleTopicAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleTopicAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
HandleTopicAsync processes the IRC TOPIC command for a channel. It verifies the caller is registered, resolves the channel name from the command parameters, and either sends the current topic or updates it via the channel service using the provided topic text. On a successful update, it broadcasts the change to SignalR clients and echoes the topic back to the IRC client; on failure it returns an appropriate IRC numeric error.
## Remarks
This method acts as the integration point between IRC command handling, domain services, and client notifications. It relies on the authentication check (RequireRegisteredAsync) and uses the channel service to persist topic changes while informing connected clients through the chat service. Numeric errors are produced through IrcNumericReply based on the nature of the failure (non-existent channel vs. insufficient privileges), ensuring correct IRC protocol behavior. When a topic is cleared, a whitespace topic is treated as null and passed to UpdateTopicAsync, signaling a topic removal.
## Notes
- The method returns early if the caller is not registered or if there are insufficient parameters, preventing unintended state changes.
- It uses a null-forgiving operator on UserId when updating the topic; preconditions ensure a valid user context.
- Topic clearing is achieved by passing null to UpdateTopicAsync when the provided topic string is whitespace.
- Error handling maps ChannelError.NotFound to ERR_NOSUCHCHANNEL and all other failure cases to ERR_CHANOPRIVSNEEDED, aligning with IRC protocol expectations.
---
### HandleUserAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleUserAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Handles the USER command as part of the IRC registration handshake. It first rejects re-registration attempts, then validates that enough parameters are present, stores the provided username and real name on the connection, and finally triggers registration completion if a nickname has already been supplied.
## Remarks
This method encapsulates the user-side portion of the registration flow, coordinating between the incoming command data (via IrcMessage.Parameters) and the connection state. By separating the completion trigger (TryCompleteRegistrationAsync) from initial USER parsing, it keeps the registration logic cohesive and allows the NICK/USER agreement to occur in any order. It relies on the server-generated numeric replies to communicate errors back to the client and uses the connection state to decide when registration can advance.
## Notes
- Parameter indexing assumes four parameters for a valid USER command; if fewer are provided, the handler responds with ERR_NEEDMOREPARAMS. The RealName is taken from Parameters[3], which is a potential source of off-by-one mistakes if the protocol is extended or parameters are reformatted.
- A full registration is only completed when a nickname is already present; otherwise, the method merely populates Username and RealName and leaves completion to a later trigger when Nickname arrives.
- The code does not validate that Username or RealName are non-empty; additional validation may be needed if stricter user data integrity is required.
---
### HandleWhoAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleWhoAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Responds to an IRC WHO request for a channel by listing online users and signaling completion. It is invoked when a registered client asks for the current participants of a channel; it maps the supplied channel parameter to EchoHub's channel, retrieves online users via the chat service, and streams RPL_WHOREPLY rows followed by RPL_ENDOFWHO to the client.
## Remarks
By translating EchoHub's channel membership into IRC WHO semantics, this method acts as the bridge between the IRC protocol and the chat model. It performs early guards (registration and parameter validation) before querying the chat service, ensuring consistent behavior and preventing unnecessary work for unauthenticated callers. Each user is emitted with a RPL_WHOREPLY line containing their nick, username, server, and away/here flag, followed by a final EndOfWho line to signal completion.
## Notes
- Always emits an End of WHO line even if the channel has no online users.
- Away vs. here status is encoded as 'G' for away and 'H' for present, matching IRC conventions.
---
### IrcToEchoHubChannel
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private static string? IrcToEchoHubChannel(string ircChannel)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `ircChannel` | `string` | — |
**Returns:** `string?`
Converts a raw IRC channel into a canonical EchoHub channel name by stripping the leading '#', lowercasing, and trimming the remainder, returning null if the result does not satisfy ValidationConstants.ChannelNameRegex. This is used when bridging IRC channels to EchoHub to obtain a policy-compliant channel identifier.
## Remarks
Centralizes the logic for translating IRC-style channels into EchoHub identifiers and enforces channel naming policy via ValidationConstants.ChannelNameRegex. It returns a lowercase, trimmed name when valid, or null when the input cannot be mapped, allowing callers to handle non-mappable channels explicitly.
## Notes
- If ircChannel is null, this method will throw a NullReferenceException; callers should ensure a non-null value before calling.
- Results are always lowercase due to ToLowerInvariant, providing a consistent channel namespace.
- A non-matching input yields null rather than an exception, signaling an unmapped channel to the caller.
---
### RequireRegisteredAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task<bool> RequireRegisteredAsync()
```
**Returns:** `Task<bool>`
RequireRegisteredAsync is a small helper that enforces a precondition: the client connection must be registered before proceeding with commands that require registration. It returns true when the connection is already registered; otherwise it sends the IRC error reply ERR_NOTREGISTERED and returns false. Callers await this method to guard subsequent operations and avoid duplicating boilerplate checks across command handlers.
## Remarks
This abstraction centralizes the registration precondition and the associated user feedback. It guarantees consistent behavior by issuing the standard ERR_NOTREGISTERED along with the message You have not registered, matching the IRC protocol's expectations, and it short-circuits command execution when the precondition isnt met.
## Example
```csharp
// Usage: ensure the user is registered before issuing a command that requires registration
if (!await RequireRegisteredAsync())
{
return; // bail out if not registered
}
// proceed with the operation that requires registration
```
## Notes
- Ensure the caller returns immediately when RequireRegisteredAsync() returns false to avoid sending duplicate replies.
- This helper assumes the underlying connection (_conn) and the server name (ServerName) are initialized; null references may occur if called too early.
---
### RunAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
public async Task RunAsync(CancellationToken ct)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `ct` | `CancellationToken` | — |
**Returns:** `Task`
Runs an asynchronous loop that continuously reads lines from the IRC connection, trims trailing CR/LF, ignores blank lines, and dispatches each non-empty message to the IRC command handler until cancellation is requested. This is the core IO loop for processing incoming IRC traffic in the command handler lifecycle; you start it to begin processing and cancel it to stop.
## Remarks
RunAsync is the primary lifecycle loop for the IRC command processor. It reads a raw line via _conn.ReadLineAsync(ct), cleans trailing CR/LF, and skips empty lines before turning the line into an IrcMessage with IrcMessage.Parse. The resulting message is passed to HandleCommandAsync for per-command processing, and any exceptions thrown during that processing are caught and logged to avoid tearing down the loop. Only the HandleCommandAsync call is wrapped in the try-catch; errors in reading, parsing, or line pre-processing may bubble up if they throw, which means callers should supervise the task accordingly.
## Notes
- Exceptions from ReadLineAsync or IrcMessage.Parse are not caught here; they could terminate the loop.
- The loop ends when a null line is read (end of stream) or when the cancellation token is canceled.
- Whitespace-only lines are ignored; lines are trimmed before parsing.
---
### SendChannelTopicAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task SendChannelTopicAsync(string channelName)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `channelName` | `string` | — |
**Returns:** `Task`
Fetches the current topic for the specified channel and sends the corresponding IRC numeric to the client. It queries the channel service for (topic, exists) and, if the channel exists, emits RPL_TOPIC when a topic is set or RPL_NOTOPIC when no topic is configured; if the channel doesn't exist, it returns without replying.
## Remarks
By centralizing the topic-resolution and numeric-emission logic in a single private method, this symbol encapsulates the IRC topic-response behavior for channel-related command flow. It hides the implementation details of IrcNumericReply mappings behind a concise interface and ensures consistent message formatting (channel name prefixed with '#', topic payload prefixed with ':') when interacting with the connection and channel services.
## Notes
- If exists is false, the method returns early with no notification to the client.
- When a channel exists but has no topic, a RPL_NOTOPIC reply is sent with the message "No topic is set".
---
### SendModeErrorAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task SendModeErrorAsync(string channelName, ChannelOperationResult result)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `channelName` | `string` | — |
| `result` | [`ChannelOperationResult`](../EchoHub.Core/DTOs/CommonDtos.cs.md) | — |
**Returns:** `Task`
Translates a channel operation error into the corresponding IRC numeric response and sends it to the client for the specified channel. It chooses the numeric based on result.Error (NotFound -> ERR_NOSUCHCHANNEL, Forbidden -> ERR_CHANOPRIVSNEEDED, otherwise ERR_KEYSET) and delivers a message containing the channel (prefixed with '#') and the human-readable error via _conn.SendNumericAsync(ServerName, numeric, `#${channelName} :${result.ErrorMessage}`). This method centralizes the error reporting for channel-mode operations so callers don't duplicate the mapping and formatting logic.
## Remarks
By centralizing the error-to-numeric mapping, this method ensures consistent client feedback and prevents duplication of channel-name formatting and error-message construction across callers. It relies on the surrounding classs _conn and ServerName being available; changes to the mapping or messaging format would affect all mode-error reports produced by this helper.
## Notes
- The error mapping is not exhaustive: any ChannelError value not explicitly NotFound or Forbidden will default to ERR_KEYSET.
- This method is private and intended solely for internal command-handling use; it is not part of the public API.
---
### SendMotdAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task SendMotdAsync()
```
**Returns:** `Task`
SendMotdAsync is an internal helper that transmits the server's Message of the Day (MOTD) to the connected client. It validates the configured Motd; if it is missing or whitespace it replies with ERR_NOMOTD and stops. Otherwise it sends a MOTD banner with RPL_MOTDSTART, then each newline-delimited line as an RPL_MOTD, trimming CR characters, and ends with RPL_ENDOFMOTD.
This method consolidates MOTD delivery behind a private surface, so higher-level Irc command handlers don't need to know the exact numeric codes or line-breaking semantics. It depends on _conn for transport and _options for the Motd value, and it's a private method intended to be invoked by the MOTD-related command flow.
## Remarks
Encapsulates the formatting and transport of MOTD to ensure consistent behavior across the server. By isolating the MOTD delivery, it keeps the command-handling code focused on protocol logic rather than presentation details.
## Notes
- If Motd is null or whitespace, the method sends ERR_NOMOTD and returns without sending any MOTD lines.
- Each MOTD line is sent as a separate RPL_MOTD message; the code splits on '\n' and trims a trailing '\r' from each line to normalize Windows-style endings. A trailing newline in Motd may produce an empty MOTD line.
- All sends are awaited asynchronous calls to the connection; exceptions propagate to the caller.
---
### SendNamesReplyAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task SendNamesReplyAsync(string channelName)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `channelName` | `string` | — |
**Returns:** `Task`
Sends the channels NAMES list to the IRC client by querying the chat service for online users in the channel, producing a space-separated set of nicknames, and then emitting two standard IRC numerics: RPL_NAMREPLY with the channel and nicklist, and RPL_ENDOFNAMES to mark completion. This method is invoked when handling a NAMES request for a channel, and it centralizes the formatting and numeric-codes so callers don't have to build the response themselves.
## Remarks
This keeps NAMES formatting centralized and aligns with the IRC protocol surface exposed by IrcNumericReply. It delegates data retrieval to _chatService and transmission to _conn, making the implementation resilient to channel naming and user list changes. It also ensures the end-of-list is always signaled after the list is sent, which is essential for IRC clients to know the response is complete.
## Notes
- If no online users are found, the NAMES reply will carry an empty nicklist while still issuing EndOfNames; clients should handle an empty list gracefully.
- Any exceptions raised by GetOnlineUsersAsync or SendNumericAsync bubble up to the caller, so this method assumes the surrounding command handler will decide how to respond to errors.
---
### SendWelcomeBurstAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task SendWelcomeBurstAsync()
```
**Returns:** `Task`
Sends the IRC welcome burst to a newly connected client by issuing the standard numeric replies (RPL_WELCOME, RPL_YOURHOST, RPL_CREATED, RPL_MYINFO, RPL_ISUPPORT) and then starts the MOTD flow via SendMotdAsync. It uses the current connection's nickname and the server name to populate the messages, and awaits each dispatch to preserve the canonical handshake order.
## Remarks
It centralizes the initial handshake, ensuring a consistent greeting sequence for every new user. By consuming IrcNumericReply codes and composing messages with the live nickname, server name, and current time, it guarantees the client receives both identification and capability information before proceeding. The method delegates the final output of the MOTD to SendMotdAsync, keeping the handshake concerns isolated from the MOTD generation.
## Notes
- Relies on _conn and Nickname being non-null; the null-forgiving operator means a null nickname could yield a greeting with an empty nickname.
- RPL_CREATED uses DateTimeOffset.UtcNow; this stamps the handshake time rather than the server creation date, which may be intentional for the MOTD moment but can be misleading if interpreted as server age.
---
### TryCompleteRegistrationAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task TryCompleteRegistrationAsync()
```
**Returns:** `Task`
Finalizes the user's registration by completing the authentication handshake and establishing an active session. It guards against concurrent registration work by returning early if capability negotiation is still in progress or the user is already registered. If SASL-based authentication has already succeeded (IsAuthenticated and UserId is not null), it marks the connection as registered, notifies the chat service of the connected user, and sends the welcome burst to complete onboarding.
If SASL authentication is not yet complete, it enforces a password-based login: a missing password results in an IRC error and authentication failure. When a password is supplied, it delegates to the user service to authenticate; if that fails, it attempts to register a new user with the provided nickname and password. On a successful outcome, it stores the resulting UserId and Username on the connection, marks the connection as authenticated and registered, signals the chat service that the user has connected, and sends the welcome burst.
The method is a private helper used during the IRC session setup to ensure the connection transitions to a fully authenticated and registered state before normal chat activity begins.
---
## HandleWhoisAsync
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
> **Kind:** method
```csharp
private async Task HandleWhoisAsync(IrcMessage msg)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `msg` | [`IrcMessage`](IrcMessage.cs.md) | — |
**Returns:** `Task`
Handles an IRC WHOIS command by querying the target nick's user profile and returning the standard WHOIS information to the requester. It validates the connection is registered, extracts the nick from the message, fetches the user profile via _userService, and then dispatches a sequence of numeric replies: WHOIS user, WHOISSERVER, and optionally WHOISCHANNELS, RPL_AWAY if the user is away, and RPL_WHOISIDLE with idle and sign-on times, finishing with RPL_ENDOFWHOIS. If no profile exists for the nick, it replies with ERR_NOSUCHNICK. The method relies on asynchronous services and formats times using the profile's LastSeenAt and CreatedAt to populate idle and sign-on data.
## Remarks
This method centralizes the WHOIS response logic for a given nickname, encapsulating the sequence of IRC numeric replies required to convey user information. It coordinates multiple collaborators (the connection, user service, and chat service) to assemble a consistent, standards-compliant response stream without leaking implementation details to callers. The precondition that the connection must be registered is enforced up front, ensuring WHOIS handling only occurs in an appropriate session context.
## Notes
- If the target profile cannot be found, the handler emits ERR_NOSUCHNICK and aborts further replies.
- Idle time is calculated from LastSeenAt and sign-on time from CreatedAt; both are emitted via RPL_WHOISIDLE when available.
- The RPL_WHOISCHANNELS reply is sent only when the user belongs to one or more channels; otherwise this section is omitted.
- Away status (RPL_AWAY) is emitted only if the profile.Status is Away and a StatusMessage exists.
---
@@ -0,0 +1,48 @@
# IrcGatewayService
> **File:** `src/EchoHub.Server.Irc/IrcGatewayService.cs`
> **Kind:** class
*Figure: How IrcGatewayService 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")) --> checkOpt{"Check IrcOptions.Enabled?"}
checkOpt -- "false" --> logDisabled["Log #quot;IRC gateway is disabled#quot;"]
logDisabled --> end1(("End"))
checkOpt -- "true" --> init["Initialize listeners list and add RunListenerAsync(Options.Port, useTls:false)"]
init --> checkTls{"Are Options.TlsEnabled and Options.TlsCertPath set?"}
checkTls -- "true" --> addTls["Add RunListenerAsync(Options.TlsPort, useTls:true)"]
checkTls -- "false" --> awaitAll
addTls --> awaitAll["Await Task.WhenAll(listeners)"]
awaitAll --> runListener["RunListenerAsync: start TcpListener and loop AcceptTcpClientAsync"]
runListener --> acceptClient["On accept: fire-and-forget HandleClientAsync(tcpClient, useTls)"]
acceptClient --> runListener
acceptClient --> createConn["Create new IrcClientConnection and add to _connections"]
createConn --> handleClient{"HandleClientAsync: useTls?"}
handleClient -- "true" --> tlsHandshake["Load cert from IrcOptions and AuthenticateAsServerAsync"]
tlsHandshake --> handshakeOk{"TLS handshake succeeded?"}
handshakeOk -- "false" --> closeTcp["Log error and close tcpClient"]
closeTcp --> endConn(("End connection setup"))
handshakeOk -- "true" --> proceedConn["Assign SslStream and continue"]
handleClient -- "false" --> proceedConn
proceedConn --> addConn["Add connection to _connections dictionary (IrcClientConnection)"]
addConn --> startProcessing["Start message processing with IrcCommandHandler and required services (IChatService, IUserService, IChannelService, IMessageEncryptionService)"]
startProcessing --> endConn
```
```csharp
public sealed class IrcGatewayService : BackgroundService
```
Provides a hosted IRC gateway that listens for incoming TCP (and optional TLS) client connections and dispatches each to an IrcCommandHandler that bridges IRC protocol traffic to the application's chat, user and channel services. Start this BackgroundService when you want the application to accept IRC client connections without manually managing TcpListeners, TLS handshakes, or per-connection handler wiring.
## Remarks
This BackgroundService reads configuration from IrcOptions and opens one or two listeners (plain and optionally TLS) for the ports configured. For every accepted TcpClient it creates an IrcClientConnection, stores it in an internal ConcurrentDictionary keyed by ConnectionId, and constructs an IrcCommandHandler (using IChatService, IUserService, IChannelService and IMessageEncryptionService from DI) to drive the connection. The service centralizes lifecycle concerns: listener startup/shutdown, TLS handshake and per-connection dispatching so higher-level application code can focus on chat/user/channel logic implemented in the injected services.
## Notes
- If IrcOptions.Enabled is false the service logs and returns immediately; no listeners are started.
- TLS is only attempted when TlsEnabled is true and TlsCertPath is provided; TLS handshake failures are logged and the client connection is closed.
- The Connections collection is a ConcurrentDictionary and entries are added when clients connect. Public helper methods (GetAllConnections, GetConnectionsInChannel) filter by IrcClientConnection.IsAuthenticated — use those to obtain the set of active, authenticated clients rather than inspecting the raw dictionary directly.
@@ -0,0 +1,17 @@
# IrcMessage
> **File:** `src/EchoHub.Server.Irc/IrcMessage.cs`
> **Kind:** class
```csharp
public sealed class IrcMessage
```
IrcMessage is a parsed representation of an IRC protocol line that exposes the optional Prefix, the Command, and the Parameters that form the line's arguments; if a trailing payload is present, Trailing provides access to it. Use IrcMessage.Parse to convert a raw line into a structured object and inspect the command and its arguments without manual parsing.
## Remarks
IrcMessage encapsulates the parsing result and keeps IRC-logic separate from application code. Its properties are immutable (init-only), which makes parsed messages safe to share across components after parsing. Trailing is a derived convenience that reflects the trailing payload via the Parameters collection, aligning with the IRC grammar without introducing extra mutable state.
## Notes
- Trailing property returns the last parameter when any parameters exist; it's a convenience for the trailing payload and assumes a leading ':' in the raw line to populate it. If there was no trailing parameter in the line, Trailing will reflect the final parameter but may not be semantically a trailing payload.
@@ -0,0 +1,21 @@
# IrcMessageFormatter
> **File:** `src/EchoHub.Server.Irc/IrcMessageFormatter.cs`
> **Kind:** class
```csharp
public static class IrcMessageFormatter
```
IrcMessageFormatter is a small, focused helper that converts a MessageDto into IRC PRIVMSG lines suitable for delivery in an IRC channel. It handles plain text and CTCP ACTION content, prefixes replies with the standard '> nick: snippet | ' format, and renders attachments as separate URL lines with concise type tags, using an absolute URL when a public base URL is supplied.
## Remarks
It centralizes the IRC-specific formatting and line-breaking logic used by the server when presenting messages to IRC clients, shielding callers from the quirks of the IRC protocol (such as per-line length limits and CTCP wrapping). The private FormatReplyPrefix creates a consistent context string for replies, including redaction of room ciphertext when needed and truncating long snippets to a safe length. Attachments are surfaced as individual lines with a small tag ([Image: ...], [Audio: ...], or [File: ...]) followed by an absolute URL, aligning with common IRC client behavior and improving link reliability. Embeds are appended using FormatEmbed, enabling rich previews where supported.
## Notes
- The FormatMessage path enforces line-length constraints via MaxIrcLineContentBytes, causing long content to be split across multiple PRIVMSG lines as needed.
- Encrypted-reply content is masked by the ciphertext-detection logic (e.g., [encrypted]) to avoid leaking room ciphertext in IRC.
- Absolute URL generation relies on ToAbsoluteUrl and the optional publicBaseUrl; without a base URL, attachments may render with their original (potentially relative) URLs.
@@ -0,0 +1,24 @@
# IrcNumericReply
> **File:** `src/EchoHub.Server.Irc/IrcNumericReply.cs`
> **Kind:** class
```csharp
public static class IrcNumericReply
```
IrcNumericReply is a static container of string constants that encode the standard IRC protocol numeric replies. It centralizes the protocols numeric codes so developers can reference them by name (e.g., RPL_WELCOME, ERR_UNKNOWNCOMMAND) instead of sprinkling literal strings throughout the codebase. The constants are organized by functional areas such as registration, MOTD, channel operations, list operations, WHO/WHOIS, away status, mode, errors, and SASL.
## Remarks
Having all codes in one static class provides a single source of truth and makes it straightforward to update or extend the set as the IRC spec evolves. It also clarifies intent at call sites: emitting an IRC reply uses the corresponding constant rather than a magic string, and parsing branches can compare against these constants with confidence. This abstraction keeps server and client code aligned on canonical codes without duplicating literals.
## Example
```csharp
// Example: emit a welcome reply using the canonical code
string code = IrcNumericReply.RPL_WELCOME; // "001"
string reply = $":server {code} Welcome to the IRC network";
```
## Notes
- The constants are strings, not integers; avoid parsing them as numbers if you need to preserve leading zeros (e.g., "001").
@@ -0,0 +1,19 @@
# IrcOptions
> **File:** `src/EchoHub.Server.Irc/IrcOptions.cs`
> **Kind:** class
```csharp
public sealed class IrcOptions
```
IrcOptions is a simple configuration container that aggregates the settings controlling EchoHub's IRC bridge. It exposes toggles and values for enabling IRC, selecting ports for non-TLS and TLS connections, TLS certificate details, the server identity, an optional MOTD, and how attachment URLs are resolved via a public base URL. An application binds this object from configuration to influence how the IRC integration is started and how clients connect securely.
## Remarks
This class acts as a plain data container that centralizes IRC-related settings, separating configuration concerns from connection logic. The SectionName constant indicates the configuration section used when binding settings, while PublicBaseUrl affects how attachment URLs are translated for IRC clients—absolute URLs when set, otherwise relative paths. It is designed to be a simple DTO bound from configuration rather than responsible for validation or side effects.
## Notes
- If TLS is enabled but a certificate path or password is missing or invalid, TLS connections may fail; ensure a valid certificate and credentials are supplied when TlsEnabled is true.
- PublicBaseUrl, when set, makes attachment URLs absolute for IRC clients; if left unset, attachment lines fall back to the relative path.
- The defaults describe typical behavior: Port = 6667, TlsPort = 6697, and ServerName = "echohub".
@@ -0,0 +1,19 @@
# IrcServiceExtensions
> **File:** `src/EchoHub.Server.Irc/IrcServiceExtensions.cs`
> **Kind:** class
```csharp
public static class IrcServiceExtensions
```
Extends WebApplicationBuilder with AddIrcGateway to wire up IRC gateway support. It reads a configuration flag to enable or disable the gateway and wires the necessary services when enabled, returning the builder for fluent startup configuration.
## Remarks
Centralizes startup concerns for the IRC gateway: the extension reads IrcOptions from a configured section and conditionally registers the gateway components, enabling the feature via configuration. It keeps startup code concise and tests-focused by encapsulating the wiring behind a single extension method.
## Notes
- IrcGatewayService and IrcBroadcaster registrations are conditional on Irc:Enabled; if false, IRC components are not registered.
- Ensure IrcOptions.SectionName matches your configuration so there is a valid section to bind from.
- Returning the builder enables fluent chaining like builder.AddIrcGateway().<other extensions>()
@@ -0,0 +1,18 @@
# JwtTokenService
> **File:** `src/EchoHub.Server/Auth/JwtTokenService.cs`
> **Kind:** class
```csharp
public class JwtTokenService
```
JwtTokenService centralizes the creation of JSON Web Tokens used for authenticating API requests. It reads the signing secret, issuer, and audience from configuration and exposes two overloads of GenerateAccessToken for User and UserProfileDto, returning the token string along with its expiration timestamp. Each generated token includes standard claims such as sub (the user/profile id), username, display_name (falling back to username if not provided), role, and a unique jti, and is signed with HmacSha256 using the configured secret. Access tokens expire after 15 minutes, while a companion refresh token can be generated with GenerateRefreshToken and hashed with HashToken for secure storage.
## Remarks
JwtTokenService centralizes token creation, ensuring consistent signing, claims, and expiry semantics across authentication flows. By loading Jwt:Secret, Jwt:Issuer, and Jwt:Audience from configuration in one place, it reduces the risk of mismatched values and scattered configuration access. The two overloads for GenerateAccessToken allow tokens to be produced from either a User or a UserProfileDto while preserving a uniform JWT shape and claims set, including a unique jti for traceability.
## Notes
- If Jwt:Secret, Jwt:Issuer, or Jwt:Audience is missing from configuration, the constructor throws an InvalidOperationException with a clear message, preventing startup with a misconfigured token engine.
- GenerateRefreshToken produces a cryptographically random 64-byte value and returns it as a base64 string; HashToken provides a SHA-256-based digest suitable for secure, persisted storage. The class itself does not persist refresh tokens, so you should implement storage and revocation logic in your authentication flow if needed.
@@ -0,0 +1,11 @@
# ServerLogsOptions
> **File:** `src/EchoHub.Server/Config/ServerLogsOptions.cs`
> **Kind:** class
```csharp
public sealed class ServerLogsOptions
```
Live server-log room configuration is encapsulated by this strongly-typed options class. It binds to the ServerLogs config section and supports environment overrides, controlling whether the live streaming channel is created and who can view it. When Enabled is true, a read-only system channel is auto-created and log events are streamed in real time; log lines themselves are not stored as messages in the database, with persistence remaining in the rolling Serilog log files.
@@ -0,0 +1,19 @@
# SpamOptions
> **File:** `src/EchoHub.Server/Config/SpamOptions.cs`
> **Kind:** class
```csharp
public sealed class SpamOptions
```
SpamOptions is a configuration object that encapsulates the anti-spam thresholds used by the server. It is bound from the Spam config section and exposes the toggles and numeric limits that govern how the system enforces per-user rate limits, duplicate message handling, auto-muting behavior, and the protections around first-time channel joins and channel creation. The defaults are intentionally lenient so a fast typist wont trip them, and moderators (and above) are exempt from these protections. Use this class to adjust spam-protection policy without changing code.
## Remarks
SpamOptions centralizes policy decisions for anti-spam enforcement, serving as a single source of truth for the thresholds consumed by the spam protection subsystem. By binding to configuration, it keeps rules out of hard-coded logic and enables runtime tuning via the Spam section. The design separates concerns across rate limiting (per-user messages), duplicate detection, auto-mute behavior, and early channel-join/channel-create protections, making it easier to tune each facet without collateral impact. The auto-mute behavior ties into the existing moderation tooling (MuteExpirationService), illustrating cohesive behavior with the broader user-suspension lifecycle. The note about end-to-end encrypted rooms clarifies that identical plaintext can yield different ciphertext, so the duplicate-detection rule may not apply in those contexts.
## Notes
- Auto-mute is controlled by AutoMuteMinutes. Setting AutoMuteMinutes to 0 disables auto-mute (rejections still apply if thresholds are reached).
- MaxMessagesPerWindow and WindowSeconds govern per-user message rate; adjust them with awareness of your typical user pacing to avoid false positives.
- MaxJoinsPerWindow and JoinWindowSeconds apply to first-time channel joins; joins to channels the user already belongs to do not count toward the limit, ensuring normal reconnects dont trigger protections.
@@ -0,0 +1,19 @@
# StatsOptions
> **File:** `src/EchoHub.Server/Config/StatsOptions.cs`
> **Kind:** class
```csharp
public sealed class StatsOptions
```
StatsOptions is a bound configuration object that governs the periodic server-stats reporter. When Enabled is true, a background job periodically snapshots server activity, logs the snapshot as pretty-printed JSON, and persists it to the database; IntervalHours controls cadence, and RetentionDays controls how long reports are kept. The environment override Stats__Enabled allows turning the reporter on or off via environment configuration without changing code.
## Remarks
StatsOptions serves as a simple, sealed data contract that the configuration system binds to at startup, providing a single source of truth for the reporter settings. Centralizing these knobs here avoids scattering config keys throughout the code and makes it easy to swap configuration providers or add validation in one place. The defaults (Enabled = true, IntervalHours = 6, RetentionDays = 90) define the out-of-the-box behavior and can be overridden by environment or configuration.
## Notes
- RetentionDays: 0 means keep reports indefinitely; any positive number prunes older entries.
- IntervalHours is a double; fractional values (e.g., 1.5) are allowed, but scheduling resolution depends on the hosting environment.
- Enabled acts as the master switch for the background job; disabling it stops snapshots until re-enabled.
@@ -0,0 +1,31 @@
# UploadLimits
> **File:** `src/EchoHub.Server/Config/UploadLimits.cs`
> **Kind:** class
```csharp
public sealed class UploadLimits
```
UploadLimits is a configuration-bound value object that centralizes the admin-defined upload size caps. It reads sizes in megabytes from the Uploads configuration and exposes corresponding byte-sized properties used during enforcement. When the Uploads section is missing or incomplete, the defaults mirror HubConstants to preserve the historical built-in limits.
## Remarks
UploadLimits centralizes the policy governing uploads (files, images, audio, avatars) and the maximum number of attachments per message. The MB-based properties feed their byte-sized counterparts (MaxFileSizeBytes, MaxImageSizeBytes, etc.) for enforcement. MaxForKind provides a per-kind ceiling, while MaxRequestBodyBytes computes the overall request-body cap (largest file size multiplied by the attachment limit) to ensure configuration changes actually take effect at the HTTP boundary.
## Example
```csharp
var limits = new UploadLimits
{
MaxFileSizeMB = 64,
MaxAttachmentsPerMessage = 4
};
long maxImageBytes = limits.MaxImageSizeBytes;
long imageCeiling = limits.MaxForKind(AttachmentKind.Image);
long requestBody = limits.MaxRequestBodyBytes;
```
## Notes
- Changing MaxAttachmentsPerMessage scales the MaxRequestBodyBytes non-linearly; the request-body cap will constrain multipart uploads even if per-file size increases.
- Defaults are tied to HubConstants; if those constants change, the default limits change too unless overridden in the Uploads configuration.
@@ -0,0 +1,21 @@
# AuthController
> **File:** `src/EchoHub.Server/Controllers/AuthController.cs`
> **Kind:** class
```csharp
[ApiController]
[Route("api/auth")]
[EnableRateLimiting("auth")]
public class AuthController : ControllerBase
```
AuthController is the API surface that coordinates user authentication. It exposes endpoints for registering, logging in, refreshing tokens, and logging out under /api/auth, and ties together user management, JWT token generation, and refresh-token persistence.
## Remarks
AuthController centralizes authentication concerns to enable consistent security policies such as token lifetimes and rotation. It orchestrates between user management (IUserService), token generation (JwtTokenService), and persistence of refresh tokens (EchoHubDbContext), including rotation semantics to revoke old tokens on each refresh.
## Notes
- Refresh token rotation: on a successful refresh, the old token is revoked (RevokedAt is set) and a new token pair is issued. Clients should replace the old token with the new one and avoid reusing the former.
- Security handles: access tokens have shorter lifetimes, refresh tokens are hashed in storage, and all token exchanges occur over HTTPS. Treat tokens as highly sensitive data and store them securely on the client side.
@@ -0,0 +1,453 @@
# ChannelsController.cs
> **Source:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
## Contents
- [ChannelsController](#channelscontroller)
- [ChannelsController (constructor)](#channelscontroller-constructor)
- [CreateChannel](#createchannel)
- [DeleteChannel](#deletechannel)
- [GetChannelCrypto](#getchannelcrypto)
- [MapChannelError](#mapchannelerror)
- [ParseKind](#parsekind)
- [GetChannelMeta](#getchannelmeta)
- [GetChannels](#getchannels)
- [RekeyChannel](#rekeychannel)
- [SendMessageWithAttachments](#sendmessagewithattachments)
- [SendUrl](#sendurl)
- [UpdateTopic](#updatetopic)
---
## ChannelsController
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** class
```csharp
[ApiController]
[Route("api/channels")]
[Authorize]
[EnableRateLimiting("general")]
public class ChannelsController : ControllerBase
```
Exposes the HTTP surface for channel-related operations under the route prefix api/channels. Authenticated clients use this controller to list and create channels, retrieve public crypto metadata and human-facing channel summaries, perform passphrase rewraps (rekey), update topics, delete channels, and post messages (including multipart uploads). Prefer calling these endpoints from client code or tests; use the underlying services (IChannelService, IMessageEncryptionService, etc.) directly only when you need to bypass HTTP semantics or perform server-side orchestration.
## Remarks
This controller is a thin HTTP façade that orchestrates several backend services rather than implementing business logic itself. It enforces [Authorize] and configurable rate-limiting (attributes show general and upload policy groups) and delegates persistence, file storage, ASCII preview generation, encryption operations, and chat routing to injected dependencies such as IChannelService, EchoHubDbContext, FileStorageService, ImageToAsciiService, IMessageEncryptionService, IChatService and UploadLimits. Upload size and multipart limits are applied at runtime using the UploadLimits configuration rather than compile-time attributes so the controller can honor configurable limits for large attachments.
## Notes
- GetChannelCrypto returns public crypto metadata and the PBKDF2 salt clients need to derive a join credential; it never returns the wrapped room key (that is issued only after a successful join).
- RekeyChannel re-wraps the room key to change the passphrase; historical messages are not re-encrypted (the room content key itself does not change).
- SendMessageWithAttachments applies upload limits at runtime from UploadLimits; the controller trusts clients for encrypted-channel attachments (clients must declare each file's kind and provide room-encrypted previews), while for non-encrypted channels the server may inspect files and generate ASCII previews for images.
---
### ChannelsController (constructor)
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** constructor
```csharp
public ChannelsController(
IChannelService channelService,
EchoHubDbContext db,
FileStorageService fileStorage,
ImageToAsciiService asciiService,
IHttpClientFactory httpClientFactory,
IChatService chatService,
IMessageEncryptionService encryption,
UploadLimits uploadLimits,
ILogger<ChannelsController> logger)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `channelService` | [`IChannelService`](../../EchoHub.Core/Contracts/IChannelService.cs.md) | — |
| `db` | [`EchoHubDbContext`](../Data/EchoHubDbContext.cs.md) | — |
| `fileStorage` | [`FileStorageService`](../Services/FileStorageService.cs.md) | — |
| `asciiService` | [`ImageToAsciiService`](../../EchoHub.Core/Services/ImageToAsciiService.cs.md) | — |
| `httpClientFactory` | `IHttpClientFactory` | — |
| `chatService` | [`IChatService`](../../EchoHub.Core/Contracts/IChatService.cs.md) | — |
| `encryption` | [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) | — |
| `uploadLimits` | [`UploadLimits`](../Config/UploadLimits.cs.md) | — |
| `logger` | `ILogger<ChannelsController>` | — |
The ChannelsController constructor wires up the controller by receiving its dependencies through dependency injection and assigning them to private fields. This pattern allows the controller to orchestrate channel-related functionality by delegating to dedicated services such as IChannelService, EchoHubDbContext, FileStorageService, ImageToAsciiService, IHttpClientFactory, IChatService, IMessageEncryptionService, UploadLimits, and `ILogger<ChannelsController>`. The framework supplies these collaborators at creation time, enabling a testable, loosely coupled design where concerns are separated and easily mockable for unit tests. This constructor is invoked by the ASP.NET Core runtime during request handling, not by consumer code directly.
## Remarks
The constructor centralizes the wiring of the controller's collaborators, which supports clean separation of concerns and testability. It enables the ChannelsController to delegate specialized tasks (e.g., data access, file handling, image processing, HTTP calls, chat interactions, and encryption) to dedicated services rather than embedding logic directly.
The lack of explicit null validation means misconfigured dependency injection (missing service registrations) could surface as NullReferenceExceptions later when members are used. Relying on the DI container to validate registrations is common, but tests should provide explicit mocks to ensure predictable behavior.
## Notes
- The constructor does not perform null checks; ensure all dependencies are registered in the DI container to avoid runtime null reference issues.
- When writing unit tests for ChannelsController, provide concrete or mock implementations for all injected services to exercise behavior reliably.
---
### CreateChannel
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpPost]
public async Task<IActionResult> CreateChannel([FromBody] CreateChannelRequest request)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `request` | [`CreateChannelRequest`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) | — |
**Returns:** `[HttpPost]
public async `Task<IActionResult>``
Source Code
The CreateChannel action handles the HTTP POST to create a new channel for the authenticated user. It first verifies authentication by pulling the user ID from the current users claims; if the claim is missing, it responds with Unauthorized and an ErrorResponse indicating that authentication is required. It then delegates the actual creation to the channel service via CreateChannelAsync, passing the callers GUID along with the channel properties supplied in the request (Name, Topic, IsPublic, Password, EncryptionSalt, WrappedRoomKey). If the service reports a failure, the action returns a mapped error via MapChannelError. If a channel is successfully created and it is public, it broadcasts the updated channel through the chat service to notify connected clients. Finally, it returns a 201 Created response with the location of the new channel and the Channel data in the response body.
Dependencies
- IActionResult
- ErrorResponse
- User
- ClaimTypes
- Guid
- Channel
Dependency APIs (verified signatures)
- record [`ErrorResponse`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) (`src/EchoHub.Core/DTOs/CommonDtos.cs`)
- property [`User`](../../EchoHub.Core/Models/User.cs.md) (`src/EchoHub.Core/Models/RefreshToken.cs`)
- class [`Channel`](../../EchoHub.Core/Models/Channel.cs.md) (`src/EchoHub.Core/Models/Channel.cs`)
- `Guid Id`
- `string Name`
- `string? Topic`
- `bool IsPublic`
- `bool IsSystem`
- `string? PasswordHash`
- `string? EncryptionSalt`
- `string? WrappedRoomKey`
- `DateTimeOffset CreatedAt`
- `Guid CreatedByUserId`
- `List<Message> Messages`
Symbol To Document
- Name: CreateChannel
- Kind: method
- File: src/EchoHub.Server/Controllers/ChannelsController.cs
- Language: csharp
- ID: 373f63c8-2a87-460c-9821-46640d93a9fc
## Remarks
Creates a channel on behalf of the authenticated user and encapsulates the orchestration between the domain service and the HTTP response surface. It relies on _channelService to enforce business rules and persistence, and on _chatService to refresh client views when appropriate. This action adheres to RESTful semantics by returning 401 for unauthenticated requests, propagating domain errors via MapChannelError, broadcasting updates for public channels, and signaling successful creation with 201 and the new channel resource.
## Notes
- Be aware that Guid.Parse is used on the user ID claim. If the claim value is not a valid GUID, this will throw. Consider validating with Guid.TryParse at the call site if you anticipate non-GUID claim values.
- The publication check (IsPublic) gates whether a channel update is broadcast to clients; non-public channels skip broadcasting to peers.
---
### DeleteChannel
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpDelete("{channel}")]
public async Task<IActionResult> DeleteChannel(string channel)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `"{channel}"` | — | — |
Deletes a channel for the authenticated user by handling an HTTP DELETE request to the channel route. It reads the user's ID from the authentication claims, delegates the deletion to the channel service using that ID and the channel name, and, on success, broadcasts the deletion to the chat service before returning HTTP 204 No Content. If authentication is missing, the method responds with 401 Unauthorized and an ErrorResponse.
## Remarks
This endpoint acts as a thin HTTP boundary that orchestrates authentication, domain deletion, and cross-service notification. It centralizes HTTP-level error handling (Unauthorized, error mapping) while delegating business rules to the channel service and the side-effect of notifying the chat service. The normalization of the channel name for the broadcast (lowercase and trimmed) helps ensure consumers react to a consistent channel identifier.
## Notes
- Authentication is required; requests without a valid NameIdentifier claim result in 401 Unauthorized with an ErrorResponse.
- The broadcast step uses channel.ToLowerInvariant().Trim(); differences between input casing and broadcast casing could affect downstream consumers.
- If the channel is deleted successfully but the broadcast fails, the method will surface a failure (no explicit retry here); consider compensating actions if eventual consistency is important.
---
### GetChannelCrypto
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpGet("{channel}/crypto")]
public async Task<IActionResult> GetChannelCrypto(string channel)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `"{channel}/crypto"` | — | — |
GetChannelCrypto is an HTTP GET action on ChannelsController that exposes essential cryptographic metadata for a channel. It indicates whether the channel is end-to-end encrypted and provides the PBKDF2 salt clients need to derive their join credential. The endpoint deliberately does not return the wrapped room key; that secret is only handed out after a successful join. The action delegates retrieval to the channel service and translates the result into standard HTTP responses: 200 OK with the crypto data when the channel exists, or 404 Not Found with an ErrorResponse if the channel does not exist.
## Remarks
By wrapping the service call behind a minimal HTTP surface, this symbol centralizes how cryptographic metadata is surfaced while keeping the actual cryptographic material protected. It demonstrates a clear separation of concerns: business logic lives in the ChannelService, while the controller handles HTTP semantics and error translation. The exposed salt enables client-side credential derivation, while the wrapped key remains strictly withheld until the proper join flow.
## Notes
- The action does not perform explicit authorization; ensure the surrounding middleware or route configuration enforces the intended access policy.
- It returns 404 with a generic ErrorResponse when the channel does not exist; clients should handle this scenario as an absence of channel crypto metadata.
- Do not rely on this endpoint to retrieve any sensitive material beyond allowed cryptographic metadata; the wrapped key must never be exposed through this action.
---
### MapChannelError
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
private IActionResult MapChannelError(ChannelOperationResult result) => result.Error switch
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `result` | [`ChannelOperationResult`](../../EchoHub.Core/DTOs/CommonDtos.cs.md) | — |
**Returns:** `IActionResult`
Converts a ChannelOperationResult into an API response by pattern-matching on result.Error and returning an appropriate HTTP result that wraps an ErrorResponse. It centralizes the translation from channel-domain errors to standard HTTP status codes (400, 403, 404, 409) so the rest of the controller does not duplicate error handling logic.
## Remarks
This encapsulates the error-handling policy for channel operations, ensuring clients see consistent HTTP semantics across all channel actions. It decouples domain error codes from HTTP choices, so updates to status codes or payload shapes can be made in one place rather than at every call site.
## Notes
- The branches pass result.ErrorMessage! into ErrorResponse; if ErrorMessage can be null for any mapped error, this will throw at runtime.
- New ChannelError values require extending this switch to preserve the API's error contract.
---
### ParseKind
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
private static AttachmentKind ParseKind(string? kind) => kind?.ToLowerInvariant() switch
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `kind` | `string?` | — |
**Returns:** [`AttachmentKind`](../../EchoHub.Core/Models/AttachmentKind.cs.md)
This private helper translates a nullable string that labels an attachment into a concrete AttachmentKind enum. It uses a case-insensitive comparison (ToLowerInvariant) to recognize 'image' and 'audio' and map them to AttachmentKind.Image and AttachmentKind.Audio, respectively; any other label (including null) falls back to AttachmentKind.File. Callers rely on this mapping when normalizing incoming attachment metadata before further processing in the channel/server pipeline.
## Remarks
Centralizes the normalization logic so all attachment-kind labels are interpreted consistently across the server. By funneling strings through this method, the rest of the attachment processing can operate on a well-defined enum, reducing branching and potential mismatches.
## Notes
- Unknown labels are treated as File by design; if a new kind is introduced, update this method or extend the enum.
- Because the method is private, it's exercised via the class's public APIs; ensure tests cover scenarios that exercise this mapping through those entry points.
---
## GetChannelMeta
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpGet("{channel}/meta")]
public async Task<IActionResult> GetChannelMeta(string channel)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `"{channel}/meta"` | — | — |
Retrieves the channel metadata for a given channel identifier via HTTP GET. It returns key overview details such as message count, the number of unique posters, an estimated size, the creation date, and the room id. These metadata are tracked by the server and are available even for encrypted channels, where the server cannot access the actual messages. If the channel does not exist, it responds with 404 and an ErrorResponse; otherwise it returns the metadata payload with a 200 OK.
## Remarks
This endpoint provides a read-only surface for obtaining channel overview information without exposing message contents. It enables clients to populate channel lists or dashboards while preserving message privacy, including for encrypted channels. By delegating the data retrieval to _channelService.GetChannelMetaAsync, the API keeps data access concerns centralized and allows the underlying storage/collection strategy to evolve without changing the surface contract.
## Notes
- The caller must handle a 404 NotFound with an ErrorResponse when the channel is missing. The error payload documents the failure reason.
- The endpoint exposes only metadata about a channel; actual messages remain inaccessible, preserving privacy for encrypted channels.
- The operation is asynchronous; consider service performance characteristics or potential caching strategies if metadata is requested frequently.
---
## GetChannels
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpGet]
public async Task<IActionResult> GetChannels([FromQuery] int offset = 0, [FromQuery] int limit = 50)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `offset` | `int` | `0` |
| `limit` | `int` | `50` |
**Returns:** `[HttpGet]
public async `Task<IActionResult>``
Gets a paged list of channels for the authenticated user. It enforces authentication by checking the user claims, reads the user's GUID from the claims, normalizes paging parameters (offset non-negative; limit clamped to 1100), and delegates to the channel service to retrieve the channels, returning the result in an HTTP 200 response.
## Remarks
This action is intentionally thin: it performs authentication, input normalization, and orchestration between the API layer and the domain service. Centralizing paging bounds and user identification here provides consistent behavior and error handling for per-user channel retrieval across clients.
## Notes
- Be aware that if the NameIdentifier claim is present but is not a valid GUID, Guid.Parse will throw. Prefer Guid.TryParse or ensure identity claims are well-formed.
- The limit is clamped to the range 1100; requests outside that range are adjusted to the nearest bound.
---
## RekeyChannel
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpPost("{channel}/rekey")]
public async Task<IActionResult> RekeyChannel(string channel, [FromBody] RekeyChannelRequest request)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `"{channel}/rekey"` | — | — |
Changes an encrypted channel's passphrase by re-wrapping its room key. The caller proves knowledge of the existing passphrase via the old authentication key, and this operation preserves history by not changing the room content key.
## Remarks
RekeyChannel acts as a thin HTTP boundary that enforces authentication and delegates the cryptographic work to the channel service. By re-wrapping the existing room key instead of re-encrypting the historical content, it minimizes disruption while changing access controls. The controller handles authentication and error translation, while RekeyChannelAsync encapsulates the cryptographic policy in the domain layer.
## Notes
- Authentication is mandatory; if the user is not authenticated, the endpoint returns 401 Unauthorized with ErrorResponse("Authentication required.").
- The code uses Guid.Parse on the NameIdentifier claim; if the claim is present but not a valid GUID, a runtime exception may be thrown.
---
## SendMessageWithAttachments
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpPost("{channel}/messages")]
[EnableRateLimiting("upload")]
public async Task<IActionResult> SendMessageWithAttachments(string channel, [FromQuery] string? size = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `"{channel}/messages"` | — | — |
Use SendMessageWithAttachments when you need to post a chat message to a channel that includes optional text and one or more attachments, while enforcing per-channel upload limits and channel permissions.
It supports both cleartext and end-to-end encrypted channels: in cleartext channels the server inspects file kinds to render ASCII previews and decrypts the content, while in encrypted channels the client uploads ciphertext with per-file kind and a pre-rendered encrypted preview and the server never inspects the ciphertext.
## Remarks
This endpoint centralizes the server-side orchestration for uploading messages with attachments, coordinating authentication via user claims, channel validation and mutability checks, multipart form handling, per-attachment processing, and interaction with the encryption and upload-limit subsystems. It relies on collaborators such as the channel service, the database context, and the encryption helper to enforce read-only channels, mute state, and maximum message length in a consistent manner. By encapsulating these concerns, it ensures secure, policy-compliant message delivery and prevents plaintext exposure of encrypted payloads. In short, it is the single integration point for sending rich messages with attachments in EchoHub.Server.
## Notes
- The request size is governed at runtime by UploadLimits; configure this to control maximum allowed payloads.
- For encrypted channels, ensure that per-file previews are provided in the ciphertext workflow and that file order remains aligned with the declared previews to avoid misrendering on the client.
---
## SendUrl
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpPost("{channel}/send-url")]
[EnableRateLimiting("upload")]
public async Task<IActionResult> SendUrl(string channel, [FromBody] SendUrlRequest request, [FromQuery] string? size = null)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `"{channel}/send-url"` | — | — |
SendUrl is an HTTP POST endpoint on ChannelsController that accepts a channel name, a request body containing an image URL, and an optional size parameter. It authenticates the caller, validates the channel, enforces channel policies (rejects system/read-only and end-to-end encrypted channels), validates the URL, downloads the image server-side, enforces size limits, validates the image format, saves the file, and generates an ASCII preview for display in the channel.
## Remarks
Centralizes remote image ingestion with strict, server-side validation to prevent improper content, inconsistent client behavior, or abuse. The endpoint relies on the application's security and storage abstractions: it checks user claims, ensures channel permissions, uses FileStorage to persist the file, and uses ImageToAsciiService to produce a lightweight ASCII representation for previews. The EnableRateLimiting("upload") attribute signals this is a potentially resource-intensive operation and should be throttled to guard against abuse.
## Notes
- Requires authentication; missing user claims yield Unauthorized responses with a helpful error.
- Validates channel state: if the channel does not exist, is system (read-only), or is encrypted, it responds with NotFound/403/400 and an ErrorResponse explaining the reason.
- Validates the supplied URL and only accepts http/https URLs; invalid URLs or unsupported schemes produce a BadRequest with a descriptive message.
- Downloads the image server-side using an HttpClient named "ImageDownload". It handles timeouts and HTTP errors by returning BadRequest with a clear message.
- Enforces file size limits via _uploadLimits.MaxImageSizeBytes before and after downloading the content.
- Validates that the downloaded content is a real image (JPEG, PNG, GIF, WebP) before persisting.
- Determines a filename from the URL or Content-Type; if missing, it falls back to a generated name with an appropriate extension.
- Persists the file and creates an ASCII representation (via ImageToAsciiService) for downstream use.
---
## UpdateTopic
> **File:** `src/EchoHub.Server/Controllers/ChannelsController.cs`
> **Kind:** method
```csharp
[HttpPut("{channel}/topic")]
public async Task<IActionResult> UpdateTopic(string channel, [FromBody] UpdateTopicRequest request)
```
**Parameters:**
| Parameter | Type | Default |
|-----------|------|---------|
| `"{channel}/topic"` | — | — |
Updates a channel's topic for the currently authenticated user via HTTP PUT. It verifies authentication, calls ChannelService.UpdateTopicAsync with the user's ID, the channel, and the new topic, and on success broadcasts the channel update before returning the updated channel; on failure or missing authentication, it yields an HTTP error.
## Remarks
Acts as the HTTP API boundary for updating a channel topic, delegating the actual update to the domain service and handling authentication. It centralizes error translation via MapChannelError and ensures clients are informed of changes in real time by broadcasting after a successful update.
## Notes
- Be aware that Guid.Parse could throw if the user claim is not a valid GUID; consider Guid.TryParse to avoid runtime exceptions.
- The broadcast channel is normalized by lowercasing and trimming the channel name; this affects how subscribers perceive channel identifiers in updates.
---
@@ -0,0 +1,23 @@
# FilesController
> **File:** `src/EchoHub.Server/Controllers/FilesController.cs`
> **Kind:** class
```csharp
[ApiController]
[Route("api/files")]
[Authorize]
[EnableRateLimiting("general")]
public class FilesController : ControllerBase
```
Serves an uploaded file anonymously by design: the unguessable GUID in the URL is the access token (Discord-CDN-style capability URL), so attachment links can be opened directly in a browser and shared to IRC clients. E2E-encrypted room blobs are ciphertext at rest, so anonymous access reveals nothing for those channels.
## Remarks
This symbol provides a minimal, token-based file access surface that does not require user authentication. It delegates path resolution to FileStorageService and consolidates content-type handling in one place, so callers can rely on consistent delivery behavior across file types. The design emphasizes shareable, browser-friendly links while safeguarding sensitive payloads behind the GUID-based URL.
## Notes
- The endpoint validates the fileId as a GUID before attempting any storage access; invalid IDs yield a BadRequest response.
- Content types are determined by file extension with a broad fallback to application/octet-stream; unknown extensions will download as a generic binary.
- Images and audio files are rendered inline in the browser, while other types are delivered as attachments with the original file name.

Some files were not shown because too many files have changed in this diff Show More