diff --git a/README.md b/README.md index d0ab1f4..ebb1c26 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ - [ ] [Video chat support](https://github.com/polyphony-chat/chorus/issues/49) ### Permissions and Roles -- [ ] [Role management](https://github.com/polyphony-chat/chorus/issues/46) (creation, deletion, modification) +- [x] [Role management](https://github.com/polyphony-chat/chorus/issues/46) (creation, deletion, modification) - [ ] [Permission management](https://github.com/polyphony-chat/chorus/issues/46) (assigning and revoking permissions) -- [ ] [Channel-specific permissions](https://github.com/polyphony-chat/chorus/issues/88) -- [ ] Role-based access control +- [x] [Channel-specific permissions](https://github.com/polyphony-chat/chorus/issues/88) +- [x] Role-based access control ### Guild Management - [x] Guild creation diff --git a/src/api/channels/mod.rs b/src/api/channels/mod.rs index 28a9dd3..94b42b6 100644 --- a/src/api/channels/mod.rs +++ b/src/api/channels/mod.rs @@ -1,7 +1,9 @@ pub mod channels; pub mod messages; +pub mod permissions; pub mod reactions; pub use channels::*; pub use messages::*; +pub use permissions::*; pub use reactions::*; diff --git a/src/api/channels/permissions.rs b/src/api/channels/permissions.rs new file mode 100644 index 0000000..0005d15 --- /dev/null +++ b/src/api/channels/permissions.rs @@ -0,0 +1,95 @@ +use reqwest::Client; +use serde_json::to_string; + +use crate::{ + errors::ChorusLibError, + instance::UserMeta, + limit::LimitedRequester, + types::{self, PermissionOverwrite}, +}; + +impl types::Channel { + /// Edits the permission overwrites for a channel. + /// + /// # Arguments + /// + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// * `channel_id` - A string slice representing the ID of the channel. + /// * `overwrite` - A [`PermissionOverwrite`] instance representing the new permission overwrites. + /// + /// # Returns + /// + /// This function returns [`None`] if the request is successful, otherwise it returns a [`ChorusLibError`] instance. + pub async fn edit_permissions( + user: &mut UserMeta, + channel_id: &str, + overwrite: PermissionOverwrite, + ) -> Option { + let mut belongs_to = user.belongs_to.borrow_mut(); + let url = format!( + "{}/channels/{}/permissions/{}", + belongs_to.urls.get_api(), + channel_id, + overwrite.id + ); + let body = match to_string(&overwrite) { + Ok(string) => string, + Err(e) => { + return Some(ChorusLibError::FormCreationError { + error: e.to_string(), + }); + } + }; + let request = Client::new().put(url).bearer_auth(user.token()).body(body); + LimitedRequester::new() + .await + .send_request( + request, + crate::api::limits::LimitType::Channel, + &mut belongs_to.limits, + &mut user.limits, + ) + .await + .unwrap(); + None + } + + /// Deletes a permission overwrite for a channel. + /// + /// # Arguments + /// + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// * `channel_id` - A string slice representing the ID of the channel. + /// * `overwrite_id` - A string slice representing the ID of the permission overwrite to delete. + /// + /// # Returns + /// + /// This function returns [`None`] if the request is successful, otherwise it returns a [`ChorusLibError`] instance. + pub async fn delete_permission( + user: &mut UserMeta, + channel_id: &str, + overwrite_id: &str, + ) -> Option { + let mut belongs_to = user.belongs_to.borrow_mut(); + let url = format!( + "{}/channels/{}/permissions/{}", + belongs_to.urls.get_api(), + channel_id, + overwrite_id + ); + let request = Client::new().delete(url).bearer_auth(user.token()); + let response = LimitedRequester::new() + .await + .send_request( + request, + crate::api::limits::LimitType::Channel, + &mut belongs_to.limits, + &mut user.limits, + ) + .await; + if response.is_err() { + return Some(response.err().unwrap()); + } + None + } +} diff --git a/src/api/guilds/member.rs b/src/api/guilds/member.rs index b301b99..a6ec7d7 100644 --- a/src/api/guilds/member.rs +++ b/src/api/guilds/member.rs @@ -1,8 +1,60 @@ use reqwest::Client; +use serde_json::from_str; -use crate::{instance::UserMeta, limit::LimitedRequester, types}; +use crate::{errors::ChorusLibError, instance::UserMeta, limit::LimitedRequester, types}; impl types::GuildMember { + /// Retrieves a guild member by their ID. + /// + /// # Arguments + /// + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// * `guild_id` - The ID of the guild. + /// * `member_id` - The ID of the member. + /// + /// # Returns + /// + /// A [`Result`] containing a [`GuildMember`] if the request succeeds, or a [`ChorusLibError`] if the request fails. + pub async fn get( + user: &mut UserMeta, + guild_id: &str, + member_id: &str, + ) -> Result { + let mut belongs_to = user.belongs_to.borrow_mut(); + let url = format!( + "{}/guilds/{}/members/{}/", + belongs_to.urls.get_api(), + guild_id, + member_id + ); + let request = Client::new().get(url).bearer_auth(user.token()); + let response = LimitedRequester::new() + .await + .send_request( + request, + crate::api::limits::LimitType::Guild, + &mut belongs_to.limits, + &mut user.limits, + ) + .await + .unwrap(); + let response_text = match response.text().await { + Ok(string) => string, + Err(e) => { + return Err(ChorusLibError::InvalidResponseError { + error: e.to_string(), + }) + } + }; + let member = from_str::(&response_text); + if member.is_err() { + return Err(ChorusLibError::InvalidResponseError { + error: member.err().unwrap().to_string(), + }); + } + Ok(member.unwrap()) + } + /// Adds a role to a guild member. /// /// # Arguments diff --git a/src/api/guilds/roles.rs b/src/api/guilds/roles.rs index a65b36d..b2e9b31 100644 --- a/src/api/guilds/roles.rs +++ b/src/api/guilds/roles.rs @@ -30,7 +30,7 @@ impl types::RoleObject { let mut belongs_to = user.belongs_to.borrow_mut(); let url = format!("{}/guilds/{}/roles/", belongs_to.urls.get_api(), guild_id); let request = Client::new().get(url).bearer_auth(user.token()); - let requester = match LimitedRequester::new() + let result = match LimitedRequester::new() .await .send_request( request, @@ -43,7 +43,14 @@ impl types::RoleObject { Ok(request) => request, Err(e) => return Err(e), }; - let roles: Vec = from_str(&requester.text().await.unwrap()).unwrap(); + let roles: Vec = match from_str(&result.text().await.unwrap()) { + Ok(roles) => roles, + Err(e) => { + return Err(ChorusLibError::InvalidResponseError { + error: e.to_string(), + }) + } + }; if roles.is_empty() { return Ok(None); @@ -52,6 +59,59 @@ impl types::RoleObject { Ok(Some(roles)) } + /// Retrieves a single role for a given guild. + /// + /// # Arguments + /// + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// * `guild_id` - The ID of the guild to retrieve the role from. + /// * `role_id` - The ID of the role to retrieve. + /// + /// # Returns + /// + /// A `Result` containing the retrieved [`RoleObject`] if successful, or a [`ChorusLibError`] if the request fails or if the response is invalid. + /// + /// # Errors + /// + /// Returns a [`ChorusLibError`] if the request fails or if the response is invalid. + pub async fn get( + user: &mut UserMeta, + guild_id: &str, + role_id: &str, + ) -> Result { + let mut belongs_to = user.belongs_to.borrow_mut(); + let url = format!( + "{}/guilds/{}/roles/{}/", + belongs_to.urls.get_api(), + guild_id, + role_id + ); + let request = Client::new().get(url).bearer_auth(user.token()); + let result = match LimitedRequester::new() + .await + .send_request( + request, + crate::api::limits::LimitType::Guild, + &mut belongs_to.limits, + &mut user.limits, + ) + .await + { + Ok(request) => request, + Err(e) => return Err(e), + }; + let role: RoleObject = match from_str(&result.text().await.unwrap()) { + Ok(role) => role, + Err(e) => { + return Err(ChorusLibError::InvalidResponseError { + error: e.to_string(), + }) + } + }; + + Ok(role) + } + /// Creates a new role for a given guild. /// /// # Arguments @@ -161,4 +221,69 @@ impl types::RoleObject { }; Ok(role) } + + /// Updates a role in a guild. + /// + /// # Arguments + /// + /// * `user` - A mutable reference to a [`UserMeta`] instance. + /// * `guild_id` - The ID of the guild to update the role in. + /// * `role_id` - The ID of the role to update. + /// * `role_create_schema` - A [`RoleCreateModifySchema`] instance containing the new properties of the role. + /// + /// # Returns + /// + /// A `Result` containing the updated [`RoleObject`] if successful, or a [`ChorusLibError`] if the request fails or if the response is invalid. + /// + /// # Errors + /// + /// Returns a [`ChorusLibError`] if the request fails or if the response is invalid. + pub async fn update( + user: &mut UserMeta, + guild_id: &str, + role_id: &str, + role_create_schema: RoleCreateModifySchema, + ) -> Result { + let mut belongs_to = user.belongs_to.borrow_mut(); + let url = format!( + "{}/guilds/{}/roles/{}", + belongs_to.urls.get_api(), + guild_id, + role_id + ); + let body = match to_string::(&role_create_schema) { + Ok(string) => string, + Err(e) => { + return Err(ChorusLibError::FormCreationError { + error: e.to_string(), + }) + } + }; + let request = Client::new() + .patch(url) + .bearer_auth(user.token()) + .body(body); + let result = match LimitedRequester::new() + .await + .send_request( + request, + crate::api::limits::LimitType::Guild, + &mut belongs_to.limits, + &mut user.limits, + ) + .await + { + Ok(request) => request, + Err(e) => return Err(e), + }; + let role: RoleObject = match from_str(&result.text().await.unwrap()) { + Ok(role) => role, + Err(e) => { + return Err(ChorusLibError::InvalidResponseError { + error: e.to_string(), + }) + } + }; + Ok(role) + } } diff --git a/src/types/schema/role.rs b/src/types/schema/role.rs index 97ab248..65faa39 100644 --- a/src/types/schema/role.rs +++ b/src/types/schema/role.rs @@ -21,6 +21,6 @@ pub struct RoleCreateModifySchema { /// Represents the schema which needs to be sent to update a roles' position. /// See: [https://docs.spacebar.chat/routes/#cmp--schemas-rolepositionupdateschema](https://docs.spacebar.chat/routes/#cmp--schemas-rolepositionupdateschema) pub struct RolePositionUpdateSchema { - pub id: Snowflake, - pub position: i32, + pub id: String, + pub position: u16, } diff --git a/tests/channel.rs b/tests/channel.rs index 85b019b..8ae8e58 100644 --- a/tests/channel.rs +++ b/tests/channel.rs @@ -1,5 +1,5 @@ mod common; -use chorus::types::{self, Channel}; +use chorus::types::{self, Channel, PermissionFlags, PermissionOverwrite}; #[tokio::test] async fn get_channel() { @@ -54,5 +54,31 @@ async fn modify_channel() { .await .unwrap(); assert_eq!(result.name, Some("beepboop".to_string())); + + let permission_override = PermissionFlags::from_vec(Vec::from([ + PermissionFlags::MANAGE_CHANNELS, + PermissionFlags::MANAGE_MESSAGES, + ])); + let permission_override = PermissionOverwrite { + id: bundle.user.object.id.to_string(), + overwrite_type: "1".to_string(), + allow: permission_override, + deny: "0".to_string(), + }; + + Channel::edit_permissions( + &mut bundle.user, + bundle.channel.id.to_string().as_str(), + permission_override.clone(), + ) + .await; + + Channel::delete_permission( + &mut bundle.user, + bundle.channel.id.to_string().as_str(), + &permission_override.id, + ) + .await; + common::teardown(bundle).await } diff --git a/tests/member.rs b/tests/member.rs index f4b22fd..a19f5e0 100644 --- a/tests/member.rs +++ b/tests/member.rs @@ -7,7 +7,32 @@ async fn add_remove_role() { let role_id = &bundle.role.id.to_string(); let user_id = &bundle.user.object.id.to_string(); chorus::types::GuildMember::add_role(&mut bundle.user, guild_id, user_id, role_id).await; + let member = chorus::types::GuildMember::get(&mut bundle.user, &guild_id, &user_id) + .await + .unwrap(); + let mut role_found = false; + for role in member.roles.iter() { + if role == role_id { + println!("Role found: {:?}", role); + role_found = true; + } + } + if !role_found { + assert!(false) + } chorus::types::GuildMember::remove_role(&mut bundle.user, guild_id, user_id, role_id).await; - // TODO: Implement /guilds/{guild_id}/members/{member_id}/ GET route. + let member = chorus::types::GuildMember::get(&mut bundle.user, &guild_id, &user_id) + .await + .unwrap(); + for role in member.roles.iter() { + if role != role_id { + role_found = false; + } else { + assert!(false); + } + } + if role_found { + assert!(false) + } common::teardown(bundle).await } diff --git a/tests/roles.rs b/tests/roles.rs index 3eee2b8..2663d25 100644 --- a/tests/roles.rs +++ b/tests/roles.rs @@ -34,3 +34,16 @@ async fn create_and_get_roles() { assert_eq!(role, expected); common::teardown(bundle).await } + +#[tokio::test] +async fn get_singular_role() { + let mut bundle = common::setup().await; + let guild_id = &bundle.guild.id.to_string(); + let role_id = &bundle.role.id.to_string(); + let role = bundle.role.clone(); + let same_role = chorus::types::RoleObject::get(&mut bundle.user, guild_id, role_id) + .await + .unwrap(); + assert_eq!(role, same_role); + common::teardown(bundle).await +}