Compare commits

...

5 Commits

Author SHA1 Message Date
Zomatree
c020ef97ab fix: add MoveChannels permission and check permissions when moving channels 2025-07-31 14:39:46 +01:00
Zomatree
e346464720 fix: reference correct function name 2025-06-08 01:45:20 +01:00
Zomatree
3ab5fda21d chore: add unit test 2025-05-07 15:30:23 +01:00
Zomatree
9e17a5e73a feat: add parent and update permission calculators 2025-05-07 00:55:03 +01:00
Zomatree
c7f09ab9d0 feat: initial category permissions and rework of category crud routes 2025-05-06 17:48:38 +01:00
25 changed files with 831 additions and 63 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

@@ -100,6 +100,10 @@ auto_derived!(
)]
role_permissions: HashMap<String, OverrideField>,
/// Category the channel is in
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
@@ -131,6 +135,10 @@ auto_derived!(
)]
role_permissions: HashMap<String, OverrideField>,
/// Category the channel is in
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
/// Whether this channel is marked as not safe for work
#[serde(skip_serializing_if = "crate::if_false", default)]
nsfw: bool,
@@ -161,6 +169,8 @@ auto_derived!(
pub default_permissions: Option<OverrideField>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
}
/// Optional fields on channel object
@@ -219,6 +229,7 @@ impl Channel {
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
parent: None
},
v0::LegacyServerChannelType::Voice => Channel::VoiceChannel {
id: id.clone(),
@@ -229,6 +240,7 @@ impl Channel {
default_permissions: None,
role_permissions: HashMap::new(),
nsfw: data.nsfw.unwrap_or(false),
parent: None
},
};
@@ -437,6 +449,13 @@ impl Channel {
}
}
pub fn parent(&self) -> Option<&str> {
match self {
Channel::TextChannel { parent, .. } | Channel::VoiceChannel { parent, .. } => parent.as_deref(),
_ => None
}
}
/// Set role permission on a channel
pub async fn set_role_permission(
&mut self,

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

@@ -187,6 +187,7 @@ impl From<crate::Channel> for Channel {
last_message_id,
default_permissions,
role_permissions,
parent,
nsfw,
} => Channel::TextChannel {
id,
@@ -197,6 +198,7 @@ impl From<crate::Channel> for Channel {
last_message_id,
default_permissions,
role_permissions,
parent,
nsfw,
},
crate::Channel::VoiceChannel {
@@ -207,6 +209,7 @@ impl From<crate::Channel> for Channel {
icon,
default_permissions,
role_permissions,
parent,
nsfw,
} => Channel::VoiceChannel {
id,
@@ -216,6 +219,7 @@ impl From<crate::Channel> for Channel {
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
parent,
nsfw,
},
}
@@ -267,6 +271,7 @@ impl From<Channel> for crate::Channel {
last_message_id,
default_permissions,
role_permissions,
parent,
nsfw,
} => crate::Channel::TextChannel {
id,
@@ -277,6 +282,7 @@ impl From<Channel> for crate::Channel {
last_message_id,
default_permissions,
role_permissions,
parent,
nsfw,
},
Channel::VoiceChannel {
@@ -287,6 +293,7 @@ impl From<Channel> for crate::Channel {
icon,
default_permissions,
role_permissions,
parent,
nsfw,
} => crate::Channel::VoiceChannel {
id,
@@ -296,6 +303,7 @@ impl From<Channel> for crate::Channel {
icon: icon.map(|file| file.into()),
default_permissions,
role_permissions,
parent,
nsfw,
},
}
@@ -315,6 +323,7 @@ impl From<crate::PartialChannel> for PartialChannel {
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
parent: value.parent,
}
}
}
@@ -332,6 +341,7 @@ impl From<PartialChannel> for crate::PartialChannel {
role_permissions: value.role_permissions,
default_permissions: value.default_permissions,
last_message_id: value.last_message_id,
parent: value.parent,
}
}
}
@@ -729,9 +739,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 +765,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 +791,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 +815,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 +861,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 +873,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::{
DEFAULT_PERMISSION_DIRECT_MESSAGE,
};
use crate::{Channel, Database, Member, Server, User};
use crate::{Category, Channel, Database, Member, Server, User};
#[derive(Clone)]
pub struct BulkDatabasePermissionQuery<'a> {
@@ -197,7 +197,7 @@ async fn calculate_members_permissions<'a>(
) -> HashMap<String, PermissionValue> {
let mut resp = HashMap::new();
let (_, channel_role_permissions, channel_default_permissions) = match query
let (_, channel_role_permissions, channel_default_permissions, parent) = match query
.channel
.as_ref()
.expect("A channel must be assigned to calculate channel permissions")
@@ -207,14 +207,16 @@ async fn calculate_members_permissions<'a>(
id,
role_permissions,
default_permissions,
parent,
..
}
| Channel::VoiceChannel {
id,
role_permissions,
default_permissions,
parent,
..
} => (id, role_permissions, default_permissions),
} => (id, role_permissions, default_permissions, parent),
_ => panic!("Calculation of member permissions must be done on a server channel"),
};
@@ -268,6 +270,8 @@ async fn calculate_members_permissions<'a>(
.map(|m| (&m.id.user, m)),
);
let category = parent.map(|parent| query.server.categories.get(&parent).expect("Channel parent doesnt exist"));
for user in users {
let member = members.get(&user.id);
@@ -298,11 +302,39 @@ async fn calculate_members_permissions<'a>(
// Get the user's server permissions
let mut permission = calculate_server_permissions(&query.server, user, member);
// Apply category overrides
if let Some(category) = category {
if let Some(defaults) = category.default_permissions {
permission.apply(defaults.into());
};
// Get the applicable role overrides for the category
let mut roles = category.role_permissions
.iter()
.filter(|(id, _)| member.roles.contains(id))
.filter_map(|(id, permission)| {
query.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));
let overrides = roles.into_iter().map(|(_, v)| v);
for role_override in overrides {
permission.apply(role_override)
}
};
// apply channel overrides
if let Some(defaults) = channel_default_permissions {
permission.apply(defaults.into());
}
// Get the applicable role overrides
// Get the applicable role overrides for the channel
let mut roles = channel_role_permissions
.iter()
.filter(|(id, _)| member.roles.contains(id))

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 = channel.parent().and_then(|id| server.categories.get(id)).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

@@ -101,6 +101,10 @@ auto_derived!(
)]
role_permissions: HashMap<String, OverrideField>,
/// Category the channel is in
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
/// Whether this channel is marked as not safe for work
#[cfg_attr(
feature = "serde",
@@ -138,6 +142,10 @@ auto_derived!(
)]
role_permissions: HashMap<String, OverrideField>,
/// Category the channel is in
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
/// Whether this channel is marked as not safe for work
#[cfg_attr(
feature = "serde",
@@ -170,6 +178,8 @@ auto_derived!(
pub default_permissions: Option<OverrideField>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub last_message_id: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub parent: Option<String>
}
/// Optional fields on channel object

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

@@ -96,8 +96,10 @@ pub enum ChannelPermission {
/// Mention roles
MentionRoles = 1 << 38,
MoveChannels = 1 << 39,
// * Misc. permissions
// % Bits 38 to 52: free area
// % Bits 40 to 52: free area
// % Bits 53 to 64: do not use
// * Grant all permissions

View File

@@ -91,6 +91,18 @@ async fn validate_user_permissions() {
async fn set_server_from_channel(&mut self) {
unreachable!()
}
async fn get_default_category_permissions(&mut self) -> Override {
unreachable!()
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
unreachable!()
}
async fn set_category_from_channel(&mut self) {
unreachable!()
}
}
}
@@ -183,6 +195,18 @@ async fn validate_group_permissions() {
async fn set_server_from_channel(&mut self) {
unreachable!()
}
async fn get_default_category_permissions(&mut self) -> Override {
unreachable!()
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
unreachable!()
}
async fn set_category_from_channel(&mut self) {
unreachable!()
}
}
}
@@ -287,6 +311,18 @@ async fn validate_server_permissions() {
async fn set_server_from_channel(&mut self) {
// no-op
}
async fn get_default_category_permissions(&mut self) -> Override {
Override { allow: 0, deny: 0 }
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn set_category_from_channel(&mut self) {
// no-op
}
}
}
@@ -373,5 +409,126 @@ async fn validate_timed_out_member() {
async fn set_server_from_channel(&mut self) {
// no-op
}
async fn get_default_category_permissions(&mut self) -> Override {
Override { allow: 0, deny: 0 }
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn set_category_from_channel(&mut self) {
// no-op
}
}
}
#[async_std::test]
async fn validate_category_permissions() {
/// Scenario in which we are in a channel in a server and has a role
/// Server defaults let us send messages, view channels
/// Category default denies sending, allows reacting
/// Category role allows sending message
struct Scenario {}
let mut query = Scenario {};
let perms = calculate_channel_permissions(&mut query).await;
let value: u64 = perms.into();
assert_eq!(value, ChannelPermission::ViewChannel as u64 | ChannelPermission::React as u64 | ChannelPermission::SendMessage as u64);
#[async_trait]
impl PermissionQuery for Scenario {
async fn are_we_privileged(&mut self) -> bool {
false
}
async fn are_we_a_bot(&mut self) -> bool {
unreachable!()
}
async fn are_the_users_same(&mut self) -> bool {
unreachable!()
}
async fn user_relationship(&mut self) -> RelationshipStatus {
unreachable!()
}
async fn user_is_bot(&mut self) -> bool {
unreachable!()
}
async fn have_mutual_connection(&mut self) -> bool {
unreachable!()
}
async fn are_we_server_owner(&mut self) -> bool {
false
}
async fn are_we_a_member(&mut self) -> bool {
true
}
async fn get_default_server_permissions(&mut self) -> u64 {
ChannelPermission::SendMessage as u64 | ChannelPermission::ViewChannel as u64
}
async fn get_our_server_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn are_we_timed_out(&mut self) -> bool {
false
}
async fn get_channel_type(&mut self) -> ChannelType {
ChannelType::ServerChannel
}
async fn get_default_channel_permissions(&mut self) -> Override {
Override { allow: 0, deny: 0 }
}
async fn get_our_channel_role_overrides(&mut self) -> Vec<Override> {
vec![]
}
async fn do_we_own_the_channel(&mut self) -> bool {
unreachable!()
}
async fn are_we_part_of_the_channel(&mut self) -> bool {
unreachable!()
}
async fn set_recipient_as_user(&mut self) {
unreachable!()
}
async fn set_server_from_channel(&mut self) {
// no-op
}
async fn get_default_category_permissions(&mut self) -> Override {
Override {
allow: ChannelPermission::React as u64,
deny: ChannelPermission::SendMessage as u64
}
}
async fn get_our_category_role_overrides(&mut self) -> Vec<Override> {
vec![
Override {
allow: ChannelPermission::SendMessage as u64,
deny: 0
}
]
}
async fn set_category_from_channel(&mut self) {
// no-op
}
}
}

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

@@ -192,6 +192,7 @@ mod test {
d: ChannelPermission::ViewChannel as i64,
}),
last_message_id: None,
parent: None,
};
locked_channel
.update(&harness.db, partial, vec![])

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,107 @@
use revolt_config::config;
use revolt_database::{
util::{permissions::DatabasePermissionQuery, reference::Reference}, Category, Channel, Database, PartialCategory, PartialChannel, 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).category(&category);
let permissions = calculate_server_permissions(&mut query)
.await;
permissions.throw_if_lacking_channel_permission(ChannelPermission::ManageChannel)?;
let DataEditCategory {
title,
mut channels,
remove
} = data;
if channels.is_some() {
permissions.throw_if_lacking_channel_permission(ChannelPermission::MoveChannels)?;
}
// if any channel we are adding to this category are in another category we need to make sure we have MoveChannels in the category before moving it over.
for category in server.categories.values() {
if category.channels.iter().any(|c| channels.as_ref().is_some_and(|cs| cs.contains(c))) {
let mut category_query = query.clone().category(category);
calculate_server_permissions(&mut category_query)
.await
.throw_if_lacking_channel_permission(ChannelPermission::MoveChannels)?;
}
}
// remove the channels from any existing categories to avoid it having two
for category in server.categories.values_mut() {
category.channels.retain(|c| channels.as_ref().is_some_and(|cs| cs.contains(c)));
}
// only keep channels which exist in the server
if let Some(channels) = &mut channels {
channels.retain(|c| server.channels.contains(c));
}
// unset parent from all channels which are removed from the category
for channel_id in &category.channels {
if channels.as_ref().is_some_and(|cs| !cs.contains(channel_id)) {
db.update_channel(channel_id, &PartialChannel { parent: None, ..Default::default() }, Vec::new()).await?;
};
};
// update the category with the new values
category.update(
db,
&mut server,
PartialCategory {
title,
channels: channels.clone(),
..Default::default()
},
remove
.map(|v| v.into_iter().map(Into::into).collect())
.unwrap_or_default()
).await?;
let channels = db.fetch_channels(&channels.unwrap_or_default()).await?;
// update all channels to have the parent set
for channel in channels {
if let Channel::TextChannel { ref parent, .. } | Channel::VoiceChannel { ref parent, .. } = channel {
if parent.as_ref() != Some(&category.id) {
db.update_channel(channel.id(), &PartialChannel { parent: Some(category.id.clone()), ..Default::default() }, Vec::new()).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_permissions,
]
}

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