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;
|
||||
}
|
||||
}
|
||||
66
gateway/src/handler/content/chat.rs
Normal file
66
gateway/src/handler/content/chat.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
use anyhow::Result;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use crate::{
|
||||
net::state::{BroadcastMsg, State},
|
||||
proto::{ChatMessagePayload, ErrorCode, PacketId, encode_packet, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle(session_id: &str, mut msg: ChatMessagePayload, state: &State) -> Result<()> {
|
||||
let sess = match crate::domain::session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if sess.channel_id.as_deref() != Some(&msg.channel_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Rate limit check (token bucket per session).
|
||||
if !state.rate_limiter.try_consume(session_id) {
|
||||
state.metrics.inc(&state.metrics.rate_limited_events);
|
||||
warn!("rate-limited session {session_id}");
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: None,
|
||||
exclude_session: None,
|
||||
target_session_id: Some(session_id.to_string()),
|
||||
data: encode_packet(
|
||||
PacketId::Error,
|
||||
0,
|
||||
&to_payload(&crate::proto::ErrorPayload {
|
||||
code: ErrorCode::RateLimited as u32,
|
||||
message: "you are sending messages too quickly".into(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
msg.sender_id = sess.user_id.clone();
|
||||
if msg.timestamp == 0 {
|
||||
msg.timestamp = now_ms();
|
||||
}
|
||||
|
||||
state.storage.save_message(&msg).await?;
|
||||
state.metrics.inc(&state.metrics.messages_sent);
|
||||
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(msg.channel_id.clone()),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::ChatMessage, 0, &to_payload(&msg)),
|
||||
});
|
||||
|
||||
debug!(
|
||||
"chat {} → {}: {}",
|
||||
sess.nickname, msg.channel_id, msg.content
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn now_ms() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64
|
||||
}
|
||||
93
gateway/src/handler/content/message_edit.rs
Normal file
93
gateway/src/handler/content/message_edit.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use anyhow::Result;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::state::{BroadcastMsg, State},
|
||||
proto::{ChatMessagePayload, MessageDeletePayload, PacketId, encode_packet, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_message_edit(session_id: &str, payload: &[u8], state: &State) -> Result<()> {
|
||||
let edit: crate::proto::MessageEditPayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if sess.channel_id.as_deref() != Some(&edit.channel_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Verify sender owns the message
|
||||
let sender = state.storage.get_message_sender(&edit.message_id).await?;
|
||||
if sender.as_deref() != Some(&sess.user_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let msg = state.storage.get_message(&edit.message_id).await?;
|
||||
let msg = match msg {
|
||||
Some(m) => m,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
state
|
||||
.storage
|
||||
.edit_message(&edit.message_id, &edit.content)
|
||||
.await?;
|
||||
|
||||
let broadcast_msg = ChatMessagePayload {
|
||||
message_id: edit.message_id.clone(),
|
||||
channel_id: edit.channel_id.clone(),
|
||||
sender_id: sess.user_id.clone(),
|
||||
content: edit.content,
|
||||
timestamp: msg.timestamp,
|
||||
edited: true,
|
||||
reply_to: msg.reply_to.clone(),
|
||||
};
|
||||
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(edit.channel_id.clone()),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::MessageEdit, 0, &to_payload(&broadcast_msg)),
|
||||
});
|
||||
|
||||
debug!(
|
||||
"edit {} {}:{}",
|
||||
sess.nickname, edit.channel_id, edit.message_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_message_delete(session_id: &str, payload: &[u8], state: &State) -> Result<()> {
|
||||
let delete: MessageDeletePayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if sess.channel_id.as_deref() != Some(&delete.channel_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Verify sender owns the message
|
||||
let sender = state.storage.get_message_sender(&delete.message_id).await?;
|
||||
if sender.as_deref() != Some(&sess.user_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state.storage.delete_message(&delete.message_id).await?;
|
||||
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(delete.channel_id.clone()),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::MessageDelete, 0, &to_payload(&delete)),
|
||||
});
|
||||
|
||||
debug!(
|
||||
"delete {} {}:{}",
|
||||
sess.nickname, delete.channel_id, delete.message_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
9
gateway/src/handler/content/mod.rs
Normal file
9
gateway/src/handler/content/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
pub mod chat;
|
||||
pub mod message_edit;
|
||||
pub mod presence;
|
||||
pub mod reaction;
|
||||
pub mod read_receipt;
|
||||
pub mod typing;
|
||||
|
||||
pub use read_receipt::handle_read_receipt;
|
||||
pub use typing::handle_typing_start;
|
||||
84
gateway/src/handler/content/presence.rs
Normal file
84
gateway/src/handler/content/presence.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{
|
||||
io,
|
||||
state::{BroadcastMsg, State},
|
||||
},
|
||||
proto::{
|
||||
PacketId, PresenceEventPayload, PresenceInfo, PresenceSyncPayload, PresenceUpdatePayload,
|
||||
SessionCrypto, encode_packet, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn handle_presence_update(
|
||||
_stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
_crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: PresenceUpdatePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let info = PresenceInfo {
|
||||
user_id: sess.user_id.clone(),
|
||||
nickname: sess.nickname.clone(),
|
||||
status: req.status.clone(),
|
||||
activity_type: req.activity_type,
|
||||
activity_text: req.activity_text,
|
||||
};
|
||||
|
||||
state
|
||||
.presences
|
||||
.write()
|
||||
.await
|
||||
.insert(sess.user_id.clone(), info.clone());
|
||||
|
||||
// Broadcast to everyone (friends/guild-mates will filter client-side for now)
|
||||
let event = PresenceEventPayload {
|
||||
user_id: info.user_id,
|
||||
nickname: info.nickname,
|
||||
status: info.status,
|
||||
activity_type: info.activity_type,
|
||||
activity_text: info.activity_text,
|
||||
};
|
||||
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: None,
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::PresenceEvent, *seq, &to_payload(&event)),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_presence_sync(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let _sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let presences: Vec<PresenceInfo> = state.presences.read().await.values().cloned().collect();
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::PresenceSync,
|
||||
seq,
|
||||
&to_payload(&PresenceSyncPayload { presences }),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
92
gateway/src/handler/content/reaction.rs
Normal file
92
gateway/src/handler/content/reaction.rs
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
use anyhow::Result;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::state::{BroadcastMsg, State},
|
||||
proto::{PacketId, ReactionPayload, encode_packet, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_reaction_add(
|
||||
session_id: &str,
|
||||
mut payload: ReactionPayload,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if sess.channel_id.as_deref() != Some(&payload.channel_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Verify message exists in the channel
|
||||
let msg = state.storage.get_message(&payload.message_id).await?;
|
||||
if msg.is_none() || msg.as_ref().unwrap().channel_id != payload.channel_id {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
payload.user_id = sess.user_id.clone();
|
||||
state
|
||||
.storage
|
||||
.add_reaction(&payload.message_id, &sess.user_id, &payload.emoji)
|
||||
.await?;
|
||||
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(payload.channel_id.clone()),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::MessageReactionAdd, 0, &to_payload(&payload)),
|
||||
});
|
||||
|
||||
debug!(
|
||||
"reaction {} +{} on {}",
|
||||
sess.nickname, payload.emoji, payload.message_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_reaction_remove(
|
||||
session_id: &str,
|
||||
mut payload: ReactionPayload,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if sess.channel_id.as_deref() != Some(&payload.channel_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
payload.user_id = sess.user_id.clone();
|
||||
|
||||
// Check user owns the reaction
|
||||
let has = state
|
||||
.storage
|
||||
.has_user_reacted(&payload.message_id, &sess.user_id, &payload.emoji)
|
||||
.await?;
|
||||
if !has {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state
|
||||
.storage
|
||||
.remove_reaction(&payload.message_id, &sess.user_id, &payload.emoji)
|
||||
.await?;
|
||||
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(payload.channel_id.clone()),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::MessageReactionRemove, 0, &to_payload(&payload)),
|
||||
});
|
||||
|
||||
debug!(
|
||||
"reaction {} -{} on {}",
|
||||
sess.nickname, payload.emoji, payload.message_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
46
gateway/src/handler/content/read_receipt.rs
Normal file
46
gateway/src/handler/content/read_receipt.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::state::{BroadcastMsg, State},
|
||||
proto::{PacketId, ReadReceiptPayload, encode_packet, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_read_receipt(
|
||||
_stream: &mut tokio::net::TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
_crypto: &crate::proto::SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: ReadReceiptPayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
state
|
||||
.storage
|
||||
.update_read_receipt(&req.channel_id, &sess.user_id, &req.last_read_message_id)
|
||||
.await?;
|
||||
|
||||
let broadcast_data = serde_json::json!({
|
||||
"channel_id": req.channel_id,
|
||||
"user_id": sess.user_id,
|
||||
"last_read_message_id": req.last_read_message_id,
|
||||
});
|
||||
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(req.channel_id),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(
|
||||
PacketId::ReadReceiptBroadcast,
|
||||
*seq,
|
||||
&to_payload(&broadcast_data),
|
||||
),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
30
gateway/src/handler/content/typing.rs
Normal file
30
gateway/src/handler/content/typing.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::state::{BroadcastMsg, State},
|
||||
proto::{PacketId, TypingStartPayload, encode_packet, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_typing_start(session_id: &str, payload: &[u8], state: &State) -> Result<()> {
|
||||
let req: TypingStartPayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let data = serde_json::json!({
|
||||
"user_id": sess.user_id,
|
||||
"nickname": sess.nickname,
|
||||
"channel_id": req.channel_id,
|
||||
});
|
||||
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: Some(req.channel_id),
|
||||
exclude_session: Some(session_id.into()),
|
||||
target_session_id: None,
|
||||
data: encode_packet(PacketId::TypingStart, 0, &to_payload(&data)),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
gateway/src/handler/deliver.rs
Normal file
39
gateway/src/handler/deliver.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{io, state::BroadcastMsg, state::State},
|
||||
proto::SessionCrypto,
|
||||
};
|
||||
|
||||
pub async fn deliver_encrypted(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
msg: &BroadcastMsg,
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
if msg.exclude_session.as_deref() == Some(session_id) {
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(ref target) = msg.target_session_id {
|
||||
if target != session_id {
|
||||
return Ok(());
|
||||
}
|
||||
io::deliver_encrypted(stream, crypto, seq, &msg.data).await?;
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(ref ch) = msg.channel_id {
|
||||
let in_ch = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.and_then(|s| s.channel_id)
|
||||
.as_deref()
|
||||
== Some(ch.as_str());
|
||||
if !in_ch {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
io::deliver_encrypted(stream, crypto, seq, &msg.data).await
|
||||
}
|
||||
46
gateway/src/handler/direct_message/history.rs
Normal file
46
gateway/src/handler/direct_message/history.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{io, state::State},
|
||||
proto::{DmHistoryPayload, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_dm_history(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: DmHistoryPayload = serde_json::from_slice(payload)?;
|
||||
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
let my_id = sess.user_id.clone();
|
||||
drop(sess);
|
||||
|
||||
let _other_id = state
|
||||
.storage
|
||||
.get_dm_user_id(&req.dm_id, &my_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("user not in DM"))?;
|
||||
|
||||
let limit = req.limit.unwrap_or(50);
|
||||
let messages = state
|
||||
.storage
|
||||
.get_dm_messages(&req.dm_id, limit, req.search_query.as_deref(), None)
|
||||
.await?;
|
||||
|
||||
let resp = DmHistoryPayload {
|
||||
dm_id: req.dm_id,
|
||||
messages,
|
||||
search_query: None,
|
||||
limit: None,
|
||||
};
|
||||
io::send_encrypted(stream, PacketId::DmHistory, seq, &to_payload(&resp), crypto).await?;
|
||||
Ok(())
|
||||
}
|
||||
7
gateway/src/handler/direct_message/mod.rs
Normal file
7
gateway/src/handler/direct_message/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
mod history;
|
||||
mod send;
|
||||
mod start;
|
||||
|
||||
pub use history::handle_dm_history;
|
||||
pub use send::handle_dm_message;
|
||||
pub use start::{handle_dm_read_ack, handle_dm_start};
|
||||
113
gateway/src/handler/direct_message/send.rs
Normal file
113
gateway/src/handler/direct_message/send.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{
|
||||
io,
|
||||
state::{BroadcastMsg, State},
|
||||
},
|
||||
proto::{
|
||||
self, DmMessagePayload, ErrorCode, PacketId, SessionCrypto, encode_packet, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn handle_dm_message(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let msg: DmMessagePayload = serde_json::from_slice(payload)?;
|
||||
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
let my_id = sess.user_id.clone();
|
||||
drop(sess);
|
||||
|
||||
// Rate limit check (token bucket per session).
|
||||
if !state.rate_limiter.try_consume(session_id) {
|
||||
state.metrics.inc(&state.metrics.rate_limited_events);
|
||||
warn!("rate-limited DM session {session_id}");
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&proto::ErrorPayload {
|
||||
code: ErrorCode::RateLimited as u32,
|
||||
message: "you are sending messages too quickly".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let other_id = state
|
||||
.storage
|
||||
.get_dm_user_id(&msg.dm_id, &my_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("user not in DM"))?;
|
||||
if state.storage.is_blocked(&other_id, &my_id).await? {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&proto::ErrorPayload {
|
||||
code: proto::ErrorCode::Blocked as u32,
|
||||
message: "blocked".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let saved = state
|
||||
.storage
|
||||
.save_dm_message(&msg.dm_id, &my_id, &msg.content)
|
||||
.await?;
|
||||
state.metrics.inc(&state.metrics.dm_messages_sent);
|
||||
|
||||
state
|
||||
.storage
|
||||
.increment_dm_unread(&msg.dm_id, &other_id)
|
||||
.await?;
|
||||
|
||||
if let Some(recipient_sid) =
|
||||
session::get_session_id_by_user_id(&state.sessions, &other_id).await
|
||||
{
|
||||
let data = encode_packet(
|
||||
PacketId::DmMessage,
|
||||
0,
|
||||
&to_payload(&DmMessagePayload {
|
||||
dm_id: msg.dm_id.clone(),
|
||||
sender_id: my_id.clone(),
|
||||
content: msg.content.clone(),
|
||||
timestamp: saved.timestamp,
|
||||
}),
|
||||
);
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: None,
|
||||
exclude_session: None,
|
||||
target_session_id: Some(recipient_sid),
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
warn!("dm recipient offline: {}", &other_id[..8]);
|
||||
}
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::DmMessage,
|
||||
seq,
|
||||
&to_payload(&saved),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
113
gateway/src/handler/direct_message/start.rs
Normal file
113
gateway/src/handler/direct_message/start.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{io, state::State},
|
||||
proto::{self, DmStartPayload, DmStartResponsePayload, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_dm_start(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: DmStartPayload = serde_json::from_slice(payload)?;
|
||||
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
let my_id = sess.user_id.clone();
|
||||
drop(sess);
|
||||
|
||||
if state
|
||||
.storage
|
||||
.is_blocked(&req.target_user_id, &my_id)
|
||||
.await?
|
||||
{
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&proto::ErrorPayload {
|
||||
code: proto::ErrorCode::Blocked as u32,
|
||||
message: "blocked".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if req.target_user_id == my_id {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&proto::ErrorPayload {
|
||||
code: proto::ErrorCode::InvalidPacket as u32,
|
||||
message: "cannot DM yourself".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (dm_id, unread_count) = state
|
||||
.storage
|
||||
.find_or_create_dm(&my_id, &req.target_user_id)
|
||||
.await?;
|
||||
let other_id = state
|
||||
.storage
|
||||
.get_dm_user_id(&dm_id, &my_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("user not in DM"))?;
|
||||
let nickname = state
|
||||
.storage
|
||||
.get_dm_nickname(&other_id)
|
||||
.await?
|
||||
.unwrap_or_else(|| other_id[..8].to_string());
|
||||
let messages = state
|
||||
.storage
|
||||
.get_dm_messages(&dm_id, 50, None, None)
|
||||
.await?;
|
||||
|
||||
let resp = DmStartResponsePayload {
|
||||
dm_id,
|
||||
other_user_id: other_id,
|
||||
other_nickname: nickname,
|
||||
messages,
|
||||
unread_count: unread_count as u32,
|
||||
};
|
||||
io::send_encrypted(stream, PacketId::DmStart, seq, &to_payload(&resp), crypto).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_dm_read_ack(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: serde_json::Value = serde_json::from_slice(payload)?;
|
||||
let dm_id = req["dm_id"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing dm_id"))?;
|
||||
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
let my_id = sess.user_id.clone();
|
||||
drop(sess);
|
||||
|
||||
state.storage.reset_dm_unread(dm_id, &my_id).await?;
|
||||
|
||||
io::send_encrypted(stream, PacketId::DmReadAck, seq, b"{}", crypto).await?;
|
||||
Ok(())
|
||||
}
|
||||
187
gateway/src/handler/dispatch.rs
Normal file
187
gateway/src/handler/dispatch.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
use anyhow::Result;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
net::io,
|
||||
proto::{
|
||||
ChatMessagePayload, JoinChannelPayload, LeaveChannelPayload, PacketId, PingPayload,
|
||||
PongPayload, ReactionPayload, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{Ctx, channel, content, direct_message, friends, guild};
|
||||
|
||||
pub async fn dispatch(
|
||||
ctx: &mut Ctx<'_>,
|
||||
session_id: &str,
|
||||
pid: PacketId,
|
||||
payload: &[u8],
|
||||
addr: std::net::SocketAddr,
|
||||
) -> Result<()> {
|
||||
let Ctx {
|
||||
stream,
|
||||
seq,
|
||||
crypto,
|
||||
state,
|
||||
} = ctx;
|
||||
match pid {
|
||||
PacketId::Ping => {
|
||||
let ping: PingPayload = serde_json::from_slice(payload)?;
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Pong,
|
||||
seq,
|
||||
&to_payload(&PongPayload {
|
||||
timestamp: ping.timestamp,
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
PacketId::JoinChannel => {
|
||||
let m: JoinChannelPayload = serde_json::from_slice(payload)?;
|
||||
channel::join(stream, seq, session_id, &m.channel_id, crypto, state).await?;
|
||||
}
|
||||
PacketId::LeaveChannel => {
|
||||
let m: LeaveChannelPayload = serde_json::from_slice(payload)?;
|
||||
channel::leave(stream, seq, session_id, &m.channel_id, crypto, state).await?;
|
||||
}
|
||||
PacketId::ChannelCreate => {
|
||||
channel::handle_channel_create(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::ChannelDelete => {
|
||||
channel::handle_channel_delete(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::ChannelList => {
|
||||
channel::handle_channel_list(stream, seq, session_id, crypto, state).await?;
|
||||
}
|
||||
PacketId::ChatMessage => {
|
||||
let m: ChatMessagePayload = serde_json::from_slice(payload)?;
|
||||
content::chat::handle(session_id, m, state).await?;
|
||||
}
|
||||
PacketId::DmStart => {
|
||||
direct_message::handle_dm_start(stream, seq, session_id, payload, crypto, state)
|
||||
.await?;
|
||||
}
|
||||
PacketId::DmMessage => {
|
||||
direct_message::handle_dm_message(stream, seq, session_id, payload, crypto, state)
|
||||
.await?;
|
||||
}
|
||||
PacketId::DmHistory => {
|
||||
direct_message::handle_dm_history(stream, seq, session_id, payload, crypto, state)
|
||||
.await?;
|
||||
}
|
||||
PacketId::DmReadAck => {
|
||||
direct_message::handle_dm_read_ack(stream, seq, session_id, payload, crypto, state)
|
||||
.await?;
|
||||
}
|
||||
PacketId::GuildCreate => {
|
||||
guild::handle_guild_create(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::GuildDelete => {
|
||||
guild::handle_guild_delete(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::GuildList => {
|
||||
guild::handle_guild_list(stream, seq, session_id, crypto, state).await?;
|
||||
}
|
||||
PacketId::GuildMemberJoin => {
|
||||
guild::handle_guild_member_join(stream, seq, session_id, payload, crypto, state)
|
||||
.await?;
|
||||
}
|
||||
PacketId::GuildMemberLeave => {
|
||||
guild::handle_guild_member_leave(stream, seq, session_id, payload, crypto, state)
|
||||
.await?;
|
||||
}
|
||||
PacketId::GuildMemberKick => {
|
||||
guild::handle_guild_member_kick(stream, seq, session_id, payload, crypto, state)
|
||||
.await?;
|
||||
}
|
||||
PacketId::RoleCreate => {
|
||||
guild::handle_role_create(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::RoleDelete => {
|
||||
guild::handle_role_delete(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::InviteCreate => {
|
||||
guild::handle_invite_create(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::InviteAccept => {
|
||||
guild::handle_invite_accept(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::InviteDelete => {
|
||||
guild::handle_invite_delete(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::GuildAuditLogFetch => {
|
||||
guild::handle_audit_log_fetch(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::GuildMemberListFetch => {
|
||||
guild::handle_member_list_fetch(stream, seq, session_id, payload, crypto, state)
|
||||
.await?;
|
||||
}
|
||||
PacketId::GuildRoleAssign => {
|
||||
guild::handle_role_assign(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::GuildRoleUnassign => {
|
||||
guild::handle_role_unassign(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::GuildRoleListFetch => {
|
||||
guild::handle_role_list_fetch(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::PresenceUpdate => {
|
||||
content::presence::handle_presence_update(
|
||||
stream, seq, session_id, payload, crypto, state,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
PacketId::PresenceSync => {
|
||||
content::presence::handle_presence_sync(stream, seq, session_id, crypto, state).await?;
|
||||
}
|
||||
PacketId::FriendRequest => {
|
||||
friends::handle_friend_request(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::FriendAccept => {
|
||||
friends::handle_friend_accept(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::FriendDecline => {
|
||||
friends::handle_friend_decline(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::FriendRemove => {
|
||||
friends::handle_friend_remove(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::FriendList => {
|
||||
friends::handle_friend_list(stream, seq, session_id, crypto, state).await?;
|
||||
}
|
||||
PacketId::BlockUser => {
|
||||
friends::handle_block_user(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::UnblockUser => {
|
||||
friends::handle_unblock_user(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::BlockList => {
|
||||
friends::handle_block_list(stream, seq, session_id, crypto, state).await?;
|
||||
}
|
||||
PacketId::MessageReactionAdd => {
|
||||
let m: ReactionPayload = serde_json::from_slice(payload)?;
|
||||
content::reaction::handle_reaction_add(session_id, m, state).await?;
|
||||
}
|
||||
PacketId::MessageReactionRemove => {
|
||||
let m: ReactionPayload = serde_json::from_slice(payload)?;
|
||||
content::reaction::handle_reaction_remove(session_id, m, state).await?;
|
||||
}
|
||||
PacketId::MessageEdit => {
|
||||
content::message_edit::handle_message_edit(session_id, payload, state).await?;
|
||||
}
|
||||
PacketId::MessageDelete => {
|
||||
content::message_edit::handle_message_delete(session_id, payload, state).await?;
|
||||
}
|
||||
PacketId::TypingStart => {
|
||||
content::handle_typing_start(session_id, payload, state).await?;
|
||||
}
|
||||
PacketId::ReadReceipt => {
|
||||
content::handle_read_receipt(stream, seq, session_id, payload, crypto, state).await?;
|
||||
}
|
||||
PacketId::Disconnect => debug!("{addr} DISCONNECT"),
|
||||
other => debug!("{addr} unhandled {:?}", other),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
37
gateway/src/handler/friends/decline.rs
Normal file
37
gateway/src/handler/friends/decline.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::io,
|
||||
proto::{FriendDeclinePayload, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_friend_decline(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &crate::net::state::State,
|
||||
) -> Result<()> {
|
||||
let req: FriendDeclinePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.decline_friend_request(&req.from_user_id, &sess.user_id)
|
||||
.await?;
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::FriendDecline,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"status": "DECLINED"})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
47
gateway/src/handler/friends/list.rs
Normal file
47
gateway/src/handler/friends/list.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{io, state::State},
|
||||
proto::{FriendInfo, FriendListPayload, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_friend_list(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let friend_ids = state.storage.list_friends(&sess.user_id).await?;
|
||||
let mut friends = Vec::new();
|
||||
let presences = state.presences.read().await;
|
||||
|
||||
for fid in &friend_ids {
|
||||
let nick = state.storage.get_nickname(fid).await?.unwrap_or_default();
|
||||
let presence = presences.get(fid);
|
||||
friends.push(FriendInfo {
|
||||
user_id: fid.clone(),
|
||||
nickname: nick,
|
||||
status: presence
|
||||
.map(|p| p.status.clone())
|
||||
.unwrap_or_else(|| "OFFLINE".into()),
|
||||
since: 0,
|
||||
});
|
||||
}
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::FriendList,
|
||||
seq,
|
||||
&to_payload(&FriendListPayload { friends }),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
122
gateway/src/handler/friends/manage.rs
Normal file
122
gateway/src/handler/friends/manage.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{io, state::State},
|
||||
proto::{
|
||||
BlockListPayload, BlockUserPayload, FriendRemovePayload, PacketId, SessionCrypto,
|
||||
UnblockUserPayload, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
pub async fn handle_friend_remove(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: FriendRemovePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.remove_friend(&sess.user_id, &req.user_id)
|
||||
.await?;
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::FriendRemove,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"removed": true})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_block_user(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: BlockUserPayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.block_user(&sess.user_id, &req.user_id)
|
||||
.await?;
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::BlockUser,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"blocked": true})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_unblock_user(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: UnblockUserPayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.unblock_user(&sess.user_id, &req.user_id)
|
||||
.await?;
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::UnblockUser,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"unblocked": true})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_block_list(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let blocked = state.storage.list_blocks(&sess.user_id).await?;
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::BlockList,
|
||||
seq,
|
||||
&to_payload(&BlockListPayload { blocked }),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
37
gateway/src/handler/friends/mod.rs
Normal file
37
gateway/src/handler/friends/mod.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
mod decline;
|
||||
mod list;
|
||||
mod manage;
|
||||
mod requests;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
net::io,
|
||||
proto::{ErrorPayload, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
async fn send_err(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
code: crate::proto::ErrorCode,
|
||||
msg: &str,
|
||||
crypto: &SessionCrypto,
|
||||
) -> Result<()> {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&ErrorPayload {
|
||||
code: code as u32,
|
||||
message: msg.into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub use decline::handle_friend_decline;
|
||||
pub use list::handle_friend_list;
|
||||
pub use manage::{handle_block_list, handle_block_user, handle_friend_remove, handle_unblock_user};
|
||||
pub use requests::{handle_friend_accept, handle_friend_request};
|
||||
152
gateway/src/handler/friends/requests.rs
Normal file
152
gateway/src/handler/friends/requests.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{
|
||||
io,
|
||||
state::{BroadcastMsg, State},
|
||||
},
|
||||
proto::{
|
||||
FriendAcceptPayload, FriendEventPayload, FriendRequestPayload, PacketId, SessionCrypto,
|
||||
encode_packet, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
use super::send_err;
|
||||
|
||||
pub async fn handle_friend_request(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: FriendRequestPayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
if state
|
||||
.storage
|
||||
.is_blocked(&req.to_user_id, &sess.user_id)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::Blocked,
|
||||
"you are blocked by this user",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if state
|
||||
.storage
|
||||
.is_friend(&sess.user_id, &req.to_user_id)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::InvalidPacket,
|
||||
"already friends",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ok = state
|
||||
.storage
|
||||
.create_friend_request(&sess.user_id, &req.to_user_id)
|
||||
.await?;
|
||||
if !ok {
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::InvalidPacket,
|
||||
"already friends or request pending",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ev = FriendEventPayload {
|
||||
event: "REQUEST_RECEIVED".into(),
|
||||
user_id: sess.user_id.clone(),
|
||||
nickname: Some(sess.nickname.clone()),
|
||||
};
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: None,
|
||||
exclude_session: None,
|
||||
target_session_id: Some(req.to_user_id.clone()),
|
||||
data: encode_packet(PacketId::FriendRequest, *seq, &to_payload(&ev)),
|
||||
});
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::FriendRequest,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"status": "PENDING", "to_user_id": req.to_user_id})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_friend_accept(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: FriendAcceptPayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let ok = state
|
||||
.storage
|
||||
.accept_friend_request(&req.from_user_id, &sess.user_id)
|
||||
.await?;
|
||||
if !ok {
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::InvalidPacket,
|
||||
"no pending request",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let ev = FriendEventPayload {
|
||||
event: "REQUEST_ACCEPTED".into(),
|
||||
user_id: sess.user_id.clone(),
|
||||
nickname: Some(sess.nickname.clone()),
|
||||
};
|
||||
let _ = state.broadcast.send(BroadcastMsg {
|
||||
channel_id: None,
|
||||
exclude_session: None,
|
||||
target_session_id: Some(req.from_user_id.clone()),
|
||||
data: encode_packet(PacketId::FriendAccept, *seq, &to_payload(&ev)),
|
||||
});
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::FriendAccept,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"status": "ACCEPTED", "from_user_id": req.from_user_id})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
86
gateway/src/handler/guild/audit.rs
Normal file
86
gateway/src/handler/guild/audit.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::permissions::Permissions,
|
||||
net::{io, state::State},
|
||||
proto::{
|
||||
AuditLogEntryPayload, GuildAuditLogFetchPayload, GuildAuditLogPayload, PacketId,
|
||||
SessionCrypto, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
/// Fetch audit log entries for a guild (admin-only: requires VIEW_AUDIT_LOG or owner).
|
||||
pub async fn handle_audit_log_fetch(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: GuildAuditLogFetchPayload = serde_json::from_slice(payload)?;
|
||||
let sess = match crate::domain::session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// Permission check: owner always passes; otherwise require VIEW_AUDIT_LOG.
|
||||
// We reuse the existing helper via the same logic.
|
||||
let allowed = if let Some(g) = state.storage.get_guild(&req.guild_id).await?
|
||||
&& g.owner_id == sess.user_id
|
||||
{
|
||||
true
|
||||
} else {
|
||||
// No VIEW_AUDIT_LOG bit defined yet — fall back to MANAGE_GUILD.
|
||||
let perms = state
|
||||
.storage
|
||||
.get_user_role_perms(&req.guild_id, &sess.user_id)
|
||||
.await?;
|
||||
Permissions::from_role_perms(&perms).has(Permissions::MANAGE_GUILD)
|
||||
};
|
||||
|
||||
if !allowed {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&crate::proto::ErrorPayload {
|
||||
code: crate::proto::ErrorCode::PermissionDenied as u32,
|
||||
message: "audit log access requires MANAGE_GUILD".into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let limit = req.limit.clamp(1, 200);
|
||||
let rows = state.storage.get_audit_log(&req.guild_id, limit).await?;
|
||||
let entries: Vec<AuditLogEntryPayload> = rows
|
||||
.into_iter()
|
||||
.map(|r| AuditLogEntryPayload {
|
||||
id: r.id,
|
||||
guild_id: r.guild_id,
|
||||
actor_id: r.actor_id,
|
||||
action: r.action,
|
||||
target_id: r.target_id,
|
||||
target_type: r.target_type,
|
||||
reason: r.reason,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect();
|
||||
let resp = GuildAuditLogPayload {
|
||||
guild_id: req.guild_id,
|
||||
entries,
|
||||
};
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildAuditLog,
|
||||
seq,
|
||||
&to_payload(&resp),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
124
gateway/src/handler/guild/crud.rs
Normal file
124
gateway/src/handler/guild/crud.rs
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{io, state::State},
|
||||
proto::{GuildCreatePayload, GuildInfo, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
use super::send_err;
|
||||
|
||||
pub async fn handle_guild_create(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: GuildCreatePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
if req.name.len() < 2 || req.name.len() > 100 {
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::InvalidPacket,
|
||||
"guild name must be 2-100 chars",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let guild_id = state.storage.create_guild(&sess.user_id, &req.name).await?;
|
||||
let guild = state
|
||||
.storage
|
||||
.get_guild(&guild_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("guild not found after create"))?;
|
||||
|
||||
let info = GuildInfo {
|
||||
id: guild.id.clone(),
|
||||
owner_id: guild.owner_id,
|
||||
name: guild.name,
|
||||
member_count: guild.member_count,
|
||||
created_at: guild.created_at,
|
||||
};
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildCreate,
|
||||
seq,
|
||||
&to_payload(&info),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
debug!(
|
||||
"guild: {} created {} (owner={})",
|
||||
sess.nickname, guild_id, sess.user_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_guild_delete(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Req {
|
||||
guild_id: String,
|
||||
}
|
||||
let req: Req = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let guild = state
|
||||
.storage
|
||||
.get_guild(&req.guild_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("guild not found"))?;
|
||||
if guild.owner_id != sess.user_id {
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::PermissionDenied,
|
||||
"only owner can delete guild",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state.storage.delete_guild(&req.guild_id).await?;
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
"GUILD_DELETE",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildDelete,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"guild_id": req.guild_id})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
219
gateway/src/handler/guild/invites.rs
Normal file
219
gateway/src/handler/guild/invites.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::{permissions::Permissions, session},
|
||||
net::{io, state::State},
|
||||
proto::{
|
||||
InviteAcceptPayload, InviteCreatePayload, InviteDeletePayload, InviteInfo, PacketId,
|
||||
SessionCrypto, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{now_ms, send_err};
|
||||
|
||||
pub async fn handle_invite_create(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: InviteCreatePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
if !super::require_perm(
|
||||
state,
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
Permissions::CREATE_INVITE,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::PermissionDenied,
|
||||
"missing CREATE_INVITE permission",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let inv = state
|
||||
.storage
|
||||
.create_invite(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
req.max_uses,
|
||||
req.expires_in_seconds,
|
||||
)
|
||||
.await?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
"INVITE_CREATE",
|
||||
Some(&inv.id),
|
||||
Some("invite"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let info = InviteInfo {
|
||||
id: inv.id,
|
||||
guild_id: inv.guild_id,
|
||||
guild_name: inv.guild_name,
|
||||
code: inv.code,
|
||||
creator_id: inv.creator_id,
|
||||
max_uses: inv.max_uses,
|
||||
uses: inv.uses,
|
||||
expires_at: inv.expires_at,
|
||||
created_at: inv.created_at,
|
||||
};
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::InviteCreate,
|
||||
seq,
|
||||
&to_payload(&info),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_invite_accept(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: InviteAcceptPayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let inv = state
|
||||
.storage
|
||||
.get_invite_by_code(&req.code)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow::anyhow!("invite not found"))?;
|
||||
|
||||
if let Some(exp) = inv.expires_at {
|
||||
let now = now_ms();
|
||||
if now > exp {
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::InvalidPacket,
|
||||
"invite expired",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
if let Some(max) = inv.max_uses
|
||||
&& inv.uses >= max
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::InvalidPacket,
|
||||
"invite max uses reached",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state
|
||||
.storage
|
||||
.add_guild_member(&inv.guild_id, &sess.user_id)
|
||||
.await?;
|
||||
state.storage.use_invite(&inv.id).await?;
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&inv.guild_id,
|
||||
&sess.user_id,
|
||||
"MEMBER_JOIN_INVITE",
|
||||
Some(&sess.user_id),
|
||||
Some("member"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::InviteAccept,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"guild_id": inv.guild_id, "guild_name": inv.guild_name})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_invite_delete(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: InviteDeletePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
if !super::require_perm(
|
||||
state,
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
Permissions::MANAGE_GUILD,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::PermissionDenied,
|
||||
"missing MANAGE_GUILD permission",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state.storage.delete_invite(&req.invite_id).await?;
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
"INVITE_DELETE",
|
||||
Some(&req.invite_id),
|
||||
Some("invite"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::InviteDelete,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"invite_id": req.invite_id})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
61
gateway/src/handler/guild/list.rs
Normal file
61
gateway/src/handler/guild/list.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::session,
|
||||
net::{io, state::State},
|
||||
proto::{GuildInfo, GuildListPayload, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
pub async fn handle_guild_list(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let rows = state.storage.list_user_guilds(&sess.user_id).await?;
|
||||
let guilds: Vec<GuildInfo> = rows
|
||||
.iter()
|
||||
.map(|g| GuildInfo {
|
||||
id: g.id.clone(),
|
||||
owner_id: g.owner_id.clone(),
|
||||
name: g.name.clone(),
|
||||
member_count: g.member_count,
|
||||
created_at: g.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
for g in &rows {
|
||||
let roles = state.storage.get_user_roles(&g.id, &sess.user_id).await?;
|
||||
let color = roles
|
||||
.iter()
|
||||
.max_by_key(|r| r.position)
|
||||
.map(|r| r.color.clone())
|
||||
.unwrap_or_else(|| "#ffffff".into());
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::UserRoleUpdate,
|
||||
seq,
|
||||
&to_payload(
|
||||
&serde_json::json!({"user_id": sess.user_id, "guild_id": g.id, "color": color}),
|
||||
),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildList,
|
||||
seq,
|
||||
&to_payload(&GuildListPayload { guilds }),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
154
gateway/src/handler/guild/members.rs
Normal file
154
gateway/src/handler/guild/members.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::{permissions::Permissions, session},
|
||||
net::{io, state::State},
|
||||
proto::{
|
||||
GuildMemberJoinPayload, GuildMemberKickPayload, GuildMemberLeavePayload, PacketId,
|
||||
SessionCrypto, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
use super::send_err;
|
||||
|
||||
pub async fn handle_guild_member_join(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: GuildMemberJoinPayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.add_guild_member(&req.guild_id, &sess.user_id)
|
||||
.await?;
|
||||
|
||||
let roles = state
|
||||
.storage
|
||||
.get_user_roles(&req.guild_id, &sess.user_id)
|
||||
.await?;
|
||||
let color = roles
|
||||
.iter()
|
||||
.max_by_key(|r| r.position)
|
||||
.map(|r| r.color.clone())
|
||||
.unwrap_or_else(|| "#ffffff".into());
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildMemberJoin,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"guild_id": req.guild_id, "user_id": sess.user_id})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::UserRoleUpdate,
|
||||
seq,
|
||||
&to_payload(
|
||||
&serde_json::json!({"user_id": sess.user_id, "guild_id": req.guild_id, "color": color}),
|
||||
),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_guild_member_leave(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: GuildMemberLeavePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
let target = if req.user_id.is_empty() {
|
||||
&sess.user_id
|
||||
} else {
|
||||
&req.user_id
|
||||
};
|
||||
state
|
||||
.storage
|
||||
.remove_guild_member(&req.guild_id, target)
|
||||
.await?;
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildMemberLeave,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"guild_id": req.guild_id, "user_id": target})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_guild_member_kick(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: GuildMemberKickPayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
if !super::require_perm(
|
||||
state,
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
Permissions::KICK_MEMBERS,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::PermissionDenied,
|
||||
"missing KICK_MEMBERS permission",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state
|
||||
.storage
|
||||
.remove_guild_member(&req.guild_id, &req.user_id)
|
||||
.await?;
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
"MEMBER_KICK",
|
||||
Some(&req.user_id),
|
||||
Some("member"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildMemberKick,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"guild_id": req.guild_id, "user_id": req.user_id})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
230
gateway/src/handler/guild/members_list.rs
Normal file
230
gateway/src/handler/guild/members_list.rs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::{permissions::Permissions, session},
|
||||
net::{io, state::State},
|
||||
proto::{
|
||||
GuildMemberInfoPayload, GuildMemberListFetchPayload, GuildMemberListPayload,
|
||||
GuildRoleInfoPayload, GuildRoleListFetchPayload, GuildRoleListPayload, PacketId,
|
||||
RoleAssignPayload, SessionCrypto, to_payload,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{require_perm, send_err};
|
||||
|
||||
/// Fetch the member list for a guild (visible to all members).
|
||||
pub async fn handle_member_list_fetch(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: GuildMemberListFetchPayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// Optional: verify the requester is a member of the guild.
|
||||
let owner_id = state
|
||||
.storage
|
||||
.get_guild(&req.guild_id)
|
||||
.await?
|
||||
.map(|g| g.owner_id);
|
||||
|
||||
let rows = state.storage.list_guild_members(&req.guild_id).await?;
|
||||
let members: Vec<GuildMemberInfoPayload> = rows
|
||||
.into_iter()
|
||||
.map(|r| GuildMemberInfoPayload {
|
||||
user_id: r.user_id.clone(),
|
||||
nickname: r.nickname,
|
||||
joined_at: r.joined_at,
|
||||
role_color: r.role_color,
|
||||
role_name: r.role_name,
|
||||
is_owner: owner_id.as_deref() == Some(&r.user_id),
|
||||
})
|
||||
.collect();
|
||||
let _ = sess;
|
||||
let resp = GuildMemberListPayload {
|
||||
guild_id: req.guild_id,
|
||||
members,
|
||||
};
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildMemberList,
|
||||
seq,
|
||||
&to_payload(&resp),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assign a role to a user (admin only: requires MANAGE_ROLES).
|
||||
pub async fn handle_role_assign(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: RoleAssignPayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if !require_perm(
|
||||
state,
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
Permissions::MANAGE_ROLES,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::PermissionDenied,
|
||||
"missing MANAGE_ROLES permission",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state
|
||||
.storage
|
||||
.assign_role(&req.guild_id, &req.user_id, &req.role_id)
|
||||
.await?;
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
"ROLE_ASSIGN",
|
||||
Some(&req.user_id),
|
||||
Some("user"),
|
||||
Some(&req.role_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Confirm to caller.
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildRoleAssign,
|
||||
seq,
|
||||
&to_payload(
|
||||
&serde_json::json!({"ok": true, "user_id": req.user_id, "role_id": req.role_id}),
|
||||
),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a role from a user (admin only: requires MANAGE_ROLES).
|
||||
pub async fn handle_role_unassign(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: RoleAssignPayload = serde_json::from_slice(payload)?;
|
||||
let sess = match session::get(&state.sessions, session_id).await {
|
||||
Some(s) => s,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
if !require_perm(
|
||||
state,
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
Permissions::MANAGE_ROLES,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::PermissionDenied,
|
||||
"missing MANAGE_ROLES permission",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state
|
||||
.storage
|
||||
.remove_role_from_user(&req.guild_id, &req.user_id, &req.role_id)
|
||||
.await?;
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
"ROLE_UNASSIGN",
|
||||
Some(&req.user_id),
|
||||
Some("user"),
|
||||
Some(&req.role_id),
|
||||
)
|
||||
.await?;
|
||||
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildRoleUnassign,
|
||||
seq,
|
||||
&to_payload(
|
||||
&serde_json::json!({"ok": true, "user_id": req.user_id, "role_id": req.role_id}),
|
||||
),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fetch all roles defined in a guild (visible to all members).
|
||||
pub async fn handle_role_list_fetch(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: GuildRoleListFetchPayload = serde_json::from_slice(payload)?;
|
||||
let _sess = session::get(&state.sessions, session_id).await;
|
||||
|
||||
let rows = state.storage.list_guild_roles(&req.guild_id).await?;
|
||||
let roles: Vec<GuildRoleInfoPayload> = rows
|
||||
.into_iter()
|
||||
.map(|r| GuildRoleInfoPayload {
|
||||
id: r.id,
|
||||
guild_id: r.guild_id,
|
||||
name: r.name,
|
||||
color: r.color,
|
||||
permissions: r.permissions as u64,
|
||||
position: r.position,
|
||||
})
|
||||
.collect();
|
||||
let resp = GuildRoleListPayload {
|
||||
guild_id: req.guild_id,
|
||||
roles,
|
||||
};
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::GuildRoleList,
|
||||
seq,
|
||||
&to_payload(&resp),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
68
gateway/src/handler/guild/mod.rs
Normal file
68
gateway/src/handler/guild/mod.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
mod audit;
|
||||
mod crud;
|
||||
mod invites;
|
||||
mod list;
|
||||
mod members;
|
||||
mod members_list;
|
||||
mod roles;
|
||||
|
||||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::permissions::Permissions,
|
||||
net::{io, state::State},
|
||||
proto::{ErrorPayload, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
async fn send_err(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
code: crate::proto::ErrorCode,
|
||||
msg: &str,
|
||||
crypto: &SessionCrypto,
|
||||
) -> Result<()> {
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::Error,
|
||||
seq,
|
||||
&to_payload(&ErrorPayload {
|
||||
code: code as u32,
|
||||
message: msg.into(),
|
||||
}),
|
||||
crypto,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn require_perm(
|
||||
state: &State,
|
||||
guild_id: &str,
|
||||
user_id: &str,
|
||||
required: Permissions,
|
||||
) -> Result<bool> {
|
||||
if let Some(g) = state.storage.get_guild(guild_id).await?
|
||||
&& g.owner_id == user_id
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
let perms = state.storage.get_user_role_perms(guild_id, user_id).await?;
|
||||
Ok(Permissions::from_role_perms(&perms).has(required))
|
||||
}
|
||||
|
||||
fn now_ms() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as i64
|
||||
}
|
||||
|
||||
pub use audit::handle_audit_log_fetch;
|
||||
pub use crud::{handle_guild_create, handle_guild_delete};
|
||||
pub use invites::{handle_invite_accept, handle_invite_create, handle_invite_delete};
|
||||
pub use list::handle_guild_list;
|
||||
pub use members::{handle_guild_member_join, handle_guild_member_kick, handle_guild_member_leave};
|
||||
pub use members_list::{
|
||||
handle_member_list_fetch, handle_role_assign, handle_role_list_fetch, handle_role_unassign,
|
||||
};
|
||||
pub use roles::{handle_role_create, handle_role_delete};
|
||||
128
gateway/src/handler/guild/roles.rs
Normal file
128
gateway/src/handler/guild/roles.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{
|
||||
domain::{permissions::Permissions, session},
|
||||
net::{io, state::State},
|
||||
proto::{PacketId, RoleCreatePayload, RoleDeletePayload, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
use super::send_err;
|
||||
|
||||
pub async fn handle_role_create(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: RoleCreatePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
if !super::require_perm(
|
||||
state,
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
Permissions::MANAGE_ROLES,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::PermissionDenied,
|
||||
"missing MANAGE_ROLES permission",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let color = req.color.as_deref().unwrap_or("#ffffff");
|
||||
let permissions = req.permissions.unwrap_or(0);
|
||||
let role_id = state
|
||||
.storage
|
||||
.create_role(&req.guild_id, &req.name, color, permissions, 1)
|
||||
.await?;
|
||||
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
"ROLE_CREATE",
|
||||
Some(&role_id),
|
||||
Some("role"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::RoleCreate,
|
||||
seq,
|
||||
&to_payload(
|
||||
&serde_json::json!({"id": role_id, "guild_id": req.guild_id, "name": req.name}),
|
||||
),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_role_delete(
|
||||
stream: &mut TcpStream,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
payload: &[u8],
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
) -> Result<()> {
|
||||
let req: RoleDeletePayload = serde_json::from_slice(payload)?;
|
||||
let sess = session::get(&state.sessions, session_id)
|
||||
.await
|
||||
.ok_or_else(|| anyhow::anyhow!("session not found"))?;
|
||||
|
||||
if !super::require_perm(
|
||||
state,
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
Permissions::MANAGE_ROLES,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
send_err(
|
||||
stream,
|
||||
seq,
|
||||
crate::proto::ErrorCode::PermissionDenied,
|
||||
"missing MANAGE_ROLES permission",
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
state.storage.delete_role(&req.role_id).await?;
|
||||
state
|
||||
.storage
|
||||
.append_audit_log(
|
||||
&req.guild_id,
|
||||
&sess.user_id,
|
||||
"ROLE_DELETE",
|
||||
Some(&req.role_id),
|
||||
Some("role"),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
io::send_encrypted(
|
||||
stream,
|
||||
PacketId::RoleDelete,
|
||||
seq,
|
||||
&to_payload(&serde_json::json!({"role_id": req.role_id})),
|
||||
crypto,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
22
gateway/src/handler/mod.rs
Normal file
22
gateway/src/handler/mod.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
pub mod channel;
|
||||
pub mod content;
|
||||
pub mod deliver;
|
||||
pub mod direct_message;
|
||||
pub mod dispatch;
|
||||
pub mod friends;
|
||||
pub mod guild;
|
||||
pub mod run;
|
||||
|
||||
pub use dispatch::dispatch;
|
||||
pub use run::run_session;
|
||||
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::{net::state::State, proto::SessionCrypto};
|
||||
|
||||
pub struct Ctx<'a> {
|
||||
pub stream: &'a mut TcpStream,
|
||||
pub seq: &'a mut u32,
|
||||
pub crypto: &'a SessionCrypto,
|
||||
pub state: &'a State,
|
||||
}
|
||||
55
gateway/src/handler/run.rs
Normal file
55
gateway/src/handler/run.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
use anyhow::Result;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::broadcast;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
net::{io, state::BroadcastMsg, state::State},
|
||||
proto::{self, PacketId, SessionCrypto, to_payload},
|
||||
};
|
||||
|
||||
use super::{Ctx, deliver::deliver_encrypted, dispatch};
|
||||
|
||||
pub async fn run_session(
|
||||
stream: &mut TcpStream,
|
||||
addr: std::net::SocketAddr,
|
||||
seq: &mut u32,
|
||||
session_id: &str,
|
||||
crypto: &SessionCrypto,
|
||||
state: &State,
|
||||
bcast_rx: &mut broadcast::Receiver<BroadcastMsg>,
|
||||
) -> Result<()> {
|
||||
let mut ctx = Ctx {
|
||||
stream,
|
||||
seq,
|
||||
crypto,
|
||||
state,
|
||||
};
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = io::read_encrypted(ctx.stream, ctx.crypto) => {
|
||||
let (hdr, payload) = result?;
|
||||
let pid = match PacketId::from_u16(hdr.packet_id) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
warn!("{addr} unknown 0x{:04X}", hdr.packet_id);
|
||||
io::send_encrypted(ctx.stream, PacketId::Error, ctx.seq, &to_payload(&proto::ErrorPayload {
|
||||
code: proto::ErrorCode::InvalidPacket as u32,
|
||||
message: "unknown packet".into(),
|
||||
}), ctx.crypto).await?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
dispatch(&mut ctx, session_id, pid, &payload, addr).await?;
|
||||
if pid == PacketId::Disconnect { return Ok(()); }
|
||||
}
|
||||
bcast = bcast_rx.recv() => {
|
||||
match bcast {
|
||||
Ok(msg) => deliver_encrypted(ctx.stream, ctx.seq, session_id, &msg, ctx.crypto, ctx.state).await?,
|
||||
Err(broadcast::error::RecvError::Lagged(n)) => warn!("{addr} lagged {n}"),
|
||||
Err(broadcast::error::RecvError::Closed) => return Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue