diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ef57f30 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,202 @@ +# Changelog + +All notable changes to this project are documented here. +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Added — Phase 2 guild member list + identity export/import + channel rate limit (fourth batch) + +#### Phase 2 guild member list with kick / role-assign UI (Phase E4) +- **New PacketIds:** `GuildMemberListFetch` (0x010E), `GuildMemberList` (0x010F), `GuildRoleAssign` (0x0110), `GuildRoleUnassign` (0x0111), `GuildRoleListFetch` (0x0112), `GuildRoleList` (0x0113). +- **New storage methods:** + - `Storage::list_guild_members(guild_id)` — returns each member's nickname, joined_at, highest-role color + name (via SQL subqueries on `member_roles`/`roles`). + - `Storage::assign_role(guild_id, user_id, role_id)` — idempotent insert into `member_roles`. + - `Storage::remove_role_from_user(guild_id, user_id, role_id)`. + - `Storage::list_guild_roles(guild_id)` — full role rows with permissions + position. +- **New gateway handlers:** `handle_member_list_fetch`, `handle_role_assign`, `handle_role_unassign`, `handle_role_list_fetch`. + - Role assign/unassign require `MANAGE_ROLES` permission (or owner bypass) and append to the audit log. +- **Client wiring:** `NetCommand::{GuildMemberListFetch, GuildRoleAssign, GuildRoleUnassign, GuildRoleListFetch}` + `NetEvent::{GuildMemberList, GuildRoleList}` + dispatch + UI handling. +- **👥 button in guild bar** — opens the guild members modal (disabled when no guild is selected). On open, fetches both member list and role list in parallel. +- **Guild members modal:** + - Member rows with role-colored avatar ring + nickname + role name + truncated user ID. + - 👑 crown indicator for the guild owner. + - 👢 kick button (hidden for owner) — calls `GuildMemberKick` and auto-refreshes the list after 200 ms. + - "+ role" combo — lists all guild roles; clicking a role name calls `GuildRoleAssign`. + - ↻ refresh button. + - "N members" header with close button. + +#### Phase 2 server-side channel rate limit (Phase E6) +- `handle_channel_create` now consumes a token from the per-session rate limiter (same bucket as chat/DM messages). + - When exceeded, replies `ErrorPayload{ RateLimited }` with message "slow down — too many channel operations". + - Bumps `rate_limited_events_total` counter. +- Default channels ("general", "voice") remain protected from deletion (already in place from previous batch). + +#### Phase 2 identity export / import to keyfile (Phase E7) +- **`identity::export_keyfile(identity, passphrase)`** — serializes the identity to JSON, seals it with the same Argon2id + ChaCha20-Poly1305 scheme as the on-disk vault, returns the keyfile JSON string. Passphrase is optional (empty = plain JSON). +- **`identity::import_keyfile(keyfile_json, passphrase)`** — opens a keyfile and returns the deserialized identity (without persisting — caller calls `save()` separately). +- **Export keyfile modal** in Settings → Identity: + - Optional passphrase + confirm field (with mismatch check). + - "Generate keyfile" button produces the JSON output. + - Output shown in a scrollable multiline text box. + - "⧉ Copy" button to clipboard. + - "Save to disk" button writes to `~/Documents/vnox--.vnoxkey`. +- **Import keyfile modal:** + - Multiline paste area for the keyfile JSON. + - Optional passphrase field. + - "Import & Replace Identity" button — calls `import_keyfile` + `save(identity, None)`. + - Warning that the current identity will be replaced; restart required. +- New `UiState` fields: `export_keyfile_open`, `export_keyfile_pass`, `export_keyfile_confirm`, `export_keyfile_output`, `export_keyfile_error`, `import_keyfile_open`, `import_keyfile_input`, `import_keyfile_pass`, `import_keyfile_error`. + +### Build verification + +- `cargo clippy --workspace` — 0 warnings. +- `cargo test --workspace` — 41/41 tests pass. + +### Added — Phase 2 server-side channels + identity vault UI + audit log (third batch, kept for history) + +#### Phase 2 server-side channel management +- **`ChannelCreate` packet (0x0033)** — register a new channel in the gateway's channel store. + - Validates `kind` ("text" or "voice"); rejects empty `channel_id`. + - Returns `ChannelState` to the creator and broadcasts `ChannelCreate` to all other sessions. + - Returns `ErrorPayload{ ChannelNotFound }` (reused code) if a channel with the same id exists. +- **`ChannelDelete` packet (0x0034)** — remove a channel from the store. + - Protects default channels ("general", "voice") with `PermissionDenied`. + - Broadcasts `ChannelDelete` to all sessions; client removes it from sidebar and clears cached messages. +- **`ChannelList` packet (0x0035)** — fetch all known channels. Client replaces its local list with the server-authoritative one (preserving member lists for channels already joined). +- New `channels::create` / `channels::delete` / `channels::list` ops on the gateway. +- Client wiring: `NetCommand::{ChannelCreate, ChannelDelete, ChannelList}` + `NetEvent::{ChannelCreated, ChannelDeleted, ChannelListEvent}` + dispatch + UI handling. +- **Channel create popup** now sends `ChannelCreate` to the gateway instead of adding locally — sidebar updates come back through the broadcast. +- **Channel delete context menu** — right-click any non-default channel in the sidebar to delete it. + +#### Phase 2 identity vault UI +- **Set-passphrase modal** in Settings → Identity: + - Two passphrase fields with match check + min-8-char warning. + - Calls `identity::save(identity, Some(passphrase))` to encrypt the keypair at rest with Argon2id + ChaCha20-Poly1305. + - Shows error messages on save failure. +- **Remove-passphrase modal** — type `REMOVE` to confirm; calls `identity::save(identity, None)` to revert to plain JSON. +- **Vault status block** — shows current encryption state (🔒 encrypted / 🔓 plain) with explanatory text. +- **Copy pubkey button** — copies the hex pubkey to clipboard via `egui::Context::copy_text`. +- New `UiState` fields: `vault_set_open`, `vault_remove_open`, `vault_passphrase_input`, `vault_passphrase_confirm`, `vault_error`. + +#### Phase 2 audit log viewer +- **`GuildAuditLogFetch` packet (0x010C)** — admin-only endpoint; requires `MANAGE_GUILD` permission (or owner). + - Returns last 50 audit log entries (clamped 1..=200) via `GuildAuditLogPayload`. +- New storage method `Storage::get_audit_log(guild_id, limit)` — newest first. +- Client wiring: `NetCommand::GuildAuditLogFetch`, `NetEvent::GuildAuditLog`, dispatch, `UiState.audit_log_entries`. +- **📋 button in guild bar** — opens the audit log modal for the active guild (disabled when no guild is selected). +- **Audit log modal** — scrollable list of entries with: + - Action-specific icons (🏗 create, 🗑 delete, 👢 kick, 🏷 role, 🔗 invite, ✓ accept). + - Actor ID (truncated to 8 hex chars), target ID, reason (italic). + - Relative timestamp ("2m ago", "3h ago", "5d ago"). + - Empty state with explanation. + +### Fixed + +- Renumbered `PID_FRIEND_EVENT` from 0x0155 to 0x0158 to avoid collision with server `BlockUser=0x0155` (already done in previous batch, re-confirmed here). + +### Build verification (third batch) + +- `cargo clippy --workspace` — 0 warnings. +- `cargo test --workspace` — 41/41 tests pass. + +### Added — Phase 2 hardening + Phase 1.3 polish (second batch, kept for history) + +#### Phase 2 server hardening +- **Admin HTTP server (axum) on gateway.** Default bind `0.0.0.0:7601`, configurable via `[gateway] admin_bind`. + - `GET /health` — JSON `{status, uptime_seconds, node, address, private_mode}` for orchestrator liveness probes. + - `GET /version` — `{name, version, lnex_version}`. + - `GET /metrics` — Prometheus text exposition format v0.0.4 with counters/gauges for messages, DMs, voice packets, connections, auth failures, rate-limited events, errors, guilds, friends requests, sessions, channels, uptime. +- **Per-session token-bucket rate limiting** on chat and DM messages. + - Configurable via `[gateway] message_rate_per_sec` (default 5) and `message_rate_burst` (default 10). + - When a session exceeds the limit, server replies `ErrorPayload{ code: RateLimited }` and bumps `rate_limited_events_total` counter. + - Idle buckets are pruned on session disconnect via `RateLimiter::remove()`. +- **Prometheus metrics module** (`gateway/src/admin/metrics.rs`) with lock-free atomic counters shared across all session tasks. +- **`ensure_column` idempotent migration helper** — adds new SQLite columns on existing databases without dropping data. Used to add `messages.reply_to` for the reply feature. + +#### Phase 2 identity security +- **Encrypted identity vault** (`client/src/identity_vault.rs`) — Argon2id passphrase + ChaCha20-Poly1305 AEAD. + - Vault format: `{version, scheme, salt, nonce, ciphertext, plaintext}` (JSON). + - When passphrase is set, identity is encrypted at rest with Argon2id (m=64 MiB, t=3, p=4) for KDF + ChaCha20-Poly1305 AEAD. + - When passphrase is empty, falls back to plain JSON (legacy behavior preserved). + - Backward-compatible: legacy `identity.json` is auto-migrated to `identity.vault.json` on next save. + - `is_vault_encrypted()` for UI to show lock state. + - Unit tests: roundtrip encrypted, wrong passphrase fails, plaintext roundtrip, empty passphrase treated as plain. + +#### Phase 1.3 chat UX +- **Message context menu** (right-click on any message row): + - Quick-reactions row (👍 ❤️ 😂 🎉 👀 🤔) — toggles reaction on click. + - Reply — sets `replying_to` state, preview bar shown above chat input. + - Copy text — copies message content to clipboard via `egui::Context::copy_text`. + - Edit (own messages only) — fills chat input with current content, switches to edit mode. + - Delete (own messages only) — sends MessageDelete to gateway. +- **Reply feature** end-to-end: + - Wire protocol: `ChatMessagePayload.reply_to: Option` (optional message_id). + - DB schema: `messages.reply_to TEXT` column (added via `ensure_column` migration). + - `NetCommand::SendChat { reply_to: Option }` carries the reference through the network layer. + - UI: italic "↳ alice: " preview above the message body when `reply_to` is set. +- **Custom status + activity status UI**: + - Right-click on user-bar status label opens a context menu. + - Status picker: Online / Idle / DND / Invisible (with color dots). + - Custom status text editor (Discord-style "what's on your mind?"). + - Activity type combo (Playing / Listening / Watching / Streaming) + activity text input. + - All changes sync to gateway via `PresenceUpdate` with `activity_type`/`activity_text`/`custom_status` fields. + - User bar displays: voice state → custom status → activity (icon + text) → online/latency. + +#### Phase 1.3 block list (was: placeholder) +- **Block / Unblock commands** wired end-to-end: + - Client: `NetCommand::BlockUser`, `UnblockUser`, `BlockList` + payloads + PID constants (0x0155-0x0157). + - Server handlers (already present) now reachable. + - Client events: `NetEvent::BlockList`, `BlockedUser`, `UnblockedUser` dispatched to `social::handle`. + - `blocked_users: Vec` field on `UiState`. + - Friends panel "Blocked" tab now shows: input row to block by user ID, list of blocked users with "Unblock" button. + - Auto-fetches block list on first open of the Blocked tab. + +#### Phase 1.3 per-user speaking attribution +- **Voice packet protocol extended** — plaintext now includes `[sender_id:32]` (raw Ed25519 pubkey) between channel_id and opus data. + - `voice::build_packet()` accepts `sender_pubkey: &[u8; 32]`. + - `voice::spawn_recv()` parses sender_id and emits hex-encoded `sender_id` in `NetEvent::VoicePacket`. + - Legacy packets (no sender_id) gracefully handled — empty `sender_id` string. + - Unit test `build_packet_header_layout` updated to verify sender_id roundtrip. +- **Per-user speaking indicator in voice panel**: + - `last_remote_speaker_id` field on `UiState`, updated on every received voice packet. + - Voice panel matches `last_remote_speaker_id` against channel members and highlights only the speaking member (green border, name, 🔊 emoji). + - Voice activity banner now shows the speaker's nickname: "🔊 alice is talking" instead of generic "someone is talking". + - Speaker ID decays after 500 ms of silence (driven by `remote_speaking(500)`). + +#### Phase 1.3 channel creation UI +- **"Create a Channel" popup** — accessible from the channel list header via the "+" button. + - Input: channel name (free text). + - Type selector: Text (`#`) or Voice (`🔊`). + - Creates the channel in the local `s.channels` list so it appears in the sidebar immediately. + - Future JoinChannel attempt will register with the gateway (server-side channel-create is Phase 2 backend work). + +### Fixed + +- **NetEvent dispatch bug (from previous commit):** `GuildMemberKicked`, `InviteCreated/Accepted/Deleted`, `RoleCreated/Deleted` were silently dropped — now routed to `social::handle`. +- **`FriendAccepted` handler (from previous commit):** now uses `user_id` and adds the friend to the list. +- **PID collision:** `PID_FRIEND_EVENT` was 0x0155, conflicting with server `BlockUser=0x0155`. Renumbered: `BlockUser=0x0155`, `UnblockUser=0x0156`, `BlockList=0x0157`, `FriendEvent=0x0158`. +- **PresenceUpdate wire format mismatch:** client was sending `{custom_status, activity}` but server expected `{activity_type, activity_text}`. Now the client serializes `activity_type`/`activity_text`/`custom_status` correctly, matching the gateway's `PresenceUpdatePayload`. + +### Documentation + +- `docs/00-status.md` and `docs/06-roadmap.md` updated in previous commit — Phase 1.1/1.2/1.3 marked DONE where implemented. +- `CHANGELOG.md` expanded with all new entries. + +### Build verification (second batch) + +- `cargo clippy --workspace` — 0 warnings. +- `cargo test --workspace` — 41/41 tests pass (22 in client lib incl. new rate_limit + vault tests, 10 in gateway, 9 in voice-node). + +## [0.1.0] - 2025-05-21 + +### Added + +- Initial workspace: gateway, voice-node, client, sdk stub, federation stub. +- LNEx Phase 1 over TCP (JSON payloads): auth, channels, text chat. +- UDP voice relay between clients in the same channel. +- SQLite message history and user records. +- Desktop client shell with egui UI and net layer. + +[Unreleased]: https://github.com/loki5512344/Vnox/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/loki5512344/Vnox/releases/tag/v0.1.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0293f64 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# VNOX + +Self-hosted realtime voice and chat. Decentralized. Lightweight. Moddable. +Built on LNEx, a custom protocol for low-latency federated communication. + +Not Discord. Not TeamSpeak. Not cloud. + +``` +vnox://server/channel +``` + +## Quick links + +- [Architecture](docs/01-architecture.md) +- [Protocol](docs/02-protocol/README.md) +- [Current status and limitations](docs/00-status.md) +- [Server setup](docs/03-server/deployment.md) +- [Local dev config](dev/README.md) +- [Contributing](docs/community/contributing.md) +- [Changelog](CHANGELOG.md) + +## Status + +Phase 1: implemented, not production ready. + +See [docs/00-status.md](docs/00-status.md) for an honest list of what works, +what is only specified on paper, and known gaps. + +| Component | Status | +|----------------|--------| +| Gateway | TCP listener, LNEx handshake, channels, chat, SQLite | +| Voice node | UDP relay, voice packet routing | +| Desktop client | egui UI, net layer, audio pipeline (partial) | +| LNEx protocol | Specified and implemented (JSON in Phase 1) | +| Federation | Planned (Phase 3) | +| Mobile client | Planned (Phase 3) | + +Traffic in v0.1.x is **unencrypted plaintext**. Do not use in production. + +## Running locally + +Requires Rust 1.85+. + +```sh +# terminal 1 - gateway +cargo run -p vnox-gateway -- --config dev/config.toml + +# terminal 2 - voice node +cargo run -p vnox-voice-node -- --config dev/config.toml + +# terminal 3 - client +cargo run -p vnox-client +``` + +The client connects to `127.0.0.1:7600` by default (editable in the UI). +Config details: [dev/README.md](dev/README.md). + +### Opus on Windows + +`audiopus_sys` builds libopus from source via CMake. +CMake 4.x requires a policy flag, already set in `.cargo/config.toml`: + +```toml +[env] +CMAKE_POLICY_VERSION_MINIMUM = "3.5" +``` + +No manual steps needed. + +## License + +GPL-3.0. See [docs/LICENSE.md](docs/LICENSE.md). + +The LNEx protocol specification is CC0 (public domain). diff --git a/docs/05-features/admin-panel.md b/docs/05-features/admin-panel.md new file mode 100644 index 0000000..0a56a7c --- /dev/null +++ b/docs/05-features/admin-panel.md @@ -0,0 +1,405 @@ +# Admin Panel — Design + +> Target: Phase 1.1 (core) → Phase 1.2 (guilds, roles) → Phase 2 (plugins, bot permissions) + +## Separation of concerns + +| Layer | Config file (TOML) | Admin panel (GUI) | +|-------|-------------------|-------------------| +| **Infrastructure** | `bind`, `ports`, `data_dir`, TLS certs | — | +| **Node identity** | `name`, `address` | Display only | +| **Storage** | `backend`, `sqlite_path`, `retention` | — | +| **Security** | federation gate, rate limits, session timeout | Ban list, mute, kick | +| **Runtime state** | — | Channels, roles, members, invites, audit log | +| **Plugins** | Plugin enable/disable | Permission keys, commands | + +**Rule of thumb:** If it needs a server restart → TOML. If it can apply live → GUI. + +--- + +## Permission System + +Designed for both built-in actions and plugin/bot commands (inspired by LuckPerms). + +### Built-in permission keys + +``` +vnox.admin.manage_channels — create, edit, delete channels +vnox.admin.manage_roles — create, edit, delete roles +vnox.admin.manage_members — kick, ban, mute, timeout +vnox.admin.manage_invites — create, delete invites +vnox.admin.manage_guild — edit guild name, icon, settings +vnox.admin.view_audit_log — read audit log + +vnox.channel.read — view channel +vnox.channel.send — send messages +vnox.channel.voice_connect — join voice channel +vnox.channel.voice_speak — speak in voice (push-to-talk) +vnox.channel.voice_mute_members — server-mute others + +vnox.dm.send — send direct messages +vnox.friend.request — send friend requests +vnox.presence.view — see online status +``` + +### Custom permission keys (plugin/bot commands) + +Plugins register their permissions at load time via the RPC API: + +``` +mybot.command.greet — /greet command +mybot.command.play — /play music +mybot.command.ban_override — override sub-command +``` + +**Wildcard support:** +``` +vnox.admin.* — all admin permissions +vnox.channel.* — all channel permissions +mybot.* — all commands from mybot +* — everything (owner only) +``` + +### How it works + +``` +┌──────────────────────────────────────────────────┐ +│ Role Editor — "Moderator" │ +├──────────────────────────────────────────────────┤ +│ Permission keys: │ +│ │ +│ ┌─ Built-in ──────────────────────────────┐ │ +│ │ ☑ vnox.admin.manage_channels │ │ +│ │ ☑ vnox.admin.manage_members │ │ +│ │ ☐ vnox.admin.manage_roles │ │ +│ │ ☑ vnox.admin.view_audit_log │ │ +│ │ ☑ vnox.channel.* │ │ +│ └──────────────────────────────────────────┘ │ +│ │ +│ ┌─ Plugin permissions ────────────────────┐ │ +│ │ ☑ mybot.command.greet │ │ +│ │ ☐ mybot.command.play │ │ +│ │ ☑ mybot.command.* │ │ +│ └──────────────────────────────────────────┘ │ +│ │ +│ ┌─ Custom key ────────────────────────────┐ │ +│ │ [ Type a permission key... ] [ +Add ] │ │ +│ └──────────────────────────────────────────┘ │ +│ │ +│ [Cancel] [Save] │ +└──────────────────────────────────────────────────┘ +``` + +**Resolution order (highest priority wins):** +1. Owner — overrides everything +2. Channel override (user-specific) — allow/deny +3. Channel override (role-specific) — allow/deny +4. Role hierarchy (top role has higher priority) +5. @everyone default +6. Server-wide default + +Each override is `(allow_mask, deny_mask)` — deny always beats allow at the same level. + +### Database + +Already specified in `docs/10-database.md`: + +``` +roles(id, guild_id, name, color, permissions BITMASK, position) +channel_overrides(id, channel_id, target_id, target_type, allow_mask, deny_mask) +member_roles(member_guild_id, member_user_id, role_id) +``` + +**Custom permission keys** are stored separately: + +```sql +CREATE TABLE permission_keys ( + id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + name TEXT NOT NULL, -- e.g. "mybot.command.greet" + description TEXT, -- e.g. "Allow using /greet command" + plugin_id TEXT, -- which plugin registered it, null = built-in + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX idx_perm_keys_guild_name ON permission_keys(guild_id, name); +``` + +Role grants use the same `roles.permissions` bitmask for built-in keys (first 128 bits). +For custom/plugin keys beyond 128, they're stored in a separate table: + +```sql +CREATE TABLE role_perm_grants ( + role_id TEXT NOT NULL, + permission_key TEXT NOT NULL, -- "mybot.command.greet" + granted INTEGER NOT NULL, -- 1 = allow, 0 = deny + PRIMARY KEY (role_id, permission_key), + FOREIGN KEY(role_id) REFERENCES roles(id) +); +``` + +--- + +## Channel Management + +### Create Channel + +``` +Right-click channel list → "Create Channel" + or +Server Settings → Channels → [+ Add] +``` + +**Modal:** + +``` +┌──────────────────────────────────────┐ +│ Create Channel │ +├──────────────────────────────────────┤ +│ │ +│ Name [_________________] │ +│ │ +│ Type ○ Text ● Voice │ +│ │ +│ Category [General ▼] [None] │ +│ │ +│ Topic [_________________] │ +│ │ +│ ┌─ Permissions ▾ ─────────────┐ │ +│ │ @everyone │ │ +│ │ ☑ View Channel │ │ +│ │ ☑ Send Messages │ │ +│ │ ☐ Voice Connect │ │ +│ │ │ │ +│ │ @admin │ │ +│ │ ☑ Everything │ │ +│ └──────────────────────────────┘ │ +│ │ +│ [Cancel] [Create] │ +└──────────────────────────────────────┘ +``` + +### Edit Channel + +Double-click channel or right-click → "Edit Channel". + +Same modal pre-filled, plus: +- **Name change** — updates slug +- **Category change** — moves channel +- **Drag position** — reorder inside category +- **Delete** — with confirmation (soft delete in DB) + +### Drag & Drop + +``` +Channels Server: My Server +───────────────────────────────────────────── +▶ TEXT CHANNELS [+] + ○ general + ○ announcements +▸ VOICE CHANNELS [+] + ○ lobby ← drag handles ≡ + ○ afk ← can drag to reorder inside category + ← can drag to another category +``` + +**Behavior:** +- Drag handle `≡` on hover +- Drop zone highlight between channels and around categories +- Hold `Ctrl` to copy permission overrides when moving to another category +- Position stored in `channels.position` column (integer, step by 10 for gaps) +- Changes broadcast via `CHANNEL_UPDATED` packet to all guild members in real-time + +--- + +## Server Settings UI Layout + +``` +Server Settings: "My Server" +───────────────────────────────────────────── +│ Navigation │ Content Panel │ +│ │ │ +│ Overview │ ┌──────────────┐ │ +│ Channels │ │ │ │ +│ Roles │ │ (dynamic) │ │ +│ Members │ │ │ │ +│ Invites │ └──────────────┘ │ +│ Bans │ │ +│ Audit Log │ │ +│ Plugins │ │ +│ ─────────────────── │ │ +│ Danger Zone │ │ +│ Delete Server │ │ +│ Transfer Owner │ │ +│ │ │ +└───────────────────────────────────────────┘ +``` + +### Overview tab + +``` +┌──────────────────────────────────────────┐ +│ Server Overview │ +│ │ +│ Name [My Server________________] │ +│ Description [_______________________] │ +│ │ +│ Icon [🔵 Upload image] drag & drop │ +│ │ +│ Owner: loki5512344 │ +│ Region: Warsaw (default) │ +│ Created: 2026-06-06 │ +│ │ +│ ████████████████████░░ Manage Channels │ +│ ████████████░░░░░░░░░░ Manage Roles │ +│ │ +│ [Save Changes] │ +└──────────────────────────────────────────┘ +``` + +### Plugins tab + +``` +┌──────────────────────────────────────────┐ +│ Plugins │ +│ │ +│ ┌─ Installed ─────────────────────┐ │ +│ │ 🎵 MusicBot v1.2 [Config] │ │ +│ │ [Disable] [Permissions ▸] │ │ +│ │ │ │ +│ │ 🤖 ModBot v0.8 [Config] │ │ +│ │ [Disable] [Permissions ▸] │ │ +│ └──────────────────────────────────┘ │ +│ │ +│ [Install from URL...] │ +│ │ +│ ┌─ Permission Keys ──────────────────┐ │ +│ │ Search: [___________________] 🔍 │ │ +│ │ │ │ +│ │ Key │ Granted │ │ +│ │ ──────────────────────────────── │ │ +│ │ musicbot.command.play │ admin │ │ +│ │ musicbot.command.skip │ @everyone│ │ +│ │ modbot.warn │ mod │ │ +│ │ modbot.ban │ admin │ │ +│ └──────────────────────────────────────┘ +└──────────────────────────────────────────┘ +``` + +--- + +## LNEx Protocol Packets + +New packets for admin operations: + +``` +PID 0x0070 ADMIN_LIST_CHANNELS client → gateway +PID 0x0071 ADMIN_CREATE_CHANNEL client → gateway +PID 0x0072 ADMIN_UPDATE_CHANNEL client → gateway +PID 0x0073 ADMIN_DELETE_CHANNEL client → gateway +PID 0x0074 CHANNEL_CREATED gateway → broadcast +PID 0x0075 CHANNEL_UPDATED gateway → broadcast +PID 0x0076 CHANNEL_DELETED gateway → broadcast + +PID 0x0080 ADMIN_LIST_ROLES client → gateway +PID 0x0081 ADMIN_CREATE_ROLE client → gateway +PID 0x0082 ADMIN_UPDATE_ROLE client → gateway +PID 0x0083 ADMIN_DELETE_ROLE client → gateway +PID 0x0084 ADMIN_ADD_ROLE_MEMBER client → gateway +PID 0x0085 ADMIN_REMOVE_ROLE_MEMBER client → gateway + +PID 0x0090 ADMIN_KICK_MEMBER client → gateway +PID 0x0091 ADMIN_BAN_MEMBER client → gateway +PID 0x0092 ADMIN_UNBAN_MEMBER client → gateway +PID 0x0093 ADMIN_MUTE_MEMBER client → gateway + +PID 0x00A0 GUILD_UPDATE client → gateway +PID 0x00A1 GUILD_DELETE client → gateway (owner only) +PID 0x00A2 GUILD_TRANSFER client → gateway (owner only) +``` + +**Examples:** + +**ADMIN_CREATE_CHANNEL (0x0071):** +```json +{ + "guild_id": "uuid", + "category_id": null, + "type": "voice", + "name": "lobby", + "topic": "General voice" +} +``` + +**CHANNEL_CREATED broadcast (0x0074):** +```json +{ + "channel": { + "id": "uuid", + "guild_id": "uuid", + "category_id": null, + "type": "voice", + "name": "lobby", + "topic": "General voice", + "position": 3 + } +} +``` + +**ADMIN_UPDATE_ROLE (0x0082):** +```json +{ + "role_id": "uuid", + "name": "Moderator", + "color": 16744960, + "permissions": { + "vnox.admin.manage_channels": true, + "vnox.admin.manage_members": true, + "mybot.command.greet": true, + "mybot.command.warn": true + }, + "hoist": true, + "mentionable": true +} +``` + +**ADMIN_UPDATE_CHANNEL (0x0072) — includes position changes via drag & drop:** +```json +{ + "channel_id": "uuid", + "name": "general", + "category_id": "uuid", + "position": 5, + "topic": "General discussion" +} +``` + +--- + +## Permission checks flow + +``` +Client click "Create Channel" + │ + ▼ +Client sends ADMIN_CREATE_CHANNEL → Gateway + │ + ▼ +Gateway checks: + 1. Is user authenticated? → 403 if no + 2. Is user in guild? → 403 if no + 3. Does user have vnox.admin.manage_channels? + - Resolve role hierarchy + - Check channel_overrides (none — new channel) + - Check wildcards (vnox.admin.*) + - Owner override → 403 if no + │ + ▼ +Gateway: INSERT into channels table +Gateway: INSERT into channel_overrides if specified +Gateway: Broadcast CHANNEL_CREATED to all guild members +Gateway: Log to audit_logs + │ + ▼ +All clients in guild see new channel in channel list +``` diff --git a/docs/05-features/central-hub.md b/docs/05-features/central-hub.md new file mode 100644 index 0000000..cf684c5 --- /dev/null +++ b/docs/05-features/central-hub.md @@ -0,0 +1,65 @@ +# Central Hub — опциональный координационный сервер + +## Мотивация + +VNOX спроектирован как self-hosted платформа: каждый сам поднимает свой нод. +Но для adoption нужна точка входа для тех, у кого нет своего сервера, и механизм +обнаружения публичных нодов. + +Central Hub — опциональный сервер, который **не заменяет** собственный нод +и **не хостит** голос/чат. Он только координирует. + +## Что делает Central Hub + +### 1. Discovery / Registry — реестр публичных нодов + +Владельцы нодов могут опубликовать свой сервер в реестре: +- адрес, название, описание, регион +- теги (`gaming`, `dev`, `ru`, `cs2`, ...) +- мета: онлайне, кол-во пользователей + +Клиент может найти сервер по тегам/названию и подключиться. +Без Central Hub — только прямой ввод адреса или закладки. + +### 2. Global identity bridge — аккаунт без своего нода + +Пользователь регистрирует ключ на Central Hub: +- `user@hub` — глобальный идентификатор +- никнейм, аватар (опционально) +- привязка к одному или нескольким "гостевым" нодам + +Позволяет зайти в VNOX без поднятия собственной инфраструктуры. +При этом Central Hub не хранит сообщения, не релеит голос, не управляет каналами. + +### 3. Push notification relay — поддержка мобильных + +Мобильные клиенты не могут держать постоянное TCP/UDP соединение. +Central Hub принимает пуш-уведомления от нодов: +- входящее DM +- упоминание в чате +- звонок в войс + +Без этого мобильный клиент будет получать сообщения только с задержками. + +### 4. User directory / friend graph — глобальная социальная сеть + +Список друзей и поиск пользователей не привязан к одному ноду: +- `@user` ищется по Central Hub +- запрос в друзья через хаб, а не через конкретный сервер +- cross-node DM: хаб знает, на каком ноде сейчас пользователь + +## Границы (чего Central Hub НЕ делает) + +- ❌ Не хостит голосовые каналы +- ❌ Не хостит текстовые каналы +- ❌ Не хранит историю сообщений +- ❌ Не управляет сессиями — только направляет +- ❌ Не заменяет self-hosted нод — только дополняет + +Hub — это телефонная книга и коммутатор, а не сам телефон. + +## Деплой + +Central Hub — отдельный бинарник (например, `vnox-hub`). +Работает на HTTP(S) / WebSocket, минимальные требования. +Может быть поднят кем угодно, не обязательно авторами VNOX. diff --git a/docs/05-features/custom-emoji-stickers.md b/docs/05-features/custom-emoji-stickers.md new file mode 100644 index 0000000..ff54b0e --- /dev/null +++ b/docs/05-features/custom-emoji-stickers.md @@ -0,0 +1,443 @@ +# Custom Emoji, Stickers & GIF + +> Target phase: 1.3 (emoji picker) / 2 (custom emoji, stickers, GIF) +> Depends on: Phase 1.2 (guild system, permissions) + +--- + +## Overview + +Three related but distinct features: + +| Feature | Size | Format | Scope | Storage | +|---------|------|--------|-------|---------| +| **Custom Emoji** | 128×128 max | PNG, GIF, WebP | Per-guild | Server filesystem | +| **Stickers** | 512×512 max | PNG, GIF, WebP, Lottie† | Per-guild + per-user | Server filesystem | +| **GIF Picker** | External | GIF (Tenor/Giphy API) | Per-server config | Proxy via server | + +† Lottie — future, not Phase 2. + +### Difference from unicode reactions + +Unicode emoji reactions (`message_reactions` table, Phase 1.3) use single-codepoint emoji (👍, 🔥, 😂). +Custom emoji extend this with server-hosted images referenced as `:emoji_name:` in messages. + +--- + +## Storage & Schema + +### Filesystem layout + +``` +data/ +├── guilds/ +│ └── {guild_id}/ +│ ├── emojis/ +│ │ ├── kappa.png +│ │ ├── pogchamp.gif +│ │ └── ... +│ └── stickers/ +│ ├── wave.png +│ └── ... +├── users/ +│ └── {user_id}/ +│ └── stickers/ # personal stickers +│ └── ... +└── emoji_cache/ # client-side cache (per client, not server) +``` + +### Database tables + +#### guild_emojis + +```sql +CREATE TABLE guild_emojis ( + id TEXT PRIMARY KEY, -- UUID + guild_id TEXT NOT NULL, -- FK guilds + name TEXT NOT NULL, -- :name: reference (lowercase, alphanumeric + underscore) + filename TEXT NOT NULL, -- on-disk filename (id.png) + content_type TEXT NOT NULL, -- image/png, image/gif, image/webp + file_size INTEGER NOT NULL, -- bytes + width INTEGER NOT NULL, -- px + height INTEGER NOT NULL, -- px + is_animated BOOLEAN NOT NULL, -- true for GIF + uploaded_by TEXT NOT NULL, -- FK users + created_at TIMESTAMP NOT NULL, + UNIQUE(guild_id, name) +); + +CREATE INDEX idx_guild_emojis_guild ON guild_emojis(guild_id); +``` + +#### guild_stickers + +```sql +CREATE TABLE guild_stickers ( + id TEXT PRIMARY KEY, + guild_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + filename TEXT NOT NULL, + content_type TEXT NOT NULL, + file_size INTEGER NOT NULL, + width INTEGER NOT NULL, + height INTEGER NOT NULL, + uploaded_by TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + UNIQUE(guild_id, name) +); + +CREATE INDEX idx_guild_stickers_guild ON guild_stickers(guild_id); +``` + +#### user_stickers + +```sql +CREATE TABLE user_stickers ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + filename TEXT NOT NULL, + content_type TEXT NOT NULL, + file_size INTEGER NOT NULL, + width INTEGER NOT NULL, + height INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL, + UNIQUE(user_id, name) +); + +CREATE INDEX idx_user_stickers_user ON user_stickers(user_id); +``` + +### Limits (configurable in server config) + +```toml +[limits.emojis] +max_per_guild = 150 # default: 150 static + animated combined +max_static_per_guild = 100 +max_animated_per_guild = 50 +max_file_size_kb = 256 # per emoji +max_dimension = 128 # px, both width and height + +[limits.stickers] +max_per_guild = 50 +max_per_user = 25 +max_file_size_kb = 512 # per sticker +max_dimension = 512 +``` + +--- + +## Protocol + +### New packet types + +``` +0x0070 EMOJI_SYNC server → client, full emoji list for guild +0x0071 EMOJI_ADD client → server, admin upload +0x0072 EMOJI_DELETE client → server, admin delete +0x0073 EMOJI_UPDATE server → client, delta update (single emoji added/removed) + +0x0074 STICKER_SYNC server → client, full sticker list +0x0075 STICKER_SEND client → server, send sticker in channel +0x0076 STICKER_UPLOAD client → server, upload sticker +0x0077 STICKER_DELETE client → server, delete sticker + +0x0078 EMOJI_DATA client → server, request emoji image bytes +0x0079 EMOJI_DATA_RESP server → client, image bytes (lazy-load, not in sync) + +0x0080 GIF_SEARCH client → server, proxy search to Tenor/Giphy +0x0081 GIF_TRENDING client → server, trending GIFs +``` + +### EMOJI_SYNC (0x0070) + +Server sends on guild join. Client caches locally. + +```json +{ + "guild_id": "uuid", + "emojis": [ + { + "id": "uuid", + "name": "kappa", + "content_type": "image/png", + "is_animated": false, + "width": 128, + "height": 128, + "file_size": 4096 + } + ] +} +``` + +Image bytes are NOT included — client lazy-loads via `EMOJI_DATA` + `EMOJI_DATA_RESP`. + +### EMOJI_ADD (0x0071) + +Admin uploads new emoji. Requires `MANAGE_EMOJIS` permission. + +```json +{ + "guild_id": "uuid", + "name": "pogchamp", + "data": "" +} +``` + +Server validates: +- Name: lowercase alphanumeric + underscore, 2-32 chars +- Image: PNG/GIF/WebP, max 256KB, max 128×128 +- Count: under guild limit + +Response: new emoji object (same shape as in EMOJI_SYNC), server broadcasts `EMOJI_UPDATE` to guild. + +### EMOJI_DELETE (0x0072) + +```json +{ + "guild_id": "uuid", + "emoji_id": "uuid" +} +``` + +Server removes file + DB row, broadcasts `EMOJI_UPDATE` with `deleted: true`. + +### EMOJI_DATA / EMOJI_DATA_RESP (0x0078/0x0079) + +Lazy-load pattern — client requests image bytes when first rendering an emoji. + +Request: +```json +{ + "emoji_id": "uuid" +} +``` + +Response: +```json +{ + "emoji_id": "uuid", + "content_type": "image/png", + "data": "" +} +``` + +Client caches image bytes on disk in `emoji_cache/{emoji_id}.{ext}`. + +### STICKER_SEND (0x0075) + +Stickers are sent as a separate message type (not inline). The sticker reference is embedded in a chat message. + +```json +{ + "channel_id": "uuid", + "sticker_id": "uuid", + "sticker_type": "guild" // "guild" | "user" +} +``` + +Gateway creates a message with `content_type: "sticker"` and `content: {sticker_id, sticker_type}`. + +### GIF_SEARCH (0x0080) + +Client requests GIF search via server (server proxies to Tenor/Giphy so API key stays server-side). + +```json +{ + "query": "cat dancing", + "limit": 20, + "offset": 0 +} +``` + +Response: +```json +{ + "results": [ + { + "id": "tenor_12345", + "url": "https://media.tenor.com/...", + "preview_url": "https://media.tenor.com/.../tiny.gif", + "width": 498, + "height": 280, + "title": "Cat Dancing" + } + ] +} +``` + +GIF config in server `config.toml`: + +```toml +[gif] +enabled = true +provider = "tenor" # "tenor" | "giphy" +api_key = "TENOR_API_KEY" +max_results = 50 +``` + +When `enabled = false` the GIF picker is hidden in all clients. + +--- + +## Client Integration + +### Message format with custom emoji + +Messages reference custom emoji as `<:emoji_name:emoji_id>` inline in text content: + +``` +User: check out this <:kappa:abc123> and <:pog:def456> +``` + +Client renders by looking up emoji_id in local cache, falling back to lazy-load via `EMOJI_DATA_RESP`. + +Unicode emoji (`👍`) render as-is via system font. + +### Emoji picker + +New widget: `client/src/ui/chat/emoji_picker.rs` + +``` +┌─────────────────────────────┐ +│ [Search emoji... 🔍] │ +│─────────────────────────────│ +│ Favorites │ Custom │ GIF │ ← tabs +│────────────┴─────────┴──────│ +│ 👍 😂 🔥 💯 🙏 😎 │ +│ 🎉 ✨ 😭 💀 👀 🫡 │ +│ 😈 🐱 💪 🚀 🫶 🔒 │ +│─────────────────────────────│ +│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ +│ │Kappa│ │Pog │ │Hmm │ │Clap│ │ ← custom (rendered as images) +│ └───┘ └───┘ └───┘ └───┘ │ +└─────────────────────────────┘ +``` + +### Sticker panel + +New widget: `client/src/ui/chat/sticker_panel.rs` + +- Grid of sticker thumbnails +- Tabs: Guild stickers / My stickers +- Click to send as sticker message (not inline) +- Send button shows sticker preview before sending + +### GIF picker + +New widget: `client/src/ui/chat/gif_picker.rs` + +- Search bar at top +- Trending grid when empty query +- Results grid with animated previews +- Click to send GIF as embedded image in chat message + +### Local cache + +``` +%APPDATA%/vnox/cache/ +├── emojis/{emoji_id}.{ext} # custom emoji images +├── stickers/{sticker_id}.{ext} # sticker images +└── gifs/{gif_id}.gif # recently used GIFs (LRU, max 50) +``` + +Cache invalidation: client re-requests on version mismatch (server increments `emoji_version` per guild). + +--- + +## Admin UX + +### Permissions + +| Permission | Bit | Description | +| ----------------- | --- | --------------------------- | +| MANAGE_EMOJIS | 12 | Upload/delete guild emojis | +| MANAGE_STICKERS | 13 | Upload/delete guild stickers| + +These fit into the existing u128 permission bitmask (bits 12-13 are available). + +### Emoji management panel + +Accessible from guild settings (admin only): + +``` +┌──────────────────────────────────────────────────┐ +│ Guild Emojis [Upload] │ +│──────────────────────────────────────────────────│ +│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────┐│ +│ │Kappa │ │ Pog │ │ Hmm │ │ Clap │ │ (free) ││ +│ │ 4KB │ │ 12KB │ │ 8KB │ │ 16KB │ │ ││ +│ │ [✕] │ │ [✕] │ │ [✕] │ │ [✕] │ │ ││ +│ └──────┘ └──────┘ └──────┘ └──────┘ └────────┘│ +│──────────────────────────────────────────────────│ +│ 4/150 emojis used [Save order]│ +└──────────────────────────────────────────────────┘ +``` + +Upload flow: +1. Admin clicks Upload → file dialog (PNG/GIF/WebP) +2. Client validates: ≤256KB, ≤128×128, valid format +3. Client sends `EMOJI_ADD` with base64 data +4. Server validates + stores + broadcasts `EMOJI_UPDATE` + +### Sticker management + +Same pattern as emoji management, separate tab in guild settings. + +Personal stickers managed in user settings (no admin required). + +--- + +## Federation Considerations (Phase 3) + +### Emoji visibility across nodes + +When two nodes federate: +1. Each node maintains its own emoji set +2. Guild emoji metadata synced via federation protocol +3. Image bytes fetched on-demand (lazy, same as client) +4. Reference format includes origin node: `<:emoji_name:emoji_id@node>` + +### Cross-node sticker send + +1. Sticker reference sent as `{sticker_id, origin_node}` +2. Receiving node fetches sticker metadata + image from origin node +3. Caches locally for subsequent renders + +### GIF proxy + +GIF search always goes through the user's home node. +The server proxies to Tenor/Giphy — no federation needed for GIF results (they're just URLs). + +--- + +## Migration Path + +### Phase 1.3 — Unicode reactions only +- `message_reactions` table (already designed) +- Unicode emoji picker in client +- No custom emoji, no stickers, no GIF + +### Phase 2 — Custom emoji +- Add `guild_emojis` table +- Add packet types 0x0070–0x0073, 0x0078–0x0079 +- Client emoji picker with custom tab +- Admin upload/delete UI +- No federation support yet + +### Phase 2 — Stickers +- Add `guild_stickers` + `user_stickers` tables +- Add packet types 0x0074–0x0077 +- Sticker panel in client +- New message content_type: "sticker" + +### Phase 2 — GIF picker +- Add Tenor/Giphy config to gateway +- Add packet types 0x0080–0x0081 +- GIF picker widget in client +- Server-side proxy (API key protection) + +### Phase 3 — Federation +- Cross-node emoji/sticker sync +- `@node` suffix in references diff --git a/docs/05-features/direct-messages.md b/docs/05-features/direct-messages.md new file mode 100644 index 0000000..ed7fb0f --- /dev/null +++ b/docs/05-features/direct-messages.md @@ -0,0 +1,131 @@ +# Direct Messages — Phase 1.1 + +## Goal + +Allow users to send private messages to each other outside of channels. +DM conversations appear in a dedicated section of the sidebar (like Discord). + +## Wire protocol + +New packet type in LNEx: + +``` +DM_START → client→gateway: "open DM with user X" +DM_MESSAGE → client→gateway: "send message to DM" + gateway→client: "new message in DM" +DM_HISTORY → client→gateway: "get DM history" + gateway→client: DM message list +``` + +### DM_START payload + +```json +{ + "packet_id": "DM_START", + "target_user_id": "" +} +``` + +Gateway response: + +```json +{ + "packet_id": "DM_START", + "dm_id": "dm__", + "other_user": { + "user_id": "", + "nickname": "alice" + }, + "messages": [] +} +``` + +### DM_MESSAGE payload + +```json +{ + "packet_id": "DM_MESSAGE", + "dm_id": "dm__", + "sender_id": "", + "content": "hello!", + "timestamp": 1715000000 +} +``` + +## DM ID format + +`dm__` where user1 < user2 lexicographically. +This ensures the same DM has the same ID on both sides. + +## Database schema + +```sql +CREATE TABLE direct_messages ( + id TEXT PRIMARY KEY, -- "dm__" + user1_id TEXT NOT NULL, -- lexicographically smaller + user2_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + last_message_at INTEGER, + UNIQUE(user1_id, user2_id) +); + +CREATE TABLE dm_messages ( + id TEXT PRIMARY KEY, + dm_id TEXT NOT NULL REFERENCES direct_messages(id), + sender_id TEXT NOT NULL, + body TEXT NOT NULL, + created_at INTEGER NOT NULL +); + +CREATE INDEX idx_dm_messages_dm_id ON dm_messages(dm_id, created_at); +CREATE INDEX idx_dm_participant ON direct_messages(user1_id, user2_id); +``` + +## Gateway handler + +`gateway/src/handler/direct_message.rs` (new file): + +1. `handle_dm_start(session, target_user)`: + - Find or create DM record (lexicographic user ID ordering) + - Return DM history + +2. `handle_dm_send(session, dm_id, body)`: + - Validate session is participant of this DM + - Save to `dm_messages` table + - If recipient is connected → forward `DM_MESSAGE` to their session + - If recipient is offline → stored for delivery on reconnect + +3. `handle_dm_history(session, dm_id)`: + - Return last N messages (N = 50 default) + +## Client UI + +- New section in sidebar: "Direct Messages" with user list +- Click a user → opens DM chat panel (same layout as channel chat) +- Button on user context menu: "Message" +- Unread count badge on DM entries +- New messages trigger notification dot + +## Client state + +Add to `client/src/ui/state/types.rs`: + +```rust +pub struct DmConversation { + pub dm_id: String, + pub other_user_id: String, + pub other_nickname: String, + pub messages: Vec, + pub unread_count: u32, +} + +// Add to UiState: +pub dms: Vec, +pub active_dm: Option, +``` + +## Open questions + +- Should DMs support group conversations (3+ users)? → Phase 2 +- Should DMs be encrypted (E2EE)? → Phase 2 +- Should users be able to block DMs from specific users? → Phase 2 diff --git a/docs/05-features/encryption.md b/docs/05-features/encryption.md new file mode 100644 index 0000000..f9175b3 --- /dev/null +++ b/docs/05-features/encryption.md @@ -0,0 +1,78 @@ +# Encryption — Phase 1.1 + +> **Phase 1.1 status:** Fully implemented. ChaCha20-Poly1305 AEAD with X25519 ECDH + HKDF-SHA256 key exchange. Both TCP (control) and UDP (voice) paths are now encrypted. Forward secrecy via ephemeral session keys. + +## Goal + +Replace all plaintext traffic (TCP + UDP) with ChaCha20-Poly1305 AEAD encryption. +X25519 ECDH ephemeral key exchange during handshake for forward secrecy. + +✅ **COMPLETED in Phase 1.1** + +## Key exchange (during HELLO handshake) + +1. Server generates ephemeral X25519 keypair per session +2. Server sends public key in `HELLO` packet +3. Client generates own ephemeral X25519 keypair +4. Both compute shared secret via X25519 ECDH +5. Shared secret → HKDF-SHA256 → two keys: + - `client→server` encryption key + - `server→client` encryption key +6. Ephemeral keys discarded after session (forward secrecy) + +## Packet encryption + +- Algorithm: **ChaCha20-Poly1305** (AEAD) +- 96-bit nonce: `session_id || packet_sequence` +- 16-byte Poly1305 authentication tag per packet +- Applied to ALL packets on both TCP and UDP + +## Why ChaCha20-Poly1305 over AES-GCM + +- Constant-time on all platforms (no hardware AES requirement) +- Faster in software on ARM (common for mobile — Phase 3) +- Simpler nonce management (no IV collision risk) + +## Implementation plan + +### ✅ Client (DONE) +- `client/src/net/crypto.rs` — SessionCrypto with ChaCha20-Poly1305 +- `client/src/net/voice.rs` — `build_packet()` encrypts voice payloads with c2s_key +- `client/src/net/voice.rs` — `spawn_recv()` decrypts incoming voice with s2c_key +- `client/src/net/session/session_loop.rs` — voice_seq counter for encryption nonces + +### ✅ Gateway (DONE) +- `gateway/src/proto/crypto.rs` — X25519 ECDH, HKDF-SHA256, ChaCha20-Poly1305 +- `gateway/src/net/handshake.rs` — ephemeral key exchange in HELLO/AUTH +- `gateway/src/net/io.rs` — TCP framing with encryption + +### ✅ Voice +- `client/src/net/voice.rs` — encrypted voice packets on UDP (plaintext at rest in voice-node, encrypted on wire) +- Voice-node treats packets as opaque bytes (transparent relay) +- End-to-end encryption: client A → encrypted → voice-node → encrypted → client B + +## Nonce management + +Each session has a monotonic sequence counter: +- Start at 0 on session establishment +- Increment per packet (both directions independently) +- Nonce = `session_id (8 bytes) || sequence (4 bytes)` +- 12-byte nonce fits ChaCha20-Poly1305 standard + +## Key dependencies + +Already in `Cargo.toml` (workspace): +- `chacha20poly1305 = "0.10"` +- `x25519-dalek = { version = "2", features = ["static_secrets"] }` + +Need to add: +- `hkdf = "0.12"` for key derivation +- `sha2 = "0.10"` (HKDF dependency, likely already transitive) + +## Testing + +- ✅ Unit test: encrypt → decrypt round-trip with known keys (`client/src/net/crypto.rs`) +- ✅ Unit test: tampered ciphertext fails authentication +- ✅ Unit test: build_packet creates valid encrypted packets +- Manual test: Run client + gateway + voice-node, join voice channel, verify packets encrypted +- Wireshark: Capture UDP traffic, confirm it's not readable plaintext diff --git a/docs/05-features/private-mode.md b/docs/05-features/private-mode.md new file mode 100644 index 0000000..692974c --- /dev/null +++ b/docs/05-features/private-mode.md @@ -0,0 +1,84 @@ +# Private Mode — Phase 1.1 + +## Goal + +Allow running a VNOX server in fully isolated mode. +When private, the server does not discover, connect to, or federate with any other node. + +This makes VNOX safe for personal/family use (OwnCord-style), +while keeping the option to go federated later. + +## Config + +In `dev/config.toml`: + +```toml +[federation] +enabled = false # ← PRIVATE: server is isolated +# enabled = true # ← FEDERATED: can peer with other nodes +``` + +Or with explicit mode: + +```toml +[server] +mode = "private" # isolated, no federation +# mode = "federated" # can discover and peer +``` + +## Implementation + +### Gateway config + +```rust +// gateway/src/domain/config.rs +pub struct Config { + pub server: ServerConfig, + pub federation: FederationConfig, +} + +pub struct ServerConfig { + pub mode: ServerMode, // Private | Federated +} + +pub enum ServerMode { + Private, + Federated, +} + +pub struct FederationConfig { + pub enabled: bool, + pub known_nodes: Vec, +} +``` + +### Behavior when private + +- Gateway does not send any federation packets +- Gateway does not accept inbound federation connections +- No node discovery (DNS SRV queries skipped) +- No federation port needs to be open +- All features (channels, voice, DMs) work normally, just local-only + +### Gate check + +```rust +// gateway/src/handler/federation.rs +pub async fn should_sync_to_remotes(config: &Config) -> bool { + config.federation.enabled +} + +// All federation entry points start with: +if !config.federation.enabled { + return Ok(()); // silently skip +} +``` + +## Result + +| Mode | Behaviour | +|------|-----------| +| `private` | Fully isolated. Like OwnCord. Safe for personal LAN/ home server. | +| `federated` | Can discover, peer, and sync with other VNOX nodes. Like Matrix. | + +Default is `private` — opt-in to federation. diff --git a/docs/05-features/slint-migration.md b/docs/05-features/slint-migration.md new file mode 100644 index 0000000..66cb8b7 --- /dev/null +++ b/docs/05-features/slint-migration.md @@ -0,0 +1,108 @@ +# Slint UI Migration — Phase 1.1 + +## Current state + +Desktop client uses **egui** (immediate-mode GUI). Functional but: +- Grey, utilitarian aesthetic +- Limited design system +- Text-heavy, no modern chat app feel +- Difficult to style consistently + +## Why Slint + +| Criteria | egui | Slint | Iced | +|----------|------|-------|------| +| UI language | Rust code | Declarative DSL (`.slint`) | Elm-style | +| Design quality | Utilitarian | Design-oriented | Good | +| Performance | Fast | Fast | Moderate | +| Chat app fit | ❌ feel | ✅ modern | ✅ modern | +| Learning curve | Low | Medium | Medium | + +**Recommendation:** Slint — declarative DSL makes UI easier to iterate, +built-in design system produces professional look out of the box. + +## Migration strategy + +### Stage 1 — Side-by-side (week 1) +- Embed Slint canvas alongside existing egui panels +- Rewrite sidebar (server list + channel list) in Slint first +- Keep chat area in egui +- Verify data flow works between both frameworks + +### Stage 2 — Chat panel (week 2) +- Rewrite channel chat (text messages + input bar) in Slint +- Rewrite voice panel in Slint +- Remove egui chat dependency + +### Stage 3 — Settings & polish (week 3) +- Rewrite settings window in Slint +- Add dark/light theme toggle +- Add accent color picker +- Apply consistent spacing/typography + +## Example Slint structure + +```slint +// client/src/ui/main.slint +export component MainWindow inherits Window { + in-out property <[Channel]> channels; + in-out property <[Message]> messages; + in-out property active-channel; + + HorizontalLayout { + ServerSidebar {} + ChatPanel {} + VoicePanel {} + } +} + +component ServerSidebar { + VerticalLayout { + Text { text: "VNOX"; } + ListView { + for ch in channels: ChannelRow { + text: ch.name; + icon: ch.kind == "voice" ? "~" : "#"; + } + } + UserBar {} + } +} +``` + +## Data binding + +Use Slint's `in-out property` bindings to sync with Rust state: + +```rust +// client/src/ui/app.rs +use slint::ComponentHandle; + +let ui = MainWindow::new()?; +ui.set_channels(slint::ModelRc::from(models)); +ui.on_send_message(|content| { + // forward to net layer +}); +ui.run()?; +``` + +## File structure after migration + +``` +client/src/ui/ +├── app.rs # entry, wiring +├── main.slint # main layout +├── sidebar.slint # server sidebar + channel list +├── chat.slint # message list + input bar +├── voice.slint # voice panel +├── settings.slint # settings window +├── theme.slint # color/font definitions +└── state/ # UiState (stays in Rust) +``` + +## Open questions + +- Slint's rendering backend (gl/winit/software) — test on all target platforms +- Slint's text input / rich text support for chat messages (emoji, markdown?) +- Whether to remove egui entirely or keep for specific panels (e.g. overlay) +- Licensing: Slint is LGPL (compatible with GPL-3.0 for now, but check for future) diff --git a/docs/05-features/video-screen-audit.md b/docs/05-features/video-screen-audit.md new file mode 100644 index 0000000..453a4f1 --- /dev/null +++ b/docs/05-features/video-screen-audit.md @@ -0,0 +1,453 @@ +# Video, Camera & Screen Share — Design Audit + +> Audits `docs/05-features/video.md` against the current codebase and roadmap. +> Each gap has: problem, recommendation, priority, target file. + +--- + +## Summary + +`video.md` provides a solid high-level sketch (video-node, H.264 UDP relay, nokhwa capture, grid UI). +But critical details are missing for actual implementation. This audit fills those gaps. + +--- + +## 1. Video-node — Integration with existing architecture + +### Gap 1.1 — Video-node lifecycle + +**Problem:** `video.md` says "separate binary or embedded in voice-node" but doesn't decide. Voice-node has just been refactored with jitter buffer — video needs the same relay pattern. + +**Recommendation:** Separate binary (`vnox-video-node`). Video frames are fundamentally different from voice packets (keyframes, fragmentation, higher bandwidth). Keeping them separate avoids coupling and allows independent scaling. + +**Priority:** P0 — must decide before writing a line of code. + +**Target:** `video.md` — add "Deployment" section. + +### Gap 1.2 — Channel membership signalling + +**Problem:** Video-node needs to know which users are in which channel. Currently this is managed by gateway. How does video-node learn membership? + +**Recommendation:** Gateway sends `VIDEO_SESSION_JOIN` / `VIDEO_SESSION_LEAVE` events to video-node over TCP (internal control channel), same pattern as planned for gateway→voice-node membership signalling (Phase 2 in roadmap). + +```json +// Gateway → Video-node (internal TCP) +{ + "event": "VIDEO_SESSION_JOIN", + "channel_id": 12345, + "user_id": "", + "socket_addr": "192.168.1.5:7701" +} +``` + +**Priority:** P0 — video relay can't work without knowing recipients. + +**Target:** `video.md` — add "Gateway integration" section. + +### Gap 1.3 — No encryption spec + +**Problem:** Voice packets are encrypted with ChaCha20-Poly1305. Video packets are not addressed — are they encrypted? With what keys? + +**Recommendation:** Same ChaCha20-Poly1305 AEAD as voice. Each video session gets ephemeral keys derived via the same X25519 ECDH + HKDF-SHA256 path used for voice. Video-node does NOT decrypt (transparent relay, same as voice-node). + +The session key exchange happens during `HELLO` handshake — add a `video_key` field alongside the existing `voice_key`. + +**Priority:** P0 — security regression if video is plaintext. + +**Target:** `video.md` — add "Encryption" section; `docs/02-protocol/security.md` — add video encryption note. + +--- + +## 2. Camera Capture — Missing details + +### Gap 2.1 — nokhwa API surface and fallback + +**Problem:** `video.md` mentions `nokhwa` but not the concrete API or what happens on platforms where it doesn't work (Linux v4l2 permissions, Wayland). + +**Recommendation:** Abstract behind a `VideoSource` trait: + +```rust +// client/src/video/capture.rs +pub trait VideoSource { + fn devices() -> Vec; + fn start(config: StreamConfig) -> Result; +} + +struct NokhwaSource { ... } +struct StubSource { ... } // fallback + +pub struct StreamConfig { + pub device_id: String, + pub resolution: Resolution, // 640x480, 1280x720, 1920x1080 + pub fps: u32, // 15, 30 + pub format: PixelFormat, // NV12, I420, BGRA +} +``` + +Make `nokhwa` a feature gate (same pattern as `rnnoise` for voice). + +**Priority:** P0 — blocks camera implementation. + +**Target:** New file: `client/src/video/capture.rs` (spec in `video.md`). + +### Gap 2.2 — Device hotplug / switch + +**Problem:** No mention of what happens when camera is unplugged/replugged or user switches camera mid-call. + +**Recommendation:** `VideoSource` emits events: +```rust +enum CaptureEvent { + Frame(VideoFrame), + DeviceLost(String), + DeviceReconnected(String), + Error(String), +} +``` + +Client handles `DeviceLost` by showing placeholder tile. User can select new device in settings without leaving call. + +**Priority:** P1 — quality of life, not MVP blocker. + +**Target:** `video.md` — add "Device lifecycle" to capture section. + +### Gap 2.3 — Resolution / FPS negotiation + +**Problem:** `video.md` says "720p default, 30 fps" but doesn't say how clients agree on parameters. Different clients have different cameras. + +**Recommendation:** Server advertises max resolution per channel. Client sends its capability in `VIDEO_SESSION_JOIN`. Server picks min(max_server, max_client) per sender. + +```toml +# server config +[video.channel_defaults] +max_resolution = "1280x720" +max_fps = 30 +max_bitrate_kbps = 2500 +``` + +**Priority:** P1 — needed before multi-user video testing. + +**Target:** `video.md` — add "Capability negotiation" section. + +--- + +## 3. Screen Share — Almost entirely missing + +### Gap 3.1 — Screen capture crate + +**Problem:** `video.md` mentions "Screen sharing (desktop capture)" as one bullet in Phase 2, no detail. + +**Recommendation:** + +| Platform | Crate | Notes | +|----------|-------|-------| +| Windows | `windows-capture` or DXGI via `winapi` | DXGI Desktop Duplication API — best perf | +| Linux X11 | `x11cap` or raw XSHM | X11 only, no Wayland | +| Linux Wayland | `pipewire` via `pw-video` or xdg-desktop-portal | Portal is the standard path | +| macOS | `screencapturekit` via objc bindings or `CGDisplay` | ScreenCaptureKit requires macOS 13+ | + +Abstract behind same `VideoSource` trait as camera. Two implementations: `CameraSource`, `ScreenSource`. + +**Priority:** P1 — needed for Phase 2 screen share. + +**Target:** New doc: `docs/05-features/screen-share.md` or extend `video.md` Phase 2. + +### Gap 3.2 — Window/display selection UI + +**Problem:** User needs to pick which screen or window to share. No UI spec. + +**Recommendation:** Modal dialog when user clicks "Screen Share": + +``` +┌─────────────────────────────────────────┐ +│ Share Your Screen │ +│─────────────────────────────────────────│ +│ [Screens] [Windows] │ ← tabs +│─────────────────────────────────────────│ +│ ┌──────────┐ ┌──────────┐ │ +│ │ Display 1│ │ Display 2│ │ +│ │1920×1080 │ │2560×1440 │ │ +│ │ [✓] │ │ │ │ +│ └──────────┘ └──────────┘ │ +│─────────────────────────────────────────│ +│ ☐ Share system audio │ +│ ☐ Optimize for video (60fps) │ +│─────────────────────────────────────────│ +│ [Cancel] [Share] │ +└─────────────────────────────────────────┘ +``` + +**Priority:** P1 — UX blocker for screen share. + +**Target:** `video.md` — add "Screen share UI" section. + +### Gap 3.3 — System audio capture with screen + +**Problem:** "Share system audio" checkbox in the mockup — how? No mention in video.md. + +**Recommendation:** +- Windows: WASAPI loopback (`cpal` supports it with `loopback` config) +- Linux: PulseAudio monitor or PipeWire +- macOS: BlackHole or ScreenCaptureKit (built-in on 13+) + +Feature-gate it: `--features screen-audio`. Not all platforms support it cleanly. + +**Priority:** P2 — nice to have, not MVP. + +**Target:** `video.md` Phase 2 — add "System audio" bullet. + +### Gap 3.4 — Screen share FPS strategy + +**Problem:** Screen share has different FPS needs than camera. Coding: 15fps is fine. Gaming: 60fps needed. + +**Recommendation:** Client detects content type (static → low FPS, motion → high FPS) and adapts. User can override with "Optimize for video" checkbox. + +Default: 15fps for screen share, 30fps for camera. + +**Priority:** P2 — optimization, not MVP. + +**Target:** `video.md` — add to Phase 2. + +--- + +## 4. Interaction Model — Video + Voice + +### Gap 4.1 — Video tied to voice channel + +**Problem:** `video.md` implies video is in the same channel as voice. What if users want video-only (no voice) or voice-only (no video)? + +**Recommendation:** Video is a capability toggle within a voice channel, not a separate channel type: + +1. User joins voice channel (as today) +2. User clicks "Enable Camera" → client starts sending video frames to video-node +3. User clicks "Disable Camera" → stops sending, voice continues +4. Same for "Share Screen" + +Channel membership = voice channel membership. Video is additive. + +New voice state: `VOICE_STATE` gets a `video: bool` field. + +**Priority:** P0 — architectural decision, affects protocol design. + +**Target:** `video.md` — add "Interaction model" section; `docs/02-protocol/packets.md` — extend VOICE_STATE. + +### Gap 4.2 — Video without voice + +**Problem:** Some users may want to watch a stream without transmitting audio. Current model requires voice channel join. + +**Recommendation:** Phase 1: allow joining voice channel muted+deafened to watch video. Phase 3: separate "watch-only" mode (VIEW permission). + +**Priority:** P2 — Phase 1 workaround is acceptable. + +**Target:** `video.md` — add "Watch-only mode" to Phase 3. + +--- + +## 5. Performance & Quality + +### Gap 5.1 — Bitrate adaptation + +**Problem:** No mechanism to adjust video bitrate based on network conditions. Voice has jitter buffer adaptation, video has nothing. + +**Recommendation:** Client-side adaptive bitrate (ABR): +- Measure packet loss and RTT from video-node ACKs +- Adjust encoder bitrate up/down based on available bandwidth +- 3 tiers: low (500kbps), medium (1500kbps), high (4000kbps) + +Video-node sends periodic `VIDEO_STATS` with per-receiver loss rates: + +```json +{ + "channel_id": 12345, + "receivers": { + "user_a": { "loss_pct": 0.5, "rtt_ms": 12 }, + "user_b": { "loss_pct": 8.0, "rtt_ms": 150 } + } +} +``` + +**Priority:** P1 — needed for real-world use. + +**Target:** `video.md` — add "Adaptive bitrate" section. + +### Gap 5.2 — Simulcast design + +**Problem:** "Simulcast" is listed as Phase 3 with no design. Different receivers have different bandwidth. + +**Recommendation:** Sender encodes 2-3 quality layers. Video-node forwards appropriate layer to each receiver based on their reported bandwidth. + +``` +Sender → encode: 1080p (4Mbps) + 720p (1.5Mbps) + 360p (500kbps) + │ + Video-node + ╱ │ ╲ + 1080p→A 720p→B 360p→C (based on per-receiver stats) +``` + +Requires VP9 SVC or H.264 simulcast. Complex — Phase 3 is correct. + +**Priority:** P2 — Phase 3, but design it now to avoid architecture lock-in. + +**Target:** `video.md` Phase 3 — expand "Simulcast" bullet. + +### Gap 5.3 — Keyframe interval + +**Problem:** New viewers joining mid-stream need a keyframe to start decoding. Video.md doesn't mention keyframe strategy. + +**Recommendation:** +- Sender inserts keyframe every 2 seconds (configurable) +- Video-node caches last keyframe per sender +- On new viewer join: video-node sends cached keyframe immediately, then regular frames +- `VIDEO_SESSION_JOIN` triggers keyframe push + +Add `REQUEST_KEYFRAME` packet for explicit PLI (Picture Loss Indication). + +**Priority:** P1 — new viewers can't see video without keyframe. + +**Target:** `video.md` — add "Keyframe handling" section. + +--- + +## 6. Missing Protocol Details + +### Gap 6.1 — No packet fragmentation spec + +**Problem:** Video frames can be 50-100KB. UDP MTU is ~1400 bytes. Video.md says "Packetize into MTU-friendly chunks" but doesn't define the fragmentation protocol. + +**Recommendation:** Reuse the LNEx fragmentation flags from the base packet header (bits 2-3): + +``` +FRAGMENTED (bit 2) + LAST_FRAG (bit 3) +``` + +Each video frame: +1. Split into 1400-byte chunks +2. Each chunk gets same `frame_seq`, incrementing `fragment_seq` +3. Last chunk sets `LAST_FRAG` flag +4. Receiver reassembles before decode + +New packet type: `VIDEO_FRAME_FRAGMENT (0x0012)` — separate from unfragmented `VIDEO_FRAME (0x0011)`. + +Wait — `VIDEO_FRAME` ID isn't assigned in the packet registry yet. Register: + +``` +0x0011 VIDEO_FRAME single (small) video frame, no fragmentation +0x0012 VIDEO_FRAME_FRAG fragment of a larger video frame +``` + +**Priority:** P0 — video can't work over UDP without fragmentation. + +**Target:** `video.md` — replace packet format section; `docs/02-protocol/packets.md` — add 0x0011/0x0012 to registry. + +### Gap 6.2 — No RTCP-like receiver reports + +**Problem:** Sender has no feedback about what receivers are experiencing. Voice has implicit feedback (jitter buffer stats), video needs explicit. + +**Recommendation:** Minimal receiver report packet: + +```json +// 0x0013 VIDEO_RECEIVER_REPORT — client → video-node → sender +{ + "frame_seq": 1042, + "loss_cumulative": 15, + "loss_fraction": 2, // percent of last N packets + "jitter_ms": 8, + "rtt_ms": 35 +} +``` + +Client sends every 1 second. Video-node aggregates and forwards to sender. + +**Priority:** P1 — needed for ABR and quality adaptation. + +**Target:** `video.md` — add "Receiver reports" section. + +--- + +## 7. Guild Integration (Phase 1.2) + +### Gap 7.1 — Video permissions already defined + +**Status:** ✅ `STREAM` (bit 18) permission is already in the community model (`docs/07-community-model.md`). Covers both camera and screen share. + +No gap here — just implement the check in gateway when processing `VIDEO_SESSION_JOIN`. + +### Gap 7.2 — Channel-level video settings + +**Problem:** Guild admins may want to disable video in certain channels (text-only channels, AFK channel). + +**Recommendation:** Add `video_allowed: bool` to channel config. Default: `true` for voice channels, `false` for text channels. + +```sql +ALTER TABLE channels ADD COLUMN video_allowed BOOLEAN NOT NULL DEFAULT 1; +``` + +Gateway rejects `VIDEO_SESSION_JOIN` if `video_allowed = false`. + +**Priority:** P1 — admin control expected by guild owners. + +**Target:** `docs/10-database.md` — add column to channels table; `docs/07-community-model.md` — add `video_allowed` to channel settings. + +--- + +## 8. Mobile Considerations (Phase 3) + +### Gap 8.1 — Front/back camera switch + +**Problem:** Mobile video.md doesn't mention camera switching at all. + +**Recommendation:** Mobile client sends `CAMERA_SWITCH` event to video-node — no protocol change needed, just local capture change. New frame stream with `camera: front|back` metadata. + +**Priority:** P2 — Phase 3. + +**Target:** `docs/04-clients/mobile.md` (doesn't exist yet — create in Phase 3). + +### Gap 8.2 — Self preview + +**Problem:** Desktop video grid has "Self-view (small, picture-in-picture corner)". On mobile the self-view is more important (front camera framing). + +**Recommendation:** Mobile layout: self-view full-width at top, other participants in scrollable grid below. Toggle button to swap. + +**Priority:** P2 — Phase 3. + +**Target:** `docs/04-clients/mobile.md`. + +--- + +## Gap Summary — What to Add Before Implementation + +| # | Gap | Priority | Target file | +|---|-----|----------|-------------| +| 1 | Video-node binary decision + deployment | P0 | `video.md` | +| 2 | Gateway membership signalling to video-node | P0 | `video.md` | +| 3 | Video encryption (ChaCha20-Poly1305) | P0 | `video.md`, `security.md` | +| 4 | Video frame fragmentation protocol | P0 | `video.md`, `packets.md` | +| 5 | Video ↔ voice interaction model | P0 | `video.md`, `packets.md` | +| 6 | VideoSource trait + feature-gated nokhwa | P0 | `video.md` | +| 7 | Keyframe caching + PLI request | P1 | `video.md` | +| 8 | Receiver reports (RTCP-like) | P1 | `video.md` | +| 9 | Adaptive bitrate design | P1 | `video.md` | +| 10 | Screen share crate selection + ScreenSource | P1 | `video.md` | +| 11 | Window/display selection UI | P1 | `video.md` | +| 12 | Resolution/FPS negotiation | P1 | `video.md` | +| 13 | Channel-level video_allowed setting | P1 | `database.md`, `community-model.md` | +| 14 | Camera hotplug/switch handling | P1 | `video.md` | +| 15 | Video-only (watch) mode | P2 | `video.md` | +| 16 | System audio with screen share | P2 | `video.md` | +| 17 | Screen share FPS strategy | P2 | `video.md` | +| 18 | Simulcast design | P2 | `video.md` | +| 19 | Mobile front/back camera + self preview | P2 | `mobile.md` | + +--- + +## Recommended Implementation Order + +1. **Update `video.md`** — incorporate P0 and P1 gaps from this audit into the design doc +2. **Register packet types** — 0x0011–0x0013 in `packets.md` +3. **Video-node prototype** — separate binary, UDP relay, no encoding/decoding (transparent relay like voice) +4. **Gateway ↔ video-node signalling** — internal TCP control channel for membership +5. **Client camera capture** — `VideoSource` trait + `NokhwaSource` behind feature gate +6. **Fragmentation** — implement FRAGMENTED/LAST_FRAG for video frames +7. **Grid UI** — basic 2×2 grid in client +8. **Encryption** — ChaCha20-Poly1305 for video frames +9. **ABR + receiver reports** — iterate toward production quality +10. **Screen share** — `ScreenSource` implementation + picker UI diff --git a/docs/05-features/video.md b/docs/05-features/video.md new file mode 100644 index 0000000..3e3e878 --- /dev/null +++ b/docs/05-features/video.md @@ -0,0 +1,99 @@ +# Video Chat — Architecture + +## Goal + +Add real-time video to voice channels. +Users can share their webcam feed alongside voice. + +## Architecture + +``` +Current (voice only): +├─ Gateway: TCP (signaling, chat) +├─ Voice-node: UDP (Opus voice relay) +└─ Client: mic → Opus → UDP → voice-node → UDP → client → playback + +With video: +├─ Gateway: TCP (signaling) — UNCHANGED +├─ Voice-node: UDP (Opus voice) — UNCHANGED +├─ Video-node: UDP (H.264/VP9 relay) — NEW +│ └─ Receives encoded frames, relays to channel members +└─ Client: mic → Opus → UDP → voice-node + webcam → H.264 → UDP → video-node ← UDP → decode → display grid +``` + +## Video-node + +New optional component, separate binary or embedded in voice-node. + +### Responsibilities +- Receive encoded video frames over UDP +- Relay to all other clients in the same voice channel +- No transcoding (relay only — CPU efficient) +- Max resolution / bitrate per channel configurable + +### Packet format + +```json +{ + "packet_id": "VIDEO_FRAME", + "channel_id": 12345, + "sender_id": "", + "frame_seq": 42, + "codec": "h264", // or "vp9" + "keyframe": false, + "data": "" +} +``` + +Initially JSON (matching Phase 1 convention), binary framing in Phase 2. + +## Client capture pipeline + +New file: `client/src/video/capture.rs` + +1. Enumerate webcam devices via `nokhwa` or `video4linux` +2. Capture frames at configurable resolution (720p default) +3. Encode to H.264 via `ffmpeg-next` or hardware encoder +4. Packetize into MTU-friendly chunks +5. Send over UDP to video-node + +### Dependencies + +- `nokhwa` — cross-platform camera capture (Rust) +- `ffmpeg-next` or `rav1e` — H.264/VP9 encoding + +## Client UI — Video grid + +New component: `client/src/ui/video.rs` + +- Grid layout (max 4×4 = 16 participants visible) +- Active speaker highlight (green border) +- Self-view (small, picture-in-picture corner) +- Mute video button per participant +- Resolution/quality indicator per stream + +Layout modes: +- 1 participant → full width +- 2-4 → 2×2 grid +- 5-9 → 3×3 grid +- 10-16 → 4×4 grid with scrolling + +## Implementation phases + +### Phase 1 (MVP) +- H.264 encoding with `nokhwa` + `ffmpeg-next` +- Single video-node binary +- 2×2 grid in UI +- 720p max resolution, 30 fps + +### Phase 2 +- VP9 support (better quality/bitrate) +- Adaptive resolution (auto downscale on packet loss) +- Picture-in-picture self-view +- Screen sharing (desktop capture) + +### Phase 3 +- Hardware encoding (NVENC/VAAPI) +- Simulcast (different resolution per stream) +- Recording support diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..15e07a4 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +# Clients + +VNOX is a native-only platform. There is no web client and none is planned. + +## Why no web client + +- WebRTC adds latency overhead incompatible with VNOX's voice latency targets +- WASM + wgpu in browser is not a viable egui deployment target today +- Electron is explicitly rejected (it's what we're building against) +- A browser tab is not the right environment for a persistent voice client + +The desktop client is a real native binary. It starts fast, uses minimal memory, +and has direct access to audio hardware without browser sandboxing. + +## Clients + +- [desktop.md](desktop.md) — primary client, Windows / macOS / Linux +- [mobile.md](mobile.md) — Phase 3, stack TBD +- [overlay.md](overlay.md) — in-game HUD, Phase 2 diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..0b82a46 --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,400 @@ +# VNOX Client — Design System + +> Based on Hi-Fi Minimalism v2.0 +> Adapted for: native desktop voice/chat client (Rust + egui + wgpu) + +--- + +## Philosophy + +VNOX UI должен ощущаться как инструмент, а не социальная сеть. + +``` +calm · focused · fast · warm · precise +``` + +Не: +``` +gamer RGB · Discord clone · neon cyberpunk · glassmorphism · corporate SaaS +``` + +> The client is infrastructure. The UI is just the control surface. + +--- + +## Color Tokens + +```css +/* Backgrounds — layered, never pure black */ +--bg-base: #0d0d0d; /* root, titlebar */ +--bg-surface: #151515; /* panels, sidebars (was #111111 — lifted for panel contrast) */ +--bg-elevated: #1d1d1d; /* inputs, cards (was #141414 — lifted for depth) */ +--bg-interactive: #2a2a2a; /* hover targets, dropdowns */ + +/* Borders — lifted significantly for visual hierarchy */ +--border-subtle: #1c1c1c; /* was #171717 */ +--border-default: #262626; /* was #1e1e1e — now actually visible */ +--border-strong: #2e2e2e; +--border-accent: rgba(255, 107, 53, 0.20); + +/* Accent — warm orange */ +--accent: #ff6b35; +--accent-hover: #ff844f; +--accent-active: #e85d04; +--accent-10: rgba(255, 107, 53, 0.08); +--accent-20: rgba(255, 107, 53, 0.15); + +/* Text — warm, never pure white */ +--text-primary: #f4c89a; /* main content (added) */ +--text-secondary: #c88b5a; /* names, labels */ +--text-muted: #8a6a52; /* secondary info */ +--text-dim: #6a4a2a; /* hints, timestamps */ +--text-ghost: #5a422c; /* section labels, separators (was #3d2e20 — lifted) */ +--text-invisible: #443222; /* near-invisible, decorative (was #2d1e12 — lifted) */ + +/* Semantic */ +--success: #7cb87a; /* online, connected, low latency */ +--success-muted: rgba(124, 184, 122, 0.10); +--warning: #e6a230; /* ping indicator, unread */ +--warning-muted: rgba(230, 162, 48, 0.10); +--error: #d9604a; /* muted mic (when needed), errors */ +--error-muted: rgba(217, 96, 74, 0.10); +--info: #6a9ecf; /* second user accent color */ +--info-muted: rgba(106, 158, 207, 0.10); +``` + +--- + +## Typography + +Клиент использует **только monospace**. Это намеренно — усиливает ощущение инструмента. + +``` +Primary font: IBM Plex Mono +Fallback: Fira Code, Geist Mono, monospace +``` + +### Scale (клиент-специфичный, компактный) + +| Role | Size | Weight | Color | +|------|------|--------|-------| +| Section label | 10px | 400 | `--text-ghost` | +| Timestamp, ID | 9px | 400 | `--text-invisible` | +| Status, badge | 9px | 500 | semantic | +| Channel name | 12px | 400 | `--text-ghost` → `--text-secondary` | +| Message text | 11px | 400 | `--text-muted` | +| Username | 11px | 600 | varies per user | +| Node name | 13px | 600 | `--text-secondary` | +| Settings title | 13px | 600 | `--text-secondary` | +| Wordmark VNOX | 11px | 600 | letter-spacing: 0.2em | +| Panel title | 11px | 400 | `--text-secondary` | +| Dashboard heading | 14px | 600 | `--text-secondary` | + +Letter spacing для section labels: `0.10–0.12em`, text-transform: uppercase. + +--- + +## Layout + +### Title bar (34px) + +- Background: `--bg-strip` (`theme::BG_STRIP`); **без отдельной painter-linии** под всю ширину — переход «titlebar ↔ контент» только за счёт контраста `--bg-strip` vs `--bg-base`. Раньше 1px `hline` воспринимался как случайная полоска «под логотипом» из-за состыковки с левым rail. +- Сетка из **трёх равных колонок**: + - **левая** — **`[ prefs ]`** + **`VNOX`** (wordmark `--text-primary`, слева направо после prefs); + - **центр** — статус ноды: **`●`** + **`NODE: …`** (`OFFLINE` / `CONNECTING` / имя ноды), **по центру средней колонки** (= визуальный центр окна); + - **правая** — **transport‑подсказка** справа: `transport: idle` · `transport: connecting…` · `transport: quic/v1`. +- Вход в настройки: **`[ prefs ]`** — явная консольная кнопка (без нестабильных Unicode‑иконок); при открытых настройках тот же текст, цвет **accent**. Дубль: кнопка **open prefs** в нижней левой колонке, **только пока offline**. +- Нативный заголовок ОС (**taskbar / список окон**) — короткий **`Vnox`**, чтобы не дублировать тот же wordmark **`VNOX`** внутри клиента. + +### Структура окна + +``` +┌─────────────────────────────────────────────────────┐ +│ [prefs] VNOX │ ● NODE: OFFLINE │ transport: idle │ +│ (нет линии 1px на всю ширину под панелью) │ +├──────────────────────────────────────────────────────┤ +│ rail │ channels (208px) │ main content │ +│ (52px) │ │ │ +│ SERVERS │ node / status │ connect or chat │ +│ [AB] │ channel sections │ │ +│ [+] │ ─────────────── │ │ +│ │ identity + voice UI │ │ +└─────────┴─────────────────────┴─────────────────────┘ +``` + +### Node rail (bookmark strip, 52px) + +Узкая колонка **закладок серверов** (не «Discord‑кругляши»): + +- Ширина **52px**, фон `--bg-strip`, как верхний titlebar для визуальной связности. +- Заголовок колонки: **SERVERS** (8px, `--text-ghost`, центрируется по ширине rail). +- Плитки **30×30px**, скругление 5px, **строго центрируются** по горизонтали (без случайной короткой линии‑разделителя посередине — она давала эффект «криво» в узком rail). +- **Idle** (нет активной закладки): рамка 1px `--border-faint`, подпись `···` во внутреннем поле, tooltip объясняет «подключайся через Connect». +- **Активная нода**: 2 символа аббревиатуры, `--accent` левый акцент 2px внутри плитки, `--accent-10` фон. +- **«+»**: только одна вторичная плита внизу; tooltip — «bookmark позже». + +### Node switcher (legacy note) + +Исторически описывался как 40px rail с линией перед `+`; в текущем клиенте заменено на блок **Node rail** выше. + +### Channel list (208px) + +- Node info (верх): имя ноды 12px semibold + `lnex://nc.` 9px `--text-ghost` при коннекте; офлайн — одна строка **not connected** +- Section labels: 9px uppercase, `--text-ghost`, letter-spacing 0.12em +- Channel item: 4px 12px padding, gap 6px (icon + name) + - Default: `--text-ghost` + - Hover: `--text-dim` (без background) + - Active: `--text-secondary` + левая полоска 2px `--accent` + `--accent-10` bg +- Voice users под каналом: indent 26px, 10px, `--success` для говорящих, `--text-ghost` для muted + +### Bottom-left identity bar + +``` +[ open prefs ] (только пока offline) + +— voice ~ channel 00:42 + [x] microphone [x] hear others + +[YU] you + a1b2…9f0e (pubkey hex, middle-truncated) +``` + +- **Не показывать** фиктивный RTT (например «12ms»), если нет реального измерения. +- **Не дублировать** протокольную строку вида `LNEx v1 · udp` под ником — перегружает и налезает на аватар/текст; протокол раскрывается в Connect / docs. +- Настройки **не** прячем в ряд мелких иконок у ника: основной вход — **`[ prefs ]`** в title bar; офлайн — компактная кнопка **open prefs**. +- В голосе: чекбоксы **microphone** / **hear others** (без эмодзи/символов, которые на части шрифтов дают «квадратики»). +- Avatar: 32×32px, radius 6px, disabled button (только визуал), self — `--accent-10` + border. + +### Main area + +- Channel header: 36px, border-bottom `--border-subtle` +- Messages: padding 12px 14px, gap между группами 8px +- Input: `#general ›` prefix + cursor blink + hint text + +--- + +## Components + +### Toggle + +``` +Off: width 30px, height 16px, bg --bg-elevated, border --border-default + thumb: 10px circle, bg #2d2d2d, left 2px + +On: bg --accent-10, border --border-accent + thumb: bg --accent, left 16px, glow rgba(255,107,53,0.4) + +Transition: 150ms ease +``` + +### Badge + +```css +/* Protocol / transport */ +.badge-lnex { color: #ff844f; border: 1px solid rgba(255,107,53,0.2); bg: rgba(255,107,53,0.08) } +.badge-udp { color: #6a9ecf; border: 1px solid rgba(106,158,207,0.2); bg: rgba(106,158,207,0.10) } +.badge-tcp { color: #e6a230; border: 1px solid rgba(230,162,48,0.2); bg: rgba(230,162,48,0.10) } +.badge-enc { color: #7cb87a; border: 1px solid rgba(124,184,122,0.2); bg: rgba(124,184,122,0.10) } + +/* Размер: 9px, padding 1px 6px, border-radius 3px */ +``` + +### Select / Input + +``` +bg: --bg-elevated (#111) +border: 1px solid --border-default +border-radius: 5px +font: 10px IBM Plex Mono +color: --text-muted + +focus: + border-color: --border-accent +``` + +### Slider + +``` +track: 2px height, --border-default +thumb: 10px circle, --accent, subtle glow + +::-webkit-slider-thumb { + background: var(--accent); + box-shadow: 0 0 6px rgba(255, 107, 53, 0.3); +} +``` + +### User avatar + +``` +Size variants: + sm — 24×24px, border-radius 5px (identity bar) + md — 26×26px, border-radius 6px (member list) + lg — 36×36px, border-radius 8px (identity card in settings) + +Default: bg --bg-elevated, border --border-default, color --text-muted +Self: bg --accent-10, border --border-accent, color --accent +Other: custom per-user, based on their seed color (--info, --text-secondary, etc.) +``` + +### System message / separator + +``` +font-size: 9–10px +color: --text-ghost +display: flex + ::before/::after lines in --border-subtle + +Examples: + ● connected · nightcore.lnex · LNEx v1 + — today — +``` + +--- + +## Active / Indicator Language + +Везде используется одна и та же визуальная метафора для "активно": + +``` +Левая вертикальная полоска 2px --accent ++ subtle --accent-10 background +``` + +Это применяется к: +- активной ноде в switcher +- активному каналу в списке +- активному разделу в настройках + +Не используется background без полоски, и не используется полоска без подсветки. + +--- + +## Voice Indicators + +``` +Говорит: dot 5px #7cb87a, box-shadow 0 0 6px rgba(124,184,122,0.45) +Muted: icon ti-microphone-off, color --text-ghost +В канале: dot или icon перед именем, indent 26px от иконки канала +``` + +В voice badge (активный call): +``` +dot 5px --accent, box-shadow 0 0 6px rgba(255,107,53,0.5) +``` + +--- + +## Settings Layout + +``` +┌─────────────────────────────────────────────────────┐ +│ titlebar — [prefs]* │ VNOX │ … │ +│ открыть настройки: клик [prefs]; обратный маршрут │ +│ тот же, пока приложение показывает settings screen │ +├──────────────────────────────────────────────────────┤ +│ nav (160px) │ content │ +│ │ │ +│ account │ [page title] │ +│ identity │ [subtitle] │ +│ │ │ +│ audio │ [group label] ────────────────── │ +│ voice ◄ │ row: label + control │ +│ output │ row: label + control │ +│ │ │ +│ network │ [group label] ────────────────── │ +│ network │ ... │ +│ overlay │ │ +│ │ │ +│ app │ │ +│ appearance │ │ +│ keybinds │ │ +│ plugins │ │ +│ │ │ +│ debug │ │ +│ advanced │ │ +└─────────────────┴────────────────────────────────────┘ +``` + +- Nav width: 160px, bg `--bg-base` +- Section labels в nav: 9px uppercase, `--text-ghost` +- Nav item: 11px, default `--text-ghost`, hover `--text-dim`, active `--text-secondary` + левая полоска +- Content padding: 16px 20px +- Group label: 9px uppercase, `--text-ghost`, border-bottom `--border-subtle`, margin-bottom 8px +- Row: `display:flex; justify-content:space-between; align-items:center; padding:6px 0` +- Row separator: `border-top: 1px solid #0f0f0f` (почти невидимый, только ритм) + +--- + +## Motion + +``` +duration-instant: 80ms — toggle, dot +duration-fast: 120ms — hover color change +duration-base: 150ms — toggle thumb, panel transitions +duration-slow: 250ms — page switch in settings + +ease: cubic-bezier(0.0, 0.0, 0.2, 1) — ease-out для всего +``` + +Никаких scale transforms на hover. Только: +- `color` transition +- `background` transition +- `border-color` transition +- `box-shadow` для glow (subtle) +- `left` для toggle thumb + +--- + +## Anti-patterns (клиент-специфично) + +| ❌ Не делать | ✅ Делать | +|-------------|---------| +| Отдельная 1px линия на всю ширину только под titlebar (стек с rail = «случайная полоска») | Отделение только контрастом `--bg-strip` / `--bg-base`; линии локально там, где группируешь контент | +| Круглые аватары | Квадратные с border-radius 5–8px | +| Sidebar с серверами как у Discord (72px, круглые) | Узкий rail **52px**, квадратные плитки **30×30**, аббревиатура, заголовок **SERVERS** | +| Панель участников справа как у Discord | Нет отдельной панели, участники — в боковом списке | +| Neon glow на элементах | Subtle glow max 0.18 opacity | +| Pure black backgrounds | Минимум #0a0a0a | +| Pure white text | Максимум #f4c89a | +| RGB accent | Один тёплый accent #ff6b35 | +| Rounded pill buttons everywhere | border-radius 4–6px, pill только для badge | +| Жирные разделители | 1px --border-subtle, почти невидимые | + +--- + +## Noise Texture + +Лёгкий шум поверх всего интерфейса — добавляет аналоговое ощущение. + +```css +.root::after { + content: ''; + position: absolute; + inset: 0; + background-image: url("data:image/svg+xml,..."); /* SVG feTurbulence */ + opacity: 0.025; /* максимум 0.03, иначе грязно */ + pointer-events: none; + z-index: 999; +} +``` + +В egui/wgpu: реализуется как overlay texture на финальном render pass. + +--- + +## Seed Colors для пользователей + +У каждого пользователя свой цвет ника, детерминированный от pubkey. + +Палитра допустимых цветов (тёплая, совместимая с системой): + +``` +#c88b5a — warm amber (default / self) +#6a9ecf — steel blue +#7cb87a — warm green +#c49a6c — sand +#b07cc6 — muted purple +#d4876a — terracotta +``` + +Не используются: яркие/neon цвета, холодные синие, чистый белый/красный. diff --git a/docs/desktop.md b/docs/desktop.md new file mode 100644 index 0000000..23f3e89 --- /dev/null +++ b/docs/desktop.md @@ -0,0 +1,195 @@ +# Desktop Client + +The primary VNOX client. Native, fast, lightweight. + +## Stack + +| Layer | Technology | +|-------|-----------| +| Language | Rust | +| UI framework | egui | +| Renderer | wgpu | +| Audio capture/playback | cpal | +| Audio processing | rodio | +| Voice codec | opus (libopus bindings) | +| Networking | quinn (QUIC/UDP), tokio | +| Serialization | prost (Protobuf) | + +### Why egui + +- immediate mode UI: simple to reason about, no complex state trees +- runs on wgpu: same renderer as the rest of the GPU pipeline +- truly cross-platform: one codebase, same behavior on Windows/macOS/Linux +- no runtime dependencies: ships as a single binary + +### Why wgpu + +- modern GPU API (Vulkan / Metal / DX12 / WebGPU backend) +- future-proof for overlay rendering and spatial audio visualizations +- native on all tier-1 platforms + +--- + +## Platforms + +| Platform | Status | +|----------|--------| +| Linux x86_64 | Phase 1 | +| Windows x86_64 | Phase 1 | +| macOS (Apple Silicon) | Phase 1 | +| macOS (Intel) | Phase 1 | +| Linux ARM64 | Phase 2 | + +--- + +## Audio pipeline + +``` +cpal (capture) + │ PCM f32 48000Hz + ▼ +RNNoise (noise suppression) + ▼ +AEC (echo cancellation) + ▼ +VAD (voice activity detection) + ▼ +opus encode + ▼ +LNEx UDP packet → voice-node +``` + +Playback: + +``` +LNEx UDP packet ← voice-node + ▼ +jitter buffer + ▼ +opus decode + ▼ +rodio (playback) + ▼ +cpal (output device) +``` + +Audio device selection is configurable in Settings → Voice and Settings → Audio Output. + +--- + +## Design system + +The client follows the VNOX Hi-Fi Minimalism design system. +Full specification: `docs/04-clients/design-system.md` + +Key decisions: +- monospace font throughout (IBM Plex Mono) +- warm dark palette (`#0d0d0d` base, `#ff6b35` accent) +- no round server icons — square with abbreviation, 40px sidebar +- latency indicator bottom-left, next to identity +- no separate member list panel — voice users shown inline in channel list + +--- + +## Layout + +``` +┌──────────────────────────────────────────────────────┐ +│ titlebar: dots · VNOX wordmark · connected node │ +├────────┬──────────────────┬───────────────────────── │ +│ nodes │ channels │ main (chat / voice) │ +│ 40px │ 190px │ flex │ +│ │ │ │ +│ NC ◄ │ nightcore.lnex │ #general │ +│ DV · │ ───────────── │ ───────────────────── │ +│ GG │ # general ◄ │ messages │ +│ VD │ # dev-talk │ │ +│ + │ # plugins │ │ +│ │ │ │ +│ │ ~ lobby │ │ +│ │ · raven │ ───────────────────── │ +│ │ · 0xmist │ #general › [input] │ +│ │ 🔇 lurker_7 │ │ +│ │ ~ gaming │ │ +│ │ ───────────── │ │ +│ │ ● 12ms lnex v1 │ │ +│ │ [YU] you 🎤 🎧 ⚙ │ │ +└────────┴──────────────────┴──────────────────────────┘ +``` + +--- + +## Settings + +Settings are stored locally. No settings are synced to the server. + +### Voice +- input device +- input volume +- noise suppression (RNNoise on/off) +- echo cancellation (on/off) +- activation mode: push-to-talk / voice activity / always on +- VAD threshold +- Opus bitrate (8–128k) +- Opus frame interval (10 / 20 / 40ms) + +### Audio output +- output device +- output volume +- jitter buffer size +- adaptive jitter buffer (on/off) + +### Network +- relay address +- auto relay selection +- UDP port +- force relay only (disable direct) + +### Identity +- view pubkey +- export keypair +- seed phrase backup +- rotate keypair + +### Appearance +- color scheme +- UI scale +- font + +### Keybinds +- push-to-talk key +- mute toggle +- deafen toggle +- overlay toggle + +### Plugins +- installed plugin list +- enable / disable per plugin + +### Advanced (debug) +- log level +- log voice packets +- show packet stats +- disable encryption (dev only) + +--- + +## Building from source + +```bash +# Prerequisites: Rust stable, system audio libs + +# Linux (Ubuntu/Debian) +apt install libasound2-dev libopus-dev + +# macOS +brew install opus + +# Build +git clone https://github.com/vnox/vnox +cd vnox/client +cargo build --release + +# Binary +./target/release/vnox-client +``` diff --git a/docs/mobile.md b/docs/mobile.md new file mode 100644 index 0000000..ac01905 --- /dev/null +++ b/docs/mobile.md @@ -0,0 +1,71 @@ +# Mobile Client + +> Status: Phase 3 — not yet started. +> This document tracks intentions and open questions. + +--- + +## Goal + +A native mobile client for iOS and Android. +Not a PWA. Not a wrapper around the desktop client. + +## Open questions + +### UI framework + +egui on mobile is not practical today — touch input support is limited, +and the immediate mode model doesn't map well to mobile interaction patterns. + +Candidates under consideration: + +| Option | Notes | +|--------|-------| +| Rust + custom egui mobile backend | Most consistent with desktop codebase, significant work | +| Rust + Makepad | Rust-native UI designed for mobile, less mature | +| Rust core + Flutter UI | Dart for UI, Rust for audio/networking via FFI | +| Rust core + Swift/Kotlin UI | Platform-native UI, Rust for the important parts | + +Decision: deferred to Phase 3. + +### Audio + +Mobile audio APIs are significantly more constrained than desktop: + +- iOS: AVAudioSession, strict background audio rules +- Android: AAudio / OpenSL ES, varying latency by device + +opus encoding is the same. cpal has partial mobile support. +The audio pipeline will need platform-specific tuning. + +### Background operation + +Voice calls in the background require OS-level permission and +platform-specific handling (CallKit on iOS, ConnectionService on Android). +This is non-trivial and will be a significant portion of mobile dev effort. + +--- + +## What mobile must support (MVP) + +- connect to a VNOX node +- join voice channels +- push-to-talk +- text chat +- identity (same keypair as desktop, importable via QR or keyfile) + +## What mobile explicitly will not do + +- host a node (gateway or voice-node) +- run plugins +- game overlay + +--- + +## Timeline + +Mobile client is Phase 3, after: +- Phase 1: desktop client + server MVP +- Phase 2: overlay, permissions, friend system + +Estimated start: after Phase 2 is stable. diff --git a/docs/overlay.md b/docs/overlay.md new file mode 100644 index 0000000..2f108b1 --- /dev/null +++ b/docs/overlay.md @@ -0,0 +1,106 @@ +# Overlay + +> Status: Phase 2 — design draft. + +The VNOX overlay renders a HUD on top of running games and applications, +showing voice channel state without alt-tabbing. + +--- + +## What it shows + +- who is currently speaking (avatar / nickname + audio indicator) +- your own mic state (active / muted / push-to-talk held) +- current channel name +- latency (RTT to node) +- hotkey state hints + +--- + +## Architecture + +The overlay is a separate process that communicates with the main VNOX client +via a local IPC socket (Unix socket / named pipe). + +The client pushes state updates to the overlay: +- user speaking events (`VOICE_STATE`) +- channel changes +- mute state changes + +The overlay renders on top of other applications. + +### Rendering approach + +| Platform | Method | +|----------|--------| +| Windows | DirectX overlay injection or transparent top-level window | +| Linux (X11) | Shaped transparent window, always-on-top | +| Linux (Wayland) | Layer shell protocol (wlr-layer-shell) | +| macOS | CGWindow overlay | + +Implementation complexity varies significantly by platform. +Windows DX injection is the most reliable for full-screen games. + +--- + +## Game integrations + +For games that support it, the overlay can receive additional data: + +### Positional voice (Phase 4) + +Games that expose player position data can send it to VNOX, +enabling positional audio — players hear each other based on +in-game distance and direction. + +Integration methods: + +| Game / Engine | Method | +|---------------|--------| +| Minecraft | Fabric/Forge mod that sends position via local socket | +| Source Engine | Client plugin / VScript | +| Unreal Engine | Plugin exposing position to named pipe | +| Unity | SDK that writes position to shared memory | + +Protocol for positional data is TBD (Phase 4 design). + +### Speaking indicators in-game + +Some games support custom HUD elements. Where possible, the overlay +will render directly within the game's UI rather than as an external window. + +--- + +## Configuration + +```toml +[overlay] +enabled = true + +# Overlay position on screen +position = "top-right" # top-left | top-right | bottom-left | bottom-right + +# Opacity (0.0–1.0) +opacity = 0.85 + +# Show latency +show_latency = false + +# Hotkey to toggle overlay visibility +toggle_hotkey = "ctrl+shift+o" +``` + +--- + +## Hotkeys + +All hotkeys are global (work even when VNOX window is not focused). + +| Action | Default | +|--------|---------| +| Push-to-talk | `mouse4` | +| Mute toggle | `ctrl+m` | +| Deafen toggle | `ctrl+d` | +| Toggle overlay | `ctrl+shift+o` | + +Configurable in Settings → Keybinds.