diff --git a/Cargo.lock b/Cargo.lock index 4d450e0..fd290d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.13" @@ -143,6 +158,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.4", +] + [[package]] name = "clap" version = "4.5.4" @@ -189,6 +218,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crypto-common" version = "0.1.6" @@ -439,6 +474,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -554,6 +612,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -660,10 +727,11 @@ dependencies = [ [[package]] name = "rbxcloud" -version = "0.14.0" +version = "0.15.0" dependencies = [ "anyhow", "base64 0.22.0", + "chrono", "clap", "md-5", "reqwest", @@ -1199,6 +1267,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index c0de43b..9b882f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rbxcloud" -version = "0.14.0" +version = "0.15.0" description = "CLI and SDK for the Roblox Open Cloud APIs" authors = ["Stephen Leitnick"] license = "MIT" @@ -18,3 +18,4 @@ reqwest = { version = "0.12.2", default-features = false, features = ["rustls-tl serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" tokio = { version = "1.36.0", features = ["full"] } +chrono = "0.4.38" diff --git a/README.md b/README.md index 3007fd0..c2c9a02 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Possible use-cases: | | API v2 (Beta) | | -- | -- | +| :x: | Data Stores | | :white_check_mark: | Groups | | :white_check_mark: | Universes | | :white_check_mark: | Places | @@ -33,6 +34,7 @@ Possible use-cases: | :x: | Inventory | | :white_check_mark: | User Notifications | | :white_check_mark: | User | +| :white_check_mark: | User Restrictions | | :x: | Creator Store | - :white_check_mark: = Supported diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 1c06e27..59a40ed 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -9,10 +9,12 @@ mod place_cli; mod subscription_cli; mod universe_cli; mod user_cli; +mod user_restriction_cli; use clap::{Parser, Subcommand}; use universe_cli::Universe; use user_cli::User; +use user_restriction_cli::UserRestriction; use self::{ assets_cli::Assets, datastore_cli::DataStore, experience_cli::Experience, group_cli::Group, @@ -22,13 +24,13 @@ use self::{ #[derive(Debug, Parser)] #[clap(name = "rbxcloud", version)] -pub struct Cli { +pub(crate) struct Cli { #[clap(subcommand)] pub command: Command, } #[derive(Debug, Subcommand)] -pub enum Command { +pub(crate) enum Command { /// Access the Roblox Assets API Assets(Assets), @@ -61,10 +63,13 @@ pub enum Command { /// Access the Roblox User API User(User), + + /// Access to the Roblox User Restriction API + UserRestriction(UserRestriction), } impl Cli { - pub async fn run(self) -> anyhow::Result> { + pub(crate) async fn run(self) -> anyhow::Result> { match self.command { Command::Assets(command) => command.run().await, Command::Experience(command) => command.run().await, @@ -77,6 +82,7 @@ impl Cli { Command::Place(command) => command.run().await, Command::Universe(command) => command.run().await, Command::User(command) => command.run().await, + Command::UserRestriction(command) => command.run().await, } } } diff --git a/src/cli/user_restriction_cli.rs b/src/cli/user_restriction_cli.rs new file mode 100644 index 0000000..d9f1014 --- /dev/null +++ b/src/cli/user_restriction_cli.rs @@ -0,0 +1,278 @@ +use clap::{Args, Subcommand}; +use rbxcloud::rbx::{ + types::{PlaceId, RobloxUserId, UniverseId}, + v2::{Client, UserRestrictionParams}, +}; + +#[derive(Debug, Subcommand)] +pub(crate) enum UserRestrictionCommands { + /// Get user restriction information + Get { + /// Universe ID + #[clap(short, long, value_parser)] + universe_id: u64, + + /// User ID + #[clap(short = 'U', long, value_parser)] + user_id: u64, + + /// Place ID + #[clap(short = 'P', long, value_parser)] + place_id: Option, + + /// Pretty-print the JSON response + #[clap(short, long, value_parser, default_value_t = false)] + pretty: bool, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser, env = "RBXCLOUD_API_KEY")] + api_key: String, + }, + + /// Update user restriction information + Update { + /// Universe ID + #[clap(short, long, value_parser)] + universe_id: u64, + + /// User ID + #[clap(short = 'U', long, value_parser)] + user_id: u64, + + /// Place ID + #[clap(short = 'P', long, value_parser)] + place_id: Option, + + /// Restriction active + #[clap(short = 'A', long, value_parser)] + active: Option, + + /// Restriction duration (seconds) + #[clap(short, long, value_parser)] + duration: Option, + + /// Private reason + #[clap(short = 'r', long, value_parser)] + private_reason: Option, + + /// Display reason + #[clap(short = 'D', long, value_parser)] + display_reason: Option, + + /// Exclude alternate accounts + #[clap(short, long, value_parser)] + exclude_alts: Option, + + /// Pretty-print the JSON response + #[clap(short, long, value_parser, default_value_t = false)] + pretty: bool, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser, env = "RBXCLOUD_API_KEY")] + api_key: String, + }, + + /// List user restrictions + List { + /// Universe ID + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Place ID + #[clap(short = 'P', long, value_parser)] + place_id: Option, + + /// Max page size + #[clap(short = 's', long, value_parser)] + page_size: Option, + + /// Next page token + #[clap(short, long, value_parser)] + token: Option, + + /// Filter + #[clap(short, long, value_parser)] + filter: Option, + + /// Pretty-print the JSON response + #[clap(short, long, value_parser, default_value_t = false)] + pretty: bool, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser, env = "RBXCLOUD_API_KEY")] + api_key: String, + }, + + /// List user restriction logs + Logs { + /// Universe ID + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Place ID + #[clap(short = 'P', long, value_parser)] + place_id: Option, + + /// Max page size + #[clap(short = 's', long, value_parser)] + page_size: Option, + + /// Next page token + #[clap(short, long, value_parser)] + token: Option, + + /// Filter + #[clap(short, long, value_parser)] + filter: Option, + + /// Pretty-print the JSON response + #[clap(short, long, value_parser, default_value_t = false)] + pretty: bool, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser, env = "RBXCLOUD_API_KEY")] + api_key: String, + }, +} + +#[derive(Debug, Args)] +pub(crate) struct UserRestriction { + #[clap(subcommand)] + command: UserRestrictionCommands, +} + +impl UserRestriction { + pub(crate) async fn run(self) -> anyhow::Result> { + match self.command { + UserRestrictionCommands::Get { + universe_id, + user_id, + place_id, + pretty, + api_key, + } => { + let client = Client::new(&api_key); + let user_restriction_client = client.user_restriction(UniverseId(universe_id)); + let res = user_restriction_client + .get_user_restriction( + RobloxUserId(user_id), + place_id.and_then(|id| Some(PlaceId(id))), + ) + .await; + match res { + Ok(info) => { + let r = if pretty { + serde_json::to_string_pretty(&info)? + } else { + serde_json::to_string(&info)? + }; + Ok(Some(r)) + } + Err(err) => Err(anyhow::anyhow!(err)), + } + } + + UserRestrictionCommands::Update { + universe_id, + user_id, + place_id, + active, + duration, + private_reason, + display_reason, + exclude_alts, + pretty, + api_key, + } => { + let client = Client::new(&api_key); + let mut user_restriction_client = client.user_restriction(UniverseId(universe_id)); + let res = user_restriction_client + .update_user_restriction(&UserRestrictionParams { + user_id: RobloxUserId(user_id), + place_id: place_id.and_then(|id| Some(PlaceId(id))), + active, + duration, + private_reason, + display_reason, + exclude_alt_accounts: exclude_alts, + }) + .await; + match res { + Ok(info) => { + let r = if pretty { + serde_json::to_string_pretty(&info)? + } else { + serde_json::to_string(&info)? + }; + Ok(Some(r)) + } + Err(err) => Err(anyhow::anyhow!(err)), + } + } + + UserRestrictionCommands::List { + universe_id, + place_id, + page_size, + token, + filter, + pretty, + api_key, + } => { + let client = Client::new(&api_key); + let user_restriction_client = client.user_restriction(UniverseId(universe_id)); + let res = user_restriction_client + .list_user_restrictions( + place_id.and_then(|id| Some(PlaceId(id))), + page_size, + filter, + token, + ) + .await; + match res { + Ok(info) => { + let r = if pretty { + serde_json::to_string_pretty(&info)? + } else { + serde_json::to_string(&info)? + }; + Ok(Some(r)) + } + Err(err) => Err(anyhow::anyhow!(err)), + } + } + + UserRestrictionCommands::Logs { + universe_id, + place_id, + page_size, + token, + filter, + pretty, + api_key, + } => { + let client = Client::new(&api_key); + let user_restriction_client = client.user_restriction(UniverseId(universe_id)); + let res = user_restriction_client + .list_user_restriction_logs( + place_id.and_then(|id| Some(PlaceId(id))), + page_size, + filter, + token, + ) + .await; + match res { + Ok(info) => { + let r = if pretty { + serde_json::to_string_pretty(&info)? + } else { + serde_json::to_string(&info)? + }; + Ok(Some(r)) + } + Err(err) => Err(anyhow::anyhow!(err)), + } + } + } + } +} diff --git a/src/rbx/v2/mod.rs b/src/rbx/v2/mod.rs index 6c5d4d3..8b51e9c 100644 --- a/src/rbx/v2/mod.rs +++ b/src/rbx/v2/mod.rs @@ -11,6 +11,10 @@ use user::{ GenerateUserThumbnailOperationResponse, GenerateUserThumbnailParams, GetUserParams, GetUserResponse, UserThumbnailFormat, UserThumbnailShape, UserThumbnailSize, }; +use user_restriction::{ + GetUserRestrictionParams, ListUserRestrictionLogsParams, ListUserRestrictionsParams, + UpdateUserRestrictionParams, UserRestriction, UserRestrictionList, UserRestrictionLogsList, +}; use self::{ group::{ @@ -28,6 +32,7 @@ pub mod place; pub mod subscription; pub mod universe; pub mod user; +pub mod user_restriction; use crate::rbx::error::Error; @@ -75,6 +80,22 @@ pub struct UserClient { pub api_key: String, } +pub struct UserRestrictionClient { + pub api_key: String, + pub universe_id: UniverseId, + idempotent_gen: u64, +} + +pub struct UserRestrictionParams { + pub user_id: RobloxUserId, + pub place_id: Option, + pub active: Option, + pub duration: Option, + pub private_reason: Option, + pub display_reason: Option, + pub exclude_alt_accounts: Option, +} + impl GroupClient { pub async fn get_info(&self) -> Result { group::get_group(&GetGroupParams { @@ -242,6 +263,79 @@ impl UserClient { } } +impl UserRestrictionClient { + pub async fn list_user_restrictions( + &self, + place_id: Option, + max_page_size: Option, + filter: Option, + page_token: Option, + ) -> Result { + user_restriction::list_user_restrictions(&ListUserRestrictionsParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + place_id, + max_page_size, + page_token, + filter, + }) + .await + } + + pub async fn get_user_restriction( + &self, + user_id: RobloxUserId, + place_id: Option, + ) -> Result { + user_restriction::get_user_restriction(&GetUserRestrictionParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + place_id, + user_id, + }) + .await + } + + pub async fn update_user_restriction( + &mut self, + params: &UserRestrictionParams, + ) -> Result { + self.idempotent_gen += 1; + let key = format!("{}", self.idempotent_gen); + user_restriction::update_user_restriction(&UpdateUserRestrictionParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + place_id: params.place_id, + user_id: params.user_id, + idempotency_key: key, + active: params.active, + duration: params.duration.and_then(|d| Some(format!("{}s", d))), + private_reason: params.private_reason.clone(), + display_reason: params.display_reason.clone(), + exclude_alt_accounts: params.exclude_alt_accounts, + }) + .await + } + + pub async fn list_user_restriction_logs( + &self, + place_id: Option, + max_page_size: Option, + page_token: Option, + filter: Option, + ) -> Result { + user_restriction::list_user_restriction_logs(&ListUserRestrictionLogsParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + place_id, + max_page_size, + page_token, + filter, + }) + .await + } +} + impl Client { pub fn new(api_key: &str) -> Client { Client { @@ -289,4 +383,12 @@ impl Client { api_key: self.api_key.clone(), } } + + pub fn user_restriction(&self, universe_id: UniverseId) -> UserRestrictionClient { + UserRestrictionClient { + api_key: self.api_key.clone(), + universe_id, + idempotent_gen: 0, + } + } } diff --git a/src/rbx/v2/subscription.rs b/src/rbx/v2/subscription.rs index c3dee4e..8bd9e94 100644 --- a/src/rbx/v2/subscription.rs +++ b/src/rbx/v2/subscription.rs @@ -107,7 +107,7 @@ pub async fn get_subscription( let mut query: QueryString = vec![]; if let Some(view) = ¶ms.view { - query.push(("view", view.to_string())) + query.push(("view", view.to_string())); } let res = client diff --git a/src/rbx/v2/user_restriction.rs b/src/rbx/v2/user_restriction.rs new file mode 100644 index 0000000..0e2e451 --- /dev/null +++ b/src/rbx/v2/user_restriction.rs @@ -0,0 +1,327 @@ +use serde::{Deserialize, Serialize}; + +use crate::rbx::{ + error::Error, + types::{PlaceId, RobloxUserId, UniverseId}, + util::QueryString, +}; + +use super::http_err::handle_http_err; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GameJoinRestriction { + pub active: bool, + pub start_time: Option, + pub duration: Option, + pub private_reason: String, + pub display_reason: String, + pub exclude_alt_accounts: bool, + pub inherited: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UserRestriction { + pub path: String, + pub update_time: Option, + pub user: String, + pub game_join_restriction: GameJoinRestriction, +} + +pub struct UpdateUserRestrictionParams { + pub api_key: String, + pub universe_id: UniverseId, + pub place_id: Option, + pub user_id: RobloxUserId, + pub idempotency_key: String, + pub active: Option, + pub duration: Option, + pub private_reason: Option, + pub display_reason: Option, + pub exclude_alt_accounts: Option, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +struct UpdateUserRestriction { + game_join_restriction: GameJoinRestriction, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UserRestrictionList { + pub user_restrictions: Vec, + pub next_page_token: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GameServerScript { + // empty +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub enum UserRestrictionModerator { + RobloxUser(String), + GameServerScript(GameServerScript), +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UserRestrictionLog { + pub user: String, + pub place: String, + pub create_time: String, + pub active: bool, + pub start_time: String, + pub duration: String, + pub private_reason: String, + pub display_reason: String, + pub exclude_alt_accounts: bool, + pub moderator: UserRestrictionModerator, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UserRestrictionLogsList { + pub logs: Vec, + pub next_page_token: Option, +} + +pub struct GetUserRestrictionParams { + pub api_key: String, + pub universe_id: UniverseId, + pub place_id: Option, + pub user_id: RobloxUserId, +} + +pub struct ListUserRestrictionsParams { + pub api_key: String, + pub universe_id: UniverseId, + pub place_id: Option, + pub max_page_size: Option, + pub page_token: Option, + pub filter: Option, +} + +pub struct ListUserRestrictionLogsParams { + pub api_key: String, + pub universe_id: UniverseId, + pub place_id: Option, + pub max_page_size: Option, + pub page_token: Option, + pub filter: Option, +} + +pub async fn get_user_restriction( + params: &GetUserRestrictionParams, +) -> Result { + let client = reqwest::Client::new(); + + let url = if let Some(place_id) = params.place_id { + format!( + "https://apis.roblox.com/cloud/v2/universes/{universeId}/places/{placeId}/user-restrictions/{user}", + universeId = ¶ms.universe_id, + placeId = &place_id, + user = ¶ms.user_id, + ) + } else { + format!( + "https://apis.roblox.com/cloud/v2/universes/{universeId}/user-restrictions/{user}", + universeId = ¶ms.universe_id, + user = ¶ms.user_id, + ) + }; + + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .send() + .await?; + + let status = res.status(); + + if !status.is_success() { + let code = status.as_u16(); + return handle_http_err(code); + } + + let body = res.json::().await?; + Ok(body) +} + +pub async fn list_user_restrictions( + params: &ListUserRestrictionsParams, +) -> Result { + let client = reqwest::Client::new(); + + let url = if let Some(place_id) = params.place_id { + format!( + "https://apis.roblox.com/cloud/v2/universes/{universeId}/places/{placeId}/user-restrictions", + universeId = ¶ms.universe_id, + placeId = &place_id, + ) + } else { + format!( + "https://apis.roblox.com/cloud/v2/universes/{universeId}/user-restrictions", + universeId = ¶ms.universe_id, + ) + }; + + let mut query: QueryString = vec![]; + if let Some(max_page_size) = ¶ms.max_page_size { + query.push(("maxPageSize", max_page_size.to_string())); + } + if let Some(page_token) = ¶ms.page_token { + query.push(("pageToken", page_token.to_string())); + } + if let Some(filter) = ¶ms.filter { + query.push(("filter", filter.to_string())); + } + + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + + let status = res.status(); + + if !status.is_success() { + let code = status.as_u16(); + return handle_http_err(code); + } + + let body = res.json::().await?; + Ok(body) +} + +pub async fn update_user_restriction( + params: &UpdateUserRestrictionParams, +) -> Result { + let client = reqwest::Client::new(); + + let url = if let Some(place_id) = params.place_id { + format!( + "https://apis.roblox.com/cloud/v2/universes/{universeId}/places/{placeId}/user-restrictions/{user}", + universeId = ¶ms.universe_id, + placeId = &place_id, + user = ¶ms.user_id, + ) + } else { + format!( + "https://apis.roblox.com/cloud/v2/universes/{universeId}/user-restrictions/{user}", + universeId = ¶ms.universe_id, + user = ¶ms.user_id, + ) + }; + + // Build update mask based on provided parameters: + let mut update_mask: Vec<&str> = vec![]; + if params.active.is_some() { + update_mask.push("gameJoinRestriction.active"); + } + if params.duration.is_some() { + update_mask.push("gameJoinRestriction.duration"); + } + if params.private_reason.is_some() { + update_mask.push("gameJoinRestriction.privateReason"); + } + if params.display_reason.is_some() { + update_mask.push("gameJoinRestriction.displayReason"); + } + if params.exclude_alt_accounts.is_some() { + update_mask.push("gameJoinRestriction.excludeAltAccounts"); + } + let update_mask_str = update_mask.join(","); + + // See: https://create.roblox.com/docs/cloud/reference/types#timestamp + let timestamp = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true); + + let query: QueryString = vec![ + ("updateMask", update_mask_str), + ("idempotencyKey.key", params.idempotency_key.to_string()), + ("idempotencyKey.firstSent", timestamp.clone()), + ]; + + let body = serde_json::to_string(&UpdateUserRestriction { + game_join_restriction: GameJoinRestriction { + active: params.active.unwrap_or(false), + start_time: Some(timestamp), + duration: params.duration.clone(), + private_reason: params.private_reason.clone().unwrap_or("".into()), + display_reason: params.display_reason.clone().unwrap_or("".into()), + exclude_alt_accounts: params.exclude_alt_accounts.unwrap_or(false), + inherited: false, + }, + })?; + + let res = client + .patch(url) + .header("x-api-key", ¶ms.api_key) + .header("Content-Type", "application/json") + .query(&query) + .body(body) + .send() + .await?; + + let status = res.status(); + + if !status.is_success() { + let code = status.as_u16(); + return handle_http_err(code); + } + + let body = res.json::().await?; + Ok(body) +} + +pub async fn list_user_restriction_logs( + params: &ListUserRestrictionLogsParams, +) -> Result { + let client = reqwest::Client::new(); + + let url = if let Some(place_id) = params.place_id { + format!( + "https://apis.roblox.com/cloud/v2/universes/{universeId}/places/{placeId}/user-restrictions:listLogs", + universeId = ¶ms.universe_id, + placeId = &place_id, + ) + } else { + format!( + "https://apis.roblox.com/cloud/v2/universes/{universeId}/user-restrictions:listLogs", + universeId = ¶ms.universe_id, + ) + }; + + let mut query: QueryString = vec![]; + if let Some(max_page_size) = ¶ms.max_page_size { + query.push(("maxPageSize", max_page_size.to_string())); + } + if let Some(page_token) = ¶ms.page_token { + query.push(("pageToken", page_token.to_string())); + } + if let Some(filter) = ¶ms.filter { + query.push(("filter", filter.to_string())); + } + + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + + let status = res.status(); + + if !status.is_success() { + let code = status.as_u16(); + return handle_http_err(code); + } + + let body = res.json::().await?; + Ok(body) +}