From 61dbecb937e87ac73be698d784523084f6fda859 Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Thu, 28 Mar 2024 17:41:01 -0400 Subject: [PATCH 1/5] Notifications --- src/cli/mod.rs | 9 ++- src/cli/notification_cli.rs | 73 ++++++++++++++++++++++++ src/rbx/v2/mod.rs | 31 +++++++++- src/rbx/v2/notification.rs | 111 ++++++++++++++++++++++++++++++++++++ 4 files changed, 221 insertions(+), 3 deletions(-) create mode 100644 src/cli/notification_cli.rs create mode 100644 src/rbx/v2/notification.rs diff --git a/src/cli/mod.rs b/src/cli/mod.rs index aa254c5..0105d43 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,6 +3,7 @@ mod datastore_cli; mod experience_cli; mod group_cli; mod messaging_cli; +mod notification_cli; mod ordered_datastore_cli; mod subscription_cli; @@ -10,8 +11,8 @@ use clap::{Parser, Subcommand}; use self::{ assets_cli::Assets, datastore_cli::DataStore, experience_cli::Experience, group_cli::Group, - messaging_cli::Messaging, ordered_datastore_cli::OrderedDataStore, - subscription_cli::Subscription, + messaging_cli::Messaging, notification_cli::Notification, + ordered_datastore_cli::OrderedDataStore, subscription_cli::Subscription, }; #[derive(Debug, Parser)] @@ -43,6 +44,9 @@ pub enum Command { /// Access the Roblox Subscription API Subscription(Subscription), + + /// Access the Roblox Notification API + Notification(Notification), } impl Cli { @@ -55,6 +59,7 @@ impl Cli { Command::OrderedDatastore(command) => command.run().await, Command::Group(command) => command.run().await, Command::Subscription(command) => command.run().await, + Command::Notification(command) => command.run().await, } } } diff --git a/src/cli/notification_cli.rs b/src/cli/notification_cli.rs new file mode 100644 index 0000000..d06516c --- /dev/null +++ b/src/cli/notification_cli.rs @@ -0,0 +1,73 @@ +use clap::{Args, Subcommand}; +use rbxcloud::rbx::{ + types::{RobloxUserId, UniverseId}, + v2::Client, +}; + +#[derive(Debug, Subcommand)] +pub enum NotificationCommands { + Send { + /// Universe ID + #[clap(short, long, value_parser)] + universe_id: u64, + + /// User ID + #[clap(short, long, value_parser)] + user_id: u64, + + /// Payload + #[clap(short = 'P', long, value_parser)] + payload: String, + + /// Pretty-print the JSON response + #[clap(short, long, value_parser, default_value_t = false)] + pretty: bool, + + /// Roblox Open Cloud API Key + #[clap(short, long, value_parser, env = "RBXCLOUD_API_KEY")] + api_key: String, + }, +} + +#[derive(Debug, Args)] +pub struct Notification { + #[clap(subcommand)] + command: NotificationCommands, +} + +impl Notification { + pub async fn run(self) -> anyhow::Result> { + match self.command { + NotificationCommands::Send { + universe_id, + user_id, + payload, + pretty, + api_key, + } => { + let client = Client::new(&api_key); + let notification_client = client.notification(UniverseId(universe_id)); + + let notification = serde_json::from_str::< + rbxcloud::rbx::v2::notification::Notification, + >(&payload)?; + + let res = notification_client + .send(RobloxUserId(user_id), notification) + .await; + + match res { + Ok(subscription_info) => { + let r = if pretty { + serde_json::to_string_pretty(&subscription_info)? + } else { + serde_json::to_string(&subscription_info)? + }; + Ok(Some(r)) + } + Err(err) => Err(anyhow::anyhow!(err)), + } + } + } + } +} diff --git a/src/rbx/v2/mod.rs b/src/rbx/v2/mod.rs index b6cfb4d..8112359 100644 --- a/src/rbx/v2/mod.rs +++ b/src/rbx/v2/mod.rs @@ -8,15 +8,17 @@ use self::{ ListGroupMembershipsParams, ListGroupMembershipsResponse, ListGroupRolesParams, ListGroupRolesResponse, }, + notification::{Notification, NotificationParams, NotificationResponse}, subscription::{GetSubscriptionParams, GetSubscriptionResponse, SubscriptionView}, }; pub mod group; pub(crate) mod http_err; +pub mod notification; pub mod subscription; use crate::rbx::error::Error; -use super::types::{GroupId, UniverseId}; +use super::types::{GroupId, RobloxUserId, UniverseId}; /// Access into the Roblox Open Cloud APIs. /// @@ -40,6 +42,11 @@ pub struct SubscriptionClient { pub api_key: String, } +pub struct NotificationClient { + pub api_key: String, + pub universe_id: UniverseId, +} + impl GroupClient { pub async fn get_info(&self) -> Result { group::get_group(&GetGroupParams { @@ -107,6 +114,21 @@ impl SubscriptionClient { } } +impl NotificationClient { + pub async fn send( + &self, + user_id: RobloxUserId, + notification: Notification, + ) -> Result { + notification::send_notification(&NotificationParams { + api_key: self.api_key.clone(), + user_id, + notification, + }) + .await + } +} + impl Client { pub fn new(api_key: &str) -> Client { Client { @@ -126,4 +148,11 @@ impl Client { api_key: self.api_key.clone(), } } + + pub fn notification(&self, universe_id: UniverseId) -> NotificationClient { + NotificationClient { + api_key: self.api_key.clone(), + universe_id, + } + } } diff --git a/src/rbx/v2/notification.rs b/src/rbx/v2/notification.rs new file mode 100644 index 0000000..1a566df --- /dev/null +++ b/src/rbx/v2/notification.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::rbx::{error::Error, types::RobloxUserId}; + +use super::http_err::handle_http_err; + +#[derive(Deserialize, Serialize, Debug)] +pub enum NotificationType { + TypeUnspecified, + Moment, +} + +impl std::fmt::Display for NotificationType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:?}", + match self { + Self::TypeUnspecified => "TYPE_UNSPECIFIED", + Self::Moment => "MOMENT", + } + ) + } +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct AnalyticsData { + category: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct JoinExperience { + launch_data: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Parameter { + pub string_value: Option, + pub int64_value: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct NotificationSource { + pub universe: String, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct NotificationPayload { + pub message_id: String, + #[serde(rename(serialize = "type"))] + pub notification_type: NotificationType, + pub parameters: Option>, + pub join_experience: Option, + pub analytics_data: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Notification { + pub source: NotificationSource, + pub payload: NotificationPayload, +} + +#[derive(Debug)] +pub struct NotificationParams { + pub api_key: String, + pub user_id: RobloxUserId, + pub notification: Notification, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct NotificationResponse { + pub path: String, + pub id: String, +} + +pub async fn send_notification(params: &NotificationParams) -> Result { + let client = reqwest::Client::new(); + + let url = format!( + "https://apis.roblox.com/cloud/v2/users/{user}/notifications", + user = ¶ms.user_id, + ); + + let body = serde_json::to_string(¶ms.notification)?; + + let res = client + .post(url) + .header("x-api-key", ¶ms.api_key) + .body(body) + .send() + .await?; + + let status = res.status(); + + if !status.is_success() { + let code = status.as_u16(); + return handle_http_err(code); + } + + let body = res.json::().await?; + Ok(body) +} From c22055642eea47a8cc02021051a1031854ef282e Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Thu, 28 Mar 2024 17:57:41 -0400 Subject: [PATCH 2/5] Notification docs --- docs/cli/cli-notification.md | 21 +++++++++++++++++++++ docs/cli/cli-subscription.md | 2 +- mkdocs.yml | 1 + src/cli/notification_cli.rs | 3 ++- 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 docs/cli/cli-notification.md diff --git a/docs/cli/cli-notification.md b/docs/cli/cli-notification.md new file mode 100644 index 0000000..bdc8d10 --- /dev/null +++ b/docs/cli/cli-notification.md @@ -0,0 +1,21 @@ +# Notification API + +## Send Notification +Send a notification to a user. +``` +Usage: rbxcloud notification send [OPTIONS] --universe-id --user-id --payload --api-key + +Options: + -u, --universe-id Universe ID + -U, --user-id User ID + -P, --payload Payload + -p, --pretty Pretty-print the JSON response + -a, --api-key Roblox Open Cloud API Key [env: RBXCLOUD_API_KEY=] + -h, --help Print help +``` + +### Example +For an example payload, see the [Roblox documentation](https://create.roblox.com/docs/cloud/reference/UserNotification#Create-User-Notification). +``` +$ rbxcloud notification send -p -u 12345 -U 6789 -P PAYLOAD_JSON -a MY_KEY +``` diff --git a/docs/cli/cli-subscription.md b/docs/cli/cli-subscription.md index 16084bb..8e22df1 100644 --- a/docs/cli/cli-subscription.md +++ b/docs/cli/cli-subscription.md @@ -2,7 +2,7 @@ ## Getting Subscription Info ``` -Usage: rbxcloud.exe subscription get [OPTIONS] --universe-id --product --subscription --api-key +Usage: rbxcloud subscription get [OPTIONS] --universe-id --product --subscription --api-key Options: -u, --universe-id Universe ID diff --git a/mkdocs.yml b/mkdocs.yml index f8c8122..ee91bca 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,6 +15,7 @@ nav: - OrderedDataStore: cli/cli-ordered-datastore.md - Group: cli/cli-group.md - Subscription: cli/cli-subscription.md + - Notification: cli/cli-notification.md - Rust Lib: - Install: lib/lib-install.md diff --git a/src/cli/notification_cli.rs b/src/cli/notification_cli.rs index d06516c..f482880 100644 --- a/src/cli/notification_cli.rs +++ b/src/cli/notification_cli.rs @@ -6,13 +6,14 @@ use rbxcloud::rbx::{ #[derive(Debug, Subcommand)] pub enum NotificationCommands { + /// Send a notification to a user Send { /// Universe ID #[clap(short, long, value_parser)] universe_id: u64, /// User ID - #[clap(short, long, value_parser)] + #[clap(short = 'U', long, value_parser)] user_id: u64, /// Payload From 9c875bf01f182a95202d09034c76ceb658436910 Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Thu, 28 Mar 2024 18:11:17 -0400 Subject: [PATCH 3/5] Notification example --- examples/send-notification.rs | 66 +++++++++++++++++++++++++++++++++++ src/rbx/v2/notification.rs | 10 ++---- 2 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 examples/send-notification.rs diff --git a/examples/send-notification.rs b/examples/send-notification.rs new file mode 100644 index 0000000..fb60d5d --- /dev/null +++ b/examples/send-notification.rs @@ -0,0 +1,66 @@ +use std::collections::HashMap; + +use rbxcloud::rbx::{ + error::Error, + types::{RobloxUserId, UniverseId}, + v2::{ + notification::{ + JoinExperience, Notification, NotificationPayload, NotificationResponse, + NotificationSource, NotificationType, Parameter, + }, + Client, + }, +}; + +async fn send_notification() -> Result { + // Inputs: + let api_key = "MY_API_KEY"; + let message_id = "MY_MESSAGE_ID"; + let universe_id = 9876543210; + let user_id = 308165; + + let client = Client::new(api_key); + let notification_client = client.notification(UniverseId(universe_id)); + + let notification = Notification { + source: NotificationSource { + universe: format!("universe/{}", universe_id), + }, + payload: NotificationPayload { + message_id: message_id.to_string(), + notification_type: NotificationType::TypeUnspecified, + join_experience: Some(JoinExperience { + launch_data: "Some launch data here".to_string(), + }), + analytics_data: Some(HashMap::from([( + "category".to_string(), + "Bronze egg hatched".to_string(), + )])), + parameters: Some(HashMap::from([( + "key".to_string(), + Parameter { + string_value: Some("bronze egg".to_string()), + int64_value: None, + }, + )])), + }, + }; + + notification_client + .send(RobloxUserId(user_id), notification) + .await +} + +#[tokio::main] +async fn main() { + let send_result = send_notification().await; + + match send_result { + Ok(result) => { + println!("Notification sent: {:?}", result); + } + Err(e) => { + eprintln!("{e:?}"); + } + } +} diff --git a/src/rbx/v2/notification.rs b/src/rbx/v2/notification.rs index 1a566df..7d19ce0 100644 --- a/src/rbx/v2/notification.rs +++ b/src/rbx/v2/notification.rs @@ -25,16 +25,10 @@ impl std::fmt::Display for NotificationType { } } -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct AnalyticsData { - category: String, -} - #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct JoinExperience { - launch_data: String, + pub launch_data: String, } #[derive(Deserialize, Serialize, Debug)] @@ -58,7 +52,7 @@ pub struct NotificationPayload { pub notification_type: NotificationType, pub parameters: Option>, pub join_experience: Option, - pub analytics_data: Option, + pub analytics_data: Option>, } #[derive(Deserialize, Serialize, Debug)] From fff0812c71e2d658339753eb09f2750a0f646161 Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Thu, 28 Mar 2024 18:11:53 -0400 Subject: [PATCH 4/5] Bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 4 ++-- docs/cli/cli-install.md | 4 ++-- docs/lib/lib-install.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f5136d..0ca3807 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -660,7 +660,7 @@ dependencies = [ [[package]] name = "rbxcloud" -version = "0.9.0" +version = "0.10.0" dependencies = [ "anyhow", "base64 0.22.0", diff --git a/Cargo.toml b/Cargo.toml index 631e54e..fdbf967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rbxcloud" -version = "0.9.0" +version = "0.10.0" description = "CLI and SDK for the Roblox Open Cloud APIs" authors = ["Stephen Leitnick"] license = "MIT" diff --git a/README.md b/README.md index 14d2ae3..e43101f 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The goal of this project is to support all API endpoints that Roblox provides. ### Aftman Run the `aftman add` command within your project directory. This will add `rbxcloud` to the project's `aftman.toml` file (or create one if it doesn't yet exist). ```sh -$ aftman add Sleitnick/rbxcloud@0.9.0 +$ aftman add Sleitnick/rbxcloud@0.10.0 ``` ### From Release @@ -59,7 +59,7 @@ The library built for the CLI tool is available to use directly in Rust projects To use `rbxcloud` in a Rust project, simply add `rbxcloud` to the `Cargo.toml` dependency list. ```toml [dependencies] -rbxcloud = "0.9.0" +rbxcloud = "0.10.0" ``` Alternatively, use `cargo add`. diff --git a/docs/cli/cli-install.md b/docs/cli/cli-install.md index d3e2bc8..74dd0e8 100644 --- a/docs/cli/cli-install.md +++ b/docs/cli/cli-install.md @@ -7,7 +7,7 @@ There are a few different ways to install the `rbxcloud` CLI. ### [Aftman](https://github.com/LPGhatguy/aftman) (Preferred) Run the `aftman add` command within your project directory. This will add `rbxcloud` to the project's `aftman.toml` file (or create one if it doesn't yet exist). ```sh -$ aftman add Sleitnick/rbxcloud@0.9.0 +$ aftman add Sleitnick/rbxcloud@0.10.0 ``` Next, run `aftman install` to install `rbxcloud`. @@ -17,7 +17,7 @@ Add `rbxcloud` under the `[tools]` section of your `foreman.toml` file. ```toml # foreman.toml [tools] -rbxcloud = { github = "Sleitnick/rbxcloud", version = "0.9.0" } +rbxcloud = { github = "Sleitnick/rbxcloud", version = "0.10.0" } ``` Next, run `foreman install` to install `rbxcloud`. diff --git a/docs/lib/lib-install.md b/docs/lib/lib-install.md index 33d4a36..2d43994 100644 --- a/docs/lib/lib-install.md +++ b/docs/lib/lib-install.md @@ -5,7 +5,7 @@ To use `rbxcloud` in a Rust project, simply add `rbxcloud` to the `Cargo.toml` dependency list. ```toml [dependencies] -rbxcloud = "0.9.0" +rbxcloud = "0.10.0" ``` Alternatively, use `cargo add`. From 3859725258cf5fdd097669d2014a095bb3a94ca6 Mon Sep 17 00:00:00 2001 From: Stephen Leitnick Date: Thu, 28 Mar 2024 18:12:31 -0400 Subject: [PATCH 5/5] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e43101f..ebd21e6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Possible use-cases: | :x: | Instances | | :white_check_mark: | Subscriptions | | :x: | Inventory | -| :x: | User Notifications | +| :white_check_mark: | User Notifications | - :white_check_mark: = Supported - :x: = Not yet supported