202 lines
14 KiB
Markdown
202 lines
14 KiB
Markdown
# 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-<nickname>-<pubkey8>.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<String>` (optional message_id).
|
|
- DB schema: `messages.reply_to TEXT` column (added via `ensure_column` migration).
|
|
- `NetCommand::SendChat { reply_to: Option<String> }` carries the reference through the network layer.
|
|
- UI: italic "↳ alice: <snippet>" 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<String>` 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
|