Initial commit: split VNOX monorepo into VNOX-Client

Desktop client with egui + wgpu UI, audio pipeline (cpal/opus/rodio),
networking (LNEx protocol), and identity management with encrypted vault.

Jitter buffer module extracted from voice-node for standalone operation.
This commit is contained in:
loki5512344 2026-07-09 11:52:57 +02:00
commit bf0b237afd
112 changed files with 19113 additions and 0 deletions

View file

@ -0,0 +1,162 @@
use anyhow::{Result, anyhow};
use serde_json;
use tokio::sync::mpsc;
use super::super::super::payloads::*;
use super::super::super::types::NetEvent;
use super::super::super::types::{ChannelListItem as InfoChannelListItem, ChatMsg, MemberInfo};
pub async fn handle(pid: u16, payload: &[u8], tx: &mpsc::Sender<NetEvent>) -> Result<()> {
match pid {
PID_CHANNEL_STATE => {
let p: ChannelStatePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ChannelState {
channel_id: p.channel_id,
channel_name: p.channel_name,
kind: p.kind,
members: p
.members
.into_iter()
.map(|m| MemberInfo {
user_id: m.user_id,
nickname: m.nickname,
in_voice: m.in_voice,
})
.collect(),
voice_endpoint: p.voice_endpoint,
})
.await;
}
PID_CHANNEL_CREATE => {
let p: ChannelCreatePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ChannelCreated {
channel_id: p.channel_id,
channel_name: p.channel_name,
kind: p.kind,
})
.await;
}
PID_CHANNEL_DELETE => {
let p: ChannelDeletePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ChannelDeleted {
channel_id: p.channel_id,
})
.await;
}
PID_CHANNEL_LIST => {
let p: ChannelListPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ChannelListEvent {
channels: p
.channels
.into_iter()
.map(|c| InfoChannelListItem {
channel_id: c.channel_id,
channel_name: c.channel_name,
kind: c.kind,
})
.collect(),
})
.await;
}
PID_CHAT_MESSAGE => {
let p: ChatMessagePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ChatMessage {
message_id: p.message_id,
channel_id: p.channel_id,
sender_id: p.sender_id.clone(),
nickname: p.sender_id,
content: p.content,
timestamp: p.timestamp,
reply_to: p.reply_to,
})
.await;
}
PID_CHAT_HISTORY => {
let p: ChatHistoryPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ChatHistory {
channel_id: p.channel_id,
messages: p
.messages
.into_iter()
.map(|m| ChatMsg {
message_id: m.message_id,
sender_id: m.sender_id,
content: m.content,
timestamp: m.timestamp,
reply_to: m.reply_to,
})
.collect(),
})
.await;
}
PID_USER_JOIN => {
let p: UserJoinPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::UserJoin {
channel_id: p.channel_id,
user_id: p.user_id,
nickname: p.nickname,
})
.await;
}
PID_USER_LEAVE => {
let p: UserLeavePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::UserLeave {
channel_id: p.channel_id,
user_id: p.user_id,
})
.await;
}
PID_PONG => {
let p: PongPayload = serde_json::from_slice(payload)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
let rtt_ms = (now - p.timestamp).max(0) as u32;
let _ = tx.send(NetEvent::LatencyUpdate { rtt_ms }).await;
}
PID_READ_RECEIPT_BROADCAST => {
let p: serde_json::Value = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ReadReceiptBroadcast {
channel_id: p["channel_id"].as_str().unwrap_or_default().into(),
user_id: p["user_id"].as_str().unwrap_or_default().into(),
last_read_message_id: p["last_read_message_id"]
.as_str()
.unwrap_or_default()
.into(),
})
.await;
}
PID_ERROR => {
let p: ErrorPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::Error {
code: p.code,
message: p.message,
})
.await;
}
PID_DISCONNECT => return Err(anyhow!("server disconnected")),
PID_TYPING_START => {
let p: serde_json::Value = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::TypingStart {
user_id: p["user_id"].as_str().unwrap_or_default().into(),
nickname: p["nickname"].as_str().unwrap_or_default().into(),
channel_id: p["channel_id"].as_str().unwrap_or_default().into(),
})
.await;
}
_ => {}
}
Ok(())
}

View file

@ -0,0 +1,152 @@
use anyhow::Result;
use serde_json;
use tokio::sync::mpsc;
use tracing::debug;
use super::super::super::payloads::*;
use super::super::super::types::*;
pub async fn handle(pid: u16, payload: &[u8], tx: &mpsc::Sender<NetEvent>) -> Result<()> {
match pid {
PID_DM_START => {
let p: DmStartResponsePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::DmStart {
dm_id: p.dm_id,
other_user_id: p.other_user_id,
other_nickname: p.other_nickname,
messages: p
.messages
.into_iter()
.map(|m| ChatMsg {
message_id: String::new(),
sender_id: m.sender_id,
content: m.content,
timestamp: m.timestamp,
reply_to: None,
})
.collect(),
unread_count: p.unread_count,
})
.await;
}
PID_DM_MESSAGE => {
let p: DmMessagePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::DmMessage {
dm_id: p.dm_id,
sender_id: p.sender_id,
content: p.content,
timestamp: p.timestamp,
})
.await;
}
PID_DM_HISTORY => {
let p: DmHistoryPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::DmHistory {
dm_id: p.dm_id,
messages: p
.messages
.into_iter()
.map(|m| ChatMsg {
message_id: String::new(),
sender_id: m.sender_id,
content: m.content,
timestamp: m.timestamp,
reply_to: None,
})
.collect(),
})
.await;
}
PID_FRIEND_LIST => {
let p: FriendListPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::FriendList {
friends: p
.friends
.into_iter()
.map(|f| FriendInfo {
user_id: f.user_id,
nickname: f.nickname,
status: f.status,
since: f.since,
})
.collect(),
})
.await;
}
PID_MESSAGE_REACTION_ADD => {
let p: ReactionPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ReactionAdded {
channel_id: p.channel_id,
message_id: p.message_id,
user_id: p.user_id,
emoji: p.emoji,
})
.await;
}
PID_MESSAGE_REACTION_REMOVE => {
let p: ReactionPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::ReactionRemoved {
channel_id: p.channel_id,
message_id: p.message_id,
user_id: p.user_id,
emoji: p.emoji,
})
.await;
}
PID_MESSAGE_EDIT => {
let p: ChatMessagePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::MessageEdited {
channel_id: p.channel_id,
message_id: p.message_id,
content: p.content,
edited: p.edited,
})
.await;
}
PID_MESSAGE_DELETE => {
let p: MessageDeletePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::MessageDeleted {
channel_id: p.channel_id,
message_id: p.message_id,
})
.await;
}
PID_FRIEND_EVENT => {
let p: FriendEventPayload = serde_json::from_slice(payload)?;
match p.event_type.as_str() {
"request" => {
let _ = tx
.send(NetEvent::FriendRequested {
user_id: p.user_id,
nickname: p.nickname.unwrap_or_default(),
})
.await;
}
"accepted" => {
let _ = tx
.send(NetEvent::FriendAccepted {
user_id: p.user_id,
nickname: p.nickname.unwrap_or_default(),
})
.await;
}
"removed" => {
let _ = tx
.send(NetEvent::FriendRemoved { user_id: p.user_id })
.await;
}
_ => debug!("unhandled friend event: {}", p.event_type),
}
}
_ => {}
}
Ok(())
}

View file

@ -0,0 +1,53 @@
use anyhow::Result;
use tokio::sync::mpsc;
use tracing::debug;
use super::super::payloads::*;
use super::super::types::*;
mod channel;
mod dm;
mod social;
pub async fn incoming(pid: u16, payload: &[u8], tx: &mpsc::Sender<NetEvent>) -> Result<()> {
match pid {
PID_CHANNEL_STATE
| PID_CHAT_MESSAGE
| PID_CHAT_HISTORY
| PID_USER_JOIN
| PID_USER_LEAVE
| PID_PONG
| PID_READ_RECEIPT_BROADCAST
| PID_ERROR
| PID_DISCONNECT
| PID_TYPING_START => channel::handle(pid, payload, tx).await?,
PID_DM_START
| PID_DM_MESSAGE
| PID_DM_HISTORY
| PID_FRIEND_LIST
| PID_MESSAGE_REACTION_ADD
| PID_MESSAGE_REACTION_REMOVE
| PID_MESSAGE_EDIT
| PID_MESSAGE_DELETE
| PID_FRIEND_EVENT => dm::handle(pid, payload, tx).await?,
PID_GUILD_LIST
| PID_GUILD_CREATE
| PID_GUILD_DELETE
| PID_GUILD_MEMBER_JOIN
| PID_GUILD_MEMBER_LEAVE
| PID_GUILD_MEMBER_KICK
| PID_INVITE_CREATE
| PID_INVITE_ACCEPT
| PID_INVITE_DELETE
| PID_ROLE_CREATE
| PID_ROLE_DELETE
| PID_USER_ROLE_UPDATE
| PID_PRESENCE_SYNC
| PID_PRESENCE_EVENT => social::handle(pid, payload, tx).await?,
other => debug!("unhandled 0x{:04X}", other),
}
Ok(())
}

View file

@ -0,0 +1,232 @@
use anyhow::Result;
use serde_json;
use tokio::sync::mpsc;
use super::super::super::payloads::*;
use super::super::super::types::*;
pub async fn handle(pid: u16, payload: &[u8], tx: &mpsc::Sender<NetEvent>) -> Result<()> {
match pid {
PID_GUILD_LIST => {
let p: GuildListPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildList {
guilds: p
.guilds
.into_iter()
.map(|g| GuildInfo {
guild_id: g.guild_id,
name: g.name,
owner_id: g.owner_id,
icon_url: g.icon_url,
member_count: g.member_count,
})
.collect(),
})
.await;
}
PID_GUILD_CREATE => {
let p: GuildInfoPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildCreated {
guild_id: p.guild_id,
name: p.name,
owner_id: p.owner_id,
})
.await;
}
PID_GUILD_DELETE => {
let p: GuildDeletePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildDeleted {
guild_id: p.guild_id,
})
.await;
}
PID_GUILD_MEMBER_JOIN => {
let p: serde_json::Value = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildMemberJoined {
guild_id: p["guild_id"].as_str().unwrap_or_default().into(),
user_id: p["user_id"].as_str().unwrap_or_default().into(),
nickname: p["nickname"].as_str().unwrap_or_default().into(),
})
.await;
}
PID_GUILD_MEMBER_LEAVE => {
let p: serde_json::Value = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildMemberLeft {
guild_id: p["guild_id"].as_str().unwrap_or_default().into(),
user_id: p["user_id"].as_str().unwrap_or_default().into(),
})
.await;
}
PID_USER_ROLE_UPDATE => {
let p: UserRoleUpdatePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::UserColor {
user_id: p.user_id,
color: p.color,
})
.await;
}
PID_PRESENCE_SYNC => {
let p: PresenceSyncPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::PresenceSync {
presences: p
.presences
.into_iter()
.map(|p| PresenceInfo {
user_id: p.user_id,
status: p.status,
custom_status: p.custom_status,
activity: p.activity,
activity_text: p.activity_text,
})
.collect(),
})
.await;
}
PID_PRESENCE_EVENT => {
let p: PresenceEventPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::PresenceUpdated {
user_id: p.user_id,
status: p.status,
custom_status: p.custom_status,
activity: p.activity,
})
.await;
}
PID_GUILD_MEMBER_KICK => {
let p: serde_json::Value = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildMemberKicked {
guild_id: p["guild_id"].as_str().unwrap_or_default().into(),
user_id: p["user_id"].as_str().unwrap_or_default().into(),
})
.await;
}
PID_INVITE_CREATE => {
let p: InviteInfoPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::InviteCreated {
id: p.id,
guild_id: p.guild_id,
guild_name: p.guild_name,
code: p.code,
creator_id: p.creator_id,
max_uses: p.max_uses,
uses: p.uses,
expires_at: p.expires_at,
created_at: p.created_at,
})
.await;
}
PID_INVITE_ACCEPT => {
let p: serde_json::Value = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::InviteAccepted {
guild_id: p["guild_id"].as_str().unwrap_or_default().into(),
guild_name: p["guild_name"].as_str().unwrap_or_default().into(),
})
.await;
}
PID_INVITE_DELETE => {
let p: InviteDeletePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::InviteDeleted {
guild_id: p.guild_id,
invite_id: p.invite_id,
})
.await;
}
PID_ROLE_CREATE => {
let p: serde_json::Value = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::RoleCreated {
role_id: p["id"].as_str().unwrap_or_default().into(),
guild_id: p["guild_id"].as_str().unwrap_or_default().into(),
name: p["name"].as_str().unwrap_or_default().into(),
})
.await;
}
PID_ROLE_DELETE => {
let p: RoleDeletePayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::RoleDeleted {
role_id: p.role_id,
guild_id: p.guild_id,
})
.await;
}
PID_GUILD_AUDIT_LOG => {
let p: GuildAuditLogPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildAuditLog {
guild_id: p.guild_id,
entries: p.entries,
})
.await;
}
PID_GUILD_MEMBER_LIST => {
let p: GuildMemberListPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildMemberList {
guild_id: p.guild_id,
members: p.members,
})
.await;
}
PID_GUILD_ROLE_LIST => {
let p: GuildRoleListPayload = serde_json::from_slice(payload)?;
let _ = tx
.send(NetEvent::GuildRoleList {
guild_id: p.guild_id,
roles: p.roles,
})
.await;
}
PID_BLOCK_LIST => {
// Server may send {"blocked": [...]} — try to parse properly first,
// fall back to JSON value extraction.
let blocked: Vec<String> = match serde_json::from_slice::<BlockListPayload>(payload) {
Ok(p) => p.blocked,
Err(_) => {
let v: serde_json::Value = serde_json::from_slice(payload)?;
v["blocked"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
};
let _ = tx.send(NetEvent::BlockList { blocked }).await;
}
PID_BLOCK_USER => {
let v: serde_json::Value = serde_json::from_slice(payload).unwrap_or_default();
let user_id = v["user_id"]
.as_str()
.or_else(|| v["blocked"].as_str())
.unwrap_or_default()
.to_string();
let _ = tx.send(NetEvent::BlockedUser { user_id }).await;
}
PID_UNBLOCK_USER => {
let v: serde_json::Value = serde_json::from_slice(payload).unwrap_or_default();
let user_id = v["user_id"]
.as_str()
.or_else(|| v["unblocked"].as_str())
.unwrap_or_default()
.to_string();
let _ = tx.send(NetEvent::UnblockedUser { user_id }).await;
}
_ => {}
}
Ok(())
}

1
src/net/internal/mod.rs Normal file
View file

@ -0,0 +1 @@
pub(super) mod dispatch;