From df9c2568918dd8667a6f88d8b3554676e1f3e86c Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Wed, 8 Feb 2023 21:21:51 -0500 Subject: [PATCH 1/5] Fix datastores --- Cargo.lock | 2 +- Cargo.toml | 40 +- src/cli/datastore_cli.rs | 1002 ++++++++++++++++++------------------- src/cli/messaging_cli.rs | 104 ++-- src/main.rs | 44 +- src/rbx/datastore.rs | 1023 +++++++++++++++++++------------------- src/rbx/error.rs | 14 +- src/rbx/experience.rs | 2 +- src/rbx/mod.rs | 748 ++++++++++++++-------------- 9 files changed, 1487 insertions(+), 1492 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88afa4d..3aa1455 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,7 +651,7 @@ dependencies = [ [[package]] name = "rbxcloud" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 15e71e5..291eb8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,20 @@ -[package] -name = "rbxcloud" -version = "0.2.1" -description = "CLI and SDK for the Roblox Open Cloud APIs" -authors = ["Stephen Leitnick"] -license = "MIT" -repository = "https://github.com/Sleitnick/rbxcloud" -readme = "README.md" -documentation = "https://sleitnick.github.io/rbxcloud/" -edition = "2021" - -[dependencies] -anyhow = "1.0.58" -base64 = "0.13.0" -clap = { version = "3.2.14", features = ["derive"] } -md-5 = "0.10.1" -reqwest = { version = "0.11.11", features = ["json"] } -serde = { version = "1.0.140", features = ["derive"] } -serde_json = "1.0.82" -tokio = { version = "1.20.1", features = ["full"] } +[package] +name = "rbxcloud" +version = "0.2.2" +description = "CLI and SDK for the Roblox Open Cloud APIs" +authors = ["Stephen Leitnick"] +license = "MIT" +repository = "https://github.com/Sleitnick/rbxcloud" +readme = "README.md" +documentation = "https://sleitnick.github.io/rbxcloud/" +edition = "2021" + +[dependencies] +anyhow = "1.0.58" +base64 = "0.13.0" +clap = { version = "3.2.14", features = ["derive"] } +md-5 = "0.10.1" +reqwest = { version = "0.11.11", features = ["json"] } +serde = { version = "1.0.140", features = ["derive"] } +serde_json = "1.0.82" +tokio = { version = "1.20.1", features = ["full"] } diff --git a/src/cli/datastore_cli.rs b/src/cli/datastore_cli.rs index 3f98509..742aa88 100644 --- a/src/cli/datastore_cli.rs +++ b/src/cli/datastore_cli.rs @@ -1,501 +1,501 @@ -use clap::{Args, Subcommand, ValueEnum}; - -use rbxcloud::rbx::{ - DataStoreDeleteEntry, DataStoreGetEntry, DataStoreGetEntryVersion, DataStoreIncrementEntry, - DataStoreListEntries, DataStoreListEntryVersions, DataStoreListStores, DataStoreSetEntry, - RbxCloud, ReturnLimit, RobloxUserId, UniverseId, -}; - -#[derive(Debug, Subcommand)] -pub enum DataStoreCommands { - /// List all DataStores in a given universe - ListStores { - /// Return only DataStores with this prefix - #[clap(short, long, value_parser)] - prefix: Option, - - /// Maximum number of items to return - #[clap(short, long, value_parser)] - limit: u64, - - /// Cursor for the next set of data - #[clap(short, long, value_parser)] - cursor: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// List all entries in a DataStore - List { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// If true, return keys from all scopes - #[clap(short = 'o', long, value_parser)] - all_scopes: bool, - - /// Return only DataStores with this prefix - #[clap(short, long, value_parser)] - prefix: Option, - - /// Maximum number of items to return - #[clap(short, long, value_parser)] - limit: u64, - - /// Cursor for the next set of data - #[clap(short, long, value_parser)] - cursor: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Get a DataStore entry - Get { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Set or create the value of a DataStore entry - Set { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// Only update if the current version matches this - #[clap(short = 'i', long, value_parser)] - match_version: Option, - - /// Only create the entry if it does not exist - #[clap(short, long, value_parser)] - exclusive_create: Option, - - /// JSON-stringified data (up to 4MB) - #[clap(short = 'D', long, value_parser)] - data: String, - - /// Associated UserID (can be multiple) - #[clap(short = 'U', long, value_parser)] - user_ids: Option>, - - /// JSON-stringified attributes data - #[clap(short = 't', long, value_parser)] - attributes: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Increment or create the value of a DataStore entry - Increment { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// The amount by which the entry should be incremented - #[clap(short, long, value_parser)] - increment_by: f64, - - /// Comma-separated list of Roblox user IDs - #[clap(short = 'U', long, value_parser)] - user_ids: Option>, - - /// JSON-stringified attributes data - #[clap(short = 't', long, value_parser)] - attributes: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Delete a DataStore entry - Delete { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// List all versions of a DataStore entry - ListVersions { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// Start time constraint (ISO UTC Datetime) - #[clap(short = 't', long, value_parser)] - start_time: Option, - - /// End time constraint (ISO UTC Datetime) - #[clap(short = 'e', long, value_parser)] - end_time: Option, - - /// Sort order - #[clap(short = 'o', long, value_enum)] - sort_order: ListEntrySortOrder, - - /// Maximum number of items to return - #[clap(short, long, value_parser)] - limit: u64, - - /// Cursor for the next set of data - #[clap(short, long, value_parser)] - cursor: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Get the value of a specific entry version - GetVersion { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// The version of the key - #[clap(short = 'i', long, value_parser)] - version_id: String, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, -} - -#[derive(Debug, Clone, ValueEnum)] -pub enum ListEntrySortOrder { - Ascending, - Descending, -} - -#[derive(Debug, Args)] -pub struct DataStore { - #[clap(subcommand)] - command: DataStoreCommands, -} - -#[inline] -fn u64_ids_to_roblox_ids(user_ids: Option>) -> Option> { - user_ids.map(|ids| { - ids.into_iter() - .map(RobloxUserId) - .collect::>() - }) -} - -impl DataStore { - pub async fn run(self) -> anyhow::Result> { - match self.command { - DataStoreCommands::ListStores { - prefix, - limit, - cursor, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .list_stores(&DataStoreListStores { - cursor, - limit: ReturnLimit(limit), - prefix, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{:#?}", data))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::List { - prefix, - limit, - cursor, - universe_id, - api_key, - datastore_name, - scope, - all_scopes, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .list_entries(&DataStoreListEntries { - name: datastore_name, - scope, - all_scopes, - prefix, - limit: ReturnLimit(limit), - cursor, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{:#?}", data))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::Get { - datastore_name, - scope, - key, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .get_entry_string(&DataStoreGetEntry { - name: datastore_name, - scope, - key, - }) - .await; - match res { - Ok(data) => Ok(Some(data)), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::Set { - datastore_name, - scope, - key, - match_version, - exclusive_create, - data, - user_ids, - attributes, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let ids = u64_ids_to_roblox_ids(user_ids); - let res = datastore - .set_entry(&DataStoreSetEntry { - name: datastore_name, - scope, - key, - match_version, - exclusive_create, - roblox_entry_user_ids: ids, - roblox_entry_attributes: attributes, - data, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{:#?}", data))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::Increment { - datastore_name, - scope, - key, - increment_by, - user_ids, - attributes, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let ids = u64_ids_to_roblox_ids(user_ids); - let res = datastore - .increment_entry(&DataStoreIncrementEntry { - name: datastore_name, - scope, - key, - roblox_entry_user_ids: ids, - roblox_entry_attributes: attributes, - increment_by, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{}", data))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::Delete { - datastore_name, - scope, - key, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .delete_entry(&DataStoreDeleteEntry { - name: datastore_name, - scope, - key, - }) - .await; - match res { - Ok(_) => Ok(None), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::ListVersions { - datastore_name, - scope, - key, - start_time, - end_time, - sort_order, - limit, - cursor, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .list_entry_versions(&DataStoreListEntryVersions { - name: datastore_name, - scope, - key, - start_time, - end_time, - sort_order: format!("{:?}", sort_order), - limit: ReturnLimit(limit), - cursor, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{:#?}", data))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::GetVersion { - datastore_name, - scope, - key, - version_id, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .get_entry_version(&DataStoreGetEntryVersion { - name: datastore_name, - scope, - key, - version_id, - }) - .await; - match res { - Ok(data) => Ok(Some(data)), - Err(err) => Err(err.into()), - } - } - } - } -} +use clap::{Args, Subcommand, ValueEnum}; + +use rbxcloud::rbx::{ + DataStoreDeleteEntry, DataStoreGetEntry, DataStoreGetEntryVersion, DataStoreIncrementEntry, + DataStoreListEntries, DataStoreListEntryVersions, DataStoreListStores, DataStoreSetEntry, + RbxCloud, ReturnLimit, RobloxUserId, UniverseId, +}; + +#[derive(Debug, Subcommand)] +pub enum DataStoreCommands { + /// List all DataStores in a given universe + ListStores { + /// Return only DataStores with this prefix + #[clap(short, long, value_parser)] + prefix: Option, + + /// Maximum number of items to return + #[clap(short, long, value_parser)] + limit: u64, + + /// Cursor for the next set of data + #[clap(short, long, value_parser)] + cursor: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// List all entries in a DataStore + List { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// If true, return keys from all scopes + #[clap(short = 'o', long, value_parser)] + all_scopes: bool, + + /// Return only DataStores with this prefix + #[clap(short, long, value_parser)] + prefix: Option, + + /// Maximum number of items to return + #[clap(short, long, value_parser)] + limit: u64, + + /// Cursor for the next set of data + #[clap(short, long, value_parser)] + cursor: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Get a DataStore entry + Get { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Set or create the value of a DataStore entry + Set { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// Only update if the current version matches this + #[clap(short = 'i', long, value_parser)] + match_version: Option, + + /// Only create the entry if it does not exist + #[clap(short, long, value_parser)] + exclusive_create: Option, + + /// JSON-stringified data (up to 4MB) + #[clap(short = 'D', long, value_parser)] + data: String, + + /// Associated UserID (can be multiple) + #[clap(short = 'U', long, value_parser)] + user_ids: Option>, + + /// JSON-stringified attributes data + #[clap(short = 't', long, value_parser)] + attributes: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Increment or create the value of a DataStore entry + Increment { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// The amount by which the entry should be incremented + #[clap(short, long, value_parser)] + increment_by: f64, + + /// Comma-separated list of Roblox user IDs + #[clap(short = 'U', long, value_parser)] + user_ids: Option>, + + /// JSON-stringified attributes data + #[clap(short = 't', long, value_parser)] + attributes: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Delete a DataStore entry + Delete { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// List all versions of a DataStore entry + ListVersions { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// Start time constraint (ISO UTC Datetime) + #[clap(short = 't', long, value_parser)] + start_time: Option, + + /// End time constraint (ISO UTC Datetime) + #[clap(short = 'e', long, value_parser)] + end_time: Option, + + /// Sort order + #[clap(short = 'o', long, value_enum)] + sort_order: ListEntrySortOrder, + + /// Maximum number of items to return + #[clap(short, long, value_parser)] + limit: u64, + + /// Cursor for the next set of data + #[clap(short, long, value_parser)] + cursor: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Get the value of a specific entry version + GetVersion { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// The version of the key + #[clap(short = 'i', long, value_parser)] + version_id: String, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, +} + +#[derive(Debug, Clone, ValueEnum)] +pub enum ListEntrySortOrder { + Ascending, + Descending, +} + +#[derive(Debug, Args)] +pub struct DataStore { + #[clap(subcommand)] + command: DataStoreCommands, +} + +#[inline] +fn u64_ids_to_roblox_ids(user_ids: Option>) -> Option> { + user_ids.map(|ids| { + ids.into_iter() + .map(RobloxUserId) + .collect::>() + }) +} + +impl DataStore { + pub async fn run(self) -> anyhow::Result> { + match self.command { + DataStoreCommands::ListStores { + prefix, + limit, + cursor, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .list_stores(&DataStoreListStores { + cursor, + limit: ReturnLimit(limit), + prefix, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data:#?}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::List { + prefix, + limit, + cursor, + universe_id, + api_key, + datastore_name, + scope, + all_scopes, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .list_entries(&DataStoreListEntries { + name: datastore_name, + scope, + all_scopes, + prefix, + limit: ReturnLimit(limit), + cursor, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data:#?}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::Get { + datastore_name, + scope, + key, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .get_entry_string(&DataStoreGetEntry { + name: datastore_name, + scope, + key, + }) + .await; + match res { + Ok(data) => Ok(Some(data)), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::Set { + datastore_name, + scope, + key, + match_version, + exclusive_create, + data, + user_ids, + attributes, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let ids = u64_ids_to_roblox_ids(user_ids); + let res = datastore + .set_entry(&DataStoreSetEntry { + name: datastore_name, + scope, + key, + match_version, + exclusive_create, + roblox_entry_user_ids: ids, + roblox_entry_attributes: attributes, + data, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data:#?}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::Increment { + datastore_name, + scope, + key, + increment_by, + user_ids, + attributes, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let ids = u64_ids_to_roblox_ids(user_ids); + let res = datastore + .increment_entry(&DataStoreIncrementEntry { + name: datastore_name, + scope, + key, + roblox_entry_user_ids: ids, + roblox_entry_attributes: attributes, + increment_by, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::Delete { + datastore_name, + scope, + key, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .delete_entry(&DataStoreDeleteEntry { + name: datastore_name, + scope, + key, + }) + .await; + match res { + Ok(_) => Ok(None), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::ListVersions { + datastore_name, + scope, + key, + start_time, + end_time, + sort_order, + limit, + cursor, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .list_entry_versions(&DataStoreListEntryVersions { + name: datastore_name, + scope, + key, + start_time, + end_time, + sort_order: format!("{sort_order:?}"), + limit: ReturnLimit(limit), + cursor, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data:#?}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::GetVersion { + datastore_name, + scope, + key, + version_id, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .get_entry_version(&DataStoreGetEntryVersion { + name: datastore_name, + scope, + key, + version_id, + }) + .await; + match res { + Ok(data) => Ok(Some(data)), + Err(err) => Err(err.into()), + } + } + } + } +} diff --git a/src/cli/messaging_cli.rs b/src/cli/messaging_cli.rs index 7a3f341..7eb1c9e 100644 --- a/src/cli/messaging_cli.rs +++ b/src/cli/messaging_cli.rs @@ -1,52 +1,52 @@ -use clap::{Args, Subcommand}; - -use rbxcloud::rbx::{RbxCloud, UniverseId}; - -#[derive(Debug, Subcommand)] -pub enum MessagingCommands { - /// Publish a message - Publish { - /// Message topic - #[clap(short, long, value_parser)] - topic: String, - - /// Message to send - #[clap(short, long, value_parser)] - message: String, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, -} - -#[derive(Debug, Args)] -pub struct Messaging { - #[clap(subcommand)] - command: MessagingCommands, -} - -impl Messaging { - pub async fn run(self) -> anyhow::Result> { - match self.command { - MessagingCommands::Publish { - topic, - message, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let messaging = rbx_cloud.messaging(&topic); - let res = messaging.publish(&message).await; - match res { - Ok(()) => Ok(Some(format!("published message to topic {}", topic))), - Err(err) => Err(anyhow::anyhow!(err)), - } - } - } - } -} +use clap::{Args, Subcommand}; + +use rbxcloud::rbx::{RbxCloud, UniverseId}; + +#[derive(Debug, Subcommand)] +pub enum MessagingCommands { + /// Publish a message + Publish { + /// Message topic + #[clap(short, long, value_parser)] + topic: String, + + /// Message to send + #[clap(short, long, value_parser)] + message: String, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, +} + +#[derive(Debug, Args)] +pub struct Messaging { + #[clap(subcommand)] + command: MessagingCommands, +} + +impl Messaging { + pub async fn run(self) -> anyhow::Result> { + match self.command { + MessagingCommands::Publish { + topic, + message, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let messaging = rbx_cloud.messaging(&topic); + let res = messaging.publish(&message).await; + match res { + Ok(()) => Ok(Some(format!("published message to topic {topic}"))), + Err(err) => Err(anyhow::anyhow!(err)), + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 51081ad..df7da7a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,22 @@ -mod cli; - -use clap::Parser; -use cli::Cli; -use std::process; - -#[tokio::main] -async fn main() { - let cli_args = Cli::parse(); - - match cli_args.run().await { - Ok(str) => { - if let Some(s) = str { - println!("{}", s); - } - } - Err(err) => { - eprintln!("{:?}", err); - process::exit(1); - } - } -} +mod cli; + +use clap::Parser; +use cli::Cli; +use std::process; + +#[tokio::main] +async fn main() { + let cli_args = Cli::parse(); + + match cli_args.run().await { + Ok(str) => { + if let Some(s) = str { + println!("{s}"); + } + } + Err(err) => { + eprintln!("{err:?}"); + process::exit(1); + } + } +} diff --git a/src/rbx/datastore.rs b/src/rbx/datastore.rs index 9ab6286..4b5a9ca 100644 --- a/src/rbx/datastore.rs +++ b/src/rbx/datastore.rs @@ -1,514 +1,509 @@ -//! Low-level DataStore API operations. -//! -//! Typically, these operations should be consumed through the `RbxExperience` -//! struct, obtained through the `RbxCloud` struct. - -use std::fmt; - -use md5::{Digest, Md5}; -use reqwest::Response; -use serde::{de::DeserializeOwned, Deserialize}; - -use crate::rbx::{error::Error, ReturnLimit, RobloxUserId, UniverseId}; - -type QueryString = Vec<(&'static str, String)>; - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListDataStoreEntry { - pub name: String, - pub created_time: String, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListDataStoresResponse { - pub datastores: Vec, - pub next_page_cursor: Option, -} - -pub struct ListDataStoresParams { - pub api_key: String, - pub universe_id: UniverseId, - pub prefix: Option, - pub limit: ReturnLimit, - pub cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct DataStoreErrorResponse { - pub error: String, - pub message: String, - pub error_details: Vec, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct DataStoreErrorDetail { - pub error_detail_type: String, - pub datastore_error_code: DataStoreErrorCode, -} - -#[derive(Deserialize, Debug)] -pub enum DataStoreErrorCode { - ContentLengthRequired, - InvalidUniverseId, - InvalidCursor, - InvalidVersionId, - ExistingValueNotNumeric, - IncrementValueTooLarge, - IncrementValueTooSmall, - InvalidDataStoreScope, - InvalidEntryKey, - InvalidDataStoreName, - InvalidStartTime, - InvalidEndTime, - InvalidAttributes, - InvalidUserIds, - ExclusiveCreateAndMatchVersionCannotBeSet, - ContentTooBig, - ChecksumMismatch, - ContentNotJson, - InvalidSortOrder, - Forbidden, - InsufficientScope, - DatastoreNotFound, - EntryNotFound, - VersionNotFound, - TooManyRequests, - Unknown, -} - -impl fmt::Display for DataStoreErrorResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let details = self - .error_details - .iter() - .map(|item| format!("{:?}", item.datastore_error_code)) - .collect::>() - .join(", "); - write!(f, "[{}] - {}", details, self.message) - } -} - -pub struct ListEntriesParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub all_scopes: bool, - pub prefix: Option, - pub limit: ReturnLimit, - pub cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListEntriesResponse { - pub keys: Vec, - pub next_page_cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListEntriesKey { - pub scope: String, - pub key: String, -} - -pub struct GetEntryParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, -} - -pub struct SetEntryParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, - pub match_version: Option, - pub exclusive_create: Option, - pub roblox_entry_user_ids: Option>, - pub roblox_entry_attributes: Option, - pub data: String, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SetEntryResponse { - pub version: String, - pub deleted: bool, - pub content_length: u64, - pub created_time: String, - pub object_created_time: String, -} - -pub struct IncrementEntryParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, - pub roblox_entry_user_ids: Option>, - pub roblox_entry_attributes: Option, - pub increment_by: f64, -} - -pub struct DeleteEntryParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, -} - -pub struct ListEntryVersionsParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, - pub start_time: Option, - pub end_time: Option, - pub sort_order: String, - pub limit: ReturnLimit, - pub cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListEntryVersionsResponse { - pub versions: Vec, - pub next_page_cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListEntryVersion { - pub version: String, - pub deleted: bool, - pub content_length: u64, - pub created_time: String, - pub object_created_time: String, -} - -pub struct GetEntryVersionParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, - pub version_id: String, -} - -async fn handle_res(res: Response) -> Result { - match res.status().is_success() { - true => { - let body = res.json::().await?; - Ok(body) - } - false => { - let err_res = res.json::().await?; - Err(Error::DataStoreError(err_res)) - } - } -} - -async fn handle_res_string(res: Response) -> Result { - match res.status().is_success() { - true => { - let body = res.text().await?; - Ok(body) - } - false => { - let err_res = res.json::().await?; - Err(Error::DataStoreError(err_res)) - } - } -} - -async fn handle_res_ok(res: Response) -> Result<(), Error> { - match res.status().is_success() { - true => Ok(()), - false => { - let err_res = res.json::().await?; - Err(Error::DataStoreError(err_res)) - } - } -} - -fn build_url(endpoint: &str, universe_id: UniverseId) -> String { - if endpoint.is_empty() { - format!( - "https://apis.roblox.com/datastores/v1/universes/{universeId}/standard-datastores", - universeId = universe_id, - ) - } else { - format!( - "https://apis.roblox.com/datastores/v1/universes/{universeId}/standard-datastores/{endpoint}", - universeId=universe_id, - endpoint=endpoint, - ) - } -} - -#[inline] -fn get_checksum_base64(data: &String) -> String { - let mut md5_hash = Md5::new(); - md5_hash.update(&data.as_bytes()); - base64::encode(md5_hash.finalize()) -} - -/// List all DataStores within an experience. -pub async fn list_datastores( - params: &ListDataStoresParams, -) -> Result { - let client = reqwest::Client::new(); - let url = build_url("", params.universe_id); - let mut query: QueryString = vec![("limit", params.limit.to_string())]; - if let Some(prefix) = ¶ms.prefix { - query.push(("prefix", prefix.clone())); - } - if let Some(cursor) = ¶ms.cursor { - query.push(("cursor", cursor.clone())); - } - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res::(res).await -} - -/// List all entries of a DataStore. -pub async fn list_entries(params: &ListEntriesParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries", params.universe_id); - let mut query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ("limit", params.limit.to_string()), - ("AllScopes", params.all_scopes.to_string()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ]; - if let Some(prefix) = ¶ms.prefix { - query.push(("prefix", prefix.clone())); - } - if let Some(cursor) = ¶ms.cursor { - query.push(("cursor", cursor.clone())); - } - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res::(res).await -} - -async fn get_entry_response(params: &GetEntryParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry", params.universe_id); - let query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.clone()), - ]; - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - Ok(res) -} - -/// Get the value of an entry as a string. -pub async fn get_entry_string(params: &GetEntryParams) -> Result { - let res = get_entry_response(params).await?; - handle_res_string(res).await -} - -/// Get the value of an entry as a JSON-deserialized type `T`. -pub async fn get_entry(params: &GetEntryParams) -> Result { - let res = get_entry_response(params).await?; - handle_res::(res).await -} - -fn build_ids_csv(ids: &Option>) -> String { - ids.as_ref() - .unwrap_or(&vec![]) - .iter() - .map(|id| format!("{}", id)) - .collect::>() - .join(",") -} - -/// Set the value of an entry. -pub async fn set_entry(params: &SetEntryParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry", params.universe_id); - let mut query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.clone()), - ]; - if let Some(match_version) = ¶ms.match_version { - query.push(("matchVersion", match_version.clone())); - } - if let Some(exclusive_create) = ¶ms.exclusive_create { - query.push(("exclusiveCreate", exclusive_create.to_string())); - } - let res = client - .post(url) - .header("x-api-key", ¶ms.api_key) - .header("Content-Type", "application/json") - .header( - "roblox-entry-userids", - format!("[{}]", build_ids_csv(¶ms.roblox_entry_user_ids)), - ) - .header( - "roblox-entry-attributes", - params - .roblox_entry_attributes - .as_ref() - .unwrap_or(&String::from("{}")), - ) - .header("content-md5", get_checksum_base64(¶ms.data)) - .body(params.data.clone()) - .query(&query) - .send() - .await?; - handle_res::(res).await -} - -/// Increment the value of an entry. -pub async fn increment_entry(params: &IncrementEntryParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry/increment", params.universe_id); - let query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.clone()), - ("incrementBy", params.increment_by.to_string()), - ]; - let ids = build_ids_csv(¶ms.roblox_entry_user_ids); - let res = client - .post(url) - .header("x-api-key", ¶ms.api_key) - .header("roblox-entry-userids", format!("[{}]", ids)) - .header( - "roblox-entry-attributes", - params - .roblox_entry_attributes - .as_ref() - .unwrap_or(&"{}".to_string()), - ) - .query(&query) - .send() - .await?; - match handle_res_string(res).await { - Ok(data) => match data.parse::() { - Ok(num) => Ok(num), - Err(e) => Err(e.into()), - }, - Err(err) => Err(err), - } -} - -/// Delete an entry. -pub async fn delete_entry(params: &DeleteEntryParams) -> Result<(), Error> { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry", params.universe_id); - let query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.clone()), - ]; - let res = client - .delete(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res_ok(res).await -} - -/// List all of the versions of an entry. -pub async fn list_entry_versions( - params: &ListEntryVersionsParams, -) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry/versions", params.universe_id); - let mut query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.to_string()), - ("limit", params.limit.to_string()), - ("sortOrder", params.sort_order.to_string()), - ]; - if let Some(start_time) = ¶ms.start_time { - query.push(("startTime", start_time.clone())); - } - if let Some(end_time) = ¶ms.end_time { - query.push(("endTime", end_time.clone())); - } - if let Some(cursor) = ¶ms.cursor { - query.push(("cursor", cursor.clone())); - } - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res::(res).await -} - -/// Get the value of a specific entry version. -pub async fn get_entry_version(params: &GetEntryVersionParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url( - "/datastore/entries/entry/versions/version", - params.universe_id, - ); - let query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.to_string()), - ("versionId", params.version_id.to_string()), - ]; - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res_string(res).await -} +//! Low-level DataStore API operations. +//! +//! Typically, these operations should be consumed through the `RbxExperience` +//! struct, obtained through the `RbxCloud` struct. + +use std::fmt; + +use md5::{Digest, Md5}; +use reqwest::Response; +use serde::{de::DeserializeOwned, Deserialize}; + +use crate::rbx::{error::Error, ReturnLimit, RobloxUserId, UniverseId}; + +type QueryString = Vec<(&'static str, String)>; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListDataStoreEntry { + pub name: String, + pub created_time: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListDataStoresResponse { + pub datastores: Vec, + pub next_page_cursor: Option, +} + +pub struct ListDataStoresParams { + pub api_key: String, + pub universe_id: UniverseId, + pub prefix: Option, + pub limit: ReturnLimit, + pub cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DataStoreErrorResponse { + pub error: String, + pub message: String, + pub error_details: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DataStoreErrorDetail { + pub error_detail_type: String, + pub datastore_error_code: DataStoreErrorCode, +} + +#[derive(Deserialize, Debug)] +pub enum DataStoreErrorCode { + ContentLengthRequired, + InvalidUniverseId, + InvalidCursor, + InvalidVersionId, + ExistingValueNotNumeric, + IncrementValueTooLarge, + IncrementValueTooSmall, + InvalidDataStoreScope, + InvalidEntryKey, + InvalidDataStoreName, + InvalidStartTime, + InvalidEndTime, + InvalidAttributes, + InvalidUserIds, + ExclusiveCreateAndMatchVersionCannotBeSet, + ContentTooBig, + ChecksumMismatch, + ContentNotJson, + InvalidSortOrder, + Forbidden, + InsufficientScope, + DatastoreNotFound, + EntryNotFound, + VersionNotFound, + TooManyRequests, + Unknown, +} + +impl fmt::Display for DataStoreErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let details = self + .error_details + .iter() + .map(|item| format!("{:?}", item.datastore_error_code)) + .collect::>() + .join(", "); + write!(f, "[{}] - {}", details, self.message) + } +} + +pub struct ListEntriesParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub all_scopes: bool, + pub prefix: Option, + pub limit: ReturnLimit, + pub cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListEntriesResponse { + pub keys: Vec, + pub next_page_cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListEntriesKey { + pub scope: String, + pub key: String, +} + +pub struct GetEntryParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, +} + +pub struct SetEntryParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, + pub match_version: Option, + pub exclusive_create: Option, + pub roblox_entry_user_ids: Option>, + pub roblox_entry_attributes: Option, + pub data: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SetEntryResponse { + pub version: String, + pub deleted: bool, + pub content_length: u64, + pub created_time: String, + pub object_created_time: String, +} + +pub struct IncrementEntryParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, + pub roblox_entry_user_ids: Option>, + pub roblox_entry_attributes: Option, + pub increment_by: f64, +} + +pub struct DeleteEntryParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, +} + +pub struct ListEntryVersionsParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, + pub start_time: Option, + pub end_time: Option, + pub sort_order: String, + pub limit: ReturnLimit, + pub cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListEntryVersionsResponse { + pub versions: Vec, + pub next_page_cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListEntryVersion { + pub version: String, + pub deleted: bool, + pub content_length: u64, + pub created_time: String, + pub object_created_time: String, +} + +pub struct GetEntryVersionParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, + pub version_id: String, +} + +async fn handle_res(res: Response) -> Result { + match res.status().is_success() { + true => { + let body = res.json::().await?; + Ok(body) + } + false => { + let err_res = res.json::().await?; + Err(Error::DataStoreError(err_res)) + } + } +} + +async fn handle_res_string(res: Response) -> Result { + match res.status().is_success() { + true => { + let body = res.text().await?; + Ok(body) + } + false => { + let err_res = res.json::().await?; + Err(Error::DataStoreError(err_res)) + } + } +} + +async fn handle_res_ok(res: Response) -> Result<(), Error> { + match res.status().is_success() { + true => Ok(()), + false => { + let err_res = res.json::().await?; + Err(Error::DataStoreError(err_res)) + } + } +} + +fn build_url(endpoint: &str, universe_id: UniverseId) -> String { + if endpoint.is_empty() { + format!("https://apis.roblox.com/datastores/v1/universes/{universe_id}/standard-datastores",) + } else { + format!( + "https://apis.roblox.com/datastores/v1/universes/{universe_id}/standard-datastores{endpoint}", + ) + } +} + +#[inline] +fn get_checksum_base64(data: &String) -> String { + let mut md5_hash = Md5::new(); + md5_hash.update(data.as_bytes()); + base64::encode(md5_hash.finalize()) +} + +/// List all DataStores within an experience. +pub async fn list_datastores( + params: &ListDataStoresParams, +) -> Result { + let client = reqwest::Client::new(); + let url = build_url("", params.universe_id); + let mut query: QueryString = vec![("limit", params.limit.to_string())]; + if let Some(prefix) = ¶ms.prefix { + query.push(("prefix", prefix.clone())); + } + if let Some(cursor) = ¶ms.cursor { + query.push(("cursor", cursor.clone())); + } + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res::(res).await +} + +/// List all entries of a DataStore. +pub async fn list_entries(params: &ListEntriesParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries", params.universe_id); + let mut query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ("limit", params.limit.to_string()), + ("AllScopes", params.all_scopes.to_string()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ]; + if let Some(prefix) = ¶ms.prefix { + query.push(("prefix", prefix.clone())); + } + if let Some(cursor) = ¶ms.cursor { + query.push(("cursor", cursor.clone())); + } + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res::(res).await +} + +async fn get_entry_response(params: &GetEntryParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry", params.universe_id); + let query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.clone()), + ]; + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + Ok(res) +} + +/// Get the value of an entry as a string. +pub async fn get_entry_string(params: &GetEntryParams) -> Result { + let res = get_entry_response(params).await?; + handle_res_string(res).await +} + +/// Get the value of an entry as a JSON-deserialized type `T`. +pub async fn get_entry(params: &GetEntryParams) -> Result { + let res = get_entry_response(params).await?; + handle_res::(res).await +} + +fn build_ids_csv(ids: &Option>) -> String { + ids.as_ref() + .unwrap_or(&vec![]) + .iter() + .map(|id| format!("{id}")) + .collect::>() + .join(",") +} + +/// Set the value of an entry. +pub async fn set_entry(params: &SetEntryParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry", params.universe_id); + let mut query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.clone()), + ]; + if let Some(match_version) = ¶ms.match_version { + query.push(("matchVersion", match_version.clone())); + } + if let Some(exclusive_create) = ¶ms.exclusive_create { + query.push(("exclusiveCreate", exclusive_create.to_string())); + } + let res = client + .post(url) + .header("x-api-key", ¶ms.api_key) + .header("Content-Type", "application/json") + .header( + "roblox-entry-userids", + format!("[{}]", build_ids_csv(¶ms.roblox_entry_user_ids)), + ) + .header( + "roblox-entry-attributes", + params + .roblox_entry_attributes + .as_ref() + .unwrap_or(&String::from("{}")), + ) + .header("content-md5", get_checksum_base64(¶ms.data)) + .body(params.data.clone()) + .query(&query) + .send() + .await?; + handle_res::(res).await +} + +/// Increment the value of an entry. +pub async fn increment_entry(params: &IncrementEntryParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry/increment", params.universe_id); + let query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.clone()), + ("incrementBy", params.increment_by.to_string()), + ]; + let ids = build_ids_csv(¶ms.roblox_entry_user_ids); + let res = client + .post(url) + .header("x-api-key", ¶ms.api_key) + .header("roblox-entry-userids", format!("[{ids}]")) + .header( + "roblox-entry-attributes", + params + .roblox_entry_attributes + .as_ref() + .unwrap_or(&"{}".to_string()), + ) + .query(&query) + .send() + .await?; + match handle_res_string(res).await { + Ok(data) => match data.parse::() { + Ok(num) => Ok(num), + Err(e) => Err(e.into()), + }, + Err(err) => Err(err), + } +} + +/// Delete an entry. +pub async fn delete_entry(params: &DeleteEntryParams) -> Result<(), Error> { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry", params.universe_id); + let query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.clone()), + ]; + let res = client + .delete(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res_ok(res).await +} + +/// List all of the versions of an entry. +pub async fn list_entry_versions( + params: &ListEntryVersionsParams, +) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry/versions", params.universe_id); + let mut query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.to_string()), + ("limit", params.limit.to_string()), + ("sortOrder", params.sort_order.to_string()), + ]; + if let Some(start_time) = ¶ms.start_time { + query.push(("startTime", start_time.clone())); + } + if let Some(end_time) = ¶ms.end_time { + query.push(("endTime", end_time.clone())); + } + if let Some(cursor) = ¶ms.cursor { + query.push(("cursor", cursor.clone())); + } + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res::(res).await +} + +/// Get the value of a specific entry version. +pub async fn get_entry_version(params: &GetEntryVersionParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url( + "/datastore/entries/entry/versions/version", + params.universe_id, + ); + let query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.to_string()), + ("versionId", params.version_id.to_string()), + ]; + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res_string(res).await +} diff --git a/src/rbx/error.rs b/src/rbx/error.rs index d4201dd..4992db2 100644 --- a/src/rbx/error.rs +++ b/src/rbx/error.rs @@ -55,13 +55,13 @@ impl From for Error { impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { match self { - Self::FileLoadError(s) => write!(f, "failed to read file: {}", s), - Self::HttpStatusError { code, msg } => write!(f, "http {}: {}", code, msg), - Self::ReqwestError(e) => write!(f, "{:?}", e), - Self::IOError(e) => write!(f, "{:?}", e), - Self::SerdeJsonError(e) => write!(f, "{:?}", e), - Self::DataStoreError(e) => write!(f, "{:?}", e), - Self::ParseFloatError(e) => write!(f, "{:?}", e), + Self::FileLoadError(s) => write!(f, "failed to read file: {s}"), + Self::HttpStatusError { code, msg } => write!(f, "http {code}: {msg}"), + Self::ReqwestError(e) => write!(f, "{e:?}"), + Self::IOError(e) => write!(f, "{e:?}"), + Self::SerdeJsonError(e) => write!(f, "{e:?}"), + Self::DataStoreError(e) => write!(f, "{e:?}"), + Self::ParseFloatError(e) => write!(f, "{e:?}"), } } } diff --git a/src/rbx/experience.rs b/src/rbx/experience.rs index 23ca33e..7127245 100644 --- a/src/rbx/experience.rs +++ b/src/rbx/experience.rs @@ -20,7 +20,7 @@ pub enum PublishVersionType { impl fmt::Display for PublishVersionType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{:?}", self) + write!(f, "{self:?}") } } diff --git a/src/rbx/mod.rs b/src/rbx/mod.rs index f325b5c..78384d3 100644 --- a/src/rbx/mod.rs +++ b/src/rbx/mod.rs @@ -1,374 +1,374 @@ -//! Access into Roblox APIs. -//! -//! Most usage should go through the `RbxCloud` struct. -pub mod datastore; -pub mod error; -pub mod experience; -pub mod messaging; - -pub use experience::PublishVersionType; -use serde::de::DeserializeOwned; - -use self::{ - datastore::{ - DeleteEntryParams, GetEntryParams, GetEntryVersionParams, IncrementEntryParams, - ListDataStoresParams, ListDataStoresResponse, ListEntriesParams, ListEntriesResponse, - ListEntryVersionsParams, ListEntryVersionsResponse, SetEntryParams, SetEntryResponse, - }, - error::Error, - experience::{PublishExperienceParams, PublishExperienceResponse}, - messaging::PublishMessageParams, -}; - -/// Represents the UniverseId of a Roblox experience. -#[derive(Debug, Clone, Copy)] -pub struct UniverseId(pub u64); - -/// Represents the PlaceId of a specific place within a Roblox experience. -#[derive(Debug, Clone, Copy)] -pub struct PlaceId(pub u64); - -// Number of items to return. -#[derive(Debug, Clone, Copy)] -pub struct ReturnLimit(pub u64); - -/// Represents a Roblox user's ID. -#[derive(Debug, Clone, Copy)] -pub struct RobloxUserId(pub u64); - -impl std::fmt::Display for UniverseId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::fmt::Display for PlaceId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::fmt::Display for ReturnLimit { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::fmt::Display for RobloxUserId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -pub struct RbxExperience { - pub universe_id: UniverseId, - pub place_id: PlaceId, - pub api_key: String, -} - -impl RbxExperience { - /// Publish a place. - /// - /// The filename should point to a `*.rbxl` or `*.rbxlx` file. - pub async fn publish( - &self, - filename: &str, - version_type: PublishVersionType, - ) -> Result { - experience::publish_experience(&PublishExperienceParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - place_id: self.place_id, - version_type, - filename: filename.to_string(), - }) - .await - } -} - -pub struct RbxMessaging { - pub api_key: String, - pub universe_id: UniverseId, - pub topic: String, -} - -impl RbxMessaging { - /// Publish a message. - pub async fn publish(&self, message: &str) -> Result<(), Error> { - messaging::publish_message(&PublishMessageParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - topic: self.topic.clone(), - message: message.to_string(), - }) - .await - } -} - -pub struct RbxDataStore { - pub api_key: String, - pub universe_id: UniverseId, -} - -pub struct DataStoreListStores { - pub prefix: Option, - pub limit: ReturnLimit, - pub cursor: Option, -} - -pub struct DataStoreListEntries { - pub name: String, - pub scope: Option, - pub all_scopes: bool, - pub prefix: Option, - pub limit: ReturnLimit, - pub cursor: Option, -} - -pub struct DataStoreGetEntry { - pub name: String, - pub scope: Option, - pub key: String, -} - -pub struct DataStoreSetEntry { - pub name: String, - pub scope: Option, - pub key: String, - pub match_version: Option, - pub exclusive_create: Option, - pub roblox_entry_user_ids: Option>, - pub roblox_entry_attributes: Option, - pub data: String, -} - -pub struct DataStoreIncrementEntry { - pub name: String, - pub scope: Option, - pub key: String, - pub roblox_entry_user_ids: Option>, - pub roblox_entry_attributes: Option, - pub increment_by: f64, -} - -pub struct DataStoreDeleteEntry { - pub name: String, - pub scope: Option, - pub key: String, -} - -pub struct DataStoreListEntryVersions { - pub name: String, - pub scope: Option, - pub key: String, - pub start_time: Option, - pub end_time: Option, - pub sort_order: String, - pub limit: ReturnLimit, - pub cursor: Option, -} - -pub struct DataStoreGetEntryVersion { - pub name: String, - pub scope: Option, - pub key: String, - pub version_id: String, -} - -impl RbxDataStore { - /// List DataStores within the experience. - pub async fn list_stores( - &self, - params: &DataStoreListStores, - ) -> Result { - datastore::list_datastores(&ListDataStoresParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - prefix: params.prefix.clone(), - limit: params.limit, - cursor: params.cursor.clone(), - }) - .await - } - - /// List key entries in a specific DataStore. - pub async fn list_entries( - &self, - params: &DataStoreListEntries, - ) -> Result { - datastore::list_entries(&ListEntriesParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - all_scopes: params.all_scopes, - prefix: params.prefix.clone(), - limit: params.limit, - cursor: params.cursor.clone(), - }) - .await - } - - /// Get the entry string representation of a specific key. - pub async fn get_entry_string(&self, params: &DataStoreGetEntry) -> Result { - datastore::get_entry_string(&GetEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - }) - .await - } - - /// Get the entry of a specific key, deserialized as `T`. - pub async fn get_entry( - &self, - params: &DataStoreGetEntry, - ) -> Result { - datastore::get_entry::(&GetEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - }) - .await - } - - /// Set (or create) the entry value of a specific key. - pub async fn set_entry(&self, params: &DataStoreSetEntry) -> Result { - datastore::set_entry(&SetEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - match_version: params.match_version.clone(), - exclusive_create: params.exclusive_create, - roblox_entry_user_ids: params.roblox_entry_user_ids.clone(), - roblox_entry_attributes: params.roblox_entry_attributes.clone(), - data: params.data.clone(), - }) - .await - } - - /// Increment (or create) the value of a specific key. - /// - /// If the value does not yet exist, it will be treated as `0`, and thus - /// the resulting value will simply be the increment amount. - /// - /// If the value _does_ exist, but it is _not_ a number, then the increment - /// process will fail, and a DataStore error will be returned in the result. - pub async fn increment_entry(&self, params: &DataStoreIncrementEntry) -> Result { - datastore::increment_entry(&IncrementEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - roblox_entry_user_ids: params.roblox_entry_user_ids.clone(), - roblox_entry_attributes: params.roblox_entry_attributes.clone(), - increment_by: params.increment_by, - }) - .await - } - - /// Delete an entry. - pub async fn delete_entry(&self, params: &DataStoreDeleteEntry) -> Result<(), Error> { - datastore::delete_entry(&DeleteEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - }) - .await - } - - /// List all versions of an entry. - /// - /// To get the specific value of a given entry, use `get_entry_version()`. - pub async fn list_entry_versions( - &self, - params: &DataStoreListEntryVersions, - ) -> Result { - datastore::list_entry_versions(&ListEntryVersionsParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - start_time: params.start_time.clone(), - end_time: params.end_time.clone(), - sort_order: params.sort_order.clone(), - limit: params.limit, - cursor: params.cursor.clone(), - }) - .await - } - - /// Get the entry value of a specific version. - pub async fn get_entry_version( - &self, - params: &DataStoreGetEntryVersion, - ) -> Result { - datastore::get_entry_version(&GetEntryVersionParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - version_id: params.version_id.clone(), - }) - .await - } -} - -/// Access into the Roblox Open Cloud APIs. -/// -/// ```rust,no_run -/// use rbxcloud::rbx::{RbxCloud, UniverseId}; -/// -/// let cloud = RbxCloud::new("API_KEY", UniverseId(9876543210)); -/// ``` -#[derive(Debug)] -pub struct RbxCloud { - /// Roblox API key. - pub api_key: String, - - /// The UniverseId of a given Roblox experience. - pub universe_id: UniverseId, -} - -impl RbxCloud { - pub fn new(api_key: &str, universe_id: UniverseId) -> RbxCloud { - RbxCloud { - api_key: api_key.to_string(), - universe_id, - } - } - - pub fn experience(&self, place_id: PlaceId) -> RbxExperience { - RbxExperience { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - place_id, - } - } - - pub fn messaging(&self, topic: &str) -> RbxMessaging { - RbxMessaging { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - topic: topic.to_string(), - } - } - - pub fn datastore(&self) -> RbxDataStore { - RbxDataStore { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - } - } -} +//! Access into Roblox APIs. +//! +//! Most usage should go through the `RbxCloud` struct. +pub mod datastore; +pub mod error; +pub mod experience; +pub mod messaging; + +pub use experience::PublishVersionType; +use serde::de::DeserializeOwned; + +use self::{ + datastore::{ + DeleteEntryParams, GetEntryParams, GetEntryVersionParams, IncrementEntryParams, + ListDataStoresParams, ListDataStoresResponse, ListEntriesParams, ListEntriesResponse, + ListEntryVersionsParams, ListEntryVersionsResponse, SetEntryParams, SetEntryResponse, + }, + error::Error, + experience::{PublishExperienceParams, PublishExperienceResponse}, + messaging::PublishMessageParams, +}; + +/// Represents the UniverseId of a Roblox experience. +#[derive(Debug, Clone, Copy)] +pub struct UniverseId(pub u64); + +/// Represents the PlaceId of a specific place within a Roblox experience. +#[derive(Debug, Clone, Copy)] +pub struct PlaceId(pub u64); + +// Number of items to return. +#[derive(Debug, Clone, Copy)] +pub struct ReturnLimit(pub u64); + +/// Represents a Roblox user's ID. +#[derive(Debug, Clone, Copy)] +pub struct RobloxUserId(pub u64); + +impl std::fmt::Display for UniverseId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Display for PlaceId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Display for ReturnLimit { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Display for RobloxUserId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +pub struct RbxExperience { + pub universe_id: UniverseId, + pub place_id: PlaceId, + pub api_key: String, +} + +impl RbxExperience { + /// Publish a place. + /// + /// The filename should point to a `*.rbxl` or `*.rbxlx` file. + pub async fn publish( + &self, + filename: &str, + version_type: PublishVersionType, + ) -> Result { + experience::publish_experience(&PublishExperienceParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + place_id: self.place_id, + version_type, + filename: filename.to_string(), + }) + .await + } +} + +pub struct RbxMessaging { + pub api_key: String, + pub universe_id: UniverseId, + pub topic: String, +} + +impl RbxMessaging { + /// Publish a message. + pub async fn publish(&self, message: &str) -> Result<(), Error> { + messaging::publish_message(&PublishMessageParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + topic: self.topic.clone(), + message: message.to_string(), + }) + .await + } +} + +pub struct RbxDataStore { + pub api_key: String, + pub universe_id: UniverseId, +} + +pub struct DataStoreListStores { + pub prefix: Option, + pub limit: ReturnLimit, + pub cursor: Option, +} + +pub struct DataStoreListEntries { + pub name: String, + pub scope: Option, + pub all_scopes: bool, + pub prefix: Option, + pub limit: ReturnLimit, + pub cursor: Option, +} + +pub struct DataStoreGetEntry { + pub name: String, + pub scope: Option, + pub key: String, +} + +pub struct DataStoreSetEntry { + pub name: String, + pub scope: Option, + pub key: String, + pub match_version: Option, + pub exclusive_create: Option, + pub roblox_entry_user_ids: Option>, + pub roblox_entry_attributes: Option, + pub data: String, +} + +pub struct DataStoreIncrementEntry { + pub name: String, + pub scope: Option, + pub key: String, + pub roblox_entry_user_ids: Option>, + pub roblox_entry_attributes: Option, + pub increment_by: f64, +} + +pub struct DataStoreDeleteEntry { + pub name: String, + pub scope: Option, + pub key: String, +} + +pub struct DataStoreListEntryVersions { + pub name: String, + pub scope: Option, + pub key: String, + pub start_time: Option, + pub end_time: Option, + pub sort_order: String, + pub limit: ReturnLimit, + pub cursor: Option, +} + +pub struct DataStoreGetEntryVersion { + pub name: String, + pub scope: Option, + pub key: String, + pub version_id: String, +} + +impl RbxDataStore { + /// List DataStores within the experience. + pub async fn list_stores( + &self, + params: &DataStoreListStores, + ) -> Result { + datastore::list_datastores(&ListDataStoresParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + prefix: params.prefix.clone(), + limit: params.limit, + cursor: params.cursor.clone(), + }) + .await + } + + /// List key entries in a specific DataStore. + pub async fn list_entries( + &self, + params: &DataStoreListEntries, + ) -> Result { + datastore::list_entries(&ListEntriesParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + all_scopes: params.all_scopes, + prefix: params.prefix.clone(), + limit: params.limit, + cursor: params.cursor.clone(), + }) + .await + } + + /// Get the entry string representation of a specific key. + pub async fn get_entry_string(&self, params: &DataStoreGetEntry) -> Result { + datastore::get_entry_string(&GetEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + }) + .await + } + + /// Get the entry of a specific key, deserialized as `T`. + pub async fn get_entry( + &self, + params: &DataStoreGetEntry, + ) -> Result { + datastore::get_entry::(&GetEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + }) + .await + } + + /// Set (or create) the entry value of a specific key. + pub async fn set_entry(&self, params: &DataStoreSetEntry) -> Result { + datastore::set_entry(&SetEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + match_version: params.match_version.clone(), + exclusive_create: params.exclusive_create, + roblox_entry_user_ids: params.roblox_entry_user_ids.clone(), + roblox_entry_attributes: params.roblox_entry_attributes.clone(), + data: params.data.clone(), + }) + .await + } + + /// Increment (or create) the value of a specific key. + /// + /// If the value does not yet exist, it will be treated as `0`, and thus + /// the resulting value will simply be the increment amount. + /// + /// If the value _does_ exist, but it is _not_ a number, then the increment + /// process will fail, and a DataStore error will be returned in the result. + pub async fn increment_entry(&self, params: &DataStoreIncrementEntry) -> Result { + datastore::increment_entry(&IncrementEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + roblox_entry_user_ids: params.roblox_entry_user_ids.clone(), + roblox_entry_attributes: params.roblox_entry_attributes.clone(), + increment_by: params.increment_by, + }) + .await + } + + /// Delete an entry. + pub async fn delete_entry(&self, params: &DataStoreDeleteEntry) -> Result<(), Error> { + datastore::delete_entry(&DeleteEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + }) + .await + } + + /// List all versions of an entry. + /// + /// To get the specific value of a given entry, use `get_entry_version()`. + pub async fn list_entry_versions( + &self, + params: &DataStoreListEntryVersions, + ) -> Result { + datastore::list_entry_versions(&ListEntryVersionsParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + start_time: params.start_time.clone(), + end_time: params.end_time.clone(), + sort_order: params.sort_order.clone(), + limit: params.limit, + cursor: params.cursor.clone(), + }) + .await + } + + /// Get the entry value of a specific version. + pub async fn get_entry_version( + &self, + params: &DataStoreGetEntryVersion, + ) -> Result { + datastore::get_entry_version(&GetEntryVersionParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + version_id: params.version_id.clone(), + }) + .await + } +} + +/// Access into the Roblox Open Cloud APIs. +/// +/// ```rust,no_run +/// use rbxcloud::rbx::{RbxCloud, UniverseId}; +/// +/// let cloud = RbxCloud::new("API_KEY", UniverseId(9876543210)); +/// ``` +#[derive(Debug)] +pub struct RbxCloud { + /// Roblox API key. + pub api_key: String, + + /// The UniverseId of a given Roblox experience. + pub universe_id: UniverseId, +} + +impl RbxCloud { + pub fn new(api_key: &str, universe_id: UniverseId) -> RbxCloud { + RbxCloud { + api_key: api_key.to_string(), + universe_id, + } + } + + pub fn experience(&self, place_id: PlaceId) -> RbxExperience { + RbxExperience { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + place_id, + } + } + + pub fn messaging(&self, topic: &str) -> RbxMessaging { + RbxMessaging { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + topic: topic.to_string(), + } + } + + pub fn datastore(&self) -> RbxDataStore { + RbxDataStore { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + } + } +} From 87500dc486b64e3642b41ece969d424d928da636 Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Wed, 8 Feb 2023 21:26:36 -0500 Subject: [PATCH 2/5] EOL --- .vscode/settings.json | 17 +- examples/datastore-get-entry.rs | 4 +- examples/publish-message.rs | 2 +- examples/publish-place.rs | 2 +- src/cli/datastore_cli.rs | 1002 +++++++++++++++--------------- src/cli/messaging_cli.rs | 104 ++-- src/main.rs | 44 +- src/rbx/datastore.rs | 1018 +++++++++++++++---------------- src/rbx/mod.rs | 748 +++++++++++------------ 9 files changed, 1471 insertions(+), 1470 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 17f0f65..d8388db 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,9 @@ -{ - "yaml.schemas": { - "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" - }, - "[rust]": { - "editor.formatOnSave": true - } -} +{ + "yaml.schemas": { + "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + }, + "[rust]": { + "editor.formatOnSave": true + }, + "files.eol": "\n" +} diff --git a/examples/datastore-get-entry.rs b/examples/datastore-get-entry.rs index a36032f..4b7cf07 100644 --- a/examples/datastore-get-entry.rs +++ b/examples/datastore-get-entry.rs @@ -24,10 +24,10 @@ async fn main() { // Print entry result or error: match entry_result { Ok(result) => { - println!("{}", result); + println!("{result}"); } Err(e) => { - eprintln!("{:?}", e); + eprintln!("{e:?}"); } } } diff --git a/examples/publish-message.rs b/examples/publish-message.rs index 51b255e..d03b6cf 100644 --- a/examples/publish-message.rs +++ b/examples/publish-message.rs @@ -25,7 +25,7 @@ async fn main() { println!("Message successfully published"); } Err(e) => { - eprintln!("{:?}", e); + eprintln!("{e:?}"); } } diff --git a/examples/publish-place.rs b/examples/publish-place.rs index 4e4d551..321e9dc 100644 --- a/examples/publish-place.rs +++ b/examples/publish-place.rs @@ -20,7 +20,7 @@ async fn main() { println!("Published place! New version: {}", result.version_number); } Err(e) => { - eprintln!("{:?}", e); + eprintln!("{e:?}"); } } } diff --git a/src/cli/datastore_cli.rs b/src/cli/datastore_cli.rs index 742aa88..a45921a 100644 --- a/src/cli/datastore_cli.rs +++ b/src/cli/datastore_cli.rs @@ -1,501 +1,501 @@ -use clap::{Args, Subcommand, ValueEnum}; - -use rbxcloud::rbx::{ - DataStoreDeleteEntry, DataStoreGetEntry, DataStoreGetEntryVersion, DataStoreIncrementEntry, - DataStoreListEntries, DataStoreListEntryVersions, DataStoreListStores, DataStoreSetEntry, - RbxCloud, ReturnLimit, RobloxUserId, UniverseId, -}; - -#[derive(Debug, Subcommand)] -pub enum DataStoreCommands { - /// List all DataStores in a given universe - ListStores { - /// Return only DataStores with this prefix - #[clap(short, long, value_parser)] - prefix: Option, - - /// Maximum number of items to return - #[clap(short, long, value_parser)] - limit: u64, - - /// Cursor for the next set of data - #[clap(short, long, value_parser)] - cursor: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// List all entries in a DataStore - List { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// If true, return keys from all scopes - #[clap(short = 'o', long, value_parser)] - all_scopes: bool, - - /// Return only DataStores with this prefix - #[clap(short, long, value_parser)] - prefix: Option, - - /// Maximum number of items to return - #[clap(short, long, value_parser)] - limit: u64, - - /// Cursor for the next set of data - #[clap(short, long, value_parser)] - cursor: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Get a DataStore entry - Get { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Set or create the value of a DataStore entry - Set { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// Only update if the current version matches this - #[clap(short = 'i', long, value_parser)] - match_version: Option, - - /// Only create the entry if it does not exist - #[clap(short, long, value_parser)] - exclusive_create: Option, - - /// JSON-stringified data (up to 4MB) - #[clap(short = 'D', long, value_parser)] - data: String, - - /// Associated UserID (can be multiple) - #[clap(short = 'U', long, value_parser)] - user_ids: Option>, - - /// JSON-stringified attributes data - #[clap(short = 't', long, value_parser)] - attributes: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Increment or create the value of a DataStore entry - Increment { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// The amount by which the entry should be incremented - #[clap(short, long, value_parser)] - increment_by: f64, - - /// Comma-separated list of Roblox user IDs - #[clap(short = 'U', long, value_parser)] - user_ids: Option>, - - /// JSON-stringified attributes data - #[clap(short = 't', long, value_parser)] - attributes: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Delete a DataStore entry - Delete { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// List all versions of a DataStore entry - ListVersions { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// Start time constraint (ISO UTC Datetime) - #[clap(short = 't', long, value_parser)] - start_time: Option, - - /// End time constraint (ISO UTC Datetime) - #[clap(short = 'e', long, value_parser)] - end_time: Option, - - /// Sort order - #[clap(short = 'o', long, value_enum)] - sort_order: ListEntrySortOrder, - - /// Maximum number of items to return - #[clap(short, long, value_parser)] - limit: u64, - - /// Cursor for the next set of data - #[clap(short, long, value_parser)] - cursor: Option, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, - - /// Get the value of a specific entry version - GetVersion { - /// DataStore name - #[clap(short, long, value_parser)] - datastore_name: String, - - /// DataStore scope - #[clap(short, long, value_parser)] - scope: Option, - - /// The key of the entry - #[clap(short, long, value_parser)] - key: String, - - /// The version of the key - #[clap(short = 'i', long, value_parser)] - version_id: String, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, -} - -#[derive(Debug, Clone, ValueEnum)] -pub enum ListEntrySortOrder { - Ascending, - Descending, -} - -#[derive(Debug, Args)] -pub struct DataStore { - #[clap(subcommand)] - command: DataStoreCommands, -} - -#[inline] -fn u64_ids_to_roblox_ids(user_ids: Option>) -> Option> { - user_ids.map(|ids| { - ids.into_iter() - .map(RobloxUserId) - .collect::>() - }) -} - -impl DataStore { - pub async fn run(self) -> anyhow::Result> { - match self.command { - DataStoreCommands::ListStores { - prefix, - limit, - cursor, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .list_stores(&DataStoreListStores { - cursor, - limit: ReturnLimit(limit), - prefix, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{data:#?}"))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::List { - prefix, - limit, - cursor, - universe_id, - api_key, - datastore_name, - scope, - all_scopes, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .list_entries(&DataStoreListEntries { - name: datastore_name, - scope, - all_scopes, - prefix, - limit: ReturnLimit(limit), - cursor, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{data:#?}"))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::Get { - datastore_name, - scope, - key, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .get_entry_string(&DataStoreGetEntry { - name: datastore_name, - scope, - key, - }) - .await; - match res { - Ok(data) => Ok(Some(data)), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::Set { - datastore_name, - scope, - key, - match_version, - exclusive_create, - data, - user_ids, - attributes, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let ids = u64_ids_to_roblox_ids(user_ids); - let res = datastore - .set_entry(&DataStoreSetEntry { - name: datastore_name, - scope, - key, - match_version, - exclusive_create, - roblox_entry_user_ids: ids, - roblox_entry_attributes: attributes, - data, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{data:#?}"))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::Increment { - datastore_name, - scope, - key, - increment_by, - user_ids, - attributes, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let ids = u64_ids_to_roblox_ids(user_ids); - let res = datastore - .increment_entry(&DataStoreIncrementEntry { - name: datastore_name, - scope, - key, - roblox_entry_user_ids: ids, - roblox_entry_attributes: attributes, - increment_by, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{data}"))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::Delete { - datastore_name, - scope, - key, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .delete_entry(&DataStoreDeleteEntry { - name: datastore_name, - scope, - key, - }) - .await; - match res { - Ok(_) => Ok(None), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::ListVersions { - datastore_name, - scope, - key, - start_time, - end_time, - sort_order, - limit, - cursor, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .list_entry_versions(&DataStoreListEntryVersions { - name: datastore_name, - scope, - key, - start_time, - end_time, - sort_order: format!("{sort_order:?}"), - limit: ReturnLimit(limit), - cursor, - }) - .await; - match res { - Ok(data) => Ok(Some(format!("{data:#?}"))), - Err(err) => Err(err.into()), - } - } - - DataStoreCommands::GetVersion { - datastore_name, - scope, - key, - version_id, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let datastore = rbx_cloud.datastore(); - let res = datastore - .get_entry_version(&DataStoreGetEntryVersion { - name: datastore_name, - scope, - key, - version_id, - }) - .await; - match res { - Ok(data) => Ok(Some(data)), - Err(err) => Err(err.into()), - } - } - } - } -} +use clap::{Args, Subcommand, ValueEnum}; + +use rbxcloud::rbx::{ + DataStoreDeleteEntry, DataStoreGetEntry, DataStoreGetEntryVersion, DataStoreIncrementEntry, + DataStoreListEntries, DataStoreListEntryVersions, DataStoreListStores, DataStoreSetEntry, + RbxCloud, ReturnLimit, RobloxUserId, UniverseId, +}; + +#[derive(Debug, Subcommand)] +pub enum DataStoreCommands { + /// List all DataStores in a given universe + ListStores { + /// Return only DataStores with this prefix + #[clap(short, long, value_parser)] + prefix: Option, + + /// Maximum number of items to return + #[clap(short, long, value_parser)] + limit: u64, + + /// Cursor for the next set of data + #[clap(short, long, value_parser)] + cursor: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// List all entries in a DataStore + List { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// If true, return keys from all scopes + #[clap(short = 'o', long, value_parser)] + all_scopes: bool, + + /// Return only DataStores with this prefix + #[clap(short, long, value_parser)] + prefix: Option, + + /// Maximum number of items to return + #[clap(short, long, value_parser)] + limit: u64, + + /// Cursor for the next set of data + #[clap(short, long, value_parser)] + cursor: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Get a DataStore entry + Get { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Set or create the value of a DataStore entry + Set { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// Only update if the current version matches this + #[clap(short = 'i', long, value_parser)] + match_version: Option, + + /// Only create the entry if it does not exist + #[clap(short, long, value_parser)] + exclusive_create: Option, + + /// JSON-stringified data (up to 4MB) + #[clap(short = 'D', long, value_parser)] + data: String, + + /// Associated UserID (can be multiple) + #[clap(short = 'U', long, value_parser)] + user_ids: Option>, + + /// JSON-stringified attributes data + #[clap(short = 't', long, value_parser)] + attributes: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Increment or create the value of a DataStore entry + Increment { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// The amount by which the entry should be incremented + #[clap(short, long, value_parser)] + increment_by: f64, + + /// Comma-separated list of Roblox user IDs + #[clap(short = 'U', long, value_parser)] + user_ids: Option>, + + /// JSON-stringified attributes data + #[clap(short = 't', long, value_parser)] + attributes: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Delete a DataStore entry + Delete { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// List all versions of a DataStore entry + ListVersions { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// Start time constraint (ISO UTC Datetime) + #[clap(short = 't', long, value_parser)] + start_time: Option, + + /// End time constraint (ISO UTC Datetime) + #[clap(short = 'e', long, value_parser)] + end_time: Option, + + /// Sort order + #[clap(short = 'o', long, value_enum)] + sort_order: ListEntrySortOrder, + + /// Maximum number of items to return + #[clap(short, long, value_parser)] + limit: u64, + + /// Cursor for the next set of data + #[clap(short, long, value_parser)] + cursor: Option, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, + + /// Get the value of a specific entry version + GetVersion { + /// DataStore name + #[clap(short, long, value_parser)] + datastore_name: String, + + /// DataStore scope + #[clap(short, long, value_parser)] + scope: Option, + + /// The key of the entry + #[clap(short, long, value_parser)] + key: String, + + /// The version of the key + #[clap(short = 'i', long, value_parser)] + version_id: String, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, +} + +#[derive(Debug, Clone, ValueEnum)] +pub enum ListEntrySortOrder { + Ascending, + Descending, +} + +#[derive(Debug, Args)] +pub struct DataStore { + #[clap(subcommand)] + command: DataStoreCommands, +} + +#[inline] +fn u64_ids_to_roblox_ids(user_ids: Option>) -> Option> { + user_ids.map(|ids| { + ids.into_iter() + .map(RobloxUserId) + .collect::>() + }) +} + +impl DataStore { + pub async fn run(self) -> anyhow::Result> { + match self.command { + DataStoreCommands::ListStores { + prefix, + limit, + cursor, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .list_stores(&DataStoreListStores { + cursor, + limit: ReturnLimit(limit), + prefix, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data:#?}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::List { + prefix, + limit, + cursor, + universe_id, + api_key, + datastore_name, + scope, + all_scopes, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .list_entries(&DataStoreListEntries { + name: datastore_name, + scope, + all_scopes, + prefix, + limit: ReturnLimit(limit), + cursor, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data:#?}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::Get { + datastore_name, + scope, + key, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .get_entry_string(&DataStoreGetEntry { + name: datastore_name, + scope, + key, + }) + .await; + match res { + Ok(data) => Ok(Some(data)), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::Set { + datastore_name, + scope, + key, + match_version, + exclusive_create, + data, + user_ids, + attributes, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let ids = u64_ids_to_roblox_ids(user_ids); + let res = datastore + .set_entry(&DataStoreSetEntry { + name: datastore_name, + scope, + key, + match_version, + exclusive_create, + roblox_entry_user_ids: ids, + roblox_entry_attributes: attributes, + data, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data:#?}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::Increment { + datastore_name, + scope, + key, + increment_by, + user_ids, + attributes, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let ids = u64_ids_to_roblox_ids(user_ids); + let res = datastore + .increment_entry(&DataStoreIncrementEntry { + name: datastore_name, + scope, + key, + roblox_entry_user_ids: ids, + roblox_entry_attributes: attributes, + increment_by, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::Delete { + datastore_name, + scope, + key, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .delete_entry(&DataStoreDeleteEntry { + name: datastore_name, + scope, + key, + }) + .await; + match res { + Ok(_) => Ok(None), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::ListVersions { + datastore_name, + scope, + key, + start_time, + end_time, + sort_order, + limit, + cursor, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .list_entry_versions(&DataStoreListEntryVersions { + name: datastore_name, + scope, + key, + start_time, + end_time, + sort_order: format!("{sort_order:?}"), + limit: ReturnLimit(limit), + cursor, + }) + .await; + match res { + Ok(data) => Ok(Some(format!("{data:#?}"))), + Err(err) => Err(err.into()), + } + } + + DataStoreCommands::GetVersion { + datastore_name, + scope, + key, + version_id, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let datastore = rbx_cloud.datastore(); + let res = datastore + .get_entry_version(&DataStoreGetEntryVersion { + name: datastore_name, + scope, + key, + version_id, + }) + .await; + match res { + Ok(data) => Ok(Some(data)), + Err(err) => Err(err.into()), + } + } + } + } +} diff --git a/src/cli/messaging_cli.rs b/src/cli/messaging_cli.rs index 7eb1c9e..aad7ae3 100644 --- a/src/cli/messaging_cli.rs +++ b/src/cli/messaging_cli.rs @@ -1,52 +1,52 @@ -use clap::{Args, Subcommand}; - -use rbxcloud::rbx::{RbxCloud, UniverseId}; - -#[derive(Debug, Subcommand)] -pub enum MessagingCommands { - /// Publish a message - Publish { - /// Message topic - #[clap(short, long, value_parser)] - topic: String, - - /// Message to send - #[clap(short, long, value_parser)] - message: String, - - /// Universe ID of the experience - #[clap(short, long, value_parser)] - universe_id: u64, - - /// Roblox Open Cloud API Key - #[clap(short, long, value_parser)] - api_key: String, - }, -} - -#[derive(Debug, Args)] -pub struct Messaging { - #[clap(subcommand)] - command: MessagingCommands, -} - -impl Messaging { - pub async fn run(self) -> anyhow::Result> { - match self.command { - MessagingCommands::Publish { - topic, - message, - universe_id, - api_key, - } => { - let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); - let messaging = rbx_cloud.messaging(&topic); - let res = messaging.publish(&message).await; - match res { - Ok(()) => Ok(Some(format!("published message to topic {topic}"))), - Err(err) => Err(anyhow::anyhow!(err)), - } - } - } - } -} +use clap::{Args, Subcommand}; + +use rbxcloud::rbx::{RbxCloud, UniverseId}; + +#[derive(Debug, Subcommand)] +pub enum MessagingCommands { + /// Publish a message + Publish { + /// Message topic + #[clap(short, long, value_parser)] + topic: String, + + /// Message to send + #[clap(short, long, value_parser)] + message: String, + + /// Universe ID of the experience + #[clap(short, long, value_parser)] + universe_id: u64, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser)] + api_key: String, + }, +} + +#[derive(Debug, Args)] +pub struct Messaging { + #[clap(subcommand)] + command: MessagingCommands, +} + +impl Messaging { + pub async fn run(self) -> anyhow::Result> { + match self.command { + MessagingCommands::Publish { + topic, + message, + universe_id, + api_key, + } => { + let rbx_cloud = RbxCloud::new(&api_key, UniverseId(universe_id)); + let messaging = rbx_cloud.messaging(&topic); + let res = messaging.publish(&message).await; + match res { + Ok(()) => Ok(Some(format!("published message to topic {topic}"))), + Err(err) => Err(anyhow::anyhow!(err)), + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index df7da7a..6e9dd45 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,22 +1,22 @@ -mod cli; - -use clap::Parser; -use cli::Cli; -use std::process; - -#[tokio::main] -async fn main() { - let cli_args = Cli::parse(); - - match cli_args.run().await { - Ok(str) => { - if let Some(s) = str { - println!("{s}"); - } - } - Err(err) => { - eprintln!("{err:?}"); - process::exit(1); - } - } -} +mod cli; + +use clap::Parser; +use cli::Cli; +use std::process; + +#[tokio::main] +async fn main() { + let cli_args = Cli::parse(); + + match cli_args.run().await { + Ok(str) => { + if let Some(s) = str { + println!("{s}"); + } + } + Err(err) => { + eprintln!("{err:?}"); + process::exit(1); + } + } +} diff --git a/src/rbx/datastore.rs b/src/rbx/datastore.rs index 4b5a9ca..7ffc1d7 100644 --- a/src/rbx/datastore.rs +++ b/src/rbx/datastore.rs @@ -1,509 +1,509 @@ -//! Low-level DataStore API operations. -//! -//! Typically, these operations should be consumed through the `RbxExperience` -//! struct, obtained through the `RbxCloud` struct. - -use std::fmt; - -use md5::{Digest, Md5}; -use reqwest::Response; -use serde::{de::DeserializeOwned, Deserialize}; - -use crate::rbx::{error::Error, ReturnLimit, RobloxUserId, UniverseId}; - -type QueryString = Vec<(&'static str, String)>; - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListDataStoreEntry { - pub name: String, - pub created_time: String, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListDataStoresResponse { - pub datastores: Vec, - pub next_page_cursor: Option, -} - -pub struct ListDataStoresParams { - pub api_key: String, - pub universe_id: UniverseId, - pub prefix: Option, - pub limit: ReturnLimit, - pub cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct DataStoreErrorResponse { - pub error: String, - pub message: String, - pub error_details: Vec, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct DataStoreErrorDetail { - pub error_detail_type: String, - pub datastore_error_code: DataStoreErrorCode, -} - -#[derive(Deserialize, Debug)] -pub enum DataStoreErrorCode { - ContentLengthRequired, - InvalidUniverseId, - InvalidCursor, - InvalidVersionId, - ExistingValueNotNumeric, - IncrementValueTooLarge, - IncrementValueTooSmall, - InvalidDataStoreScope, - InvalidEntryKey, - InvalidDataStoreName, - InvalidStartTime, - InvalidEndTime, - InvalidAttributes, - InvalidUserIds, - ExclusiveCreateAndMatchVersionCannotBeSet, - ContentTooBig, - ChecksumMismatch, - ContentNotJson, - InvalidSortOrder, - Forbidden, - InsufficientScope, - DatastoreNotFound, - EntryNotFound, - VersionNotFound, - TooManyRequests, - Unknown, -} - -impl fmt::Display for DataStoreErrorResponse { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let details = self - .error_details - .iter() - .map(|item| format!("{:?}", item.datastore_error_code)) - .collect::>() - .join(", "); - write!(f, "[{}] - {}", details, self.message) - } -} - -pub struct ListEntriesParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub all_scopes: bool, - pub prefix: Option, - pub limit: ReturnLimit, - pub cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListEntriesResponse { - pub keys: Vec, - pub next_page_cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListEntriesKey { - pub scope: String, - pub key: String, -} - -pub struct GetEntryParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, -} - -pub struct SetEntryParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, - pub match_version: Option, - pub exclusive_create: Option, - pub roblox_entry_user_ids: Option>, - pub roblox_entry_attributes: Option, - pub data: String, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SetEntryResponse { - pub version: String, - pub deleted: bool, - pub content_length: u64, - pub created_time: String, - pub object_created_time: String, -} - -pub struct IncrementEntryParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, - pub roblox_entry_user_ids: Option>, - pub roblox_entry_attributes: Option, - pub increment_by: f64, -} - -pub struct DeleteEntryParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, -} - -pub struct ListEntryVersionsParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, - pub start_time: Option, - pub end_time: Option, - pub sort_order: String, - pub limit: ReturnLimit, - pub cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListEntryVersionsResponse { - pub versions: Vec, - pub next_page_cursor: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ListEntryVersion { - pub version: String, - pub deleted: bool, - pub content_length: u64, - pub created_time: String, - pub object_created_time: String, -} - -pub struct GetEntryVersionParams { - pub api_key: String, - pub universe_id: UniverseId, - pub datastore_name: String, - pub scope: Option, - pub key: String, - pub version_id: String, -} - -async fn handle_res(res: Response) -> Result { - match res.status().is_success() { - true => { - let body = res.json::().await?; - Ok(body) - } - false => { - let err_res = res.json::().await?; - Err(Error::DataStoreError(err_res)) - } - } -} - -async fn handle_res_string(res: Response) -> Result { - match res.status().is_success() { - true => { - let body = res.text().await?; - Ok(body) - } - false => { - let err_res = res.json::().await?; - Err(Error::DataStoreError(err_res)) - } - } -} - -async fn handle_res_ok(res: Response) -> Result<(), Error> { - match res.status().is_success() { - true => Ok(()), - false => { - let err_res = res.json::().await?; - Err(Error::DataStoreError(err_res)) - } - } -} - -fn build_url(endpoint: &str, universe_id: UniverseId) -> String { - if endpoint.is_empty() { - format!("https://apis.roblox.com/datastores/v1/universes/{universe_id}/standard-datastores",) - } else { - format!( - "https://apis.roblox.com/datastores/v1/universes/{universe_id}/standard-datastores{endpoint}", - ) - } -} - -#[inline] -fn get_checksum_base64(data: &String) -> String { - let mut md5_hash = Md5::new(); - md5_hash.update(data.as_bytes()); - base64::encode(md5_hash.finalize()) -} - -/// List all DataStores within an experience. -pub async fn list_datastores( - params: &ListDataStoresParams, -) -> Result { - let client = reqwest::Client::new(); - let url = build_url("", params.universe_id); - let mut query: QueryString = vec![("limit", params.limit.to_string())]; - if let Some(prefix) = ¶ms.prefix { - query.push(("prefix", prefix.clone())); - } - if let Some(cursor) = ¶ms.cursor { - query.push(("cursor", cursor.clone())); - } - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res::(res).await -} - -/// List all entries of a DataStore. -pub async fn list_entries(params: &ListEntriesParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries", params.universe_id); - let mut query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ("limit", params.limit.to_string()), - ("AllScopes", params.all_scopes.to_string()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ]; - if let Some(prefix) = ¶ms.prefix { - query.push(("prefix", prefix.clone())); - } - if let Some(cursor) = ¶ms.cursor { - query.push(("cursor", cursor.clone())); - } - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res::(res).await -} - -async fn get_entry_response(params: &GetEntryParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry", params.universe_id); - let query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.clone()), - ]; - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - Ok(res) -} - -/// Get the value of an entry as a string. -pub async fn get_entry_string(params: &GetEntryParams) -> Result { - let res = get_entry_response(params).await?; - handle_res_string(res).await -} - -/// Get the value of an entry as a JSON-deserialized type `T`. -pub async fn get_entry(params: &GetEntryParams) -> Result { - let res = get_entry_response(params).await?; - handle_res::(res).await -} - -fn build_ids_csv(ids: &Option>) -> String { - ids.as_ref() - .unwrap_or(&vec![]) - .iter() - .map(|id| format!("{id}")) - .collect::>() - .join(",") -} - -/// Set the value of an entry. -pub async fn set_entry(params: &SetEntryParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry", params.universe_id); - let mut query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.clone()), - ]; - if let Some(match_version) = ¶ms.match_version { - query.push(("matchVersion", match_version.clone())); - } - if let Some(exclusive_create) = ¶ms.exclusive_create { - query.push(("exclusiveCreate", exclusive_create.to_string())); - } - let res = client - .post(url) - .header("x-api-key", ¶ms.api_key) - .header("Content-Type", "application/json") - .header( - "roblox-entry-userids", - format!("[{}]", build_ids_csv(¶ms.roblox_entry_user_ids)), - ) - .header( - "roblox-entry-attributes", - params - .roblox_entry_attributes - .as_ref() - .unwrap_or(&String::from("{}")), - ) - .header("content-md5", get_checksum_base64(¶ms.data)) - .body(params.data.clone()) - .query(&query) - .send() - .await?; - handle_res::(res).await -} - -/// Increment the value of an entry. -pub async fn increment_entry(params: &IncrementEntryParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry/increment", params.universe_id); - let query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.clone()), - ("incrementBy", params.increment_by.to_string()), - ]; - let ids = build_ids_csv(¶ms.roblox_entry_user_ids); - let res = client - .post(url) - .header("x-api-key", ¶ms.api_key) - .header("roblox-entry-userids", format!("[{ids}]")) - .header( - "roblox-entry-attributes", - params - .roblox_entry_attributes - .as_ref() - .unwrap_or(&"{}".to_string()), - ) - .query(&query) - .send() - .await?; - match handle_res_string(res).await { - Ok(data) => match data.parse::() { - Ok(num) => Ok(num), - Err(e) => Err(e.into()), - }, - Err(err) => Err(err), - } -} - -/// Delete an entry. -pub async fn delete_entry(params: &DeleteEntryParams) -> Result<(), Error> { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry", params.universe_id); - let query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.clone()), - ]; - let res = client - .delete(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res_ok(res).await -} - -/// List all of the versions of an entry. -pub async fn list_entry_versions( - params: &ListEntryVersionsParams, -) -> Result { - let client = reqwest::Client::new(); - let url = build_url("/datastore/entries/entry/versions", params.universe_id); - let mut query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.to_string()), - ("limit", params.limit.to_string()), - ("sortOrder", params.sort_order.to_string()), - ]; - if let Some(start_time) = ¶ms.start_time { - query.push(("startTime", start_time.clone())); - } - if let Some(end_time) = ¶ms.end_time { - query.push(("endTime", end_time.clone())); - } - if let Some(cursor) = ¶ms.cursor { - query.push(("cursor", cursor.clone())); - } - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res::(res).await -} - -/// Get the value of a specific entry version. -pub async fn get_entry_version(params: &GetEntryVersionParams) -> Result { - let client = reqwest::Client::new(); - let url = build_url( - "/datastore/entries/entry/versions/version", - params.universe_id, - ); - let query: QueryString = vec![ - ("datastoreName", params.datastore_name.clone()), - ( - "scope", - params.scope.clone().unwrap_or_else(|| "global".to_string()), - ), - ("entryKey", params.key.to_string()), - ("versionId", params.version_id.to_string()), - ]; - let res = client - .get(url) - .header("x-api-key", ¶ms.api_key) - .query(&query) - .send() - .await?; - handle_res_string(res).await -} +//! Low-level DataStore API operations. +//! +//! Typically, these operations should be consumed through the `RbxExperience` +//! struct, obtained through the `RbxCloud` struct. + +use std::fmt; + +use md5::{Digest, Md5}; +use reqwest::Response; +use serde::{de::DeserializeOwned, Deserialize}; + +use crate::rbx::{error::Error, ReturnLimit, RobloxUserId, UniverseId}; + +type QueryString = Vec<(&'static str, String)>; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListDataStoreEntry { + pub name: String, + pub created_time: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListDataStoresResponse { + pub datastores: Vec, + pub next_page_cursor: Option, +} + +pub struct ListDataStoresParams { + pub api_key: String, + pub universe_id: UniverseId, + pub prefix: Option, + pub limit: ReturnLimit, + pub cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DataStoreErrorResponse { + pub error: String, + pub message: String, + pub error_details: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct DataStoreErrorDetail { + pub error_detail_type: String, + pub datastore_error_code: DataStoreErrorCode, +} + +#[derive(Deserialize, Debug)] +pub enum DataStoreErrorCode { + ContentLengthRequired, + InvalidUniverseId, + InvalidCursor, + InvalidVersionId, + ExistingValueNotNumeric, + IncrementValueTooLarge, + IncrementValueTooSmall, + InvalidDataStoreScope, + InvalidEntryKey, + InvalidDataStoreName, + InvalidStartTime, + InvalidEndTime, + InvalidAttributes, + InvalidUserIds, + ExclusiveCreateAndMatchVersionCannotBeSet, + ContentTooBig, + ChecksumMismatch, + ContentNotJson, + InvalidSortOrder, + Forbidden, + InsufficientScope, + DatastoreNotFound, + EntryNotFound, + VersionNotFound, + TooManyRequests, + Unknown, +} + +impl fmt::Display for DataStoreErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let details = self + .error_details + .iter() + .map(|item| format!("{:?}", item.datastore_error_code)) + .collect::>() + .join(", "); + write!(f, "[{}] - {}", details, self.message) + } +} + +pub struct ListEntriesParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub all_scopes: bool, + pub prefix: Option, + pub limit: ReturnLimit, + pub cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListEntriesResponse { + pub keys: Vec, + pub next_page_cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListEntriesKey { + pub scope: String, + pub key: String, +} + +pub struct GetEntryParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, +} + +pub struct SetEntryParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, + pub match_version: Option, + pub exclusive_create: Option, + pub roblox_entry_user_ids: Option>, + pub roblox_entry_attributes: Option, + pub data: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SetEntryResponse { + pub version: String, + pub deleted: bool, + pub content_length: u64, + pub created_time: String, + pub object_created_time: String, +} + +pub struct IncrementEntryParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, + pub roblox_entry_user_ids: Option>, + pub roblox_entry_attributes: Option, + pub increment_by: f64, +} + +pub struct DeleteEntryParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, +} + +pub struct ListEntryVersionsParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, + pub start_time: Option, + pub end_time: Option, + pub sort_order: String, + pub limit: ReturnLimit, + pub cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListEntryVersionsResponse { + pub versions: Vec, + pub next_page_cursor: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ListEntryVersion { + pub version: String, + pub deleted: bool, + pub content_length: u64, + pub created_time: String, + pub object_created_time: String, +} + +pub struct GetEntryVersionParams { + pub api_key: String, + pub universe_id: UniverseId, + pub datastore_name: String, + pub scope: Option, + pub key: String, + pub version_id: String, +} + +async fn handle_res(res: Response) -> Result { + match res.status().is_success() { + true => { + let body = res.json::().await?; + Ok(body) + } + false => { + let err_res = res.json::().await?; + Err(Error::DataStoreError(err_res)) + } + } +} + +async fn handle_res_string(res: Response) -> Result { + match res.status().is_success() { + true => { + let body = res.text().await?; + Ok(body) + } + false => { + let err_res = res.json::().await?; + Err(Error::DataStoreError(err_res)) + } + } +} + +async fn handle_res_ok(res: Response) -> Result<(), Error> { + match res.status().is_success() { + true => Ok(()), + false => { + let err_res = res.json::().await?; + Err(Error::DataStoreError(err_res)) + } + } +} + +fn build_url(endpoint: &str, universe_id: UniverseId) -> String { + if endpoint.is_empty() { + format!("https://apis.roblox.com/datastores/v1/universes/{universe_id}/standard-datastores",) + } else { + format!( + "https://apis.roblox.com/datastores/v1/universes/{universe_id}/standard-datastores{endpoint}", + ) + } +} + +#[inline] +fn get_checksum_base64(data: &String) -> String { + let mut md5_hash = Md5::new(); + md5_hash.update(data.as_bytes()); + base64::encode(md5_hash.finalize()) +} + +/// List all DataStores within an experience. +pub async fn list_datastores( + params: &ListDataStoresParams, +) -> Result { + let client = reqwest::Client::new(); + let url = build_url("", params.universe_id); + let mut query: QueryString = vec![("limit", params.limit.to_string())]; + if let Some(prefix) = ¶ms.prefix { + query.push(("prefix", prefix.clone())); + } + if let Some(cursor) = ¶ms.cursor { + query.push(("cursor", cursor.clone())); + } + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res::(res).await +} + +/// List all entries of a DataStore. +pub async fn list_entries(params: &ListEntriesParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries", params.universe_id); + let mut query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ("limit", params.limit.to_string()), + ("AllScopes", params.all_scopes.to_string()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ]; + if let Some(prefix) = ¶ms.prefix { + query.push(("prefix", prefix.clone())); + } + if let Some(cursor) = ¶ms.cursor { + query.push(("cursor", cursor.clone())); + } + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res::(res).await +} + +async fn get_entry_response(params: &GetEntryParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry", params.universe_id); + let query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.clone()), + ]; + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + Ok(res) +} + +/// Get the value of an entry as a string. +pub async fn get_entry_string(params: &GetEntryParams) -> Result { + let res = get_entry_response(params).await?; + handle_res_string(res).await +} + +/// Get the value of an entry as a JSON-deserialized type `T`. +pub async fn get_entry(params: &GetEntryParams) -> Result { + let res = get_entry_response(params).await?; + handle_res::(res).await +} + +fn build_ids_csv(ids: &Option>) -> String { + ids.as_ref() + .unwrap_or(&vec![]) + .iter() + .map(|id| format!("{id}")) + .collect::>() + .join(",") +} + +/// Set the value of an entry. +pub async fn set_entry(params: &SetEntryParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry", params.universe_id); + let mut query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.clone()), + ]; + if let Some(match_version) = ¶ms.match_version { + query.push(("matchVersion", match_version.clone())); + } + if let Some(exclusive_create) = ¶ms.exclusive_create { + query.push(("exclusiveCreate", exclusive_create.to_string())); + } + let res = client + .post(url) + .header("x-api-key", ¶ms.api_key) + .header("Content-Type", "application/json") + .header( + "roblox-entry-userids", + format!("[{}]", build_ids_csv(¶ms.roblox_entry_user_ids)), + ) + .header( + "roblox-entry-attributes", + params + .roblox_entry_attributes + .as_ref() + .unwrap_or(&String::from("{}")), + ) + .header("content-md5", get_checksum_base64(¶ms.data)) + .body(params.data.clone()) + .query(&query) + .send() + .await?; + handle_res::(res).await +} + +/// Increment the value of an entry. +pub async fn increment_entry(params: &IncrementEntryParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry/increment", params.universe_id); + let query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.clone()), + ("incrementBy", params.increment_by.to_string()), + ]; + let ids = build_ids_csv(¶ms.roblox_entry_user_ids); + let res = client + .post(url) + .header("x-api-key", ¶ms.api_key) + .header("roblox-entry-userids", format!("[{ids}]")) + .header( + "roblox-entry-attributes", + params + .roblox_entry_attributes + .as_ref() + .unwrap_or(&"{}".to_string()), + ) + .query(&query) + .send() + .await?; + match handle_res_string(res).await { + Ok(data) => match data.parse::() { + Ok(num) => Ok(num), + Err(e) => Err(e.into()), + }, + Err(err) => Err(err), + } +} + +/// Delete an entry. +pub async fn delete_entry(params: &DeleteEntryParams) -> Result<(), Error> { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry", params.universe_id); + let query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.clone()), + ]; + let res = client + .delete(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res_ok(res).await +} + +/// List all of the versions of an entry. +pub async fn list_entry_versions( + params: &ListEntryVersionsParams, +) -> Result { + let client = reqwest::Client::new(); + let url = build_url("/datastore/entries/entry/versions", params.universe_id); + let mut query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.to_string()), + ("limit", params.limit.to_string()), + ("sortOrder", params.sort_order.to_string()), + ]; + if let Some(start_time) = ¶ms.start_time { + query.push(("startTime", start_time.clone())); + } + if let Some(end_time) = ¶ms.end_time { + query.push(("endTime", end_time.clone())); + } + if let Some(cursor) = ¶ms.cursor { + query.push(("cursor", cursor.clone())); + } + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res::(res).await +} + +/// Get the value of a specific entry version. +pub async fn get_entry_version(params: &GetEntryVersionParams) -> Result { + let client = reqwest::Client::new(); + let url = build_url( + "/datastore/entries/entry/versions/version", + params.universe_id, + ); + let query: QueryString = vec![ + ("datastoreName", params.datastore_name.clone()), + ( + "scope", + params.scope.clone().unwrap_or_else(|| "global".to_string()), + ), + ("entryKey", params.key.to_string()), + ("versionId", params.version_id.to_string()), + ]; + let res = client + .get(url) + .header("x-api-key", ¶ms.api_key) + .query(&query) + .send() + .await?; + handle_res_string(res).await +} diff --git a/src/rbx/mod.rs b/src/rbx/mod.rs index 78384d3..f325b5c 100644 --- a/src/rbx/mod.rs +++ b/src/rbx/mod.rs @@ -1,374 +1,374 @@ -//! Access into Roblox APIs. -//! -//! Most usage should go through the `RbxCloud` struct. -pub mod datastore; -pub mod error; -pub mod experience; -pub mod messaging; - -pub use experience::PublishVersionType; -use serde::de::DeserializeOwned; - -use self::{ - datastore::{ - DeleteEntryParams, GetEntryParams, GetEntryVersionParams, IncrementEntryParams, - ListDataStoresParams, ListDataStoresResponse, ListEntriesParams, ListEntriesResponse, - ListEntryVersionsParams, ListEntryVersionsResponse, SetEntryParams, SetEntryResponse, - }, - error::Error, - experience::{PublishExperienceParams, PublishExperienceResponse}, - messaging::PublishMessageParams, -}; - -/// Represents the UniverseId of a Roblox experience. -#[derive(Debug, Clone, Copy)] -pub struct UniverseId(pub u64); - -/// Represents the PlaceId of a specific place within a Roblox experience. -#[derive(Debug, Clone, Copy)] -pub struct PlaceId(pub u64); - -// Number of items to return. -#[derive(Debug, Clone, Copy)] -pub struct ReturnLimit(pub u64); - -/// Represents a Roblox user's ID. -#[derive(Debug, Clone, Copy)] -pub struct RobloxUserId(pub u64); - -impl std::fmt::Display for UniverseId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::fmt::Display for PlaceId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::fmt::Display for ReturnLimit { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::fmt::Display for RobloxUserId { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -pub struct RbxExperience { - pub universe_id: UniverseId, - pub place_id: PlaceId, - pub api_key: String, -} - -impl RbxExperience { - /// Publish a place. - /// - /// The filename should point to a `*.rbxl` or `*.rbxlx` file. - pub async fn publish( - &self, - filename: &str, - version_type: PublishVersionType, - ) -> Result { - experience::publish_experience(&PublishExperienceParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - place_id: self.place_id, - version_type, - filename: filename.to_string(), - }) - .await - } -} - -pub struct RbxMessaging { - pub api_key: String, - pub universe_id: UniverseId, - pub topic: String, -} - -impl RbxMessaging { - /// Publish a message. - pub async fn publish(&self, message: &str) -> Result<(), Error> { - messaging::publish_message(&PublishMessageParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - topic: self.topic.clone(), - message: message.to_string(), - }) - .await - } -} - -pub struct RbxDataStore { - pub api_key: String, - pub universe_id: UniverseId, -} - -pub struct DataStoreListStores { - pub prefix: Option, - pub limit: ReturnLimit, - pub cursor: Option, -} - -pub struct DataStoreListEntries { - pub name: String, - pub scope: Option, - pub all_scopes: bool, - pub prefix: Option, - pub limit: ReturnLimit, - pub cursor: Option, -} - -pub struct DataStoreGetEntry { - pub name: String, - pub scope: Option, - pub key: String, -} - -pub struct DataStoreSetEntry { - pub name: String, - pub scope: Option, - pub key: String, - pub match_version: Option, - pub exclusive_create: Option, - pub roblox_entry_user_ids: Option>, - pub roblox_entry_attributes: Option, - pub data: String, -} - -pub struct DataStoreIncrementEntry { - pub name: String, - pub scope: Option, - pub key: String, - pub roblox_entry_user_ids: Option>, - pub roblox_entry_attributes: Option, - pub increment_by: f64, -} - -pub struct DataStoreDeleteEntry { - pub name: String, - pub scope: Option, - pub key: String, -} - -pub struct DataStoreListEntryVersions { - pub name: String, - pub scope: Option, - pub key: String, - pub start_time: Option, - pub end_time: Option, - pub sort_order: String, - pub limit: ReturnLimit, - pub cursor: Option, -} - -pub struct DataStoreGetEntryVersion { - pub name: String, - pub scope: Option, - pub key: String, - pub version_id: String, -} - -impl RbxDataStore { - /// List DataStores within the experience. - pub async fn list_stores( - &self, - params: &DataStoreListStores, - ) -> Result { - datastore::list_datastores(&ListDataStoresParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - prefix: params.prefix.clone(), - limit: params.limit, - cursor: params.cursor.clone(), - }) - .await - } - - /// List key entries in a specific DataStore. - pub async fn list_entries( - &self, - params: &DataStoreListEntries, - ) -> Result { - datastore::list_entries(&ListEntriesParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - all_scopes: params.all_scopes, - prefix: params.prefix.clone(), - limit: params.limit, - cursor: params.cursor.clone(), - }) - .await - } - - /// Get the entry string representation of a specific key. - pub async fn get_entry_string(&self, params: &DataStoreGetEntry) -> Result { - datastore::get_entry_string(&GetEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - }) - .await - } - - /// Get the entry of a specific key, deserialized as `T`. - pub async fn get_entry( - &self, - params: &DataStoreGetEntry, - ) -> Result { - datastore::get_entry::(&GetEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - }) - .await - } - - /// Set (or create) the entry value of a specific key. - pub async fn set_entry(&self, params: &DataStoreSetEntry) -> Result { - datastore::set_entry(&SetEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - match_version: params.match_version.clone(), - exclusive_create: params.exclusive_create, - roblox_entry_user_ids: params.roblox_entry_user_ids.clone(), - roblox_entry_attributes: params.roblox_entry_attributes.clone(), - data: params.data.clone(), - }) - .await - } - - /// Increment (or create) the value of a specific key. - /// - /// If the value does not yet exist, it will be treated as `0`, and thus - /// the resulting value will simply be the increment amount. - /// - /// If the value _does_ exist, but it is _not_ a number, then the increment - /// process will fail, and a DataStore error will be returned in the result. - pub async fn increment_entry(&self, params: &DataStoreIncrementEntry) -> Result { - datastore::increment_entry(&IncrementEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - roblox_entry_user_ids: params.roblox_entry_user_ids.clone(), - roblox_entry_attributes: params.roblox_entry_attributes.clone(), - increment_by: params.increment_by, - }) - .await - } - - /// Delete an entry. - pub async fn delete_entry(&self, params: &DataStoreDeleteEntry) -> Result<(), Error> { - datastore::delete_entry(&DeleteEntryParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - }) - .await - } - - /// List all versions of an entry. - /// - /// To get the specific value of a given entry, use `get_entry_version()`. - pub async fn list_entry_versions( - &self, - params: &DataStoreListEntryVersions, - ) -> Result { - datastore::list_entry_versions(&ListEntryVersionsParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - start_time: params.start_time.clone(), - end_time: params.end_time.clone(), - sort_order: params.sort_order.clone(), - limit: params.limit, - cursor: params.cursor.clone(), - }) - .await - } - - /// Get the entry value of a specific version. - pub async fn get_entry_version( - &self, - params: &DataStoreGetEntryVersion, - ) -> Result { - datastore::get_entry_version(&GetEntryVersionParams { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - datastore_name: params.name.clone(), - scope: params.scope.clone(), - key: params.key.clone(), - version_id: params.version_id.clone(), - }) - .await - } -} - -/// Access into the Roblox Open Cloud APIs. -/// -/// ```rust,no_run -/// use rbxcloud::rbx::{RbxCloud, UniverseId}; -/// -/// let cloud = RbxCloud::new("API_KEY", UniverseId(9876543210)); -/// ``` -#[derive(Debug)] -pub struct RbxCloud { - /// Roblox API key. - pub api_key: String, - - /// The UniverseId of a given Roblox experience. - pub universe_id: UniverseId, -} - -impl RbxCloud { - pub fn new(api_key: &str, universe_id: UniverseId) -> RbxCloud { - RbxCloud { - api_key: api_key.to_string(), - universe_id, - } - } - - pub fn experience(&self, place_id: PlaceId) -> RbxExperience { - RbxExperience { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - place_id, - } - } - - pub fn messaging(&self, topic: &str) -> RbxMessaging { - RbxMessaging { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - topic: topic.to_string(), - } - } - - pub fn datastore(&self) -> RbxDataStore { - RbxDataStore { - api_key: self.api_key.clone(), - universe_id: self.universe_id, - } - } -} +//! Access into Roblox APIs. +//! +//! Most usage should go through the `RbxCloud` struct. +pub mod datastore; +pub mod error; +pub mod experience; +pub mod messaging; + +pub use experience::PublishVersionType; +use serde::de::DeserializeOwned; + +use self::{ + datastore::{ + DeleteEntryParams, GetEntryParams, GetEntryVersionParams, IncrementEntryParams, + ListDataStoresParams, ListDataStoresResponse, ListEntriesParams, ListEntriesResponse, + ListEntryVersionsParams, ListEntryVersionsResponse, SetEntryParams, SetEntryResponse, + }, + error::Error, + experience::{PublishExperienceParams, PublishExperienceResponse}, + messaging::PublishMessageParams, +}; + +/// Represents the UniverseId of a Roblox experience. +#[derive(Debug, Clone, Copy)] +pub struct UniverseId(pub u64); + +/// Represents the PlaceId of a specific place within a Roblox experience. +#[derive(Debug, Clone, Copy)] +pub struct PlaceId(pub u64); + +// Number of items to return. +#[derive(Debug, Clone, Copy)] +pub struct ReturnLimit(pub u64); + +/// Represents a Roblox user's ID. +#[derive(Debug, Clone, Copy)] +pub struct RobloxUserId(pub u64); + +impl std::fmt::Display for UniverseId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Display for PlaceId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Display for ReturnLimit { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Display for RobloxUserId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +pub struct RbxExperience { + pub universe_id: UniverseId, + pub place_id: PlaceId, + pub api_key: String, +} + +impl RbxExperience { + /// Publish a place. + /// + /// The filename should point to a `*.rbxl` or `*.rbxlx` file. + pub async fn publish( + &self, + filename: &str, + version_type: PublishVersionType, + ) -> Result { + experience::publish_experience(&PublishExperienceParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + place_id: self.place_id, + version_type, + filename: filename.to_string(), + }) + .await + } +} + +pub struct RbxMessaging { + pub api_key: String, + pub universe_id: UniverseId, + pub topic: String, +} + +impl RbxMessaging { + /// Publish a message. + pub async fn publish(&self, message: &str) -> Result<(), Error> { + messaging::publish_message(&PublishMessageParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + topic: self.topic.clone(), + message: message.to_string(), + }) + .await + } +} + +pub struct RbxDataStore { + pub api_key: String, + pub universe_id: UniverseId, +} + +pub struct DataStoreListStores { + pub prefix: Option, + pub limit: ReturnLimit, + pub cursor: Option, +} + +pub struct DataStoreListEntries { + pub name: String, + pub scope: Option, + pub all_scopes: bool, + pub prefix: Option, + pub limit: ReturnLimit, + pub cursor: Option, +} + +pub struct DataStoreGetEntry { + pub name: String, + pub scope: Option, + pub key: String, +} + +pub struct DataStoreSetEntry { + pub name: String, + pub scope: Option, + pub key: String, + pub match_version: Option, + pub exclusive_create: Option, + pub roblox_entry_user_ids: Option>, + pub roblox_entry_attributes: Option, + pub data: String, +} + +pub struct DataStoreIncrementEntry { + pub name: String, + pub scope: Option, + pub key: String, + pub roblox_entry_user_ids: Option>, + pub roblox_entry_attributes: Option, + pub increment_by: f64, +} + +pub struct DataStoreDeleteEntry { + pub name: String, + pub scope: Option, + pub key: String, +} + +pub struct DataStoreListEntryVersions { + pub name: String, + pub scope: Option, + pub key: String, + pub start_time: Option, + pub end_time: Option, + pub sort_order: String, + pub limit: ReturnLimit, + pub cursor: Option, +} + +pub struct DataStoreGetEntryVersion { + pub name: String, + pub scope: Option, + pub key: String, + pub version_id: String, +} + +impl RbxDataStore { + /// List DataStores within the experience. + pub async fn list_stores( + &self, + params: &DataStoreListStores, + ) -> Result { + datastore::list_datastores(&ListDataStoresParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + prefix: params.prefix.clone(), + limit: params.limit, + cursor: params.cursor.clone(), + }) + .await + } + + /// List key entries in a specific DataStore. + pub async fn list_entries( + &self, + params: &DataStoreListEntries, + ) -> Result { + datastore::list_entries(&ListEntriesParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + all_scopes: params.all_scopes, + prefix: params.prefix.clone(), + limit: params.limit, + cursor: params.cursor.clone(), + }) + .await + } + + /// Get the entry string representation of a specific key. + pub async fn get_entry_string(&self, params: &DataStoreGetEntry) -> Result { + datastore::get_entry_string(&GetEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + }) + .await + } + + /// Get the entry of a specific key, deserialized as `T`. + pub async fn get_entry( + &self, + params: &DataStoreGetEntry, + ) -> Result { + datastore::get_entry::(&GetEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + }) + .await + } + + /// Set (or create) the entry value of a specific key. + pub async fn set_entry(&self, params: &DataStoreSetEntry) -> Result { + datastore::set_entry(&SetEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + match_version: params.match_version.clone(), + exclusive_create: params.exclusive_create, + roblox_entry_user_ids: params.roblox_entry_user_ids.clone(), + roblox_entry_attributes: params.roblox_entry_attributes.clone(), + data: params.data.clone(), + }) + .await + } + + /// Increment (or create) the value of a specific key. + /// + /// If the value does not yet exist, it will be treated as `0`, and thus + /// the resulting value will simply be the increment amount. + /// + /// If the value _does_ exist, but it is _not_ a number, then the increment + /// process will fail, and a DataStore error will be returned in the result. + pub async fn increment_entry(&self, params: &DataStoreIncrementEntry) -> Result { + datastore::increment_entry(&IncrementEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + roblox_entry_user_ids: params.roblox_entry_user_ids.clone(), + roblox_entry_attributes: params.roblox_entry_attributes.clone(), + increment_by: params.increment_by, + }) + .await + } + + /// Delete an entry. + pub async fn delete_entry(&self, params: &DataStoreDeleteEntry) -> Result<(), Error> { + datastore::delete_entry(&DeleteEntryParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + }) + .await + } + + /// List all versions of an entry. + /// + /// To get the specific value of a given entry, use `get_entry_version()`. + pub async fn list_entry_versions( + &self, + params: &DataStoreListEntryVersions, + ) -> Result { + datastore::list_entry_versions(&ListEntryVersionsParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + start_time: params.start_time.clone(), + end_time: params.end_time.clone(), + sort_order: params.sort_order.clone(), + limit: params.limit, + cursor: params.cursor.clone(), + }) + .await + } + + /// Get the entry value of a specific version. + pub async fn get_entry_version( + &self, + params: &DataStoreGetEntryVersion, + ) -> Result { + datastore::get_entry_version(&GetEntryVersionParams { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + datastore_name: params.name.clone(), + scope: params.scope.clone(), + key: params.key.clone(), + version_id: params.version_id.clone(), + }) + .await + } +} + +/// Access into the Roblox Open Cloud APIs. +/// +/// ```rust,no_run +/// use rbxcloud::rbx::{RbxCloud, UniverseId}; +/// +/// let cloud = RbxCloud::new("API_KEY", UniverseId(9876543210)); +/// ``` +#[derive(Debug)] +pub struct RbxCloud { + /// Roblox API key. + pub api_key: String, + + /// The UniverseId of a given Roblox experience. + pub universe_id: UniverseId, +} + +impl RbxCloud { + pub fn new(api_key: &str, universe_id: UniverseId) -> RbxCloud { + RbxCloud { + api_key: api_key.to_string(), + universe_id, + } + } + + pub fn experience(&self, place_id: PlaceId) -> RbxExperience { + RbxExperience { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + place_id, + } + } + + pub fn messaging(&self, topic: &str) -> RbxMessaging { + RbxMessaging { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + topic: topic.to_string(), + } + } + + pub fn datastore(&self) -> RbxDataStore { + RbxDataStore { + api_key: self.api_key.clone(), + universe_id: self.universe_id, + } + } +} From aac69cca18bfe02852de472e662adaa23ea527df Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Wed, 8 Feb 2023 21:28:05 -0500 Subject: [PATCH 3/5] Release tag --- .github/workflows/release.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 60fc453..02fa831 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -2,7 +2,8 @@ name: Release on: push: - tags: "v*" + tags: + - v* jobs: create-release: From a6be47154752a04a980e64434811c05e1fe1c3a3 Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Wed, 8 Feb 2023 21:28:48 -0500 Subject: [PATCH 4/5] EOL --- Cargo.toml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 291eb8c..108d2ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,20 @@ -[package] -name = "rbxcloud" -version = "0.2.2" -description = "CLI and SDK for the Roblox Open Cloud APIs" -authors = ["Stephen Leitnick"] -license = "MIT" -repository = "https://github.com/Sleitnick/rbxcloud" -readme = "README.md" -documentation = "https://sleitnick.github.io/rbxcloud/" -edition = "2021" - -[dependencies] -anyhow = "1.0.58" -base64 = "0.13.0" -clap = { version = "3.2.14", features = ["derive"] } -md-5 = "0.10.1" -reqwest = { version = "0.11.11", features = ["json"] } -serde = { version = "1.0.140", features = ["derive"] } -serde_json = "1.0.82" -tokio = { version = "1.20.1", features = ["full"] } +[package] +name = "rbxcloud" +version = "0.2.2" +description = "CLI and SDK for the Roblox Open Cloud APIs" +authors = ["Stephen Leitnick"] +license = "MIT" +repository = "https://github.com/Sleitnick/rbxcloud" +readme = "README.md" +documentation = "https://sleitnick.github.io/rbxcloud/" +edition = "2021" + +[dependencies] +anyhow = "1.0.58" +base64 = "0.13.0" +clap = { version = "3.2.14", features = ["derive"] } +md-5 = "0.10.1" +reqwest = { version = "0.11.11", features = ["json"] } +serde = { version = "1.0.140", features = ["derive"] } +serde_json = "1.0.82" +tokio = { version = "1.20.1", features = ["full"] } From 83fa9b9a9a067912686a35559d523d424306629a Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Wed, 8 Feb 2023 21:30:45 -0500 Subject: [PATCH 5/5] EOL --- .vscode/settings.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d8388db..1987de2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ -{ - "yaml.schemas": { - "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" - }, - "[rust]": { - "editor.formatOnSave": true - }, - "files.eol": "\n" -} +{ + "yaml.schemas": { + "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + }, + "[rust]": { + "editor.formatOnSave": true + }, + "files.eol": "\n" +}