feat: initial category permissions and rework of category crud routes

This commit is contained in:
Zomatree
2025-05-06 17:48:38 +01:00
parent f75d635c81
commit c7f09ab9d0
19 changed files with 550 additions and 58 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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<String>,
/// Categories for this server
#[serde(skip_serializing_if = "Option::is_none")]
pub categories: Option<Vec<Category>>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub categories: HashMap<String, Category>,
/// Configuration for sending system event messages
#[serde(skip_serializing_if = "Option::is_none")]
pub system_messages: Option<SystemMessageChannels>,
@@ -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<OverrideField>,
/// Permissions assigned based on role to this category
#[serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)]
pub role_permissions: HashMap<String, OverrideField>,
/// Channels in this category
pub channels: Vec<String>,
}
},
"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<Category> {
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<FieldsCategory>) -> 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<String> {
let mut ids = HashSet::new();

View File

@@ -729,9 +729,7 @@ impl From<crate::Server> 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<Server> 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<crate::PartialServer> 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<PartialServer> 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<crate::Category> 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<Category> for crate::Category {
id: value.id,
title: value.title,
channels: value.channels,
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
}
}
}
impl From<crate::FieldsCategory> for FieldsCategory {
fn from(value: crate::FieldsCategory) -> Self {
match value {
crate::FieldsCategory::DefaultPermissions => FieldsCategory::DefaultPermissions,
}
}
}
impl From<FieldsCategory> for crate::FieldsCategory {
fn from(value: FieldsCategory) -> crate::FieldsCategory {
match value {
FieldsCategory::DefaultPermissions => crate::FieldsCategory::DefaultPermissions,
}
}
}

View File

@@ -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<Cow<'a, User>>,
channel: Option<Cow<'a, Channel>>,
category: Option<Cow<'a, Category>>,
server: Option<Cow<'a, Server>>,
member: Option<Cow<'a, Member>>,
@@ -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<Override> {
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::<Vec<(i64, Override)>>();
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 {

View File

@@ -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

View File

@@ -28,8 +28,8 @@ auto_derived_partial!(
// TODO: investigate if this is redundant and can be removed
pub channels: Vec<String>,
/// Categories for this server
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub categories: Option<Vec<Category>>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "HashMap::is_empty"))]
pub categories: HashMap<String, Category>,
/// Configuration for sending system event messages
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub system_messages: Option<SystemMessageChannels>,
@@ -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<OverrideField>,
/// Permissions assigned based on role to this channel
#[cfg_attr(
feature = "serde",
serde(
default = "HashMap::<String, OverrideField>::new",
skip_serializing_if = "HashMap::<String, OverrideField>::is_empty"
)
)]
pub role_permissions: HashMap<String, OverrideField>,
/// Channels in this category
pub channels: Vec<String>,
}
#[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<Vec<String>>,
}
#[cfg_attr(feature = "validator", derive(Validate))]
pub struct DataEditCategory {
#[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))]
pub title: Option<String>,
/// Channels in this category
pub channels: Option<Vec<String>>,
/// Fields to remove from category object
pub remove: Option<Vec<FieldsCategory>>,
}
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<String>,
/// Attachment Id for banner
pub banner: Option<String>,
/// Category structure for server
#[cfg_attr(feature = "validator", validate)]
pub categories: Option<Vec<Category>>,
/// System message configuration
pub system_messages: Option<SystemMessageChannels>,

View File

@@ -68,6 +68,26 @@ pub async fn calculate_server_permissions<P: PermissionQuery>(query: &mut P) ->
permissions
}
pub async fn calculate_category_permissions<P: PermissionQuery>(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<P: PermissionQuery>(query: &mut P) -> PermissionValue {
if query.are_we_privileged().await {
@@ -109,11 +129,19 @@ pub async fn calculate_channel_permissions<P: PermissionQuery>(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<P: PermissionQuery>(query: &mut P) ->
}
ChannelType::Unknown => 0_u64.into(),
}
}
}

View File

@@ -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<Override>;
/// 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<Override>;
/// 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);
}

View File

@@ -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,

View File

@@ -115,6 +115,10 @@ pub enum ErrorType {
TooManyRoles {
max: usize,
},
TooManyCategories {
max: usize,
},
UnknownCategory,
AlreadyInServer,
CannotTimeoutYourself,

View File

@@ -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,

View File

@@ -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("/<target>/categories", data = "<data>")]
pub async fn create(
db: &State<Database>,
user: User,
target: Reference,
data: Json<v0::DataCreateCategory>,
) -> Result<Json<v0::Category>> {
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()))
}

View File

@@ -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("/<server>/categories/<category>")]
pub async fn delete(
db: &State<Database>,
user: User,
server: Reference,
category: String
) -> Result<EmptyResponse> {
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)
}

View File

@@ -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("/<server>/categories/<category>", data = "<data>")]
pub async fn edit(
db: &State<Database>,
user: User,
server: Reference,
category: String,
data: Json<v0::DataEditCategory>,
) -> Result<Json<v0::Category>> {
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()))
}

View File

@@ -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("/<server>/categories/<category>/permissions/<role_id>", data = "<data>", rank = 2)]
pub async fn set_role_permissions(
db: &State<Database>,
user: User,
server: Reference,
category: String,
role_id: String,
data: Json<v0::DataSetRolePermissions>,
) -> Result<Json<v0::Category>> {
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))
}
}

View File

@@ -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("/<server>/categories/<target>/permissions/default", data = "<data>", rank = 1)]
pub async fn set_default_permissions(
db: &State<Database>,
user: User,
server: Reference,
target: String,
data: Json<v0::DataDefaultCategoryPermissions>,
) -> Result<Json<v0::Category>> {
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()))
}

View File

@@ -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<Route>, OpenApi) {
openapi_get_routes_spec![
@@ -47,6 +52,11 @@ pub fn routes() -> (Vec<Route>, 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,
]
}

View File

@@ -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?);