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:
loki5512344 2026-07-09 11:52:56 +02:00
commit e751e0bf5b
94 changed files with 9771 additions and 0 deletions

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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(())
}

View 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};

View 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(())
}