From cd0737911b8b4c538cdda2a9c0d9ae490b922ac9 Mon Sep 17 00:00:00 2001 From: Paul Makles Date: Fri, 27 Oct 2023 16:31:37 +0100 Subject: [PATCH] refactor: use fixtures to populate db refactor: method for creating server channels --- .../database/fixtures/server_with_roles.json | 89 ++++++++ .../database/src/models/channels/model.rs | 192 +++++++----------- .../core/database/src/models/servers/model.rs | 105 +--------- crates/core/database/src/util/mod.rs | 3 + crates/core/database/src/util/reference.rs | 7 +- .../core/database/src/util/test_fixtures.rs | 103 ++++++++++ crates/core/models/src/v0/channels.rs | 36 +++- .../src/routes/servers/channel_create.rs | 134 +++--------- crates/delta/src/routes/servers/mod.rs | 2 +- 9 files changed, 340 insertions(+), 331 deletions(-) create mode 100644 crates/core/database/fixtures/server_with_roles.json create mode 100644 crates/core/database/src/util/test_fixtures.rs diff --git a/crates/core/database/fixtures/server_with_roles.json b/crates/core/database/fixtures/server_with_roles.json new file mode 100644 index 00000000..065e21c3 --- /dev/null +++ b/crates/core/database/fixtures/server_with_roles.json @@ -0,0 +1,89 @@ +[ + { + "_object_type": "User", + "_id": "__ID:0__", + "username": "Owner", + "discriminator": "0001" + }, + { + "_object_type": "User", + "_id": "__ID:1__", + "username": "Moderator", + "discriminator": "0001" + }, + { + "_object_type": "User", + "_id": "__ID:2__", + "username": "User", + "discriminator": "0001" + }, + { + "_object_type": "Channel", + "_id": "__ID:3__", + "channel_type": "TextChannel", + "name": "General", + "server": "__ID:4__", + "default_permissions": { + "a": 0, + "d": 1048576 + }, + "role_permissions": { + "__ID:5__": { + "a": 1048576, + "d": 0 + } + } + }, + { + "_object_type": "Server", + "_id": "__ID:4__", + "owner": "__ID:0__", + "name": "Server", + "channels": ["__ID:3__"], + "roles": { + "__ID:5__": { + "name": "Moderator", + "permissions": { + "a": 545270208, + "d": 0 + }, + "rank": 3 + }, + "__ID:6__": { + "name": "Owner", + "permissions": { + "a": 0, + "d": 0 + }, + "rank": 0 + } + }, + "default_permissions": 4000322560 + }, + { + "_object_type": "ServerMember", + "_id": { + "user": "__ID:0__", + "server": "__ID:4__" + }, + "roles": ["__ID:6__"], + "joined_at": 1698318340195 + }, + { + "_object_type": "ServerMember", + "_id": { + "user": "__ID:1__", + "server": "__ID:4__" + }, + "roles": ["__ID:5__"], + "joined_at": 1698318340195 + }, + { + "_object_type": "ServerMember", + "_id": { + "user": "__ID:2__", + "server": "__ID:4__" + }, + "joined_at": 1698318340195 + } +] diff --git a/crates/core/database/src/models/channels/model.rs b/crates/core/database/src/models/channels/model.rs index 0f1ec626..52db968f 100644 --- a/crates/core/database/src/models/channels/model.rs +++ b/crates/core/database/src/models/channels/model.rs @@ -6,7 +6,10 @@ use revolt_permissions::OverrideField; use revolt_result::Result; use serde::{Deserialize, Serialize}; -use crate::{events::client::EventV1, Database, File, IntoDocumentPath, SystemMessage, User}; +use crate::{ + events::client::EventV1, Database, File, IntoDocumentPath, PartialServer, Server, + SystemMessage, User, +}; auto_derived!( #[serde(tag = "channel_type")] @@ -189,6 +192,67 @@ impl Channel { Ok(()) }*/ + /// Create a new server channel + pub async fn create_server_channel( + db: &Database, + server: &mut Server, + data: v0::DataCreateServerChannel, + update_server: bool, + ) -> Result { + let config = config().await; + if server.channels.len() > config.features.limits.default.server_channels { + return Err(create_error!(TooManyChannels { + max: config.features.limits.default.server_channels, + })); + }; + + let id = ulid::Ulid::new().to_string(); + let channel = match data.channel_type { + v0::LegacyServerChannelType::Text => Channel::TextChannel { + id: id.clone(), + server: server.id.to_owned(), + name: data.name, + description: data.description, + icon: None, + last_message_id: None, + default_permissions: None, + role_permissions: HashMap::new(), + nsfw: data.nsfw.unwrap_or(false), + }, + v0::LegacyServerChannelType::Voice => Channel::VoiceChannel { + id: id.clone(), + server: server.id.to_owned(), + name: data.name, + description: data.description, + icon: None, + default_permissions: None, + role_permissions: HashMap::new(), + nsfw: data.nsfw.unwrap_or(false), + }, + }; + + db.insert_channel(&channel).await?; + + if update_server { + server + .update( + db, + PartialServer { + channels: Some([server.channels.clone(), [id].into()].concat()), + ..Default::default() + }, + vec![], + ) + .await?; + + EventV1::ChannelCreate(channel.clone().into()) + .p(server.id.clone()) + .await; + } + + Ok(channel) + } + /// Create a group pub async fn create_group( db: &Database, @@ -627,130 +691,18 @@ impl IntoDocumentPath for FieldsChannel { #[cfg(test)] mod tests { - use std::collections::HashMap; + use revolt_permissions::{calculate_channel_permissions, ChannelPermission}; - use revolt_permissions::{calculate_channel_permissions, ChannelPermission, OverrideField}; - - use crate::{ - util::permissions::DatabasePermissionQuery, Channel, Member, MemberCompositeKey, Role, - Server, User, - }; + use crate::{fixture, util::permissions::DatabasePermissionQuery}; #[async_std::test] async fn permissions_text_channel() { database_test!(|db| async move { - let owner = User::create(&db, "Owner".to_string(), None, None) - .await - .unwrap(); - - let moderator = User::create(&db, "Moderator".to_string(), None, None) - .await - .unwrap(); - - let user = User::create(&db, "User".to_string(), None, None) - .await - .unwrap(); - - let server_id = ulid::Ulid::new().to_string(); - - let channel = Channel::TextChannel { - id: ulid::Ulid::new().to_string(), - server: server_id.clone(), - name: "Channel".to_string(), - description: None, - icon: None, - last_message_id: None, - default_permissions: Some(OverrideField { - d: 1048576, // TODO: bitfield - ..Default::default() - }), - role_permissions: HashMap::from([( - "01F9HFTSBWTNA2F4TMSV7VM3FG".to_string(), - OverrideField { - a: 1048576, // TODO: bitfield - ..Default::default() - }, - )]), - nsfw: false, - }; - - let server = Server { - id: server_id, - owner: owner.id.clone(), - name: "My Server".to_string(), - description: None, - channels: vec![channel.id()], - categories: None, - system_messages: None, - roles: HashMap::from([ - ( - "01F9HFTSBWTNA2F4TMSV7VM3FG".to_string(), - Role { - name: "Moderator".to_string(), - permissions: OverrideField { - a: 545270208, // TODO: explicit - ..Default::default() - }, - colour: None, - hoist: true, - rank: 3, - }, - ), - ( - "01FBF9DNHSRPVTWFMNB3JNB8FK".to_string(), - Role { - name: "Owner".to_string(), - permissions: Default::default(), - colour: None, - hoist: true, - rank: 0, - }, - ), - ]), - default_permissions: 4000322560, // TODO: use bitfield - icon: None, - banner: None, - flags: None, - nsfw: false, - analytics: false, - discoverable: false, - }; - - // TODO: proper creation - db.insert_channel(&channel).await.unwrap(); - server.create(&db).await.unwrap(); - - db.insert_member(&Member { - id: MemberCompositeKey { - user: owner.id.clone(), - server: server.id.clone(), - }, - roles: vec!["01FBF9DNHSRPVTWFMNB3JNB8FK".to_string()], - ..Default::default() - }) - .await - .unwrap(); - - db.insert_member(&Member { - id: MemberCompositeKey { - user: moderator.id.clone(), - server: server.id.clone(), - }, - roles: vec!["01F9HFTSBWTNA2F4TMSV7VM3FG".to_string()], - ..Default::default() - }) - .await - .unwrap(); - - db.insert_member(&Member { - id: MemberCompositeKey { - user: user.id.clone(), - server: server.id.clone(), - }, - ..Default::default() - }) - .await - .unwrap(); + fixture!(db, "server_with_roles", + owner user 0 + moderator user 1 + user user 2 + channel channel 3); let mut query = DatabasePermissionQuery::new(&db, &owner).channel(&channel); assert!(calculate_channel_permissions(&mut query) diff --git a/crates/core/database/src/models/servers/model.rs b/crates/core/database/src/models/servers/model.rs index 8c12eaa8..97b408d6 100644 --- a/crates/core/database/src/models/servers/model.rs +++ b/crates/core/database/src/models/servers/model.rs @@ -453,109 +453,18 @@ impl SystemMessageChannels { #[cfg(test)] mod tests { - use std::collections::HashMap; + use revolt_permissions::{calculate_server_permissions, ChannelPermission}; - use revolt_permissions::{calculate_server_permissions, ChannelPermission, OverrideField}; - - use crate::{ - util::permissions::DatabasePermissionQuery, Member, MemberCompositeKey, Role, Server, User, - }; + use crate::{fixture, util::permissions::DatabasePermissionQuery}; #[async_std::test] async fn permissions() { database_test!(|db| async move { - let owner = User::create(&db, "Owner".to_string(), None, None) - .await - .unwrap(); - - let moderator = User::create(&db, "Moderator".to_string(), None, None) - .await - .unwrap(); - - let user = User::create(&db, "User".to_string(), None, None) - .await - .unwrap(); - - let server_id = ulid::Ulid::new().to_string(); - - // TODO: seeder functions - // e.g. seed!("channel", "file.json") - let server = Server { - id: server_id, - owner: owner.id.clone(), - name: "My Server".to_string(), - description: None, - channels: vec![], - categories: None, - system_messages: None, - roles: HashMap::from([ - ( - "01F9HFTSBWTNA2F4TMSV7VM3FG".to_string(), - Role { - name: "Moderator".to_string(), - permissions: OverrideField { - a: 545270208, // TODO: explicit - ..Default::default() - }, - colour: None, - hoist: true, - rank: 3, - }, - ), - ( - "01FBF9DNHSRPVTWFMNB3JNB8FK".to_string(), - Role { - name: "Owner".to_string(), - permissions: Default::default(), - colour: None, - hoist: true, - rank: 0, - }, - ), - ]), - default_permissions: 4000322560, // TODO: use bitfield - icon: None, - banner: None, - flags: None, - nsfw: false, - analytics: false, - discoverable: false, - }; - - // TODO: proper creation - server.create(&db).await.unwrap(); - - db.insert_member(&Member { - id: MemberCompositeKey { - user: owner.id.clone(), - server: server.id.clone(), - }, - roles: vec!["01FBF9DNHSRPVTWFMNB3JNB8FK".to_string()], - ..Default::default() - }) - .await - .unwrap(); - - db.insert_member(&Member { - id: MemberCompositeKey { - user: moderator.id.clone(), - server: server.id.clone(), - }, - roles: vec!["01F9HFTSBWTNA2F4TMSV7VM3FG".to_string()], - ..Default::default() - }) - .await - .unwrap(); - - db.insert_member(&Member { - id: MemberCompositeKey { - user: user.id.clone(), - server: server.id.clone(), - }, - ..Default::default() - }) - .await - .unwrap(); + fixture!(db, "server_with_roles", + owner user 0 + moderator user 1 + user user 2 + server server 4); let mut query = DatabasePermissionQuery::new(&db, &owner).server(&server); assert!(calculate_server_permissions(&mut query) diff --git a/crates/core/database/src/util/mod.rs b/crates/core/database/src/util/mod.rs index 8f4f4c76..3315e41b 100644 --- a/crates/core/database/src/util/mod.rs +++ b/crates/core/database/src/util/mod.rs @@ -2,3 +2,6 @@ pub mod bridge; pub mod idempotency; pub mod permissions; pub mod reference; + +#[cfg(test)] +pub mod test_fixtures; diff --git a/crates/core/database/src/util/reference.rs b/crates/core/database/src/util/reference.rs index 033dd0e9..f1200644 100644 --- a/crates/core/database/src/util/reference.rs +++ b/crates/core/database/src/util/reference.rs @@ -7,7 +7,7 @@ use schemars::{ JsonSchema, }; -use crate::{Bot, Channel, Database, Emoji, Message, User, Webhook}; +use crate::{Bot, Channel, Database, Emoji, Message, Server, User, Webhook}; /// Reference to some object in the database #[derive(Serialize, Deserialize)] @@ -42,6 +42,11 @@ impl Reference { db.fetch_message(&self.id).await } + /// Fetch server from Ref + pub async fn as_server(&self, db: &Database) -> Result { + db.fetch_server(&self.id).await + } + /// Fetch user from Ref pub async fn as_user(&self, db: &Database) -> Result { db.fetch_user(&self.id).await diff --git a/crates/core/database/src/util/test_fixtures.rs b/crates/core/database/src/util/test_fixtures.rs new file mode 100644 index 00000000..0ec3acfb --- /dev/null +++ b/crates/core/database/src/util/test_fixtures.rs @@ -0,0 +1,103 @@ +use std::collections::HashMap; + +use once_cell::sync::Lazy; +use regex::{Captures, Regex}; +use serde_json::from_str; + +use crate::{Channel, Database, Member, Server, User}; + +static RE_ID: Lazy = Lazy::new(|| Regex::new("__ID:(\\d+)__").unwrap()); + +#[derive(Debug, Deserialize)] +#[serde(tag = "_object_type")] +enum LoadedFixture { + User(User), + Channel(Channel), + Server(Server), + ServerMember(Member), +} + +pub async fn load_fixture(db: &Database, input: &str) -> HashMap { + let mut ids = HashMap::::new(); + let input = RE_ID.replace_all(input, |cap: &Captures| { + let d = cap.get(1).unwrap().as_str(); + + if !ids.contains_key(d) { + ids.insert(d.to_string(), ulid::Ulid::new().to_string()); + } + + ids.get(d).unwrap().clone() + }); + + // Deserialise the fixtures + let items: Vec = from_str(&input).expect("Failed to deserialise fixture"); + + // Load all of the items within + for item in items { + #[allow(clippy::disallowed_methods)] + match item { + LoadedFixture::User(user) => db.insert_user(&user).await.unwrap(), + LoadedFixture::Channel(channel) => db.insert_channel(&channel).await.unwrap(), + LoadedFixture::Server(server) => db.insert_server(&server).await.unwrap(), + LoadedFixture::ServerMember(member) => db.insert_member(&member).await.unwrap(), + } + } + + // Return IDs for ease of use + ids +} + +#[async_trait] +pub trait FetchFixture { + async fn user(&self, db: &Database, d: usize) -> User; + async fn channel(&self, db: &Database, d: usize) -> Channel; + async fn server(&self, db: &Database, d: usize) -> Server; + async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member; +} + +#[async_trait] +impl FetchFixture for HashMap { + async fn user(&self, db: &Database, d: usize) -> User { + db.fetch_user(self.get(&d.to_string()).unwrap()) + .await + .unwrap() + } + + async fn channel(&self, db: &Database, d: usize) -> Channel { + db.fetch_channel(self.get(&d.to_string()).unwrap()) + .await + .unwrap() + } + + async fn server(&self, db: &Database, d: usize) -> Server { + db.fetch_server(self.get(&d.to_string()).unwrap()) + .await + .unwrap() + } + + async fn member(&self, db: &Database, d_server: usize, d_user: usize) -> Member { + db.fetch_member( + self.get(&d_server.to_string()).unwrap(), + self.get(&d_user.to_string()).unwrap(), + ) + .await + .unwrap() + } +} + +#[macro_export] +macro_rules! fixture { + ( $database:expr, $name:expr, $( $variable:ident $type:ident $id: expr )+ ) => { + use $crate::util::test_fixtures::FetchFixture; + + let fixtures = $crate::util::test_fixtures::load_fixture( + &$database, + include_str!(concat!("../../../fixtures/", $name, ".json")), + ) + .await; + + $( + let $variable = fixtures.$type(&$database, $id).await; + )+ + }; +} diff --git a/crates/core/models/src/v0/channels.rs b/crates/core/models/src/v0/channels.rs index 4e5f55b0..d06888d2 100644 --- a/crates/core/models/src/v0/channels.rs +++ b/crates/core/models/src/v0/channels.rs @@ -212,24 +212,52 @@ auto_derived!( #[cfg_attr(feature = "validator", derive(validator::Validate))] pub struct DataCreateGroup { /// Group name - #[validate(length(min = 1, max = 32))] + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))] pub name: String, /// Group description - #[validate(length(min = 0, max = 1024))] + #[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))] pub description: Option, /// Group icon - #[validate(length(min = 1, max = 128))] + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 128)))] pub icon: Option, /// Array of user IDs to add to the group /// /// Must be friends with these users. - #[validate(length(min = 0, max = 49))] + #[cfg_attr(feature = "validator", validate(length(min = 0, max = 49)))] #[serde(default)] pub users: HashSet, /// Whether this group is age-restricted #[serde(skip_serializing_if = "Option::is_none")] pub nsfw: Option, } + + /// Server Channel Type + #[derive(Default)] + pub enum LegacyServerChannelType { + /// Text Channel + #[default] + Text, + /// Voice Channel + Voice, + } + + /// Create new server channel + #[derive(Default)] + #[cfg_attr(feature = "validator", derive(validator::Validate))] + pub struct DataCreateServerChannel { + /// Channel type + #[serde(rename = "type", default = "LegacyServerChannelType::default")] + pub channel_type: LegacyServerChannelType, + /// Channel name + #[cfg_attr(feature = "validator", validate(length(min = 1, max = 32)))] + pub name: String, + /// Channel description + #[cfg_attr(feature = "validator", validate(length(min = 0, max = 1024)))] + pub description: Option, + /// Whether this channel is age restricted + #[serde(skip_serializing_if = "Option::is_none")] + pub nsfw: Option, + } ); impl Channel { diff --git a/crates/delta/src/routes/servers/channel_create.rs b/crates/delta/src/routes/servers/channel_create.rs index 51e856be..ae35bb46 100644 --- a/crates/delta/src/routes/servers/channel_create.rs +++ b/crates/delta/src/routes/servers/channel_create.rs @@ -1,119 +1,39 @@ -use std::collections::HashMap; - -use revolt_quark::{ - models::{server::PartialServer, Channel, User}, - perms, Db, Error, Permission, Ref, Result, variables::delta::MAX_CHANNEL_COUNT, -}; +use revolt_database::util::permissions::DatabasePermissionQuery; +use revolt_database::{util::reference::Reference, Channel, Database, User}; +use revolt_models::v0; +use revolt_permissions::{calculate_server_permissions, ChannelPermission}; +use revolt_result::{create_error, Result}; use rocket::serde::json::Json; -use serde::{Deserialize, Serialize}; -use ulid::Ulid; +use rocket::State; use validator::Validate; -/// # Channel Type -#[derive(Serialize, Deserialize, JsonSchema, Default)] -enum ChannelType { - /// Text Channel - #[default] - Text, - /// Voice Channel - Voice, -} - -/// # Channel Data -#[derive(Validate, Serialize, Deserialize, JsonSchema)] -pub struct DataCreateChannel { - /// Channel type - #[serde(rename = "type", default = "ChannelType::default")] - channel_type: ChannelType, - /// Channel name - #[validate(length(min = 1, max = 32))] - name: String, - /// Channel description - #[validate(length(min = 0, max = 1024))] - description: Option, - /// Whether this channel is age restricted - #[serde(skip_serializing_if = "Option::is_none")] - nsfw: Option, -} - /// # Create Channel /// /// Create a new Text or Voice channel. #[openapi(tag = "Server Information")] -#[post("//channels", data = "")] -pub async fn req( - db: &Db, +#[post("//channels", data = "")] +pub async fn create_server_channel( + db: &State, user: User, - target: Ref, - info: Json, -) -> Result> { - let info = info.into_inner(); - info.validate() - .map_err(|error| Error::FailedValidation { error })?; + server: 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?; - perms(&user) - .server(&server) - .throw_permission(db, Permission::ManageChannel) - .await?; + let mut server = server.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)?; - if server.channels.len() > *MAX_CHANNEL_COUNT { - return Err(Error::TooManyChannels { max: *MAX_CHANNEL_COUNT }) - }; - - let id = Ulid::new().to_string(); - let mut channels = server.channels.clone(); - channels.push(id.clone()); - - let DataCreateChannel { - name, - description, - nsfw, - channel_type, - } = info; - let channel = match channel_type { - ChannelType::Text => Channel::TextChannel { - id, - server: server.id.clone(), - - name, - description, - - icon: None, - last_message_id: None, - - default_permissions: None, - role_permissions: HashMap::new(), - - nsfw: nsfw.unwrap_or(false), - }, - ChannelType::Voice => Channel::VoiceChannel { - id, - server: server.id.clone(), - - name, - description, - icon: None, - - default_permissions: None, - role_permissions: HashMap::new(), - - nsfw: nsfw.unwrap_or(false), - }, - }; - - channel.create(db).await?; - server - .update( - db, - PartialServer { - channels: Some(channels), - ..Default::default() - }, - vec![], - ) - .await?; - - Ok(Json(channel)) + Channel::create_server_channel(db, &mut server, data, true) + .await + .map(|channel| channel.into()) + .map(Json) } diff --git a/crates/delta/src/routes/servers/mod.rs b/crates/delta/src/routes/servers/mod.rs index a3ebeb75..96bf0452 100644 --- a/crates/delta/src/routes/servers/mod.rs +++ b/crates/delta/src/routes/servers/mod.rs @@ -30,7 +30,7 @@ pub fn routes() -> (Vec, OpenApi) { server_fetch::req, server_edit::req, server_ack::req, - channel_create::req, + channel_create::create_server_channel, member_fetch_all::req, member_remove::req, member_fetch::req,