roblox-api 0.1.2

Roblox web api bindings
Documentation
use std::time::SystemTime;

use base64::{Engine, prelude::BASE64_STANDARD};
use p256::{
    ecdsa::{Signature, SigningKey, signature::Signer},
    elliptic_curve::rand_core::OsRng,
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};

use crate::{DateTime, Error, api::hba_service, client::Client};

pub const URL: &str = "https://auth.roblox.com/v1";

#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
pub enum LoginType {
    Email,
    #[default]
    Username,
    PhoneNumber,
    EmailOtpSessionToken,
    AuthToken,
    Passkey,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub enum MediaType {
    Email,
    SMS,
    Authenticator,
    RecoveryCode,
    SecurityKey,
    CrossDevice,
    Password,
    Passkey,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct RecommendedUsernamesFromDisplayName {
    #[serde(rename = "didGenerateNewUsername")]
    pub new_name_generated: bool,
    #[serde(rename = "suggestedUsernames")]
    pub suggested_names: Vec<String>,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct User {
    pub id: u64,
    pub name: String,
    #[serde(rename = "displayName")]
    pub display_name: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct TwoStepVerificationInfo {
    #[serde(rename = "mediaType")]
    pub media_type: MediaType,
    pub ticket: String,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct LoginResponse {
    pub user: User,
    #[serde(rename = "twoStepVerificationData")]
    pub two_step_verification_info: TwoStepVerificationInfo,
    #[serde(rename = "identityVerificationLoginTicket")]
    pub verification_ticket: String,
    #[serde(rename = "isBanned")]
    pub is_banned: bool,
    #[serde(rename = "shouldUpdateEmail")]
    pub should_update_email: bool,
    #[serde(rename = "recoveryEmail")]
    pub recovery_email: String,
    #[serde(rename = "accountBlob")]
    pub account_blob: String,
}

#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
struct AuthenticationIntent {
    #[serde(rename = "clientPublicKey")]
    public_key: String,
    #[serde(rename = "clientEpochTimestamp")]
    epoch_timestamp: u64,
    #[serde(rename = "saiSignature")]
    signature: String,
    #[serde(rename = "serverNonce")]
    nonce: String,
}

async fn authentication_intent(client: &mut Client) -> Result<AuthenticationIntent, Error> {
    let nonce = hba_service::v1::server_nonce(client).await?;

    let unix = SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)
        .unwrap()
        .as_secs();

    let key = SigningKey::random(&mut OsRng);
    let public_key = BASE64_STANDARD.encode(key.verifying_key().to_sec1_bytes());

    let binding = format!("{}:{}:{}", public_key, unix, nonce);
    let hash = Sha256::digest(binding);

    let signature: Signature = key.sign(&hash[..]);
    let signature = String::from_utf8_lossy(&signature.to_bytes()).to_string();

    Ok(AuthenticationIntent {
        public_key,
        epoch_timestamp: unix,
        signature,
        nonce,
    })
}

pub async fn login(
    client: &mut Client,
    login: &str,
    key: &str,
    login_type: LoginType,
) -> Result<LoginResponse, Error> {
    #[derive(Serialize)]
    struct Request<'a> {
        #[serde(rename = "ctype")]
        login_type: LoginType,
        #[serde(rename = "cvalue")]
        login: &'a str,
        #[serde(rename = "password")]
        key: &'a str,

        #[serde(rename = "secureAuthenticationIntent")]
        authentication_intent: AuthenticationIntent,
    }

    let authentication_intent = authentication_intent(client).await?;
    let result = client
        .requestor
        .client
        .post(format!("{URL}/login"))
        .headers(client.requestor.default_headers.clone())
        .json(&Request {
            login_type,
            login,
            key,
            authentication_intent,
        })
        .send()
        .await;

    let response = client.validate_response(result).await?;
    client.requestor.parse_json::<LoginResponse>(response).await
}

pub async fn recommended_usernames_from_display_name(
    client: &mut Client,
    display_name: &str,
    birthday: DateTime,
) -> Result<RecommendedUsernamesFromDisplayName, Error> {
    #[derive(Serialize)]
    struct Request<'a> {
        #[serde(rename = "displayName")]
        display_name: &'a str,
        birthday: &'a str,
    }

    let result = client
        .requestor
        .client
        .post(format!(
            "{URL}/validators/recommendedUsernameFromDisplayName"
        ))
        .headers(client.requestor.default_headers.clone())
        .json(&Request {
            display_name,
            birthday: birthday.to_string().as_str(),
        })
        .send()
        .await;

    let response = client.validate_response(result).await?;
    client
        .requestor
        .parse_json::<RecommendedUsernamesFromDisplayName>(response)
        .await
}