From 77ef47dce4fc8937c9548f4df59f0ddd53418bb9 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Wed, 28 Jul 2021 16:27:38 -0400 Subject: [PATCH] Add RC account info --- Cargo.toml | 6 +- src/robocraft/account.rs | 163 +++++++++++++++++++++++++++++++++++++ src/robocraft/mod.rs | 3 + test.sh | 3 +- tests/robocraft_auth.rs | 32 ++++++++ tests/robocraft_factory.rs | 2 +- 6 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 src/robocraft/account.rs create mode 100644 tests/robocraft_auth.rs diff --git a/Cargo.toml b/Cargo.toml index 1c02225..a028040 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libfj" -version = "0.5.1" +version = "0.5.2" authors = ["NGnius (Graham) "] edition = "2018" description = "An unofficial collection of APIs used in FreeJam games and mods" @@ -35,8 +35,10 @@ genmesh = {version = "^0.6", optional = true} tokio = { version = "1.4.0", features = ["macros"]} [features] +all = ["simple", "robocraft", "cardlife", "techblox", "convert"] +default = ["all"] simple = ["ureq"] -robocraft = ["reqwest"] +robocraft = ["reqwest", "ureq"] cardlife = ["reqwest"] techblox = ["chrono", "highhash", "half", "libfj_parsable_macro_derive"] convert = ["obj", "genmesh"] diff --git a/src/robocraft/account.rs b/src/robocraft/account.rs new file mode 100644 index 0000000..a27b86b --- /dev/null +++ b/src/robocraft/account.rs @@ -0,0 +1,163 @@ +use serde::{Deserialize, Serialize}; +use ureq::{Agent, Error}; +use serde_json::{to_string, from_slice}; + +use crate::robocraft::ITokenProvider; + +/// Token provider for an existing Robocraft account. +/// +/// Steam accounts are not supported. +pub struct AuthenticatedTokenProvider { + /// The account's username + pub username: String, + /// The account's password + pub password: String, + /// Ureq HTTP client + client: Agent, +} + +impl AuthenticatedTokenProvider { + pub fn with_email(email: &str, password: &str) -> Result { + let client = Agent::new(); + let payload = AuthenticationEmailPayload { + email_address: email.to_string(), + password: password.to_string(), + }; + let response = client.post("https://account.freejamgames.com/api/authenticate/email/web") + .set("Content-Type", "application/json") + .send_string(&to_string(&payload).unwrap())?; + let json_res = response.into_json::()?; + Ok(Self { + username: json_res.decode_jwt_data().display_name, + password: password.to_string(), + client, + }) + } + + pub fn with_username(username: &str, password: &str) -> Result { + let new_obj = Self { + username: username.to_string(), + password: password.to_string(), + client: Agent::new(), + }; + new_obj.do_auth()?; + Ok(new_obj) + } + + fn do_auth(&self) -> Result { + let payload = AuthenticationUsernamePayload { + username: self.username.clone(), + password: self.password.clone(), + }; + let response = self.client.post("https://account.freejamgames.com/api/authenticate/displayname/web") + .set("Content-Type", "application/json") + .send_string(&to_string(&payload).unwrap())?; + let json_res = response.into_json::()?; + Ok(json_res) + } + + pub fn get_account_info(&self) -> Result { + let json_res = self.do_auth()?; + Ok(json_res.decode_jwt_data()) + } +} + +impl ITokenProvider for AuthenticatedTokenProvider { + fn token(&self) -> Result { + let json_res = self.do_auth().map_err(|_|())?; + Ok(json_res.token) + } +} + +#[derive(Deserialize, Serialize, Clone)] +pub(crate) struct AuthenticationEmailPayload { + #[serde(rename = "EmailAddress")] + pub email_address: String, + #[serde(rename = "Password")] + pub password: String, +} + +#[derive(Deserialize, Serialize, Clone)] +pub(crate) struct AuthenticationUsernamePayload { + #[serde(rename = "DisplayName")] + pub username: String, + #[serde(rename = "Password")] + pub password: String, +} + +#[derive(Deserialize, Serialize, Clone)] +pub(crate) struct AuthenticationResponseInfo { + #[serde(rename = "Token")] + pub token: String, + #[serde(rename = "RefreshToken")] + pub refresh_token: String, + #[serde(rename = "RefreshTokenExpiry")] + pub refresh_token_expiry: String, +} + +impl AuthenticationResponseInfo { + pub fn decode_jwt_data(&self) -> AccountInfo { + // Refer to https://jwt.io/ + // header is before dot, signature is after dot. + // data is sandwiched in the middle, and it's all we care about + let data = self.token.split(".").collect::>()[1]; + let data_vec = base64::decode(data).unwrap(); + from_slice::(&data_vec).unwrap() + } +} + +/// Robocraft account information. +#[derive(Deserialize, Serialize, Clone)] +pub struct AccountInfo { + /// User's public ID + #[serde(rename = "PublicId")] + pub public_id: String, + /// Account display name + #[serde(rename = "DisplayName")] + pub display_name: String, + /// Account GUID, or display name for older accounts + #[serde(rename = "RobocraftName")] + pub robocraft_name: String, + /// ??? is confirmed? + #[serde(rename = "Confirmed")] + pub confirmed: bool, + /// Freejam support code + #[serde(rename = "SupportCode")] + pub support_code: String, + /// User's email address + #[serde(rename = "EmailAddress")] + pub email_address: String, + /// Email address is verified? + #[serde(rename = "EmailVerified")] + pub email_verified: bool, + /// Account creation date + #[serde(rename = "CreatedDate")] + pub created_date: String, + /// Owned products (?) + #[serde(rename = "Products")] + pub products: Vec, + /// Account flags + #[serde(rename = "Flags")] + pub flags: Vec, + /// Account has a password? + #[serde(rename = "HasPassword")] + pub has_password: bool, + /// Mailing lists that the account is signed up for + #[serde(rename = "MailingLists")] + pub mailing_lists: Vec, + /// Is Steam account? (always false) + #[serde(rename = "HasSteam")] + pub has_steam: bool, + /// iss (?) + #[serde(rename = "iss")] + pub iss: String, + /// sub (?) + #[serde(rename = "sub")] + pub sub: String, + /// Token created at (unix time) (?) + #[serde(rename = "iat")] + pub iat: u64, + /// Token expiry (unix time) (?) + #[serde(rename = "exp")] + pub exp: u64, +} diff --git a/src/robocraft/mod.rs b/src/robocraft/mod.rs index 11b4102..5d0d4b9 100644 --- a/src/robocraft/mod.rs +++ b/src/robocraft/mod.rs @@ -16,5 +16,8 @@ pub use self::cubes::{Cube, Cubes}; mod auth; pub use self::auth::{ITokenProvider, DefaultTokenProvider}; +mod account; +pub use self::account::{AuthenticatedTokenProvider, AccountInfo}; + /// Token defined in a javascript file from Freejam which never expires pub const DEFAULT_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQdWJsaWNJZCI6IjEyMyIsIkRpc3BsYXlOYW1lIjoiVGVzdCIsIlJvYm9jcmFmdE5hbWUiOiJGYWtlQ1JGVXNlciIsIkZsYWdzIjpbXSwiaXNzIjoiRnJlZWphbSIsInN1YiI6IldlYiIsImlhdCI6MTU0NTIyMzczMiwiZXhwIjoyNTQ1MjIzNzkyfQ.ralLmxdMK9rVKPZxGng8luRIdbTflJ4YMJcd25dKlqg"; diff --git a/test.sh b/test.sh index 157f5fa..18c35e3 100755 --- a/test.sh +++ b/test.sh @@ -1,5 +1,6 @@ #!/bin/bash -RUST_BACKTRACE=1 cargo test --all-features -- --nocapture +# RUST_BACKTRACE=1 cargo test --all-features -- --nocapture # RUST_BACKTRACE=1 cargo test --release --all-features -- --nocapture # RUST_BACKTRACE=1 cargo test --features techblox -- --nocapture +RUST_BACKTRACE=1 cargo test --features robocraft -- --nocapture exit $? diff --git a/tests/robocraft_auth.rs b/tests/robocraft_auth.rs new file mode 100644 index 0000000..22a7767 --- /dev/null +++ b/tests/robocraft_auth.rs @@ -0,0 +1,32 @@ +#[cfg(feature = "robocraft")] +use libfj::robocraft; +#[cfg(feature = "robocraft")] +use libfj::robocraft::ITokenProvider; + +#[cfg(feature = "robocraft")] +#[test] +fn robocraft_auth_login() -> Result<(), ()> { + let token_maybe = robocraft::AuthenticatedTokenProvider::with_email("melon.spoik@gmail.com", "P4$$w0rd"); + assert!(token_maybe.is_ok()); + let token_maybe = robocraft::AuthenticatedTokenProvider::with_username("FJAPIC00L", "P4$$w0rd"); + assert!(token_maybe.is_ok()); + let token_p = token_maybe.unwrap(); + let raw_token_maybe = token_p.token(); + assert!(raw_token_maybe.is_ok()); + println!("Token: {}", raw_token_maybe.unwrap()); + Ok(()) +} + +#[cfg(feature = "robocraft")] +#[test] +fn robocraft_account() -> Result<(), ()> { + let token_maybe = robocraft::AuthenticatedTokenProvider::with_username("FJAPIC00L", "P4$$w0rd"); + assert!(token_maybe.is_ok()); + let token_provider = token_maybe.unwrap(); + let account_maybe = token_provider.get_account_info(); + assert!(account_maybe.is_ok()); + let account = account_maybe.unwrap(); + assert_eq!(account.display_name, "FJAPIC00L"); + assert_eq!(account.created_date, "2019-01-18T14:48:09"); + Ok(()) +} diff --git a/tests/robocraft_factory.rs b/tests/robocraft_factory.rs index 9896138..6fe739b 100644 --- a/tests/robocraft_factory.rs +++ b/tests/robocraft_factory.rs @@ -78,7 +78,7 @@ async fn robocraft_factory_custom_query() -> Result<(), ()> { #[tokio::test] async fn robocraft_factory_player_query() -> Result<(), ()> { let result = builder() - .text("MilanZhi".to_string()) // there is a featured robot by this user, so this should never fail + .text("Spacecam".to_string()) // there is a featured robot by this user, so this should never fail .text_search_type(robocraft::FactoryTextSearchType::Player) .items_per_page(10) .send().await;