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
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(())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue