mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 16:46:08 +02:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dbcc517a4 | ||
|
|
91ea76ffc7 | ||
|
|
2d5f8eeded | ||
|
|
65766eab1f | ||
|
|
675a39cb15 | ||
|
|
cda890de60 | ||
|
|
7eced63608 | ||
|
|
07996a78ed | ||
|
|
329dfe422b | ||
|
|
b1aeb8cbb2 | ||
|
|
7964355e08 | ||
|
|
1e877cf886 | ||
|
|
0105f798e0 | ||
|
|
607217b314 | ||
|
|
40aea9a04b | ||
|
|
f8f4e03ddd | ||
|
|
4dcb480d1d | ||
|
|
45dc0f9d0f | ||
|
|
fbeb546ac5 | ||
|
|
e3654c637c | ||
|
|
2297b58c74 | ||
|
|
6e57247a45 | ||
|
|
fd61d9cb9c | ||
|
|
79e5a1191f | ||
|
|
7525f8b1d8 | ||
|
|
0c2e8eae87 | ||
|
|
75ff10c5fd | ||
|
|
46afd2a195 | ||
|
|
38eca99fb1 |
@@ -75,3 +75,19 @@ Irc__Enabled=false
|
|||||||
|
|
||||||
# ── Logging ──────────────────────────────────────────────────────────
|
# ── Logging ──────────────────────────────────────────────────────────
|
||||||
# Serilog__MinimumLevel__Default=Information
|
# Serilog__MinimumLevel__Default=Information
|
||||||
|
|
||||||
|
# ── Server logs room ─────────────────────────────────────────────────
|
||||||
|
# Read-only system channel that live-streams Serilog events to Mod+ users.
|
||||||
|
# ServerLogs__Enabled=true
|
||||||
|
# ServerLogs__RoomName=server-logs
|
||||||
|
# ServerLogs__MinRole=Mod
|
||||||
|
# ServerLogs__MinLevel=Information
|
||||||
|
# ServerLogs__BacklogLines=100
|
||||||
|
# ServerLogs__LogDirectory=logs
|
||||||
|
# ServerLogs__LogFilePattern=echohub-server-*.log
|
||||||
|
|
||||||
|
# ── Periodic stats report ────────────────────────────────────────────
|
||||||
|
# Aggregate activity snapshot logged as JSON and persisted to the DB.
|
||||||
|
# Stats__Enabled=true
|
||||||
|
# Stats__IntervalHours=6
|
||||||
|
# Stats__RetentionDays=90
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
"MD013": false,
|
"MD013": false,
|
||||||
// Allow duplicate headings in different sections
|
// Allow duplicate headings in different sections
|
||||||
"MD024": { "siblings_only": true },
|
"MD024": { "siblings_only": true },
|
||||||
|
// Allow files to open with a centered HTML header block instead of an H1 (README)
|
||||||
|
"MD041": false,
|
||||||
// Allow inline HTML (docfx uses it)
|
// Allow inline HTML (docfx uses it)
|
||||||
"MD033": false,
|
"MD033": false,
|
||||||
// Allow bare URLs
|
// Allow bare URLs
|
||||||
@@ -20,6 +22,7 @@
|
|||||||
".dev/**",
|
".dev/**",
|
||||||
"docs/_site/**",
|
"docs/_site/**",
|
||||||
"docs/api/**",
|
"docs/api/**",
|
||||||
|
"docs/auriondocs/**",
|
||||||
"node_modules/**",
|
"node_modules/**",
|
||||||
"**/bin/**",
|
"**/bin/**",
|
||||||
"**/obj/**",
|
"**/obj/**",
|
||||||
|
|||||||
@@ -1,41 +1,23 @@
|
|||||||
# EchoHub
|
<div align="center">
|
||||||
|
|
||||||
<p align="center">
|
<img src="https://cdn.voidcube.cloud/assets/hue_icon.svg" alt="EchoHub" width="112" />
|
||||||
<img src="https://cdn.voidcube.cloud/assets/hue_icon.svg" alt="EchoHub Logo" width="120" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">EchoHub</h1>
|
<h1>EchoHub</h1>
|
||||||
|
|
||||||
<p align="center">
|
**No tracking. No subscriptions. No "enhanced AI features". Just chat.**
|
||||||
No tracking. No subscriptions. No "enhanced AI features". Just chat.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
Self-hosted, IRC-inspired chat with an open API. Use the terminal, a desktop app, IRC, or a client you build yourself.
|
||||||
<a href="#what-is-this">What</a> •
|
|
||||||
<a href="#getting-started">Setup</a> •
|
|
||||||
<a href="#irc-gateway">IRC</a> •
|
|
||||||
<a href="#deployment-with-nginx">Deploy</a> •
|
|
||||||
<a href="#client-commands">Commands</a> •
|
|
||||||
<a href="#configuration">Config</a> •
|
|
||||||
<a href="#license">License</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
[Website](https://echohub.voidcube.cloud/) · [Public servers](https://echohub.voidcube.cloud/servers) · [Documentation](https://huebyte.github.io/EchoHub/) · [Changelog](docs/changelog/index.md)
|
||||||
<a href="https://echohub.voidcube.cloud/">Website</a> •
|
|
||||||
<a href="https://echohub.voidcube.cloud/servers">Public Servers</a> •
|
|
||||||
<a href="https://huebyte.github.io/EchoHub/">Documentation</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/HueByte/EchoHub/actions/workflows/ci.yml"><img alt="Build" src="https://img.shields.io/github/actions/workflow/status/HueByte/EchoHub/ci.yml?branch=master&style=flat-square&logo=github&label=Build" /></a>
|
<a href="https://github.com/HueByte/EchoHub/actions/workflows/ci.yml"><img alt="Build" src="https://img.shields.io/github/actions/workflow/status/HueByte/EchoHub/ci.yml?branch=master&style=flat-square&logo=github&label=Build" /></a>
|
||||||
<a href="https://github.com/HueByte/EchoHub/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/HueByte/EchoHub?style=flat-square&logo=github&label=Release" /></a>
|
<a href="https://github.com/HueByte/EchoHub/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/HueByte/EchoHub?style=flat-square&logo=github&label=Release" /></a>
|
||||||
<a href="https://community.chocolatey.org/packages/echohub"><img alt="Chocolatey" src="https://img.shields.io/chocolatey/v/echohub?style=flat-square&logo=chocolatey&label=Chocolatey" /></a>
|
<a href="https://community.chocolatey.org/packages/echohub"><img alt="Chocolatey" src="https://img.shields.io/chocolatey/v/echohub?style=flat-square&logo=chocolatey&label=Chocolatey" /></a>
|
||||||
<a href="https://github.com/HueByte/EchoHub/pkgs/container/echohub-server"><img alt="Docker" src="https://img.shields.io/badge/Docker-GHCR-2496ED?style=flat-square&logo=docker&logoColor=white" /></a>
|
<a href="https://github.com/HueByte/EchoHub/pkgs/container/echohub-server"><img alt="Docker" src="https://img.shields.io/badge/Docker-GHCR-2496ED?style=flat-square&logo=docker&logoColor=white" /></a>
|
||||||
<img alt=".NET 10" src="https://img.shields.io/badge/.NET-10-512BD4?style=flat-square&logo=dotnet&logoColor=white" />
|
<img alt=".NET 10" src="https://img.shields.io/badge/.NET-10-512BD4?style=flat-square&logo=dotnet&logoColor=white" />
|
||||||
<img alt="Terminal.Gui" src="https://img.shields.io/badge/TUI-Terminal.Gui%20v2-yellow?style=flat-square" />
|
|
||||||
<a href="LICENSE"><img alt="License" src="https://img.shields.io/github/license/HueByte/EchoHub?style=flat-square" /></a>
|
<a href="LICENSE"><img alt="License" src="https://img.shields.io/github/license/HueByte/EchoHub?style=flat-square" /></a>
|
||||||
<img alt="Repo size" src="https://img.shields.io/github/repo-size/HueByte/EchoHub?style=flat-square&label=Size" />
|
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -43,355 +25,153 @@
|
|||||||
|
|
||||||
Chat apps used to be simple. You connected to a server, joined a channel, and talked to people. No one was mining your messages for ad targeting, no one was selling your "engagement metrics", and the app didn't need 2GB of RAM to display text.
|
Chat apps used to be simple. You connected to a server, joined a channel, and talked to people. No one was mining your messages for ad targeting, no one was selling your "engagement metrics", and the app didn't need 2GB of RAM to display text.
|
||||||
|
|
||||||
EchoHub is a return to that. Self-hosted, IRC-inspired chat that runs in your terminal. You own the server, you own the data, and the client won't try to upsell you on a premium tier.
|
EchoHub is a return to that. Self-hosted, IRC-inspired chat where you own the server and the data, and the client won't try to upsell you on a premium tier.
|
||||||
|
|
||||||
Each server is fully independent — no central authority, no account federation, no corporate overlord. Just spin one up and go.
|
Each server is fully independent. No central authority, no account federation, no corporate overlord. Just spin one up and go.
|
||||||
|
|
||||||
|
The terminal client in this repo is the main way in: a proper native app that runs right in your terminal, no browser or Electron in sight. But you're not locked to it. The server speaks an open API (REST and SignalR) plus native IRC, so you can reach it however you like. There's already [**decho**](https://github.com/Stone-Red-Code/Decho), any IRC client works fine, and if none of those suit you, writing your own is fair game.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Screenshots / demo GIF go here once ready. Drop them in docs/images/ and reference like:
|
||||||
|
<p align="center"><img src="docs/images/demo.gif" alt="EchoHub in action" width="820" /></p>
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
One server process speaks two protocols: SignalR for native clients and raw IRC for everything else. Both land on the same `ChatService`, so a message from irssi and one from the terminal client look identical by the time they hit the database.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD
|
graph LR
|
||||||
subgraph Server["Server"]
|
subgraph Clients
|
||||||
ChatSvc["ChatService"]
|
direction TB
|
||||||
Hub["SignalR ChatHub"]
|
TUI["Terminal client<br/>Terminal.Gui v2"]
|
||||||
IRC["IRC Gateway :6667"]
|
Desk["decho · your app<br/>REST + SignalR"]
|
||||||
|
IRCC["IRC clients<br/>irssi · WeeChat · …"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Server["EchoHub Server · ASP.NET Core"]
|
||||||
|
direction TB
|
||||||
|
Hub["SignalR Hub"]
|
||||||
|
GW["IRC Gateway<br/>:6667"]
|
||||||
|
Chat["ChatService"]
|
||||||
Auth["JWT Auth"]
|
Auth["JWT Auth"]
|
||||||
DB["SQLite DB (EF Core)"]
|
DB[("SQLite · EF Core")]
|
||||||
Files["File Storage"]
|
Files[("File store")]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Clients["Clients"]
|
TUI == "WebSocket" ==> Hub
|
||||||
TUI["Terminal GUI (TUI)"]
|
TUI -. "REST" .-> Auth
|
||||||
IRCClient["IRC Client (irssi, WeeChat, ...)"]
|
Desk == "WebSocket" ==> Hub
|
||||||
end
|
Desk -. "REST" .-> Auth
|
||||||
|
IRCC == "TCP" ==> GW
|
||||||
|
Hub --> Chat
|
||||||
|
GW --> Chat
|
||||||
|
Auth --> Chat
|
||||||
|
Chat --> DB
|
||||||
|
Chat --> Files
|
||||||
|
|
||||||
TUI -- "WebSocket" --> Hub
|
classDef core fill:#512BD4,stroke:#c3b5ff,color:#ffffff,stroke-width:1.5px;
|
||||||
TUI -- "REST" --> Auth
|
classDef io fill:#1f6feb,stroke:#9dc1ff,color:#ffffff;
|
||||||
IRCClient -- "TCP" --> IRC
|
classDef data fill:#0f7b8a,stroke:#7fd3de,color:#ffffff;
|
||||||
Hub --> ChatSvc
|
classDef client fill:#22272e,stroke:#768390,color:#e6edf3;
|
||||||
IRC --> ChatSvc
|
|
||||||
ChatSvc --> DB
|
class Chat core
|
||||||
Auth --> DB
|
class Hub,GW,Auth io
|
||||||
Files --> DB
|
class DB,Files data
|
||||||
|
class TUI,Desk,IRCC client
|
||||||
```
|
```
|
||||||
|
|
||||||
## What you get
|
## Highlights
|
||||||
|
|
||||||
### Server
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| 🔒 **Actually private** | Self-hosted, no telemetry. Password rooms are end-to-end encrypted with a key derived from the passphrase on your own machine, so not even the server owner can read them. |
|
||||||
|
| 🖥️ **Native client, open API** | A real terminal client ships in the box. The REST + SignalR API is open too, so you're never stuck with it: there's the [**decho**](https://github.com/Stone-Red-Code/Decho) desktop app, any IRC client, or roll your own. |
|
||||||
|
| 💬 **IRC still works** | A built-in gateway drops irssi and WeeChat users into the same rooms as everyone else, live. Actions, replies and presence all carry across. |
|
||||||
|
| 🛡️ **Moderation, built in** | Four roles with ban, kick, mute and invite-only signup. Spam and flooding earn an automatic timed mute, no plugins to install. |
|
||||||
|
| 📎 **Files, images, audio** | Uploads are checked by their real bytes, not the file extension. Images even render as ASCII in the terminal, because why not. |
|
||||||
|
| 📤 **Take your data and go** | One command exports everything the server knows about you. Another deletes the account for good. |
|
||||||
|
|
||||||
- **Self-hostable** — your server, your rules, your data
|
Full feature tour in the [documentation](https://huebyte.github.io/EchoHub/articles/getting-started.html).
|
||||||
- **Docker ready** — `docker compose up -d` and you're done
|
|
||||||
- **Real-time messaging** via SignalR WebSockets
|
|
||||||
- **IRC gateway** — native IRC clients connect alongside TUI users, full cross-protocol messaging
|
|
||||||
- **JWT auth** with short-lived access tokens and 30-day refresh tokens
|
|
||||||
- **Channels** — create, set topics, delete (no 47-step permission wizard required)
|
|
||||||
- **Moderation** — ban, mute (timed or permanent), kick, role assignment
|
|
||||||
- **Registration control** — open, invite-code-gated (`/invite`), or closed; codes live in your own database
|
|
||||||
- **File & image uploads** with actual validation (magic bytes, not just trusting the extension)
|
|
||||||
- **Image-to-ASCII** — because images in a terminal is objectively cool
|
|
||||||
- **Presence tracking** — online/away/DND/invisible with custom status messages
|
|
||||||
- **Rate limiting** — in case someone gets too excited
|
|
||||||
- **Spam protection** — per-user flood/duplicate limits with auto-mute escalation, covering TUI and IRC alike; configurable under `Spam`, Mods exempt
|
|
||||||
- **Auto-restart** on crash with exponential backoff — it picks itself back up
|
|
||||||
- **Serilog logging** — console + rolling file, because `Console.WriteLine` isn't a logging strategy
|
|
||||||
- **Zero config first run** — generates its own JWT secret and config on launch
|
|
||||||
|
|
||||||
### Client
|
## Clients
|
||||||
|
|
||||||
- **Runs in your terminal** — no browser, no Electron, no 500MB of bundled Chromium
|
The terminal client is the main, native interface. Beyond that the API and the IRC gateway are open, so you can bring whatever you like.
|
||||||
- **14 built-in themes** — including `hacker` for when you want to feel like you're in a movie
|
|
||||||
- **Slash commands** — `/join`, `/send`, `/status`, `/theme`, etc.
|
|
||||||
- **Colored nicknames** — pick your hex color, express yourself
|
|
||||||
- **Clickable everything** — usernames, @mentions, #channels — just press Enter
|
|
||||||
- **File/image sharing** — local files or URLs; drag & drop a file onto the terminal to send it; save the original behind any ASCII-art image
|
|
||||||
- **End-to-end encrypted rooms** — password-protected channels are encrypted with a passphrase-derived key that never reaches the server, so not even the server owner can read messages or files (they can still see counts and storage size)
|
|
||||||
- **Replies** — quote a message, jump back to the original; being replied to pings like a mention
|
|
||||||
- **Your data is yours** — `/export` everything the server holds about you; `/deleteaccount` removes it
|
|
||||||
- **Multi-server** — save and switch between servers
|
|
||||||
- **Auto-reconnect** — drops happen, it rejoins your channels automatically
|
|
||||||
- **Auto-updater** — updates in-place with automatic rollback if something goes wrong
|
|
||||||
- **Message history** on join — you won't miss context
|
|
||||||
|
|
||||||
## Getting Started
|
| Client | Platform | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **[Terminal client](src/EchoHub.Client)** | Windows · macOS · Linux | The main, native client (Terminal.Gui v2), shipped as a self-contained binary |
|
||||||
|
| **[decho](https://github.com/Stone-Red-Code/Decho)** | Desktop | A community desktop client |
|
||||||
|
| **Any IRC client** | Everywhere | irssi, WeeChat, TheLounge, and friends, through the built-in gateway |
|
||||||
|
| **Your own** | anything | Build against the open REST + SignalR API. See the [documentation](https://huebyte.github.io/EchoHub/) |
|
||||||
|
|
||||||
### Install the Client
|
## Quick start
|
||||||
|
|
||||||
**Windows (Chocolatey):**
|
### Install the client
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Windows (Chocolatey)
|
||||||
choco install echohub
|
choco install echohub
|
||||||
```
|
|
||||||
|
|
||||||
**Linux / macOS:**
|
# Linux / macOS
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh
|
curl -sSfL https://raw.githubusercontent.com/HueByte/EchoHub/master/scripts/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
**Manual download:** grab a self-contained binary from [Releases](../../releases) — no runtime needed, just run it.
|
Or grab a self-contained binary from [Releases](../../releases). No runtime required.
|
||||||
|
|
||||||
### Host a Server
|
### Host a server
|
||||||
|
|
||||||
**Docker (recommended):**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Pre-built images on [GHCR](https://github.com/HueByte/EchoHub/pkgs/container/echohub-server) — `linux/amd64` and `linux/arm64`.
|
Pre-built multi-arch images live on [GHCR](https://github.com/HueByte/EchoHub/pkgs/container/echohub-server). Prefer running from source? `dotnet run --project src/EchoHub.Server` (needs the [.NET 10 SDK](https://dotnet.microsoft.com/download)). The first launch writes its config, generates a JWT secret, and creates the database with a `#general` channel. No manual setup.
|
||||||
|
|
||||||
**From source:**
|
→ **[Getting started](https://huebyte.github.io/EchoHub/articles/getting-started.html)** · **[Docker & deployment](https://huebyte.github.io/EchoHub/articles/docker.html)** · **[Configuration](https://huebyte.github.io/EchoHub/articles/configuration.html)**
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Everything lives at **[huebyte.github.io/EchoHub](https://huebyte.github.io/EchoHub/)**:
|
||||||
|
|
||||||
|
| Guide | What's inside |
|
||||||
|
| --- | --- |
|
||||||
|
| [Getting started](https://huebyte.github.io/EchoHub/articles/getting-started.html) | Install, first connection, the onboarding flow |
|
||||||
|
| [TUI guide](https://huebyte.github.io/EchoHub/articles/tui-guide.html) | Slash commands, themes, keybindings, message actions |
|
||||||
|
| [Configuration](https://huebyte.github.io/EchoHub/articles/configuration.html) | Every `appsettings.json` / env key explained |
|
||||||
|
| [IRC gateway](https://huebyte.github.io/EchoHub/articles/irc-gateway.html) | Connecting IRC clients, what maps to what, TLS |
|
||||||
|
| [Encrypted rooms](https://huebyte.github.io/EchoHub/articles/encrypted-rooms.html) | How zero-knowledge password channels work |
|
||||||
|
| [Moderation](https://huebyte.github.io/EchoHub/articles/moderation.html) | Roles, bans, mutes, invite codes |
|
||||||
|
| [Docker & deployment](https://huebyte.github.io/EchoHub/articles/docker.html) | Compose, reverse proxy, TLS (see also [`examples/nginx.conf`](examples/nginx.conf)) |
|
||||||
|
| [Architecture](https://huebyte.github.io/EchoHub/articles/architecture.html) | How the pieces above actually fit together |
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
dotnet run --project src/EchoHub.Server
|
dotnet build src/EchoHub.slnx # build everything
|
||||||
|
dotnet test src/EchoHub.Tests # run the test suite
|
||||||
```
|
```
|
||||||
|
|
||||||
Requires [.NET 10 SDK](https://dotnet.microsoft.com/download). First run does everything for you:
|
Contributions are welcome. Open an issue to discuss larger changes first.
|
||||||
|
|
||||||
1. Creates `appsettings.json` from the example config
|
## Contributors
|
||||||
2. Generates a secure JWT secret
|
|
||||||
3. Creates the database with a `#general` channel
|
|
||||||
|
|
||||||
### Run the Client (from source)
|
<div align="center">
|
||||||
|
|
||||||
```bash
|
<sub>The people building EchoHub.</sub>
|
||||||
dotnet run --project src/EchoHub.Client
|
|
||||||
```
|
|
||||||
|
|
||||||
Connect, register, chat. That's the whole onboarding flow.
|
<p align="center">
|
||||||
|
<a href="https://github.com/HueByte"><img src="https://wsrv.nl/?url=github.com/HueByte.png&w=220&h=220&fit=cover&r=32" width="116" height="116" alt="HueByte" /></a>
|
||||||
|
|
||||||
|
<a href="https://github.com/Stone-Red-Code"><img src="https://wsrv.nl/?url=github.com/Stone-Red-Code.png&w=220&h=220&fit=cover&r=32" width="116" height="116" alt="Stone_Red" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
### Build from Source
|
<p align="center">
|
||||||
|
<a href="https://github.com/HueByte"><b>HueByte</b></a>
|
||||||
|
·
|
||||||
|
<a href="https://github.com/Stone-Red-Code"><b>Stone_Red</b></a>
|
||||||
|
<br />
|
||||||
|
<sub>Creators & maintainers</sub>
|
||||||
|
</p>
|
||||||
|
|
||||||
```bash
|
</div>
|
||||||
dotnet build src/EchoHub.slnx
|
|
||||||
```
|
|
||||||
|
|
||||||
## IRC Gateway
|
|
||||||
|
|
||||||
EchoHub includes a built-in IRC protocol gateway. Any standard IRC client can connect to the same server and chat alongside TUI users — messages flow both ways in real time.
|
|
||||||
|
|
||||||
### Enable It
|
|
||||||
|
|
||||||
In the server's `appsettings.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Irc": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Port": 6667,
|
|
||||||
"ServerName": "echohub",
|
|
||||||
"Motd": "Welcome to EchoHub IRC Gateway!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Connect
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# irssi
|
|
||||||
irssi -c your-server.com -p 6667 -w <password> -n <username>
|
|
||||||
|
|
||||||
# WeeChat
|
|
||||||
/server add echohub your-server.com/6667 -password=<password> -nicks=<username>
|
|
||||||
/connect echohub
|
|
||||||
```
|
|
||||||
|
|
||||||
Auth works via `PASS`/`NICK`/`USER` or SASL PLAIN. New usernames are auto-registered on first connect — no separate signup needed.
|
|
||||||
|
|
||||||
### What Works
|
|
||||||
|
|
||||||
| Feature | How it maps to IRC |
|
|
||||||
| ------- | ------------------ |
|
|
||||||
| Text messages | Standard `PRIVMSG` (long messages split at ~400 byte chunks) |
|
|
||||||
| `/me` actions | Native CTCP ACTION in both directions |
|
|
||||||
| Replies | TUI replies arrive as `> nick: snippet \| text` |
|
|
||||||
| Images | `[Image: filename]` + download URL + ASCII art line-by-line |
|
|
||||||
| File uploads | `[File: filename] /api/files/{id}` |
|
|
||||||
| Channels | `JOIN`, `PART`, `NAMES`, `TOPIC`, `LIST` |
|
|
||||||
| Presence | `AWAY`, `WHO`, `WHOIS` |
|
|
||||||
| Status | Maps to IRC away/here |
|
|
||||||
|
|
||||||
### TLS
|
|
||||||
|
|
||||||
If running behind **nginx** (recommended), let nginx handle TLS -- see [Deployment with nginx](#deployment-with-nginx) below.
|
|
||||||
|
|
||||||
For direct TLS without a reverse proxy, the IRC gateway can terminate TLS itself:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"Irc": {
|
|
||||||
"TlsEnabled": true,
|
|
||||||
"TlsPort": 6697,
|
|
||||||
"TlsCertPath": "/path/to/cert.pfx",
|
|
||||||
"TlsCertPassword": "your-password"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Client Commands
|
|
||||||
|
|
||||||
| Command | Description |
|
|
||||||
| ------- | ----------- |
|
|
||||||
| `/join <channel> [password]` | Join a channel (passphrase for encrypted channels) |
|
|
||||||
| `/me <action>` | Action message — `* nick waves` (native CTCP ACTION over IRC) |
|
|
||||||
| `/banner <text>` | Render short text as a big ASCII banner |
|
|
||||||
| `/passwd <old> <new>` | Change the current encrypted channel's passphrase (creator only) |
|
|
||||||
| `/size [s\|m\|l]` | ASCII-art size for attached images (no arg = picker) |
|
|
||||||
| `/downloadpath [path]` | Set the download folder (no path = native folder picker) |
|
|
||||||
| `/leave` | Leave current channel |
|
|
||||||
| `/topic <text>` | Set channel topic (creator only) |
|
|
||||||
| `/send <file or URL>` | Upload a file or image |
|
|
||||||
| `/status <online\|away\|dnd\|invisible>` | Set your status |
|
|
||||||
| `/status msg <text>` | Set a status message (keeps your status; empty text clears it) |
|
|
||||||
| `/nick <name>` | Set display name |
|
|
||||||
| `/color <#hex>` | Set nickname color |
|
|
||||||
| `/theme <name>` | Switch theme |
|
|
||||||
| `/profile` | Open profile editor |
|
|
||||||
| `/users` | List online users in channel |
|
|
||||||
| `/servers` | Manage saved servers |
|
|
||||||
| `/invite [uses] [hours]` | Create a registration invite code (Admin+); also `list` / `revoke <code>` |
|
|
||||||
| `/export` | Download everything the server stores about you as JSON |
|
|
||||||
| `/deleteaccount` | Permanently delete your account (password re-confirmed) |
|
|
||||||
| `/help` | Show help |
|
|
||||||
| `/quit` | Exit |
|
|
||||||
|
|
||||||
**Message actions:** **right-click a message** for a context menu — reply (quotes the message; Esc cancels a pending reply), delete, save/download/play its attachment, mention the sender, view their profile, or copy the text. (Keyboard alternative: press <kbd>F6</kbd> to focus the message list, select with the arrow keys, and press <kbd>Delete</kbd>; <kbd>F6</kbd> again returns to the input.) You can always delete your own messages; moderators and above can delete others' messages, but only from users below their own role.
|
|
||||||
|
|
||||||
## Themes
|
|
||||||
|
|
||||||
`/theme <name>` to switch:
|
|
||||||
|
|
||||||
| Theme | Vibe |
|
|
||||||
| ----- | ---- |
|
|
||||||
| `default` | Gray on black — clean and quiet |
|
|
||||||
| `transparent` | White on black — for fancy transparent terminals |
|
|
||||||
| `transparentlight` | Black on transparent — dark characters for light transparent terminals |
|
|
||||||
| `classic` | White on blue — IRC nostalgia |
|
|
||||||
| `light` | Black on white — for the brave |
|
|
||||||
| `hacker` | Green on black — *I'm in* |
|
|
||||||
| `solarized` | Cyan/yellow on dark gray — for the refined |
|
|
||||||
| `dracula` | Purple accents on black — the classic dark theme |
|
|
||||||
| `monokai` | Yellow highlights on black — warm and familiar |
|
|
||||||
| `nord` | Cool blues — arctic vibes |
|
|
||||||
| `gruvbox` | Earthy yellows on black — retro warmth |
|
|
||||||
| `ocean` | Cyan on deep blue — underwater aesthetics |
|
|
||||||
| `highcontrast` | Bright yellow on black — maximum readability |
|
|
||||||
| `rosepine` | Muted pinks on black — cozy and soft |
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
`appsettings.json` is auto-generated on first run. Tweak what you need:
|
|
||||||
|
|
||||||
| Key | Default | Description |
|
|
||||||
| --- | ------- | ----------- |
|
|
||||||
| `Urls` | `http://0.0.0.0:5000` | Listen address and port |
|
|
||||||
| `ConnectionStrings:DefaultConnection` | *(empty — app directory)* | SQLite connection string |
|
|
||||||
| `Jwt:Secret` | *(auto-generated)* | JWT signing key |
|
|
||||||
| `Server:Name` | `My EchoHub Server` | Server display name |
|
|
||||||
| `Server:Description` | `A self-hosted EchoHub chat server` | Server description |
|
|
||||||
| `Server:Registration` | `open` | `open`, `invite` (codes via `/invite`, Admin+), or `closed` |
|
|
||||||
| `Server:PublicServer` | `false` | List on the [public directory](https://echohub.voidcube.cloud/servers) |
|
|
||||||
| `Server:PublicHost` | *(empty)* | Public hostname for directory listing |
|
|
||||||
| `Irc:Enabled` | `false` | Enable the IRC gateway |
|
|
||||||
| `Irc:Port` | `6667` | IRC listen port |
|
|
||||||
| `Irc:TlsEnabled` | `false` | Enable TLS for IRC |
|
|
||||||
| `Irc:TlsPort` | `6697` | IRC TLS port |
|
|
||||||
| `Irc:ServerName` | `echohub` | IRC server name in protocol messages |
|
|
||||||
| `Irc:Motd` | `Welcome to EchoHub IRC Gateway!` | Message of the day |
|
|
||||||
| `Cors:AllowedOrigins` | *(all origins)* | CORS whitelist |
|
|
||||||
|
|
||||||
Logging uses Serilog — console + daily rolling files with 14-day retention. Configure in the `Serilog` section.
|
|
||||||
|
|
||||||
## Deployment with nginx
|
|
||||||
|
|
||||||
Most production deployments run behind nginx. Here's a config that handles both the HTTP/WebSocket server and the IRC gateway:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# HTTP + WebSocket (EchoHub Server API + SignalR)
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
server_name echohub.example.com;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/echohub.example.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/echohub.example.com/privkey.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://127.0.0.1:5000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
|
|
||||||
# Required for SignalR WebSocket
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection $http_connection;
|
|
||||||
|
|
||||||
proxy_read_timeout 86400s;
|
|
||||||
proxy_send_timeout 86400s;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Increase max upload size for file sharing
|
|
||||||
client_max_body_size 10m;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTP → HTTPS redirect
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name echohub.example.com;
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# IRC TLS (port 6697 → plain IRC on 6667)
|
|
||||||
stream {
|
|
||||||
upstream irc_backend {
|
|
||||||
server 127.0.0.1:6667;
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 6697 ssl;
|
|
||||||
proxy_pass irc_backend;
|
|
||||||
|
|
||||||
ssl_certificate /etc/letsencrypt/live/echohub.example.com/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/echohub.example.com/privkey.pem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
With this setup, keep the EchoHub IRC gateway's `TlsEnabled` set to `false` — nginx terminates TLS. See the full example at [`examples/nginx.conf`](examples/nginx.conf).
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
├── EchoHub.Core/ # Shared models, DTOs, contracts, validation
|
|
||||||
│ ├── Constants/ # ValidationConstants, HubConstants
|
|
||||||
│ ├── Contracts/ # IChatService, IChatBroadcaster, IEchoHubClient
|
|
||||||
│ ├── DTOs/ # Record DTOs
|
|
||||||
│ └── Models/ # Entity models
|
|
||||||
│
|
|
||||||
├── EchoHub.Server/ # ASP.NET Core server
|
|
||||||
│ ├── Auth/ # JWT token service
|
|
||||||
│ ├── Controllers/ # REST API endpoints
|
|
||||||
│ ├── Data/ # EF Core DbContext + migrations
|
|
||||||
│ ├── Hubs/ # SignalR ChatHub
|
|
||||||
│ ├── Services/ # ChatService, presence, file storage, image processing
|
|
||||||
│ └── Setup/ # First-run setup, DB initialization
|
|
||||||
│
|
|
||||||
├── EchoHub.Server.Irc/ # IRC protocol gateway
|
|
||||||
│ ├── IrcGatewayService # TCP listener (BackgroundService)
|
|
||||||
│ ├── IrcCommandHandler # IRC command dispatch (JOIN, PRIVMSG, etc.)
|
|
||||||
│ ├── IrcBroadcaster # Fans chat events to IRC connections
|
|
||||||
│ └── IrcMessageFormatter # MessageDto → IRC PRIVMSG lines
|
|
||||||
│
|
|
||||||
├── EchoHub.Client/ # Terminal.Gui TUI client
|
|
||||||
│ ├── Config/ # Client configuration
|
|
||||||
│ ├── Services/ # API client, SignalR connection
|
|
||||||
│ ├── Themes/ # 13 built-in themes
|
|
||||||
│ └── UI/ # MainWindow, dialogs, chat renderer
|
|
||||||
│
|
|
||||||
└── EchoHub.slnx # Solution file
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](LICENSE) — do whatever you want with it.
|
|
||||||
|
|||||||
@@ -98,8 +98,11 @@ Access tokens expire after 15 minutes, refresh tokens after 30 days with rotatio
|
|||||||
| `Server:Name` | `My EchoHub Server` | Display name shown to clients |
|
| `Server:Name` | `My EchoHub Server` | Display name shown to clients |
|
||||||
| `Server:Description` | `A self-hosted EchoHub chat server` | Server description |
|
| `Server:Description` | `A self-hosted EchoHub chat server` | Server description |
|
||||||
| `Server:PublicServer` | `false` | Register on the [public directory](https://echohub.voidcube.cloud/servers) |
|
| `Server:PublicServer` | `false` | Register on the [public directory](https://echohub.voidcube.cloud/servers) |
|
||||||
| `Server:PublicHost` | *(empty)* | Public hostname for the directory listing (e.g. `chat.example.com:5000`) |
|
| `Server:PublicHosts` | `[]` | Hostnames advertised to the directory (array, e.g. `["chat.example.com"]`). Required when `PublicServer` is `true` |
|
||||||
|
| `Server:Tags` | `[]` | Topic tags surfaced in the directory browser (array, e.g. `["community", "gaming"]`) |
|
||||||
| `Server:Admins` | `[]` | Array of admin usernames (e.g. `["alice", "bob"]`) |
|
| `Server:Admins` | `[]` | Array of admin usernames (e.g. `["alice", "bob"]`) |
|
||||||
|
| `Server:Registration` | `open` | Registration mode: `open`, `invite` (codes via `/invite`, Admin+), or `closed` |
|
||||||
|
| `Server:DirectoryClaimPath` | *(empty)* | Overrides where the directory claim token file is stored. Empty = `directory-claim.json` next to the SQLite database. Treat it as a secret and back it up with the database |
|
||||||
|
|
||||||
### Uploads
|
### Uploads
|
||||||
|
|
||||||
@@ -118,6 +121,26 @@ absent or partial `Uploads` section keeps the built-in defaults. See
|
|||||||
The server sizes its request-body limits from these values, so raising a limit here is all
|
The server sizes its request-body limits from these values, so raising a limit here is all
|
||||||
that's needed — no separate Kestrel tuning.
|
that's needed — no separate Kestrel tuning.
|
||||||
|
|
||||||
|
### Spam Protection
|
||||||
|
|
||||||
|
Per-user flood, duplicate, join, and channel-create limits with auto-mute escalation. Mods and
|
||||||
|
above are always exempt, and the defaults are lenient enough that a fast typist never trips them.
|
||||||
|
An absent or partial `Spam` section keeps these defaults.
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `Spam:Enabled` | `true` | Master switch for all spam protection |
|
||||||
|
| `Spam:MaxMessagesPerWindow` | `8` | Max messages per `WindowSeconds` before a send is rejected |
|
||||||
|
| `Spam:WindowSeconds` | `5` | Sliding window (seconds) for the message-rate check |
|
||||||
|
| `Spam:MaxDuplicateMessages` | `3` | Identical messages in a row tolerated before rejection (E2E rooms are exempt — ciphertext differs each time) |
|
||||||
|
| `Spam:AutoMuteMinutes` | `5` | Auto-mute duration once a user hits the violation threshold. `0` disables auto-mute (rejections still apply) |
|
||||||
|
| `Spam:ViolationThreshold` | `5` | Rejected sends within `ViolationWindowMinutes` that trigger an auto-mute |
|
||||||
|
| `Spam:ViolationWindowMinutes` | `5` | Window (minutes) over which violations accumulate |
|
||||||
|
| `Spam:MaxJoinsPerWindow` | `25` | Max first-time channel joins per `JoinWindowSeconds` — keep this above your public channel count |
|
||||||
|
| `Spam:JoinWindowSeconds` | `30` | Sliding window (seconds) for the join-rate check |
|
||||||
|
| `Spam:MaxChannelCreatesPerWindow` | `3` | Max channel creations per `ChannelCreateWindowMinutes` |
|
||||||
|
| `Spam:ChannelCreateWindowMinutes` | `10` | Window (minutes) for the channel-create check |
|
||||||
|
|
||||||
### Encryption
|
### Encryption
|
||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
@@ -129,6 +152,7 @@ that's needed — no separate Kestrel tuning.
|
|||||||
|
|
||||||
| Key | Default | Description |
|
| Key | Default | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| `Storage:Path` | *(empty)* | Directory for uploaded file blobs (attachments, avatars). Empty = `uploads/` in the app directory; the Docker image sets it to `/app/data/uploads` on the persistent volume |
|
||||||
| `Storage:CleanupIntervalHours` | `1` | How often the cleanup job runs (hours) |
|
| `Storage:CleanupIntervalHours` | `1` | How often the cleanup job runs (hours) |
|
||||||
| `Storage:RetentionDays` | `30` | Days to keep uploaded files before cleanup |
|
| `Storage:RetentionDays` | `30` | Days to keep uploaded files before cleanup |
|
||||||
|
|
||||||
@@ -145,6 +169,23 @@ that's needed — no separate Kestrel tuning.
|
|||||||
| `Irc:ServerName` | `echohub` | IRC server name in protocol messages |
|
| `Irc:ServerName` | `echohub` | IRC server name in protocol messages |
|
||||||
| `Irc:Motd` | `Welcome to EchoHub IRC Gateway!` | Message of the day |
|
| `Irc:Motd` | `Welcome to EchoHub IRC Gateway!` | Message of the day |
|
||||||
|
|
||||||
|
### Server Logs Room
|
||||||
|
|
||||||
|
When enabled, EchoHub auto-creates a read-only system channel and streams log events to it live,
|
||||||
|
so operators can watch the server from inside the app. Log lines are **never stored as messages** —
|
||||||
|
the rolling Serilog files remain the only persistence, and the room replays recent lines from those
|
||||||
|
files when someone opens it.
|
||||||
|
|
||||||
|
| Key | Default | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `ServerLogs:Enabled` | `true` | Master switch for the live log room |
|
||||||
|
| `ServerLogs:RoomName` | `server-logs` | Name of the auto-created channel (reserved — users can't create a channel with this name) |
|
||||||
|
| `ServerLogs:MinRole` | `Mod` | Minimum server role that can see and join the room (`Member`, `Mod`, `Admin`, `Owner`) |
|
||||||
|
| `ServerLogs:MinLevel` | `Information` | Minimum log level streamed to the room (`Verbose`, `Debug`, `Information`, `Warning`, `Error`, `Fatal`) — affects only the room, not the file/console sinks |
|
||||||
|
| `ServerLogs:BacklogLines` | `100` | Recent log lines replayed from file when someone opens the room |
|
||||||
|
| `ServerLogs:LogDirectory` | `logs` | Directory holding the rolling log files (must match the Serilog file sink path below) |
|
||||||
|
| `ServerLogs:LogFilePattern` | `echohub-server-*.log` | Filename glob for the rolling log files inside `LogDirectory` |
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
||||||
EchoHub uses [Serilog](https://serilog.net/) for structured logging — console output + daily rolling files with 14-day retention by default.
|
EchoHub uses [Serilog](https://serilog.net/) for structured logging — console output + daily rolling files with 14-day retention by default.
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Agent Context Pack — HueByte/EchoHub
|
||||||
|
|
||||||
|
Machine-oriented context for AI coding agents, generated deterministically from
|
||||||
|
the repository's symbol graph (no LLM involved). Reading this pack instead of
|
||||||
|
scanning the source tree saves tokens: one map read plus targeted doc lookups
|
||||||
|
replaces repeated file reads and greps.
|
||||||
|
|
||||||
|
| File | What it is | When to read it |
|
||||||
|
|---|---|---|
|
||||||
|
| [repo-map](repo-map.md) | Subsystem map + most-connected symbols | First — orientation |
|
||||||
|
| [symbol-graph.json](symbol-graph.json) | Full queryable symbol graph (kind, file, subsystem, complexity, dependencies, doc path) | To trace exact dependencies or find a symbol |
|
||||||
|
| `../Code/**` | Per-file generated documentation | For the behaviour of a specific file |
|
||||||
|
| `../Synthesis/Architecture.md` | Cross-cutting architecture narrative + diagram | For the big picture |
|
||||||
|
|
||||||
|
Suggested workflow: read `repo-map.md`; locate the symbols relevant to your task in
|
||||||
|
`symbol-graph.json` (each carries a `doc` path and `dependsOn` edges); open only
|
||||||
|
those docs/sources. The pack is regenerated on every full documentation run, so it
|
||||||
|
matches the commit recorded in the graph's `commit` field.
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
# Repo Map — HueByte/EchoHub
|
||||||
|
|
||||||
|
> Deterministic structural map generated for AI agents. Read this file for
|
||||||
|
> orientation, query [`symbol-graph.json`](symbol-graph.json) for exact
|
||||||
|
> dependencies, and open the linked per-file docs for behaviour — instead of
|
||||||
|
> scanning the source tree.
|
||||||
|
|
||||||
|
Commit `40aea9a04b2b4bd3a2e431cdd1cf4e1bfa11c343` · 201 symbols · 128 files · 655 dependency edges
|
||||||
|
|
||||||
|
## Subsystems
|
||||||
|
|
||||||
|
*Structural clusters detected from the dependency graph — groups of symbols more densely wired to each other than to the rest of the codebase.*
|
||||||
|
|
||||||
|
### src/EchoHub.Client/Services · AppOrchestrator
|
||||||
|
|
||||||
|
37 symbols across 23 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`AppOrchestrator`](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) (class) — `src/EchoHub.Client/AppOrchestrator.cs`
|
||||||
|
- [`ConnectionManager`](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) (class) — `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||||
|
- [`MessageDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`EchoHubConnection`](../Code/src/EchoHub.Client/Services/EchoHubConnection.cs.md) (class) — `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
- [`RefreshToken`](../Code/src/EchoHub.Core/Models/RefreshToken.cs.md) (class) — `src/EchoHub.Core/Models/RefreshToken.cs`
|
||||||
|
- [`SavedServer`](../Code/src/EchoHub.Client/Config/ClientConfig.cs.md) (class) — `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
- [`RoomKeyStore`](../Code/src/EchoHub.Client/Services/RoomKeyStore.cs.md) (class) — `src/EchoHub.Client/Services/RoomKeyStore.cs`
|
||||||
|
- [`IrcMessageFormatter`](../Code/src/EchoHub.Server.Irc/IrcMessageFormatter.cs.md) (class) — `src/EchoHub.Server.Irc/IrcMessageFormatter.cs`
|
||||||
|
- *…and 29 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI · MainWindow
|
||||||
|
|
||||||
|
22 symbols across 19 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`MainWindow`](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) (class) — `src/EchoHub.Client/UI/MainWindow.cs`
|
||||||
|
- [`ChatMessageManager`](../Code/src/EchoHub.Client/UI/Chat/ChatMessageManager.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatMessageManager.cs`
|
||||||
|
- [`ChatLine`](../Code/src/EchoHub.Client/UI/Chat/ChatLine.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatLine.cs`
|
||||||
|
- [`ProfileViewDialog`](../Code/src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
|
||||||
|
- [`AttachmentKind`](../Code/src/EchoHub.Core/Models/AttachmentKind.cs.md) (enum) — `src/EchoHub.Core/Models/AttachmentKind.cs`
|
||||||
|
- [`AttachmentDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`ChatColors`](../Code/src/EchoHub.Client/UI/Chat/ChatColors.cs.md) (class) — `src/EchoHub.Client/UI/Chat/ChatColors.cs`
|
||||||
|
- [`ChatSegment`](../Code/src/EchoHub.Client/UI/Chat/ChatSegment.cs.md) (record) — `src/EchoHub.Client/UI/Chat/ChatSegment.cs`
|
||||||
|
- *…and 14 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Core/DTOs · ApiClient
|
||||||
|
|
||||||
|
19 symbols across 11 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ApiClient`](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) (class) — `src/EchoHub.Client/Services/ApiClient.cs`
|
||||||
|
- [`ModerationController`](../Code/src/EchoHub.Server/Controllers/ModerationController.cs.md) (class) — `src/EchoHub.Server/Controllers/ModerationController.cs`
|
||||||
|
- [`ServerStatsCollector`](../Code/src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs.md) (class) — `src/EchoHub.Server/Services/Stats/ServerStatsCollector.cs`
|
||||||
|
- [`RefreshRequest`](../Code/src/EchoHub.Core/DTOs/AuthDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/AuthDtos.cs`
|
||||||
|
- [`AssignRoleRequest`](../Code/src/EchoHub.Core/DTOs/ModerationDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ModerationDtos.cs`
|
||||||
|
- [`UpdateProfileRequest`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
- [`AvatarHelper`](../Code/src/EchoHub.Client/Services/AvatarHelper.cs.md) (class) — `src/EchoHub.Client/Services/AvatarHelper.cs`
|
||||||
|
- [`OutgoingAttachment`](../Code/src/EchoHub.Client/Services/OutgoingAttachment.cs.md) (record) — `src/EchoHub.Client/Services/OutgoingAttachment.cs`
|
||||||
|
- *…and 11 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Server · User
|
||||||
|
|
||||||
|
19 symbols across 17 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`User`](../Code/src/EchoHub.Core/Models/User.cs.md) (class) — `src/EchoHub.Core/Models/User.cs`
|
||||||
|
- [`UsersController`](../Code/src/EchoHub.Server/Controllers/UsersController.cs.md) (class) — `src/EchoHub.Server/Controllers/UsersController.cs`
|
||||||
|
- [`EchoHubDbContext`](../Code/src/EchoHub.Server/Data/EchoHubDbContext.cs.md) (class) — `src/EchoHub.Server/Data/EchoHubDbContext.cs`
|
||||||
|
- [`ServerRole`](../Code/src/EchoHub.Core/Models/ServerRole.cs.md) (enum) — `src/EchoHub.Core/Models/ServerRole.cs`
|
||||||
|
- [`AuthController`](../Code/src/EchoHub.Server/Controllers/AuthController.cs.md) (class) — `src/EchoHub.Server/Controllers/AuthController.cs`
|
||||||
|
- [`UserProfileDto`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
- [`UserService`](../Code/src/EchoHub.Server/Services/UserService.cs.md) (class) — `src/EchoHub.Server/Services/UserService.cs`
|
||||||
|
- [`IUserService`](../Code/src/EchoHub.Core/Contracts/IUserService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IUserService.cs`
|
||||||
|
- *…and 11 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Core/DTOs · ChannelService
|
||||||
|
|
||||||
|
13 symbols across 9 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ChannelService`](../Code/src/EchoHub.Server/Services/ChannelService.cs.md) (class) — `src/EchoHub.Server/Services/ChannelService.cs`
|
||||||
|
- [`IrcCommandHandler`](../Code/src/EchoHub.Server.Irc/IrcCommandHandler.cs.md) (class) — `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
- [`IChannelService`](../Code/src/EchoHub.Core/Contracts/IChannelService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChannelService.cs`
|
||||||
|
- [`ChannelOperationResult`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
- [`FakeChannelService`](../Code/src/EchoHub.Tests/Irc/TestHelpers.cs.md) (class) — `src/EchoHub.Tests/Irc/TestHelpers.cs`
|
||||||
|
- [`ValidationConstants`](../Code/src/EchoHub.Core/Constants/ValidationConstants.cs.md) (class) — `src/EchoHub.Core/Constants/ValidationConstants.cs`
|
||||||
|
- [`ChannelCryptoDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`ChannelError`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (enum) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
- *…and 5 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Server · Program
|
||||||
|
|
||||||
|
13 symbols across 12 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`Program`](../Code/src/EchoHub.Server/Program.cs.md) (file) — `src/EchoHub.Server/Program.cs`
|
||||||
|
- [`HubConstants`](../Code/src/EchoHub.Core/Constants/HubConstants.cs.md) (class) — `src/EchoHub.Core/Constants/HubConstants.cs`
|
||||||
|
- [`PresenceTracker`](../Code/src/EchoHub.Server/Services/PresenceTracker.cs.md) (class) — `src/EchoHub.Server/Services/PresenceTracker.cs`
|
||||||
|
- [`ImageToAsciiService`](../Code/src/EchoHub.Core/Services/ImageToAsciiService.cs.md) (class) — `src/EchoHub.Core/Services/ImageToAsciiService.cs`
|
||||||
|
- [`UploadLimits`](../Code/src/EchoHub.Server/Config/UploadLimits.cs.md) (class) — `src/EchoHub.Server/Config/UploadLimits.cs`
|
||||||
|
- [`DatabaseSetup`](../Code/src/EchoHub.Server/Setup/DatabaseSetup.cs.md) (class) — `src/EchoHub.Server/Setup/DatabaseSetup.cs`
|
||||||
|
- [`DirectoryClaimStore`](../Code/src/EchoHub.Server/Services/DirectoryClaimStore.cs.md) (class) — `src/EchoHub.Server/Services/DirectoryClaimStore.cs`
|
||||||
|
- [`LinkEmbedService`](../Code/src/EchoHub.Server/Services/LinkEmbedService.cs.md) (class) — `src/EchoHub.Server/Services/LinkEmbedService.cs`
|
||||||
|
- *…and 5 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Contracts · ChannelDto
|
||||||
|
|
||||||
|
9 symbols across 9 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ChannelDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`UserPresenceDto`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
- [`ChatHub`](../Code/src/EchoHub.Server/Hubs/ChatHub.cs.md) (class) — `src/EchoHub.Server/Hubs/ChatHub.cs`
|
||||||
|
- [`IChatService`](../Code/src/EchoHub.Core/Contracts/IChatService.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChatService.cs`
|
||||||
|
- [`IChatBroadcaster`](../Code/src/EchoHub.Core/Contracts/IChatBroadcaster.cs.md) (interface) — `src/EchoHub.Core/Contracts/IChatBroadcaster.cs`
|
||||||
|
- [`IrcBroadcaster`](../Code/src/EchoHub.Server.Irc/IrcBroadcaster.cs.md) (class) — `src/EchoHub.Server.Irc/IrcBroadcaster.cs`
|
||||||
|
- [`SignalRBroadcaster`](../Code/src/EchoHub.Server/Services/SignalRBroadcaster.cs.md) (class) — `src/EchoHub.Server/Services/SignalRBroadcaster.cs`
|
||||||
|
- [`IEchoHubClient`](../Code/src/EchoHub.Core/Contracts/IEchoHubClient.cs.md) (interface) — `src/EchoHub.Core/Contracts/IEchoHubClient.cs`
|
||||||
|
- *…and 1 more (see symbol-graph.json)*
|
||||||
|
|
||||||
|
### src/EchoHub.Server · ServerDirectoryService
|
||||||
|
|
||||||
|
7 symbols across 1 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ServerDirectoryService`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`ErrorDetail`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`Response`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`DirectoryProtocol`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`DirectoryRegistrationErrors`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (class) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`RegisterServerDto`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
- [`RegisterServerResult`](../Code/src/EchoHub.Server/Services/ServerDirectoryService.cs.md) (record) — `src/EchoHub.Server/Services/ServerDirectoryService.cs`
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI · Channel
|
||||||
|
|
||||||
|
6 symbols across 4 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`Channel`](../Code/src/EchoHub.Core/Models/Channel.cs.md) (class) — `src/EchoHub.Core/Models/Channel.cs`
|
||||||
|
- [`SearchDialog`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
- [`SearchResultType`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (enum) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
- [`SearchListSource`](../Code/src/EchoHub.Client/UI/ListSources/SearchListSource.cs.md) (class) — `src/EchoHub.Client/UI/ListSources/SearchListSource.cs`
|
||||||
|
- [`SearchResult`](../Code/src/EchoHub.Client/UI/Dialogs/SearchDialog.cs.md) (record) — `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
- [`IrcNumericReply`](../Code/src/EchoHub.Server.Irc/IrcNumericReply.cs.md) (class) — `src/EchoHub.Server.Irc/IrcNumericReply.cs`
|
||||||
|
|
||||||
|
### src/EchoHub.Client/UI · UserStatus
|
||||||
|
|
||||||
|
6 symbols across 5 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`UserStatus`](../Code/src/EchoHub.Core/Models/UserStatus.cs.md) (enum) — `src/EchoHub.Core/Models/UserStatus.cs`
|
||||||
|
- [`StatusDialog`](../Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md) (class) — `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
|
||||||
|
- [`UserSession`](../Code/src/EchoHub.Client/Services/UserSession.cs.md) (class) — `src/EchoHub.Client/Services/UserSession.cs`
|
||||||
|
- [`StatusDialogResult`](../Code/src/EchoHub.Client/UI/Dialogs/StatusDialog.cs.md) (record) — `src/EchoHub.Client/UI/Dialogs/StatusDialog.cs`
|
||||||
|
- [`UserDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`UpdateStatusRequest`](../Code/src/EchoHub.Core/DTOs/ProfileDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ProfileDtos.cs`
|
||||||
|
|
||||||
|
### src/EchoHub.Core/DTOs · ChannelsController
|
||||||
|
|
||||||
|
6 symbols across 3 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`ChannelsController`](../Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md) (class) — `src/EchoHub.Server/Controllers/ChannelsController.cs`
|
||||||
|
- [`RekeyChannelRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`FileValidationHelper`](../Code/src/EchoHub.Core/Services/FileValidationHelper.cs.md) (class) — `src/EchoHub.Core/Services/FileValidationHelper.cs`
|
||||||
|
- [`CreateChannelRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`SendUrlRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
- [`UpdateTopicRequest`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
|
||||||
|
### src/EchoHub.Core/Models
|
||||||
|
|
||||||
|
6 symbols across 6 files. Key symbols (by connectivity):
|
||||||
|
|
||||||
|
- [`Message`](../Code/src/EchoHub.Core/Models/Message.cs.md) (class) — `src/EchoHub.Core/Models/Message.cs`
|
||||||
|
- [`DataMigrationService`](../Code/src/EchoHub.Server/Setup/DataMigrationService.cs.md) (class) — `src/EchoHub.Server/Setup/DataMigrationService.cs`
|
||||||
|
- [`Attachment`](../Code/src/EchoHub.Core/Models/Attachment.cs.md) (class) — `src/EchoHub.Core/Models/Attachment.cs`
|
||||||
|
- [`AsyncRunner`](../Code/src/EchoHub.Client/Services/AsyncRunner.cs.md) (class) — `src/EchoHub.Client/Services/AsyncRunner.cs`
|
||||||
|
- [`MessageType`](../Code/src/EchoHub.Core/Models/MessageType.cs.md) (enum) — `src/EchoHub.Core/Models/MessageType.cs`
|
||||||
|
- [`ApiResponse`](../Code/src/EchoHub.Core/DTOs/CommonDtos.cs.md) (record) — `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
|
||||||
|
*…and 11 smaller subsystems (see symbol-graph.json).*
|
||||||
|
|
||||||
|
## Most connected symbols
|
||||||
|
|
||||||
|
The load-bearing symbols — changes here have the widest blast radius.
|
||||||
|
|
||||||
|
| Symbol | Kind | Used by | Uses | File |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| [`AppOrchestrator`](../Code/src/EchoHub.Client/AppOrchestrator.cs.md) | class | 3 | 54 | `src/EchoHub.Client/AppOrchestrator.cs` |
|
||||||
|
| [`Program`](../Code/src/EchoHub.Server/Program.cs.md) | file | 0 | 34 | `src/EchoHub.Server/Program.cs` |
|
||||||
|
| [`ApiClient`](../Code/src/EchoHub.Client/Services/ApiClient.cs.md) | class | 3 | 29 | `src/EchoHub.Client/Services/ApiClient.cs` |
|
||||||
|
| [`ChatService`](../Code/src/EchoHub.Server/Services/ChatService.cs.md) | class | 2 | 29 | `src/EchoHub.Server/Services/ChatService.cs` |
|
||||||
|
| [`Channel`](../Code/src/EchoHub.Core/Models/Channel.cs.md) | class | 29 | 1 | `src/EchoHub.Core/Models/Channel.cs` |
|
||||||
|
| [`MainWindow`](../Code/src/EchoHub.Client/UI/MainWindow.cs.md) | class | 2 | 25 | `src/EchoHub.Client/UI/MainWindow.cs` |
|
||||||
|
| [`Message`](../Code/src/EchoHub.Core/Models/Message.cs.md) | class | 21 | 4 | `src/EchoHub.Core/Models/Message.cs` |
|
||||||
|
| [`ChannelsController`](../Code/src/EchoHub.Server/Controllers/ChannelsController.cs.md) | class | 0 | 25 | `src/EchoHub.Server/Controllers/ChannelsController.cs` |
|
||||||
|
| [`ConnectionManager`](../Code/src/EchoHub.Client/Services/ConnectionManager.cs.md) | class | 1 | 22 | `src/EchoHub.Client/Services/ConnectionManager.cs` |
|
||||||
|
| [`MessageDto`](../Code/src/EchoHub.Core/DTOs/ChatDtos.cs.md) | record | 20 | 3 | `src/EchoHub.Core/DTOs/ChatDtos.cs` |
|
||||||
|
|
||||||
|
*Regenerated on every full documentation run; see [README](README.md) for how to use this pack.*
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,229 @@
|
|||||||
|
# HueByte/EchoHub — Documentation
|
||||||
|
|
||||||
|
*Generated from branch `master`*
|
||||||
|
*Commit: `40aea9a0`*
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
- **C#**: 128 files
|
||||||
|
|
||||||
|
## Statistics
|
||||||
|
|
||||||
|
- **Files processed:** 128
|
||||||
|
- **Lines of code:** 22,133
|
||||||
|
- **Symbols documented:** 617
|
||||||
|
- **Validation retries:** 69
|
||||||
|
|
||||||
|
## 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,115 @@
|
|||||||
|
# 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 optional account presentation attributes used by client configuration. It encapsulates a DisplayName, Bio, and NicknameColor so a named preset can be stored, transferred, or reapplied as a unit to influence how an account is presented in the UI.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This type exists to package related display properties together, enabling reuse and persistence of account presentation presets. Since all properties are nullable, consumers can merge a preset with existing data and only override the attributes that are explicitly set.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var preset = new AccountPreset
|
||||||
|
{
|
||||||
|
DisplayName = "Nova",
|
||||||
|
Bio = "Exploring the stars of code",
|
||||||
|
NicknameColor = "#1E90FF"
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Null properties indicate that the corresponding attribute should not override any existing value when applying the preset to an existing account.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ClientConfig
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ClientConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ClientConfig is a simple data container that groups the client’s preferences and runtime settings into a single object. It includes the list of configured servers (`SavedServers`), the default account preset (`DefaultPreset`), the currently selected theme (`ActiveTheme`), and the notification configuration (`Notifications`). It also carries optional application paths and rendering settings: `DownloadPath` specifies where attachments are saved (null means use the OS Downloads folder), and `DefaultAsciiSize` selects the ASCII-art rendering size for attached images (values 's', 'm', or 'l', defaulting to 'm').
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ClientConfig centralizes user preferences and runtime settings, so components can rely on a single source of truth for initialization, persistence, and UI decisions. It folds server configuration (`SavedServers`) together with user-facing settings like the default preset (`DefaultPreset`), the active theme (`ActiveTheme`), and notification behavior (`Notifications`), reducing coupling between subsystems. By exposing `DownloadPath` and `DefaultAsciiSize`, it also captures file-management and rendering preferences that affect attachments across the app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NotificationConfig
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class NotificationConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `NotificationConfig` class is a small, strongly-typed container for notification playback settings used by the client. It exposes `Enabled`, `Volume`, and an optional `SoundFile` to customize sound behavior. By default, `Enabled` is `true`, `Volume` is `30`, and `SoundFile` is unset, making it ready to bind from configuration sources.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This is a lightweight configuration object that decouples notification behavior from business logic and supports binding from JSON or other configuration providers. It keeps the surface minimal while making it easy to override defaults without code changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SavedServer
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ClientConfig.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SavedServer
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
SavedServer is a client-side representation of a configured server for the EchoHub client. It aggregates the server identity (Name and Url), optional user credentials (Username and RefreshToken), user preferences (RememberMe), and per-server state needed to restore a session across restarts. Notably, it includes per-channel encryption state (ChannelKeys), channel-level navigation state (LeftChannels), and per-channel read-tracking (LastReadMessages). These members are stored locally and are not exposed to the server; the server never sees the encryption keys, which are encrypted at rest and scoped to the local machine (see [`RoomKeyProtector`](../Services/RoomKeyProtector.cs.md)). At startup, the client can deserialize this object to rehydrate connections, rejoin channels (excluding those the user explicitly left), and persist unread counts and mentions across restarts.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The `SavedServer` acts as a simple data container that binds together server identity, user identity (when supplied), and user-driven state that enhances the reconnect experience. It sits at the boundary between the persistence layer and the networking layer: serialization of this object enables quick restoration of a user session without re-issuing authentication or resynchronizing channel state. The `ChannelKeys` field, in particular, represents sensitive data tied to end-to-end encrypted channels and is kept on the client; its lifecycle is intentionally scoped to the user’s device and is managed with the same care prescribed for the `RefreshToken`.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var server = new SavedServer
|
||||||
|
{
|
||||||
|
Name = "EchoHub",
|
||||||
|
Url = "https://echo.example",
|
||||||
|
Username = "alice",
|
||||||
|
RememberMe = true,
|
||||||
|
LastConnected = DateTimeOffset.UtcNow,
|
||||||
|
ChannelKeys = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "general", "base64encryptedKeyHere" }
|
||||||
|
},
|
||||||
|
LeftChannels = new List<string> { "old-channel" },
|
||||||
|
LastReadMessages = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "general", "12345" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Treat `ChannelKeys` as sensitive data: avoid logging them or exposing them to the UI; ensure at-rest encryption via the client’s security model. The keys are stored only on the client device and are not sent to `server` endpoints.
|
||||||
|
- This class is intended as a plain data carrier (DTO) used by the persistence and connection layers; do not embed domain logic here. When upgrading or migrating fields, consider versioning in the surrounding storage layer to preserve compatibility.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# ConfigManager
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Config/ConfigManager.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ConfigManager
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ConfigManager is a static helper that persists the client configuration to a JSON file under the user's profile directory and provides focused APIs for loading, saving, and managing saved servers. It centralizes file I/O behind a private lock to serialize access from UI actions and background tasks (token refresh, room keys, last-read checkpoints), helping prevent race conditions that could corrupt the config.
|
||||||
|
|
||||||
|
Use `ConfigManager.Load()` to obtain the current configuration (or a default [`ClientConfig`](ClientConfig.cs.md) when the file is missing or unreadable), modify the returned object, and persist changes with `ConfigManager.Save(config)`.
|
||||||
|
|
||||||
|
To manage saved servers, use `ConfigManager.SaveServer(...)` to upsert by `Url` and `ConfigManager.RemoveServer(string url)` to delete by `Url` (case-insensitive).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
All file I/O performed by `ConfigManager` is guarded by a single static lock (the private `Lock` named `FileLock`), ensuring reads and writes do not interleave across threads. The design favors resilience: a missing or unreadable config yields a fresh [`ClientConfig`](ClientConfig.cs.md), and save errors are swallowed to avoid crashing the host process. When upserting or removing saved servers, the code compares the server URLs using a case-insensitive match (`StringComparison.OrdinalIgnoreCase`), so entries differing only by casing do not duplicate and removals reliably locate targets.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Saves are best-effort; any exception during persistence is swallowed so callers should not depend on hard failures for user feedback.
|
||||||
|
- If the config file is absent, the directory is created and a default [`ClientConfig`](ClientConfig.cs.md) is used when loading.
|
||||||
|
- URL-based operations for saved servers use case-insensitive matching to maintain a consistent, deduplicated set.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Program
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Program.cs`
|
||||||
|
> **Kind:** file
|
||||||
|
|
||||||
|
|
||||||
|
The `Program` file serves as the entry point for the EchoHub client. It bootstraps startup by handling a potential CLI rollback (`--rollback`), performing a best-effort Unix execute-permission check, provisioning configuration (loading from `appsettings.json` with a fallback embedded resource at `EchoHub.Client.appsettings.example.json`), and configuring `Serilog` from the configuration before loading the runtime settings via [`ConfigManager`](Config/ConfigManager.cs.md) and initializing the Terminal.Gui UI with `Application.Create().Init()`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This file centralizes environment preparation and startup orchestration, encapsulating cross-platform concerns (rollback handling, permission checks, path setup, and post-update housekeeping) so the rest of the application can assume a ready, consistent runtime context. It also exposes a clear, testable bootstrap path that wires configuration, logging, and the UI startup in a single phase, reducing duplication across modules.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Rolling back can terminate startup early because `UpdateBackupService.RestoreBackup()` or subsequent error paths invoke `Environment.Exit`.
|
||||||
|
- Unix permission checks are best-effort and any failures are swallowed to avoid blocking startup on platform quirks.
|
||||||
|
- The initial configuration may be sourced from an embedded resource (`EchoHub.Client.appsettings.example.json`) if `appsettings.json` is absent, providing a safe fallback during first-run scenarios.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
# AsyncRunner
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/AsyncRunner.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class AsyncRunner
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Runs the provided asynchronous work on a background thread and routes exceptions to the UI, eliminating boilerplate like `Task.Run`/try/catch/`app.Invoke(ShowError)`.
|
||||||
|
|
||||||
|
`AsyncRunner.Run` takes an `IApplication` (`app`), a `Func<Task>` representing the work, an `Action<string>` (`showError`), a string (`errorPrefix`) used in the user-facing error, and an optional `string? logContext` to enrich logs; if an exception occurs, it logs with `Log.Error` and invokes the UI thread to display the error via `showError`.
|
||||||
|
|
||||||
|
This pattern centralizes background execution and UI-error reporting, so callers need only supply the work and error message components and can rely on consistent logging and user feedback.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This abstraction isolates the cross-cutting concerns of background execution and UI error presentation. By encapsulating this pattern, it avoids duplicating boilerplate across call sites and ensures errors are logged with contextual information and surfaced on the UI thread via `IApplication.Invoke`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- This method is fire-and-forget: it launches the work and does not return a `Task`; callers cannot await completion or observe exceptions from the caller's context. If you need completion signaling, consider returning a `Task` or providing a completion callback.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AudioPlaybackService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/AudioPlaybackService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class AudioPlaybackService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AudioPlaybackService provides a thread-safe, asynchronous facade for audio playback using a private `Player` instance. It exposes the playback state via `IsPlaying` and `IsPaused`, and it forwards a `PlaybackFinished` event when the underlying `Player` completes playback. All public operations are serialized with a private `SemaphoreSlim` named `_lock` to prevent concurrent access to the player. When you call `PlayAsync`, if something is already playing it stops it before starting the new file; `PauseAsync`, `ResumeAsync`, and [`StopAsync`](../../EchoHub.Server/Services/ServerDirectoryService.cs.md) similarly acquire the lock, perform the appropriate operation if possible, and log any exceptions with `Log.Warning`. Volume is controlled via `SetVolumeAsync`, which clamps the requested volume to a maximum of 100 using `Math.Min`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This abstraction centralizes concurrency concerns and error handling around audio playback. By bridging the `Player` with a single, serialized surface, it reduces race conditions when multiple callers request playback from different parts of the application. The `PlaybackFinished` event provides a clean notification channel to consumers without exposing the internal player, enabling a decoupled UI or service layer to react to completion.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Exceptions during playback operations are swallowed after being logged with `Log.Warning`, so callers do not observe crashes but must rely on the logs to diagnose issues.
|
||||||
|
- All playback-related methods acquire the `_lock` semaphore, meaning long-running operations inside any call can block other playback requests and should be kept短-lived to avoid contention.
|
||||||
|
- `SetVolumeAsync` caps the volume at 100 via `Math.Min`, ensuring the underlying player never receives an out-of-range value.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# AvatarHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/AvatarHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal static class AvatarHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AvatarHelper provides a single entry point to upload an avatar from either a local file path or a remote URL by converting the target into a `Stream`, then delegating the actual upload to `ApiClient.UploadAvatarAsync`. It abstracts away the file I/O and HTTP fetch logic, ensuring callers don't need to manage streams or HTTP requests themselves. It returns the server's ASCII art response as a `string?` and guarantees the `Stream` is disposed after the upload.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
AvatarHelper isolates avatar uploading behind a focused API, so higher-level code doesn't need to know whether the source is a local file or a URL. It accepts either a local path or an HTTP(S) URL, resolves a valid `fileName` (defaulting to `avatar.png` when the URL doesn't supply one), and streams the content to `ApiClient.UploadAvatarAsync`. The helper ensures proper resource management by disposing the `Stream` after the upload, and it centralizes the cross-cutting concern of avatar uploads to a single place.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# ClientEncryptionService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ClientEncryptionService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ClientEncryptionService : IMessageEncryptionService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ClientEncryptionService implements client-side encryption using AES-256-GCM to protect messages before sending them to the server, aligning with the server's ciphertext format so decryption occurs only with the shared key. After you provide a base64-encoded key via `SetKey`, it encrypts plaintext by generating a fresh 12-byte nonce and a 16-byte authentication tag, returning a string that starts with the `EncryptionPrefix` and includes base64-encoded nonce and payload; if no key has been set (`_key` is null), `Encrypt` returns the plaintext unchanged.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This class hides cryptography behind the [`IMessageEncryptionService`](../../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) contract, offering a simple, predictable API for encryption and decryption while keeping key material private. It ensures that only a server-provisioned key enables encryption, and it produces self-contained ciphertext that carries its nonce and tag so the server can decrypt it reliably. The design also provides nullable-friendly helpers (`EncryptNullable`, `DecryptNullable`) to gracefully handle missing values.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Example usage of client-side encryption
|
||||||
|
var encryption = new ClientEncryptionService();
|
||||||
|
string base64Key = "<32-byte-base64-key>";
|
||||||
|
encryption.SetKey(base64Key);
|
||||||
|
string plaintext = "Secret message";
|
||||||
|
string ciphertext = encryption.Encrypt(plaintext);
|
||||||
|
string decrypted = encryption.Decrypt(ciphertext);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Encrypt before calling `SetKey` is a no-op: the input plaintext is returned unchanged when `_key` is null.
|
||||||
|
- Decrypt returns the original content if `_key` is null or the input does not start with the expected `EncryptionPrefix`.
|
||||||
|
- `SetKey` enforces a 32-byte (256-bit) key length and throws `InvalidOperationException` if the length is not exactly 32 bytes.
|
||||||
|
- Decryption errors are handled gracefully; if decryption fails for any reason, a sentinel message is returned: "[encrypted message — decryption failed, try re-logging to fetch the latest key]".
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# ClipboardFiles
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ClipboardFiles.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ClipboardFiles
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ClipboardFiles reads the OS clipboard to obtain a list of files when the clipboard holds a file-list (such as after copying files in a file manager). This enables scenarios where a copied set of files can be pasted or attached directly, without requiring the user to paste raw text paths. Call `TryGetFiles` to retrieve existing file paths from the clipboard; the method returns true when one or more valid paths are found, and false otherwise (including on platforms without file-list clipboard support).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This helper abstracts away platform differences in clipboard formats and presents a single, cohesive API for retrieving file lists from the clipboard. On Windows it enumerates files via the CF_HDROP channel and returns the paths that point to existing files. On Linux it reads a `text/uri-list` from the clipboard (via `wl-paste` or `xclip`), converts `file://` URLs to local paths, and keeps only paths that exist. The implementation favors a graceful failure path: any read-time exception is logged and the caller simply receives a non-success result, allowing callers to degrade gracefully without crashing. The API design emphasizes a simple success/failure boolean along with a concrete list of files, enabling straightforward integration into UX flows that want to treat copied files as attachable entities rather than plain text.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
if (ClipboardFiles.TryGetFiles(out var files))
|
||||||
|
{
|
||||||
|
foreach (var path in files)
|
||||||
|
Console.WriteLine(path);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- macOS and other non-supported platforms do not provide a file-list clipboard, so `TryGetFiles` returns false there.
|
||||||
|
- The method only returns paths that actually exist on disk; non-existent or malformed clipboard entries are ignored, and an empty result yields false.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# ClipboardImage
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ClipboardImage.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ClipboardImage
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Reads raw image bytes from the platform clipboard and returns them as a PNG byte array when available. Use `ClipboardImage.TryGetPng` when you need a canonical, pasteable PNG representation of whatever image the user has on the clipboard (for example, when accepting pasted screenshots or images in a terminal or chat input that cannot accept raw bitmap data).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ClipboardImage` centralizes platform-specific clipboard handling: `TryGetPng` dispatches to `TryGetWindows`, `TryGetLinux`, or `TryGetMacOS` depending on `OperatingSystem` checks, and normalizes all outputs to PNG. When the clipboard format already contains PNG bytes (detected using the `PngMagic` signature or platform-registered PNG formats such as those discovered via `RegisterClipboardFormatW` on Windows), the bytes are passed through to preserve fidelity and transparency. When the clipboard exposes a DIB/bitmap (`CfDib` on Windows), the `DibToPng` helper builds a minimal BMP wrapper around the DIB bytes, decodes it with `Image.Load`, and re-encodes the result as PNG; this covers screenshots and editors that expose only device-independent bitmaps.
|
||||||
|
|
||||||
|
The class intentionally swallows and logs exceptions (via `Log.Warning`) from clipboard access and image decoding so callers get a simple success/failure result from `TryGetPng` instead of propagating clipboard or image-library exceptions.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Clipboard APIs are platform and threading sensitive. On Windows the OS clipboard typically requires running on an STA thread; calling `TryGetPng` from a non-STA thread may fail or return false. Ensure clipboard access is performed on an appropriate thread context for the platform.
|
||||||
|
- `DibToPng` validates the DIB header (minimum 40 bytes, header size bounds) and returns null for malformed input. Decoding can still fail at `Image.Load` for unsupported or corrupted bitmaps; such failures are logged and surface as a failure to `TryGetPng`.
|
||||||
|
- Re-encoding a DIB to PNG may not preserve alpha/transparency if the original bitmap format lacks alpha channels (DIB/CF_DIB often does not include alpha). If preserving exact alpha semantics is required, prefer sources that supply native PNG clipboard formats when possible.
|
||||||
|
- Converting clipboard data allocates buffers (the BMP wrapper and the resulting PNG byte array) and performs image decode/encode work; callers should expect a non-trivial CPU and memory cost for large images.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Owns the full client-side connection lifecycle: authenticating via the `ApiClient`, establishing and wiring an `EchoHubConnection` (SignalR) for realtime events, enabling end-to-end encryption via the `ClientEncryptionService`, and tracking channel membership in `RoomKeyStore` and `_joinedChannels`. Reach for `ConnectionManager` when you want a single, high-level component to manage connection setup, token refresh handling, event forwarding, and channel join/leave logic instead of manipulating `ApiClient` and `EchoHubConnection` directly.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ConnectionManager` is the orchestration point between the networking primitives (`ApiClient` and `EchoHubConnection`) and the UI layer. It centralizes responsibility for: authenticating (including login, register, and refresh-token flows), persisting rotated refresh tokens via `OnTokensRefreshed`/`HandleTokensRefreshed`, attempting to establish an E2E encryption key with `ClientEncryptionService`, and forwarding SignalR events to consumers through its public events (for example `MessageReceived`, `UserJoined`, `ChannelUpdated`, and `ConnectionStatusChanged`). By exposing `IsConnected`, `IsAuthenticated`, `Api`, and `RoomKeys`, it gives callers enough state to update UI and perform API operations without needing to manage low-level connection state or event wiring.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `ConnectAsync` throws on authentication failure — callers are expected to handle saved-session expiry and related UI flows. See the `ConnectAsync` progress messages for how the method reports intermediate status.
|
||||||
|
- The class disposes and replaces the internal `ApiClient` during `ConnectAsync` (it calls `_apiClient?.Dispose()`), and implements `IAsyncDisposable`; callers should ensure `DisposeAsync` is invoked when the manager is no longer needed to avoid resource leaks.
|
||||||
|
- Encryption is best-effort: if fetching the encryption key fails (`GetEncryptionKeyAsync`), the manager logs a warning and continues with an unencrypted session — consumers should not assume messages are always encrypted.
|
||||||
|
- The implementation mutates internal fields like `_apiClient`, `_connection`, and `_joinedChannels` without visible synchronization. The class appears intended for single-threaded/UI-thread usage; consumers that access it from multiple threads should serialize calls externally to avoid race conditions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ConnectResult
|
||||||
|
> **File:** `src/EchoHub.Client/Services/ConnectionManager.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal record ConnectResult(
|
||||||
|
LoginResponse Login,
|
||||||
|
List<ChannelDto> Channels,
|
||||||
|
Dictionary<string, List<MessageDto>> Histories,
|
||||||
|
ServerStatusDto? ServerInfo = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Login` | `LoginResponse` | — |
|
||||||
|
| `Channels` | `List<ChannelDto>` | — |
|
||||||
|
| `Histories` | `Dictionary<string, List<MessageDto>>` | — |
|
||||||
|
| `ServerInfo` | `ServerStatusDto?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
ConnectResult is an internal, immutable `record` that represents the successful outcome of establishing a connection and is returned to the `AppOrchestrator` to drive UI updates. It bundles the login information (`Login`) of type `LoginResponse`, the joined channels (`Channels`) as `List<ChannelDto>`, the initial per-channel histories (`Histories`) as `Dictionary<string, List<MessageDto>>`, and optional server status (`ServerInfo`) as `ServerStatusDto?`. The `Histories` dictionary maps channel names to their corresponding history lists and always includes the default channel.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ConnectResult acts as a single, UI-facing snapshot of the connected state. It collects authentication results, channel roster, initial per-channel histories, and optional server health/status so the `AppOrchestrator` can immediately render the connected view without issuing further requests.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- ConnectResult is immutable; use a `with` expression to derive a modified copy rather than mutating the existing instance.
|
||||||
|
- `ServerInfo` may be null; callers should handle absence gracefully.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ChannelPasswordRequiredException represents the domain condition that a join operation on a channel cannot proceed because a password is required or the provided password was invalid. It is intended to be caught by the UI layer, which then prompts the user for the correct password and retries the join operation. The exception carries the channel name via the `ChannelName` property to identify which channel needs authentication.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Using a distinct exception type to signal password-related authentication flows keeps the connection logic decoupled from the UI. The `ChannelName` property provides channel-specific context for prompts, enabling precise feedback such as prompting for the password of the channel identified by `ChannelName` when retrying.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Use a specific catch for `ChannelPasswordRequiredException` rather than a broad catch of `Exception`, to avoid handling unrelated failures; access the `ChannelName` to present a contextual, channel-specific prompt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EchoHubConnection
|
||||||
|
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class EchoHubConnection : IAsyncDisposable
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A lightweight, event-driven wrapper around a SignalR `HubConnection` that manages authentication, reconnection and client-side handlers for the chat protocol. Use `EchoHubConnection` when you need a high-level, strongly-typed bridge between the server's [`IEchoHubClient`](../../EchoHub.Core/Contracts/IEchoHubClient.cs.md) callbacks and your UI or application logic — it registers the server method handlers, decrypts incoming content, exposes simple events (for messages, presence, channel updates, errors, etc.), and surfaces connection state changes.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`EchoHubConnection` centralizes SignalR integration concerns: it creates and configures the underlying `HubConnection` (including token provisioning via the provided [`ApiClient`](ApiClient.cs.md)), wires up automatic reconnect behavior, and maps server-invoked methods to public events such as `OnMessageReceived`, `OnUserJoined`, `OnChannelUpdated`, and others. Incoming [`MessageDto`](../../EchoHub.Core/DTOs/ChatDtos.cs.md) instances are run through the connection's decryption path (see `DecryptMessage`/`DecryptField`) before being forwarded, and encrypted content that cannot be unlocked is replaced by the `LockedMessagePlaceholder`. The class implements `IAsyncDisposable` so consumers should `await DisposeAsync()` to cleanly stop the connection.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Assume these are already created: serverUrl (string), apiClient (ApiClient),
|
||||||
|
// encryption (ClientEncryptionService), roomKeys (RoomKeyStore).
|
||||||
|
var connection = new EchoHubConnection(serverUrl, apiClient, encryption, roomKeys);
|
||||||
|
|
||||||
|
connection.OnConnectionStateChanged += state => Console.WriteLine($"State: {state}");
|
||||||
|
connection.OnMessageReceived += message => Console.WriteLine($"Message from {message.From}: {message.Content}");
|
||||||
|
connection.OnReconnected += () => Console.WriteLine("Reconnected to hub");
|
||||||
|
|
||||||
|
// When finished with the connection:
|
||||||
|
await connection.DisposeAsync();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Event handlers are invoked from the SignalR callbacks — subscribers should ensure any UI updates or shared-state mutations are marshalled to the correct synchronization context or made thread-safe.
|
||||||
|
- Encrypted message content is represented by the `LockedMessagePlaceholder` when the client lacks the room key; rejoining the channel with the passphrase (and so populating [`RoomKeyStore`](RoomKeyStore.cs.md)) is required to decrypt those contents.
|
||||||
|
- `IsConnected` reflects the underlying `HubConnection.State` at the moment of access and may change shortly after; use `OnConnectionStateChanged` and `OnReconnected` for lifecycle-driven logic.
|
||||||
|
- Attempting to join a password-protected channel can surface a `ChannelPasswordRequiredException` — callers that perform join flows should handle that explicitly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RoomLockedException
|
||||||
|
> **File:** `src/EchoHub.Client/Services/EchoHubConnection.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class RoomLockedException : Exception
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`RoomLockedException` is thrown when attempting to send into an end-to-end encrypted channel whose room key isn’t cached. Without the key, the operation would emit plaintext, which must never happen, so the exception blocks the send. The `ChannelName` property exposes which channel is locked, and the constructor formats the failure message to include `#{channelName}` to guide unlocking.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This exception acts as a boundary between encryption state and message-sending logic. It is a domain-level signal distinct from other transport or I/O failures, enabling callers to trigger a user prompt to unlock the channel and retry the operation once unlocked. The `ChannelName` property ties the failure to a specific channel, enabling precise remediation flows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the result of joining a channel: the decrypted message history and, for end-to-end encrypted rooms, the key envelope needed to unlock the room content key. `History` is a `List<MessageDto>` containing the decrypted messages, and `WrappedRoomKey` (with optional `EncryptionSalt`) provides the cryptographic envelope when encryption is in play.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating the join outcome in a single type, the caller can render history and prepare for decryption in one step. The nullable `WrappedRoomKey` and `EncryptionSalt` signal whether encryption is active for the channel; callers not using end-to-end encryption can ignore them. This keeps the join path concise while preserving a clear contract about what data is available after join.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `EncryptionSalt` and `WrappedRoomKey` are nullable; guard for nulls and only attempt decryption when these values are provided.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# 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 chooser by shelling out to platform-specific dialogs (Windows Explorer, macOS Finder, Linux GTK/KDE), allowing the TUI to remain GUI-toolkit agnostic. It dispatches to the appropriate platform helper at runtime and returns a `FolderPickResult` with a `PickerOutcome` of `Unavailable` when no native dialog can run, so callers can fall back to a configured path. Failures are caught and logged to avoid crashing the UI, and the dialog title is a fixed prompt guiding the user to select EchoHub’s download folder.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By shielding native dialogs behind `NativeFolderPicker`, the rest of the application stays decoupled from platform GUI toolkits, improving portability and testability. The abstraction also centralizes cross‑platform quirks (Windows PowerShell quoting, AppleScript invocation, and GTK/KDialog fallbacks) in one place, reducing duplication and ensuring a consistent user experience across environments.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Headless Linux environments (no `DISPLAY` or `WAYLAND_DISPLAY`) cause the picker to return `PickerOutcome.Unavailable`.
|
||||||
|
- Windows path handling escapes apostrophes in the initial directory to survive the embedded PowerShell script.
|
||||||
|
- If the user cancels the dialog or no path is selected, the result is `PickerOutcome.Cancelled` rather than an error; callers should handle this as a user action.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 container that captures the result of a native folder picker operation. It pairs the `PickerOutcome` with an optional `Path`, letting callers distinguish between a successful selection and cancellation while carrying the selected folder path only when available.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
As a `record`, `FolderPickResult` benefits from value-based equality and supports deconstruction, enabling concise comparisons and pattern matching when consuming results from the native folder picker. It encapsulates the outcome and potential path in a single, strongly-typed value, simplifying higher-level handling and reducing the need for multiple disparate return values.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- `Path` is nullable; validate before use and prefer accessing `Path` only when `Outcome` indicates a successful result.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## PickerOutcome
|
||||||
|
> **File:** `src/EchoHub.Client/Services/NativeFolderPicker.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum PickerOutcome
|
||||||
|
{
|
||||||
|
Chosen,
|
||||||
|
|
||||||
|
Cancelled,
|
||||||
|
|
||||||
|
Unavailable,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the outcome of prompting the user to pick a folder via the native picker. Use it to branch logic based on whether the user selected a folder, cancelled the dialog, or the environment doesn't provide a picker.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By isolating the three possible results into a single enum, callers can write concise, robust code without tying their logic to UI details. The Cancelled and Unavailable outcomes allow you to differentiate between a user-initiated abort and a runtime environment where the picker isn't present, enabling graceful fallbacks. Tie the Chosen outcome to a corresponding `FolderPickResult` instance that carries the selected path in its `Path` property.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Example: respond to folder-picking outcomes
|
||||||
|
public void HandleOutcome(PickerOutcome outcome, FolderPickResult folderPath)
|
||||||
|
{
|
||||||
|
switch (outcome)
|
||||||
|
{
|
||||||
|
case PickerOutcome.Chosen:
|
||||||
|
Console.WriteLine($"Selected folder: {folderPath.Path}");
|
||||||
|
break;
|
||||||
|
case PickerOutcome.Cancelled:
|
||||||
|
// User cancelled the dialog; no folder selected.
|
||||||
|
break;
|
||||||
|
case PickerOutcome.Unavailable:
|
||||||
|
// Fall back to a non-UI flow
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not access `FolderPickResult.Path` when outcome is not `PickerOutcome.Chosen`.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# NotificationSoundService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/NotificationSoundService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class NotificationSoundService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
NotificationSoundService coordinates playback of the application's notification sound using a configurable file path and volume. It exposes `PlayAsync` for normal operation (respecting the `Enabled` setting) and `PlayTestAsync` to audition the sound regardless of that setting; internally it resolves the sound path, applies the configured volume, and uses a `SemaphoreSlim` lock plus a timeout (`PlaybackTimeout`) to avoid blocking future notifications.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Architecturally, this class centralizes notification sound behavior so callers don't need to touch the `_player` or handle `PlaybackFinished` events directly. It encapsulates path resolution: first a user-configured path (`_config.SoundFile`), if present and exists, else a bundled default at `Path.Combine(AppContext.BaseDirectory, "Assets", "Notification.mp3")`. The combination of a serializing lock (`_lock`) and a guarded finish path ensures only one sound plays at a time and that resources are released promptly even if playback misbehaves.
|
||||||
|
|
||||||
|
The playback flow subscribes to `_player.PlaybackFinished` and uses a `TaskCompletionSource` to await either completion or the timeout; this design guarantees the lock is released even if playback misfires or completes synchronously.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- If no valid sound file is found, notifications will be silent (log: "No notification sound file found — notifications will be silent").
|
||||||
|
- `PlayAsync` will early-return if `_config.Enabled` is false or `_resolvedSoundPath` is null; `PlayTestAsync` will still return early if `_resolvedSoundPath` is null. Both rely on a correctly resolved path to function.
|
||||||
|
- The `_lock` is released in a `finally` block to guarantee progress even when exceptions occur.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 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 compact, immutable data carrier that bundles the pieces needed to upload a file as part of a message: the content as a `Stream` and the original `FileName`. When using end-to-end encrypted channels, `DeclaredKind` signals the attachment type (image, audio, or file) and `EncryptedPreview` holds the room-encrypted ASCII preview for images; on normal channels, only `Stream` and `FileName` are populated.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a `record`, `OutgoingAttachment` provides value-based equality, making attachments easy to compare, cache, or deduplicate as they traverse the messaging pipeline. The optional `DeclaredKind` and `EncryptedPreview` fields separate transport payload from encryption/presentation concerns, keeping encoding logic out of the transport object.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If `DeclaredKind` is provided for an encrypted attachment, ensure `EncryptedPreview` is also supplied to avoid inconsistent previews.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# PathSetup
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/PathSetup.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class PathSetup
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
PathSetup is a small helper that ensures the application's directory is present on the system PATH so users can run the `echohub` CLI from any terminal without specifying the full path. The public entry point, `EnsureOnPath`, checks the current PATH and, if the app directory isn't already included, updates PATH in a platform-appropriate way: Windows adds the directory to the user-level PATH, while Unix-like systems append an export line to common shell profile files. The implementation derives the target directory from `AppContext.BaseDirectory`, normalizes path separators, and gracefully handles failures by logging at the debug level if PATH modification cannot be completed.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
PathSetup centralizes platform-specific PATH augmentation behind a simple, testable API. It makes the side-effect of PATH modification explicit and isolated from business logic, reducing duplication and potential inconsistencies across the codebase. The class uses an idempotent approach: it first checks whether the directory is already on PATH and only proceeds if needed. On Unix-like systems, it uses a persistent marker (`# Added by EchoHub`) to identify its export line in shell profiles, and it guards against duplicating entries. The combination of platform-specific handling, guarded writes, and informative logging ensures predictable behavior during installation and first-run setup while minimizing surprises for end users.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Typical usage during installation or first-run setup
|
||||||
|
PathSetup.EnsureOnPath();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- On Windows, the path update affects only the current user by modifying the user PATH environment variable, avoiding system-wide changes.
|
||||||
|
- On Unix-like systems, the code appends a PATH export line to common shell profiles (``.profile``, ``.bashrc``, ``.zshrc``); it skips profiles that already contain the app directory and creates ``~/.profile`` as a fallback when no profiles exist.
|
||||||
|
- A persistent marker (``# Added by EchoHub``) helps avoid duplicating the export line on repeated runs.
|
||||||
|
- The operation is best observed after restarting terminals or re-sourcing profiles; until that point, newly opened sessions may not reflect the updated PATH.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# 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 permissions 0600 (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 primary public surface consists of:
|
||||||
|
- `Protect(byte[] roomKey)`: encrypts a room key for storage in the config.
|
||||||
|
- `TryUnprotect(string stored, out byte[] roomKey, out bool wasLegacy)`: decrypts a stored value back into a room key.
|
||||||
|
The class caches the per-user master key and selects the protection mechanism based on the platform (DPAPI on Windows when enabled, otherwise the per-user master-key path). It also handles migration of legacy entries by re-encrypting them using the active scheme on subsequent saves. The constants `DpapiPrefix` and `KeyFilePrefix` label the on-disk formats, ensuring callers remain agnostic to the underlying storage strategy.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# RoomKeyStore
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/RoomKeyStore.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class RoomKeyStore
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Holds and manages room content keys for end-to-end encrypted channels for the active session and the persisted per-server client configuration. Use `RoomKeyStore` when you need a single place to cache decrypted room keys in memory, persist them encrypted to the local config (so users don't retype passphrases on each launch), and track which channels are known to be end-to-end encrypted.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`RoomKeyStore` is the in-process authority for room keys: it keeps a memory cache (`_keys`) for the running session and a set (`_encryptedChannels`) to mark channels that are treated as encrypted. It delegates on-disk protection to [`RoomKeyProtector`](RoomKeyProtector.cs.md) so keys never leave the machine in plaintext. Calling `LoadForServer` binds the store to a specific server URL, loads that server's `SavedServer.ChannelKeys` via `ConfigManager.Load()`, and hydates the in-memory cache (skipping unreadable entries). Legacy plaintext/legacy-storage entries detected by `RoomKeyProtector.TryUnprotect` are re-encrypted and re-persisted as a one-way upgrade. All public mutation and lookup methods synchronize on the internal `Lock` (`_lock`) to provide basic thread-safety for concurrent callers.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `TryGetKey` returns the stored byte array reference from the internal `_keys` map (no defensive copy). Callers must not mutate the returned `byte[]` in-place — clone it first if modification is required.
|
||||||
|
- Channel name lookup is case-insensitive because the internal collections use `StringComparer.OrdinalIgnoreCase`. Treat channel names consistently to avoid duplicate/lookup surprises.
|
||||||
|
- Loading ignores unreadable cached entries and will re-persist only entries that [`RoomKeyProtector`](RoomKeyProtector.cs.md) could successfully unprotect; `TryStoreFromEnvelope` returns false when the provided KEK fails to unwrap the envelope and will leave the cache unchanged. Storing or removing a key persists the corresponding `SavedServer.ChannelKeys` entry immediately (via the store's persistence path).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Defines a source-generated JSON serialization context for `BackupInfo` by annotating the internal partial class ``BackupJsonContext`` with ``JsonSerializable(typeof(BackupInfo))``. This enables high-performance, reflection-free JSON serialization and deserialization via System.Text.Json's source generator when working with ``BackupInfo``.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing the JSON metadata in ``BackupJsonContext``, the codebase gains a single, version-stable contract for serializing ``BackupInfo``. The generated ```JsonTypeInfo<BackupInfo>``` exposed as ``BackupJsonContext.Default.BackupInfo`` is consumed by ``JsonSerializer`` overloads that accept type metadata, reducing runtime reflection and enabling better inlining and optimization. This scope-limited context also makes it straightforward to extend serialization support to additional related types by extending the same context without changing call-sites.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UpdateBackupService
|
||||||
|
> **File:** `src/EchoHub.Client/Services/UpdateBackupService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class UpdateBackupService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
UpdateBackupService is a centralized helper that manages pre-update backups and rollback restoration for the auto-updater. It stores backups under the user profile in `~/.echohub/update-backup/` and exposes operations to create a snapshot, verify an existing backup, and read its metadata. Before applying an update, `CreateBackup()` snapshots the current application directory (via `AppContext.BaseDirectory`) into a ZIP named `backup.zip` and writes a `backup-info.json` containing the version, app directory, and timestamp. It skips log files to avoid locking issues, uses `CompressionLevel.Fastest` for speed, and annotates the backup with the current version from `UpdateChecker.CurrentVersion`. `BackupExists()` checks for the presence of both `backup.zip` and `backup-info.json`, while `GetBackupInfo()` reads and deserializes the metadata using `BackupJsonContext.Default.BackupInfo`. The `IsPostUpdate` flag signals that a post-update backup was produced and may influence rollback or recovery flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 `record` that encapsulates the metadata for a backup produced by the application. It aggregates the `Version` string, the `AppDirectory` path where the backup resides, and the creation timestamp `CreatedAt` as a `DateTimeOffset`, providing a single, immutable value that callers can transport, compare, or display without reconstructing individual fields. Use this type whenever you need to pass around a complete snapshot of backup identity and location rather than scattering primitive values.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because `BackupInfo` is a `record`, it provides value-based equality and immutability, so two backups with the same `Version`, `AppDirectory`, and `CreatedAt` compare as equal. This makes it ideal as a transport object across service boundaries and as a stable key or result in collections. It also supports deconstruction, enabling concise extraction of its three fields when needed.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- This object is immutable; its properties are set at construction time and cannot be changed afterward.
|
||||||
|
- The `CreatedAt` value uses `DateTimeOffset` to preserve the exact point in time including offset, which is important for cross-system backups and logs.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# UpdateChecker
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/UpdateChecker.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class UpdateChecker : IDisposable
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Checks for application updates on a background schedule and coordinates a safe, post-TUI update process. Use `UpdateChecker` when you want automatic or on-demand update checks inside a Terminal.Gui-based host but need the actual download/extract/restart work to run after the UI main loop has exited.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`UpdateChecker` encapsulates the interaction between the UI, a periodic `Updater` and the host process that must perform the actual update. It listens for `Updater` events and, when the user confirms an update via `UpdateConfirmDialog.Show`, sets the public [`PendingUpdate`](../AppOrchestrator.cs.md) delegate and requests the UI to stop so the host can perform the heavy work on a plain console. This design avoids the console deadlock that would occur if the updating process tried to restart while the Terminal.Gui main loop still owned the console. `CurrentVersion` exposes the assembly version used in the confirmation UI.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// During application startup
|
||||||
|
var checker = new UpdateChecker(app);
|
||||||
|
checker.Start(); // starts periodic checks in RELEASE builds
|
||||||
|
|
||||||
|
// Trigger a manual check from UI or command handler
|
||||||
|
await checker.CheckNowAsync();
|
||||||
|
|
||||||
|
// After the Terminal.Gui main loop exits, the host should run any pending update
|
||||||
|
if (checker.PendingUpdate != null)
|
||||||
|
{
|
||||||
|
await checker.PendingUpdate(); // will download/extract and may restart the process
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- [`PendingUpdate`](../AppOrchestrator.cs.md) is only set when the user confirms an available update via `UpdateConfirmDialog.Show`; the host must check and invoke [`PendingUpdate`](../AppOrchestrator.cs.md) after the TUI main loop exits.
|
||||||
|
- `Start()` is conditional on the `RELEASE` build symbol — in non-RELEASE builds the periodic checker does not run.
|
||||||
|
- Invoking the [`PendingUpdate`](../AppOrchestrator.cs.md) delegate runs the updater on a plain console and may end by restarting the app (the code calls into the `Updater` which performs download/extract/restart). The host should not expect normal process continuation after the update completes.
|
||||||
|
- `CurrentVersion` reads the assembly version and will return `"0.0.0"` if the assembly version cannot be determined.
|
||||||
|
- Backup creation is attempted via `UpdateBackupService.CreateBackup()` before applying an update; failures are logged and the update continues without a backup.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# UserSession
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/Services/UserSession.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal sealed class UserSession
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the current user\'s session state within the client, encapsulating the `Username`, the presence `Status` from [`UserStatus`](../../EchoHub.Core/Models/UserStatus.cs.md), and an optional `StatusMessage`. It is a lightweight in-memory container used by UI and networking layers to track who is logged in and how they present themselves. The `Reset` method reinitializes all fields to their defaults: `Username` to empty, `Status` to `UserStatus.Online`, and `StatusMessage` to `null`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This small class centralizes session-related data so multiple components can read and update the user\'s identity and presence from a single source of truth. By being `internal` and `sealed`, it communicates that this is an implementation detail of the client assembly and should not be extended or exposed publicly.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `Theme` class encapsulates the color palette used by the UI. It groups per-surface color sets for the main surfaces (`Base`, `Menu`, `Dialog`, `Status`) and exposes an optional `Border` color that can override the window frame independently of text. By providing a name and a complete set of colors, a developer can switch or define visual styles at runtime and apply them to the UI. If you do not need a separate border color, leave `Border` as null to fall back to `Base`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The `Theme` object acts as a central theme descriptor that isolates surface-specific colors from the core palette, making it easy to create variants (e.g., light, dark, or glassy appearances) without scattering color values through the code. The optional `Border` enables stylistic nuances for window chrome without altering text or control coloring, helping to achieve subtler, themed aesthetics while preserving readability.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var theme = new Theme
|
||||||
|
{
|
||||||
|
Name = "Glass",
|
||||||
|
Base = new ThemeColors(), // default color family for surfaces
|
||||||
|
Border = null // explicit fallback to Base colors for borders
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ThemeColors
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/Theme.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ThemeColors
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ThemeColors is a small data container that groups the color tokens used by the UI: `Foreground`, `Background`, `FocusForeground`, and `FocusBackground`. Create and pass a single `ThemeColors` instance to ensure consistent theming across components rather than scattering color literals throughout the code.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing color choices in `ThemeColors`, the UI can swap themes or provide variations without touching individual controls. The default initializers encode a high-contrast dark theme (white text on black, focus highlight in blue), but you can override any property to tailor a theme for a particular context.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Mutability: the properties have public setters, so the color values can be changed after construction; if a `ThemeColors` instance is shared, mutations will affect all dependents.
|
||||||
|
- Defaults are defined via property initializers; override them on construction if you want a different baseline.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,559 @@
|
|||||||
|
# ThemeManager.cs
|
||||||
|
|
||||||
|
> **Source:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [ThemeManager](#thememanager)
|
||||||
|
- [ApplyTheme](#applytheme)
|
||||||
|
- [BuildColorScheme](#buildcolorscheme)
|
||||||
|
- [GetAvailableThemes](#getavailablethemes)
|
||||||
|
- [GetTheme](#gettheme)
|
||||||
|
- [ParseColor](#parsecolor)
|
||||||
|
- [SaveTheme](#savetheme)
|
||||||
|
- [BuiltInThemes](#builtinthemes)
|
||||||
|
- [ClassicTheme](#classictheme)
|
||||||
|
- [DefaultTheme](#defaulttheme)
|
||||||
|
- [DraculaTheme](#draculatheme)
|
||||||
|
- [GruvboxTheme](#gruvboxtheme)
|
||||||
|
- [HackerTheme](#hackertheme)
|
||||||
|
- [HighContrastTheme](#highcontrasttheme)
|
||||||
|
- [JsonOptions](#jsonoptions)
|
||||||
|
- [LightTheme](#lighttheme)
|
||||||
|
- [MonokaiTheme](#monokaitheme)
|
||||||
|
- [NordTheme](#nordtheme)
|
||||||
|
- [OceanTheme](#oceantheme)
|
||||||
|
- [RosePineTheme](#rosepinetheme)
|
||||||
|
- [SolarizedTheme](#solarizedtheme)
|
||||||
|
- [ThemeDir](#themedir)
|
||||||
|
- [TransparentLightTheme](#transparentlighttheme)
|
||||||
|
- [TransparentTheme](#transparenttheme)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ThemeManager
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class ThemeManager
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ThemeManager is a static helper that centralizes theming for the client UI. It defines built-in themes, reads user-defined themes from the user's theme directory, and exposes methods to enumerate available themes, fetch a theme by name, apply a theme at runtime, and persist theme definitions to disk. Developers reach for it when they need to present theme choices to users, switch the active look, or save a customized theme for future sessions.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Theme definitions live as [`Theme`](Theme.cs.md) instances inside the manager, with a fixed set of built-ins (e.g. `DefaultTheme`, `TransparentTheme`, `TransparentLightTheme`, `ClassicTheme`, `LightTheme`, `HackerTheme`, `SolarizedTheme`, `DraculaTheme`, `MonokaiTheme`, `NordTheme`, `GruvboxTheme`, `OceanTheme`, `HighContrastTheme`, `RosePineTheme`) and a mechanism to discover additional user themes from the directory located at `ThemeDir`. `GetAvailableThemes()` merges these sources while skipping duplicates by name and ignoring malformed theme files; if the theme directory cannot be read, it gracefully falls back to the built-ins. The color wiring happens in `BuildColorScheme(ThemeColors colors)` to ensure the editor surfaces—such as `TextView` and `TextField`—are pinned to the theme’s colors so transparency is preserved (e.g. transparent themes do not render an opaque input background). `ApplyTheme(Theme theme)` applies the chosen look to UI chrome like frame borders and titles, while `SaveTheme(Theme theme)` persists changes to disk as a best-effort operation.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Reading themes from disk is guarded with a fallback to built-ins; IO failures result in a safe degradation rather than a crash.
|
||||||
|
- Saving themes is a best-effort operation and may fail silently to avoid impacting startup or runtime stability.
|
||||||
|
- Color parsing relies on `ParseColor(string colorName)`; ensure color names in themes map to known colors to avoid rendering surprises.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
ApplyTheme translates a [`Theme`](Theme.cs.md) into runtime color schemes and registers them with the central scheme registry (`SchemeManager`). For each area (`Base`, `Menu`, `Dialog`) it calls `BuildColorScheme` and registers the result via `SchemeManager.AddScheme`. The `Border` area is populated as well, using `theme.Border` when provided or falling back to `theme.Base` when it is not, ensuring frame decorations always have a defined appearance.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By encapsulating the mapping from a [`Theme`](Theme.cs.md) to per-area color schemes, `ApplyTheme` centralizes theming logic and reduces boilerplate across the UI. It also encodes the intended fallback for borders: if a `Border` scheme isn't specified, the `Base` scheme is reused so borders and title bars stay consistent with the rest of the theme.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- If `theme.Base` is null and no explicit `theme.Border` is provided, `BuildColorScheme` will receive null, which could lead to an exception at runtime. Ensure `theme.Base` is non-null when a border theme isn't supplied.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 is an internal helper that converts a [`ThemeColors`](Theme.cs.md) instance into a complete `Scheme` by translating the theme's foreground/background for normal and focused states into two `Attribute`s and applying them across the scheme's state properties (`Normal`, `Focus`, `HotNormal`, `HotFocus`, `Disabled`, `Editable`, `ReadOnly`). It ensures the editable areas reflect the same colors as the surrounding background, which matters for transparent themes.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Conceptually, this centralizes the translation from [`ThemeColors`](Theme.cs.md) to a `Scheme`, guaranteeing consistent color usage across `Normal`/`Focus` and their hot variants. By reusing the same color attributes for `Normal`, `Disabled`, and the editable states, it reduces drift when themes change and keeps UI elements visually cohesive. The inline comment explains the rationale: binding `Editable` and `ReadOnly` to the theme's `Normal` colors ensures the input areas don't render an opaque box behind transparent themes.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Disabled uses the same color as `Normal`; if you need a distinct disabled appearance, this method would need to be extended.
|
||||||
|
- Editable and ReadOnly are pinned to `Normal` to preserve background transparency; changing this could cause mismatches with the theme's background in transparent themes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GetAvailableThemes
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static List<Theme> GetAvailableThemes()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `List<Theme>`
|
||||||
|
|
||||||
|
|
||||||
|
GetAvailableThemes collects the available themes by starting with the built-in set (`BuiltInThemes`), then augmenting it with user-provided themes discovered as JSON files in `ThemeDir`. It reads each `*.json` file, deserializes the content into a [`Theme`](Theme.cs.md) using `JsonSerializer` with `JsonOptions`, and, if the resulting theme has a non-empty `Name` and isn't already present (checked by name using `StringComparison.OrdinalIgnoreCase`), adds it to the list. If the theme directory can't be read or a file is malformed, those items are skipped and the method returns the built-in themes as a fallback. The result is a `List<Theme>` that callers can present to the user.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The `GetAvailableThemes` abstraction centralizes theme discovery, ensuring that built-in themes serve as a baseline while allowing runtime customization through JSON files in `ThemeDir`. It performs simple de-duplication by `Theme.Name` in a case-insensitive manner, so user-provided themes do not create duplicates of built-ins. The design favors resilience: IO or deserialization failures are swallowed so startup remains stable, and valid themes are still returned. This function depends on the shape of the [`Theme`](Theme.cs.md) model (e.g., `Name`, `Base`/`Menu`/`Dialog` color sets) to render themes in the UI.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var themes = ThemeManager.GetAvailableThemes();
|
||||||
|
foreach (var t in themes)
|
||||||
|
{
|
||||||
|
Console.WriteLine(t.Name);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- IO or JSON parsing errors for individual files are ignored; only valid themes are included in the result.
|
||||||
|
- If `ThemeDir` does not exist or cannot be read, the method falls back to returning only the built-in themes.
|
||||||
|
- A runtime-provided theme with a name equal (ignoring case) to an existing built-in theme will be skipped to avoid duplicates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
|
||||||
|
Returns the [`Theme`](Theme.cs.md) whose `Name` matches the provided `name` using a case-insensitive comparison (`StringComparison.OrdinalIgnoreCase`), sourcing candidates from `GetAvailableThemes()`. If no match is found, it returns `DefaultTheme` as a safe fallback. This encapsulates the pattern of resolving a theme by name and protects callers from handling nulls or missing themes themselves.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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` value by delegating to `Color.TryParse`. If the parse succeeds, it returns the resulting color (or `Color.White` if the parsed value is null). If parsing fails, it falls back to `Color.White`. This provides a safe, centralized way to convert string-based color specifications (for example, theme or config values) into a concrete `Color` without forcing callers to handle parsing errors themselves.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method encapsulates the color-name resolution logic so the rest of the theming code does not need to repeat `TryParse` calls or null checks. It guarantees a non-null `Color` return value by defaulting to `Color.White`, thereby defining a system-wide fallback policy for theme colors. Being a private helper, it represents an internal implementation detail of the theme system rather than a public API, which keeps the surface area clean for consumers.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Invalid or unrecognized color names map to `Color.White`, which can mask configuration errors; consider validating color names if distinguishing between an explicit white and a default fallback is important.
|
||||||
|
- If `colorName` is null or empty, the method still returns `Color.White` via the parse/fallback path, ensuring callers always receive a concrete `Color` without exceptions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
Saves a [`Theme`](Theme.cs.md) to disk as a JSON file under `ThemeDir`. It ensures `ThemeDir` exists, constructs the file path using the theme's name (the value of `theme.Name`) with a `.json` extension, serializes the [`Theme`](Theme.cs.md) with `JsonSerializer` using `JsonOptions`, and writes the resulting JSON to disk. Any exceptions are swallowed, making this a best-effort persistence rather than a guaranteed save.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
SaveTheme encapsulates the simple, best-effort persistence strategy for user-defined themes and deliberately avoids propagating IO errors to callers. It is safe to call during normal operation without risking user-facing crashes, but callers should not rely on this method to succeed every time. Because the file name is derived from `theme.Name`, unmapped or invalid characters in names can cause a write to fail silently.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The empty catch means failures won't surface to the caller; consider validating `theme.Name` to ensure a valid file name before invoking this method.
|
||||||
|
- Writes are synchronous and will overwrite an existing file named after the theme.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 of [`Theme`](Theme.cs.md) instances that enumerates the built-in themes shipped with the client. It is initialized with a predefined sequence of themes: `DefaultTheme`, `TransparentTheme`, `TransparentLightTheme`, `ClassicTheme`, `LightTheme`, `HackerTheme`, `SolarizedTheme`, `DraculaTheme`, `MonokaiTheme`, `NordTheme`, `GruvboxTheme`, `OceanTheme`, `HighContrastTheme`, and `RosePineTheme`, and is used internally by the theming subsystem to provide a centralized source of available themes without constructing them at runtime.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This private, static collection centralizes the built-in theme catalog used by the theming system. The `readonly` modifier prevents reassigning the field, but the underlying `List<Theme>` can still be mutated by internal code, which means changes to the set of built-ins could affect any UI that relies on them. If true immutability is required, consider exposing a read-only wrapper or a dedicated API surface.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `List<Theme>` is mutable even though the field is `readonly`; external code cannot access it, but internal code can modify its contents. If you need to guarantee immutability, replace with a read-only wrapper such as `ReadOnlyCollection<Theme>` and expose a safe accessor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ClassicTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme ClassicTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ClassicTheme is a privately scoped, statically initialized [`Theme`](Theme.cs.md) instance that serves as the built-in look-and-feel blueprint used by the UI. It defines color mappings for the `Base`, `Menu`, `Dialog`, and `Status` surfaces, establishing a cohesive appearance across the application. Because it is declared as `private static readonly`, the instance is created once during type initialization and is shared for the lifetime of the process, acting as a default theme reference for the `ThemeManager`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing the palette in a single, private field, the `ThemeManager` can apply a consistent Classic style across all major surfaces without requiring external configuration. The private visibility keeps the default theme encapsulated within the theming code, making it straightforward to introduce additional themes or swap them by adding alternative static fields or exposing a configuration mechanism in the future.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DefaultTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme DefaultTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the canonical default color theme used by the theming subsystem. As a private static readonly [`Theme`](Theme.cs.md) named `Default`, it seeds the color configuration for core surfaces (`Base`, `Menu`, `Dialog`, `Status`) so the UI maintains a consistent palette when no user-provided theme is supplied.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This value acts as the internal seed for all theming operations within the `ThemeManager`. Centralizing the default colors in a single `DefaultTheme` instance ensures consistent visuals across surfaces and avoids duplicating color choices. Note that while the field is `readonly`, its nested [`ThemeColors`](Theme.cs.md) objects may still be mutable at runtime, depending on their mutability; consuming code should not rely on deep immutability unless enforced by the type definitions. The arrangement guarantees uniform behavior for the `Base`, `Menu`, `Dialog`, and `Status` color states (foreground, background, and focus states).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Although the field is `readonly` at the top level, the nested [`ThemeColors`](Theme.cs.md) instances may be mutated; treat this as a potential mutation point.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### DraculaTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme DraculaTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
DraculaTheme is a preconfigured [`Theme`](Theme.cs.md) instance that encodes the Dracula color palette for the UI. Declared as a private static readonly field named `DraculaTheme`, it defines a single, shared palette used by the application to color the core surfaces — `Base`, `Menu`, `Dialog`, and `Status` — with per-surface mappings such as foregrounds, backgrounds, and focus colors that collectively establish a cohesive, dark interface with magenta accents on focus. With `Name` set to Dracula, this theme provides a consistent Dracula aesthetic across the application.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Centralizes the Dracula color choices in one place to ensure visual consistency across surfaces and to simplify theme swapping by the `ThemeManager` without recalculating colors at render time. The per-surface [`ThemeColors`](Theme.cs.md) definitions govern how content appears on the main areas (`Base`), the navigation (`Menu`), popups (`Dialog`), and status indicators (`Status`).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The nested [`ThemeColors`](Theme.cs.md) objects may be mutable; treat DraculaTheme as effectively immutable only if those types are immutable, or clone before modification if variations are needed.
|
||||||
|
- Because the field is private, external code cannot reference it directly; expose an accessor or copy if you need to reuse this theme outside its containing class.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GruvboxTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme GruvboxTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
GruvboxTheme is a private static readonly [`Theme`](Theme.cs.md) that defines the Gruvbox color palette used by the UI. It initializes `Name` to "Gruvbox" and provides color configurations for the core UI regions via `Base`, `Menu`, `Dialog`, and `Status`, each specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` values.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
GruvboxTheme serves as a single source of truth for the Gruvbox palette, making it easy to apply the same colors across `Base`, `Menu`, `Dialog`, and `Status` without duplicating literals elsewhere. Because the field is `static` and `readonly`, the palette is established once during type initialization and cannot be mutated at runtime, ensuring a consistent theme until a deliberate change is made in code. External code relies on the public theming surface to apply the Gruvbox palette; GruvboxTheme itself remains a private, immutable foundation for that surface.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Private field scope means external code cannot reference `GruvboxTheme` directly; use the public theming API (e.g., `ThemeManager`) to switch or retrieve themes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HackerTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme HackerTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
HackerTheme is a private static readonly instance of [`Theme`](Theme.cs.md) that defines the 'Hacker' color scheme used by the UI. It centralizes the color configuration for the core regions—`Base`, `Menu`, `Dialog`, and `Status`—by specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` to deliver a cohesive hacker aesthetic across the interface, and is reused internally rather than rebuilt for each component.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By housing the entire color palette in a single static field, the code ensures visual consistency across all UI surfaces that adopt this theme. The `HackerTheme` instance is created once at class initialization and referenced wherever a [`Theme`](Theme.cs.md) is needed within the theme system, promoting reuse and reducing the risk of divergent color values. Keeping this configuration private reinforces encapsulation: external code cannot mutate the theme inadvertently, preserving the intended appearance.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HighContrastTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme HighContrastTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Defines a preconfigured [`Theme`](Theme.cs.md) instance named `HighContrast` that drives a high-contrast UI palette. It is exposed internally as a private static readonly field `HighContrastTheme` and initializes the `Base`, `Menu`, `Dialog`, and `Status` surfaces with a dark background (`Black`) and bright foreground (`BrightYellow`), while tuning region-specific focus colors to preserve legibility. Because it is static and readonly, the theme is constructed once and reused by the UI theming system rather than rebuilt at runtime.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This field acts as a canonical, immutable high-contrast palette for the theming subsystem. By centralizing the color choices for `Base`, `Menu`, `Dialog`, and `Status`, it ensures consistent accessibility-friendly visuals across the application and prevents drift between components. Its private visibility indicates it is an internal implementation detail of the theme infrastructure, intended to be consumed by the theme-management logic rather than by consumer code directly.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `HighContrastTheme` is immutable after initialization due to `readonly`; runtime theme switching would require a separate mechanism to swap themes.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### JsonOptions
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Defines a shared `JsonSerializerOptions` instance named `JsonOptions` used by the `ThemeManager` to serialize theme data with consistent formatting. It configures pretty-printed JSON by setting `WriteIndented` to true and enforces camelCase property names by using `PropertyNamingPolicy` via `JsonNamingPolicy.CamelCase`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By making the field `static` and `readonly`, the class ensures a single, immutable source of serialization configuration for all calls within the ThemeManager, reducing duplication and the risk of inconsistent formatting. This centralization also minimizes drift if multiple serialization sites exist in the class.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not mutate `JsonOptions` after initialization; although `JsonSerializerOptions` properties are mutable, the field is intended to be consumed as a fixed configuration.
|
||||||
|
- If a one-off operation requires a different formatting (e.g., a different naming policy or indentation), create and use a separate `JsonSerializerOptions` instance instead of modifying this field.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### LightTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme LightTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `LightTheme` field provides a concrete, immutable light color scheme used by the theming system. It centralizes color definitions for the main UI surfaces: `Base`, the `Menu`, `Dialog`, and `Status` areas, ensuring consistent foreground/background combinations across the application and predictable focus states.
|
||||||
|
|
||||||
|
With `Name` set to `Light` and color pairs like `Foreground`/`Background` and `FocusForeground`/`FocusBackground` defined per surface, it enables the ThemeManager to apply the light theme quickly without reconstructing the palette each time.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By keeping the field `private static readonly`, the code guarantees a single, shared instance of the light theme that cannot be modified at runtime, avoiding drift between components. This centralization also clarifies the intended visual identity for the light mode and reduces duplication whenever a light theme is needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 encapsulates the internal Monokai color palette used by the UI. It defines a single [`Theme`](Theme.cs.md) named `Monokai` with dedicated [`ThemeColors`](Theme.cs.md) for `Base`, `Menu`, `Dialog`, and `Status`, specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` to ensure the interface presents a cohesive look.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
MonokaiTheme centralizes the Monokai palette for the UI, providing a single source of truth for the [`Theme`](Theme.cs.md) the `ThemeManager` applies across components. Its private static readonly scope ensures a stable, class-wide instance isn't exposed or replaced by external code, preserving the intended appearance. If internal code mutates the nested [`ThemeColors`](Theme.cs.md) objects, the look could drift, so treat the instance as effectively immutable after initialization.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `readonly` prevents reassigning the field, but nested color objects may still be mutated; ensure internal code avoids mutating the theme after initialization or consider making the color data immutable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### NordTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme NordTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A private static readonly [`Theme`](Theme.cs.md) named `NordTheme` encodes the Nord color palette for the UI. It initializes `Base`, `Menu`, `Dialog`, and `Status` color schemes with explicit foreground and background values, serving as an immutable, centralized Nord appearance that the theme system can apply when Nord is active.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
NordTheme acts as a self-contained Nord theme preset, isolating color mappings for core UI regions. Because it is `static` and `readonly`, the palette is stabilized at startup, ensuring consistent visuals across the app when Nord is selected. Each region (`Base`, `Menu`, `Dialog`, `Status`) groups foreground/background pairs, making future tweaks localized to this single field.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Since `NordTheme` is `private`, external code cannot reference it directly; if runtime theme switching is needed, introduce a public API or factory to expose a Nord palette.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### OceanTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme OceanTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The private static readonly field `OceanTheme` is a [`Theme`](Theme.cs.md) instance configured with a named palette Ocean and dedicated [`ThemeColors`](Theme.cs.md) for its `Base`, `Menu`, `Dialog`, and `Status` sections. It is initialized inline with specific color tokens such as `BrightCyan`, `DarkBlue`, and `DarkCyan` to ensure a cohesive, visually distinct look across the UI. Being `static readonly` means this instance is created once at type initialization and cannot be reassigned, serving as an internal, consistent theme blueprint for the `ThemeManager`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
This field encapsulates a concrete theme configuration that `ThemeManager` uses internally, without exposing mutable defaults to consumers. Centralizing color mappings for `Base`, `Menu`, `Dialog`, and `Status` in a single private field reduces duplication and promotes visual consistency across the UI. Because the field is private, external code cannot reference or alter it directly; changes must go through the public theming API, preserving encapsulation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### RosePineTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme RosePineTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
RosePineTheme is a private static readonly [`Theme`](Theme.cs.md) instance named `RosePine` that encodes a RosePine color palette for the UI. It defines color roles for `Base`, `Menu`, `Dialog`, and `Status` via nested [`ThemeColors`](Theme.cs.md) objects, specifying `Foreground`, `Background`, `FocusForeground`, and `FocusBackground` values. This single, prebuilt object lets the rest of the UI apply a cohesive RosePine appearance without reconstructing a [`Theme`](Theme.cs.md) from scratch.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Centralizes the RosePine aesthetic in one place, ensuring consistent color usage across the core chrome (`Base`, `Menu`, `Dialog`, `Status`). As a private static field, it is intended for internal composition by the theme system, reducing boilerplate when constructing themes at runtime. If you need to expose it externally, you would typically wrap or copy it behind a public API.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Although the field is `readonly`, the nested [`ThemeColors`](Theme.cs.md) instances may still be mutable if their properties have setters. Treat the object as immutable; avoid mutating to preserve a consistent RosePine theme.
|
||||||
|
- The field is private, so external consumers cannot reference `RosePineTheme` directly; changes to the theme would require a public accessor or method in `ThemeManager`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SolarizedTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme SolarizedTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
This field defines the pre-defined Solarized color theme as a private, static, readonly [`Theme`](Theme.cs.md) instance named `SolarizedTheme`. It bundles color roles for the base chrome, menus, dialogs, and status areas, providing a centralized Solarized palette that the theming subsystem can apply to the UI. The `private static readonly` designation ensures a single, immutable instance is created at startup, guaranteeing consistent visuals across the application.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Having a single [`Theme`](Theme.cs.md) instance for Solarized encapsulates the palette in one place, reducing duplication of color literals across UI surfaces. By separating the colors into `Base`, `Menu`, `Dialog`, and `Status` groups, the theme clearly communicates how each UI surface should appear and simplifies future tweaks. This private field serves as an internal canonical source for the Solarized look within the codebase and is consumed by the theming pipeline without exposing implementation details publicly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 stores the path to the per-user themes directory for the EchoHub client. It is initialized once at type initialization by combining the user's home directory (obtained via `Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)`) with the relative path `".echohub/themes"`, yielding a stable, user-scoped base for reading or enumerating theme assets.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
- By centralizing the path construction, this private static readonly field reduces duplication and ensures all theme IO uses the same base directory.
|
||||||
|
- It encodes the assumption that themes are stored under the user's profile, which keeps user-specific customization isolated from system-wide resources.
|
||||||
|
- The static readonly nature means the value is fixed after initialization, simplifying reasoning about its value and caching theme metadata.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If the environment lacks a user profile directory, `Environment.GetFolderPath` may return an empty string, which would yield an invalid `ThemeDir`. Calling code should validate the path before attempting IO.
|
||||||
|
- It is a private field; external code cannot rely on this path and must use public APIs provided by the class for theme access.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TransparentLightTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme TransparentLightTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Defines a concrete [`Theme`](Theme.cs.md) named `TransparentLight` with per-surface color rules for `Base`, `Menu`, `Dialog`, `Status`, and `Border` via [`ThemeColors`](Theme.cs.md). Each surface is configured with `Foreground`, `Background`, and `FocusForeground`/`FocusBackground` values to yield a light, nearly transparent appearance on the host UI: most surfaces use `Background = "None"`, while `Dialog` uses a light gray background and blue focus accents. This field is `private static readonly`, initialized once and used internally by the theming system to provide the `TransparentLight` theme.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing the color definitions for a light, semi-transparent appearance, this field enables consistent theming across the UI without scattering color literals throughout the code. Because it is `private`, external code cannot directly reference it; the surrounding theme infrastructure can expose higher-level theme switching that pulls from this internal variant. The immutable reference helps ensure the theme is not accidentally replaced at runtime, though the nested [`ThemeColors`](Theme.cs.md) instances may still be mutated if their properties are writable.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `readonly` modifier prevents reassignment of the field, but the nested [`ThemeColors`](Theme.cs.md) objects could still be mutated if their properties have setters; avoid mutating them at runtime to preserve theme consistency.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TransparentTheme
|
||||||
|
> **File:** `src/EchoHub.Client/Themes/ThemeManager.cs`
|
||||||
|
> **Kind:** field
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private static readonly Theme TransparentTheme = new()
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
TransparentTheme is a private, static readonly instance of [`Theme`](Theme.cs.md) that encodes the glassy, transparent UI aesthetic named 'Transparent' and is intended for internal use by the theming system rather than as a public theme. It defines color settings for `Base`, `Menu`, `Dialog`, `Status`, and `Border` to deliver a cohesive appearance, with muted `Border` colors to preserve the translucent look.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
TransparentTheme centralizes the palette for the glassy style in a single immutable object, reducing duplication across components. As a private field, it serves as an internal predefined palette that the theming system can apply without exposing a public API. This encapsulation makes it easy to tweak the look in one place while keeping the public surface stable.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# ChatColors
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatColors.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static partial class ChatColors
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Shared color attributes and small text-processing helpers used by the chat UI. Use `ChatColors` when you need a consistent set of `Attribute` values for things like timestamps, system messages, mentions, channel references, embeds and file/audio accents, or when you need to split a message into [`ChatSegment`](ChatSegment.cs.md)s that mark `@`-mentions and `#`-channel references for rendering.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ChatColors` centralizes the visual palette and simple parsing rules for chat rendering so callers don't duplicate color choices or regex logic. The static `Attribute` fields (for example `TimestampAttr`, `SystemAttr`, `MentionTextAttr`, `ChannelRefAttr`, `RailAttr`, `DateRuleAttr`, and `UnreadMarkerAttr`) are intended to be reused by rendering code. The `SplitMentions` method performs a two-pass split: it first extracts `@`-mentions (giving them `MentionTextAttr`) and then, only inside segments that were not already colored as mentions, highlights `#`-channel references with `ChannelRefAttr`. The regex helpers are implemented via `GeneratedRegex` methods (`MentionRegex` and `ChannelRefRegex`) so they are compiled at build time.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Split a message and inspect segments; mention and channel fragments receive attributes
|
||||||
|
var message = "Hey @alice, check #general and #123 -- also email [email protected]";
|
||||||
|
var segments = ChatColors.SplitMentions(message, defaultColor: null);
|
||||||
|
|
||||||
|
foreach (var seg in segments)
|
||||||
|
Console.WriteLine($"[{seg.Color}] {seg.Text}");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `SplitMentions` treats the optional `defaultColor` as the fallback attribute for non-special text; passing `null` means segment `Color` values may be `null` and the caller must handle that when rendering.
|
||||||
|
- The `MentionRegex` uses `(?<!\w)@...` to avoid matching emails, and the `ChannelRefRegex` requires at least one ASCII letter to avoid matching hex colors or bare numbers (so some international or non-ASCII usernames/channels may not match).
|
||||||
|
- Mentions take precedence: because `SplitMentions` colors `@`-matches in the first pass, any `#` inside an already-colored mention will not be reprocessed in the second pass.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A single visual line in the chat view composed of one or more colored [`ChatSegment`](ChatSegment.cs.md)s. Use `ChatLine` when preparing data for rendering or layout (wrapping, separators, attachments, and reply jump targets) rather than when working with raw message text; it carries both the display segments and metadata the view needs (attachment info, rule labels, continuation/indent hints, and navigation markers).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ChatLine` models what the UI actually renders: a sequence of colored segments in `Segments` plus a small set of rendering hints and metadata. It centralizes information the view needs for word-wrapping (`TextLength`, `Wrap`, `ContinuationIndent`, `ContinuationPrefixSegments`), special-line rendering (`RuleLabel`, `RuleAttr`, `IsUnreadMarker`), and attachment/interactivity (`AttachmentUrl`, `AttachmentFileName`, [`AttachmentKind`](../../../EchoHub.Core/Models/AttachmentKind.cs.md), `ActionSpans`). The `Wrap` method produces multiple `ChatLine` instances that fit a given column `width`, and `JumpToMessageId` links reply-quote lines back to their source message when present in the loaded history.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `TextLength` is computed once in the constructors (via `GetColumns()` on the provided text/segments). Because `Segments` is a mutable `List<ChatSegment>`, mutating `Segments` after construction will not update `TextLength`; keep them consistent or recreate the `ChatLine`.
|
||||||
|
- If `ContinuationPrefixSegments` is set it takes precedence over `ContinuationIndent` when computing the indent for continuation lines; the prefix's column width is used instead of the plain-space indent.
|
||||||
|
- `ActionSpans` columns are relative to the unwrapped line, so only the first wrapped line preserves clickable sub-line targets; setting `ActionSpans` to `null` means the whole line should use the kind's default action.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents an inclusive range of columns on a single chat line that, when clicked, triggers the given `AttachmentAction`. This lightweight, immutable value type pairs a `StartCol`, an `EndCol`, and an `AttachmentAction` to describe what should happen if a user interacts with that span during chat rendering or interaction handling.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This abstraction decouples the definition of clickable regions from the actions they perform, allowing the chat UI to map user interactions to behavior without embedding logic in the rendering layer. As a `readonly record struct`, it is cheap to copy and supports value-based equality, which makes it convenient to accumulate multiple spans in collections or pass them through APIs without risking unintended mutation. The actual interpretation of the `AttachmentAction` is delegated to higher-level components that handle click events, enabling reuse across different chat layouts or themes.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The range is inclusive; ensure `EndCol >= StartCol` before constructing an instance.
|
||||||
|
- Being a `readonly` record struct, instances are immutable; treat them as value-identity objects rather than mutable state.
|
||||||
|
- The spans should align with the chat line rendering coordinate space; changes in layout or font metrics may require revalidation of column mappings to avoid misaligned interactions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AttachmentAction
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatLine.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum AttachmentAction
|
||||||
|
{
|
||||||
|
OpenImage,
|
||||||
|
SaveImage,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the user action triggered by clicking an attachment line in the chat UI. It encodes the two currently supported outcomes for image attachments: opening the image for viewing or saving it to disk.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enumeration decouples the click-handler from concrete UI behavior, enabling a single dispatch to determine what to do with an attachment. It also makes future extension easier; adding new actions (for example, copying a link or sharing) would be done by extending this enum and updating the handlers accordingly.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
AttachmentAction action = AttachmentAction.OpenImage;
|
||||||
|
if (action == AttachmentAction.OpenImage)
|
||||||
|
{
|
||||||
|
// Open the image for viewing
|
||||||
|
}
|
||||||
|
else if (action == AttachmentAction.SaveImage)
|
||||||
|
{
|
||||||
|
// Persist the image to disk
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Adding new values requires revisiting all switch/if chains that enumerate the actions.
|
||||||
|
- Exhaustive checks are safer; consider a default fallback to surface unknown actions gracefully.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ChatListSource
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/ChatListSource.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ChatListSource : IListDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A list data source that stores [`ChatLine`](ChatLine.cs.md) instances and renders them into a UI list with per-segment coloring and mention highlighting. Reach for `ChatListSource` when you need an `IListDataSource` implementation that maintains chat-specific layout state (like `MaxItemLength`) and performs per-grapheme drawing of `ChatLine.Segments` so segment colors and mention backgrounds are respected during rendering.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ChatListSource` maintains an internal `List<ChatLine>` (`_lines`) and exposes simple mutation operations (`Add`, `AddRange`, `InsertRange`, `Clear`) while tracking the longest item in `MaxItemLength`. It raises `CollectionChanged` (unless `SuspendCollectionChangedEvent` is set) so UI consumers can refresh efficiently; `AddRange`/`InsertRange` and `Clear` invoke `RaiseCollectionChanged` only once after the batch operation. The `Render` implementation iterates each `ChatLine.Segments`, chooses an `Attribute` per segment (falling back to the list's `VisualRole.Normal` attribute or applying `ChatColors.MentionHighlightAttr.Background` when `ChatLine.IsMention`), and draws graphemes using `GraphemeHelper` while respecting `viewportX` and `width`. The class intentionally leaves `IsMarked`/`SetMark` as no-ops and has an empty `Dispose`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `MaxItemLength` is only increased when lines are added and reset only by `Clear`. There is no removal API that updates `MaxItemLength`, so it can become stale if items are removed or if existing `ChatLine.TextLength` values change externally.
|
||||||
|
- `GetLine(int)` returns `null` for out-of-range indexes, but `Render` accesses `_lines[item]` directly; callers must ensure the `item` index passed to `Render` is valid to avoid an `IndexOutOfRangeException`.
|
||||||
|
- Setting `SuspendCollectionChangedEvent` suppresses `CollectionChanged` invocations while mutations occur, but mutations still apply immediately to the internal list. Consumers that suppress events must ensure the UI is refreshed after re-enabling events (the next mutating call will raise `CollectionChanged` unless `SuspendCollectionChangedEvent` remains true).
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
# 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?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a colored piece of text within a chat line. It pairs the display text (`Text`) with an optional color styling (`Color`). As a `record`, it is an immutable, value-based container designed to be composed with other `ChatSegment`s to render a full message, applying `Color` when present; if `Color` is `null`, default styling is used.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By modeling a chat line as a sequence of `ChatSegment`s, the rendering layer can apply per-segment styling without mixing content and presentation logic. The `ChatSegment` uses a `record` to enable value-based equality, which helps with deduplication, testing, and change tracking when chat lines are built from multiple segments.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# RenderHelpers
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/RenderHelpers.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
static class RenderHelpers
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
RenderHelpers is a small static utility class that centralizes rendering concerns for `IListDataSource` implementations. It currently provides a single method, `WriteText`, which writes text to a `ListView` grapheme by grapheme, respecting a maximum width. It returns the updated drawn-columns count, enabling callers to track horizontal placement as multiple fields are rendered on a single line.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
RenderHelpers abstracts the grapheme-aware rendering logic so all list-rendering code shares the same boundary checks and column accounting. It couples the `GraphemeHelper.GetGraphemes` iteration with a safe width calculation, reducing the chance of off-by-one errors when composing UI rows. In short, it’s the single place responsible for safe, width-bound text rendering to a `ListView` in this UI layer.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The width of each grapheme is determined by `grapheme.GetColumns()`, clamped to at least 1 with `Math.Max(grapheme.GetColumns(), 1)`.
|
||||||
|
- Rendering stops when adding the next grapheme would exceed `maxWidth`; partial graphemes are not drawn.
|
||||||
|
- The method delegates actual drawing to `ListView.AddStr`, so callers should ensure the `ListView` state is appropriate for incremental writes.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# WelcomeBanner
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Chat/WelcomeBanner.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
internal static class WelcomeBanner
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `WelcomeBanner` class provides the MOTD-style splash shown in the chat pane when no channel is selected. It renders a gold-gradient ASCII logo by choosing between `BigLogo` (for wider viewports) and `SmallLogo` (for narrow panes), centers the logo within the given width, and appends a version tagline and quick-use hints. The static `Build` method returns a list of [`ChatLine`](ChatLine.cs.md) objects that the UI can render to display the branded welcome banner for a given `width` and `version` string.
|
||||||
|
|
||||||
|
The banner is designed to be self-contained: it composes ASCII art, a vertical color gradient (`Gradient`), and a small set of hints (`Hints`) into a sequence of renderable lines. This keeps the welcome experience consistent across sessions and isolates branding concerns from the main channel rendering logic.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# AudioPlayerDialog
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/AudioPlayerDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class AudioPlayerDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AudioPlayerDialog is a sealed UI helper that renders a compact, terminal-style audio player within the application. When `Show` is invoked, it builds a `Dialog` titled "Audio Player" containing a file name header, a wave visualization area, a status label, volume controls, and playback controls (Play, Stop, Close). It also orchestrates a simple block-wave animation using `WaveBlocks` and a timer to provide a visual indication of activity, while delegating actual playback logic to the provided [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
AudioPlayerDialog centralizes the presentation of audio playback in a terminal UI. It encapsulates the layout and styling (via `FileNameAttr`, `WaveIdleAttr`, and status attributes such as `StatusPlayingAttr`, `StatusPausedAttr`, and `StatusStoppedAttr`) so callers can surface audio without constructing the controls themselves. It collaborates with `IApplication` to host the dialog in the UI thread and with [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md) to reflect playback state and drive the actual audio logic while the dialog handles user interactions and visuals.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
AudioPlayerDialog.Show(app, audioService, "/path/to/song.mp3", "song.mp3");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The wave visualization relies on Unicode block characters from `WaveBlocks`; ensure the terminal/font supports these glyphs for proper rendering.
|
||||||
|
- The animation is driven by a timer using `AnimationIntervalMs`; changing the cadence affects how lively the waveform appears.
|
||||||
|
- The volume UI initializes with a local `currentVolume` and the wiring between the volume controls and [`AudioPlaybackService`](../../Services/AudioPlaybackService.cs.md) is not shown in the excerpt; connect changes to the service to affect real playback.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# ChannelPasswordDialog
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ChannelPasswordDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class ChannelPasswordDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ChannelPasswordDialog is a lightweight UI helper that prompts the user for the password required to join a password-protected channel. Its static `Show` method returns the entered password as a `string?`, or `null` if the user cancels, after presenting a small modal dialog built from `Dialog` with a channel-specific message (defaulting to `#{channelName} is password protected.`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Encapsulates the password-prompt UX for channel joins, avoiding duplication of UI logic across callers. The dialog wires up a password input and two actions: a join action that validates a non-empty password and a cancel action that returns `null`, ensuring the caller proceeds only after a password is provided or the user cancels. Providing a custom `message` lets callers tailor the prompt while preserving a consistent default behavior when none is supplied.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The call is synchronous and blocks until the user completes interaction with the dialog.
|
||||||
|
- The return value must be checked for `null` to distinguish between a canceled join and a provided password.
|
||||||
|
- The implementation relies on UI primitives (`Dialog`, `Label`, `Button`, `MessageBox`) and a password input field; ensure this is invoked on an appropriate UI thread context in your application.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# 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 dialog that gathers server connection and authentication information from the user. It optionally presents a Saved Servers list when available, and returns a `ConnectDialogResult?` when the user completes the form or null if cancelled.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating the authentication flow in a single dialog, `ConnectDialog` centralizes the user experience for establishing a server connection. It dynamically adapts its layout depending on whether [`SavedServer`](../../Config/ClientConfig.cs.md) entries are provided, showing a `ListView` of saved servers when present and keeping a compact form otherwise. It also treats credentials with care by redacting the password in the UI and indicating a saved session when a `RefreshToken` exists.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If saved servers exist, the dialog height increases to accommodate the list (24 vs 20).
|
||||||
|
- The Saved Servers display shows items built from saved server properties; a session indicator is appended when `RefreshToken` is non-empty.
|
||||||
|
- The password field is displayed as `[REDACTED:PASSWORD]` and the actual input is masked via the `Secret` flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 is an immutable data container produced by the connect dialog, encapsulating the user's input as a single value object for the subsequent connection/authentication workflow. It carries the server URL (`ServerUrl`), the user's credentials (`Username`, `Password`), and UI preferences (`IsRegister`, `RememberMe`), along with an optional `SavedRefreshToken` and possibly `DisplayName` or [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Using a `record` here provides value-based equality and convenient deconstruction, making it easy to compare results and pass them through layers without mutating state. It serves as a boundary-crossing DTO that formats UI input into a coherent package for the authentication/service layer, while supporting optional flows via `DisplayName` and [`InviteCode`](../../../EchoHub.Core/Models/InviteCode.cs.md). Because `Password` and `SavedRefreshToken` can contain sensitive data, avoid logging them and handle this object as transient UI data rather than a durable model.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log or persist the `Password` or `SavedRefreshToken` values; treat them as sensitive data.
|
||||||
|
- This object is intended to be transient UI input; avoid storing it longer than necessary or serializing it insecurely.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
CreateChannelDialog.Show renders a modal 'Create Channel' dialog via the supplied `IApplication`, collecting a channel `name`, an optional `topic`, and an optional `password`, validating inputs, and returning a `CreateChannelResult` when the user confirms, or `null` if canceled. The entered `name` is trimmed and converted to lowercase; the `topic` is optional, and a blank `password` yields a `null` password in the result.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating the dialog in a single static entry point, this symbol isolates the UI workflow from callers and centralizes its validations and layout. It coordinates several UI components (`Dialog`, `Label`, `TextField`, `Button`) and user input handling so that changes to the channel-creation UX don't ripple through the rest of the codebase.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Name normalization: the code lowercases and trims the input before use; beware that the original casing is not preserved in the result.
|
||||||
|
- Password handling: the password is optional; if left blank, the resulting `password` becomes `null`.
|
||||||
|
- Redacted password placeholder: the label uses a redacted placeholder `[REDACTED:CONNECTION_STRING_PASSWORD]`, indicating the actual password source isn't visible in the snippet; ensure the real value is supplied by the surrounding application context.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the outcome of a channel-creation operation in the UI. The `CreateChannelResult` type carries the channel's `Name`, an optional `Topic`, a boolean `IsPublic` indicating if the channel is public, and an optional `Password` for password-protected channels, enabling downstream UI logic to respond to the created channel.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# 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 is a Terminal.Gui dialog that presents a compact, form-based UI for editing a user's profile, including `Display Name`, `Bio`, and `Nickname Color`, with live color preview and an optional avatar picker. When invoked via `Show`, it pre-fills fields from the provided current values and returns a `ProfileEditResult?` when the user confirms, or `null` if the operation is cancelled. This component is intended to be used whenever your application needs an in-app, consistent way to collect profile updates from the user.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ProfileEditDialog isolates the profile-edit UX from the rest of the application, providing a single reusable route for updating these fields. It delegates color parsing to [`HexColorHelper`](../Helpers/HexColorHelper.cs.md) (e.g. `ParseHexColor`/`ParseHexToColor`) so the dialog itself remains focused on presentation and interaction. The color preview is updated in real time by wiring the `TextChanged` event on the `colorField` to `UpdateColorPreview`. The avatar picker uses an `OpenDialog` invoked through the Browse button, illustrating how file selection is integrated into a TUI form.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var result = ProfileEditDialog.Show(app, currentDisplayName, currentBio, currentColor, notificationSoundEnabled: true, notificationVolume: 50);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
// Use result to apply the edited profile values
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Color parsing is performed via [`HexColorHelper`](../Helpers/HexColorHelper.cs.md) to translate the user-entered hex string into a `Color` for the live preview; invalid inputs fall back to a safe color preview.
|
||||||
|
- The avatar field is optional; leaving it empty means no avatar is selected.
|
||||||
|
- The dialog uses a fixed size of 60x26, so ensure your terminal window can accommodate this layout to avoid clipping or overflow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 when the user finishes editing their profile in the dialog. It carries the proposed updates to `DisplayName`, `Bio`, `NicknameColor`, `AvatarPath`, and notification settings (`NotificationSoundEnabled`, `NotificationVolume`). Because all fields are nullable, callers can distinguish between fields the user left unchanged and fields the user explicitly updated, enabling partial updates to the profile.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ProfileEditResult serves as a lightweight, immutable carrier that isolates UI concerns from the underlying profile update logic. It provides a snapshot of the user's edits at dialog closure, which the caller then applies to the profile as needed. The use of nullable members communicates optional edits clearly and avoids forcing changes for fields the user did not touch.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Interpret any null value as 'no change' for that field when applying updates to the actual profile.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# 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 encapsulates the UI for inspecting a user's server profile in a terminal-style dialog. It renders a read-only view when displaying another user, and when shown for the current user via `ShowOwn`, it includes action buttons (edit profile and set status) and returns the chosen `ProfileAction`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Internally, `Show` delegates to `ShowInternal` with `isOwnProfile` set to false, while `ShowOwn` passes `isOwnProfile` true along with the current status and message. The dialog is constructed as a `Dialog` with title `My Profile` or `Profile — {profile.Username}`, and it populates rows for `Username`, `Name`, `Status`, [`Message`](../../../EchoHub.Core/Models/Message.cs.md) (when present), `Color`, and `Bio` using `Label`s and a `TextView`. The status value is chosen as the live status when viewing your own profile, otherwise the stored status from the profile; the status text is produced by `FormatStatus` and the color by `GetStatusColor`. The nickname color is parsed via `HexColorHelper.ParseHexColor` and applied as a scheme to the color label when available. If the provided `profile` is `null`, it shows an error dialog with `MessageBox.ErrorQuery` and returns `ProfileAction.Close`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If `profile` is `null`, the dialog informs the user and returns `ProfileAction.Close`, signaling callers to handle the absence gracefully.
|
||||||
|
- The nickname color is applied only when `HexColorHelper.ParseHexColor(profile.NicknameColor)` yields a valid color attribute; otherwise the color styling is skipped, avoiding exceptions.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ProfileAction
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/ProfileViewDialog.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum ProfileAction
|
||||||
|
{
|
||||||
|
Close,
|
||||||
|
EditProfile,
|
||||||
|
SetStatus
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `ProfileAction` enum encodes the concrete actions a user selects from their profile dialog. Its values `Close`, `EditProfile`, and `SetStatus` map user intent to distinct application paths, replacing ad-hoc strings with a strongly-typed signal. Consumers use this enum in the dialog result handling to drive navigation and state changes without inspecting UI text.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Source Code
|
||||||
|
Static class `SearchDialog` provides a Ctrl+K-activated, command-palette style dialog for navigating channels and triggering app actions. It merges the current `IReadOnlyList<string>` of `channels` with a fixed set of default `SearchResult` actions into a single searchable list presented in a `Dialog` consisting of a `Label` hint, a `TextField` input, and a `ListView` of results; typing filters the list and Enter selects. The `Show` method returns the selected `SearchResult` or `null` if canceled, communicating through the provided `IApplication` instance.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing both channels and common actions, `SearchDialog` reduces context switching and speeds navigation from anywhere in the UI. The implementation delegates list rendering and filtering to [`SearchListSource`](../ListSources/SearchListSource.cs.md), decoupling the data shape from the presentation; adding new channels or actions simply extends the default actions or the input channels without altering the UI flow.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The dialog binds Ctrl+K to stop the dialog, so avoid conflicting hotkeys in the surrounding application.
|
||||||
|
|
||||||
|
## Dependency APIs (verified signatures)
|
||||||
|
The REAL, parser-verified API surface of this symbol's collaborators:
|
||||||
|
|
||||||
|
- record `SearchResult` (`src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`)
|
||||||
|
- class [`SearchListSource`](../ListSources/SearchListSource.cs.md) (`src/EchoHub.Client/UI/ListSources/SearchListSource.cs`)
|
||||||
|
- field `Attribute ChannelAttribute`
|
||||||
|
- field `Attribute ActionAttribute`
|
||||||
|
- property `int Count`
|
||||||
|
- property `int MaxItemLength`
|
||||||
|
- property `bool SuspendCollectionChangedEvent`
|
||||||
|
- `void Filter(string query)`
|
||||||
|
- `SearchResult? GetItem(int index)`
|
||||||
|
- `bool IsMarked(int item)`
|
||||||
|
- `void SetMark(int item, bool value)`
|
||||||
|
- `IList ToList()`
|
||||||
|
- `void Render(ListView listView, bool selected, int item, int col, int row, int width, int viewportX)`
|
||||||
|
- `void Dispose()`
|
||||||
|
- enum `SearchResultType` (`src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`)
|
||||||
|
|
||||||
|
## Symbol To Document
|
||||||
|
- Name: `SearchDialog`
|
||||||
|
- Kind: class
|
||||||
|
- File: `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
- Language: `csharp`
|
||||||
|
- ID: `7ba458ca-8e14-48c9-9536-988f98e9e83c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 item in search results as an immutable, value-based carrier. It groups the result kind (`SearchResultType`), an identifying `Key`, and a user-facing `Label` to display in the UI. As a `record`, it gains structural equality and convenient deconstruction, which makes it easy to compare results and extract its fields when handling selections in the search dialog.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This type serves as a stable data contract between the search logic and the UI layer, decoupling data shape from presentation. It uses `record` semantics to provide value equality and immutability, enabling straightforward deduplication and pattern-based handling of results. The three members (`Type`, `Key`, `Label`) collectively support both programmatic lookup and user-friendly rendering.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `Key` should be stable and unique within a given `Type` to avoid ambiguity when presenting or selecting results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SearchResultType
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/SearchDialog.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum SearchResultType
|
||||||
|
{
|
||||||
|
Channel,
|
||||||
|
Action
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the kind of item produced by a search in the UI, distinguishing [`Channel`](../../../EchoHub.Core/Models/Channel.cs.md) results from `Action` results. Use `SearchResultType` when rendering or handling search results in the `SearchDialog` flow to steer UI decisions without inspecting the raw payload.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enum centralizes the UI's categorization of search results, enabling the dialog to select icons, labels, or handlers in a type-safe way. It decouples the results' payload from how they're displayed and makes it straightforward to extend with additional result kinds in the future.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
SearchResultType type = SearchResultType.Channel;
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case SearchResultType.Channel:
|
||||||
|
Console.WriteLine("Render as channel");
|
||||||
|
break;
|
||||||
|
case SearchResultType.Action:
|
||||||
|
Console.WriteLine("Render as action");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 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.Gui-based dialog that enables a user to set their [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) and an optional status message. The static `Show` method displays the dialog within an `IApplication`, initializes the controls from `currentStatus` and `currentMessage`, and returns a `StatusDialogResult` when the user saves, or `null` if the dialog is cancelled.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
StatusDialog serves as a focused UI primitive that isolates status-edit behavior from the rest of the application. By wiring `OptionSelector<UserStatus>` and a `TextField` to a lightweight `StatusDialogResult`, it provides a predictable, reusable pattern for collecting user input and converting it to a simple value object. This keeps the UI code cohesive while allowing the caller to handle the result without managing Terminal.Gui lifecycle details. The dialog is deliberately minimal and self-contained, relying on the provided `IApplication` to control its lifecycle.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `message` field is trimmed and, if empty or whitespace, stored as `null`.
|
||||||
|
- Cancelling returns `null` and no `StatusDialogResult` is produced.
|
||||||
|
- When saving, if the selected status is `null`, it defaults to `UserStatus.Online`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 lightweight value object that represents the outcome of the status dialog. It pairs the chosen [`UserStatus`](../../../EchoHub.Core/Models/UserStatus.cs.md) with an optional `StatusMessage`, providing a simple, transportable result for the caller to inspect and react to.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a `record`, it uses value semantics: two instances are equal if their `Status` and `StatusMessage` are equal, and it is immutable by design. This makes it ideal for passing the result across boundaries and for use in pattern matching or switch expressions when reacting to different statuses. The `StatusMessage` is nullable to allow callers to omit extra context when not needed.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# UpdateConfirmDialog
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Dialogs/UpdateConfirmDialog.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class UpdateConfirmDialog
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
UpdateConfirmDialog is a small, self-contained UI helper that presents a modal update prompt and returns the user's decision as a boolean. Call `UpdateConfirmDialog.Show` with an `IApplication` and the current and latest versions; it constructs a `Dialog` titled 'Update Available' containing a `Label` with the version message and two `Button`s, runs the dialog, and returns `true` when the user chooses to perform the update.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By encapsulating the entire dialog flow, this symbol isolates the update-confirmation UX from the rest of the UI, reducing duplication across the codebase. The modal pattern—calling `app.Run(dialog)` followed by `app.RequestStop()`—ensures callers receive the result synchronously without needing to manage focus or window lifecycles themselves. It also makes testing easier by providing a single, predictable entry point for the confirmation action.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The dialog is modal and blocks until the user presses `Update` or `Cancel`; callers should not attempt to perform further UI work until after `Show` returns.
|
||||||
|
- It interpolates `currentVersion` and `newVersion` into the message; ensure these values are safe to display and do not contain unexpected control characters.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# DroppedFileParser
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Helpers/DroppedFileParser.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class DroppedFileParser
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
DroppedFileParser exposes a small, focused set of helpers for recognizing and extracting absolute file path(s) from terminal input that arrives via drag-and-drop. It understands common path forms (quoted text, Windows drive-letter paths like `X:\`, UNC paths like `\\server\share`, and POSIX absolute paths starting with `/`) and uses a cheap pre-check (`LooksLikePath`) to avoid filesystem access unless the input plausibly contains a path. The primary entry point, `TryGetFiles`, returns true when the input resolves to one or more existing files and returns the discovered paths in the `files` out parameter; it supports a single path (quoted or not) or multiple space-separated tokens (each optionally quoted) and lets callers inject a `fileExists` predicate for testability (defaults to `File.Exists`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
DropppedFileParser centralizes the path-detection logic that UI input handlers would otherwise duplicate, simplifying callers and reducing unnecessary file-system work. `LooksLikePath` provides a fast-path signal so the expensive existence check runs only when the input plausibly represents a path, while `TryGetFiles` performs the actual existence checks and returns the concrete file list. The API supports both single-path and multi-path inputs, correctly handling spaces inside quoted paths by tokenizing tokens and stripping surrounding quotes where applicable; it enforces that all tokens are fully-qualified and existing, otherwise the call fails. The `fileExists` parameter makes unit tests deterministic by allowing injection of a fake predicate instead of touching the real filesystem.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Relative paths are not accepted by `TryGetFiles`; it requires fully-qualified paths for each token (and for single-path input).
|
||||||
|
- Quote handling is strict: [`StripQuotes`](../../Commands/CommandHandler.cs.md) removes matching leading/trailing quotes only when both ends use the same quote character; mismatched quotes may leave quotes in the token and affect parsing.
|
||||||
|
- For testing, pass a custom `fileExists` delegate to avoid real I/O; otherwise the default uses `File.Exists`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# EmojiHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Helpers/EmojiHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class EmojiHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
EmojiHelper is a static utility that converts emoji grapheme clusters in a string into text shortcodes for safe rendering in terminal-based UIs. It scans input text, splits it into grapheme elements, and replaces any grapheme containing emoji with a corresponding shortcode from `EmojiShortcodes`; if no mapping exists for the full grapheme, it attempts the base emoji (the first rune) and uses its shortcode; if that also fails, it inserts a generic `[emoji]` placeholder. Non-emoji text passes through unchanged. This approach avoids inconsistent emoji rendering across terminals by providing fixed-width ASCII representations for display-only outputs.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
EmojiHelper centralizes the emoji-to-shortcode conversion, isolating terminal rendering concerns from application logic. It relies on `EmojiShortcodes` for mapping and uses `StringInfo.GetTextElementEnumerator` to respect grapheme boundaries, ensuring sequences like complex emoji are treated coherently. The abstraction keeps emoji translation testable and swapable, so you can adjust shortcodes without touching UI code.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Unknown emoji yields a generic `[emoji]` placeholder; ensure `EmojiShortcodes` covers targets or plan fallback behavior.
|
||||||
|
- Emoji detection uses a set of Unicode ranges to decide whether a grapheme contains emoji; new or platform-specific emoji outside these ranges may be missed.
|
||||||
|
- This replacement is intended for display only; do not rely on reversibility for data persistence, and be aware that updates to `EmojiShortcodes` may change outputs.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# HexColorHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Helpers/HexColorHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class HexColorHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
HexColorHelper is a small static utility that converts hex color strings into Terminal.Gui color representations. Use `ParseHexColor` when you need an `Attribute` for immediate application to a UI element, and `ParseHexToColor` when you only need the `Color` value (with an optional `fallback`) for other color-related properties.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
These helpers centralize hex parsing to ensure consistent handling of hex colors across the UI layer. They both tolerate the common '#'-prefixed form and treat invalid inputs gracefully by returning null or a fallback color, preventing exceptions from propagating into UI code. By encapsulating parsing logic here, you avoid duplicating string-to-color conversions and make future changes (e.g., supporting shorthand hex) easier.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Leading whitespace before the optional '#' is not trimmed; strings starting with spaces will fail to parse gracefully.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# NickColorHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/Helpers/NickColorHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class NickColorHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
NickColorHelper deterministically maps a nickname to a color attribute for users who haven't picked a nickname color. The same nick always maps to the same palette entry (classic IRC client behavior), so a busy channel stays scannable without any configuration. Use `GetAttribute(string nick)` to obtain the color `Attribute` to apply to UI elements, with the color chosen from a fixed `Palette` in a deterministic way. The helper is a pure function (no Terminal.Gui types) so it is easy to unit-test without a display driver.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ChannelListSource
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/ListSources/ChannelListSource.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ChannelListSource : IListDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A specialized `IListDataSource` implementation that provides a colored, badge-capable channel list for the UI. Use `ChannelListSource` when you need a channel list that shows an active indicator, unread count badges, and visual differences for protected, private, mention, and system channels; call `Update` to replace the source data and rely on the `CollectionChanged` event to refresh the view.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ChannelListSource` centralizes both the model and the presentation hints required to render a channel list: it stores the channel names (`_channelNames`), per-channel unread counts (`_unreadCounts`), categorical sets (`_protectedChannels`, `_mentionChannels`, `_privateChannels`, `_systemChannels`), and the `_activeChannel`. Visual presentation is driven by a small set of static attributes (`ActiveAttr`, `UnreadAttr`, `NormalAttr`, `BadgeAttr`, `MentionAttr`, `SystemAttr`) and the `Render` method composes the line prefix and decorations (active marker, system rule, protection/private markers, unread badge) before drawing to the provided `ListView`. The `Update` method replaces the internal collections, recomputes `MaxItemLength` (uses `channels.Max(c => c.Length + 6)` as a conservative width heuristic), and raises a `NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)` via the `CollectionChanged` event unless `SuspendCollectionChangedEvent` is set.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `ChannelListSource` is not synchronized: internal collections are not thread-safe. Callers must ensure updates happen on the UI thread or otherwise synchronize access to avoid races.
|
||||||
|
- Set `SuspendCollectionChangedEvent` to `true` to suppress the reset notification during bulk updates; remember to re-enable it if callers rely on the `CollectionChanged` event for redraws.
|
||||||
|
- The `IsMarked` and `SetMark` implementations are no-ops, so the `IListDataSource` marking contract is not supported by this source; consumers expecting persisted item marks will not get them from `ChannelListSource`.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# SearchListSource
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/ListSources/SearchListSource.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class SearchListSource(List<SearchResult> items) : IListDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A list-data source implementation used by the search dialog that presents a filtered view of [`SearchResult`](../Dialogs/SearchDialog.cs.md) items and renders each entry with type-specific coloring. Use `SearchListSource` when you need a lightweight, read-only collection for a `ListView` that supports text filtering via `Filter` and per-item rendering via `Render`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`SearchListSource` holds the full set of items in `_allItems` and maintains a filtered snapshot in `_filtered` that drives `Count`, `MaxItemLength`, `GetItem`, and `ToList`. Filtering is performed by `Filter` using `StringComparison.OrdinalIgnoreCase` against both the `Label` and `Key` of each [`SearchResult`](../Dialogs/SearchDialog.cs.md). Rendering delegates text layout to `RenderHelpers.WriteText` and chooses visual attributes based on the [`SearchResultType`](../Dialogs/SearchDialog.cs.md) (using `ChannelAttribute` and `ActionAttribute`); when a chosen attribute has no background color it inherits the `ListView` fill background so the entry blends with the surrounding cells. The `CollectionChanged` event is raised with a `NotifyCollectionChangedEventArgs` reset after `Filter` updates unless `SuspendCollectionChangedEvent` is set.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Render` indexes into `_filtered` directly and assumes the caller supplies a valid `item` index; callers should use `Count` or `GetItem` to validate indices to avoid out-of-range access.
|
||||||
|
- `IsMarked` and `SetMark` are intentionally no-ops: this source does not track per-item marks, so code that expects marking behavior will need a wrapper or a different `IListDataSource` implementation.
|
||||||
|
- `Dispose` is a no-op; there are no native resources held by `SearchListSource`, but consumers that expect disposal semantics should be aware nothing is released by calling `Dispose`.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# UserListSource
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Client/UI/ListSources/UserListSource.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class UserListSource : IListDataSource
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Custom list data source used to render the online users panel where each user's nickname can be shown in a per-user color. Use `UserListSource` when you need a simple, read-only data source that supplies visible text, optional nickname coloring via `Attribute? NameColor`, and username lookup for a `ListView`-style UI; it encapsulates how items are drawn and when the list notifies listeners of wholesale changes.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`UserListSource` stores a list of tuples of the shape `(string Text, Attribute? NameColor, string Username)` and exposes that collection through the `IListDataSource` contract: `Count`, `ToList()`, the `CollectionChanged` event and `Render(...)`. The `Update(...)` method replaces the entire internal list, recomputes `MaxItemLength` using each item's `Text.GetColumns()`, and raises a single `NotifyCollectionChangedAction.Reset` notification unless `SuspendCollectionChangedEvent` is set. Rendering is handled by `Render(...)`: it asks `GraphemeHelper.GetGraphemes(...)` for grapheme clusters, finds where the visible username starts (skipping a leading status icon and optional role badge), draws the prefix in the normal attribute and the username in the per-user `NameColor` (unless the row is `selected`), and fills the remainder of the requested `width` with spaces.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Update(...)` replaces the entire backing list and always fires a `Reset` change notification (not incremental add/remove events). Consumers that rely on fine-grained collection changes should account for that.
|
||||||
|
- `SuspendCollectionChangedEvent` prevents `Update(...)` from raising `CollectionChanged`. This is a simple way to batch updates, but callers are responsible for firing or forcing a refresh later if needed.
|
||||||
|
- `IsMarked(...)` and `SetMark(...)` are no-ops; `UserListSource` does not track per-item marks. Callers expecting mark semantics must manage marks externally.
|
||||||
|
- `GetUsername(...)` returns `null` when `index` is out of range; callers should check for `null` before using the result.
|
||||||
|
- `MaxItemLength` is computed from `Text.GetColumns()` for each item; it reflects display column width rather than raw `string.Length` and becomes `0` when the source is empty.
|
||||||
|
- `Render(...)` uses `GraphemeHelper.GetGraphemes(...)` and per-grapheme `GetColumns()` calls and will truncate output when `drawnChars + cols > width`. This ensures column-consistent drawing for wide or combining characters but may be relatively expensive if called frequently — consider caching grapheme data or avoiding per-frame allocations if rendering many items each frame.
|
||||||
|
- When `selected` is `true`, the code uses the `Focus`/`Normal` role mapping (`normalAttr`) for both prefix and username; the `NameColor` is ignored while selected. This is an intentional styling choice but may surprise callers who expect nickname coloring even for selected rows.
|
||||||
|
- `Dispose()` is empty; there are no unmanaged resources to free. The class is not explicitly thread-safe — concurrent calls to `Update(...)` and `Render(...)` without external synchronization may race.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
|||||||
|
# HubConstants
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Constants/HubConstants.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class HubConstants
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
HubConstants acts as the single source of truth for the chat hub’s configurable limits and defaults. It groups static, compile-time constants that govern where the hub is exposed, how sessions are identified (including the IRC gateway prefix), and the upper bounds for messages, attachments, avatars, and embeds, providing a centralized reference that other components consult for validation and formatting.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
HubConstants isolates cross-cutting numerical constraints from business logic, ensuring all parts of the EchoHub system enforce the same rules. It enables tuning by operators—e.g., increasing `MaxMessageLength` or `MaxAttachmentsPerMessage`—without altering core workflows, while the IRC connection-id prefix helps the presence tracker distinguish IRC-based clients from native ones. The constants also centralize embed sizing and fetch behavior to maintain predictable link previews and resource usage across gateways and clients.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Validate message length against hub-wide limit
|
||||||
|
if (message.Text.Length > HubConstants.MaxMessageLength)
|
||||||
|
{
|
||||||
|
// handle too long
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the path for the chat hub
|
||||||
|
var hubPath = HubConstants.ChatHubPath;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- They are compile-time constants (const) and thus require a recompilation to change; runtime configuration is not supported.
|
||||||
|
- Changes to these values reflect architectural expectations across components (UI, gateway, presence tracker, and embeds) and should be coordinated to avoid breaking client assumptions.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# MessageConventions
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Constants/MessageConventions.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class MessageConventions
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Cross-protocol message conventions for action messages. Action messages (the /me style) are stored using the IRC CTCP ACTION wire format: a 0x01 prefix, the literal string `ACTION `, the text, and a trailing 0x01 suffix. This class exposes the constants `ActionPrefix` and `ActionSuffix`, plus helpers `FormatAction` and `TryParseAction` to wrap and unwrap the action text, ensuring consistent storage, rendering, and encryption behavior.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ActionConventions centralize the wire-format markers so changes in one place don't ripple through callers, and to provide a clear boundary between encoding and decoding of action messages. `FormatAction` encapsulates the exact wrapper, while `TryParseAction` validates the pattern and extracts the inner text without exposing the wire markers to callers. This avoids scattering the CTCP formatting details throughout the codebase and keeps rendering logic aligned with storage format.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
string content = MessageConventions.FormatAction("waves");
|
||||||
|
bool ok = MessageConventions.TryParseAction(content, out var actionText);
|
||||||
|
// ok == true, actionText == "waves"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `TryParseAction` requires the content to start with `ActionPrefix`, end with `ActionSuffix`, and have non-empty inner text; otherwise it returns false and sets `actionText` to null.
|
||||||
|
- The implementation uses ordinal comparisons to check the markers for performance and culture-invariant behavior.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ValidationConstants
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Constants/ValidationConstants.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static partial class ValidationConstants
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ValidationConstants is a centralized repository of validation rules used across the codebase. It defines the canonical pattern strings for usernames, channel names, and hex colors, together with numeric bounds for various user-facing fields. Specifically, it exposes the strings `UsernamePattern`, `ChannelNamePattern`, `HexColorPattern`, and several limit constants such as `MaxPasswordLength`, `MinChannelPasswordLength`, `MaxDisplayNameLength`, `MaxBioLength`, `MaxStatusMessageLength`, `MaxChannelTopicLength`, and `MaxHistoryCount`. In addition, it provides precompiled Regex accessors via the `GeneratedRegex`-decorated methods `UsernameRegex()`, `ChannelNameRegex()`, and `HexColorRegex()`, enabling fast, centralized validation without scattering literal patterns across call sites.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing these constraints, `ValidationConstants` minimizes drift in validation rules across features (sign-up, profile updates, channel creation, etc.) and makes it easy to update rules in one place. The `UsernameRegex()`, `ChannelNameRegex()`, and `HexColorRegex()` methods are generated at compile time by the `GeneratedRegex` attribute, which yields ready-to-use, presumably cached `Regex` instances, reducing runtime regex compilation overhead at validation points.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- GeneratedRegex-based accessors rely on C# source generation; ensure your project enables source generators and targets a compatible framework, otherwise these methods may not be produced.
|
||||||
|
- The constants define the canonical validation boundaries pharmacologically used by the system; changing them updates all consumers that reference these values.
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# 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"] -->|"GetChannelsAsync(userId, offset, limit)"| PaginatedResponse["Build PaginatedResponse of ChannelDto"]
|
||||||
|
PaginatedResponse -->|"items: ChannelDto"| ChannelDto["Map DB rows to ChannelDto"]
|
||||||
|
IChannelService -->|"CreateChannelAsync(creatorUserId, name, topic, isPublic, password?, encryptionSalt?, wrappedRoomKey?)"| Channel["Create Channel record"]
|
||||||
|
Channel -->|"return"| ChannelOperationResult["ChannelOperationResult (success/error)"]
|
||||||
|
IChannelService -->|"UpdateTopicAsync(callerUserId, channelName, topic?)"| ChannelOperationResult
|
||||||
|
IChannelService -->|"SetChannelPasswordAsync(callerUserId, channelName, password?)"| ChannelOperationResult
|
||||||
|
IChannelService -->|"RekeyChannelAsync(callerUserId, channelName, oldPassword, newPassword, newEncryptionSalt, newWrappedRoomKey)"| ChannelCryptoDto["Update encryptionSalt and wrappedRoomKey"]
|
||||||
|
ChannelCryptoDto -->|"return"| ChannelOperationResult
|
||||||
|
IChannelService -->|"DeleteChannelAsync(callerUserId, channelName)"| ChannelOperationResult
|
||||||
|
IChannelService -->|"GetChannelByNameAsync(channelName)"| ChannelDto
|
||||||
|
IChannelService -->|"GetChannelMetaAsync(channelName)"| ChannelMetaDto
|
||||||
|
IChannelService -->|"GetChannelCryptoAsync(channelName)"| ChannelCryptoDto
|
||||||
|
IChannelService -->|"GetChannelKeyEnvelopeAsync(channelName) -> (EncryptionSalt, WrappedRoomKey)"| ChannelCryptoDto
|
||||||
|
IChannelService -->|"GetChannelTopicAsync(channelName) -> (Topic, Exists)"| ChannelMetaDto
|
||||||
|
IChannelService -->|"GetChannelListAsync()"| ChannelListItem["Return list of ChannelListItem"]
|
||||||
|
IChannelService -->|"EnsureChannelMembershipAsync(userId, channelName, password?) -> (Success, Error, PasswordRequired)"| ChannelOperationResult
|
||||||
|
IChannelService -->|"EnsureSystemChannelAsync(channelName, topic?)"| Channel["Create or reclaim system Channel"]
|
||||||
|
Channel -->|"return ChannelDto"| ChannelDto
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [IChannelService](#ichannelservice)
|
||||||
|
- [ChannelListItem](#channellistitem)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IChannelService
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IChannelService.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IChannelService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Provides the canonical server-side API for creating, querying, updating, and deleting chat channels and for enforcing membership and channel-level security. Use `IChannelService` when implementing application logic that needs to manage channel lifecycle (CRUD), inspect channel metadata or crypto information, handle membership checks (including password-protected rooms), or ensure server-owned system channels exist and cannot be hijacked by user-created channels.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IChannelService` centralizes channel-related policy and state so higher-level features (e.g. connection/auth layers, hub message routing, admin tools) can treat channel management as a single abstraction. It separates responsibilities: CRUD and topic/password operations return a [`ChannelOperationResult`](../DTOs/CommonDtos.cs.md) that callers must inspect (via `ChannelOperationResult.IsSuccess`) while read-only queries (e.g. [`GetChannelByNameAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `GetChannelMetaAsync`, `GetChannelCryptoAsync`) let callers obtain DTO representations. Crypto and key-envelope methods (`GetChannelCryptoAsync`, [`GetChannelKeyEnvelopeAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`) keep cryptographic metadata operations colocated with channel lifecycle logic. The [`EnsureSystemChannelAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) method is intentionally server-managed: it creates missing system channels and reclaims any same-named user-owned channels so server content is never stored in a user-controlled room.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// create a public channel and then fetch its DTO if creation succeeded
|
||||||
|
var result = await channelService.CreateChannelAsync(creatorUserId, "general", "General chat", isPublic: true);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
var channel = await channelService.GetChannelByNameAsync("general");
|
||||||
|
// use 'channel' (type: ChannelDto) for further operations
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// handle failure (inspect result for details provided by the implementation)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Methods that return [`ChannelOperationResult`](../DTOs/CommonDtos.cs.md) (for example `CreateChannelAsync`, [`UpdateTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), [`SetChannelPasswordAsync`](../../EchoHub.Server/Services/ChannelService.cs.md), `RekeyChannelAsync`, `DeleteChannelAsync`) must have their `ChannelOperationResult.IsSuccess` checked before assuming the operation succeeded. Do not assume a returned DTO exists unless the operation reports success.
|
||||||
|
- Several parameters are nullable (`topic`, `password`, `encryptionSalt`, `wrappedRoomKey`); callers should explicitly pass `null` when no value is intended and be prepared for implementations to treat `null` as "no value" or as an instruction to remove/clear a setting (verify service semantics for your deployment).
|
||||||
|
- [`GetChannelTopicAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) returns `(string? Topic, bool Exists)` — a `null` `Topic` can mean either an empty topic or that no topic was set; check `Exists` to distinguish a non-existent channel from a channel with a `null` topic.
|
||||||
|
- [`EnsureChannelMembershipAsync`](../../EchoHub.Server/Services/ChannelService.cs.md) returns a tuple including `PasswordRequired`; if `PasswordRequired` is `true`, callers should prompt for and supply a password on subsequent calls. The `Error` element may contain implementation-specific failure information.
|
||||||
|
- `GetChannelsAsync` accepts `offset` and `limit` for pagination; callers are responsible for passing sensible bounds and handling potentially large result sets incrementally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 object that describes a single channel in a channel list. It carries the channel's display name (`Name`), an optional topic (`Topic`), the number of online users (`OnlineCount`), and visibility flags (`IsPublic` and `IsProtected`). As a `record`, it provides value-based equality and straightforward construction for transport or UI scenarios, with `IsPublic` defaulting to true and `IsProtected` defaulting to false.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The use of a `record` signals that this is a lightweight value object intended for transport and comparison across boundaries. It models channel metadata as a single, cohesive unit, aiding deduplication and consistent rendering in lists or API responses.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var item = new ChannelListItem("general", "General discussion", 12);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Topic may be null to indicate no topic is set.
|
||||||
|
- IsPublic defaults to true and IsProtected defaults to false; pass explicit values to override.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# IChatBroadcaster
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IChatBroadcaster.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IChatBroadcaster
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A transport-agnostic abstraction for broadcasting chat events and presence changes to connected clients. Use `IChatBroadcaster` whenever server-side code (for example a hub, worker, or command handler) needs to notify one or more clients about messages, presence updates, channel lifecycle events, moderation actions, or errors without depending on a specific delivery mechanism.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IChatBroadcaster` centralizes all outgoing chat-related notifications so callers do not need to know or implement the delivery/fan-out semantics. Each method maps to a well-defined event type: `SendMessageToChannelAsync` for chat messages, `SendUserJoinedAsync` / `SendUserLeftAsync` for presence changes, `SendChannelUpdatedAsync` / `SendChannelDeletedAsync` / `SendChannelNukedAsync` for channel lifecycle, moderation actions via `SendUserKickedAsync` / `SendUserBannedAsync`, and utility operations such as `SendMessageDeletedAsync`, `SendUserStatusChangedAsync`, `SendErrorAsync`, and `ForceDisconnectUserAsync` for forced disconnects. The interface is asynchronous (`Task`-based) so implementations can perform non-blocking I/O, retries, batching, or use different transports (for example SignalR, WebSockets, or a message bus) without changing callers. The `excludeConnectionId` parameter on message/presence methods encodes the common IRC convention of not echoing a message back to the originating connection while still delivering it to other connections belonging to the same user.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `excludeConnectionId` prevents delivery only to the specified connection; other connections for the same user still receive the event. Callers should pass the sending connection id to avoid echoing to that connection but should not rely on it to suppress notifications to other sessions of the same user.
|
||||||
|
- `SendChannelUpdatedAsync` includes an optional `channelName` parameter in addition to the [`ChannelDto`](../DTOs/ChatDtos.cs.md). The intent of the optional `channelName` (for example: target channel selection vs. previous name) is not obvious from the signature and should be clarified by the implementation or caller to avoid mismatched behavior.
|
||||||
|
- All methods return `Task` and must be awaited or otherwise observed by callers to ensure errors in the broadcasting layer are surfaced; implementations may perform I/O and should handle transient failures internally or propagate meaningful exceptions to callers.
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# IChatService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IChatService.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
*Figure: How IChatService works.*
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
%%{init: {'theme':'base','themeVariables':{'background':'#faf7ef','primaryColor':'#f0e2c2','primaryTextColor':'#1f2840','primaryBorderColor':'#8a7548','secondaryColor':'#d9efec','secondaryBorderColor':'#1d8a80','secondaryTextColor':'#1f2840','tertiaryColor':'#f2ebd8','tertiaryBorderColor':'#8a7548','tertiaryTextColor':'#1f2840','lineColor':'#1d8a80','titleColor':'#1f2840','fontSize':'14px','edgeLabelBackground':'#faf7ef','clusterBkg':'#f2ebd8','clusterBorder':'#8a7548','actorBkg':'#f0e2c2','actorBorder':'#8a7548','actorTextColor':'#1f2840','actorLineColor':'#8a7548','signalColor':'#1d8a80','signalTextColor':'#1f2840','activationBkgColor':'#d9efec','activationBorderColor':'#1d8a80','noteBkgColor':'#f2ebd8','noteBorderColor':'#8a7548','noteTextColor':'#1f2840','labelBoxBkgColor':'#f0e2c2','labelBoxBorderColor':'#8a7548','labelTextColor':'#1f2840','transitionColor':'#1d8a80','transitionLabelColor':'#1f2840','stateLabelColor':'#1f2840','altBackground':'#f2ebd8'}}}%%
|
||||||
|
flowchart TB
|
||||||
|
Start["Start"]
|
||||||
|
Start --> Conn["IChatService: UserConnectedAsync(connectionId, userId, username) registers connection"]
|
||||||
|
Conn --> Join["IChatService: JoinChannelAsync(connectionId, userId, username, channelName, password?) returns (History, Error, PasswordRequired)"]
|
||||||
|
Join --> CheckPwd{"Channel requires password?"}
|
||||||
|
CheckPwd -->|"yes"| RequirePwd["Return (History=null, Error='Password required', PasswordRequired=true)"]
|
||||||
|
CheckPwd -->|"no"| Joined["Return (History=List of MessageDto, Error=null, PasswordRequired=false)"]
|
||||||
|
Joined --> History["MessageDto: history items provided by GetChannelHistoryAsync(channelName, count, offset)"]
|
||||||
|
|
||||||
|
Conn --> Send["IChatService: SendMessageAsync(userId, username, channelName, content, originConnectionId?, replyToMessageId?)"]
|
||||||
|
Send --> CheckReply{"replyToMessageId != null?"}
|
||||||
|
CheckReply -->|"yes"| ValidateReply{"reply exists and is in same channel?"}
|
||||||
|
ValidateReply -->|"no"| RejectReply["Return Error (invalid reply target)"]
|
||||||
|
ValidateReply -->|"yes"| CreateMsg["Create MessageDto with content, sender, replyToMessageId"]
|
||||||
|
CheckReply -->|"no"| CreateMsg
|
||||||
|
CreateMsg --> Broadcast["IChatService: BroadcastMessageAsync(channelName, MessageDto) avoids echo to originConnectionId"]
|
||||||
|
Broadcast --> Channel["Channel: deliver message to channel members' connections"]
|
||||||
|
|
||||||
|
Conn --> Update["IChatService: UpdateStatusAsync(userId, username, UserStatus, statusMessage) returns optional string"]
|
||||||
|
Update --> PresenceList["IChatService: GetOnlineUsersAsync(channelName) returns list of UserPresenceDto"]
|
||||||
|
|
||||||
|
Broadcast --> ChannelUpdated["IChatService: BroadcastChannelUpdatedAsync(ChannelDto channel, channelName?)"]
|
||||||
|
ChannelUpdated --> ChannelDto["ChannelDto: channel metadata"]
|
||||||
|
ChannelUpdated --> ChannelDeleted["IChatService: BroadcastChannelDeletedAsync(channelName)"]
|
||||||
|
|
||||||
|
Conn --> Query["IChatService: GetChannelsForUserAsync(username) returns List of channels"]
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IChatService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Provides chat-layer operations for connection lifecycle, channel membership, messaging, presence and cross-process broadcasting. Reach for `IChatService` when implementing or calling the application-level chat logic (for example from controllers, real-time hubs or an IRC gateway) rather than manipulating lower-level transport or persistence APIs directly.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IChatService` centralizes the domain operations needed by the real-time chat surface: tracking connections ([`UserConnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), joining and leaving channels ([`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`LeaveChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), sending and retrieving messages ([`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), presence ([`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`GetOnlineUsersAsync`](../../EchoHub.Server/Services/ChatService.cs.md)), and broadcasting channel or message events to other processes ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface is designed for use by controllers and gateway components (the code comments indicate the IRC gateway uses several methods), so it intentionally mixes request/response operations (join, send) with one-way broadcast methods used to propagate state across processes.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Typical happy-path usage from a controller or hub
|
||||||
|
var (history, joinError, passwordRequired) = await chatService.JoinChannelAsync(connectionId, userId, username, "general");
|
||||||
|
if (joinError != null) {
|
||||||
|
// handle join failure (implementation-specific semantics)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Show the returned history to the user
|
||||||
|
foreach (var item in history) {
|
||||||
|
// item is a [`MessageDto`](../DTOs/ChatDtos.cs.md)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a message; the returned nullable string has implementation-dependent meaning
|
||||||
|
var sendResult = await chatService.SendMessageAsync(userId, username, "general", "Hello everyone!");
|
||||||
|
if (sendResult != null) {
|
||||||
|
// react to non-null result per the concrete implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast a message instance (e.g. from background processing or another gateway)
|
||||||
|
// `message` here is a [`MessageDto`](../DTOs/ChatDtos.cs.md) obtained from persistence or constructed by the implementation
|
||||||
|
// await chatService.BroadcastMessageAsync("general", message);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Several methods return `Task<string?>` (for example [`UserDisconnectedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`UpdateStatusAsync`](../../EchoHub.Server/Services/ChatService.cs.md)). The interface does not document the exact semantics of a non-null string (error message vs. identifier vs. other). Consumers must consult the concrete implementation or its docs to interpret these values correctly.
|
||||||
|
- The `originConnectionId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) is used to avoid echoing a broadcast back to the originating connection (IRC-like behavior). Other sessions owned by the same user still receive the message.
|
||||||
|
- The `replyToMessageId` parameter on [`SendMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md) must reference a message that exists in the same channel; implementations should validate this constraint.
|
||||||
|
- [`JoinChannelAsync`](../../EchoHub.Server/Services/ChatService.cs.md) returns a tuple containing `History`, `Error`, and `PasswordRequired`. Callers should handle the `Error` and `PasswordRequired` flags before assuming `History` contains usable data.
|
||||||
|
- [`GetChannelHistoryAsync`](../../EchoHub.Server/Services/ChatService.cs.md) supports simple pagination via `count` and `offset`; callers should choose `count` and `offset` to limit load and avoid returning excessively large histories in a single call.
|
||||||
|
- Broadcasting methods ([`BroadcastMessageAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelUpdatedAsync`](../../EchoHub.Server/Services/ChatService.cs.md), [`BroadcastChannelDeletedAsync`](../../EchoHub.Server/Services/ChatService.cs.md)) are intentionally one-way primitives used to notify other processes; they do not return operation results and callers should not rely on them for synchronous guarantees.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# IEchoHubClient
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IEchoHubClient.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IEchoHubClient
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents the callback contract for notifications and control messages the server can invoke on connected clients. Implement this interface on the client side (or provide a test double) when you need a strongly-typed set of server-to-client RPCs for events such as new messages, presence changes, channel updates, moderation actions, and error or disconnect notifications.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This interface centralizes all server-originated client callbacks into a single, versioned surface so the server can address connected clients with a known set of operations. Each method returns a `Task` to allow asynchronous client implementations (IO, UI dispatching, persistence) and to make the callbacks composable for test harnesses and runtime adapters. The nullable annotations on parameters (for example the `UserPresenceDto?` in `UserJoined` and `string?` in `UserKicked`) indicate which values the server may omit; implementations must handle those cases.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
public class ConsoleEchoClient : IEchoHubClient
|
||||||
|
{
|
||||||
|
public Task ReceiveMessage(MessageDto message)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[{message.Channel}] {message.Sender}: {message.Text}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserJoined(string channelName, string username, UserPresenceDto? presence)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User joined {channelName}: {username}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserLeft(string channelName, string username)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User left {channelName}: {username}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ChannelUpdated(ChannelDto channel)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Channel updated: {channel.Name}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserStatusChanged(UserPresenceDto presence)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Status changed: {presence.Username} -> {presence.Status}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserKicked(string channelName, string username, string? reason)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User kicked from {channelName}: {username} Reason: {reason ?? "(none)"}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task UserBanned(string username, string? reason)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"User banned: {username} Reason: {reason ?? "(none)"}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task MessageDeleted(string channelName, Guid messageId)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Message deleted in {channelName}: {messageId}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ChannelDeleted(string channelName)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Channel deleted: {channelName}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ChannelNuked(string channelName)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Channel nuked: {channelName}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ForceDisconnect(string reason)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Force disconnect: {reason}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Error(string message)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Error from server: {message}");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Respect nullability: parameters annotated with `?` (for example `UserPresenceDto?` and `string?`) may be `null` and callers should handle those cases gracefully.
|
||||||
|
- All methods return `Task`: implementations should avoid long-running synchronous work on the calling thread (use `async`/`await` or schedule work) to prevent blocking the runtime that invokes these callbacks.
|
||||||
|
- Implementations should avoid throwing exceptions from these methods where possible; unhandled exceptions may surface to the caller or the hosting infrastructure depending on how the callbacks are invoked.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# IMessageEncryptionService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IMessageEncryptionService.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IMessageEncryptionService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `IMessageEncryptionService` interface defines a centralized contract for encrypting and decrypting messages used in transit and at rest. It exposes a straightforward API to convert plaintext into ciphertext and back, while the `CiphertextPrefix` marks encrypted payloads so the implementation can transparently pass through values that are not encrypted. The `EncryptDatabaseEnabled` flag surfaces the server-side setting that indicates whether data stored in the database should be encrypted at rest, enabling callers to adapt their behavior to policy.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
This abstraction minimizes scattered crypto logic by presenting a single, testable surface for encryption decisions. The pass-through behavior for content that does not begin with the `CiphertextPrefix` helps prevent double-encrypting and keeps compatibility with data already in plaintext. By providing nullable-aware methods (`EncryptNullable` and `DecryptNullable`), it cleanly handles optional values without forcing callers to perform boilerplate null checks at call sites.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Assume you have an instance of IMessageEncryptionService named `service`
|
||||||
|
string ciphertext = service.Encrypt("TopSecret");
|
||||||
|
string plaintext = service.Decrypt(ciphertext); // "TopSecret"
|
||||||
|
|
||||||
|
// Decrypting non-encrypted content yields the original value (pass-through)
|
||||||
|
string passthrough = service.Decrypt("plain-text"); // "plain-text"
|
||||||
|
|
||||||
|
string? nullableValue = null;
|
||||||
|
string? encNullable = service.EncryptNullable(nullableValue); // null
|
||||||
|
string? decNullable = service.DecryptNullable(encNullable); // null
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `CiphertextPrefix` ("$ENC$v1$") is a marker used to identify encrypted data. Decrypt will return the input unchanged if it does not start with this prefix.
|
||||||
|
- `EncryptDatabaseEnabled` reflects a server policy. It indicates whether data should be encrypted at rest, but callers must still invoke `Encrypt`/`EncryptNullable` before storage to ensure encryption occurs per policy.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# IUserService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Contracts/IUserService.cs`
|
||||||
|
> **Kind:** interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IUserService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`IUserService` is the asynchronous contract for common user-account operations: registration, authentication, and profile access. Implementations may back these calls with REST, an IRC gateway, or other transports, but callers interact with this interface to perform login, account creation, and profile queries without coupling to a specific transport.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By returning `Task<UserOperationResult>` for mutating operations and `Task<UserProfileDto?>` for profile queries, the interface cleanly models success/failure and optional data. The [`UserOperationResult`](../DTOs/CommonDtos.cs.md) type provides `Success(UserProfileDto user)` and `Fail(UserError error, string message)` helpers, enabling implementations to construct consistent outcomes. The `RegisterUserAsync` method carries a server-policies cue in its comment: when `Server:Registration = "invite"`, an `inviteCode` is required; in `"closed"` mode, new accounts are refused. This centralizes registration policy at the service boundary and avoids scattering policy checks across call sites.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Example usage of IUserService
|
||||||
|
public async Task DemoAsync(IUserService userService)
|
||||||
|
{
|
||||||
|
var reg = await userService.RegisterUserAsync("alice", "Secret123", inviteCode: "INVITE-42");
|
||||||
|
if (reg.IsSuccess)
|
||||||
|
{
|
||||||
|
var profile = await userService.GetUserProfileAsync("alice");
|
||||||
|
// Use profile as needed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `inviteCode` parameter is context-sensitive and should be supplied when the server is configured with `Server:Registration = "invite"`; otherwise it may be omitted.
|
||||||
|
- All methods are asynchronous; callers should `await` the results and branch on `UserOperationResult.IsSuccess` as appropriate.
|
||||||
|
- [`GetUserProfileAsync`](../../EchoHub.Client/Services/ApiClient.cs.md) and `GetUserByIdAsync` return `UserProfileDto?`, reflecting the possibility that a user profile may not be found or accessible in certain contexts.
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# 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` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a request payload that carries the user's `Password` to re-confirm destructive self-service actions on the account. This separate `DeleteAccountRequest` DTO isolates credential input from other account data and is intended for use in flows that require explicit user re-authentication before irreversible operations (e.g., account deletion).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Isolates sensitive credential input into a minimal, purpose-built payload, enabling focused validation and auditing of destructive actions. It complements authentication state by forcing an explicit password re-entry rather than relying on session state alone, which helps mitigate accidental or unauthorized deletions. This pattern supports clearer separation of concerns between domain models and security-critical request data.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log or persist the `Password` value in plaintext; keep it transient and ensure redaction in any logs.
|
||||||
|
- Ensure transport security (`TLS`) when transmitting this payload; avoid storing passwords in memory longer than needed; clear the value after usage if possible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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, carrying the essential details needed to access and display it—`FileName`, `Url`, `FileSize`, `Kind`, `ChannelName`, and `SentAt`. It serves as a transport contract between the export logic and clients or downstream services rather than exposing internal domain entities.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
`ExportedAttachmentDto` acts as a boundary-crossing contract: it decouples the external payload from the internal attachment representation and exposes only the data consumers require. The inclusion of a `Url` implies a downloadable resource that may be protected or time-limited, so callers should treat access as potentially ephemeral and handle expiration appropriately. Because this is a `record`, instances are immutable by default, which helps preserve the integrity of the export snapshot across layers.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `Url` is often a signed or temporary link; do not assume long-lived access and design clients to handle expiration (e.g., 404 or 403 responses).
|
||||||
|
- This DTO is strictly a data carrier; avoid embedding business logic in the payload and prefer mapping from domain models to this shape when exporting data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 (record) that captures the essential data of a single exported message: the message `Id`, the `ChannelName` it was sent in, the `SentAt` timestamp, the `Content`, and an optional `ReplyToMessageId` if the message is a reply. It provides a stable, serializable contract for exporting messages to external systems or archives, decoupled from domain behavior so consumers can rely on a consistent shape without depending on domain entities.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a `record`, `ExportedMessageDto` provides value-like semantics and a predictable equality contract, which is helpful when comparing exported records or caching results during export pipelines. It also separates export concerns from the rest of the domain, making it easier to evolve the internal models without breaking external consumers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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>` | — |
|
||||||
|
|
||||||
|
|
||||||
|
`UserDataExportDto` is a `record` that represents a complete snapshot of the server's stored data for a given user, produced when exporting user data for portability or archival. It contains the export timestamp (`ExportedAt`), the originating server name ([`ServerName`](../../EchoHub.Server.Irc/IrcCommandHandler.cs.md)), the user's profile (`Profile`), and the exported content items: messages (`Messages`) and attachments (`Attachments`). In end-to-end encrypted rooms, the message payload is preserved as ciphertext, since the server cannot provide plaintext it never possessed.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This DTO acts as the stable envelope for user data exports, keeping metadata, profile, and content items together for portability and archival use. It decouples export semantics from how data is stored, permitting changes to storage without breaking export contracts. Note that for end-to-end encrypted rooms, the `Messages` are ciphertext as stored; no plaintext is accessible to the server.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Large exports can be memory-intensive; plan for streaming or chunked delivery in exporters.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# 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 payload for a login operation as an immutable data transfer object. It carries the two required fields, `Username` and `Password`, and is intended to be sent to the authentication boundary to perform sign-in. Use `LoginRequest` when you need to pass user credentials through service boundaries in a strongly-typed, single payload rather than as separate arguments.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because `LoginRequest` is a `record`, it provides value-based equality and immutability, which makes it a natural data carrier across application layers. This abstraction helps decouple transport concerns from domain logic by centralizing credentials into a single, typed payload.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log or serialize the `Password` value; treat `LoginRequest` as sensitive data and ensure transport uses TLS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` represents the result of a login attempt, carrying the [`Token`](../../EchoHub.Client/Services/ApiClient.cs.md), `RefreshToken`, `ExpiresAt`, and user identity data like `Username`, with optional `DisplayName` and `NicknameColor` for UI personalization. As a `record`, it is immutable and uses value-based equality, making it a convenient, transportable payload for authentication flows.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Immutability and value-based equality make `LoginResponse` easy to compare, cache, and pattern-match in authentication workflows. It groups all login-related data in one cohesive container, reducing the risk of mismatched fields across layers. The optional `DisplayName` and `NicknameColor` allow UI layers to present user-friendly details without forcing these values for every login.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var response = new LoginResponse(
|
||||||
|
Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
RefreshToken: "defghijklmnopqrstuvwxyz",
|
||||||
|
ExpiresAt: DateTimeOffset.UtcNow.AddHours(1),
|
||||||
|
Username: "alice",
|
||||||
|
DisplayName: "Alice",
|
||||||
|
NicknameColor: "#1E90FF"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Token and RefreshToken are sensitive; avoid logging them or exposing them in UI or analytics outputs. Treat these values as secrets and secure any transport or storage paths that handle them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RefreshRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/AuthDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record RefreshRequest(string RefreshToken)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `RefreshToken` | `string` | — |
|
||||||
|
|
||||||
|
|
||||||
|
An immutable data container representing the payload of a token refresh request. It exposes a single property, `RefreshToken`, which the authentication workflow uses to obtain new access tokens.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because this is a `record` with a single value, it provides value-based equality and straightforward deconstruction, making it ideal as a data-transfer object (DTO) across API boundaries. It decouples transport concerns from token-issuance logic, enabling the controller to receive and forward the refresh token without embedding behavior.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `RefreshToken` is sensitive data; avoid logging it or exposing it in error payloads.
|
||||||
|
- This type is a plain DTO with no validation or side effects; validation should occur in the service layer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 data-transfer object that captures the input for a user registration operation. It encapsulates the required `Username` and `Password` and includes optional `DisplayName` and [`InviteCode`](../Models/InviteCode.cs.md) so callers can supply additional metadata in a single payload to the authentication endpoint.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a simple DTO, `RegisterRequest` acts as a stable contract between the public API surface and the authentication logic. It isolates the registration input structure from internal domain models, enabling independent evolution and simpler testing while the underlying registration workflow evolves.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log or serialize the `Password` field in logs or telemetry; treat it as sensitive data and rely on transport security.
|
||||||
|
- The optional fields `DisplayName` and [`InviteCode`](../Models/InviteCode.cs.md) may be `null`; downstream code should handle nulls gracefully and only include them when provided.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,504 @@
|
|||||||
|
# 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` |
|
||||||
|
|
||||||
|
|
||||||
|
A file attached to a message is represented by `AttachmentDto`. It carries the attachment's kind ([`AttachmentKind`](../Models/AttachmentKind.cs.md)), a URL to retrieve the content (`Url`), the original file name (`FileName`), and the file size in bytes (`FileSize`). If available, `AsciiPreview` holds color-tag ASCII art for images; in end-to-end encrypted channels the data behind `Url` and the preview is ciphertext the server cannot read.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because `AttachmentDto` is a record, it provides value-based equality and immutability, making it a stable transport object across layers. It decouples the attachment metadata from the message payload, enabling clients to render previews or retrieve content on demand without embedding binary data in the message. The `AsciiPreview` field offers a lightweight preview for image attachments, while `Url` points to the resource whose handling may be encrypted in transit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 is a small data container that exposes the channel's cryptographic policy: whether encryption is enabled (`IsEncrypted`) and the salt used to derive a join credential from a passphrase (`EncryptionSalt`). Use it when you need to pass this metadata across system boundaries without exposing the wrapped room key.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Consolidating `IsEncrypted` and `EncryptionSalt` into a single value object reduces coupling between channel-joining logic and cryptographic operations. It makes intent explicit at call sites that must decide how to derive credentials from a passphrase. Importantly, the actual wrapped room key remains outside this DTO, preserving the security boundary that keys are only handled by the cryptographic subsystem. The nullable `EncryptionSalt` communicates that a salt is omitted when encryption is disabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 carries the essential metadata of a chat channel: `Id`, `Name`, `Topic`, `IsPublic`, `MessageCount`, `CreatedAt`, and the optional flags `IsProtected`, `IsEncrypted`, and `IsSystem`. As a `record`, it provides value-based equality and a straightforward bundle of properties suitable for transport across layers or API boundaries without exposing domain entities. Use it when returning channel summaries, listings, or lightweight channel representations to clients or other services, rather than leaking internal domain models.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ChannelDto exists to decouple transport contracts from domain models; by consolidating channel metadata into a single, serializable shape, it enables stable APIs and easier versioning. The `IsSystem` flag allows distinguishing system channels (like announcements) from user-created ones, while `CreatedAt` helps clients sort or display recency.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var channel = new ChannelDto(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"general",
|
||||||
|
"General discussion",
|
||||||
|
true,
|
||||||
|
128,
|
||||||
|
DateTimeOffset.UtcNow
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Topic is nullable; consumers should handle `null` before displaying a topic, or provide a fallback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 an immutable data transfer object that presents a concise, human-facing snapshot of a channel's metadata (the `/meta` command) to clients. It exposes the channel's identity (`Id`, `Name`), optional `Topic`, security/status flags (`IsEncrypted`, `IsProtected`), audience metrics (`MessageCount`, `UniqueUserCount`), and an estimated on-disk footprint (`EstimatedSizeBytes`), which is the sum of stored attachment blob sizes plus message text length and thus an estimate rather than an exact total. For encrypted channels the server still knows these figures — counts, timestamps, and stored blob sizes — even though it cannot read the content itself. The `CreatedAt` field records when the channel was created.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ChannelMetaDto serves as a stable, read-only contract between server and clients for channel overviews. As an immutable `record`, it guarantees value-based equality and prevents accidental mutation, which simplifies caching and change detection in UI layers. The metadata it carries—identity, topic, security flags, counts, and size—supports efficient rendering of channel lists and summaries without exposing the channel contents.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `EstimatedSizeBytes` is an estimate (sum of stored attachment blob sizes and message text length); it is not an exact on-disk size and can drift as content changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CreateChannelRequest
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/ChatDtos.cs`
|
||||||
|
> **Kind:** record
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record CreateChannelRequest(
|
||||||
|
string Name,
|
||||||
|
string? Topic = null,
|
||||||
|
bool IsPublic = true,
|
||||||
|
string? Password = null,
|
||||||
|
string? EncryptionSalt = null,
|
||||||
|
string? WrappedRoomKey = null)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Name` | `string` | — |
|
||||||
|
| `Topic` | `string?` | `null` |
|
||||||
|
| `IsPublic` | `bool` | `true` |
|
||||||
|
| `Password` | `string?` | `null` |
|
||||||
|
| `EncryptionSalt` | `string?` | `null` |
|
||||||
|
| `WrappedRoomKey` | `string?` | `null` |
|
||||||
|
|
||||||
|
|
||||||
|
The `CreateChannelRequest` is an immutable data transfer object that encapsulates all parameters needed to create a new chat channel. It requires a `Name` and exposes optional settings including `Topic`, whether the channel is public via `IsPublic` (default true), and optional security fields such as `Password`, `EncryptionSalt`, and `WrappedRoomKey` used for encrypted channel setup. Use this record when issuing a channel creation operation so that all related options are passed as a single, strongly-typed payload rather than a loose collection of parameters.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By collecting channel creation options into a single `CreateChannelRequest`, the boundary between API inputs and domain logic is cleanly expressed. The defaults on `IsPublic` and the optional nature of the other fields enable flexible requests while preserving a stable, serializable contract across process boundaries. This abstraction also makes future extension safer: new optional settings can be added without altering existing call sites.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log sensitive fields: avoid writing `Password`, `EncryptionSalt`, or `WrappedRoomKey` to logs or telemetry.
|
||||||
|
- Nullable fields imply validation; ensure meaningful values before persisting or acting on them.
|
||||||
|
- If `IsPublic` is false, consider validating that a `Password` is provided for access control; enforce this at the API or domain layer if required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 an immutable data container used to carry the metadata needed to render a rich embed, such as in chat messages or UI panels. It groups the surface data for an embed: `SiteName`, `Title`, `Description`, `ImageAscii`, `Url`, and an optional `ThemeColor`, so callers can supply a complete embed definition in a single object.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
As a `record`, `EmbedDto` provides value-based equality and supports deconstruction, making it straightforward to compare embeddings or pattern-match in rendering logic. It serves as a clean boundary between data authors and renderers: producers populate an `EmbedDto`, consumers render an embed from its fields without needing to understand surrounding domain.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var embed = new EmbedDto(
|
||||||
|
SiteName: "EchoHub",
|
||||||
|
Title: "Welcome",
|
||||||
|
Description: "A friendly hello from EchoHub.",
|
||||||
|
ImageAscii: " ___ \n (o o) \n \_/ ",
|
||||||
|
Url: "https://echohub.example",
|
||||||
|
ThemeColor: "#4B8BBE"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `ThemeColor` is optional; omit it to use a default theming.
|
||||||
|
- `Url` is required; ensure it is a valid URL to enable link previews.
|
||||||
|
- Because `EmbedDto` is a `record`, two instances with identical field values compare equal.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the outcome of a join-channel operation as a `JoinChannelResult` type. It exposes a `bool` `Success` flag, a `List<MessageDto>` `History` of messages retrieved for the channel, and optional metadata including a `string?` `Error`, a `bool` `PasswordRequired`, and optional encryption data (`string?` `EncryptionSalt`, `string?` `WrappedRoomKey`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a chat message as a data contract used by the chat API. It captures the message `Id`, the textual `Content`, and author info (`SenderUsername`, optional `SenderNicknameColor`, optional `SenderDisplayName`), the `ChannelName`, and the `SentAt` timestamp. Optional `Attachments` and `Embeds` support rich content, while `ReplyTo` references a prior message.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This DTO is designed as a transport-friendly aggregation of message data, suitable for serialization across clients and services. By referencing the dedicated `AttachmentDto` and `EmbedDto` types, it remains extensible for rich content, and its optional fields (`Attachments`, `Embeds`, `ReplyTo`, `SenderNicknameColor`, `SenderDisplayName`) allow the same shape to cover both simple and feature-rich messages.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Attachments` and `Embeds` may be `null`; treat them as empty sequences when rendering or iterating.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` | — |
|
||||||
|
|
||||||
|
|
||||||
|
The `RekeyChannelRequest` record represents the data the client sends to request a rekey of an encrypted channel. It conveys knowledge of the current passphrase (via `OldPassword`) and the new credentials and wrapped key to apply (via `NewPassword`, `NewEncryptionSalt`, and `NewWrappedRoomKey`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This DTO enables the server to verify the client's possession of the existing auth key while atomically applying new encryption material in a single operation. It decouples the client's input from the rekeying logic, allowing validation, auditing, and rollback policies to be applied at the server boundary.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Do not log `OldPassword` or `NewPassword`; treat these values as ephemeral and ensure transport-layer secrecy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a reference to the message that a reply targets. It carries the target message's identifier (`MessageId`), the original sender's username (`SenderUsername`), and the reply content (`Content`), which is treated exactly like message content on the wire: transport-encrypted, and for end-to-end encrypted rooms it is room ciphertext the client must decrypt (the server truncates only plaintext snippets). Null on a `MessageDto` when the original message no longer exists.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ReplyRefDto acts as a compact pointer that preserves the link between a reply and its target message without duplicating payloads. It separates transport- and encryption-aware handling from display logic, enabling clients to decrypt or render the referenced content while the server retains plaintext-only signals. In threaded chat UX, this symbol supports rendering reply previews and context for the target message.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Be aware that `Content` might be ciphertext in encrypted rooms and may not be human-readable until decrypted; do not display it as plaintext without decryption.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 a simple, immutable data carrier (record) that encapsulates the channel to which a message should be sent and the message content itself. Use this `SendMessageRequest` when you need to issue a message to a specific chat channel, providing both the `ChannelName` and the `Content` in a single object rather than passing multiple parameters or ad-hoc structures.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 minimal value object used to convey a URL as a request payload. As a `record` with a single `string Url` positional parameter, it provides value-based equality and immutability, making it ideal for passing URL data through layers or across API boundaries instead of threading raw `string` values.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`SendUrlRequest` serves as a precise contract for operations that require a URL. Its `record` semantics ensure structural equality and allow easy deconstruction; by encapsulating the `Url` property, it clarifies intent and supports serialization as a simple payload.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var req = new SendUrlRequest("https://example.com");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- No URL validation is performed by this type; validate the URL in the caller or service layer before processing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 payload to update a topic, encapsulating an optional `Topic` value. As a positional-record, it provides an immutable, lightweight data carrier that callers populate with the new topic string when issuing an update to a chat's topic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 an immutable data transfer object that carries a concise snapshot of a user for chat workflows. It exposes the user’s `Id` (`Guid`), `Username`, optional `DisplayName` and `NicknameColor`, the current `Status` ([`UserStatus`](../Models/UserStatus.cs.md)), and the `LastSeenAt` timestamp (`DateTimeOffset`). Use this DTO when returning or transferring lightweight user data across API boundaries or UI layers instead of exposing full domain entities.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Being a `record` with positional parameters, `UserDto` benefits from value-based equality and convenient deconstruction, which is helpful for tests and payload comparisons. The nullable fields `DisplayName` and `NicknameColor` reflect optional user profile data; readers should handle the possibility of missing values gracefully.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Nullable fields require null checks during consumption.
|
||||||
|
- Being immutable, modifying a `UserDto` requires creating a new instance (e.g., via a `with` expression).
|
||||||
|
- The `LastSeenAt` is a `DateTimeOffset`; ensure consistent time zone handling across systems.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
# 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` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a standard API outcome as a `record` with a `bool` `Success`, an optional `string?` [`Message`](../Models/Message.cs.md), and an optional `List<string>?` `Errors`. Use `ApiResponse` to package the result of API operations or service methods into a single, strongly-typed object for consistent client consumption instead of scattering boolean flags and messages across code.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing outcome data in `ApiResponse`, callers can handle success/failure logic in a uniform way and avoid ad-hoc boolean checks scattered through the code. The `Errors` collection is intended for granular, field-level validation messages that the client can display; the [`Message`](../Models/Message.cs.md) offers a concise summary, while `Success` drives flow control.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var success = new ApiResponse(true);
|
||||||
|
var failure = new ApiResponse(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `Errors` property is a mutable `List<string>`; external mutation is possible. If you need true immutability, consider using `IReadOnlyList<string>` or an immutable collection.
|
||||||
|
- When `Success` is true, you may omit [`Message`](../Models/Message.cs.md) and `Errors` or set them as appropriate; when `Success` is false, provide a meaningful [`Message`](../Models/Message.cs.md) and optionally populate `Errors` to detail issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
|
||||||
|
A generic wrapper for operation results that standardizes API responses. It indicates success with `Success` and carries an optional [`Message`](../Models/Message.cs.md), a `List<string>` named `Errors` for validation or processing issues, and an optional `Data` payload of type `T`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
This abstraction decouples the shape of a successful response from the actual data, enabling consistent error handling and client-side parsing across services. By returning `ApiResponse<T>` from operations, you centralize how success, messages, and validation details are conveyed, which simplifies cross-cutting concerns like localization and error translation.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Successful response with data
|
||||||
|
var success = new ApiResponse<string>(true, "Operation completed", null, "payload-data");
|
||||||
|
|
||||||
|
// Failed response with errors
|
||||||
|
var failure = new ApiResponse<string>(false, "Validation failed", new List<string> { "Name is required", "Email is invalid" }, null);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 an immutable wrapper that conveys the outcome of a channel-related operation. It either carries a [`ChannelDto`](ChatDtos.cs.md) when the operation succeeds or a `ChannelError` with an `ErrorMessage` when it fails; the static helpers `Success` and `Fail` make the intent explicit when constructing results.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ChannelOperationResult uses a C# `record` to express a simple, value-like outcome. It centralizes success/failure information for channel-oriented operations, enabling uniform error handling and reducing scattered null-checks. Consumers should inspect `IsSuccess` before accessing [`Channel`](../Models/Channel.cs.md); when `IsSuccess` is true, [`Channel`](../Models/Channel.cs.md) is non-null, and when false, `Error` and `ErrorMessage` describe the failure.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Prefer constructing via `ChannelOperationResult.Success(...)` or `ChannelOperationResult.Fail(...)` rather than the primary constructor to preserve the invariant that a successful result has a non-null [`Channel`](../Models/Channel.cs.md) and a failed result has non-null `Error`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
|
||||||
|
Encapsulates a standardized error payload with a mandatory `Error` code and an optional `Detail` string for extra context. As a `record`, it is immutable by design and supports value-based equality and deconstruction, which makes it ideal for returning a single, comparable error object from APIs or services. Use this type to produce consistent, serializable error information across the system.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Centralizes the error payload shape to ensure all error responses share a single contract. The optional `Detail` field provides human-friendly context without breaking clients that only inspect the `Error` code. Because it is a `record`, it naturally supports comparisons and pattern matching when handling error responses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 paged result set for a collection of items of type `T`. It bundles the current page of data (`Items`) with paging metadata: the total item count (`Total`), the starting offset (`Offset`), and the page size limit (`Limit`). This shape is used by APIs that support paging to convey both the data and how to fetch additional pages; the use of a `record` provides value-based equality and immutability for API responses.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Using a `record` for `PaginatedResponse<T>` gives value-based equality and an immutable data shape, which makes it natural for transporting paging results across boundaries. It centralizes both the data (`Items`) and its paging metadata (`Total`, `Offset`, `Limit`) in a single coherent DTO, reducing the risk of mismatch between data and paging state when consumed by clients or other services.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `Items` collection is a `List<T>`, which is mutable. If you require immutability guarantees, wrap it in a read-only collection or clone the list before exposure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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?` | — |
|
||||||
|
|
||||||
|
|
||||||
|
An immutable result wrapper for user-related operations. It encapsulates either a [`UserProfileDto`](ProfileDtos.cs.md) payload via [`User`](../Models/User.cs.md) on success, or a `UserError` and a diagnostic `ErrorMessage` on failure. Use the static factories `Success` and `Fail` to construct consistent results, and check `IsSuccess` to decide how to proceed.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating both success payload and failure details into a single value, this symbol standardizes how user-operation results are communicated. Callers check `IsSuccess` and then access either the [`User`](../Models/User.cs.md) payload or the `Error`/`ErrorMessage` to react. Because it is a `record`, equality is based on its contents, which helps tests and caching rely on value semantics.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Directly constructing with a mismatched state (for example, a non-null `Error` but a null or missing `ErrorMessage`) can create inconsistent results; prefer the provided factories to enforce the invariant that success results include a [`User`](../Models/User.cs.md) and no error, while failures include an `Error` and an `ErrorMessage`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ChannelError
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum ChannelError
|
||||||
|
{
|
||||||
|
ValidationFailed,
|
||||||
|
AlreadyExists,
|
||||||
|
NotFound,
|
||||||
|
Forbidden,
|
||||||
|
Protected
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ChannelError is an enum that enumerates the standard error conditions that may arise when working with channels in the `EchoHub` domain. It provides a typed set of failure reasons—`ValidationFailed`, `AlreadyExists`, `NotFound`, `Forbidden`, and `Protected`—to be returned by channel-related operations, enabling callers to branch on the specific cause and handle it uniformly rather than parsing strings.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
`ChannelError` centralizes the failure kinds that can occur during channel-related operations and is intended to be carried by DTOs that report operation results. It enables type-safe error handling, allowing callers to pattern-match on the exact failure (`ValidationFailed`, `AlreadyExists`, `NotFound`, `Forbidden`, `Protected`) and map them to appropriate responses without parsing human-generated messages. This separation of error kind from presentation keeps the API consistent as channel semantics evolve.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UserError
|
||||||
|
> **File:** `src/EchoHub.Core/DTOs/CommonDtos.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum UserError
|
||||||
|
{
|
||||||
|
ValidationFailed,
|
||||||
|
AlreadyExists,
|
||||||
|
NotFound,
|
||||||
|
InvalidCredentials,
|
||||||
|
Banned
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `UserError` enum defines the canonical set of failure conditions related to user accounts that may be surfaced by operations in the core DTO layer. Members include `ValidationFailed`, `AlreadyExists`, `NotFound`, `InvalidCredentials`, and `Banned`, each representing a distinct error scenario that downstream code can pattern-match to drive error responses and user messaging.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enum centralizes common user-domain errors so that authentication, registration, and profile-management flows can share a consistent error-handling strategy. By codifying these cases in a single type, callers can translate domain failures into uniform API responses and UI messages without depending on implementation details.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- When mapping these errors to user-facing messages, avoid exposing sensitive internal details and rely on generic messaging driven by the enum value.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# 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` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents the request payload for creating an invite, carrying optional constraints for the invite. The nullable `MaxUses` and `ExpiresInHours` allow callers to omit constraints. As a `record`, it provides value-based equality and immutability, making it a convenient, typed carrier for API calls.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This type centralizes the concept of invite constraints and cleanly separates client request construction from business logic. It interoperates with the invite-creation pathway by encoding optional parameters as nullable properties, allowing the API to apply defaults when a field is null.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Null values indicate 'not specified' and will be treated as absent by the invite-creation endpoint; set only the fields you intend to constrain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 an immutable data transfer object that carries the metadata for an invitation: the `Code`, the creator's username (`CreatedByUsername`), the creation time (`CreatedAt`), an optional expiration (`ExpiresAt`), and usage counters (`MaxUses` and `UseCount`). It is designed for transporting invitation data across application boundaries without behavior, making it easy to serialize, deserialize, and compare by value.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because it is defined as a `record`, `InviteDto` benefits from value-based equality and structural immutability, ensuring that two invitations with the same data compare equal and that the payload remains unchanged after construction. The nullable `ExpiresAt` conveys that an invitation might have no expiration; consumers must treat a null as no expiry. The `MaxUses` together with `UseCount` enables the system to enforce limits at the boundary without embedding logic here. This symbol sits at the boundary between persistence, API contracts, and business logic, keeping the shape of invitation data consistent across layers.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# 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 container (a positional `record`) that carries the target `Username` and the `Role` to be assigned. It serves as the payload for moderation workflows when granting a [`ServerRole`](../Models/ServerRole.cs.md) to a user, enabling consistent transport of this intent across API boundaries without embedding behavior. As a `record`, it uses value-based equality and can be copied with a `with` expression to create variations.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This symbol acts purely as a data carrier for the moderation flow, separating payload shape from the enforcement logic. It relies on the `Username` and `Role` values to identify the target user and the desired permission, enabling services to validate and enact the change consistently.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Ensure `Username` is a valid existing member; the DTO does not enforce existence.
|
||||||
|
- The `Role` must be a valid [`ServerRole`](../Models/ServerRole.cs.md) value; rely on server-side validation to handle invalid roles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 simple data carrier used to submit a moderation ban action, optionally including a rationale. Its only member, `Reason`, is nullable and defaults to null, so callers may omit a reason when none is provided.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 minimal, immutable data carrier used to convey a moderation kick action. It carries an optional `Reason` explaining why the kick is issued. Callers instantiate a `KickRequest` when initiating a kick, providing a `Reason` if available; if no reason is supplied, the `Reason` property is `null`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
KickRequest being a `record` makes it a value object with structural equality and immutability, which is helpful when routing kick intents through handlers or messaging layers. It encapsulates the kick payload so that higher-level services can work with a single, consistent input type rather than ad-hoc parameters.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var req = new KickRequest("Spamming in chat");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `Reason` is nullable; downstream code should handle `null` and decide whether a reason is required.
|
||||||
|
- Records provide value-based equality; two `KickRequest` instances with the same `Reason` compare equal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 lightweight data transfer object used to specify the parameters of a mute action in moderation flows. It includes two optional values: `Reason`, a `string?` describing why the mute is issued, and `DurationMinutes`, an `int?` indicating how long the mute should last; both default to `null` if not provided. This allows callers to mute with a default duration or provide additional context for auditing and user experience.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# 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 lightweight data container that carries the ASCII representation of a user-uploaded avatar. Its sole payload is the `AvatarAscii` string, which downstream clients can render to display the avatar in text form after an upload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
|
||||||
|
This `UpdateProfileRequest` is a `record` that carries a partial update payload for a user's profile. By supplying only non-null properties (e.g. `DisplayName`, `Bio`, or `NicknameColor`), callers express which fields should be updated; fields left as `null` indicate no change for that field.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Using a `record` provides value-based equality and inherent immutability, which makes it ideal for data-carrying DTOs. The ability to set properties to `null` gives a clean contract for partial updates; consumers should treat nulls as 'do not modify' for that field and pass through only the intended changes to the update operation.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var request = new UpdateProfileRequest(DisplayName: "Nova", NicknameColor: "#FFAA00");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Ensure the update handler interprets nulls as "no change" to avoid overwriting existing values.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` |
|
||||||
|
|
||||||
|
|
||||||
|
This `UpdateStatusRequest` record encapsulates the payload required to update a user's profile status. It carries the new [`UserStatus`](../Models/UserStatus.cs.md) and an optional `StatusMessage`, and is intended to be used when issuing a status update to APIs or command handlers where a consistent update payload is expected.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
By modeling the input as a dedicated value object, this abstraction centralizes validation and transport concerns at the boundaries between the domain and application layers, ensuring a stable contract for status updates. It also isolates update-related concerns from the rest of the profile payload, making it easier to evolve serialization, auditing, or routing rules without touching domain entities.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `StatusMessage` property is nullable. Callers must handle the possibility of a missing message when consuming this payload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 the presence-related data for a user in profile contexts, bundling the `Username`, optional `DisplayName`, optional `NicknameColor`, current `Status`, optional `StatusMessage`, `Role`, and the `IsIrc` flag into a single immutable DTO (with `IsIrc` defaulting to `false`). It is intended to be created and transported as a coherent unit when rendering user cards or updating presence in the UI or API responses, rather than scattering these fields across multiple structures.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Acts as a stable boundary for presence data used by profile-related UI and API surfaces, consolidating identity, status, and role information into one payload. The [`UserStatus`](../Models/UserStatus.cs.md) and [`ServerRole`](../Models/ServerRole.cs.md) collaborators encode the allowed presence states and roles, while `NicknameColor` provides a UI cue without forcing a separate domain type. Being a `record`, it relies on value equality to simplify change detection and caching as presence updates propagate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` | — |
|
||||||
|
|
||||||
|
|
||||||
|
UserProfileDto is an immutable data transfer object that represents a snapshot of a user's profile for API responses and inter-layer communication. Implemented as a `record`, it carries a stable payload including the user's identity (`Id` of type `Guid`, `Username`), optional display details (`DisplayName`, `Bio`, `NicknameColor`, `AvatarAscii`), presence (`Status` of type [`UserStatus`](../Models/UserStatus.cs.md), `StatusMessage`), role (`Role` of type [`ServerRole`](../Models/ServerRole.cs.md)), and timestamps (`CreatedAt`, `LastSeenAt` of type `DateTimeOffset`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By modelling the payload as a `record`, `UserProfileDto` benefits from value-based equality and straightforward serialization for API clients. It serves as a transport contract that decouples external API surfaces from the internal domain model, allowing optional fields to convey partial profile information without mutating server state.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# 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 concise data-transfer `record` that carries a single string property named `Key`, representing an encryption key. Use this type when an API response or internal boundary needs to convey the key as a structured envelope rather than a raw string, benefiting from the immutability and value-based equality of a `record`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By modeling the payload as its own type, this symbol helps keep key handling explicit and self-describing across boundaries. It pairs with other server DTOs to form a consistent contract for encryption-related data, and it can evolve to carry extra metadata (expiry, algorithm) without breaking existing clients.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Treat the `Key` as sensitive data; avoid logging it or exposing it in traces. Ensure it is transmitted only over secure channels and managed according to your security policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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",
|
||||||
|
string Version = "0.0.0")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Default |
|
||||||
|
|-----------|------|---------|
|
||||||
|
| `Name` | `string` | — |
|
||||||
|
| `Description` | `string?` | — |
|
||||||
|
| `OnlineUsers` | `int` | — |
|
||||||
|
| `TotalChannels` | `int` | — |
|
||||||
|
| `RegistrationMode` | `string` | `"open"` |
|
||||||
|
| `Version` | `string` | `"0.0.0"` |
|
||||||
|
|
||||||
|
|
||||||
|
Represents a compact, immutable data transfer object that conveys a server's identity and current activity. It exposes the server's `Name`, optional `Description`, the `OnlineUsers` count, the `TotalChannels`, and optional `RegistrationMode` and `Version` (defaulting to `"open"` and `"0.0.0"` when omitted). Use this DTO in API responses or status endpoints to deliver a stable snapshot of server state.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Because it is a `record`, `ServerStatusDto` benefits from value-based equality and deconstruction semantics, making it convenient to compare status payloads in tests or across clients. The trailing `RegistrationMode` and `Version` parameters are optional in construction, allowing callers to supply just the core metrics while still producing a complete payload. This DTO isolates status representation from internal domain entities and keeps the shape stable for clients and tooling.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Minimal construction: Description omitted (use null)
|
||||||
|
var status = new ServerStatusDto("EchoHub", null, 12, 3);
|
||||||
|
|
||||||
|
// Full construction with explicit values
|
||||||
|
var statusFull = new ServerStatusDto("EchoHub", "Main gateway", 12, 3, "open", "1.2.0");
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 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 any file), enabling a message to carry zero or more attachments alongside its text content. The `Attachment` entity associates a downloadable resource with its parent [`Message`](Message.cs.md) via `MessageId` and, optionally, [`Message`](Message.cs.md), while storing the attachment's URL (`Url`), filename (`FileName`), size (`FileSize`), type (`Kind`), and an optional ASCII preview (`AsciiPreview`).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
Attachments decouple media from the textual content of a message, allowing the system to manage downloads, permissions, and encryption independently from the message body. The `AsciiPreview` provides a lightweight visual cue for image attachments, and its presence is influenced by how media is encrypted at rest or within channel scopes. The [`AttachmentKind`](AttachmentKind.cs.md) helps callers distinguish among images, audio, and other file types to apply appropriate handling.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The `Url` is a relative download path (for example, `/api/files/{fileId}`); clients should prefix it with the API base URL when constructing a full link.
|
||||||
|
- The `AsciiPreview` is null for non-image attachments and is stored encrypted at rest in encrypted channels.
|
||||||
|
- The `FileSize` is the number of bytes stored for the attachment and may reflect ciphertext size when encryption is enabled.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AttachmentKind
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/AttachmentKind.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum AttachmentKind
|
||||||
|
{
|
||||||
|
Image,
|
||||||
|
Audio,
|
||||||
|
File
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AttachmentKind is an enum that encodes how a message attachment should be rendered in the client. It enables rendering logic to pick the appropriate UI: for `Image` attachments, an ASCII image preview is shown; for `Audio`, a play control is exposed; and for `File`, a download line is presented. Use this enum when you need to branch rendering behavior based on the attachment's kind, instead of scattering rendering decisions across the codebase.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enum centralizes how attachments are presented, decoupling the attachment data from UI rendering code. It helps the rendering layer evolve independently (e.g., swapping ASCII previews or adding new affordances) without changing attachment structures.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Channel
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/Channel.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class Channel
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Channel models a chat channel within EchoHub's chat surface. It exposes an identifier `Id` (`Guid`), a required `Name` (`string`), an optional `Topic` (`string?`), and a flag `IsPublic` (`bool`) that defaults to `true`. The model also supports server-managed channels via `IsSystem` (`bool`), which are auto-created and read-only for all roles; users cannot create them. When a channel is password-protected, `PasswordHash` (`string?`) stores the hashed password. For end-to-end encryption, the envelope is represented by `EncryptionSalt` (`string?`) and `WrappedRoomKey` (`string?`), both client-generated so that the server never has access to the room content. Creation metadata is captured by `CreatedAt` (`DateTimeOffset`) and `CreatedByUserId` (`Guid`). The `Messages` collection (`List<Message>`) contains the related [`Message`](Message.cs.md) entities that belong to this channel.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ChannelMembership
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/ChannelMembership.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ChannelMembership
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `ChannelMembership` class is a simple data container that models the association between a user and a channel within the EchoHub system. It stores the `UserId`, the `ChannelId`, and the time the membership was created (`JoinedAt`), which defaults to the current UTC time if not specified.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Locates a specific user's membership in a channel and records when it happened. It serves as a lightweight linkage between `UserId` and `ChannelId`, with `JoinedAt` providing a timestamp of when the membership was established.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `JoinedAt` defaults to `DateTimeOffset.UtcNow` at object creation; when loading from a data store this value may be overridden by stored data, so rely on the persisted timestamp in that case.
|
||||||
|
- There are no invariants enforced here; enforce uniqueness and referential constraints at the database or repository layer.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# InviteCode
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/InviteCode.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class InviteCode
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
A data model representing a single registration invitation. When the server is configured with `Server:Registration = "invite"`, new accounts (REST and IRC alike) must present a valid, unexpired, and not-fully-used code to register. The `InviteCode` tracks the invitation's identity and policy: the persistent identifier `Id`, the required invitation value `Code` (marked `required` in the model), who created it (`CreatedByUserId` and `CreatedByUsername`), and when it was created (`CreatedAt`). The invitation may expire via `ExpiresAt` (null meaning it never expires), and its usage is controlled by `MaxUses` with current usage stored in `UseCount`. By default, a new invite is single-use (`MaxUses` = 1) and `CreatedAt` is initialized to the current UTC moment. This class is intended to be stored and consulted by the registration workflow to enforce invite-based onboarding.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Message
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/Message.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class Message
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents a single message in a channel, encapsulating the text payload, sender identity, timestamp, and any attachments. It serves as the core record for conversations and is designed to be persisted by the data layer and consumed by the UI to render threads and channel histories. The message may carry rich content via `EmbedJson` and can reference a previous message through `ReplyToMessageId` to model simple threading. The `Content` property is required, yet a message may legitimately have empty content if it carries attachments.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Message is the domain aggregate for a chat entry, linking to its [`Channel`](Channel.cs.md) via `ChannelId`/[`Channel`](Channel.cs.md) and to its sender via `SenderUserId`/`SenderUsername`. Attachments are modeled as a separate collection (`Attachments`), enabling a clean separation between textual payloads and media. Legacy fields (`Type`, `AttachmentUrl`, `AttachmentFileName`, `AttachmentFileSize`) exist solely to support a one-time startup data migration into the new attachments model; new writes should use the `Attachments` collection, and these legacy fields are not exposed in DTOs and are nulled after migration. The `ReplyToMessageId` enables basic threading by pointing to the message this one replies to, if any; downstream logic should gracefully handle references to messages that may have been deleted.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var message = new Message
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Content = "Welcome to the channel!",
|
||||||
|
SenderUserId = Guid.NewGuid(),
|
||||||
|
SenderUsername = "system",
|
||||||
|
ChannelId = Guid.NewGuid(),
|
||||||
|
Attachments = new List<Attachment>
|
||||||
|
{
|
||||||
|
new Attachment
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
MessageId = Guid.Empty,
|
||||||
|
Url = "https://example.com/file.png",
|
||||||
|
FileName = "file.png",
|
||||||
|
FileSize = 4096
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SentAt = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Legacy fields are for migration only; do not rely on them for new code.
|
||||||
|
- `SentAt` defaults to `DateTimeOffset.UtcNow` on instantiation; override if you have a specific send time.
|
||||||
|
- Use `EmbedJson` for optional rich content, and handle its absence gracefully in the UI.
|
||||||
@@ -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 payload within the model, enabling code to distinguish between textual content, images, files, and audio. Use `MessageType` to drive type-specific logic (rendering, validation, or serialization) by switching on the enum values rather than inspecting the payload directly.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing the variety of message payloads behind a single discriminator, `MessageType` makes it easier to extend support for new kinds. Renderers, validators, and serializers can rely on this enum to route behavior without peeking into payload internals, promoting cleaner separation of concerns.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- When adding a new member to `MessageType`, update all switch expressions that handle the enum to avoid unhandled values at runtime. Prefer exhaustiveness to catch omissions at compile time.
|
||||||
|
- Do not repurpose existing values; if the meaning changes, introduce a new member to preserve backward compatibility and serialization stability.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# RefreshToken
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/RefreshToken.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class RefreshToken
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents a `RefreshToken` that carries the metadata and state needed to sustain a user session via token-based authentication. It encapsulates the token hash, the owning user, expiration, and revocation data, and exposes simple predicates to answer the token's current validity. The `TokenHash` is marked `required`, guaranteeing a hash is provided during initialization; `CreatedAt` records when the token was created (defaulting to `DateTimeOffset.UtcNow`); `ExpiresAt` defines when the token becomes invalid; `RevokedAt` records a revocation timestamp when the token is revoked. The computed properties `IsExpired`, `IsRevoked`, and `IsActive` reflect the token's lifecycle status, so callers can check validity without inspecting each field. The [`User`](User.cs.md) navigation property links the token to its owner for convenient data access in domain services or ORMs.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- `DateTimeOffset`
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Architecturally, this symbol serves as the boundary for token-based authentication in the domain. It centralizes lifecycle logic (expiry and revocation) into a single place, enabling consistent checks via `IsActive` across services. The presence of [`User`](User.cs.md) further supports straightforward navigation to the owner, which is helpful when presenting token data in dashboards or auditing scenarios.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Example: creating a new `RefreshToken` (TokenHash is required)
|
||||||
|
Guid userId = Guid.NewGuid();
|
||||||
|
var token = new RefreshToken
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
TokenHash = "sha256-abc123",
|
||||||
|
UserId = userId,
|
||||||
|
ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Be aware that `CreatedAt` is initialized to the current UTC time at construction. If you load an existing token from storage, ensure the stored value for `CreatedAt` is preserved.
|
||||||
|
- `IsActive` depends on both `IsExpired` and `IsRevoked`. If you set `RevokedAt` but forget to update `IsRevoked`, the token might appear active.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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 a user holds within a server in EchoHub. It categorizes users into distinct permission tiers: `Member`, `Mod`, `Admin`, and `Owner`, which are used to drive authorization and feature availability without scattering numeric checks throughout the codebase.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This enum provides a stable abstraction for role-based access control, allowing components to reason about capabilities (moderation, configuration, ownership) by comparing against `ServerRole` values. Centralizing roles reduces duplication of permission logic and helps ensure consistent authorization across command handlers, UI components, and services. It also offers an extension point: adding a new role or reordering the hierarchy can be localized to this enum and its consumers.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ServerStatsReport
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/ServerStatsReport.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ServerStatsReport
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ServerStatsReport is a snapshot of server activity for a single reporting window, produced periodically by the `stats-report` background job. It records when the report was generated, the start and end of the window, the window length in hours, and a set of per-window activity counters (`MessagesSent`, `FilesUploaded`, `BytesUploaded`, `NewMembers`, `ActiveMembers`, `Connections`, `Disconnections`, `Kicks`, `Bans`) as well as end-of-window totals (`TotalMembers`, `OnlineNow`, `PeakOnline`); the report is serialized as pretty-printed JSON and persisted for historical trend analysis. The window is defined as "since the previous report" (or since startup for the first report).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
ServerStatsReport serves as the canonical persisted unit for time-bounded server activity, decoupling the reporting job from storage and analytics. It combines both within-window activity and end-of-window aggregates to support dashboards, trend charts, and anomaly detection across multiple windows. As a plain data container, it is populated by the reporting process and then written to the data store; its structure is stable to ensure reliable longitudinal comparisons.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- GeneratedAt is intended to reflect the moment the window ended; ensure GeneratedAt is kept in sync with PeriodEnd to avoid confusion (GeneratedAt should effectively equal PeriodEnd when the report is produced).
|
||||||
|
- PeriodEnd should be greater than or equal to PeriodStart; WindowHours should be non-negative.
|
||||||
|
- BytesUploaded uses a 64-bit signed integer; extremely large attachment activity should still stay within `BytesUploaded`'s range to avoid overflow.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# User
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/User.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class User
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents a user entity in the EchoHub domain, aggregating identity, profile, presence, and lifecycle data. It is the primary model used when creating, retrieving, and persisting user information, with required credentials enforced at construction via the `required` modifiers on `Username` and `PasswordHash`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Designed to be a single source of truth for user state, it coordinates authentication, authorization via `Role`, and moderation flags such as `IsMuted` and `IsBanned`. The default values — `Status` set to `UserStatus.Online`, `Role` set to `ServerRole.Member`, and timestamps on creation — provide sensible startup behavior while keeping optional fields available for richer profiles. It serves as the canonical user payload across core services and data stores, reducing duplication and drift between layers.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Username = "jdoe",
|
||||||
|
PasswordHash = "pbkdf2$...",
|
||||||
|
DisplayName = "Jane Doe",
|
||||||
|
Status = UserStatus.Online,
|
||||||
|
Role = ServerRole.Member
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `required` modifier on `Username` and `PasswordHash` enforces initialization when constructing a `User` via object initializers (compile-time check).
|
||||||
|
- `CreatedAt` and `LastSeenAt` default to the moment of object creation but may be replaced by deserialized data from storage.
|
||||||
|
- `MutedUntil` is meaningful only when `IsMuted` is true; it can be null if not muted.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# UserStatus
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Models/UserStatus.cs`
|
||||||
|
> **Kind:** enum
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public enum UserStatus
|
||||||
|
{
|
||||||
|
Online,
|
||||||
|
Away,
|
||||||
|
DoNotDisturb,
|
||||||
|
Invisible
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Represents a user's presence status within the application, guiding UI rendering, presence-based filtering, and notification behavior. The enum exposes four discrete states: `Online`, `Away`, `DoNotDisturb`, and `Invisible` to express typical availability scenarios.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Centralizing presence into `UserStatus` prevents scattered string values or boolean flags across the codebase, promoting consistent semantics for how users are shown and how presence-related logic runs. It also future-proofs the API by allowing new statuses to be added without changing call-sites that consume the type. This enum typically intersects with UI components that render status indicators and with services that filter or route behavior based on a user's current state.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Changing the set of statuses (adding/removing/reordering enum members) is a breaking change that can affect serialization, persistence, and cross-boundary API compatibility; prefer backward-compatible extensions by adding new members rather than reordering existing ones.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# RoomCrypto
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Security/RoomCrypto.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class RoomCrypto
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Client-side utilities for envelope encryption used by private (end-to-end encrypted) channels. Use `RoomCrypto` when you need a simple, opinionated way to derive keys from a passphrase, generate a random room content key (RCK), and encrypt/decrypt room content in the wire format this project uses (a `$RC1$`-prefixed base64 blob for text and a nonce||tag||ciphertext blob for raw bytes).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`RoomCrypto` implements the client-side half of an envelope-encryption scheme: the client generates a random 256-bit room content key (RCK) that actually encrypts all channel content, and a key-encryption key (KEK) derived from the user's passphrase is used to wrap the RCK before the wrapped RCK is stored on the server. The derivation uses PBKDF2-SHA256 with `Pbkdf2Iterations` (210000) and a `SaltSizeBytes` (16) salt; the resulting 64 bytes are split so the first `KeySizeBytes` (32) bytes are exported as a lowercase hex `AuthKeyHex` (the join credential) and the last `KeySizeBytes` bytes are kept as the `KeyEncryptionKey`. `RoomCrypto` keeps a small, explicit surface: `GenerateSalt`, `GenerateRoomKey`, `DeriveKeys`, `EncryptText`, `TryDecryptText`, `IsRoomCiphertext`, and byte-level `EncryptBytes`/`DecryptBytes` (used internally). The text wire format is the literal `CiphertextPrefix` (`"$RC1$"`) followed by `Convert.ToBase64String(nonce||tag||ciphertext)`; binary APIs return/expect the raw `nonce||tag||ciphertext` blob. The implementation zeroes the slice of derived bytes used for the auth key after converting to hex to reduce exposure of sensitive material.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// Typical client flow: derive keys from a passphrase, create a room key, encrypt and decrypt text.
|
||||||
|
var salt = RoomCrypto.GenerateSalt();
|
||||||
|
var derived = RoomCrypto.DeriveKeys("correct horse battery staple", salt);
|
||||||
|
// `derived.AuthKeyHex` is sent to the server as the join credential; `derived.KeyEncryptionKey` stays local.
|
||||||
|
var roomKey = RoomCrypto.GenerateRoomKey();
|
||||||
|
|
||||||
|
var ciphertext = RoomCrypto.EncryptText("hello room", roomKey);
|
||||||
|
if (RoomCrypto.TryDecryptText(ciphertext, roomKey, out var plaintext))
|
||||||
|
{
|
||||||
|
// plaintext == "hello room"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `RoomCrypto` expects a `KeySizeBytes`-length key (32 bytes) for its AES-GCM operations; supplying a key of the wrong length will fail when constructing the cipher.
|
||||||
|
- Nonces are randomly generated per-encryption (`NonceSizeBytes` = 12). Do not reuse a `roomKey`/nonce pair for different plaintexts; the implementation already generates random nonces, so avoid reusing the same nonce manually.
|
||||||
|
- `TryDecryptText` returns `false` (and sets `plaintext` to empty) both for non-room content (missing the `CiphertextPrefix`) and for any integrity/format errors (bad base64, authentication failure).
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# AsciiBannerService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Services/AsciiBannerService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class AsciiBannerService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
AsciiBannerService renders a string as a five-row block-character banner using a hand-authored, figlet-style font defined entirely in code. It is self-contained — no dependencies and no network access — and returns plain text suitable for transport or encryption just like any other message. Use `Render` when you need a compact, dependency-free banner for logs, UI previews, or console-like output.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
AsciiBannerService provides a centralized, self-contained banner rendering capability that does not rely on external resources. The glyphs are embedded in a private `Font` dictionary, ensuring deterministic rendering across environments. The banner width is bounded by `MaxInputLength` (20 characters) and the height is fixed to `Rows` (5), which keeps banners predictable in size and performance. Input is normalized by converting to uppercase with `ToUpperInvariant()`, and only characters present in `Font` are rendered; unsupported characters are skipped. The final output is assembled with a `StringBuilder`, joining glyph rows horizontally with spaces and replacing `#` (ink) with the solid block character `█` and `.` (blank) with spaces. Trailing spaces on each line are trimmed to minimize payload.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var banner = AsciiBannerService.Render("TEST");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Returns `null` when the input is empty, whitespace, or contains no renderable characters.
|
||||||
|
- Non-renderable characters are skipped; only characters present in `Font` contribute to the banner.
|
||||||
|
- The input is capped at `MaxInputLength` characters, and the output always consists of exactly `Rows` lines if renderable content exists.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# FileValidationHelper
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Services/FileValidationHelper.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class FileValidationHelper
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
FileValidationHelper is a compact utility that centralizes quick, non-destructive checks for media file types. It exposes `IsValidImage(Stream)` to determine if the provided stream represents a known image format by peeking at its header bytes, while always restoring the stream's original position. It also exposes `IsAudioFile(string)` to decide whether a file name uses one of the recognized audio extensions. Use these helpers to validate inputs in upload or ingestion paths without loading or parsing full files, and to keep format-detection logic consistent across the codebase.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By coalescing the magic-byte checks in one place, this abstraction reduces duplication and the risk of inconsistent format handling across components that ingest media. The detection rules cover JPEG, PNG, GIF, and WebP at the header level, with WebP requiring a RIFF header followed by the WebP tag; the private helper `StartsWith` encapsulates the prefix comparison to keep `IsValidImage` focused on intent. The `AudioExtensions` set drives a fast, case-insensitive extension lookup for `IsAudioFile` without touching disk data.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
byte[] header = new byte[] { 0xFF, 0xD8, 0xFF };
|
||||||
|
using var ms = new MemoryStream(header);
|
||||||
|
bool isImage = FileValidationHelper.IsValidImage(ms);
|
||||||
|
|
||||||
|
bool isAudio = FileValidationHelper.IsAudioFile("song.MP3");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The stream passed to `IsValidImage` must be seekable; non-seekable streams will not have their position reset and may lead to false results.
|
||||||
|
- `IsAudioFile` performs a purely extension-based check and does not inspect file contents.
|
||||||
|
- The image-detection logic recognizes specific headers (JPEG, PNG, GIF, WebP) and is not a full format validator; for strict validation, perform content analysis beyond these checks.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# ImageToAsciiService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Core/Services/ImageToAsciiService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ImageToAsciiService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
ImageToAsciiService converts an image stream into ASCII art using two vertical pixels per character and half-block characters. The static `GetDimensions` translates a size code (`'s'`, `'m'`, `'l'`) into ASCII art dimensions (40x40, 80x80, 120x120 respectively) and returns the default dimensions from `HubConstants.AsciiArtWidth` and `HubConstants.AsciiArtHeightHalfBlock` for other codes. The instance method `ConvertToAscii` accepts a `Stream` containing an image and returns a string composed of color tokens and block characters. Each character cell encodes two vertical pixels; a foreground color token `{F:RRGGBB}` and a background color token `{B:RRGGBB}` are emitted when colors change, followed by a block character (either `█` or `▀`), with `{X}` used to reset coloring. The output uses only printable ASCII and avoids terminal escape sequences.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`ImageToAsciiService` encapsulates the image-to-ASCII rendering logic, separating it from image loading and presentation concerns. It centralizes the color-token encoding and block-character strategy so callers can produce text-based previews in environments that cannot render images. By relying on [`HubConstants`](../Constants/HubConstants.cs.md) for defaults, global rendering preferences propagate naturally to this converter.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
using var fs = File.OpenRead("path/to/image.png");
|
||||||
|
var service = new ImageToAsciiService();
|
||||||
|
string ascii = service.ConvertToAscii(fs);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The converter emits color-change tokens only when the foreground or background color differs from the previous pixel pair, which keeps the output compact for large areas of uniform color.
|
||||||
|
- Each ASCII cell represents two vertical pixels; the image is resized to the requested `width` and `height` (defaulting to `HubConstants.AsciiArtWidth` and `HubConstants.AsciiArtHeightHalfBlock` if not specified). This can alter aspect ratio, so choose dimensions with that in mind.
|
||||||
|
- The format relies on the tokenized color syntax (e.g. `{F:RRGGBB}` and `{B:RRGGBB}`) being understood by the consumer; renderers that ignore these tokens will display plain block characters without color.</
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# IrcBroadcaster
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcBroadcaster.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class IrcBroadcaster : IChatBroadcaster
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Bridges application chat events into IRC wire-protocol messages and sends them to connected IRC clients. Use `IrcBroadcaster` when you need chat activity (messages, joins/parts, kicks/bans, topic changes) reflected on an IRC gateway so traditional IRC clients see the room as an IRC channel; prefer using a plain chat broadcaster when you do not need IRC-formatted output.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IrcBroadcaster` is an [`IChatBroadcaster`](../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) implementation that adapts the application's chat model to IRC conventions. It pulls active connections from the [`IrcGatewayService`](IrcGatewayService.cs.md) (`GetConnectionsInChannel` / `GetAllConnections`), formats user-visible text with `IrcMessageFormatter.FormatMessage` (using the gateway `Options.PublicBaseUrl` for absolute links), and sends one or more IRC lines to each connection via `conn.SendAsync`. Because IRC clients cannot handle app-layer encryption, the broadcaster uses the injected [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) to decrypt transport-encrypted payloads before formatting; end-to-end room ciphertext markers (`$RC1$`) are left intact. The implementation also follows IRC conventions for echo suppression (it excludes the originating connection by `excludeConnectionId`) and for addressing (matching by `ConnectionId` or `Nickname` where appropriate).
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
// given existing instances of IrcGatewayService and IMessageEncryptionService
|
||||||
|
var gateway = /* existing IrcGatewayService */;
|
||||||
|
var encryption = /* existing IMessageEncryptionService */;
|
||||||
|
var broadcaster = new IrcBroadcaster(gateway, encryption);
|
||||||
|
|
||||||
|
// broadcast a message to the #general channel but don't echo back to the sender
|
||||||
|
await broadcaster.SendMessageToChannelAsync("general", messageDto, excludeConnectionId: "conn-123");
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `IrcBroadcaster` explicitly decrypts transport-layer encrypted content using the injected [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md); this is necessary because IRC clients do not support the application's app-layer encryption. Messages that contain E2E room ciphertext markers (`$RC1$`) are preserved as-is.
|
||||||
|
- Echo suppression is performed by comparing `conn.ConnectionId` to the provided `excludeConnectionId`. This avoids re-sending a message to the origin connection while still delivering it to other sessions belonging to the same user (different connections/nicknames).
|
||||||
|
- `IrcMessageFormatter.FormatMessage` may split a single logical message into multiple IRC lines; each resulting line is sent individually with `conn.SendAsync`, so large messages can result in multiple network writes.
|
||||||
|
- `SendUserStatusChangedAsync` is a no-op because IRC lacks an active presence broadcast; clients discover away/idle state via `WHO`/`WHOIS` instead.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# IrcClientConnection
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcClientConnection.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class IrcClientConnection : IAsyncDisposable
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Manages a single IRC-over-TCP client connection: reads and writes CRLF-delimited lines, holds simple registration state, and tracks joined channels. Use `IrcClientConnection` when the server needs a lightweight, per-socket representation of a connected IRC client (rather than passing raw `TcpClient`/`Stream` around).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IrcClientConnection` encapsulates the I/O and minimal protocol state for one IRC client. It creates `StreamReader`/`StreamWriter` pair configured with UTF-8 without BOM (via `UTF8Encoding`) and `StreamWriter` options `AutoFlush` and `NewLine = "\r\n"` so callers can read/write logical IRC lines. Outgoing writes are serialized using the private `SemaphoreSlim` (`_writeLock`) so concurrent senders do not interleave data. Channel membership is stored in the private `_joinedChannels` and guarded by `_channelLock`, allowing safe reads by broadcaster threads while command handlers mutate the set. Connection identity is exposed through `ConnectionId` (prefixed with `HubConstants.IrcConnectionIdPrefix` and a `Guid`), and a simple `Hostmask` string is provided based on `Nickname` and `Username`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- `ReadLineAsync` returns `null` on error or when the underlying read fails; callers must treat `null` as a disconnected/error condition rather than a valid empty line.
|
||||||
|
- `SendAsync` and the numeric helpers (`SendNumericAsync`) swallow exceptions raised while writing (connection loss is silently ignored), so sending failures will not throw — design choice to avoid bubbling socket errors to callers.
|
||||||
|
- Only channel-related state (`_joinedChannels`) is synchronized. Properties such as `Nickname`, `Username`, `IsRegistered`, `IsAuthenticated`, and `AwayMessage` are not individually thread-safe; if you access them concurrently from multiple threads, add external synchronization.
|
||||||
|
- `DisposeAsync` closes the underlying `TcpClient` and disposes the reader, writer, and `_writeLock`, but does not attempt to coordinate or await in-flight operations beyond disposing those resources.
|
||||||
@@ -0,0 +1,790 @@
|
|||||||
|
# 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)
|
||||||
|
- [HandleWhoisAsync](#handlewhoisasync)
|
||||||
|
- [IrcToEchoHubChannel](#irctoechohubchannel)
|
||||||
|
- [RequireRegisteredAsync](#requireregisteredasync)
|
||||||
|
- [RunAsync](#runasync)
|
||||||
|
- [SendChannelTopicAsync](#sendchanneltopicasync)
|
||||||
|
- [SendModeErrorAsync](#sendmodeerrorasync)
|
||||||
|
- [SendMotdAsync](#sendmotdasync)
|
||||||
|
- [SendNamesReplyAsync](#sendnamesreplyasync)
|
||||||
|
- [SendWelcomeBurstAsync](#sendwelcomeburstasync)
|
||||||
|
- [TryCompleteRegistrationAsync](#trycompleteregistrationasync)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## IrcCommandHandler
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class IrcCommandHandler
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Handles and dispatches IRC client commands for a single connected client and bridges them to the server-side chat services. Reach for `IrcCommandHandler` when you need an adapter that translates IRC commands (registration, authentication, channel operations, queries like `NAMES`/`WHOIS`, messaging) into calls on the backend services ([`IChatService`](../EchoHub.Core/Contracts/IChatService.cs.md), [`IUserService`](../EchoHub.Core/Contracts/IUserService.cs.md), [`IChannelService`](../EchoHub.Core/Contracts/IChannelService.cs.md)) and emits the corresponding IRC replies (welcome burst, MOTD, topic/NAMES lists, etc.).
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IrcCommandHandler` is the protocol-layer coordinator for an IRC gateway: it receives parsed [`IrcMessage`](IrcMessage.cs.md) instances from the [`IrcClientConnection`](IrcClientConnection.cs.md), interprets IRC semantics (registration flows, SASL vs PASS authentication, channel join/part, PMs, queries), and invokes the appropriate backend services and helpers ([`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) for decrypting history, `ILogger` for diagnostics). The class groups responsibilities into logical regions — authentication, welcome/MOTD, channel operations, and query commands — and exposes a long-running `RunAsync` loop driven by a `CancellationToken` to process client messages until the connection ends. Many of the private handlers (`HandleCapAsync`, `HandleAuthenticateAsync`, `HandleNickAsync`, `HandleUserAsync`, `HandleJoinAsync`, `HandlePrivmsgAsync`, `HandleNamesAsync`, `HandleTopicAsync`, `HandleWhoisAsync`, etc.) encapsulate the IRC-to-service mapping and the reply generation.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Registration/authentication ordering is important: the handler supports both SASL (`AUTHENTICATE`) and legacy `PASS` flows and contains explicit fallbacks (e.g. try to auto-register on auth failure). Callers should not assume a client is fully registered until the registration completion path in `TryCompleteRegistrationAsync` completes.
|
||||||
|
- Encrypted and system channels are treated specially: the handler deliberately prevents joining channels that cannot be safely proxied (end-to-end encrypted rooms or server-only system channels), and history replay requires decrypting messages via [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md) before sending them to the IRC client.
|
||||||
|
- All public operations are async and driven by `RunAsync(CancellationToken)`: callers should respect the `CancellationToken` and be prepared for async exceptions to surface from the handlers; the class uses `ILogger` for recording failures and important state transitions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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` | — |
|
||||||
|
|
||||||
|
|
||||||
|
Initializes a new `IrcCommandHandler` by injecting all required dependencies: [`IrcClientConnection`](IrcClientConnection.cs.md), [`IrcOptions`](IrcOptions.cs.md), [`IChatService`](../EchoHub.Core/Contracts/IChatService.cs.md), [`IUserService`](../EchoHub.Core/Contracts/IUserService.cs.md), [`IChannelService`](../EchoHub.Core/Contracts/IChannelService.cs.md), [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md), and `ILogger`. The constructor stores these in private fields so the command handler can coordinate chat, user, and channel operations, apply encryption, and log activity when processing IRC commands. This initialization pattern is typically used by the dependency injection container or in tests to assemble a fully wired, ready-to-run handler.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
The constructor serves as a wiring point that decouples `IrcCommandHandler` from concrete implementations, enabling substitution in tests and different runtime configurations. By wiring `_conn`, `_options`, `_chatService`, `_userService`, `_channelService`, `_encryption`, and `_logger`, it ensures the handler has immediate access to the resources needed to parse and route IRC commands, manage users and channels, apply encryption, and emit logs. It does not execute command logic itself; its purpose is to provide a fully initialized, ready-to-use instance for later operation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ServerName
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
> **Kind:** property
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private string ServerName => _options.ServerName
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The `ServerName` property is a private, read-only accessor that forwards to `_options.ServerName` to obtain the configured IRC server name. It serves as an internal convenience within the `IrcCommandHandler` class, enabling consistent access to the server name without coupling to the `_options` object.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This private indirection isolates the `IrcCommandHandler` from changes to where the server name is stored. If `_options`' structure changes or the server name is sourced from elsewhere, update only this member and keep the rest of the class intact. It also clarifies intent by naming and exposing the concept of 'server name' as a single retrieval point for internal command handling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 the SASL authentication flow for an IRC connection. When invoked, it interprets the first parameter to drive a SASL PLAIN exchange: it can prompt the client to provide credentials, abort SASL, or process a base64-encoded payload to authenticate or register a user, updating the connection state on success and replying with appropriate IRC numerics on failure. The method encapsulates the end-to-end SASL Plain handling, including error signaling and logging for traceability.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method centralizes the SASL PLAIN authentication handshake for a client, coordinating between the incoming [`IrcMessage`](IrcMessage.cs.md) payload, the server connection state (`_conn`), the user service (`_userService`), and server numerics. It performs decoding and validation of the SASL payload, derives a username from the payload, and attempts authentication first, then automatic registration as a fallback. On success, it binds the authenticated user to the connection (setting `Nickname`, `UserId`, and `IsAuthenticated`) and notifies the client with both `RPL_LOGGEDIN` and `RPL_SASLSUCCESS`. The structured exception handling ensures a consistent failure path with an `ERR_SASLFAIL` response and logging for operational visibility.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Malformed SASL payloads (e.g., payloads that do not yield at least three parts after decoding) trigger an authentication failure early, signaling to the client via `ERR_SASLFAIL`.
|
||||||
|
- If initial authentication fails, the flow transparently attempts to register a new user with the extracted credentials; if registration also fails, it reports the error back to the client and logs a warning.
|
||||||
|
- The password is sourced from the SASL PLAIN payload; ensure that credential handling complies with your security requirements and that secrets are managed appropriately within the `_userService`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
This private async method handles the AWAY command for the current connection. After confirming the user is registered via `RequireRegisteredAsync`, it checks for a non-empty first parameter on `msg.Parameters`; if present, it stores the away message on ``_conn`` (i.e., `_conn`), updates the user's status to `UserStatus.Away` via `_chatService`, and sends the `IrcNumericReply.RPL_NOWAWAY`. If no message is supplied, it clears the away message, updates the status to `UserStatus.Online`, and sends the `IrcNumericReply.RPL_UNAWAY`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method centralizes away-state management for the connected user by coordinating the connection state (`_conn`), persistence/update semantics (`_chatService`), and client feedback via numeric replies ([`IrcNumericReply`](IrcNumericReply.cs.md)). It ensures that providing an away message both reflects in server-side status and informs the client promptly.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Rapid, repeated calls may race with the chat service updates; consider sequencing on the caller side or adding concurrency guards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 IRC CAP negotiation messages related to SASL authentication. It inspects the first element of `msg.Parameters` to drive a small, centralized CAP flow: starting negotiation with `LS`, acknowledging or declining a SASL request with `REQ`, and ending negotiation with `END`. The method updates internal connection state via `_conn.CapNegotiating`, `_conn.IsSasl`, and coordinates with registration by triggering `TryCompleteRegistrationAsync()` when appropriate. Messages are sent back to the server using `_conn.SendAsync`, built from the current `ServerName` (e.g. `":{ServerName} CAP * LS :sasl"`) and reflecting the outcome of each branch. The logic short-circuits on insufficient parameters and handles case-insensitive comparisons for SASL requests.
|
||||||
|
|
||||||
|
The typical flow is:
|
||||||
|
- LS starts capability negotiation and marks the connection as negotiating.
|
||||||
|
- REQ sasl acknowledges SASL capability and enables SASL, while any other requested capability prompts a NAK with the requested name.
|
||||||
|
- END ends negotiation and, if credentials are present (non-null `Nickname` and `Username`) but the client is not yet registered, proceeds to complete registration via `TryCompleteRegistrationAsync()`.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
Handles an incoming IRC command by normalizing the textual command to upper-case and dispatching to the corresponding asynchronous handler. As the central router, it maps pre-registration commands (such as `CAP`, `AUTHENTICATE`, `PASS`, `NICK`, [`USER`](../EchoHub.Core/Models/User.cs.md)) and post-registration commands (such as `PING`, `JOIN`, `PART`, `PRIVMSG`, `QUIT`, `NAMES`, `TOPIC`, `WHO`, `WHOIS`, `AWAY`, `LIST`, `MODE`, `MOTD`, and related aliases) to their dedicated `HandleXAsync` methods, returning the resulting `Task`. For the `PONG` case it completes synchronously with `Task.CompletedTask`; for any unknown command, it responds via `_conn.SendNumericAsync` using `IrcNumericReply.ERR_UNKNOWNCOMMAND` and the command text. This design provides a single, maintainable dispatch point that enforces consistent routing and error reporting across all IRC commands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
`HandleJoinAsync` processes the IRC `JOIN` command for a connected user. It validates that the user is registered, requires at least one channel parameter, and then parses comma-separated channel names with optional per-channel keys; for each channel it translates the raw IRC channel name to the internal channel identifier, blocks end-to-end encrypted channels (which must be joined via the EchoHub client) and system channels (server-managed) from IRC, delegates the actual join to `_chatService.JoinChannelAsync` with the user's connection and identity, and on success updates the connection state, announces the join, and replays the channel topic, NAMES list, and decrypted history to the IRC client. On failure, it returns the appropriate IRC error (e.g. `ERR_BADCHANNELKEY` or `ERR_NOSUCHCHANNEL`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
HandleListAsync processes the IRC LIST command by emitting the list of public channels to the connected client. It first ensures the caller is registered via `RequireRegisteredAsync()`; if not, it returns immediately. It then retrieves the channel collection from `_channelService.GetChannelListAsync()` and sends an `RPL_LIST` line for each channel that has `IsPublic` set to true, formatting the line as `#{ch.Name} {ch.OnlineCount} :{lockHint}{ch.Topic ?? ""}` where `lockHint` is `[+k] ` when `IsProtected` is true. After enumerating all public channels, it issues `RPL_LISTEND` with End of LIST to finish.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By filtering to `IsPublic` channels, private channels are hidden from discovery, aligning the server's LIST output with the SignalR client's channel exposure. The `[+k]` indicator communicates a protected channel requiring a key and is propagated in the line alongside the channel's `Topic` (or an empty string if no topic is set). This method coordinates a read-only view of channel state and relies on [`IrcNumericReply`](IrcNumericReply.cs.md)-provided numeric codes (`RPL_LIST` and `RPL_LISTEND`).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The handler short-circuits if the user is not registered, so no LIST data is sent to unregistered users.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
Handles the IRC `MODE` command for a target in the gateway. It first ensures the caller is registered via `RequireRegisteredAsync` and returns user-mode information with `RPL_UMODEIS` when the target isn’t a channel, emitting a leading `+` in that case. For channel targets, it resolves the internal channel name with `IrcToEchoHubChannel`, validates the channel, and then either reports the current mode with `RPL_CHANNELMODEIS` or processes mode changes such as ban-list probes (`b`/`+b`) and password changes (`+k`/`-k`) by delegating to `_channelService.SetChannelPasswordAsync` and broadcasting results through `_conn`; unknown modes yield `ERR_UNKNOWNMODE`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 is a private asynchronous method that processes a NAMES query. It begins by verifying the caller is registered using `RequireRegisteredAsync()`, returning early if not. It then validates that a parameter is provided (`msg.Parameters.Count < 1`); if not, it returns. The first parameter is converted to the internal channel name by `IrcToEchoHubChannel`, and if this conversion yields `null`, the method exits. Otherwise, it calls `SendNamesReplyAsync(channelName)` to emit the names list for the channel.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method encapsulates the precondition checks for name-related queries and centralizes the channel-name translation, keeping the response logic contained in `SendNamesReplyAsync`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If `IrcToEchoHubChannel` cannot map the input to a channel, no response is sent.
|
||||||
|
- The method relies on `Parameters` being provided by [`IrcMessage`](IrcMessage.cs.md) and uses early returns to avoid unnecessary work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
`HandleNickAsync` handles the NICK command by validating input and updating the connection state. If no nickname is supplied, it sends `ERR_NONICKNAMEGIVEN` with a "No nickname given" message. If the nickname fails the policy check against `ValidationConstants.UsernameRegex()`, it responds with `ERR_ERRONEUSNICKNAME` and a descriptive error like "Erroneous nickname (must be 3-50 chars: a-z, 0-9, _, -)". On success, it normalizes the nickname to lowercase via `ToLowerInvariant()` and assigns it to `_conn.Nickname`. Finally, if the connection is not yet registered but already has a `Username`, it advances the registration by calling `TryCompleteRegistrationAsync()`.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By encapsulating parameter validation, nickname syntax enforcement, normalization, and the progression toward registration, this method centralizes the Nick command workflow. It coordinates with `_conn` to store the chosen nickname, uses [`IrcNumericReply`](IrcNumericReply.cs.md) values to emit exact IRC error codes for invalid or missing nicknames, and triggers `TryCompleteRegistrationAsync()` when appropriate, ensuring a cohesive startup sequence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
Handles a PART command from a registered IRC client by parsing a comma-separated list of channels from the first parameter and an optional part message from the second parameter, then leaving each channel both in the internal chat state and by sending an IRC PART message back to the client.
|
||||||
|
|
||||||
|
Channels are mapped from their raw IRC name to the internal Echo Hub channel via `IrcToEchoHubChannel`; invalid mappings are skipped. For each valid channel, the method first awaits `_chatService.LeaveChannelAsync(_conn.ConnectionId, _conn.Nickname!, channelName)`, then updates the local connection state with `_conn.LeaveChannel(channelName)`, and finally emits the IRC PART notice `":{_conn.Hostmask} PART #{channelName}"` with an optional payload appended if a part message was supplied.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Coordinates internal state with the external IRC protocol to keep the user’s channel memberships in sync across both domains. The `IrcToEchoHubChannel` mapping acts as a guardrail, ensuring only recognized channels are processed and leaving others untouched.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If `IrcToEchoHubChannel` yields `null` for a channel, that channel is ignored rather than causing an exception.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 processes the IRC PASS command for the current connection. If the connection is already registered (`_conn.IsRegistered`), it replies with `ERR_ALREADYREGISTERED` by calling `_conn.SendNumericAsync(ServerName, IrcNumericReply.ERR_ALREADYREGISTERED, ":You may not reregister")`; if a password parameter is provided, it stores the password on the connection (the exact storage is redacted in the source). The method always completes by returning a `Task`—the send task when replying, or `Task.CompletedTask` when no action is needed.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Centralizes PASS command handling within the `IrcCommandHandler` and enforces the re-registration guard in one place. It updates the connection state when a parameter is present, separating command validation from the subsequent authentication flow.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The method is not declared `async`; it returns a `Task` and may complete synchronously via `Task.CompletedTask` when no password parameter is supplied.
|
||||||
|
- The password value is written to a connection field whose exact name is redacted; handling of this sensitive data should be reviewed in the surrounding authentication flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
`HandlePingAsync` handles an IRC `PING` by replying with a `PONG` to keep the connection alive. It reads the first parameter from the incoming `IrcMessage.Parameters` as the token, or falls back to `ServerName` if none is provided, and sends the response via `_conn.SendAsync` using the IRC format `":{ServerName} PONG {ServerName} :{token}"`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 an IRC PRIVMSG from a connected user by first ensuring the user is registered, then validating that the message has enough parameters and targets a channel. If parameters are missing, it replies with `ERR_NEEDMOREPARAMS`; if the target is not a channel (does not start with `#`), it replies with `ERR_NOSUCHNICK` and instructs to use channels. It then maps the IRC channel to an internal EchoHub channel via `IrcToEchoHubChannel` and forwards the message content to the chat service through [`SendMessageAsync`](../EchoHub.Server/Services/ChatService.cs.md), supplying the current connection's user id, nickname, channel name, message content, and connection id. If the chat service reports an error, it communicates it back to the client with `ERR_CANNOTSENDTOCHAN` for the affected channel.
|
||||||
|
|
||||||
|
In short, it acts as the IRC surface to the EchoHub chat layer for channel-based private messages, performing parameter validation, channel resolution, and error propagation in a single, cohesive flow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
HandleQuitAsync is a private async method that processes a quit request by sending an IRC `ERROR` message to close the client connection. It derives the quit reason from the first element of the [`IrcMessage`](IrcMessage.cs.md)'s `Parameters` (falling back to the literal `Client quit` if none is provided) and includes the nickname via `_conn.Nickname` in the response by calling `_conn.SendAsync` with the string `ERROR :Closing Link: <nickname> (<quitMessage>)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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 an IRC TOPIC command for a channel. It ensures the caller is registered, validates parameters, converts the IRC channel name to the internal channel via `IrcToEchoHubChannel`, and then either returns the current topic with `SendChannelTopicAsync` when only the channel is provided or updates the topic via `_channelService.UpdateTopicAsync` using the current user's ID, passing `null` for an empty topic. If the update fails, it replies with an IRC numeric error—`ERR_NOSUCHCHANNEL` when the channel is not found, otherwise `ERR_CHANOPRIVSNEEDED`—including the error message; on success, it broadcasts the updated channel to connected web clients via `_chatService.BroadcastChannelUpdatedAsync` and echoes the new topic back to the IRC client with `SendAsync` using the `TOPIC` command.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method centralizes the TOPIC command flow: it validates the caller, resolves the channel, performs the update, and coordinates notification to both SignalR clients and the IRC client. It also maps domain errors to IRC numeric replies to preserve protocol semantics across layers.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
HandleUserAsync is the private asynchronous handler for processing the IRC USER command as part of the client registration flow. It first guards against re-registration by sending the IRC numeric `ERR_ALREADYREGISTERED` via `_conn.SendNumericAsync` when `_conn.IsRegistered` is true, and then returns. If there are fewer than four parameters, it responds with `ERR_NEEDMOREPARAMS` and terminates early. When invoked with a valid parameter set, it assigns the username from `msg.Parameters[0]` to `_conn.Username` and the real name from `msg.Parameters[3]` to `_conn.RealName`. Finally, if a nickname has already been established (`_conn.Nickname` is not null), it awaits `TryCompleteRegistrationAsync()` to advance the registration process.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
HandleWhoAsync processes a WHO request for a channel by validating the caller and translating the IRC channel into the EchoHub channel, then streaming the current online users. It first ensures the client is registered, validates that a channel parameter is provided, and derives the internal channel name with `IrcToEchoHubChannel`. If any of these steps fail, it exits without emitting data. When a valid channel is obtained, it fetches online users via `_chatService.GetOnlineUsersAsync(channelName)` and, for each user, sends a `RPL_WHOREPLY` using [`IrcNumericReply`](IrcNumericReply.cs.md) data, encoding the channel, user, host (`echohub`), server, user nickname, away state, hop count, and display name (falling back to the username when necessary). After listing all users, it signals completion with `RPL_ENDOFWHO`.
|
||||||
|
|
||||||
|
This method is the IRC-facing surface that translates EchoHub's online-user model into IRC protocol replies, making it the point of integration for WHO-style channel listings. The flow is fully asynchronous and relies on the [`UserStatus`](../EchoHub.Core/Models/UserStatus.cs.md) enum to determine the away flag, as well as the defined `RPL_WHOREPLY`/`RPL_ENDOFWHO` numeric replies for protocol correctness.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
HandleWhoisAsync processes the IRC WHOIS command by ensuring the requester is registered, validating the target nick parameter, and then assembling and sending the standard WHOIS information for that user. It fetches the user profile, emits the appropriate WHOIS numeric replies (and the away/idle data when available), and gracefully reports when the target nick does not exist.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method acts as a protocol adapter that wires together user data and channel memberships to produce a coherent WHOIS response. It coordinates between `_userService` for profile data, `_chatService` for channel membership, and `_conn` for sending IRC numerics, encapsulating the protocol-specific choreography in a single, testable unit. The logic defensively handles missing profile data and optional information (channels, away message) to align with RFC-like WHOIS expectations while keeping the flow readable and isolated from business rules.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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?`
|
||||||
|
|
||||||
|
|
||||||
|
IrcToEchoHubChannel converts a raw IRC channel name into EchoHub's internal channel identifier, returning null when the input cannot be mapped. It requires the input to start with the '#' prefix and to be at least two characters long; it then drops the leading '#', lowercases the remainder invariantly, trims whitespace, and validates the result against the central channel-name pattern provided by `ValidationConstants.ChannelNameRegex()`. If the name matches, the canonical, lowercased name is returned; otherwise null. This function is typically invoked when translating IRC channel references into EchoHub's normalized channel namespace, ensuring downstream logic always works with validated, consistent channel names rather than arbitrary IRC inputs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### RequireRegisteredAsync
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task<bool> RequireRegisteredAsync()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `Task<bool>`
|
||||||
|
|
||||||
|
|
||||||
|
`RequireRegisteredAsync` checks whether the IRC connection is registered and returns true when it is. If not registered, it sends the standard `ERR_NOTREGISTERED` reply using `SendNumericAsync` with `ServerName`, `IrcNumericReply.ERR_NOTREGISTERED`, and the message `":You have not registered"`, then returns false.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
Conceptually, this method centralizes the precondition for commands that require a registered session, avoiding duplicated checks across handlers. It relies on `_conn` to inspect `IsRegistered`, and on `ServerName` and `IrcNumericReply.ERR_NOTREGISTERED` to deliver a consistent IRC-compliant error. By returning a boolean, it makes the caller's flow straightforward: proceed when true, bail when false.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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, cancellation-aware loop that reads lines from the IRC connection via `_conn.ReadLineAsync(ct)`, stops when `line` is `null`, trims trailing CR/LF, skips blank lines, logs each received line with `_logger.LogDebug("IRC < {Id}: {Line}", _conn.ConnectionId, line)`, parses the line into an [`IrcMessage`](IrcMessage.cs.md) using `IrcMessage.Parse(line)`, and dispatches the resulting message to `HandleCommandAsync(msg)`. This method is the central inbound processor for an IRC connection: it bridges the raw socket input to the higher-level command handling logic and continues running until the provided `CancellationToken ct` signals cancellation or the connection ends.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
|
||||||
|
This method is the primary inbound processor for a single IRC connection, isolating IO, parsing, and command dispatch from higher-level application logic. It logs critical diagnostic information: per-line debugging via `_logger.LogDebug` and per-command failures via `_logger.LogError`, including the command name and the nick when available. By catching exceptions only around `HandleCommandAsync(msg)` it ensures that a failure in handling one command does not crash the entire loop, preserving resilience.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The call to `IrcMessage.Parse(line)` occurs outside the `try` block that guards `HandleCommandAsync(msg)`; a parsing error could bubble up and terminate the loop. Consider moving parsing inside the try/catch or adding its own guard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
This private helper fetches the current topic for a channel and, if the channel exists, delivers the appropriate IRC numeric reply to the connected client. It calls `_channelService.GetChannelTopicAsync(channelName)` to obtain `(topic, exists)` and, depending on the result, returns early when the channel doesn't exist, sends `RPL_TOPIC` with `#<channelName> :<topic>` when a topic is set, or sends `RPL_NOTOPIC` with `#<channelName> :No topic is set` when there is no topic.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method acts as a small integration point between channel-data access and IRC protocol messaging. By encapsulating the topic-notification logic, it coordinates `_channelService` (data) and `_conn` (connection) to produce consistent numeric replies via [`IrcNumericReply`](IrcNumericReply.cs.md) constants, reducing duplication across the command-handling code. Its private scope signals it's an internal helper used by higher-level IRC commands, keeping the channel-topic flow centralized.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If the channel does not exist (`exists` is false), the method returns without sending any reply, which can appear as a missing response to the client; callers should ensure channel existence or handle this case.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- IrcNumericReply
|
||||||
|
|
||||||
|
## Dependency APIs (verified signatures)
|
||||||
|
|
||||||
|
- 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: `SendChannelTopicAsync`
|
||||||
|
- Kind: `method`
|
||||||
|
- File: `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
- Language: `csharp`
|
||||||
|
- ID: `d23ca663-2c48-4576-9b5f-759527f87f1c`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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-mode operation failure into the appropriate IRC numeric for the target channel and forwards it to the server. When a mode operation fails for the given `channelName`, the method maps the domain error to an IRC numeric using a switch over [`ChannelError`](../EchoHub.Core/DTOs/CommonDtos.cs.md) (NotFound -> `IrcNumericReply.ERR_NOSUCHCHANNEL`, Forbidden -> `IrcNumericReply.ERR_CHANOPRIVSNEEDED`, otherwise `IrcNumericReply.ERR_KEYSET`) and sends a message using `_conn.SendNumericAsync(ServerName, numeric, `$"#{channelName} :{result.ErrorMessage}"`)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SendMotdAsync
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task SendMotdAsync()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `Task`
|
||||||
|
|
||||||
|
|
||||||
|
SendMotdAsync is a private asynchronous helper that delivers the server's Message of the Day to the current IRC connection. It checks the configured `Motd` on `_options` and, if missing, responds with the IRC error code `ERR_NOMOTD`; otherwise it streams the MOTD lines between `RPL_MOTDSTART` and `RPL_ENDOFMOTD` using `RPL_MOTD` for each line. This method formats each line by trimming a trailing carriage return and sends one line per message, adhering to the IRC protocol expectations.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`SendMotdAsync` centralizes MOTD delivery to ensure consistent IRC protocol formatting and behavior. It relies on `_conn` to emit numeric replies and on the [`IrcNumericReply`](IrcNumericReply.cs.md) constants to signal the start, each line, and the end of the MOTD, while consulting the configured `Motd` via the `_options` object. This encapsulation prevents duplication and makes it straightforward to adjust MOTD formatting in one place.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The method trims trailing carriage returns (`'\r'`) from each MOTD line to gracefully handle Windows-style line endings when sending lines via `RPL_MOTD`.
|
||||||
|
- If `_options.Motd` is null or whitespace, the method short-circuits and emits `ERR_NOMOTD` before attempting any `RPL_MOTD` messages.
|
||||||
|
- MOTD lines are sent individually in order, one `RPL_MOTD` message per line, followed by `RPL_ENDOFMOTD` to mark completion.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
|
||||||
|
Responds to an IRC NAMES request for a channel by collecting the currently online users and emitting the standard numeric replies that enumerate channel members. It calls `_chatService.GetOnlineUsersAsync(channelName)` to obtain user objects, builds a space-separated list of their `Username`s, and sends two numeric replies: first `IrcNumericReply.RPL_NAMREPLY` with the channel's nick list via `_conn.SendNumericAsync`, and then `IrcNumericReply.RPL_ENDOFNAMES` to mark the end of the list.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method encapsulates the protocol details of responding to the IRC `NAMES` command for a channel. It isolates the discovery of online users from the formatting and emission of the numeric replies, ensuring consistent NAMES responses and simplifying the caller's responsibilities.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If there are no online users, the constructed `nicks` string will be empty, but an `RPL_NAMREPLY` line will still be emitted followed by `RPL_ENDOFNAMES`.
|
||||||
|
- The method is private and relies on `_chatService` and `_conn` being available; callers must ensure the surrounding context handles validation and errors appropriately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SendWelcomeBurstAsync
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task SendWelcomeBurstAsync()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `Task`
|
||||||
|
|
||||||
|
|
||||||
|
This private async method emits the initial IRC handshake to the connected client by sending a series of standard numeric replies. It reads the nickname from `_conn.Nickname`, uses `ServerName` as the server identity, and dispatches the numerics `RPL_WELCOME`, `RPL_YOURHOST`, `RPL_CREATED`, `RPL_MYINFO`, and `RPL_ISUPPORT` via `_conn.SendNumericAsync`. After sending these banners, it calls `SendMotdAsync` to deliver the MOTD and complete the handshake.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method centralizes the handshake so every connection receives a consistent welcome, isolating IRC protocol formatting from higher-level command handling. It relies on the [`IrcNumericReply`](IrcNumericReply.cs.md) constants to produce the standard numerics and on `_conn` to transmit messages, keeping the transport details out of the handshake logic. The method assumes `_conn.Nickname` is non-null at the time it runs, as evidenced by the null-forgiving read.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Be aware that `_conn.Nickname` is read with a null-forgiving operator; if nickname isn't set yet, a runtime `NullReferenceException` could occur. Ensure the nickname is established earlier in the connection sequence before calling this method.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### TryCompleteRegistrationAsync
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcCommandHandler.cs`
|
||||||
|
> **Kind:** method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private async Task TryCompleteRegistrationAsync()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** `Task`
|
||||||
|
|
||||||
|
|
||||||
|
Asynchronously completes a client's registration by deciding whether SASL authentication has already succeeded, or whether to perform PASS-based login to complete or create the user. If SASL is already authenticated ( `_conn.IsAuthenticated` and `_conn.UserId` is not null ), it marks the connection as registered, notifies the chat subsystem via [`UserConnectedAsync`](../EchoHub.Server/Services/ChatService.cs.md), and then triggers the welcome sequence with `SendWelcomeBurstAsync`. If not SASL-authenticated, it requires a password; if missing, it returns `ERR_PASSWDMISMATCH` and a generic error. Otherwise it calls `_userService.AuthenticateUserAsync(_conn.Nickname, _conn.Password)` and, on failure, falls back to `_userService.RegisterUserAsync(_conn.Nickname, _conn.Password)`; on success it binds the resulting user to the connection, updates `_conn.UserId`, `_conn.Nickname`, and flags `_conn.IsAuthenticated` and `_conn.IsRegistered`, then notifies the chat service and sends the welcome burst.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This method encapsulates the end-to-end registration/authentication handoff, coordinating between the connection state, the user service, and the chat subsystem. It guards against re-entrancy by exiting early when capability negotiation is in progress or the connection is already registered, and it ensures a consistent welcome sequence is delivered once authentication or registration succeeds.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Be mindful that the initial logging emits user-identifying state (e.g. `_conn.Nickname`, `_conn.Username`); ensure logging remains appropriate for your privacy and security policy.
|
||||||
|
|
||||||
|
---
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 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 ExecuteAsync in IrcGatewayService"] --> CheckEnabled{"Check IrcOptions Enabled"}
|
||||||
|
|
||||||
|
CheckEnabled -->|"no"| LogDisabled["Log 'IRC gateway is disabled' and return"] --> End["End ExecuteAsync"]
|
||||||
|
CheckEnabled -->|"yes"| BuildListeners["Create listeners list"] --> AddPlainListener["Add RunListenerAsync for plain port (starts Task)"]
|
||||||
|
|
||||||
|
AddPlainListener -->|"starts Task"| RunListenerPlain["RunListenerAsync(port, useTls=false)"]
|
||||||
|
|
||||||
|
BuildListeners --> CheckTls{"TLS enabled and cert path set"}
|
||||||
|
CheckTls -->|"no"| WaitAll["Await Task.WhenAll(listeners)"] --> End
|
||||||
|
CheckTls -->|"yes"| AddTlsListener["Add RunListenerAsync for TLS port (starts Task)"]
|
||||||
|
AddTlsListener -->|"starts Task"| RunListenerTls["RunListenerAsync(port, useTls=true)"]
|
||||||
|
RunListenerPlain --> RunListenerCore
|
||||||
|
RunListenerTls --> RunListenerCore
|
||||||
|
|
||||||
|
RunListenerCore["RunListenerAsync body"] --> StartListener["Start TcpListener and log listening"]
|
||||||
|
StartListener --> RegisterCancel["Register ct to stop listener"] --> ListenerLoop{"ct.IsCancellationRequested"}
|
||||||
|
ListenerLoop -->|"no"| AcceptClient["AcceptTcpClientAsync"] --> SpawnHandle["Spawn HandleClientAsync(tcpClient, useTls) as fire and forget"] --> ListenerLoop
|
||||||
|
ListenerLoop -->|"yes"| StopListener["Stop listener and return from RunListenerAsync"]
|
||||||
|
|
||||||
|
SpawnHandle --> HandleClientStart["HandleClientAsync: get stream"] --> UseTls{"useTls"}
|
||||||
|
UseTls -->|"yes"| TLSHandshakeTry["Attempt TLS handshake"]
|
||||||
|
TLSHandshakeTry -->|"handshake failed"| TLSHandshakeFail["Log TLS handshake failed and close client, return"]
|
||||||
|
TLSHandshakeTry -->|"handshake succeeded"| AfterTls
|
||||||
|
UseTls -->|"no"| AfterTls["Proceed with plain stream"]
|
||||||
|
|
||||||
|
AfterTls --> CreateConnection["Create IrcClientConnection instance"] --> AddConnection["Add connection to _connections"] --> EndHandle["Return from HandleClientAsync"]
|
||||||
|
|
||||||
|
%% Simple getters
|
||||||
|
GetAll["GetAllConnections returns authenticated IrcClientConnection entries"]
|
||||||
|
GetInChannel["GetConnectionsInChannel(channelName) returns authenticated IrcClientConnection in channel"]
|
||||||
|
|
||||||
|
EndHandle --> End
|
||||||
|
LogDisabled --> End
|
||||||
|
StopListener --> End
|
||||||
|
AddConnection --> EndHandle
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class IrcGatewayService : BackgroundService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
An always-on hosted gateway that accepts raw TCP (optionally TLS) connections and exposes an IRC-compatible surface backed by the EchoHub services. `IrcGatewayService` reads configuration from [`IrcOptions`](IrcOptions.cs.md), listens on the configured ports, accepts incoming `TcpClient` connections, wraps them in [`IrcClientConnection`](IrcClientConnection.cs.md) objects, and hands each connection to an [`IrcCommandHandler`](IrcCommandHandler.cs.md) that bridges IRC commands to the application services ([`IChatService`](../EchoHub.Core/Contracts/IChatService.cs.md), [`IUserService`](../EchoHub.Core/Contracts/IUserService.cs.md), [`IChannelService`](../EchoHub.Core/Contracts/IChannelService.cs.md), [`IMessageEncryptionService`](../EchoHub.Core/Contracts/IMessageEncryptionService.cs.md)). Reach for `IrcGatewayService` when you want to run an IRC-facing adapter for the EchoHub system rather than implementing socket handling and protocol dispatch yourself.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
`IrcGatewayService` is a long-running `BackgroundService` that centralizes network-level concerns for the IRC gateway: socket listening, optional TLS handshake, acceptance of clients, and registration of active connections in the concurrent `_connections` map. It delegates protocol parsing and business-logic handling to [`IrcCommandHandler`](IrcCommandHandler.cs.md), resolving the required domain services from the DI `IServiceProvider` per connection so the gateway stays thin and focused on I/O and lifecycle. The service uses [`IrcOptions`](IrcOptions.cs.md) to control whether the gateway is enabled, which ports to bind, and whether to offer TLS; listeners are run as independent tasks and shut down when the host cancellation token is triggered.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- TLS requires a valid `IrcOptions.TlsCertPath` and password when `IrcOptions.TlsEnabled` is true; a failed TLS handshake will be logged and the connection closed (the code logs "TLS handshake failed" on exception).
|
||||||
|
- Active connections are tracked in the `ConcurrentDictionary` `_connections` and can be inspected via `GetConnectionsInChannel` and `GetAllConnections`; the dictionary makes concurrent adds/removes safe, but callers should expect the set to change while enumerating.
|
||||||
|
- The provided source was truncated inside `HandleClientAsync` in the task payload; I could not verify whether each [`IrcClientConnection`](IrcClientConnection.cs.md) is always removed from `_connections` and whether streams/clients are always disposed on disconnect. If you rely on deterministic cleanup, inspect the full `HandleClientAsync` implementation to confirm that connections are removed and resources are disposed on normal disconnect and on error.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# IrcMessage
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcMessage.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class IrcMessage
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
IrcMessage is a parsed representation of a single IRC protocol line. It exposes an optional `Prefix`, the `Command`, and the list of `Parameters` extracted from the line, with `Trailing` representing the last parameter when present; use `Parse` to convert a raw IRC line into this structured form so you can inspect the command and its arguments without manual parsing.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
IrcMessage centralizes IRC line parsing by translating the textual format into explicit properties. The `Prefix` is optional, `Command` is the verb, and `Parameters` preserve order, with the final parameter commonly used as the trailing content in IRC messages. Accessing `Trailing` provides a convenient single point for the trailing payload without scanning the list; be mindful that `Parameters` is a `List<string>` and can be mutated if you obtain a reference.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```csharp
|
||||||
|
var line = ":server PRIVMSG #channel :Hello, world!";
|
||||||
|
var msg = IrcMessage.Parse(line);
|
||||||
|
|
||||||
|
var cmd = msg.Command; // "PRIVMSG"
|
||||||
|
var target = msg.Parameters[0]; // "#channel"
|
||||||
|
var trailing = msg.Trailing; // "Hello, world!"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The `Parameters` collection is a mutable `List<string>`; if you need a stable, immutable view, clone it before usage.
|
||||||
|
- If there are no parameters, `Trailing` will be `null`; the property simply reflects the last entry of `Parameters` when any parameters exist.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# IrcMessageFormatter
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcMessageFormatter.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class IrcMessageFormatter
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
IrcMessageFormatter is a static helper that formats a `MessageDto` into one or more IRC PRIVMSG lines for posting to an IRC channel. It orchestrates the translation of message content, reply references, attachments, and embeds into IRC-compatible payloads, applying line-length constraints and CTCP ACTION handling where appropriate. Attachments are rendered as distinct link lines with an appropriate tag (image, audio, or file) and converted to absolute URLs using an optional `publicBaseUrl`. When embeds are present, they are appended via the embed formatting pipeline. The formatting rules are centralized in this class to ensure consistent IRC output across messages and channels.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
By centralizing IRC-specific formatting in `IrcMessageFormatter`, the server ensures consistent transport behavior for all messages moved from the domain model to IRC clients. It isolates concerns about line-length, reply quoting, action formatting, and attachment rendering from higher-level message construction, making it straightforward to adjust how content appears in IRC without changing business logic. The implementation supports plain content as well as CTCP ACTIONs and gracefully handles reply contexts, including a placeholder for encrypted room content when applicable.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- If `publicBaseUrl` is not provided and attachments use relative URLs, the resulting links may be non-functional in IRC clients. Ensure a base URL is supplied when needed.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# IrcNumericReply
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcNumericReply.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class IrcNumericReply
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
IrcNumericReply is a centralized, static container of IRC protocol numeric reply codes represented as strings. It defines constants for common server replies and errors, organized by category (Connection registration, MOTD, Channel operations, LIST, WHO/WHOIS, AWAY, MODE, Errors, SASL). Developers reference these constants, such as `IrcNumericReply.RPL_WELCOME` or `IrcNumericReply.ERR_NOSUCHNICK`, when constructing or interpreting IRC protocol messages instead of hard-coding literals. This reduces repetition, prevents typos, and makes maintenance safer if the IRC spec evolves or expands the set of recognized replies.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
IrcNumericReply provides a canonical reference for IRC numeric codes, solving the problem of scattered, magic string literals across message handling, parsing, and logging. It fits with any component that reads or writes server messages, allowing consistent checks for `IrcNumericReply.RPL_WELCOME` and other replies without duplicating numeric literals.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- The constants are string values representing the IRC wire codes; use `IrcNumericReply.*` wherever you compare or emit these codes to avoid accidental mismatches.
|
||||||
|
- This class contains no behavior beyond constants; place any parsing or dispatch logic elsewhere.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# IrcOptions
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcOptions.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class IrcOptions
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
IrcOptions is a lightweight configuration container for the EchoHub IRC integration. It groups together all IRC-related settings that govern whether the IRC feature is active, which ports to listen on for plain and TLS connections, optional TLS credentials, the IRC server identity, an optional Motd, and how attachment URLs are resolved for IRC clients. This class is typically populated from the `Irc` configuration section (as indicated by the `SectionName` constant) and consumed by the startup logic that initializes the IRC subsystem, allowing developers to tailor IRC behavior without touching runtime code.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
IrcOptions is a pure data carrier with defaults that reflect common IRC conventions: `Port` defaults to 6667, `TlsPort` to 6697, and [`ServerName`](IrcCommandHandler.cs.md) to `echohub`. TLS-related fields (`TlsEnabled`, `TlsPort`, `TlsCertPath`, `TlsCertPassword`) indicate TLS support is optional and configured here; the runtime code uses these values to establish TLS-protected connections when enabled. The `PublicBaseUrl` property governs how attachment URLs are rendered for IRC clients: when set, it converts relative paths to absolute links using the provided base URL; when unset, attachments fall back to their relative paths. The `Motd` field exposes an optional IRC message of the day that can be surfaced to connected clients if the IRC subsystem is started.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# IrcServiceExtensions
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Server.Irc/IrcServiceExtensions.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static class IrcServiceExtensions
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Extends `WebApplicationBuilder` with `AddIrcGateway` to wire IRC gateway support into an ASP.NET Core app. It configures [`IrcOptions`](IrcOptions.cs.md) from configuration and, when `Irc:Enabled` is true, registers [`IrcGatewayService`](IrcGatewayService.cs.md) as a singleton, wires [`IChatBroadcaster`](../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) to [`IrcBroadcaster`](IrcBroadcaster.cs.md), and adds the gateway as a hosted service, returning the original builder for fluent chaining.
|
||||||
|
|
||||||
|
## Remarks
|
||||||
|
This extension encapsulates opt-in startup logic and centralizes the wiring of the IRC gateway, ensuring consistent DI lifetimes and configuration handling across the app. It coordinates the lifecycle of [`IrcGatewayService`](IrcGatewayService.cs.md) and the broadcaster ([`IChatBroadcaster`](../EchoHub.Core/Contracts/IChatBroadcaster.cs.md) implemented by [`IrcBroadcaster`](IrcBroadcaster.cs.md)) by hosting the gateway as a background service.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Calling `AddIrcGateway` multiple times can register multiple hosted services and singletons; call it once during startup to avoid duplicate registrations.
|
||||||
|
- The extension only activates when `Irc:Enabled` is true. If the flag is false or missing, it will configure [`IrcOptions`](IrcOptions.cs.md) but will not start or register the gateway components. Ensure configuration sources are loaded before invocation.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# JwtTokenService
|
||||||
|
|
||||||
|
> **File:** `src/EchoHub.Server/Auth/JwtTokenService.cs`
|
||||||
|
> **Kind:** class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class JwtTokenService
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
JwtTokenService centralizes creation of JWTs for API authentication: it issues short-lived access tokens via `GenerateAccessToken` (from a [`User`](../../EchoHub.Core/Models/User.cs.md) or a [`UserProfileDto`](../../EchoHub.Core/DTOs/ProfileDtos.cs.md)) and provides a cryptographically secure refresh token generator via `GenerateRefreshToken` (and `HashToken` to store a hashed form). It reads its secret, issuer, and audience from configuration and enforces token lifetimes defined by `AccessTokenLifetime` and `RefreshTokenLifetime`.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user