diff --git a/Cargo.lock b/Cargo.lock index 0da2624..1656899 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" @@ -110,6 +125,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + [[package]] name = "block-buffer" version = "0.10.4" @@ -143,6 +164,21 @@ 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", + "serde", + "wasm-bindgen", + "windows-targets 0.52.4", +] + [[package]] name = "clap" version = "4.5.4" @@ -189,6 +225,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" @@ -296,7 +338,19 @@ checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -439,6 +493,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" @@ -482,9 +559,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "lock_api" @@ -550,10 +627,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "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" @@ -640,6 +726,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -658,14 +753,52 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha", + "rand_core", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", +] + [[package]] name = "rbxcloud" version = "0.17.0" dependencies = [ "anyhow", "base64 0.22.0", + "chrono", "clap", "md-5", + "rand", "reqwest", "serde", "serde_json", @@ -678,7 +811,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -733,7 +866,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.12", "libc", "spin", "untrusted", @@ -1114,6 +1247,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.92" @@ -1199,6 +1341,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" @@ -1341,6 +1492,35 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 7e79edf..359831d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,5 @@ 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 = { version = "0.4.38", features = ["serde"] } +rand = "0.9.0" diff --git a/README.md b/README.md index 074a96e..e8d1aad 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Possible use-cases: | | API v2 | | -- | -- | +| :x: | Data Stores | | :white_check_mark: | Groups | | :white_check_mark: | Universes | | :white_check_mark: | Places | @@ -33,6 +34,7 @@ Possible use-cases: | :white_check_mark: | Inventory | | :white_check_mark: | User Notifications | | :white_check_mark: | User | +| :white_check_mark: | User Restrictions | | :x: | Creator Store | | :white_check_mark: | Luau Execution | diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 18aba72..1a3ed0f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -11,12 +11,14 @@ mod place_cli; mod subscription_cli; mod universe_cli; mod user_cli; +mod user_restriction_cli; use clap::{Parser, Subcommand}; use inventory_cli::Inventory; use luau_execution_cli::Luau; 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, @@ -26,13 +28,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), @@ -70,10 +72,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, @@ -88,6 +93,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 54a4d9e..303c1cb 100644 --- a/src/rbx/v2/mod.rs +++ b/src/rbx/v2/mod.rs @@ -9,6 +9,7 @@ use luau_execution::{ LuauExecutionTaskLogView, NewLuauExecutionSessionTask, }; use place::{GetPlaceParams, PlaceInfo, UpdatePlaceInfo, UpdatePlaceParams}; +use rand::{distr::Alphanumeric, Rng}; use universe::{ GetUniverseParams, RestartUniverseServersParams, UniverseInfo, UpdateUniverseInfo, UpdateUniverseParams, @@ -17,6 +18,10 @@ use user::{ GenerateUserThumbnailOperationResponse, GenerateUserThumbnailParams, GetUserParams, GetUserResponse, UserThumbnailFormat, UserThumbnailShape, UserThumbnailSize, }; +use user_restriction::{ + GetUserRestrictionParams, ListUserRestrictionLogsParams, ListUserRestrictionsParams, + UpdateUserRestrictionParams, UserRestriction, UserRestrictionList, UserRestrictionLogsList, +}; use self::{ group::{ @@ -36,6 +41,7 @@ pub mod place; pub mod subscription; pub mod universe; pub mod user; +pub mod user_restriction; use crate::rbx::error::Error; @@ -94,6 +100,21 @@ pub struct UserClient { pub api_key: String, } +pub struct UserRestrictionClient { + pub api_key: String, + pub universe_id: UniverseId, +} + +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 { @@ -336,6 +357,82 @@ 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 { + let idempotency_key: String = rand::rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(); + 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: Some(idempotency_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 { @@ -403,4 +500,11 @@ 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, + } + } } 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..ba8b978 --- /dev/null +++ b/src/rbx/v2/user_restriction.rs @@ -0,0 +1,315 @@ +use chrono::{DateTime, SecondsFormat, Utc}; +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, + #[serde(skip_serializing_if = "Option::is_none")] + pub start_time: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + 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, + #[serde(skip_serializing_if = "Option::is_none")] + 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: Option, + 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, + ) + }; + + let timestamp = Utc::now(); + + let mut query: QueryString = vec![("updateMask", "gameJoinRestriction".to_string())]; + + if let Some(idempotency_key) = ¶ms.idempotency_key { + query.push(("idempotencyKey.key", idempotency_key.to_string())); + query.push(( + "idempotencyKey.firstSent", + timestamp.to_rfc3339_opts(SecondsFormat::Millis, true), + )); + } + + 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) +}