From fb4f6c34ed7d44d8c58d8628e3ca493c1f851a91 Mon Sep 17 00:00:00 2001 From: HueByte Date: Tue, 24 Feb 2026 20:50:20 +0100 Subject: [PATCH 1/2] feat: add detailed documentation for authentication, connection, messaging, channels, moderation, and media flows --- docs/flows/authentication.md | 142 +++++++++++++++++++++++ docs/flows/channels.md | 159 ++++++++++++++++++++++++++ docs/flows/connection.md | 150 +++++++++++++++++++++++++ docs/flows/flows.md | 3 + docs/flows/media.md | 118 ++++++++++++++++++++ docs/flows/messaging.md | 210 +++++++++++++++++++++++++++++++++++ docs/flows/moderation.md | 50 +++++++++ docs/flows/toc.yml | 12 ++ docs/toc.yml | 3 + 9 files changed, 847 insertions(+) create mode 100644 docs/flows/authentication.md create mode 100644 docs/flows/channels.md create mode 100644 docs/flows/connection.md create mode 100644 docs/flows/flows.md create mode 100644 docs/flows/media.md create mode 100644 docs/flows/messaging.md create mode 100644 docs/flows/moderation.md create mode 100644 docs/flows/toc.yml diff --git a/docs/flows/authentication.md b/docs/flows/authentication.md new file mode 100644 index 0000000..036aaab --- /dev/null +++ b/docs/flows/authentication.md @@ -0,0 +1,142 @@ +# Authentication + +## User Registration + +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 "Remember Me" sessions. + +```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->>AO: ConnectDialogResult(IsRegister: true) + AO->>CM: ConnectAsync(dialogResult) + CM->>API: RegisterAsync(username, password) + API->>Auth: POST /api/auth/register + Auth->>US: RegisterUserAsync(username, password, displayName) + US->>US: Validate (regex, length, uniqueness) + US->>DB: INSERT User (BCrypt hash) + US-->>Auth: UserOperationResult.Success + Auth->>JWT: GenerateAccessToken(user) + JWT-->>Auth: (token, expiresAt) [15 min] + Auth->>JWT: GenerateRefreshToken() + JWT-->>Auth: Base64 random (64 bytes) + Auth->>DB: INSERT RefreshToken (SHA256 hash) + Auth-->>API: LoginResponse + API->>API: SetTokens() — store in memory + set Bearer header + API-->>CM: LoginResponse + CM->>CM: Wire OnTokensRefreshed for config persistence + CM->>CM: Continue to connection setup (see Connection Flow) +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Dialog UI | `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs` | Lines 251-268 (register handler) | +| Orchestrator entry | `src/EchoHub.Client/AppOrchestrator.cs` | Lines 550-592 (`HandleConnect`) | +| ConnectionManager auth | `src/EchoHub.Client/Services/ConnectionManager.cs` | Lines 74-76 (register branch) | +| ApiClient register | `src/EchoHub.Client/Services/ApiClient.cs` | Lines 32-43 (`RegisterAsync`) | +| AuthController register | `src/EchoHub.Server/Controllers/AuthController.cs` | Lines 28-49 | +| UserService register | `src/EchoHub.Server/Services/UserService.cs` | Lines 20-59 (`RegisterUserAsync`) | +| JWT generation | `src/EchoHub.Server/Auth/JwtTokenService.cs` | Lines 30-53 (access), 80-86 (refresh) | +| Token persistence | `src/EchoHub.Client/AppOrchestrator.cs` | Lines 1039-1053 (`SaveServerToConfig`) | + +--- + +## User Login + +Returning user authenticates with username/password or a saved refresh token. + +```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->>CM: ConnectDialogResult(SavedRefreshToken: "...") + CM->>API: LoginWithRefreshTokenAsync() + API->>Auth: POST /api/auth/refresh + Auth->>DB: Lookup token by SHA256 hash + Auth->>DB: Revoke old token, issue new pair + Auth-->>API: LoginResponse (rotated tokens) + else Username + Password + UI->>CM: ConnectDialogResult(IsRegister: false) + CM->>API: LoginAsync(username, password) + API->>Auth: POST /api/auth/login + Auth->>US: AuthenticateUserAsync(username, password) + US->>DB: Fetch user, BCrypt.Verify(password, hash) + US->>DB: Update LastSeenAt + US-->>Auth: UserOperationResult.Success + Auth-->>API: LoginResponse + end + API->>API: SetTokens() +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Login button handler | `src/EchoHub.Client/UI/Dialogs/ConnectDialog.cs` | Lines 214-249 | +| Saved token branch | `src/EchoHub.Client/Services/ConnectionManager.cs` | Lines 69-71 | +| Password branch | `src/EchoHub.Client/Services/ConnectionManager.cs` | Lines 78-80 | +| ApiClient login | `src/EchoHub.Client/Services/ApiClient.cs` | Lines 45-56 (`LoginAsync`) | +| AuthController login | `src/EchoHub.Server/Controllers/AuthController.cs` | Lines 51-72 | +| AuthController refresh | `src/EchoHub.Server/Controllers/AuthController.cs` | Lines 74-108 | +| UserService authenticate | `src/EchoHub.Server/Services/UserService.cs` | Lines 61-83 | + +--- + +## Token Refresh + +Access tokens expire after 15 minutes. The client auto-refreshes transparently +before requests and on 401 responses. Refresh tokens are rotated on each use. + +```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->>API: GetValidTokenAsync() or HTTP 401 + API->>API: Token expires within 60s? + alt Proactive refresh (SignalR token provider) + API->>Auth: POST /api/auth/refresh (old refresh token) + else Reactive refresh (HTTP 401 retry) + API->>Auth: POST /api/auth/refresh (old refresh token) + end + Auth->>DB: Lookup by SHA256 hash + Auth->>DB: Revoke old refresh token + Auth->>DB: INSERT new RefreshToken + Auth-->>API: LoginResponse (new token pair) + API->>API: SetTokens() — update Bearer header + API-->>API: Fire OnTokensRefreshed event + API-->>Config: Persist new refresh token (if Remember Me) + API->>SR: Retry original request with new token +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Proactive check | `src/EchoHub.Client/Services/ApiClient.cs` | Lines 110-129 (`GetValidTokenAsync`) | +| Reactive 401 retry (GET) | `src/EchoHub.Client/Services/ApiClient.cs` | Lines 338-358 (`AuthenticatedGetAsync`) | +| Reactive 401 retry (POST/PUT/DELETE) | `src/EchoHub.Client/Services/ApiClient.cs` | Lines 364-384 (`AuthenticatedRequestAsync`) | +| Refresh HTTP call | `src/EchoHub.Client/Services/ApiClient.cs` | Lines 58-71 (`RefreshTokenAsync`) | +| SignalR token provider | `src/EchoHub.Client/Services/EchoHubConnection.cs` | Line 37 (`AccessTokenProvider`) | +| Server-side rotation | `src/EchoHub.Server/Controllers/AuthController.cs` | Lines 74-108 | +| Token persistence callback | `src/EchoHub.Client/Services/ConnectionManager.cs` | Lines 253-264 | diff --git a/docs/flows/channels.md b/docs/flows/channels.md new file mode 100644 index 0000000..278efab --- /dev/null +++ b/docs/flows/channels.md @@ -0,0 +1,159 @@ +# Channels + +## Channel Creation + +Channels are created via the REST API. Public channels are broadcast to all +connected clients. + +```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->>CC: POST /api/channels {name, topic, isPublic} + CC->>ChS: CreateChannelAsync(creatorId, name, topic, isPublic) + ChS->>ChS: Normalize name (lowercase, trim) + ChS->>ChS: Validate format (2-100 chars, regex) + ChS->>DB: Check duplicate + ChS->>DB: INSERT Channel + ChannelMembership (creator auto-added) + ChS-->>CC: ChannelDto + + alt Channel is public + CC->>CS: BroadcastChannelUpdatedAsync(channel) + CS->>SRB: SendChannelUpdatedAsync(channelDto) + SRB->>SRB: Notify all SignalR clients + CS->>IRCB: SendChannelUpdatedAsync(channelDto) + end + + CC-->>Client: 201 Created (ChannelDto) +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Controller endpoint | `src/EchoHub.Server/Controllers/ChannelsController.cs` | Lines 60-76 | +| Channel service create | `src/EchoHub.Server/Services/ChannelService.cs` | Lines 50-90 | +| Broadcast updated | `src/EchoHub.Server/Services/ChatService.cs` | Lines 308-309 | + +--- + +## Channel Deletion + +Only the channel creator (or admin) can delete a channel. The default channel +is protected. + +```mermaid +sequenceDiagram + participant Client as Client + participant CC as ChannelsController + participant ChS as ChannelService + participant DB as SQLite + + Client->>CC: DELETE /api/channels/{channel} + CC->>ChS: DeleteChannelAsync(channelName, callerId) + ChS->>ChS: Reject if default channel + ChS->>DB: Lookup channel + ChS->>ChS: Verify caller is creator or admin + ChS->>DB: DELETE Channel (cascade: messages, memberships) + ChS-->>CC: Success + CC-->>Client: 204 No Content +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Controller endpoint | `src/EchoHub.Server/Controllers/ChannelsController.cs` | Lines 94-106 | +| Channel service delete | `src/EchoHub.Server/Services/ChannelService.cs` | Lines 119-144 | + +--- + +## Joining a Channel + +Both SignalR and IRC clients join channels through `ChatService`. The presence +tracker determines if this is a genuinely new join (vs. a second connection) and +broadcasts accordingly. + +```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->>Entry: JoinChannel / JOIN #channel + Entry->>CS: JoinChannelAsync(connectionId, userId, username, channel) + CS->>ChS: EnsureChannelMembershipAsync(userId, channel) + ChS->>DB: INSERT ChannelMembership (if not exists) + + CS->>PT: JoinChannel(username, channel) + PT-->>CS: isNewJoin? + + alt First connection in this channel + CS->>DB: Fetch UserPresenceDto + CS->>CS: BroadcastToAllAsync(SendUserJoinedAsync) + par + CS->>SRB: SendUserJoinedAsync(channel, user, excludeConn) + and + CS->>IRCB: SendUserJoinedAsync(channel, user) + end + end + + CS->>DB: Fetch message history + CS-->>Entry: (history, error) + Entry-->>Client: History messages +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| SignalR hub join | `src/EchoHub.Server/Hubs/ChatHub.cs` | Lines 59-81 | +| IRC join | `src/EchoHub.Server.Irc/IrcCommandHandler.cs` | Lines 361-414 | +| ChatService join | `src/EchoHub.Server/Services/ChatService.cs` | Lines 96-135 | +| Presence join | `src/EchoHub.Server/Services/PresenceTracker.cs` | Lines 58-70 | +| SignalR broadcast | `src/EchoHub.Server/Services/SignalRBroadcaster.cs` | Lines 26-32 | +| IRC broadcast | `src/EchoHub.Server.Irc/IrcBroadcaster.cs` | Lines 34-41 | + +--- + +## Leaving a Channel + +```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->>Entry: LeaveChannel / PART #channel + Entry->>CS: LeaveChannelAsync(connectionId, username, channel) + CS->>PT: LeaveChannel(username, channel) + CS->>CS: BroadcastToAllAsync(SendUserLeftAsync) + par + CS->>SRB: SendUserLeftAsync(channel, username) + and + CS->>IRCB: SendUserLeftAsync(channel, username) + end +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| SignalR hub leave | `src/EchoHub.Server/Hubs/ChatHub.cs` | Lines 83-96 | +| IRC part | `src/EchoHub.Server.Irc/IrcCommandHandler.cs` | Lines 416-435 | +| ChatService leave | `src/EchoHub.Server/Services/ChatService.cs` | Lines 137-143 | +| Presence leave | `src/EchoHub.Server/Services/PresenceTracker.cs` | Lines 72-81 | diff --git a/docs/flows/connection.md b/docs/flows/connection.md new file mode 100644 index 0000000..ff4ccaa --- /dev/null +++ b/docs/flows/connection.md @@ -0,0 +1,150 @@ +# Connection + +## SignalR Client Connection + +After authentication, the TUI client establishes a SignalR WebSocket, registers +event handlers, joins the default channel, and loads history. + +```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->>CM: Fetch encryption key (GET /api/server/encryption-key) + CM->>EHC: new EchoHubConnection(apiClient, encryption) + EHC->>EHC: Build HubConnection (URL + JWT token provider + auto-reconnect) + EHC->>EHC: RegisterHandlers() — wire ReceiveMessage, UserJoined, etc. + EHC->>Hub: ConnectAsync() → WebSocket handshake + Hub->>CS: UserConnectedAsync(connectionId, userId, username) + CS->>PT: UserConnected(connectionId, userId, username) + CS->>DB: Update user: Status=Online, LastSeenAt=now + CM->>CM: Fetch channel list (GET /api/channels) + CM->>EHC: JoinChannelAsync("general") + EHC->>Hub: InvokeAsync("JoinChannel", "general") + Hub->>CS: JoinChannelAsync(connectionId, userId, username, "general") + CS->>DB: EnsureChannelMembership + CS->>PT: JoinChannel(username, "general") + CS->>CS: BroadcastToAllAsync → UserJoined + CS->>DB: Fetch message history + Hub-->>EHC: List (encrypted) + EHC-->>CM: Decrypted history +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Connection orchestration | `src/EchoHub.Client/Services/ConnectionManager.cs` | Lines 58-140 (`ConnectAsync`) | +| EchoHubConnection setup | `src/EchoHub.Client/Services/EchoHubConnection.cs` | Lines 29-62 (constructor) | +| Handler registration | `src/EchoHub.Client/Services/EchoHubConnection.cs` | Lines 64-122 (`RegisterHandlers`) | +| Hub OnConnected | `src/EchoHub.Server/Hubs/ChatHub.cs` | Lines 31-43 | +| ChatService connected | `src/EchoHub.Server/Services/ChatService.cs` | Lines 41-57 | +| PresenceTracker connect | `src/EchoHub.Server/Services/PresenceTracker.cs` | Lines 13-29 | +| Join channel (hub) | `src/EchoHub.Server/Hubs/ChatHub.cs` | Lines 59-81 | +| Join channel (service) | `src/EchoHub.Server/Services/ChatService.cs` | Lines 96-135 | + +--- + +## IRC Client Connection + +IRC clients connect via TCP, authenticate with PASS/NICK/USER or SASL PLAIN, +and auto-join channels. New usernames are auto-registered. + +```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->>GW: TCP connect (:6667 or :6697 TLS) + GW->>GW: Accept + create IrcClientConnection + GW->>CH: new IrcCommandHandler(connection, services) + GW->>CH: RunAsync() — start read loop + + alt SASL PLAIN + IRC->>CH: CAP REQ :sasl + CH-->>IRC: CAP ACK :sasl + IRC->>CH: AUTHENTICATE PLAIN + CH-->>IRC: AUTHENTICATE + + IRC->>CH: AUTHENTICATE + CH->>US: AuthenticateUserAsync(user, pass) + alt Auth fails → auto-register + CH->>US: RegisterUserAsync(user, pass) + end + CH-->>IRC: 903 :SASL authentication successful + else PASS/NICK/USER + IRC->>CH: PASS + IRC->>CH: NICK + IRC->>CH: USER 0 * : + CH->>US: AuthenticateUserAsync(nick, pass) + alt Auth fails → auto-register + CH->>US: RegisterUserAsync(nick, pass) + end + end + + CH->>CS: UserConnectedAsync(irc-{guid}, userId, username) + CS->>PT: UserConnected(irc-{guid}, userId, username) + CH-->>IRC: 001-004 RPL_WELCOME burst + MOTD + + Note over IRC,CH: Client is now ready for JOIN/PART/PRIVMSG +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| TCP listener | `src/EchoHub.Server.Irc/IrcGatewayService.cs` | Lines 45-90 (`ExecuteAsync`) | +| Client handler | `src/EchoHub.Server.Irc/IrcGatewayService.cs` | Lines 92-154 (`HandleClientAsync`) | +| Command read loop | `src/EchoHub.Server.Irc/IrcCommandHandler.cs` | Lines 40-98 (`RunAsync`) | +| SASL auth | `src/EchoHub.Server.Irc/IrcCommandHandler.cs` | Lines 136-207 (`HandleAuthenticateAsync`) | +| PASS/NICK/USER | `src/EchoHub.Server.Irc/IrcCommandHandler.cs` | Lines 209-267 | +| Registration completion | `src/EchoHub.Server.Irc/IrcCommandHandler.cs` | Lines 268-315 (`TryCompleteRegistrationAsync`) | +| Cleanup on disconnect | `src/EchoHub.Server.Irc/IrcGatewayService.cs` | Lines 136-153 | + +--- + +## User Disconnect & Presence + +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. + +```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->>Entry: Disconnect / TCP close + Entry->>CS: UserDisconnectedAsync(connectionId) + CS->>PT: Get username + channels (before removal) + CS->>PT: UserDisconnected(connectionId) + PT->>PT: Remove connection from tracking + + alt No remaining connections for user + CS->>DB: Update user: Status=Invisible, LastSeenAt=now + loop For each channel user was in + CS->>CS: BroadcastToAllAsync(SendUserStatusChangedAsync) + CS->>SRB: Notify channel members + end + end +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| SignalR disconnect | `src/EchoHub.Server/Hubs/ChatHub.cs` | Lines 45-57 | +| IRC cleanup | `src/EchoHub.Server.Irc/IrcGatewayService.cs` | Lines 136-153 | +| ChatService disconnect | `src/EchoHub.Server/Services/ChatService.cs` | Lines 59-94 | +| Presence disconnect | `src/EchoHub.Server/Services/PresenceTracker.cs` | Lines 31-53 | diff --git a/docs/flows/flows.md b/docs/flows/flows.md new file mode 100644 index 0000000..64f7828 --- /dev/null +++ b/docs/flows/flows.md @@ -0,0 +1,3 @@ +# Flows + +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. diff --git a/docs/flows/media.md b/docs/flows/media.md new file mode 100644 index 0000000..f76d27b --- /dev/null +++ b/docs/flows/media.md @@ -0,0 +1,118 @@ +# Media & Services + +## File Upload + +Files are uploaded via REST, validated by magic bytes, stored with GUID filenames, +and broadcast as a message with a download link. + +```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->>CC: POST /api/channels/{channel}/upload (multipart) + CC->>CC: Check file size limits + CC->>FV: IsValidImage(stream) — magic byte check + FV->>FV: Read header: JPEG(FFD8FF) / PNG(89504E47) / GIF / WebP(RIFF+WEBP) + FV-->>CC: true/false + CC->>FS: SaveFileAsync(stream, extension) + FS->>FS: Generate GUID filename, write to uploads/ + FS-->>CC: fileId (GUID) + CC->>CC: Determine MessageType (Image/Audio/File) + CC->>CS: SendMessageAsync (with file URL + optional ASCII art) + CS->>DB: INSERT Message + CS->>CS: BroadcastToAllAsync → fan out to clients +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Upload endpoint | `src/EchoHub.Server/Controllers/ChannelsController.cs` | Lines 108-200 | +| File validation | `src/EchoHub.Server/Services/FileValidationHelper.cs` | Lines 15-82 | +| File storage | `src/EchoHub.Server/Services/FileStorageService.cs` | Lines 1-47 | +| File download | `src/EchoHub.Server/Controllers/FilesController.cs` | Lines 22-53 | + +--- + +## Link Embed Resolution + +When a message contains URLs, the server fetches OpenGraph metadata for preview +embeds. + +```mermaid +sequenceDiagram + participant CS as ChatService + participant LE as LinkEmbedService + participant Web as External Website + + CS->>LE: TryGetEmbedsAsync(messageContent) + LE->>LE: Extract URLs via regex + LE->>LE: Filter: max URLs per message, skip duplicates + + loop For each URL + LE->>LE: Validate: http(s) only, block private IPs + LE->>Web: GET URL (timeout + size limit) + Web-->>LE: HTML response + LE->>LE: Parse og:title, og:description, og:site_name + LE->>LE: Parse theme-color meta tag + LE->>LE: Fallback to if no og:title + end + + LE-->>CS: List<EmbedDto> (or null) + Note over CS: Attached to MessageDto before broadcast +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Entry point | `src/EchoHub.Server/Services/LinkEmbedService.cs` | Lines 28-51 (`TryGetEmbedsAsync`) | +| URL extraction | `src/EchoHub.Server/Services/LinkEmbedService.cs` | Lines 145-160 | +| Private IP blocking | `src/EchoHub.Server/Services/LinkEmbedService.cs` | Lines 162-181 | +| OG tag parsing | `src/EchoHub.Server/Services/LinkEmbedService.cs` | Lines 187-210 | +| Theme color parsing | `src/EchoHub.Server/Services/LinkEmbedService.cs` | Lines 117-143 | +| ChatService integration | `src/EchoHub.Server/Services/ChatService.cs` | Lines 194-201 | + +--- + +## Server Directory Registration + +Public servers register with the EchoHubSpace directory for discoverability. + +```mermaid +sequenceDiagram + participant SDS as ServerDirectoryService + participant Dir as EchoHubSpace Directory + participant PT as PresenceTracker + + SDS->>SDS: Check Server:PublicServer config + SDS->>Dir: SignalR connect (echohub.voidcube.cloud/hubs/servers) + SDS->>Dir: RegisterServer(name, description, host, userCount) + Dir-->>SDS: Registered + + loop Every 30 seconds + SDS->>PT: Get online user count + alt Count changed + SDS->>Dir: UpdateUserCount(count) + end + end + + Dir->>SDS: Ping + SDS->>Dir: Heartbeat + + Note over SDS,Dir: Exponential backoff on disconnect (2s → 30s max) +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Service lifecycle | `src/EchoHub.Server/Services/ServerDirectoryService.cs` | Lines 29-122 | +| Registration | `src/EchoHub.Server/Services/ServerDirectoryService.cs` | Lines 195-212 | +| User count polling | `src/EchoHub.Server/Services/ServerDirectoryService.cs` | Lines 154-187 | +| Reconnection backoff | `src/EchoHub.Server/Services/ServerDirectoryService.cs` | Lines 77-82, 191 | diff --git a/docs/flows/messaging.md b/docs/flows/messaging.md new file mode 100644 index 0000000..1811e5d --- /dev/null +++ b/docs/flows/messaging.md @@ -0,0 +1,210 @@ +# Messaging + +## Sending a Message (SignalR) + +A message typed in the TUI travels through encryption, the SignalR hub, +`ChatService` validation, database storage, and fan-out to both SignalR and IRC +clients. + +```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->>AO: OnMessageSubmitted(channel, text) + AO->>AO: IsCommand(text)? → No + AO->>EHC: SendMessageAsync(channel, text) + EHC->>EHC: Encrypt(text) → ciphertext + EHC->>Hub: InvokeAsync("SendMessage", channel, ciphertext) + Hub->>CS: SendMessageAsync(userId, username, channel, ciphertext) + CS->>CS: Decrypt(ciphertext) → plaintext + CS->>CS: Validate (length, newlines, channel exists) + CS->>DB: Check mute status + CS->>LE: TryGetEmbedsAsync(plaintext) + LE->>LE: Extract URLs, fetch OG tags + LE-->>CS: List<EmbedDto> (or null) + CS->>DB: INSERT Message (encrypted at rest) + CS->>CS: Re-encrypt plaintext for transport + CS->>CS: Build MessageDto with embeds + + par Fan-out to all broadcasters + CS->>SRB: SendMessageToChannelAsync(channel, dto) + SRB->>Clients: HubContext.Group(channel).ReceiveMessage(dto) + and + CS->>IRCB: SendMessageToChannelAsync(channel, dto) + IRCB->>IRCB: Decrypt → format as PRIVMSG lines + IRCB->>Clients: Send to each IRC conn (skip sender) + end +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Input handler | `src/EchoHub.Client/UI/MainWindow.cs` | Lines 428-449 (`OnInputKeyDown`) | +| Orchestrator dispatch | `src/EchoHub.Client/AppOrchestrator.cs` | Lines 631-661 (`HandleMessageSubmitted`) | +| Client encrypt + send | `src/EchoHub.Client/Services/EchoHubConnection.cs` | Lines 148-153 (`SendMessageAsync`) | +| Hub receive | `src/EchoHub.Server/Hubs/ChatHub.cs` | Lines 98-111 (`SendMessage`) | +| ChatService process | `src/EchoHub.Server/Services/ChatService.cs` | Lines 145-241 (`SendMessageAsync`) | +| Mute check | `src/EchoHub.Server/Services/ChatService.cs` | Lines 177-190 | +| Link embeds | `src/EchoHub.Server/Services/LinkEmbedService.cs` | Lines 28-51 (`TryGetEmbedsAsync`) | +| DB insert | `src/EchoHub.Server/Services/ChatService.cs` | Lines 208-221 | +| Broadcast fan-out | `src/EchoHub.Server/Services/ChatService.cs` | Lines 311-324 (`BroadcastToAllAsync`) | +| SignalR broadcast | `src/EchoHub.Server/Services/SignalRBroadcaster.cs` | Lines 23-24 | +| IRC broadcast | `src/EchoHub.Server.Irc/IrcBroadcaster.cs` | Lines 17-32 | + +--- + +## Sending a Message (IRC) + +Messages from IRC clients follow the same `ChatService` path but enter as +plaintext (no app-layer encryption). + +```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->>CH: PRIVMSG #channel :Hello world + CH->>CH: Parse target + content + CH->>CH: IrcToEchoHubChannel("#channel") → "channel" + CH->>CS: SendMessageAsync(userId, username, "channel", "Hello world") + CS->>CS: Decrypt("Hello world") → passthrough (no $ENC$ prefix) + CS->>CS: Validate, check mute, fetch embeds + CS->>DB: INSERT Message + CS->>CS: Encrypt plaintext for SignalR transport + + par Fan-out + CS->>SRB: SendMessageToChannelAsync (encrypted for SignalR) + and + CS->>IRCB: SendMessageToChannelAsync (decrypt → PRIVMSG) + IRCB->>IRCB: Skip sender (IRC echo suppression) + end +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| PRIVMSG handler | `src/EchoHub.Server.Irc/IrcCommandHandler.cs` | Lines 437-469 | +| Channel name conversion | `src/EchoHub.Server.Irc/IrcCommandHandler.cs` | Line 680 (`IrcToEchoHubChannel`) | +| ChatService (shared path) | `src/EchoHub.Server/Services/ChatService.cs` | Lines 145-241 | +| IRC echo suppression | `src/EchoHub.Server.Irc/IrcBroadcaster.cs` | Lines 25-26 | + +--- + +## Receiving a Message (TUI Client) + +When a message arrives via SignalR, the client decrypts it, adds it to the chat +view, and optionally plays a notification sound for @mentions. + +```mermaid +sequenceDiagram + participant SRB as SignalRBroadcaster + participant EHC as EchoHubConnection + participant AO as AppOrchestrator + participant MM as ChatMessageManager + participant UI as MainWindow + + SRB->>EHC: ReceiveMessage(MessageDto) + EHC->>EHC: Decrypt(message.Content) + EHC-->>AO: OnMessageReceived(decrypted dto) + AO->>AO: InvokeUI (thread-safe) + AO->>MM: AddMessage(message) + MM->>UI: Render in chat ListView + alt Message contains @username + AO->>AO: PlayAsync() notification sound + end +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| SignalR handler | `src/EchoHub.Client/Services/EchoHubConnection.cs` | Lines 64-71 | +| Orchestrator receive | `src/EchoHub.Client/AppOrchestrator.cs` | Lines 372-383 | +| @mention detection | `src/EchoHub.Client/AppOrchestrator.cs` | Lines 378-382 | + +--- + +## Command Execution + +Slash commands (`/status`, `/nick`, `/kick`, etc.) are parsed client-side and +dispatched to appropriate handlers, which call REST APIs or SignalR methods. + +```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->>AO: OnMessageSubmitted(channel, "/kick baduser") + AO->>CMD: IsCommand("/kick baduser")? → true + AO->>CMD: HandleAsync("/kick baduser") + CMD->>CMD: Parse → command="kick", args="baduser" + + alt API command (kick, ban, mute, nick, etc.) + CMD-->>AO: Fire OnKickRequested("baduser") + AO->>API: KickUserAsync("baduser") + API->>Server: POST /api/moderation/kick/baduser + else Hub command (status, join, leave, etc.) + CMD-->>AO: Fire OnSetStatus / OnJoinChannel / etc. + AO->>EHC: UpdateStatusAsync() / JoinChannelAsync() / etc. + EHC->>Server: SignalR Invoke + else Local command (theme, help, quit) + CMD-->>AO: Fire OnThemeChanged / etc. + AO->>UI: Apply locally (no server call) + end + + AO->>UI: AddSystemMessage(result) +``` + +**Available commands:** + +| Command | Type | Handler | +|---------|------|---------| +| `/status <status> [message]` | Hub | `UpdateStatusAsync` | +| `/nick <name>` | API | `UpdateProfileAsync` | +| `/color <hex>` | API | `UpdateProfileAsync` | +| `/join <channel>` | Hub | `JoinChannelAsync` | +| `/leave` | Hub | `LeaveChannelAsync` | +| `/topic <text>` | API | `UpdateChannelTopicAsync` | +| `/kick <user>` | API | `POST /api/moderation/kick/{user}` | +| `/ban <user>` | API | `POST /api/moderation/ban/{user}` | +| `/unban <user>` | API | `POST /api/moderation/unban/{user}` | +| `/mute <user> [mins]` | API | `POST /api/moderation/mute/{user}` | +| `/unmute <user>` | API | `POST /api/moderation/unmute/{user}` | +| `/role <user> <role>` | API | `PUT /api/moderation/role/{user}` | +| `/nuke` | API | `DELETE /api/channels/{channel}/messages` | +| `/send <file>` | API | `POST /api/channels/{channel}/upload` | +| `/profile [user]` | Local | Show profile dialog | +| `/avatar` | API | `POST /api/users/avatar` | +| `/theme <name>` | Local | `ThemeManager.SetTheme()` | +| `/servers` | API | `GET /api/serverdir/servers` | +| `/users` | Local | Show userlist | +| `/help` | Local | Show help text | +| `/quit` | Local | Exit application | + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Command detection | `src/EchoHub.Client/AppOrchestrator.cs` | Lines 639-655 | +| Command dispatch | `src/EchoHub.Client/Commands/CommandHandler.cs` | Lines 34-69 (`HandleAsync`) | +| Command handlers wired | `src/EchoHub.Client/AppOrchestrator.cs` | Lines 97-117 | +| Individual handlers | `src/EchoHub.Client/AppOrchestrator.cs` | Lines 122-350 | diff --git a/docs/flows/moderation.md b/docs/flows/moderation.md new file mode 100644 index 0000000..692fbd1 --- /dev/null +++ b/docs/flows/moderation.md @@ -0,0 +1,50 @@ +# Moderation + +## Kick / Ban / Mute + +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. + +```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->>MC: POST /api/moderation/kick/{user} + + alt Kick + MC->>CS: Get user's channels + MC->>CS: BroadcastToAllAsync(SendUserKickedAsync) + MC->>CS: ForceDisconnectAndCleanupAsync(user) + else Ban + MC->>DB: Set user.IsBanned = true + MC->>CS: BroadcastToAllAsync(SendUserBannedAsync) + MC->>CS: ForceDisconnectAndCleanupAsync(user) + else Mute + MC->>DB: Set user.IsMuted = true, MutedUntil = now + duration + Note over DB: MuteExpirationService auto-unmutes when timer expires + end + + CS->>PT: ForceRemoveUser(username) + PT->>PT: Remove from all connections + channels + CS->>SRB: ForceDisconnectUserAsync(connectionIds, reason) + CS->>IRCB: ForceDisconnectUserAsync(connectionIds, reason) + CS->>DB: Set Status=Invisible, LastSeenAt=now +``` + +**Code references:** + +| Step | File | Location | +|------|------|----------| +| Kick endpoint | `src/EchoHub.Server/Controllers/ModerationController.cs` | Lines 62-87 | +| Ban endpoint | `src/EchoHub.Server/Controllers/ModerationController.cs` | Lines 89-112 | +| Mute endpoint | `src/EchoHub.Server/Controllers/ModerationController.cs` | Lines 130-151 | +| Force disconnect | `src/EchoHub.Server/Controllers/ModerationController.cs` | Lines 232-256 | +| Mute expiration | `src/EchoHub.Server/Services/MuteExpirationService.cs` | Lines 22-62 | +| Mute enforcement | `src/EchoHub.Server/Services/ChatService.cs` | Lines 177-190 | +| Presence force remove | `src/EchoHub.Server/Services/PresenceTracker.cs` | Lines 161-178 | diff --git a/docs/flows/toc.yml b/docs/flows/toc.yml new file mode 100644 index 0000000..3b56a17 --- /dev/null +++ b/docs/flows/toc.yml @@ -0,0 +1,12 @@ +- name: Authentication + href: authentication.md +- name: Connection + href: connection.md +- name: Messaging + href: messaging.md +- name: Channels + href: channels.md +- name: Moderation + href: moderation.md +- name: Media & Services + href: media.md diff --git a/docs/toc.yml b/docs/toc.yml index c93ba17..7682921 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,6 +1,9 @@ - name: Articles href: articles/ homepage: articles/getting-started.md +- name: Flows + href: flows/ + homepage: flows/flows.md - name: Changelog href: changelog/ homepage: changelog/index.md From a0d9d86956c2b53a51de32da196f09ddf34a4ca3 Mon Sep 17 00:00:00 2001 From: HueByte <ihuebyte@gmail.com> Date: Tue, 24 Feb 2026 21:00:51 +0100 Subject: [PATCH 2/2] fix: update markdownlint configuration to allow compact table pipe style --- .markdownlint-cli2.jsonc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 1b4e73d..d091c60 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -9,7 +9,9 @@ // Allow inline HTML (docfx uses it) "MD033": false, // Allow bare URLs - "MD034": false + "MD034": false, + // Allow compact table pipe style (flow docs use compact tables) + "MD060": false }, "globs": ["**/*.md"],