This commit is contained in:
HueByte
2026-02-24 21:03:14 +00:00
parent bd22be4181
commit 5031bf0e11
56 changed files with 8746 additions and 374 deletions
+352
View File
@@ -0,0 +1,352 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Authentication | EchoHub Documentation </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Authentication | EchoHub Documentation ">
<link rel="icon" href="../images/hue_icon.svg">
<link rel="stylesheet" href="../public/docfx.min.css">
<link rel="stylesheet" href="../public/main.css">
<meta name="docfx:navrel" content="../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../">
<meta name="docfx:docurl" content="https://github.com/HueByte/EchoHub/blob/master/docs/#L1">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../index.html">
<img id="logo" class="svg" src="../images/hue_icon.svg" alt="EchoHub">
EchoHub
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="">
<h1 id="authentication">Authentication</h1>
<h2 id="user-registration">User Registration</h2>
<p>A new user creates an account on a server. The client sends credentials via REST,
the server hashes the password, issues JWT tokens, and the client stores the
refresh token for &quot;Remember Me&quot; sessions.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant UI as ConnectDialog
participant AO as AppOrchestrator
participant CM as ConnectionManager
participant API as ApiClient
participant Auth as AuthController
participant US as UserService
participant JWT as JwtTokenService
participant DB as SQLite
UI-&gt;&gt;AO: ConnectDialogResult(IsRegister: true)
AO-&gt;&gt;CM: ConnectAsync(dialogResult)
CM-&gt;&gt;API: RegisterAsync(username, password)
API-&gt;&gt;Auth: POST /api/auth/register
Auth-&gt;&gt;US: RegisterUserAsync(username, password, displayName)
US-&gt;&gt;US: Validate (regex, length, uniqueness)
US-&gt;&gt;DB: INSERT User (BCrypt hash)
US--&gt;&gt;Auth: UserOperationResult.Success
Auth-&gt;&gt;JWT: GenerateAccessToken(user)
JWT--&gt;&gt;Auth: (token, expiresAt) [15 min]
Auth-&gt;&gt;JWT: GenerateRefreshToken()
JWT--&gt;&gt;Auth: Base64 random (64 bytes)
Auth-&gt;&gt;DB: INSERT RefreshToken (SHA256 hash)
Auth--&gt;&gt;API: LoginResponse
API-&gt;&gt;API: SetTokens() — store in memory + set Bearer header
API--&gt;&gt;CM: LoginResponse
CM-&gt;&gt;CM: Wire OnTokensRefreshed for config persistence
CM-&gt;&gt;CM: Continue to connection setup (see Connection Flow)
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Dialog UI</td>
<td><code>src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs</code></td>
<td>Lines 251-268 (register handler)</td>
</tr>
<tr>
<td>Orchestrator entry</td>
<td><code>src/EchoHub.Client/AppOrchestrator.cs</code></td>
<td>Lines 550-592 (<code>HandleConnect</code>)</td>
</tr>
<tr>
<td>ConnectionManager auth</td>
<td><code>src/EchoHub.Client/Services/ConnectionManager.cs</code></td>
<td>Lines 74-76 (register branch)</td>
</tr>
<tr>
<td>ApiClient register</td>
<td><code>src/EchoHub.Client/Services/ApiClient.cs</code></td>
<td>Lines 32-43 (<code>RegisterAsync</code>)</td>
</tr>
<tr>
<td>AuthController register</td>
<td><code>src/EchoHub.Server/Controllers/AuthController.cs</code></td>
<td>Lines 28-49</td>
</tr>
<tr>
<td>UserService register</td>
<td><code>src/EchoHub.Server/Services/UserService.cs</code></td>
<td>Lines 20-59 (<code>RegisterUserAsync</code>)</td>
</tr>
<tr>
<td>JWT generation</td>
<td><code>src/EchoHub.Server/Auth/JwtTokenService.cs</code></td>
<td>Lines 30-53 (access), 80-86 (refresh)</td>
</tr>
<tr>
<td>Token persistence</td>
<td><code>src/EchoHub.Client/AppOrchestrator.cs</code></td>
<td>Lines 1039-1053 (<code>SaveServerToConfig</code>)</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="user-login">User Login</h2>
<p>Returning user authenticates with username/password or a saved refresh token.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant UI as ConnectDialog
participant CM as ConnectionManager
participant API as ApiClient
participant Auth as AuthController
participant US as UserService
participant DB as SQLite
alt Saved refresh token (Remember Me)
UI-&gt;&gt;CM: ConnectDialogResult(SavedRefreshToken: &quot;...&quot;)
CM-&gt;&gt;API: LoginWithRefreshTokenAsync()
API-&gt;&gt;Auth: POST /api/auth/refresh
Auth-&gt;&gt;DB: Lookup token by SHA256 hash
Auth-&gt;&gt;DB: Revoke old token, issue new pair
Auth--&gt;&gt;API: LoginResponse (rotated tokens)
else Username + Password
UI-&gt;&gt;CM: ConnectDialogResult(IsRegister: false)
CM-&gt;&gt;API: LoginAsync(username, password)
API-&gt;&gt;Auth: POST /api/auth/login
Auth-&gt;&gt;US: AuthenticateUserAsync(username, password)
US-&gt;&gt;DB: Fetch user, BCrypt.Verify(password, hash)
US-&gt;&gt;DB: Update LastSeenAt
US--&gt;&gt;Auth: UserOperationResult.Success
Auth--&gt;&gt;API: LoginResponse
end
API-&gt;&gt;API: SetTokens()
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Login button handler</td>
<td><code>src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs</code></td>
<td>Lines 214-249</td>
</tr>
<tr>
<td>Saved token branch</td>
<td><code>src/EchoHub.Client/Services/ConnectionManager.cs</code></td>
<td>Lines 69-71</td>
</tr>
<tr>
<td>Password branch</td>
<td><code>src/EchoHub.Client/Services/ConnectionManager.cs</code></td>
<td>Lines 78-80</td>
</tr>
<tr>
<td>ApiClient login</td>
<td><code>src/EchoHub.Client/Services/ApiClient.cs</code></td>
<td>Lines 45-56 (<code>LoginAsync</code>)</td>
</tr>
<tr>
<td>AuthController login</td>
<td><code>src/EchoHub.Server/Controllers/AuthController.cs</code></td>
<td>Lines 51-72</td>
</tr>
<tr>
<td>AuthController refresh</td>
<td><code>src/EchoHub.Server/Controllers/AuthController.cs</code></td>
<td>Lines 74-108</td>
</tr>
<tr>
<td>UserService authenticate</td>
<td><code>src/EchoHub.Server/Services/UserService.cs</code></td>
<td>Lines 61-83</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="token-refresh">Token Refresh</h2>
<p>Access tokens expire after 15 minutes. The client auto-refreshes transparently
before requests and on 401 responses. Refresh tokens are rotated on each use.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant SR as SignalR / HTTP Request
participant API as ApiClient
participant Auth as AuthController
participant DB as SQLite
participant Config as config.json
SR-&gt;&gt;API: GetValidTokenAsync() or HTTP 401
API-&gt;&gt;API: Token expires within 60s?
alt Proactive refresh (SignalR token provider)
API-&gt;&gt;Auth: POST /api/auth/refresh (old refresh token)
else Reactive refresh (HTTP 401 retry)
API-&gt;&gt;Auth: POST /api/auth/refresh (old refresh token)
end
Auth-&gt;&gt;DB: Lookup by SHA256 hash
Auth-&gt;&gt;DB: Revoke old refresh token
Auth-&gt;&gt;DB: INSERT new RefreshToken
Auth--&gt;&gt;API: LoginResponse (new token pair)
API-&gt;&gt;API: SetTokens() — update Bearer header
API--&gt;&gt;API: Fire OnTokensRefreshed event
API--&gt;&gt;Config: Persist new refresh token (if Remember Me)
API-&gt;&gt;SR: Retry original request with new token
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Proactive check</td>
<td><code>src/EchoHub.Client/Services/ApiClient.cs</code></td>
<td>Lines 110-129 (<code>GetValidTokenAsync</code>)</td>
</tr>
<tr>
<td>Reactive 401 retry (GET)</td>
<td><code>src/EchoHub.Client/Services/ApiClient.cs</code></td>
<td>Lines 338-358 (<code>AuthenticatedGetAsync</code>)</td>
</tr>
<tr>
<td>Reactive 401 retry (POST/PUT/DELETE)</td>
<td><code>src/EchoHub.Client/Services/ApiClient.cs</code></td>
<td>Lines 364-384 (<code>AuthenticatedRequestAsync</code>)</td>
</tr>
<tr>
<td>Refresh HTTP call</td>
<td><code>src/EchoHub.Client/Services/ApiClient.cs</code></td>
<td>Lines 58-71 (<code>RefreshTokenAsync</code>)</td>
</tr>
<tr>
<td>SignalR token provider</td>
<td><code>src/EchoHub.Client/Services/EchoHubConnection.cs</code></td>
<td>Line 37 (<code>AccessTokenProvider</code>)</td>
</tr>
<tr>
<td>Server-side rotation</td>
<td><code>src/EchoHub.Server/Controllers/AuthController.cs</code></td>
<td>Lines 74-108</td>
</tr>
<tr>
<td>Token persistence callback</td>
<td><code>src/EchoHub.Client/Services/ConnectionManager.cs</code></td>
<td>Lines 253-264</td>
</tr>
</tbody>
</table>
</article>
<div class="contribution d-print-none">
<a href="https://github.com/HueByte/EchoHub/blob/master/docs/#L1" class="edit-link">Edit this page</a>
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<div class='footer-custom'><div class='footer-inner'><span class='footer-brand'>EchoHub</span><span class='footer-sep'>&middot;</span><a href='https://github.com/HueByte/EchoHub'>GitHub</a><span class='footer-sep'>&middot;</span><a href='https://echohub.voidcube.cloud'>Website</a><span class='footer-sep'>&middot;</span><span class='footer-credit'>Built with <a href='https://dotnet.github.io/docfx'>DocFX</a></span></div></div>
</div>
</div>
</footer>
</body>
</html>
+344
View File
@@ -0,0 +1,344 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Channels | EchoHub Documentation </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Channels | EchoHub Documentation ">
<link rel="icon" href="../images/hue_icon.svg">
<link rel="stylesheet" href="../public/docfx.min.css">
<link rel="stylesheet" href="../public/main.css">
<meta name="docfx:navrel" content="../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../">
<meta name="docfx:docurl" content="https://github.com/HueByte/EchoHub/blob/master/docs/#L1">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../index.html">
<img id="logo" class="svg" src="../images/hue_icon.svg" alt="EchoHub">
EchoHub
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="">
<h1 id="channels">Channels</h1>
<h2 id="channel-creation">Channel Creation</h2>
<p>Channels are created via the REST API. Public channels are broadcast to all
connected clients.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant Client as Client (TUI/API)
participant CC as ChannelsController
participant ChS as ChannelService
participant CS as ChatService
participant DB as SQLite
participant SRB as SignalRBroadcaster
participant IRCB as IrcBroadcaster
Client-&gt;&gt;CC: POST /api/channels {name, topic, isPublic}
CC-&gt;&gt;ChS: CreateChannelAsync(creatorId, name, topic, isPublic)
ChS-&gt;&gt;ChS: Normalize name (lowercase, trim)
ChS-&gt;&gt;ChS: Validate format (2-100 chars, regex)
ChS-&gt;&gt;DB: Check duplicate
ChS-&gt;&gt;DB: INSERT Channel + ChannelMembership (creator auto-added)
ChS--&gt;&gt;CC: ChannelDto
alt Channel is public
CC-&gt;&gt;CS: BroadcastChannelUpdatedAsync(channel)
CS-&gt;&gt;SRB: SendChannelUpdatedAsync(channelDto)
SRB-&gt;&gt;SRB: Notify all SignalR clients
CS-&gt;&gt;IRCB: SendChannelUpdatedAsync(channelDto)
end
CC--&gt;&gt;Client: 201 Created (ChannelDto)
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Controller endpoint</td>
<td><code>src/EchoHub.Server/Controllers/ChannelsController.cs</code></td>
<td>Lines 60-76</td>
</tr>
<tr>
<td>Channel service create</td>
<td><code>src/EchoHub.Server/Services/ChannelService.cs</code></td>
<td>Lines 50-90</td>
</tr>
<tr>
<td>Broadcast updated</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 308-309</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="channel-deletion">Channel Deletion</h2>
<p>Only the channel creator (or admin) can delete a channel. The default channel
is protected.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant Client as Client
participant CC as ChannelsController
participant ChS as ChannelService
participant DB as SQLite
Client-&gt;&gt;CC: DELETE /api/channels/{channel}
CC-&gt;&gt;ChS: DeleteChannelAsync(channelName, callerId)
ChS-&gt;&gt;ChS: Reject if default channel
ChS-&gt;&gt;DB: Lookup channel
ChS-&gt;&gt;ChS: Verify caller is creator or admin
ChS-&gt;&gt;DB: DELETE Channel (cascade: messages, memberships)
ChS--&gt;&gt;CC: Success
CC--&gt;&gt;Client: 204 No Content
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Controller endpoint</td>
<td><code>src/EchoHub.Server/Controllers/ChannelsController.cs</code></td>
<td>Lines 94-106</td>
</tr>
<tr>
<td>Channel service delete</td>
<td><code>src/EchoHub.Server/Services/ChannelService.cs</code></td>
<td>Lines 119-144</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="joining-a-channel">Joining a Channel</h2>
<p>Both SignalR and IRC clients join channels through <code>ChatService</code>. The presence
tracker determines if this is a genuinely new join (vs. a second connection) and
broadcasts accordingly.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant Client as Client (SignalR or IRC)
participant Entry as ChatHub / IrcCommandHandler
participant CS as ChatService
participant ChS as ChannelService
participant PT as PresenceTracker
participant DB as SQLite
participant SRB as SignalRBroadcaster
participant IRCB as IrcBroadcaster
Client-&gt;&gt;Entry: JoinChannel / JOIN #channel
Entry-&gt;&gt;CS: JoinChannelAsync(connectionId, userId, username, channel)
CS-&gt;&gt;ChS: EnsureChannelMembershipAsync(userId, channel)
ChS-&gt;&gt;DB: INSERT ChannelMembership (if not exists)
CS-&gt;&gt;PT: JoinChannel(username, channel)
PT--&gt;&gt;CS: isNewJoin?
alt First connection in this channel
CS-&gt;&gt;DB: Fetch UserPresenceDto
CS-&gt;&gt;CS: BroadcastToAllAsync(SendUserJoinedAsync)
par
CS-&gt;&gt;SRB: SendUserJoinedAsync(channel, user, excludeConn)
and
CS-&gt;&gt;IRCB: SendUserJoinedAsync(channel, user)
end
end
CS-&gt;&gt;DB: Fetch message history
CS--&gt;&gt;Entry: (history, error)
Entry--&gt;&gt;Client: History messages
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>SignalR hub join</td>
<td><code>src/EchoHub.Server/Hubs/ChatHub.cs</code></td>
<td>Lines 59-81</td>
</tr>
<tr>
<td>IRC join</td>
<td><code>src/EchoHub.Server.Irc/IrcCommandHandler.cs</code></td>
<td>Lines 361-414</td>
</tr>
<tr>
<td>ChatService join</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 96-135</td>
</tr>
<tr>
<td>Presence join</td>
<td><code>src/EchoHub.Server/Services/PresenceTracker.cs</code></td>
<td>Lines 58-70</td>
</tr>
<tr>
<td>SignalR broadcast</td>
<td><code>src/EchoHub.Server/Services/SignalRBroadcaster.cs</code></td>
<td>Lines 26-32</td>
</tr>
<tr>
<td>IRC broadcast</td>
<td><code>src/EchoHub.Server.Irc/IrcBroadcaster.cs</code></td>
<td>Lines 34-41</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="leaving-a-channel">Leaving a Channel</h2>
<pre><code class="lang-mermaid">sequenceDiagram
participant Client as Client
participant Entry as ChatHub / IrcCommandHandler
participant CS as ChatService
participant PT as PresenceTracker
participant SRB as SignalRBroadcaster
participant IRCB as IrcBroadcaster
Client-&gt;&gt;Entry: LeaveChannel / PART #channel
Entry-&gt;&gt;CS: LeaveChannelAsync(connectionId, username, channel)
CS-&gt;&gt;PT: LeaveChannel(username, channel)
CS-&gt;&gt;CS: BroadcastToAllAsync(SendUserLeftAsync)
par
CS-&gt;&gt;SRB: SendUserLeftAsync(channel, username)
and
CS-&gt;&gt;IRCB: SendUserLeftAsync(channel, username)
end
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>SignalR hub leave</td>
<td><code>src/EchoHub.Server/Hubs/ChatHub.cs</code></td>
<td>Lines 83-96</td>
</tr>
<tr>
<td>IRC part</td>
<td><code>src/EchoHub.Server.Irc/IrcCommandHandler.cs</code></td>
<td>Lines 416-435</td>
</tr>
<tr>
<td>ChatService leave</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 137-143</td>
</tr>
<tr>
<td>Presence leave</td>
<td><code>src/EchoHub.Server/Services/PresenceTracker.cs</code></td>
<td>Lines 72-81</td>
</tr>
</tbody>
</table>
</article>
<div class="contribution d-print-none">
<a href="https://github.com/HueByte/EchoHub/blob/master/docs/#L1" class="edit-link">Edit this page</a>
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<div class='footer-custom'><div class='footer-inner'><span class='footer-brand'>EchoHub</span><span class='footer-sep'>&middot;</span><a href='https://github.com/HueByte/EchoHub'>GitHub</a><span class='footer-sep'>&middot;</span><a href='https://echohub.voidcube.cloud'>Website</a><span class='footer-sep'>&middot;</span><span class='footer-credit'>Built with <a href='https://dotnet.github.io/docfx'>DocFX</a></span></div></div>
</div>
</div>
</footer>
</body>
</html>
+348
View File
@@ -0,0 +1,348 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Connection | EchoHub Documentation </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Connection | EchoHub Documentation ">
<link rel="icon" href="../images/hue_icon.svg">
<link rel="stylesheet" href="../public/docfx.min.css">
<link rel="stylesheet" href="../public/main.css">
<meta name="docfx:navrel" content="../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../">
<meta name="docfx:docurl" content="https://github.com/HueByte/EchoHub/blob/master/docs/#L1">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../index.html">
<img id="logo" class="svg" src="../images/hue_icon.svg" alt="EchoHub">
EchoHub
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="">
<h1 id="connection">Connection</h1>
<h2 id="signalr-client-connection">SignalR Client Connection</h2>
<p>After authentication, the TUI client establishes a SignalR WebSocket, registers
event handlers, joins the default channel, and loads history.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant CM as ConnectionManager
participant EHC as EchoHubConnection
participant Hub as ChatHub
participant CS as ChatService
participant PT as PresenceTracker
participant DB as SQLite
CM-&gt;&gt;CM: Fetch encryption key (GET /api/server/encryption-key)
CM-&gt;&gt;EHC: new EchoHubConnection(apiClient, encryption)
EHC-&gt;&gt;EHC: Build HubConnection (URL + JWT token provider + auto-reconnect)
EHC-&gt;&gt;EHC: RegisterHandlers() — wire ReceiveMessage, UserJoined, etc.
EHC-&gt;&gt;Hub: ConnectAsync() → WebSocket handshake
Hub-&gt;&gt;CS: UserConnectedAsync(connectionId, userId, username)
CS-&gt;&gt;PT: UserConnected(connectionId, userId, username)
CS-&gt;&gt;DB: Update user: Status=Online, LastSeenAt=now
CM-&gt;&gt;CM: Fetch channel list (GET /api/channels)
CM-&gt;&gt;EHC: JoinChannelAsync(&quot;general&quot;)
EHC-&gt;&gt;Hub: InvokeAsync(&quot;JoinChannel&quot;, &quot;general&quot;)
Hub-&gt;&gt;CS: JoinChannelAsync(connectionId, userId, username, &quot;general&quot;)
CS-&gt;&gt;DB: EnsureChannelMembership
CS-&gt;&gt;PT: JoinChannel(username, &quot;general&quot;)
CS-&gt;&gt;CS: BroadcastToAllAsync → UserJoined
CS-&gt;&gt;DB: Fetch message history
Hub--&gt;&gt;EHC: List&lt;MessageDto&gt; (encrypted)
EHC--&gt;&gt;CM: Decrypted history
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Connection orchestration</td>
<td><code>src/EchoHub.Client/Services/ConnectionManager.cs</code></td>
<td>Lines 58-140 (<code>ConnectAsync</code>)</td>
</tr>
<tr>
<td>EchoHubConnection setup</td>
<td><code>src/EchoHub.Client/Services/EchoHubConnection.cs</code></td>
<td>Lines 29-62 (constructor)</td>
</tr>
<tr>
<td>Handler registration</td>
<td><code>src/EchoHub.Client/Services/EchoHubConnection.cs</code></td>
<td>Lines 64-122 (<code>RegisterHandlers</code>)</td>
</tr>
<tr>
<td>Hub OnConnected</td>
<td><code>src/EchoHub.Server/Hubs/ChatHub.cs</code></td>
<td>Lines 31-43</td>
</tr>
<tr>
<td>ChatService connected</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 41-57</td>
</tr>
<tr>
<td>PresenceTracker connect</td>
<td><code>src/EchoHub.Server/Services/PresenceTracker.cs</code></td>
<td>Lines 13-29</td>
</tr>
<tr>
<td>Join channel (hub)</td>
<td><code>src/EchoHub.Server/Hubs/ChatHub.cs</code></td>
<td>Lines 59-81</td>
</tr>
<tr>
<td>Join channel (service)</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 96-135</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="irc-client-connection">IRC Client Connection</h2>
<p>IRC clients connect via TCP, authenticate with PASS/NICK/USER or SASL PLAIN,
and auto-join channels. New usernames are auto-registered.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant IRC as IRC Client
participant GW as IrcGatewayService
participant CH as IrcCommandHandler
participant US as UserService
participant CS as ChatService
participant PT as PresenceTracker
IRC-&gt;&gt;GW: TCP connect (:6667 or :6697 TLS)
GW-&gt;&gt;GW: Accept + create IrcClientConnection
GW-&gt;&gt;CH: new IrcCommandHandler(connection, services)
GW-&gt;&gt;CH: RunAsync() — start read loop
alt SASL PLAIN
IRC-&gt;&gt;CH: CAP REQ :sasl
CH--&gt;&gt;IRC: CAP ACK :sasl
IRC-&gt;&gt;CH: AUTHENTICATE PLAIN
CH--&gt;&gt;IRC: AUTHENTICATE +
IRC-&gt;&gt;CH: AUTHENTICATE &lt;base64(\0user\0pass)&gt;
CH-&gt;&gt;US: AuthenticateUserAsync(user, pass)
alt Auth fails → auto-register
CH-&gt;&gt;US: RegisterUserAsync(user, pass)
end
CH--&gt;&gt;IRC: 903 :SASL authentication successful
else PASS/NICK/USER
IRC-&gt;&gt;CH: PASS &lt;password&gt;
IRC-&gt;&gt;CH: NICK &lt;nickname&gt;
IRC-&gt;&gt;CH: USER &lt;username&gt; 0 * :&lt;realname&gt;
CH-&gt;&gt;US: AuthenticateUserAsync(nick, pass)
alt Auth fails → auto-register
CH-&gt;&gt;US: RegisterUserAsync(nick, pass)
end
end
CH-&gt;&gt;CS: UserConnectedAsync(irc-{guid}, userId, username)
CS-&gt;&gt;PT: UserConnected(irc-{guid}, userId, username)
CH--&gt;&gt;IRC: 001-004 RPL_WELCOME burst + MOTD
Note over IRC,CH: Client is now ready for JOIN/PART/PRIVMSG
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>TCP listener</td>
<td><code>src/EchoHub.Server.Irc/IrcGatewayService.cs</code></td>
<td>Lines 45-90 (<code>ExecuteAsync</code>)</td>
</tr>
<tr>
<td>Client handler</td>
<td><code>src/EchoHub.Server.Irc/IrcGatewayService.cs</code></td>
<td>Lines 92-154 (<code>HandleClientAsync</code>)</td>
</tr>
<tr>
<td>Command read loop</td>
<td><code>src/EchoHub.Server.Irc/IrcCommandHandler.cs</code></td>
<td>Lines 40-98 (<code>RunAsync</code>)</td>
</tr>
<tr>
<td>SASL auth</td>
<td><code>src/EchoHub.Server.Irc/IrcCommandHandler.cs</code></td>
<td>Lines 136-207 (<code>HandleAuthenticateAsync</code>)</td>
</tr>
<tr>
<td>PASS/NICK/USER</td>
<td><code>src/EchoHub.Server.Irc/IrcCommandHandler.cs</code></td>
<td>Lines 209-267</td>
</tr>
<tr>
<td>Registration completion</td>
<td><code>src/EchoHub.Server.Irc/IrcCommandHandler.cs</code></td>
<td>Lines 268-315 (<code>TryCompleteRegistrationAsync</code>)</td>
</tr>
<tr>
<td>Cleanup on disconnect</td>
<td><code>src/EchoHub.Server.Irc/IrcGatewayService.cs</code></td>
<td>Lines 136-153</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="user-disconnect--presence">User Disconnect &amp; Presence</h2>
<p>When a client disconnects, the presence tracker determines if the user has any
remaining connections. If not, status is set to Invisible and all channels are
notified.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant Client as Client
participant Entry as ChatHub / IrcGatewayService
participant CS as ChatService
participant PT as PresenceTracker
participant DB as SQLite
participant SRB as SignalRBroadcaster
Client-&gt;&gt;Entry: Disconnect / TCP close
Entry-&gt;&gt;CS: UserDisconnectedAsync(connectionId)
CS-&gt;&gt;PT: Get username + channels (before removal)
CS-&gt;&gt;PT: UserDisconnected(connectionId)
PT-&gt;&gt;PT: Remove connection from tracking
alt No remaining connections for user
CS-&gt;&gt;DB: Update user: Status=Invisible, LastSeenAt=now
loop For each channel user was in
CS-&gt;&gt;CS: BroadcastToAllAsync(SendUserStatusChangedAsync)
CS-&gt;&gt;SRB: Notify channel members
end
end
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>SignalR disconnect</td>
<td><code>src/EchoHub.Server/Hubs/ChatHub.cs</code></td>
<td>Lines 45-57</td>
</tr>
<tr>
<td>IRC cleanup</td>
<td><code>src/EchoHub.Server.Irc/IrcGatewayService.cs</code></td>
<td>Lines 136-153</td>
</tr>
<tr>
<td>ChatService disconnect</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 59-94</td>
</tr>
<tr>
<td>Presence disconnect</td>
<td><code>src/EchoHub.Server/Services/PresenceTracker.cs</code></td>
<td>Lines 31-53</td>
</tr>
</tbody>
</table>
</article>
<div class="contribution d-print-none">
<a href="https://github.com/HueByte/EchoHub/blob/master/docs/#L1" class="edit-link">Edit this page</a>
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<div class='footer-custom'><div class='footer-inner'><span class='footer-brand'>EchoHub</span><span class='footer-sep'>&middot;</span><a href='https://github.com/HueByte/EchoHub'>GitHub</a><span class='footer-sep'>&middot;</span><a href='https://echohub.voidcube.cloud'>Website</a><span class='footer-sep'>&middot;</span><span class='footer-credit'>Built with <a href='https://dotnet.github.io/docfx'>DocFX</a></span></div></div>
</div>
</div>
</footer>
</body>
</html>
+117
View File
@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Flows | EchoHub Documentation </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Flows | EchoHub Documentation ">
<link rel="icon" href="../images/hue_icon.svg">
<link rel="stylesheet" href="../public/docfx.min.css">
<link rel="stylesheet" href="../public/main.css">
<meta name="docfx:navrel" content="../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../">
<meta name="docfx:docurl" content="https://github.com/HueByte/EchoHub/blob/master/docs/#L1">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../index.html">
<img id="logo" class="svg" src="../images/hue_icon.svg" alt="EchoHub">
EchoHub
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="">
<h1 id="flows">Flows</h1>
<p>This section documents the major request/event flows in EchoHub, showing how data moves between the TUI client, IRC client, server, and database. Each diagram includes code references so you can jump straight to the implementation.</p>
</article>
<div class="contribution d-print-none">
<a href="https://github.com/HueByte/EchoHub/blob/master/docs/#L1" class="edit-link">Edit this page</a>
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<div class='footer-custom'><div class='footer-inner'><span class='footer-brand'>EchoHub</span><span class='footer-sep'>&middot;</span><a href='https://github.com/HueByte/EchoHub'>GitHub</a><span class='footer-sep'>&middot;</span><a href='https://echohub.voidcube.cloud'>Website</a><span class='footer-sep'>&middot;</span><span class='footer-credit'>Built with <a href='https://dotnet.github.io/docfx'>DocFX</a></span></div></div>
</div>
</div>
</footer>
</body>
</html>
+296
View File
@@ -0,0 +1,296 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Media &amp; Services | EchoHub Documentation </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Media &amp; Services | EchoHub Documentation ">
<link rel="icon" href="../images/hue_icon.svg">
<link rel="stylesheet" href="../public/docfx.min.css">
<link rel="stylesheet" href="../public/main.css">
<meta name="docfx:navrel" content="../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../">
<meta name="docfx:docurl" content="https://github.com/HueByte/EchoHub/blob/master/docs/#L1">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../index.html">
<img id="logo" class="svg" src="../images/hue_icon.svg" alt="EchoHub">
EchoHub
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="">
<h1 id="media--services">Media &amp; Services</h1>
<h2 id="file-upload">File Upload</h2>
<p>Files are uploaded via REST, validated by magic bytes, stored with GUID filenames,
and broadcast as a message with a download link.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant Client as Client
participant CC as ChannelsController
participant FV as FileValidationHelper
participant FS as FileStorageService
participant CS as ChatService
participant DB as SQLite
Client-&gt;&gt;CC: POST /api/channels/{channel}/upload (multipart)
CC-&gt;&gt;CC: Check file size limits
CC-&gt;&gt;FV: IsValidImage(stream) — magic byte check
FV-&gt;&gt;FV: Read header: JPEG(FFD8FF) / PNG(89504E47) / GIF / WebP(RIFF+WEBP)
FV--&gt;&gt;CC: true/false
CC-&gt;&gt;FS: SaveFileAsync(stream, extension)
FS-&gt;&gt;FS: Generate GUID filename, write to uploads/
FS--&gt;&gt;CC: fileId (GUID)
CC-&gt;&gt;CC: Determine MessageType (Image/Audio/File)
CC-&gt;&gt;CS: SendMessageAsync (with file URL + optional ASCII art)
CS-&gt;&gt;DB: INSERT Message
CS-&gt;&gt;CS: BroadcastToAllAsync → fan out to clients
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Upload endpoint</td>
<td><code>src/EchoHub.Server/Controllers/ChannelsController.cs</code></td>
<td>Lines 108-200</td>
</tr>
<tr>
<td>File validation</td>
<td><code>src/EchoHub.Server/Services/FileValidationHelper.cs</code></td>
<td>Lines 15-82</td>
</tr>
<tr>
<td>File storage</td>
<td><code>src/EchoHub.Server/Services/FileStorageService.cs</code></td>
<td>Lines 1-47</td>
</tr>
<tr>
<td>File download</td>
<td><code>src/EchoHub.Server/Controllers/FilesController.cs</code></td>
<td>Lines 22-53</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="link-embed-resolution">Link Embed Resolution</h2>
<p>When a message contains URLs, the server fetches OpenGraph metadata for preview
embeds.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant CS as ChatService
participant LE as LinkEmbedService
participant Web as External Website
CS-&gt;&gt;LE: TryGetEmbedsAsync(messageContent)
LE-&gt;&gt;LE: Extract URLs via regex
LE-&gt;&gt;LE: Filter: max URLs per message, skip duplicates
loop For each URL
LE-&gt;&gt;LE: Validate: http(s) only, block private IPs
LE-&gt;&gt;Web: GET URL (timeout + size limit)
Web--&gt;&gt;LE: HTML response
LE-&gt;&gt;LE: Parse og:title, og:description, og:site_name
LE-&gt;&gt;LE: Parse theme-color meta tag
LE-&gt;&gt;LE: Fallback to &lt;title&gt; if no og:title
end
LE--&gt;&gt;CS: List&lt;EmbedDto&gt; (or null)
Note over CS: Attached to MessageDto before broadcast
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Entry point</td>
<td><code>src/EchoHub.Server/Services/LinkEmbedService.cs</code></td>
<td>Lines 28-51 (<code>TryGetEmbedsAsync</code>)</td>
</tr>
<tr>
<td>URL extraction</td>
<td><code>src/EchoHub.Server/Services/LinkEmbedService.cs</code></td>
<td>Lines 145-160</td>
</tr>
<tr>
<td>Private IP blocking</td>
<td><code>src/EchoHub.Server/Services/LinkEmbedService.cs</code></td>
<td>Lines 162-181</td>
</tr>
<tr>
<td>OG tag parsing</td>
<td><code>src/EchoHub.Server/Services/LinkEmbedService.cs</code></td>
<td>Lines 187-210</td>
</tr>
<tr>
<td>Theme color parsing</td>
<td><code>src/EchoHub.Server/Services/LinkEmbedService.cs</code></td>
<td>Lines 117-143</td>
</tr>
<tr>
<td>ChatService integration</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 194-201</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="server-directory-registration">Server Directory Registration</h2>
<p>Public servers register with the EchoHubSpace directory for discoverability.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant SDS as ServerDirectoryService
participant Dir as EchoHubSpace Directory
participant PT as PresenceTracker
SDS-&gt;&gt;SDS: Check Server:PublicServer config
SDS-&gt;&gt;Dir: SignalR connect (echohub.voidcube.cloud/hubs/servers)
SDS-&gt;&gt;Dir: RegisterServer(name, description, host, userCount)
Dir--&gt;&gt;SDS: Registered
loop Every 30 seconds
SDS-&gt;&gt;PT: Get online user count
alt Count changed
SDS-&gt;&gt;Dir: UpdateUserCount(count)
end
end
Dir-&gt;&gt;SDS: Ping
SDS-&gt;&gt;Dir: Heartbeat
Note over SDS,Dir: Exponential backoff on disconnect (2s → 30s max)
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Service lifecycle</td>
<td><code>src/EchoHub.Server/Services/ServerDirectoryService.cs</code></td>
<td>Lines 29-122</td>
</tr>
<tr>
<td>Registration</td>
<td><code>src/EchoHub.Server/Services/ServerDirectoryService.cs</code></td>
<td>Lines 195-212</td>
</tr>
<tr>
<td>User count polling</td>
<td><code>src/EchoHub.Server/Services/ServerDirectoryService.cs</code></td>
<td>Lines 154-187</td>
</tr>
<tr>
<td>Reconnection backoff</td>
<td><code>src/EchoHub.Server/Services/ServerDirectoryService.cs</code></td>
<td>Lines 77-82, 191</td>
</tr>
</tbody>
</table>
</article>
<div class="contribution d-print-none">
<a href="https://github.com/HueByte/EchoHub/blob/master/docs/#L1" class="edit-link">Edit this page</a>
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<div class='footer-custom'><div class='footer-inner'><span class='footer-brand'>EchoHub</span><span class='footer-sep'>&middot;</span><a href='https://github.com/HueByte/EchoHub'>GitHub</a><span class='footer-sep'>&middot;</span><a href='https://echohub.voidcube.cloud'>Website</a><span class='footer-sep'>&middot;</span><span class='footer-credit'>Built with <a href='https://dotnet.github.io/docfx'>DocFX</a></span></div></div>
</div>
</div>
</footer>
</body>
</html>
+513
View File
@@ -0,0 +1,513 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Messaging | EchoHub Documentation </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Messaging | EchoHub Documentation ">
<link rel="icon" href="../images/hue_icon.svg">
<link rel="stylesheet" href="../public/docfx.min.css">
<link rel="stylesheet" href="../public/main.css">
<meta name="docfx:navrel" content="../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../">
<meta name="docfx:docurl" content="https://github.com/HueByte/EchoHub/blob/master/docs/#L1">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../index.html">
<img id="logo" class="svg" src="../images/hue_icon.svg" alt="EchoHub">
EchoHub
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="">
<h1 id="messaging">Messaging</h1>
<h2 id="sending-a-message-signalr">Sending a Message (SignalR)</h2>
<p>A message typed in the TUI travels through encryption, the SignalR hub,
<code>ChatService</code> validation, database storage, and fan-out to both SignalR and IRC
clients.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant UI as MainWindow (TUI)
participant AO as AppOrchestrator
participant EHC as EchoHubConnection
participant Hub as ChatHub
participant CS as ChatService
participant LE as LinkEmbedService
participant DB as SQLite
participant SRB as SignalRBroadcaster
participant IRCB as IrcBroadcaster
participant Clients as Other Clients
UI-&gt;&gt;AO: OnMessageSubmitted(channel, text)
AO-&gt;&gt;AO: IsCommand(text)? → No
AO-&gt;&gt;EHC: SendMessageAsync(channel, text)
EHC-&gt;&gt;EHC: Encrypt(text) → ciphertext
EHC-&gt;&gt;Hub: InvokeAsync(&quot;SendMessage&quot;, channel, ciphertext)
Hub-&gt;&gt;CS: SendMessageAsync(userId, username, channel, ciphertext)
CS-&gt;&gt;CS: Decrypt(ciphertext) → plaintext
CS-&gt;&gt;CS: Validate (length, newlines, channel exists)
CS-&gt;&gt;DB: Check mute status
CS-&gt;&gt;LE: TryGetEmbedsAsync(plaintext)
LE-&gt;&gt;LE: Extract URLs, fetch OG tags
LE--&gt;&gt;CS: List&lt;EmbedDto&gt; (or null)
CS-&gt;&gt;DB: INSERT Message (encrypted at rest)
CS-&gt;&gt;CS: Re-encrypt plaintext for transport
CS-&gt;&gt;CS: Build MessageDto with embeds
par Fan-out to all broadcasters
CS-&gt;&gt;SRB: SendMessageToChannelAsync(channel, dto)
SRB-&gt;&gt;Clients: HubContext.Group(channel).ReceiveMessage(dto)
and
CS-&gt;&gt;IRCB: SendMessageToChannelAsync(channel, dto)
IRCB-&gt;&gt;IRCB: Decrypt → format as PRIVMSG lines
IRCB-&gt;&gt;Clients: Send to each IRC conn (skip sender)
end
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Input handler</td>
<td><code>src/EchoHub.Client/UI/MainWindow.cs</code></td>
<td>Lines 428-449 (<code>OnInputKeyDown</code>)</td>
</tr>
<tr>
<td>Orchestrator dispatch</td>
<td><code>src/EchoHub.Client/AppOrchestrator.cs</code></td>
<td>Lines 631-661 (<code>HandleMessageSubmitted</code>)</td>
</tr>
<tr>
<td>Client encrypt + send</td>
<td><code>src/EchoHub.Client/Services/EchoHubConnection.cs</code></td>
<td>Lines 148-153 (<code>SendMessageAsync</code>)</td>
</tr>
<tr>
<td>Hub receive</td>
<td><code>src/EchoHub.Server/Hubs/ChatHub.cs</code></td>
<td>Lines 98-111 (<code>SendMessage</code>)</td>
</tr>
<tr>
<td>ChatService process</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 145-241 (<code>SendMessageAsync</code>)</td>
</tr>
<tr>
<td>Mute check</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 177-190</td>
</tr>
<tr>
<td>Link embeds</td>
<td><code>src/EchoHub.Server/Services/LinkEmbedService.cs</code></td>
<td>Lines 28-51 (<code>TryGetEmbedsAsync</code>)</td>
</tr>
<tr>
<td>DB insert</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 208-221</td>
</tr>
<tr>
<td>Broadcast fan-out</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 311-324 (<code>BroadcastToAllAsync</code>)</td>
</tr>
<tr>
<td>SignalR broadcast</td>
<td><code>src/EchoHub.Server/Services/SignalRBroadcaster.cs</code></td>
<td>Lines 23-24</td>
</tr>
<tr>
<td>IRC broadcast</td>
<td><code>src/EchoHub.Server.Irc/IrcBroadcaster.cs</code></td>
<td>Lines 17-32</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="sending-a-message-irc">Sending a Message (IRC)</h2>
<p>Messages from IRC clients follow the same <code>ChatService</code> path but enter as
plaintext (no app-layer encryption).</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant IRC as IRC Client
participant CH as IrcCommandHandler
participant CS as ChatService
participant DB as SQLite
participant SRB as SignalRBroadcaster
participant IRCB as IrcBroadcaster
IRC-&gt;&gt;CH: PRIVMSG #channel :Hello world
CH-&gt;&gt;CH: Parse target + content
CH-&gt;&gt;CH: IrcToEchoHubChannel(&quot;#channel&quot;) → &quot;channel&quot;
CH-&gt;&gt;CS: SendMessageAsync(userId, username, &quot;channel&quot;, &quot;Hello world&quot;)
CS-&gt;&gt;CS: Decrypt(&quot;Hello world&quot;) → passthrough (no $ENC$ prefix)
CS-&gt;&gt;CS: Validate, check mute, fetch embeds
CS-&gt;&gt;DB: INSERT Message
CS-&gt;&gt;CS: Encrypt plaintext for SignalR transport
par Fan-out
CS-&gt;&gt;SRB: SendMessageToChannelAsync (encrypted for SignalR)
and
CS-&gt;&gt;IRCB: SendMessageToChannelAsync (decrypt → PRIVMSG)
IRCB-&gt;&gt;IRCB: Skip sender (IRC echo suppression)
end
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>PRIVMSG handler</td>
<td><code>src/EchoHub.Server.Irc/IrcCommandHandler.cs</code></td>
<td>Lines 437-469</td>
</tr>
<tr>
<td>Channel name conversion</td>
<td><code>src/EchoHub.Server.Irc/IrcCommandHandler.cs</code></td>
<td>Line 680 (<code>IrcToEchoHubChannel</code>)</td>
</tr>
<tr>
<td>ChatService (shared path)</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 145-241</td>
</tr>
<tr>
<td>IRC echo suppression</td>
<td><code>src/EchoHub.Server.Irc/IrcBroadcaster.cs</code></td>
<td>Lines 25-26</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="receiving-a-message-tui-client">Receiving a Message (TUI Client)</h2>
<p>When a message arrives via SignalR, the client decrypts it, adds it to the chat
view, and optionally plays a notification sound for @mentions.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant SRB as SignalRBroadcaster
participant EHC as EchoHubConnection
participant AO as AppOrchestrator
participant MM as ChatMessageManager
participant UI as MainWindow
SRB-&gt;&gt;EHC: ReceiveMessage(MessageDto)
EHC-&gt;&gt;EHC: Decrypt(message.Content)
EHC--&gt;&gt;AO: OnMessageReceived(decrypted dto)
AO-&gt;&gt;AO: InvokeUI (thread-safe)
AO-&gt;&gt;MM: AddMessage(message)
MM-&gt;&gt;UI: Render in chat ListView
alt Message contains @username
AO-&gt;&gt;AO: PlayAsync() notification sound
end
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>SignalR handler</td>
<td><code>src/EchoHub.Client/Services/EchoHubConnection.cs</code></td>
<td>Lines 64-71</td>
</tr>
<tr>
<td>Orchestrator receive</td>
<td><code>src/EchoHub.Client/AppOrchestrator.cs</code></td>
<td>Lines 372-383</td>
</tr>
<tr>
<td>@mention detection</td>
<td><code>src/EchoHub.Client/AppOrchestrator.cs</code></td>
<td>Lines 378-382</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="command-execution">Command Execution</h2>
<p>Slash commands (<code>/status</code>, <code>/nick</code>, <code>/kick</code>, etc.) are parsed client-side and
dispatched to appropriate handlers, which call REST APIs or SignalR methods.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant UI as MainWindow
participant AO as AppOrchestrator
participant CMD as CommandHandler
participant API as ApiClient
participant EHC as EchoHubConnection
participant Server as Server (API/Hub)
UI-&gt;&gt;AO: OnMessageSubmitted(channel, &quot;/kick baduser&quot;)
AO-&gt;&gt;CMD: IsCommand(&quot;/kick baduser&quot;)? → true
AO-&gt;&gt;CMD: HandleAsync(&quot;/kick baduser&quot;)
CMD-&gt;&gt;CMD: Parse → command=&quot;kick&quot;, args=&quot;baduser&quot;
alt API command (kick, ban, mute, nick, etc.)
CMD--&gt;&gt;AO: Fire OnKickRequested(&quot;baduser&quot;)
AO-&gt;&gt;API: KickUserAsync(&quot;baduser&quot;)
API-&gt;&gt;Server: POST /api/moderation/kick/baduser
else Hub command (status, join, leave, etc.)
CMD--&gt;&gt;AO: Fire OnSetStatus / OnJoinChannel / etc.
AO-&gt;&gt;EHC: UpdateStatusAsync() / JoinChannelAsync() / etc.
EHC-&gt;&gt;Server: SignalR Invoke
else Local command (theme, help, quit)
CMD--&gt;&gt;AO: Fire OnThemeChanged / etc.
AO-&gt;&gt;UI: Apply locally (no server call)
end
AO-&gt;&gt;UI: AddSystemMessage(result)
</code></pre>
<p><strong>Available commands:</strong></p>
<table>
<thead>
<tr>
<th>Command</th>
<th>Type</th>
<th>Handler</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>/status &lt;status&gt; [message]</code></td>
<td>Hub</td>
<td><code>UpdateStatusAsync</code></td>
</tr>
<tr>
<td><code>/nick &lt;name&gt;</code></td>
<td>API</td>
<td><code>UpdateProfileAsync</code></td>
</tr>
<tr>
<td><code>/color &lt;hex&gt;</code></td>
<td>API</td>
<td><code>UpdateProfileAsync</code></td>
</tr>
<tr>
<td><code>/join &lt;channel&gt;</code></td>
<td>Hub</td>
<td><code>JoinChannelAsync</code></td>
</tr>
<tr>
<td><code>/leave</code></td>
<td>Hub</td>
<td><code>LeaveChannelAsync</code></td>
</tr>
<tr>
<td><code>/topic &lt;text&gt;</code></td>
<td>API</td>
<td><code>UpdateChannelTopicAsync</code></td>
</tr>
<tr>
<td><code>/kick &lt;user&gt;</code></td>
<td>API</td>
<td><code>POST /api/moderation/kick/{user}</code></td>
</tr>
<tr>
<td><code>/ban &lt;user&gt;</code></td>
<td>API</td>
<td><code>POST /api/moderation/ban/{user}</code></td>
</tr>
<tr>
<td><code>/unban &lt;user&gt;</code></td>
<td>API</td>
<td><code>POST /api/moderation/unban/{user}</code></td>
</tr>
<tr>
<td><code>/mute &lt;user&gt; [mins]</code></td>
<td>API</td>
<td><code>POST /api/moderation/mute/{user}</code></td>
</tr>
<tr>
<td><code>/unmute &lt;user&gt;</code></td>
<td>API</td>
<td><code>POST /api/moderation/unmute/{user}</code></td>
</tr>
<tr>
<td><code>/role &lt;user&gt; &lt;role&gt;</code></td>
<td>API</td>
<td><code>PUT /api/moderation/role/{user}</code></td>
</tr>
<tr>
<td><code>/nuke</code></td>
<td>API</td>
<td><code>DELETE /api/channels/{channel}/messages</code></td>
</tr>
<tr>
<td><code>/send &lt;file&gt;</code></td>
<td>API</td>
<td><code>POST /api/channels/{channel}/upload</code></td>
</tr>
<tr>
<td><code>/profile [user]</code></td>
<td>Local</td>
<td>Show profile dialog</td>
</tr>
<tr>
<td><code>/avatar</code></td>
<td>API</td>
<td><code>POST /api/users/avatar</code></td>
</tr>
<tr>
<td><code>/theme &lt;name&gt;</code></td>
<td>Local</td>
<td><code>ThemeManager.SetTheme()</code></td>
</tr>
<tr>
<td><code>/servers</code></td>
<td>API</td>
<td><code>GET /api/serverdir/servers</code></td>
</tr>
<tr>
<td><code>/users</code></td>
<td>Local</td>
<td>Show userlist</td>
</tr>
<tr>
<td><code>/help</code></td>
<td>Local</td>
<td>Show help text</td>
</tr>
<tr>
<td><code>/quit</code></td>
<td>Local</td>
<td>Exit application</td>
</tr>
</tbody>
</table>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Command detection</td>
<td><code>src/EchoHub.Client/AppOrchestrator.cs</code></td>
<td>Lines 639-655</td>
</tr>
<tr>
<td>Command dispatch</td>
<td><code>src/EchoHub.Client/Commands/CommandHandler.cs</code></td>
<td>Lines 34-69 (<code>HandleAsync</code>)</td>
</tr>
<tr>
<td>Command handlers wired</td>
<td><code>src/EchoHub.Client/AppOrchestrator.cs</code></td>
<td>Lines 97-117</td>
</tr>
<tr>
<td>Individual handlers</td>
<td><code>src/EchoHub.Client/AppOrchestrator.cs</code></td>
<td>Lines 122-350</td>
</tr>
</tbody>
</table>
</article>
<div class="contribution d-print-none">
<a href="https://github.com/HueByte/EchoHub/blob/master/docs/#L1" class="edit-link">Edit this page</a>
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<div class='footer-custom'><div class='footer-inner'><span class='footer-brand'>EchoHub</span><span class='footer-sep'>&middot;</span><a href='https://github.com/HueByte/EchoHub'>GitHub</a><span class='footer-sep'>&middot;</span><a href='https://echohub.voidcube.cloud'>Website</a><span class='footer-sep'>&middot;</span><span class='footer-credit'>Built with <a href='https://dotnet.github.io/docfx'>DocFX</a></span></div></div>
</div>
</div>
</footer>
</body>
</html>
+196
View File
@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Moderation | EchoHub Documentation </title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="title" content="Moderation | EchoHub Documentation ">
<link rel="icon" href="../images/hue_icon.svg">
<link rel="stylesheet" href="../public/docfx.min.css">
<link rel="stylesheet" href="../public/main.css">
<meta name="docfx:navrel" content="../toc.html">
<meta name="docfx:tocrel" content="toc.html">
<meta name="docfx:rel" content="../">
<meta name="docfx:docurl" content="https://github.com/HueByte/EchoHub/blob/master/docs/#L1">
<meta name="loc:inThisArticle" content="In this article">
<meta name="loc:searchResultsCount" content="{count} results for &quot;{query}&quot;">
<meta name="loc:searchNoResults" content="No results for &quot;{query}&quot;">
<meta name="loc:tocFilter" content="Filter by title">
<meta name="loc:nextArticle" content="Next">
<meta name="loc:prevArticle" content="Previous">
<meta name="loc:themeLight" content="Light">
<meta name="loc:themeDark" content="Dark">
<meta name="loc:themeAuto" content="Auto">
<meta name="loc:changeTheme" content="Change theme">
<meta name="loc:copy" content="Copy">
<meta name="loc:downloadPdf" content="Download PDF">
<script type="module" src="./../public/docfx.min.js"></script>
<script>
const theme = localStorage.getItem('theme') || 'auto'
document.documentElement.setAttribute('data-bs-theme', theme === 'auto' ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : theme)
</script>
</head>
<body class="tex2jax_ignore" data-layout="" data-yaml-mime="">
<header class="bg-body border-bottom">
<nav id="autocollapse" class="navbar navbar-expand-md" role="navigation">
<div class="container-xxl flex-nowrap">
<a class="navbar-brand" href="../index.html">
<img id="logo" class="svg" src="../images/hue_icon.svg" alt="EchoHub">
EchoHub
</a>
<button class="btn btn-lg d-md-none border-0" type="button" data-bs-toggle="collapse" data-bs-target="#navpanel" aria-controls="navpanel" aria-expanded="false" aria-label="Toggle navigation">
<i class="bi bi-three-dots"></i>
</button>
<div class="collapse navbar-collapse" id="navpanel">
<div id="navbar">
<form class="search" role="search" id="search">
<i class="bi bi-search"></i>
<input class="form-control" id="search-query" type="search" disabled placeholder="Search" autocomplete="off" aria-label="Search">
</form>
</div>
</div>
</div>
</nav>
</header>
<main class="container-xxl">
<div class="toc-offcanvas">
<div class="offcanvas-md offcanvas-start" tabindex="-1" id="tocOffcanvas" aria-labelledby="tocOffcanvasLabel">
<div class="offcanvas-header">
<h5 class="offcanvas-title" id="tocOffcanvasLabel">Table of Contents</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#tocOffcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<nav class="toc" id="toc"></nav>
</div>
</div>
</div>
<div class="content">
<div class="actionbar">
<button class="btn btn-lg border-0 d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#tocOffcanvas" aria-controls="tocOffcanvas" aria-expanded="false" aria-label="Show table of contents">
<i class="bi bi-list"></i>
</button>
<nav id="breadcrumb"></nav>
</div>
<article data-uid="">
<h1 id="moderation">Moderation</h1>
<h2 id="kick--ban--mute">Kick / Ban / Mute</h2>
<p>Moderators and admins can kick, ban, or mute users via REST API or slash commands.
These actions force-disconnect the target and broadcast the event.</p>
<pre><code class="lang-mermaid">sequenceDiagram
participant Mod as Moderator
participant MC as ModerationController
participant CS as ChatService
participant PT as PresenceTracker
participant DB as SQLite
participant SRB as SignalRBroadcaster
participant IRCB as IrcBroadcaster
Mod-&gt;&gt;MC: POST /api/moderation/kick/{user}
alt Kick
MC-&gt;&gt;CS: Get user's channels
MC-&gt;&gt;CS: BroadcastToAllAsync(SendUserKickedAsync)
MC-&gt;&gt;CS: ForceDisconnectAndCleanupAsync(user)
else Ban
MC-&gt;&gt;DB: Set user.IsBanned = true
MC-&gt;&gt;CS: BroadcastToAllAsync(SendUserBannedAsync)
MC-&gt;&gt;CS: ForceDisconnectAndCleanupAsync(user)
else Mute
MC-&gt;&gt;DB: Set user.IsMuted = true, MutedUntil = now + duration
Note over DB: MuteExpirationService auto-unmutes when timer expires
end
CS-&gt;&gt;PT: ForceRemoveUser(username)
PT-&gt;&gt;PT: Remove from all connections + channels
CS-&gt;&gt;SRB: ForceDisconnectUserAsync(connectionIds, reason)
CS-&gt;&gt;IRCB: ForceDisconnectUserAsync(connectionIds, reason)
CS-&gt;&gt;DB: Set Status=Invisible, LastSeenAt=now
</code></pre>
<p><strong>Code references:</strong></p>
<table>
<thead>
<tr>
<th>Step</th>
<th>File</th>
<th>Location</th>
</tr>
</thead>
<tbody>
<tr>
<td>Kick endpoint</td>
<td><code>src/EchoHub.Server/Controllers/ModerationController.cs</code></td>
<td>Lines 62-87</td>
</tr>
<tr>
<td>Ban endpoint</td>
<td><code>src/EchoHub.Server/Controllers/ModerationController.cs</code></td>
<td>Lines 89-112</td>
</tr>
<tr>
<td>Mute endpoint</td>
<td><code>src/EchoHub.Server/Controllers/ModerationController.cs</code></td>
<td>Lines 130-151</td>
</tr>
<tr>
<td>Force disconnect</td>
<td><code>src/EchoHub.Server/Controllers/ModerationController.cs</code></td>
<td>Lines 232-256</td>
</tr>
<tr>
<td>Mute expiration</td>
<td><code>src/EchoHub.Server/Services/MuteExpirationService.cs</code></td>
<td>Lines 22-62</td>
</tr>
<tr>
<td>Mute enforcement</td>
<td><code>src/EchoHub.Server/Services/ChatService.cs</code></td>
<td>Lines 177-190</td>
</tr>
<tr>
<td>Presence force remove</td>
<td><code>src/EchoHub.Server/Services/PresenceTracker.cs</code></td>
<td>Lines 161-178</td>
</tr>
</tbody>
</table>
</article>
<div class="contribution d-print-none">
<a href="https://github.com/HueByte/EchoHub/blob/master/docs/#L1" class="edit-link">Edit this page</a>
</div>
<div class="next-article d-print-none border-top" id="nextArticle"></div>
</div>
<div class="affix">
<nav id="affix"></nav>
</div>
</main>
<div class="container-xxl search-results" id="search-results"></div>
<footer class="border-top text-secondary">
<div class="container-xxl">
<div class="flex-fill">
<div class='footer-custom'><div class='footer-inner'><span class='footer-brand'>EchoHub</span><span class='footer-sep'>&middot;</span><a href='https://github.com/HueByte/EchoHub'>GitHub</a><span class='footer-sep'>&middot;</span><a href='https://echohub.voidcube.cloud'>Website</a><span class='footer-sep'>&middot;</span><span class='footer-credit'>Built with <a href='https://dotnet.github.io/docfx'>DocFX</a></span></div></div>
</div>
</div>
</footer>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<div id="sidetoggle">
<div>
<div class="sidefilter">
<form class="toc-filter">
<span class="glyphicon glyphicon-filter filter-icon"></span>
<span class="glyphicon glyphicon-remove clear-icon" id="toc_filter_clear"></span>
<input type="text" id="toc_filter_input" placeholder="Filter by title" onkeypress="if(event.keyCode==13) {return false;}">
</form>
</div>
<div class="sidetoc">
<div class="toc" id="toc">
<ul class="nav level1">
<li>
<a href="authentication.html" name="" title="Authentication">Authentication</a>
</li>
<li>
<a href="connection.html" name="" title="Connection">Connection</a>
</li>
<li>
<a href="messaging.html" name="" title="Messaging">Messaging</a>
</li>
<li>
<a href="channels.html" name="" title="Channels">Channels</a>
</li>
<li>
<a href="moderation.html" name="" title="Moderation">Moderation</a>
</li>
<li>
<a href="media.html" name="" title="Media &amp; Services">Media &amp; Services</a>
</li>
</ul>
</div>
</div>
</div>
</div>
+2
View File
@@ -0,0 +1,2 @@
{"items":[{"name":"Authentication","href":"authentication.html","topicHref":"authentication.html"},{"name":"Connection","href":"connection.html","topicHref":"connection.html"},{"name":"Messaging","href":"messaging.html","topicHref":"messaging.html"},{"name":"Channels","href":"channels.html","topicHref":"channels.html"},{"name":"Moderation","href":"moderation.html","topicHref":"moderation.html"},{"name":"Media & Services","href":"media.html","topicHref":"media.html"}]}