Initial commit: split VNOX monorepo into VNOX-Server
Unified server binary (vnox-serverd) combining: - Gateway (TCP, auth, channels, sessions, SQLite) - Voice-node (UDP relay, Opus, jitter buffer) Standalone gateway (vnox-gateway) and voice-node (vnox-voice-node) preserved as sub-crates.
This commit is contained in:
commit
e751e0bf5b
94 changed files with 9771 additions and 0 deletions
251
gateway/src/handler/channel/create.rs
Normal file
251
gateway/src/handler/channel/create.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::{
|
||||
domain::{channels, session},
|
||||
net::{
|
||||
io,
|
||||
state::{BroadcastMsg, State},
|
||||
},
|
||||
proto::{
|
||||
ChannelCreatePayload, ChannelDeletePayload, ChannelListItem, ChannelListPayload,
|
||||
ChannelStatePayload, PacketId, SessionCrypto, encode_packet, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
/// Handle a ChannelCreate request — register a new channel in the store and
|
||||
/// broadcast the new ChannelState to all sessions so their sidebars update.
|
||||
pub async fn handle_channel_create(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: ChannelCreatePayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// Validate kind.
|
||||
let kind = match req.kind.as_str() {
|
||||
"text" => channels::ChannelKind::Text,
|
||||
"voice" => channels::ChannelKind::Voice,
|
||||
other => {
|
||||
warn!(
|
||||
"channel_create from {}: invalid kind '{other}'",
|
||||
sess.nickname
|
||||
);
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&crate::proto::ErrorPayload {
|
||||
code: crate::proto::ErrorCode::InvalidPacket as u32,
|
||||
message: format!("invalid channel kind: {other}"),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let channel_id = req.channel_id.trim().to_string();
|
||||
let channel_name = if req.channel_name.trim().is_empty() {
|
||||
channel_id.clone()
|
||||
} else {
|
||||
req.channel_name.trim().to_string()
|
||||
};
|
||||
|
||||
if channel_id.is_empty() {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&crate::proto::ErrorPayload {
|
||||
code: crate::proto::ErrorCode::InvalidPacket as u32,
|
||||
message: "channel_id is required".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Limit channel-create rate (reuse the per-session token bucket — 1 token
|
||||
// means 1 channel-create per rate window).
|
||||
if !state.rate_limiter.try_consume(session_id) {
|
||||
state.metrics.inc(&state.metrics.rate_limited_events);
|
||||
warn!("rate-limited channel_create from {}", sess.nickname);
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&crate::proto::ErrorPayload {
|
||||
code: crate::proto::ErrorCode::RateLimited as u32,
|
||||
message: "slow down — too many channel operations".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let created = channels::create(&state.channels, &channel_id, &channel_name, kind.clone()).await;
|
||||
|
||||
if !created {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&crate::proto::ErrorPayload {
|
||||
code: crate::proto::ErrorCode::ChannelNotFound as u32,
|
||||
message: "channel already exists".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"channel_create: {} created '{}' ({}) by {}",
|
||||
channel_id,
|
||||
channel_name,
|
||||
kind.as_str(),
|
||||
sess.nickname
|
||||
);
|
||||
|
||||
// Reply to creator with ChannelState (no members yet).
|
||||
let sp = ChannelStatePayload {
|
||||
channel_id: channel_id.clone(),
|
||||
channel_name: channel_name.clone(),
|
||||
kind: kind.as_str().into(),
|
||||
members: Vec::new(),
|
||||
voice_endpoint: state.config.voice.bind.clone(),
|
||||
};
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::ChannelState,
|
||||
seq,
|
||||
&to_payload(&sp),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Broadcast a ChannelCreate event to all other sessions so their sidebars update.
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: None,
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(
|
||||
PacketId::ChannelCreate,
|
||||
0,
|
||||
&to_payload(&ChannelCreatePayload {
|
||||
channel_id: channel_id.clone(),
|
||||
channel_name: channel_name.clone(),
|
||||
kind: kind.as_str().into(),
|
||||
guild_id: req.guild_id.clone(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a ChannelDelete request — remove the channel from the store and
|
||||
/// broadcast the deletion to all sessions.
|
||||
pub async fn handle_channel_delete(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: ChannelDeletePayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// Protect default channels from deletion.
|
||||
if req.channel_id == "general" || req.channel_id == "voice" {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&crate::proto::ErrorPayload {
|
||||
code: crate::proto::ErrorCode::PermissionDenied as u32,
|
||||
message: "cannot delete default channels".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let existed = channels::delete(&state.channels, &req.channel_id).await;
|
||||
if !existed {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&crate::proto::ErrorPayload {
|
||||
code: crate::proto::ErrorCode::ChannelNotFound as u32,
|
||||
message: "channel not found".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!(
|
||||
"channel_delete: '{}' removed by {}",
|
||||
req.channel_id, sess.nickname
|
||||
);
|
||||
|
||||
// Broadcast deletion to all sessions.
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: None,
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(
|
||||
PacketId::ChannelDelete,
|
||||
0,
|
||||
&to_payload(&ChannelDeletePayload {
|
||||
channel_id: req.channel_id.clone(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a ChannelList request — reply with all known channels.
|
||||
pub async fn handle_channel_list(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let _sess = session::get(&state.sessions, session_id).await;
|
||||
let channels = channels::list(&state.channels).await;
|
||||
let items: Vec<ChannelListItem> = channels
|
||||
.iter()
|
||||
.map(|c| ChannelListItem {
|
||||
channel_id: c.id.clone(),
|
||||
channel_name: c.name.clone(),
|
||||
kind: c.kind.as_str().into(),
|
||||
})
|
||||
.collect();
|
||||
let p = ChannelListPayload { channels: items };
|
||||
io::send_encrypted(stream, PacketId::ChannelList, seq, &to_payload(&p), crypto).await?;
|
||||
Ok(())
|
||||
}
|
||||
115
gateway/src/handler/channel/join.rs
Normal file
115
gateway/src/handler/channel/join.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{
|
||||
domain::{channels, session},
|
||||
net::{
|
||||
io,
|
||||
state::{BroadcastMsg, State},
|
||||
},
|
||||
proto::{
|
||||
self, ChannelStatePayload, ChatHistoryPayload, MemberInfo, PacketId, SessionCrypto,
|
||||
UserJoinPayload, encode_packet, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{broadcast_leave, set_channel};
|
||||
|
||||
const HISTORY_LIMIT: i64 = 50;
|
||||
|
||||
pub async fn join(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
channel_id: &str,
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let prev_channel = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.and_then(|s| s.channel_id);
|
||||
|
||||
if prev_channel.as_deref() == Some(channel_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(prev) = prev_channel {
|
||||
channels::leave(&state.channels, &prev, session_id).await;
|
||||
broadcast_leave(state, &prev, session_id).await;
|
||||
}
|
||||
|
||||
let ch = match channels::get_channel(&state.channels, channel_id).await {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&proto::ErrorPayload {
|
||||
code: proto::ErrorCode::ChannelNotFound as u32,
|
||||
message: "not found".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
channels::join(&state.channels, channel_id, session_id).await;
|
||||
set_channel(state, session_id, Some(channel_id.into())).await;
|
||||
|
||||
let mut members = Vec::new();
|
||||
for sid in channels::members(&state.channels, channel_id).await {
|
||||
if let Some(s) = session::get(&state.sessions, &sid).await {
|
||||
members.push(MemberInfo {
|
||||
user_id: s.user_id.clone(),
|
||||
nickname: s.nickname.clone(),
|
||||
in_voice: ch.kind == channels::ChannelKind::Voice,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let sp = ChannelStatePayload {
|
||||
channel_id: ch.id.clone(),
|
||||
channel_name: ch.name.clone(),
|
||||
kind: ch.kind.as_str().into(),
|
||||
members,
|
||||
voice_endpoint: state.config.voice.bind.clone(),
|
||||
};
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::ChannelState,
|
||||
seq,
|
||||
&to_payload(&sp),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let history = state.storage.get_history(channel_id, HISTORY_LIMIT).await?;
|
||||
if !history.is_empty() {
|
||||
let hp = ChatHistoryPayload {
|
||||
channel_id: channel_id.into(),
|
||||
messages: history,
|
||||
};
|
||||
io::send_encrypted(stream, PacketId::ChatHistory, seq, &to_payload(&hp), crypto).await?;
|
||||
}
|
||||
|
||||
if let Some(sess) = session::get(&state.sessions, session_id).await {
|
||||
let jp = UserJoinPayload {
|
||||
channel_id: channel_id.into(),
|
||||
user_id: sess.user_id.clone(),
|
||||
nickname: sess.nickname.clone(),
|
||||
};
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(channel_id.into()),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::UserJoin, 0, &to_payload(&jp)),
|
||||
});
|
||||
}
|
||||
|
||||
info!("session {} joined {channel_id}", &session_id[..8]);
|
||||
Ok(())
|
||||
}
|
||||
22
gateway/src/handler/channel/leave.rs
Normal file
22
gateway/src/handler/channel/leave.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::info;
|
||||
|
||||
use crate::{domain::channels, net::state::State, proto::SessionCrypto};
|
||||
|
||||
use super::{broadcast_leave, set_channel};
|
||||
|
||||
pub async fn leave(
|
||||
_stream: &mut TcpStream,
|
||||
_seq: &mut u32,
|
||||
session_id: &str,
|
||||
channel_id: &str,
|
||||
_crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
channels::leave(&state.channels, channel_id, session_id).await;
|
||||
set_channel(state, session_id, None).await;
|
||||
broadcast_leave(state, channel_id, session_id).await;
|
||||
info!("session {} left {channel_id}", &session_id[..8]);
|
||||
Ok(())
|
||||
}
|
||||
33
gateway/src/handler/channel/mod.rs
Normal file
33
gateway/src/handler/channel/mod.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
pub mod create;
|
||||
pub mod join;
|
||||
pub mod leave;
|
||||
|
||||
pub use create::{handle_channel_create, handle_channel_delete, handle_channel_list};
|
||||
pub use join::join;
|
||||
pub use leave::leave;
|
||||
|
||||
use crate::{
|
||||
net::state::{BroadcastMsg, State},
|
||||
proto::{PacketId, UserLeavePayload, encode_packet, to_payload},
|
||||
};
|
||||
|
||||
pub async fn broadcast_leave(state: &State, channel_id: &str, session_id: &str) {
|
||||
if let Some(sess) = crate::domain::session::get(&state.sessions, session_id).await {
|
||||
let p = UserLeavePayload {
|
||||
channel_id: channel_id.into(),
|
||||
user_id: sess.user_id.clone(),
|
||||
};
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(channel_id.into()),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::UserLeave, 0, &to_payload(&p)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_channel(state: &State, session_id: &str, ch: Option<String>) {
|
||||
if let Some(s) = state.sessions.write().await.get_mut(session_id) {
|
||||
s.channel_id = ch;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue