Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ Marchat is a self-hosted, terminal-based chat application built in Go with a cli
The client is a standalone terminal user interface built with the Bubble Tea framework. It's a complete application that can be built and run independently. The code is split across several files:

- **`main.go`**: Core model, state, Update loop, and command handlers
- **`chrome.go`**: Status footer builder (`buildStatusFooter`), debounced tail `read_receipt` sends, and `stripKindForBanner` plus `BannerStrip` styles so `[ERROR]` / `[WARN]` / other banner lines use distinct strip colors (built-in themes and optional `banner_*` keys in custom `themes.json`)
- **`cli_output.go`**: Lipgloss helpers for pre-TUI stdout (connection, E2E status, profile flow messages)
- **`hotkeys.go`**: Key binding definitions and methods
- **`render.go`**: Message rendering and UI display logic (optional per-line metadata: message id and encrypted flag)
- **`render.go`**: Message rendering and UI display logic (optional per-line metadata: message id and encrypted flag); `System` transcript lines use `systemLineStyle` so errors and warnings are not forced into the same color as timestamps
- **`websocket.go`**: WebSocket connection management, send/receive, and E2E encryption helpers
- **`exthook/`**: Optional, experimental subprocess hooks for local automation (stdin JSON per event); see **CLIENT_HOOKS.md**
- **`commands.go`**: Help text generation and command-related utilities
Expand Down Expand Up @@ -69,7 +70,10 @@ The client is a standalone terminal user interface built with the Bubble Tea fra
- URL detection and external opening
- Tab completion for @mentions
- Connection status indicator
- Unread message count
- Unread count in the footer: increments only for other users' new `text`, `dm`, or `file` lines while the transcript viewport is scrolled up (not for typing, reactions, read receipts, edits, deletes, or your own echoed sends)
- Optional debounced `read_receipt` to the server when the viewport follows the newest messages; failures surface in the banner only
- Footer shows `E2E` when encryption is on, and `#channel` when the current room is not `general`; plaintext sessions omit an explicit `Unencrypted` label in the footer
- Automatic WebSocket reconnect with exponential backoff (capped); on each successful connect (`wsConnected`), the reference client clears the in-memory transcript and related UI state before processing server history replay, so a server restart or network drop does not duplicate messages that were already on screen
- Multi-line input via Alt+Enter / Ctrl+J
- **Diagnostics**: `-doctor` and `-doctor-json` for environment, paths, and config checks (`internal/doctor`)

Expand All @@ -80,7 +84,7 @@ The server is a standalone HTTP/WebSocket server application that provides real-
#### Core Structures

- **`Hub`**: Central message routing system managing client connections, message broadcasting, channel management, and user state; tracks reserved usernames so handshake cannot double-book the same name before a client is registered. All sends to `client.send` use non-blocking `select/default` to prevent deadlocks when a client's write buffer is full; stalled clients are dropped or the message is logged and skipped. **Text** messages fan out to plugins in a **separate goroutine** so plugin IPC never blocks the hub’s broadcast loop.
- **`Client`**: Individual WebSocket connection handler with read/write pumps and command processing. The `writePump` goroutine is started **before** history replay on connect so the send channel always has a consumer.
- **`Client`**: Individual WebSocket connection handler with read/write pumps and command processing. The `writePump` goroutine is started **before** history replay on connect so the send channel always has a consumer. `handleCommand` sends a `System` `text` reply to admins when a `:` command is not handled by plugins or built-ins (unknown token), so clients always get a wire response for that case.
- **`AdminPanel`**: Terminal-based administrative interface for server management
- **`WebAdminServer`**: Web-based administrative interface with session authentication
- **`HealthChecker`**: System health monitoring with metrics collection
Expand All @@ -100,7 +104,7 @@ The server is a standalone HTTP/WebSocket server application that provides real-
- System metrics collection and health monitoring
- Web-based admin panel with CSRF protection
- Health check endpoints for monitoring systems
- WebSocket message rate limiting
- WebSocket per-connection message rate limiting; when the configured burst is exceeded the server sends one `System` `text` notice to that client, then ignores inbound JSON until cooldown (see **PROTOCOL.md**)
- **Diagnostics**: `-doctor` and `-doctor-json` without binding ports (`internal/doctor`)

### Server Library (`server/`)
Expand Down
7 changes: 4 additions & 3 deletions PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ These values of `type` extend the core chat protocol:
| `dm` | Direct message. Requires `recipient` (target username). Delivered only to sender and recipient. |
| `search` | Full-text search. `content` is the search query; the server replies with a private `text` message from `System` listing up to 20 matches (not broadcast). |
| `pin` | Toggle pinned state for `message_id`. **Admin only**; non-admins receive an error `text` from `System`. On success, a `System` `text` notice is broadcast. |
| `read_receipt` | Read-receipt notification. Relayed to connected clients; payload conventions (e.g. which message was read) may use `content` and/or `message_id` as agreed by clients. |
| `read_receipt` | Read receipt. Clients may send `read_receipt` with `message_id` set to the latest persisted chat line they have read while the transcript viewport is scrolled to the tail (the reference client debounces bursts). The server sets `sender` from the connection, persists when `message_id` is positive (`read_receipts` table), and broadcasts the message. Receivers may ignore or surface receipts in UI; the reference TUI does not print them in the transcript. |
| `join_channel` | Join a channel. Requires `channel` (name). If the client was in another channel, they leave it first. The server sends a confirmation `text` from `System`. |
| `leave_channel` | Leave the current channel and return to `#general`. No `content` required. If already in `general`, no-op. The server sends a confirmation `text` from `System`. |

Expand Down Expand Up @@ -168,7 +168,7 @@ The server stores and relays opaque `content` (and encrypted file blobs) without
## Server Behavior

- On successful handshake:
- Sends up to 50 recent messages from history (newest first; clients typically display in chronological order).
- Sends up to 50 recent messages from history (newest first; clients typically display in chronological order). The same replay happens on every new WebSocket session after a disconnect. Clients that keep a local transcript in memory should **replace** or **dedupe** against that replay instead of appending it blindly, or they will show duplicate lines. The reference TUI clears its transcript (messages, reactions, typing state, and cached received-file metadata) when a connection succeeds so the replay is the single source of truth for the visible scrollback window.
- Sends current user list.
- On user connect/disconnect:
- Broadcasts updated user list.
Expand All @@ -178,6 +178,7 @@ The server stores and relays opaque `content` (and encrypted file blobs) without
- Reactions, read receipts, and last channel per user may be persisted server-side and replayed to reconnecting clients.
- Message history is capped at 1000 messages.
- On successful `type`: `edit`, the server updates `content`, sets `edited`, and sets stored encryption metadata from the incoming `encrypted` field (so edited ciphertext rows remain ciphertext with `is_encrypted` aligned to the wire).
- `:`-prefixed input and `admin_command` messages go through the server command path (plugins first, then built-in admin commands for admins). Non-admins may receive a `System` `text` line when a command requires admin privileges. Admins receive a `System` `text` line when no plugin handler and no built-in handler matches the first token (content includes `Unknown command:` and that token).

---

Expand All @@ -197,7 +198,7 @@ Per WebSocket connection, the server enforces rate limiting on **all** incoming
- **Burst:** at most **20 messages** per **5 second** sliding window.
- **Cooldown:** if the limit is exceeded, further incoming messages from that connection are ignored until **10 seconds** have elapsed since the violation, then counting resumes.

Exceeded messages are dropped silently from the client’s perspective (the server logs the event). Alternative clients should pace high-frequency traffic accordingly.
When the burst threshold is crossed, the server sends one `System` `text` notice to that connection, then ignores further incoming JSON until the cooldown elapses (the server still logs drops). Messages received during the cooldown window are dropped without an extra notice. Alternative clients should pace high-frequency traffic accordingly.

---

Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Screen recordings of a current build (GIF autoplay depends on the viewer).
- **Direct Messages** - Private DM conversations between users
- **Channels** - Multiple chat rooms with join/leave and per-channel messaging
- **Typing Indicators** - See when other users are typing
- **Read Receipts** - Message read acknowledgement (broadcast-level)
- **Read Receipts** - Server stores per-user read rows; the reference client sends debounced `read_receipt` when the transcript is scrolled to the newest messages
- **Plugin System** - Remote registry with text commands and Alt+key hotkeys
- **E2E Encryption** - ChaCha20-Poly1305 with a shared global key (`MARCHAT_GLOBAL_E2E_KEY`), including file transfers
- **File Sharing** - Send files up to 1MB (configurable) with interactive picker and optional E2E encryption
Expand All @@ -87,7 +87,7 @@ Screen recordings of a current build (GIF autoplay depends on the viewer).
- **Docker Support** - Containerized deployment with `docker-compose.yml` for local dev; optional **TLS reverse proxy** via Caddy ([guide](deploy/CADDY-REVERSE-PROXY.md))
- **Health Monitoring** - `/health` and `/health/simple` endpoints with system metrics
- **Structured Logging** - JSON logs with component separation and user tracking
- **UX Enhancements** - Connection status indicator, tab completion for @mentions, unread message count, multi-line input, chat export
- **UX Enhancements** - Stable status footer (connection, unread for others' new chat lines when scrolled above the tail, optional E2E and channel), banner for command feedback, tab completion for @mentions, multi-line input, chat export
- **Cross-Platform** - Runs on Linux, macOS, Windows, and Android/Termux
- **Diagnostics** - `marchat-client -doctor` and `marchat-server -doctor` (or `-doctor-json`) summarize environment, resolved paths, and configuration health

Expand Down Expand Up @@ -765,6 +765,9 @@ The TUI client can spawn **optional** external programs on send/receive and pass
| MySQL connection fails | Verify DSN prefix `mysql:` and body `user:pass@tcp(host:3306)/db?parseTime=true`; test with the `mysql` CLI. |
| SQL syntax error after backend switch | Ensure tables were created by the current server version and restart after changing `MARCHAT_DB_PATH`. |
| Message history looks incomplete | History depends on **channel**, **per-user message state**, and server filters. **Ban/unban** and related flows can reset stored state so scrollback differs from the raw DB. |
| Transcript resets after reconnect | On each successful WebSocket connect the reference client clears local messages and rebuilds from the server handshake replay (up to 50 recent lines). That avoids duplicates when the server comes back while the client stayed open. Lines older than that replay window are not shown again in that session unless you saved them with **`:export`** earlier. Third-party clients should replace or dedupe history on handshake; see **PROTOCOL.md** (Server Behavior). |
| Banner stuck on `[Sending...]` | Current **client** clears sending after each successful WebSocket write for normal chat and for `admin_command`, so dropped or slow server work does not leave the banner stuck. Current **server** sends a `System` line for unknown admin `:` commands (`Unknown command:` plus the token) and one `System` line when per-connection message rate burst is exceeded (see **PROTOCOL.md** Rate Limiting). |
| Footer unread looks wrong | The reference client increments unread only for other users' new `text`, `dm`, or `file` while the transcript is not at the bottom. It does not increment for typing, reactions, read receipts, edits, deletes, or your own echoed sends. See **ARCHITECTURE.md** (client). |
| Ban history gaps not working | Set `MARCHAT_BAN_HISTORY_GAPS=true` (default off). The server creates the **`ban_history`** table when using a database backend that runs marchat migrations. |
| TLS certificate errors | For dev/self-signed certs, pass **`--skip-tls-verify`** on the client (or enable **Skip TLS verify** in the profile / interactive setup). |
| Plugin installation fails | Check registry URL (`MARCHAT_PLUGIN_REGISTRY_URL`), network access, and JSON validity; commercial plugins need a valid license for the **plugin name** (see **PLUGIN_ECOSYSTEM.md**). |
Expand Down
6 changes: 3 additions & 3 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The Marchat test suite provides foundational coverage of the application's core
| `client/config/interactive_ui_test.go` | Client interactive UI components | TUI forms, profile selection, authentication prompts |
| `client/code_snippet_test.go` | Client code snippet functionality | Text editing, selection, clipboard, syntax highlighting |
| `client/file_picker_test.go` | Client file picker functionality | File browsing, selection, size validation, directory navigation |
| `client/main_test.go` | Client main functionality | Message rendering, user lists, URL handling, encryption functions, flag validation |
| `client/main_test.go` | Client main functionality | Message rendering, user lists, URL handling, encryption functions, flag validation, `wsConnected` transcript reset on reconnect, `TestMessageIncrementsUnread` |
| `client/websocket_sanitize_test.go` | WebSocket URL / TLS hints | Sanitization helpers for display and connection hints |
| `client/exthook/exthook_test.go` | Client hook helpers | Executable validation, hook JSON shaping, path rules |
| `internal/doctor/db_checks_test.go` | Doctor DB probes | SQLite connectivity and version checks used by `-doctor` |
Expand All @@ -55,7 +55,7 @@ The Marchat test suite provides foundational coverage of the application's core
| `server/db_ci_smoke_test.go` | CI DB smoke | Postgres/MySQL `InitDB`, `CreateSchema`, core tables (env-gated) |
| `server/message_state_test.go` | Durable reactions | Reaction persistence and replay helpers |
| `server/config_test.go` | Server configuration | Server configuration logic and validation |
| `server/client_test.go` | Server client management | WebSocket client initialization, message handling, admin operations |
| `server/client_test.go` | Server client management | WebSocket client initialization, message handling, admin operations, unknown admin command system reply (`TestHandleCommandUnknownAdminSendsSystemReply`) |
| `server/health_test.go` | Server health monitoring | Health checks, system metrics, HTTP endpoints, concurrent access |
| `plugin/sdk/plugin_test.go` | Plugin SDK | Message types, extended fields (channel, encrypted, message_id, recipient, edited), JSON serialization, omitempty validation, backwards-compat unknown-field handling |
| `plugin/sdk/stdio_test.go` | Plugin SDK stdio | `HandlePluginRequest` / `RunIO` (init, message, command, shutdown), EOF handling |
Expand Down Expand Up @@ -83,7 +83,7 @@ Per-file statement percentages for important paths are listed under [Test Covera
- **Client Config**: Configuration loading/saving, path utilities, keystore migration
- **Client Interactive UI**: TUI forms, profile selection, authentication prompts, navigation, validation
- **Client Code Snippet**: Text editing, selection, clipboard operations, syntax highlighting, state management
- **Client Main**: Message rendering, user lists, URL handling, encryption functions, flag validation
- **Client Main**: Message rendering, user lists, URL handling, encryption functions, flag validation, reconnect transcript handling (`TestWsConnectedClearsTranscript`)
- **Client WebSocket helpers**: URL / TLS hint sanitization (`websocket_sanitize_test.go`)
- **Client Hooks (`client/exthook`)**: Executable path validation and hook-safe JSON for send/receive events (`exthook_test.go`)
- **Client File Picker**: File browsing, directory navigation, file selection, size validation, error handling
Expand Down
6 changes: 6 additions & 0 deletions THEMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ Marchat comes with 4 built-in themes:
| `help_overlay_fg` | Help menu text |
| `help_overlay_border` | Help menu border |
| `help_title` | Help menu title color |
| `banner_error_bg` | Optional. Full-width banner strip background when the line starts with `[ERROR]` |
| `banner_error_fg` | Optional. Text color for the error strip (pair with `banner_error_bg`) |
| `banner_warn_bg` | Optional. Background when the line starts with `[WARN]` |
| `banner_warn_fg` | Optional. Foreground for the warn strip |
| `banner_info_bg` | Optional. Background for all other banner lines (`[OK]`, toggles, sending, and plain text) |
| `banner_info_fg` | Optional. Foreground for the info strip. If `banner_info_bg` and `banner_info_fg` are both omitted, the info strip reuses `footer_bg` and `footer_fg` |

## Using Custom Themes

Expand Down
Loading
Loading