From c7f09ab9d0077922290294c0eebad1d223ad2373 Mon Sep 17 00:00:00 2001 From: Zomatree Date: Tue, 6 May 2025 17:48:38 +0100 Subject: [PATCH] feat: initial category permissions and rework of category crud routes --- crates/core/config/Revolt.toml | 1 + crates/core/config/src/lib.rs | 1 + .../core/database/src/models/servers/model.rs | 134 +++++++++++++++++- crates/core/database/src/util/bridge/v0.rs | 36 +++-- crates/core/database/src/util/permissions.rs | 53 ++++++- crates/core/database/src/util/reference.rs | 2 +- crates/core/models/src/v0/servers.rs | 47 +++++- crates/core/permissions/src/impl.rs | 30 +++- crates/core/permissions/src/trait.rs | 9 ++ crates/core/result/src/axum.rs | 2 + crates/core/result/src/lib.rs | 4 + crates/core/result/src/rocket.rs | 4 +- .../src/routes/servers/category_create.rs | 45 ++++++ .../src/routes/servers/category_delete.rs | 38 +++++ .../delta/src/routes/servers/category_edit.rs | 62 ++++++++ .../servers/category_permissions_set.rs | 50 +++++++ .../category_permissions_set_default.rs | 50 +++++++ crates/delta/src/routes/servers/mod.rs | 12 +- .../delta/src/routes/servers/server_edit.rs | 28 +--- 19 files changed, 550 insertions(+), 58 deletions(-) create mode 100644 crates/delta/src/routes/servers/category_create.rs create mode 100644 crates/delta/src/routes/servers/category_delete.rs create mode 100644 crates/delta/src/routes/servers/category_edit.rs create mode 100644 crates/delta/src/routes/servers/category_permissions_set.rs create mode 100644 crates/delta/src/routes/servers/category_permissions_set_default.rs diff --git a/crates/core/config/Revolt.toml b/crates/core/config/Revolt.toml index 71c09800..a59108aa 100644 --- a/crates/core/config/Revolt.toml +++ b/crates/core/config/Revolt.toml @@ -200,6 +200,7 @@ message_reactions = 20 server_emoji = 100 server_roles = 200 server_channels = 200 +server_categories = 50 # How many hours since creation a user is considered new new_user_hours = 72 diff --git a/crates/core/config/src/lib.rs b/crates/core/config/src/lib.rs index 6dd730e6..2cde586a 100644 --- a/crates/core/config/src/lib.rs +++ b/crates/core/config/src/lib.rs @@ -281,6 +281,7 @@ pub struct GlobalLimits { pub server_emoji: usize, pub server_roles: usize, pub server_channels: usize, + pub server_categories: usize, pub new_user_hours: usize, diff --git a/crates/core/database/src/models/servers/model.rs b/crates/core/database/src/models/servers/model.rs index 67c4daed..3a133e83 100644 --- a/crates/core/database/src/models/servers/model.rs +++ b/crates/core/database/src/models/servers/model.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use revolt_models::v0::{self, DataCreateServerChannel}; +use revolt_models::v0::{self, DataCreateCategory, DataCreateServerChannel, DataEditCategory}; use revolt_permissions::{OverrideField, DEFAULT_PERMISSION_SERVER}; use revolt_result::Result; use ulid::Ulid; @@ -26,8 +26,8 @@ auto_derived_partial!( // TODO: investigate if this is redundant and can be removed pub channels: Vec, /// Categories for this server - #[serde(skip_serializing_if = "Option::is_none")] - pub categories: Option>, + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub categories: HashMap, /// Configuration for sending system event messages #[serde(skip_serializing_if = "Option::is_none")] pub system_messages: Option, @@ -87,17 +87,29 @@ auto_derived_partial!( "PartialRole" ); -auto_derived!( +auto_derived_partial!( /// Channel category pub struct Category { /// Unique ID for this category pub id: String, /// Title for this category pub title: String, + /// Default permissions assigned to users in this category + #[serde(skip_serializing_if = "Option::is_none")] + pub default_permissions: Option, + /// Permissions assigned based on role to this category + #[serde( + default = "HashMap::::new", + skip_serializing_if = "HashMap::::is_empty" + )] + pub role_permissions: HashMap, /// Channels in this category pub channels: Vec, - } + }, + "PartialCategory" +); +auto_derived!( /// System message channel assignments pub struct SystemMessageChannels { /// ID of channel to send user join messages in @@ -127,6 +139,10 @@ auto_derived!( pub enum FieldsRole { Colour, } + + pub enum FieldsCategory { + DefaultPermissions + } ); #[allow(clippy::disallowed_methods)] @@ -149,7 +165,7 @@ impl Server { analytics: false, banner: None, - categories: None, + categories: HashMap::new(), discoverable: false, flags: None, icon: None, @@ -221,7 +237,7 @@ impl Server { pub fn remove_field(&mut self, field: &FieldsServer) { match field { FieldsServer::Description => self.description = None, - FieldsServer::Categories => self.categories = None, + FieldsServer::Categories => self.categories.clear(), FieldsServer::SystemMessages => self.system_messages = None, FieldsServer::Icon => self.icon = None, FieldsServer::Banner => self.banner = None, @@ -334,6 +350,110 @@ impl Role { } } +impl Category { + pub fn remove_field(&mut self, field: FieldsCategory) { + match field { + FieldsCategory::DefaultPermissions => self.default_permissions = None + }; + } + + pub async fn create(db: &Database, server: &mut Server, data: DataCreateCategory) -> Result { + let channels = data.channels.clone().unwrap_or_default() + .into_iter() + .filter(|c| server.channels.contains(&c)) + .collect(); + + let category = Category { + id: Ulid::new().to_string(), + title: data.title, + channels: channels, + default_permissions: None, + role_permissions: HashMap::new() + }; + + server.categories.insert(category.id.clone(), category.clone()); + + let partial_server = PartialServer { categories: Some(server.categories.clone()), ..Default::default() }; + + db.update_server(&server.id, &partial_server, Vec::new()).await?; + + EventV1::ServerUpdate { + id: server.id.clone(), + data: partial_server.into(), + clear: Vec::new(), + } + .p(server.id.clone()) + .await; + + Ok(category) + } + + pub async fn delete(&self, db: &Database, server: &mut Server) -> Result<()> { + // update the parent server model with the new category + server.categories.remove(&self.id); + + let partial_server = PartialServer { categories: Some(server.categories.clone()), ..Default::default() }; + + db.update_server(&server.id, &partial_server, Vec::new()).await?; + + EventV1::ServerUpdate { + id: server.id.clone(), + data: partial_server.into(), + clear: Vec::new(), + } + .p(server.id.clone()) + .await; + + Ok(()) + } + + pub async fn update(&mut self, db: &Database, server: &mut Server, partial: PartialCategory, remove: Vec) -> Result<()> { + for field in remove { + self.remove_field(field); + }; + + self.apply_options(partial); + + // update the parent server model with the new category + server.categories.insert(self.id.clone(), self.clone()); + + let partial_server = PartialServer { categories: Some(server.categories.clone()), ..Default::default() }; + + db.update_server(&server.id, &partial_server, Vec::new()).await?; + + EventV1::ServerUpdate { + id: server.id.clone(), + data: partial_server.into(), + clear: Vec::new(), + } + .p(server.id.clone()) + .await; + + Ok(()) + } + + pub async fn set_role_permission(&mut self, db: &Database, server: &mut Server, role_id: String, role_override: OverrideField) -> Result<()> { + self.role_permissions.insert(role_id, role_override); + + // update the parent server model with the new category + server.categories.insert(self.id.clone(), self.clone()); + + let partial_server = PartialServer { categories: Some(server.categories.clone()), ..Default::default() }; + + db.update_server(&server.id, &partial_server, Vec::new()).await?; + + EventV1::ServerUpdate { + id: server.id.clone(), + data: partial_server.into(), + clear: Vec::new(), + } + .p(server.id.clone()) + .await; + + Ok(()) + } +} + impl SystemMessageChannels { pub fn into_channel_ids(self) -> HashSet { let mut ids = HashSet::new(); diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index 8872ef10..03a800dc 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -729,9 +729,7 @@ impl From for Server { name: value.name, description: value.description, channels: value.channels, - categories: value - .categories - .map(|categories| categories.into_iter().map(|v| v.into()).collect()), + categories: value.categories.into_iter().map(|(k, v)| (k, v.into())).collect(), system_messages: value.system_messages.map(|v| v.into()), roles: value .roles @@ -757,9 +755,7 @@ impl From for crate::Server { name: value.name, description: value.description, channels: value.channels, - categories: value - .categories - .map(|categories| categories.into_iter().map(|v| v.into()).collect()), + categories: value.categories.into_iter().map(|(k, v)| (k, v.into())).collect(), system_messages: value.system_messages.map(|v| v.into()), roles: value .roles @@ -785,9 +781,7 @@ impl From for PartialServer { name: value.name, description: value.description, channels: value.channels, - categories: value - .categories - .map(|categories| categories.into_iter().map(|v| v.into()).collect()), + categories: value.categories.map(|x| x.into_iter().map(|(k, v)| (k, v.into())).collect()), system_messages: value.system_messages.map(|v| v.into()), roles: value .roles @@ -811,9 +805,7 @@ impl From for crate::PartialServer { name: value.name, description: value.description, channels: value.channels, - categories: value - .categories - .map(|categories| categories.into_iter().map(|v| v.into()).collect()), + categories: value.categories.map(|x| x.into_iter().map(|(k, v)| (k, v.into())).collect()), system_messages: value.system_messages.map(|v| v.into()), roles: value .roles @@ -859,6 +851,8 @@ impl From for Category { id: value.id, title: value.title, channels: value.channels, + role_permissions: value.role_permissions, + default_permissions: value.default_permissions, } } } @@ -869,6 +863,24 @@ impl From for crate::Category { id: value.id, title: value.title, channels: value.channels, + role_permissions: value.role_permissions, + default_permissions: value.default_permissions, + } + } +} + +impl From for FieldsCategory { + fn from(value: crate::FieldsCategory) -> Self { + match value { + crate::FieldsCategory::DefaultPermissions => FieldsCategory::DefaultPermissions, + } + } +} + +impl From for crate::FieldsCategory { + fn from(value: FieldsCategory) -> crate::FieldsCategory { + match value { + FieldsCategory::DefaultPermissions => crate::FieldsCategory::DefaultPermissions, } } } diff --git a/crates/core/database/src/util/permissions.rs b/crates/core/database/src/util/permissions.rs index e80b0da1..3419b39f 100644 --- a/crates/core/database/src/util/permissions.rs +++ b/crates/core/database/src/util/permissions.rs @@ -5,7 +5,7 @@ use revolt_permissions::{ RelationshipStatus, DEFAULT_PERMISSION_DIRECT_MESSAGE, }; -use crate::{Channel, Database, Member, Server, User}; +use crate::{Category, Channel, Database, Member, Server, User}; /// Permissions calculator #[derive(Clone)] @@ -16,6 +16,7 @@ pub struct DatabasePermissionQuery<'a> { perspective: &'a User, user: Option>, channel: Option>, + category: Option>, server: Option>, member: Option>, @@ -288,6 +289,40 @@ impl PermissionQuery for DatabasePermissionQuery<'_> { } } + async fn get_default_category_permissions(&mut self) -> Override { + if let Some(category) = &self.category { + category.default_permissions.unwrap_or_default().into() + } else { + Default::default() + } + } + + async fn get_our_category_role_overrides(&mut self) -> Vec { + if let Some((server, category)) = self.server.as_ref().zip(self.category.as_ref()) { + let member_roles = self + .member + .as_ref() + .map(|member| member.roles.clone()) + .unwrap_or_default(); + + let mut roles = category.role_permissions + .iter() + .filter(|(id, _)| member_roles.contains(id)) + .filter_map(|(id, permission)| { + server.roles.get(id).map(|role| { + let v: Override = (*permission).into(); + (role.rank, v) + }) + }) + .collect::>(); + + roles.sort_by(|a, b| b.0.cmp(&a.0)); + roles.into_iter().map(|(_, v)| v).collect() + } else { + vec![] + } + } + /// Do we own this group or saved messages channel if it is one of those? async fn do_we_own_the_channel(&mut self) -> bool { if let Some(channel) = &self.channel { @@ -375,6 +410,14 @@ impl PermissionQuery for DatabasePermissionQuery<'_> { } } } + + async fn set_category_from_channel(&mut self) { + if let Some((server, channel)) = self.server.as_ref().zip(self.channel.as_ref()) { + let category = server.categories.values().find(|c| c.channels.contains(&channel.id().to_string())).cloned(); + + self.category = category.map(Cow::Owned); + } + } } impl<'a> DatabasePermissionQuery<'a> { @@ -385,6 +428,7 @@ impl<'a> DatabasePermissionQuery<'a> { perspective, user: None, channel: None, + category: None, server: None, member: None, @@ -443,6 +487,13 @@ impl<'a> DatabasePermissionQuery<'a> { } } + pub fn category(self, category: &'a Category) -> DatabasePermissionQuery<'a> { + DatabasePermissionQuery { + category: Some(Cow::Borrowed(category)), + ..self + } + } + /// Use member pub fn member(self, member: &'a Member) -> DatabasePermissionQuery { DatabasePermissionQuery { diff --git a/crates/core/database/src/util/reference.rs b/crates/core/database/src/util/reference.rs index 9282d39d..3f03af21 100644 --- a/crates/core/database/src/util/reference.rs +++ b/crates/core/database/src/util/reference.rs @@ -10,7 +10,7 @@ use schemars::{ }; use crate::{ - Bot, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook, + Bot, Category, Channel, Database, Emoji, Invite, Member, Message, Server, ServerBan, User, Webhook }; /// Reference to some object in the database diff --git a/crates/core/models/src/v0/servers.rs b/crates/core/models/src/v0/servers.rs index ae5a50a2..59e2ec3e 100644 --- a/crates/core/models/src/v0/servers.rs +++ b/crates/core/models/src/v0/servers.rs @@ -28,8 +28,8 @@ auto_derived_partial!( // TODO: investigate if this is redundant and can be removed pub channels: Vec, /// Categories for this server - #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] - pub categories: Option>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))] + pub categories: HashMap, /// Configuration for sending system event messages #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub system_messages: Option, @@ -126,15 +126,52 @@ auto_derived!( #[cfg_attr(feature = "validator", derive(Validate))] pub struct Category { /// Unique ID for this category - #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))] pub id: String, /// Title for this category #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))] pub title: String, + /// Default permissions assigned to users in this channel + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub default_permissions: Option, + /// Permissions assigned based on role to this channel + #[cfg_attr( + feature = "serde", + serde( + default = "HashMap::::new", + skip_serializing_if = "HashMap::::is_empty" + ) + )] + pub role_permissions: HashMap, /// Channels in this category pub channels: Vec, } + #[cfg_attr(feature = "validator", derive(Validate))] + pub struct DataCreateCategory { + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))] + pub title: String, + /// Channels in this category + pub channels: Option>, + } + + #[cfg_attr(feature = "validator", derive(Validate))] + pub struct DataEditCategory { + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))] + pub title: Option, + /// Channels in this category + pub channels: Option>, + /// Fields to remove from category object + pub remove: Option>, + } + + pub enum FieldsCategory { + DefaultPermissions + } + + pub struct DataDefaultCategoryPermissions { + pub permissions: Override + } + /// System message channel assignments pub struct SystemMessageChannels { /// ID of channel to send user join messages in @@ -226,10 +263,6 @@ auto_derived!( pub icon: Option, /// Attachment Id for banner pub banner: Option, - - /// Category structure for server - #[cfg_attr(feature = "validator", validate)] - pub categories: Option>, /// System message configuration pub system_messages: Option, diff --git a/crates/core/permissions/src/impl.rs b/crates/core/permissions/src/impl.rs index 9975eeda..5fe92c9e 100644 --- a/crates/core/permissions/src/impl.rs +++ b/crates/core/permissions/src/impl.rs @@ -68,6 +68,26 @@ pub async fn calculate_server_permissions(query: &mut P) -> permissions } +pub async fn calculate_category_permissions(query: &mut P) -> PermissionValue { + let mut permissions = calculate_server_permissions(query).await; + + permissions.apply(query.get_default_category_permissions().await); + + for role_override in query.get_our_category_role_overrides().await { + permissions.apply(role_override) + } + + if !permissions.has_channel_permission(ChannelPermission::ViewChannel) { + permissions.revoke_all(); + } + + if query.are_we_timed_out().await { + permissions.restrict(*ALLOW_IN_TIMEOUT); + } + + permissions +} + /// Calculate permissions against a channel pub async fn calculate_channel_permissions(query: &mut P) -> PermissionValue { if query.are_we_privileged().await { @@ -109,11 +129,19 @@ pub async fn calculate_channel_permissions(query: &mut P) -> } ChannelType::ServerChannel => { query.set_server_from_channel().await; + query.set_category_from_channel().await; if query.are_we_server_owner().await { ChannelPermission::GrantAllSafe.into() } else if query.are_we_a_member().await { let mut permissions = calculate_server_permissions(query).await; + + permissions.apply(query.get_default_category_permissions().await); + + for role_override in query.get_our_category_role_overrides().await { + permissions.apply(role_override) + } + permissions.apply(query.get_default_channel_permissions().await); for role_override in query.get_our_channel_role_overrides().await { @@ -135,4 +163,4 @@ pub async fn calculate_channel_permissions(query: &mut P) -> } ChannelType::Unknown => 0_u64.into(), } -} +} \ No newline at end of file diff --git a/crates/core/permissions/src/trait.rs b/crates/core/permissions/src/trait.rs index d4a7c3d3..9a3bf1cf 100644 --- a/crates/core/permissions/src/trait.rs +++ b/crates/core/permissions/src/trait.rs @@ -51,6 +51,12 @@ pub trait PermissionQuery { /// Get the ordered role overrides (from lowest to highest) for this member in this channel async fn get_our_channel_role_overrides(&mut self) -> Vec; + /// Get the default category permissions + async fn get_default_category_permissions(&mut self) -> Override; + + /// Get the ordered role overrides (from lowest to highest) for this member in this category + async fn get_our_category_role_overrides(&mut self) -> Vec; + /// Do we own this group or saved messages channel if it is one of those? async fn do_we_own_the_channel(&mut self) -> bool; @@ -64,4 +70,7 @@ pub trait PermissionQuery { /// Set the current server as the server owning this channel /// (this will only ever be called for server channels, use unimplemented!() for other code paths) async fn set_server_from_channel(&mut self); + + //// SEts the current category as the category the channel is in + async fn set_category_from_channel(&mut self); } diff --git a/crates/core/result/src/axum.rs b/crates/core/result/src/axum.rs index e4caf818..11cbafca 100644 --- a/crates/core/result/src/axum.rs +++ b/crates/core/result/src/axum.rs @@ -42,12 +42,14 @@ impl IntoResponse for Error { ErrorType::Banned => StatusCode::FORBIDDEN, ErrorType::AlreadyInServer => StatusCode::CONFLICT, ErrorType::CannotTimeoutYourself => StatusCode::BAD_REQUEST, + ErrorType::UnknownCategory => StatusCode::NOT_FOUND, ErrorType::TooManyServers { .. } => StatusCode::BAD_REQUEST, ErrorType::TooManyEmbeds { .. } => StatusCode::BAD_REQUEST, ErrorType::TooManyEmoji { .. } => StatusCode::BAD_REQUEST, ErrorType::TooManyChannels { .. } => StatusCode::BAD_REQUEST, ErrorType::TooManyRoles { .. } => StatusCode::BAD_REQUEST, + ErrorType::TooManyCategories { .. } => StatusCode::BAD_REQUEST, ErrorType::ReachedMaximumBots => StatusCode::BAD_REQUEST, ErrorType::IsBot => StatusCode::BAD_REQUEST, diff --git a/crates/core/result/src/lib.rs b/crates/core/result/src/lib.rs index 10b56d5d..0623ab09 100644 --- a/crates/core/result/src/lib.rs +++ b/crates/core/result/src/lib.rs @@ -115,6 +115,10 @@ pub enum ErrorType { TooManyRoles { max: usize, }, + TooManyCategories { + max: usize, + }, + UnknownCategory, AlreadyInServer, CannotTimeoutYourself, diff --git a/crates/core/result/src/rocket.rs b/crates/core/result/src/rocket.rs index 1d996a17..72e4b256 100644 --- a/crates/core/result/src/rocket.rs +++ b/crates/core/result/src/rocket.rs @@ -49,12 +49,14 @@ impl<'r> Responder<'r, 'static> for Error { ErrorType::Banned => Status::Forbidden, ErrorType::AlreadyInServer => Status::Conflict, ErrorType::CannotTimeoutYourself => Status::BadRequest, - + ErrorType::UnknownCategory => Status::NotFound, + ErrorType::TooManyServers { .. } => Status::BadRequest, ErrorType::TooManyEmbeds { .. } => Status::BadRequest, ErrorType::TooManyEmoji { .. } => Status::BadRequest, ErrorType::TooManyChannels { .. } => Status::BadRequest, ErrorType::TooManyRoles { .. } => Status::BadRequest, + ErrorType::TooManyCategories { .. } => Status::BadRequest, ErrorType::ReachedMaximumBots => Status::BadRequest, ErrorType::IsBot => Status::BadRequest, diff --git a/crates/delta/src/routes/servers/category_create.rs b/crates/delta/src/routes/servers/category_create.rs new file mode 100644 index 00000000..c0f167c4 --- /dev/null +++ b/crates/delta/src/routes/servers/category_create.rs @@ -0,0 +1,45 @@ +use revolt_config::config; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, Category, Database, Role, User +}; +use revolt_models::v0; +use revolt_permissions::{calculate_server_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use validator::Validate; + +/// # Create a category +/// +/// Creates a new server category. +#[openapi(tag = "Server Categories")] +#[post("//categories", data = "")] +pub async fn create( + db: &State, + user: User, + target: Reference, + data: Json, +) -> Result> { + let data = data.into_inner(); + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let mut server = target.as_server(db).await?; + let mut query = DatabasePermissionQuery::new(db, &user).server(&server); + calculate_server_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; + + let config = config().await; + if server.categories.len() >= config.features.limits.global.server_categories { + return Err(create_error!(TooManyCategories { + max: config.features.limits.global.server_categories, + })); + }; + + let category = Category::create(db, &mut server, data).await?; + + Ok(Json(category.into())) +} diff --git a/crates/delta/src/routes/servers/category_delete.rs b/crates/delta/src/routes/servers/category_delete.rs new file mode 100644 index 00000000..888a871e --- /dev/null +++ b/crates/delta/src/routes/servers/category_delete.rs @@ -0,0 +1,38 @@ +use revolt_config::config; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, Category, Database, PartialCategory, Role, User +}; +use revolt_models::v0::{self, DataEditCategory}; +use revolt_permissions::{calculate_server_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use rocket_empty::EmptyResponse; +use validator::Validate; + +/// # Edits a category +/// +/// Edits a server category. +#[openapi(tag = "Server Categories")] +#[delete("//categories/")] +pub async fn delete( + db: &State, + user: User, + server: Reference, + category: String +) -> Result { + let mut server = server.as_server(db).await?; + + let category = server.categories + .get(&category) + .ok_or(create_error!(UnknownCategory))? + .clone(); + + let mut query = DatabasePermissionQuery::new(db, &user).server(&server); + calculate_server_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; + + category.delete(db, &mut server).await?; + + Ok(EmptyResponse) +} diff --git a/crates/delta/src/routes/servers/category_edit.rs b/crates/delta/src/routes/servers/category_edit.rs new file mode 100644 index 00000000..1af20fb3 --- /dev/null +++ b/crates/delta/src/routes/servers/category_edit.rs @@ -0,0 +1,62 @@ +use revolt_config::config; +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, Category, Database, PartialCategory, Role, User +}; +use revolt_models::v0::{self, DataEditCategory}; +use revolt_permissions::{calculate_server_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; +use validator::Validate; + +/// # Edits a category +/// +/// Edits a server category. +#[openapi(tag = "Server Categories")] +#[patch("//categories/", data = "")] +pub async fn edit( + db: &State, + user: User, + server: Reference, + category: String, + data: Json, +) -> Result> { + let data = data.into_inner(); + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let mut server = server.as_server(db).await?; + + let mut category = server.categories + .get(&category) + .ok_or(create_error!(UnknownCategory))? + .clone(); + + let mut query = DatabasePermissionQuery::new(db, &user).server(&server); + calculate_server_permissions(&mut query) + .await + .throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; + + let DataEditCategory { + title, + channels, + remove + } = data; + + category.update( + db, + &mut server, + PartialCategory { + title, + channels, + ..Default::default() + }, + remove + .map(|v| v.into_iter().map(Into::into).collect()) + .unwrap_or_default() + ).await?; + + Ok(Json(category.into())) +} diff --git a/crates/delta/src/routes/servers/category_permissions_set.rs b/crates/delta/src/routes/servers/category_permissions_set.rs new file mode 100644 index 00000000..54afd47f --- /dev/null +++ b/crates/delta/src/routes/servers/category_permissions_set.rs @@ -0,0 +1,50 @@ +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, Database, User +}; +use revolt_models::v0; +use revolt_permissions::{calculate_category_permissions, ChannelPermission, Override}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +/// # Set Role Permission +/// +/// Sets permissions for the specified role in this channel. +/// +/// Channel must be a `TextChannel` or `VoiceChannel`. +#[openapi(tag = "Channel Permissions")] +#[put("//categories//permissions/", data = "", rank = 2)] +pub async fn set_role_permissions( + db: &State, + user: User, + server: Reference, + category: String, + role_id: String, + data: Json, +) -> Result> { + let mut server = server.as_server(db).await?; + let mut category = server.categories.get(&category).ok_or(create_error!(UnknownCategory))?.clone(); + + let mut query = DatabasePermissionQuery::new(db, &user).server(&server).category(&category); + let permissions = calculate_category_permissions(&mut query).await; + + permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; + + if let Some(role) = server.roles.get(&role_id) { + if role.rank <= query.get_member_rank().unwrap_or(i64::MIN) { + return Err(create_error!(NotElevated)); + } + + let current_value: Override = role.permissions.into(); + permissions + .throw_permission_override(current_value, &data.permissions) + .await?; + + category + .set_role_permission(db, &mut server, role_id, data.permissions.clone().into()) + .await?; + + Ok(Json(category.into())) + } else { + Err(create_error!(NotFound)) + } +} diff --git a/crates/delta/src/routes/servers/category_permissions_set_default.rs b/crates/delta/src/routes/servers/category_permissions_set_default.rs new file mode 100644 index 00000000..24dc6863 --- /dev/null +++ b/crates/delta/src/routes/servers/category_permissions_set_default.rs @@ -0,0 +1,50 @@ +use revolt_database::{ + util::{permissions::DatabasePermissionQuery, reference::Reference}, Channel, Database, PartialCategory, PartialChannel, User +}; +use revolt_models::v0::{self, DataDefaultChannelPermissions}; +use revolt_permissions::{calculate_category_permissions, calculate_channel_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; +use rocket::{serde::json::Json, State}; + +/// # Set Default Permission +/// +/// Sets permissions for the default role in this channel. +/// +/// Channel must be a `Group`, `TextChannel` or `VoiceChannel`. +#[openapi(tag = "Category Permissions")] +#[put("//categories//permissions/default", data = "", rank = 1)] +pub async fn set_default_permissions( + db: &State, + user: User, + server: Reference, + target: String, + data: Json, +) -> Result> { + let data = data.into_inner(); + + let mut server = server.as_server(db).await?; + let mut category = server.categories.get(&target).ok_or(create_error!(UnknownCategory))?.clone(); + + let mut query = DatabasePermissionQuery::new(db, &user).server(&server).category(&category); + let permissions = calculate_category_permissions(&mut query).await; + + permissions.throw_if_lacking_channel_permission(ChannelPermission::ManagePermissions)?; + + permissions + .throw_permission_override(category.default_permissions.map(|x| x.into()), &data.permissions) + .await?; + + category + .update( + db, + &mut server, + PartialCategory { + default_permissions: Some(data.permissions.into()), + ..Default::default() + }, + vec![], + ) + .await?; + + Ok(Json(category.into())) +} diff --git a/crates/delta/src/routes/servers/mod.rs b/crates/delta/src/routes/servers/mod.rs index a7553625..cf0e1871 100644 --- a/crates/delta/src/routes/servers/mod.rs +++ b/crates/delta/src/routes/servers/mod.rs @@ -23,6 +23,11 @@ mod server_create; mod server_delete; mod server_edit; mod server_fetch; +mod category_create; +mod category_edit; +mod category_delete; +mod category_permissions_set_default; +mod category_permissions_set; pub fn routes() -> (Vec, OpenApi) { openapi_get_routes_spec![ @@ -47,6 +52,11 @@ pub fn routes() -> (Vec, OpenApi) { roles_delete::delete, permissions_set::set_role_permission, permissions_set_default::set_default_permissions, - emoji_list::list_emoji + emoji_list::list_emoji, + category_create::create, + category_edit::edit, + category_delete::delete, + category_permissions_set_default::set_default_permissions, + category_permissions_set::set_role_permission, ] } diff --git a/crates/delta/src/routes/servers/server_edit.rs b/crates/delta/src/routes/servers/server_edit.rs index 05688473..a9e549cf 100644 --- a/crates/delta/src/routes/servers/server_edit.rs +++ b/crates/delta/src/routes/servers/server_edit.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, @@ -38,7 +38,6 @@ pub async fn edit( && data.icon.is_none() && data.banner.is_none() && data.system_messages.is_none() - && data.categories.is_none() // && data.nsfw.is_none() && data.flags.is_none() && data.analytics.is_none() @@ -63,18 +62,11 @@ pub async fn edit( { return Err(create_error!(NotPrivileged)); } - - // Changing categories requires manage channel - if data.categories.is_some() { - permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?; - } - let v0::DataEditServer { name, description, icon, banner, - categories, system_messages, flags, // nsfw, @@ -86,7 +78,6 @@ pub async fn edit( let mut partial = PartialServer { name, description, - categories: categories.map(|v| v.into_iter().map(Into::into).collect()), system_messages: system_messages.map(Into::into), flags, // nsfw, @@ -119,23 +110,6 @@ pub async fn edit( } } - if let Some(categories) = &mut partial.categories { - let mut channel_ids = HashSet::new(); - for category in categories { - for channel in &category.channels { - if channel_ids.contains(channel) { - return Err(create_error!(InvalidOperation)); - } - - channel_ids.insert(channel.to_string()); - } - - category - .channels - .retain(|item| server.channels.contains(item)); - } - } - // 3. Apply new icon if let Some(icon) = icon { partial.icon = Some(File::use_server_icon(db, &icon, &server.id, &user.id).await?);