Merge branch 'dev' into dev_bugs_attack

This commit is contained in:
Hue
2026-02-24 21:03:23 +01:00
committed by GitHub
10 changed files with 850 additions and 1 deletions
+142
View File
@@ -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 |
+159
View File
@@ -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 |
+150
View File
@@ -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<MessageDto> (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 <base64(\0user\0pass)>
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 <password>
IRC->>CH: NICK <nickname>
IRC->>CH: USER <username> 0 * :<realname>
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 |
+3
View File
@@ -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.
+118
View File
@@ -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 <title> 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 |
+210
View File
@@ -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 |
+50
View File
@@ -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 |
+12
View File
@@ -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
+3
View File
@@ -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