14 KiB
14 KiB
Changelog
All notable changes to this project are documented here. Format follows Keep a Changelog.
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 onmember_roles/roles).Storage::assign_role(guild_id, user_id, role_id)— idempotent insert intomember_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_ROLESpermission (or owner bypass) and append to the audit log.
- Role assign/unassign require
- 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
GuildMemberKickand 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_createnow 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_totalcounter.
- When exceeded, replies
- 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 callssave()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
UiStatefields: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
ChannelCreatepacket (0x0033) — register a new channel in the gateway's channel store.- Validates
kind("text" or "voice"); rejects emptychannel_id. - Returns
ChannelStateto the creator and broadcastsChannelCreateto all other sessions. - Returns
ErrorPayload{ ChannelNotFound }(reused code) if a channel with the same id exists.
- Validates
ChannelDeletepacket (0x0034) — remove a channel from the store.- Protects default channels ("general", "voice") with
PermissionDenied. - Broadcasts
ChannelDeleteto all sessions; client removes it from sidebar and clears cached messages.
- Protects default channels ("general", "voice") with
ChannelListpacket (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::listops on the gateway. - Client wiring:
NetCommand::{ChannelCreate, ChannelDelete, ChannelList}+NetEvent::{ChannelCreated, ChannelDeleted, ChannelListEvent}+ dispatch + UI handling. - Channel create popup now sends
ChannelCreateto 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
REMOVEto confirm; callsidentity::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
UiStatefields:vault_set_open,vault_remove_open,vault_passphrase_input,vault_passphrase_confirm,vault_error.
Phase 2 audit log viewer
GuildAuditLogFetchpacket (0x010C) — admin-only endpoint; requiresMANAGE_GUILDpermission (or owner).- Returns last 50 audit log entries (clamped 1..=200) via
GuildAuditLogPayload.
- Returns last 50 audit log entries (clamped 1..=200) via
- 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_EVENTfrom 0x0155 to 0x0158 to avoid collision with serverBlockUser=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) andmessage_rate_burst(default 10). - When a session exceeds the limit, server replies
ErrorPayload{ code: RateLimited }and bumpsrate_limited_events_totalcounter. - Idle buckets are pruned on session disconnect via
RateLimiter::remove().
- Configurable via
- Prometheus metrics module (
gateway/src/admin/metrics.rs) with lock-free atomic counters shared across all session tasks. ensure_columnidempotent migration helper — adds new SQLite columns on existing databases without dropping data. Used to addmessages.reply_tofor 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.jsonis auto-migrated toidentity.vault.jsonon 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.
- Vault format:
Phase 1.3 chat UX
- Message context menu (right-click on any message row):
- Quick-reactions row (👍 ❤️ 😂 🎉 👀 🤔) — toggles reaction on click.
- Reply — sets
replying_tostate, 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 TEXTcolumn (added viaensure_columnmigration). NetCommand::SendChat { reply_to: Option<String> }carries the reference through the network layer.- UI: italic "↳ alice: " preview above the message body when
reply_tois set.
- Wire protocol:
- 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
PresenceUpdatewithactivity_type/activity_text/custom_statusfields. - 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,UnblockedUserdispatched tosocial::handle. blocked_users: Vec<String>field onUiState.- 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.
- Client:
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()acceptssender_pubkey: &[u8; 32].voice::spawn_recv()parses sender_id and emits hex-encodedsender_idinNetEvent::VoicePacket.- Legacy packets (no sender_id) gracefully handled — empty
sender_idstring. - Unit test
build_packet_header_layoutupdated to verify sender_id roundtrip.
- Per-user speaking indicator in voice panel:
last_remote_speaker_idfield onUiState, updated on every received voice packet.- Voice panel matches
last_remote_speaker_idagainst 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.channelslist 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/Deletedwere silently dropped — now routed tosocial::handle. FriendAcceptedhandler (from previous commit): now usesuser_idand adds the friend to the list.- PID collision:
PID_FRIEND_EVENTwas 0x0155, conflicting with serverBlockUser=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 serializesactivity_type/activity_text/custom_statuscorrectly, matching the gateway'sPresenceUpdatePayload.
Documentation
docs/00-status.mdanddocs/06-roadmap.mdupdated in previous commit — Phase 1.1/1.2/1.3 marked DONE where implemented.CHANGELOG.mdexpanded 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.