diff --git a/README.md b/README.md index cdb973f..074a96e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Possible use-cases: | :white_check_mark: | Places | | :x: | Instances | | :white_check_mark: | Subscriptions | -| :x: | Inventory | +| :white_check_mark: | Inventory | | :white_check_mark: | User Notifications | | :white_check_mark: | User | | :x: | Creator Store | diff --git a/docs/cli/cli-inventory.md b/docs/cli/cli-inventory.md new file mode 100644 index 0000000..c48f80a --- /dev/null +++ b/docs/cli/cli-inventory.md @@ -0,0 +1,17 @@ +# Inventory API + +## List +List inventory items for a given user. + +``` +Usage: rbxcloud inventory list [OPTIONS] --user-id --api-key + +Options: + -u, --user-id Roblox user ID + -p, --pretty Pretty-print the JSON response + -m, --max-page-size Max page size + -n, --page-token Next page token + -f, --filter Filter string + -a, --api-key Roblox Open Cloud API Key [env: RBXCLOUD_API_KEY=] + -h, --help Print help +``` diff --git a/mkdocs.yml b/mkdocs.yml index 1b2c5b0..6dca0a3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -13,6 +13,7 @@ nav: - DataStore: cli/cli-datastore.md - Experience: cli/cli-experience.md - Group: cli/cli-group.md + - Inventory: cli/cli-inventory.md - Luau Execution: cli/cli-luau-execution.md - Messaging: cli/cli-messaging.md - Notification: cli/cli-notification.md diff --git a/src/cli/inventory_cli.rs b/src/cli/inventory_cli.rs new file mode 100644 index 0000000..cb1c53c --- /dev/null +++ b/src/cli/inventory_cli.rs @@ -0,0 +1,73 @@ +use clap::{Args, Subcommand}; +use rbxcloud::rbx::{types::RobloxUserId, v2::Client}; + +#[derive(Debug, Subcommand)] +pub enum InventoryCommands { + /// List inventory items for a given user + List { + /// Roblox user ID + #[clap(short, long, value_parser)] + user_id: u64, + + /// Pretty-print the JSON response + #[clap(short, long, value_parser, default_value_t = false)] + pretty: bool, + + /// Max page size + #[clap(short, long, value_parser)] + max_page_size: Option, + + /// Next page token + #[clap(short = 'n', long, value_parser)] + page_token: Option, + + /// Filter string + #[clap(short, long, value_parser)] + filter: Option, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser, env = "RBXCLOUD_API_KEY")] + api_key: String, + }, +} + +#[derive(Debug, Args)] +pub struct Inventory { + #[clap(subcommand)] + command: InventoryCommands, +} + +impl Inventory { + pub async fn run(self) -> anyhow::Result> { + match self.command { + InventoryCommands::List { + user_id, + pretty, + max_page_size, + page_token, + filter, + api_key, + } => { + let client = Client::new(&api_key); + + let inventory = client.inventory(); + + let res = inventory + .list_inventory_items(RobloxUserId(user_id), max_page_size, page_token, filter) + .await; + + match res { + Ok(data) => { + let r = if pretty { + serde_json::to_string_pretty(&data)? + } else { + serde_json::to_string(&data)? + }; + Ok(Some(r)) + } + Err(err) => Err(anyhow::anyhow!(err)), + } + } + } + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 2ecaab5..18aba72 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2,6 +2,7 @@ mod assets_cli; mod datastore_cli; mod experience_cli; mod group_cli; +mod inventory_cli; mod luau_execution_cli; mod messaging_cli; mod notification_cli; @@ -12,6 +13,7 @@ mod universe_cli; mod user_cli; use clap::{Parser, Subcommand}; +use inventory_cli::Inventory; use luau_execution_cli::Luau; use universe_cli::Universe; use user_cli::User; @@ -46,6 +48,9 @@ pub enum Command { /// Access the Roblox OrderedDataStore API OrderedDatastore(OrderedDataStore), + /// Access the Roblox user inventory API + Inventory(Inventory), + /// Access the Roblox Group API Group(Group), @@ -76,6 +81,7 @@ impl Cli { Command::Datastore(command) => command.run().await, Command::OrderedDatastore(command) => command.run().await, Command::Group(command) => command.run().await, + Command::Inventory(command) => command.run().await, Command::Luau(command) => command.run().await, Command::Subscription(command) => command.run().await, Command::Notification(command) => command.run().await, diff --git a/src/rbx/types.rs b/src/rbx/types.rs index 81eea6c..13d0127 100644 --- a/src/rbx/types.rs +++ b/src/rbx/types.rs @@ -13,7 +13,7 @@ pub struct PlaceId(pub u64); pub struct ReturnLimit(pub u64); /// Represents a Roblox user's ID. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct RobloxUserId(pub u64); #[derive(Debug, Clone, Copy)] diff --git a/src/rbx/v2/inventory.rs b/src/rbx/v2/inventory.rs new file mode 100644 index 0000000..ace218a --- /dev/null +++ b/src/rbx/v2/inventory.rs @@ -0,0 +1,180 @@ +use serde::{Deserialize, Serialize}; + +use crate::rbx::{error::Error, types::RobloxUserId, util::QueryString}; + +use super::http_err::handle_http_err; + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListInventoryItemsParams { + pub api_key: String, + pub user_id: RobloxUserId, + pub max_page_size: Option, + pub page_token: Option, + pub filter: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InventoryItems { + inventory_items: Vec, + next_page_token: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InventoryItem { + path: String, + #[serde(skip_serializing_if = "Option::is_none")] + asset_details: Option, + #[serde(skip_serializing_if = "Option::is_none")] + badge_details: Option, + #[serde(skip_serializing_if = "Option::is_none")] + game_pass_details: Option, + #[serde(skip_serializing_if = "Option::is_none")] + private_server_details: Option, + #[serde(skip_serializing_if = "Option::is_none")] + add_time: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InventoryItemAssetDetails { + asset_id: String, + instance_id: String, + inventory_item_asset_type: InventoryItemAssetType, + #[serde(skip_serializing_if = "Option::is_none")] + collectible_details: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InventoryItemBadgeDetails { + badge_id: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InventoryItemGamePassDetails { + game_pass_id: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InventoryItemPrivateServerDetails { + private_server_id: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct InventoryItemCollectibleDetails { + item_id: String, + instance_id: String, + instance_state: InventoryItemInstanceState, + #[serde(skip_serializing_if = "Option::is_none")] + serial_number: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum InventoryItemInstanceState { + CollectibleItemInstanceStateUnspecified, + Available, + Hold, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum InventoryItemAssetType { + ClassicTshirt, + Audio, + Hat, + Model, + ClassicShirt, + ClassicPants, + Decal, + ClassicHead, + Face, + Gear, + Animation, + Torso, + RightArm, + LeftArm, + LeftLeg, + RightLeg, + Package, + Plugin, + MeshPart, + HairAccessory, + FaceAccessory, + NeckAccessory, + ShoulderAccessory, + FrontAccessory, + BackAccessory, + WaistAccessory, + ClimbAnimation, + DeathAnimation, + FallAnimation, + IdleAnimation, + JumpAnimation, + RunAnimation, + SwimAnimation, + WalkAnimation, + PoseAnimation, + EmoteAnimation, + Video, + TshirtAccessory, + ShirtAccessory, + PantsAccessory, + JacketAccessory, + SweaterAccessory, + ShortsAccessory, + LeftShoeAccessory, + RightShoeAccessory, + DressSkirtAccessory, + EyebrowAccessory, + EyelashAccessory, + MoodAnimation, + DynamicHead, + CreatedPlace, + PurchasedPlace, +} + +pub async fn list_inventory_items( + params: &ListInventoryItemsParams, +) -> Result { + let client = reqwest::Client::new(); + + let url = format!( + "https://apis.roblox.com/cloud/v2/users/{userId}/inventory-items", + userId = params.user_id, + ); + + let mut query: QueryString = vec![]; + if let Some(max_page_size) = params.max_page_size { + query.push(("maxPageSize", format!("{max_page_size}"))); + } + if let Some(page_token) = ¶ms.page_token { + query.push(("pageToken", page_token.clone())); + } + if let Some(filter) = ¶ms.filter { + query.push(("filter", filter.clone())); + } + + 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) +} diff --git a/src/rbx/v2/luau_execution.rs b/src/rbx/v2/luau_execution.rs index eb29e01..62e275e 100644 --- a/src/rbx/v2/luau_execution.rs +++ b/src/rbx/v2/luau_execution.rs @@ -258,8 +258,6 @@ pub async fn get_luau_execution_task_logs( .send() .await?; - println!("URL SENT: {}", res.url()); - let status = res.status(); if !status.is_success() { diff --git a/src/rbx/v2/mod.rs b/src/rbx/v2/mod.rs index 4ace408..54a4d9e 100644 --- a/src/rbx/v2/mod.rs +++ b/src/rbx/v2/mod.rs @@ -2,6 +2,7 @@ //! //! Most usage should go through the `Client` struct. +use inventory::{InventoryItems, ListInventoryItemsParams}; use luau_execution::{ CreateLuauExecutionTaskParams, GetLuauExecutionSessionTaskLogsParams, GetLuauExecutionSessionTaskParams, LuauExecutionSessionTask, LuauExecutionSessionTaskLogPage, @@ -28,6 +29,7 @@ use self::{ }; pub mod group; pub(crate) mod http_err; +pub mod inventory; pub mod luau_execution; pub mod notification; pub mod place; @@ -57,6 +59,10 @@ pub struct GroupClient { pub group_id: GroupId, } +pub struct InventoryClient { + pub api_key: String, +} + pub struct LuauExecutionClient { pub api_key: String, pub universe_id: UniverseId, @@ -136,6 +142,25 @@ impl GroupClient { } } +impl InventoryClient { + pub async fn list_inventory_items( + &self, + user_id: RobloxUserId, + max_page_size: Option, + page_token: Option, + filter: Option, + ) -> Result { + inventory::list_inventory_items(&ListInventoryItemsParams { + api_key: self.api_key.clone(), + user_id, + max_page_size, + page_token, + filter, + }) + .await + } +} + impl LuauExecutionClient { pub async fn create_task( &self, @@ -270,7 +295,7 @@ impl UniverseClient { api_key: self.api_key.clone(), universe_id: self.universe_id, update_mask, - info: info, + info, }) .await } @@ -325,6 +350,12 @@ impl Client { } } + pub fn inventory(&self) -> InventoryClient { + InventoryClient { + api_key: self.api_key.clone(), + } + } + pub fn luau( &self, universe_id: UniverseId, diff --git a/src/rbx/v2/place.rs b/src/rbx/v2/place.rs index 61c2e6c..745df49 100644 --- a/src/rbx/v2/place.rs +++ b/src/rbx/v2/place.rs @@ -83,8 +83,7 @@ pub async fn update_place(params: &UpdatePlaceParams) -> Result Result