Files
EchoHub/docs/flows/messaging.md
T

5.8 KiB

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.

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

Sending a Message (IRC)

Messages from IRC clients follow the same ChatService path but enter as plaintext (no app-layer encryption).

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

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.

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

Command Execution

Slash commands (/status, /nick, /kick, etc.) are parsed client-side and dispatched to appropriate handlers, which call REST APIs or SignalR methods.

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