Skip to content

Commit

Permalink
Merge pull request #31 from Chloe-Woahie/dev-main
Browse files Browse the repository at this point in the history
v0.10.1
  • Loading branch information
fekie authored Apr 6, 2023
2 parents e14975b + fa4b062 commit 9029711
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 136 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = "MIT"
name = "roboat"
readme = "README.md"
repository = "https://github.com/Chloe-Woahie/roboat"
version = "0.10.0"
version = "0.10.1"

[dependencies]
reqwest = { version = "0.11.14", default-features=false, features = ["rustls-tls", "json"] }
Expand Down
12 changes: 4 additions & 8 deletions src/catalog/avatar_catalog/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,9 @@ pub enum CreatorType {
pub enum PriceStatus {
#[default]
Free,
#[serde(rename(deserialize = "Off Sale"))]
#[serde(rename(deserialize = "Offsale"))]
#[serde(alias = "Off Sale")]
Offsale,
#[serde(rename(deserialize = "No Resellers"))]
#[serde(rename(deserialize = "NoResellers"))]
#[serde(alias = "No Resellers")]
NoResellers,
}

Expand Down Expand Up @@ -247,12 +245,10 @@ pub enum Subcategory {
)]
pub struct PremiumPricing {
/// The discount percentage in the form of a value from 0-100.
#[serde(rename(deserialize = "premiumDiscountPercentage"))]
#[serde(rename(deserialize = "premium_discount_percentage"))]
#[serde(alias = "premiumDiscountPercentage")]
pub premium_discount_percentage: u64,
/// The price of the item for premium users.
#[serde(rename(deserialize = "premiumPriceInRobux"))]
#[serde(rename(deserialize = "premium_price_in_robux"))]
#[serde(alias = "premiumPriceInRobux")]
pub premium_price_in_robux: u64,
}

Expand Down
105 changes: 55 additions & 50 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::users::ClientUserInformation;
use crate::RoboatError;
use reqwest::header::HeaderValue;
// We use tokio's version of rwlock so that readers to not starve writers on linux.
use tokio::sync::RwLock;

Expand Down Expand Up @@ -46,16 +48,12 @@ use tokio::sync::RwLock;
/// ```
#[derive(Debug, Default)]
pub struct Client {
/// The cookie used for authentication.
pub(crate) roblosecurity: Option<String>,
/// The full cookie that includes the roblosecurity token.
pub(crate) cookie_string: Option<HeaderValue>,
/// The field holding the value for the X-CSRF-TOKEN header used in and returned by endpoints.
pub(crate) xcsrf: RwLock<String>,
/// The user id of the user. Not modifiable by user.
pub(crate) user_id: RwLock<Option<u64>>,
/// The username of the user. Not modifiable by user.
pub(crate) username: RwLock<Option<String>>,
/// The display name of the user. Not modifiable by user.
pub(crate) display_name: RwLock<Option<String>>,
/// Holds the user id, username, and display name of the user.
pub(crate) user_information: RwLock<Option<ClientUserInformation>>,
/// A Reqwest HTTP client used to send web requests.
pub(crate) reqwest_client: reqwest::Client,
}
Expand All @@ -69,50 +67,57 @@ pub struct ClientBuilder {

impl Client {
/// Returns the user id of the user. If the user id is not cached, it will be fetched from Roblox first.
///
/// The user id should be the only thing used to differentiate between accounts as
/// username and display name can change.
pub async fn user_id(&self) -> Result<u64, RoboatError> {
let guard = self.user_id.read().await;
let user_id_opt = *guard;

// Drop the read lock in case this thread grabs the writer lock later in the function.
drop(guard);
let guard = self.user_information.read().await;
let user_information_opt = &*guard;

match user_id_opt {
Some(user_id) => Ok(user_id),
match user_information_opt {
Some(user_information) => Ok(user_information.user_id),
None => {
// Drop the read lock as the writer lock will be requested.
drop(guard);

let user_info = self.user_information_internal().await?;
Ok(user_info.user_id)
}
}
}

/// Returns the username of the user. If the username is not cached, it will be fetched from Roblox first.
///
/// Username can change (although rarely). For this reason only user id should be used for differentiating accounts.
pub async fn username(&self) -> Result<String, RoboatError> {
let guard = self.username.read().await;
let username_opt = guard.clone();

// Drop the read lock in case this thread grabs the writer lock later in the function.
drop(guard);
let guard = self.user_information.read().await;
let user_information_opt = &*guard;

match username_opt {
Some(username) => Ok(username),
match user_information_opt {
Some(user_information) => Ok(user_information.username.clone()),
None => {
// Drop the read lock as the writer lock will be requested.
drop(guard);

let user_info = self.user_information_internal().await?;
Ok(user_info.username)
}
}
}

/// Returns the display name of the user. If the display name is not cached, it will be fetched from Roblox first.
///
/// Display name can change. For this reason only user id should be used for differentiating accounts.
pub async fn display_name(&self) -> Result<String, RoboatError> {
let guard = self.display_name.read().await;
let display_name_opt = guard.clone();

// Drop the read lock in case this thread grabs the writer lock later in the function.
drop(guard);
let guard = self.user_information.read().await;
let user_information_opt = &*guard;

match display_name_opt {
Some(display_name) => Ok(display_name),
match user_information_opt {
Some(user_information) => Ok(user_information.display_name.clone()),
None => {
// Drop the read lock as the writer lock will be requested.
drop(guard);

let user_info = self.user_information_internal().await?;
Ok(user_info.display_name)
}
Expand All @@ -121,20 +126,8 @@ impl Client {

/// Used in [`Client::user_information_internal`]. This is implemented in the client
/// module as we do not want other modules to have to interact with the rwlock directly.
pub(crate) async fn set_user_id(&self, user_id: u64) {
*self.user_id.write().await = Some(user_id);
}

/// Used in [`Client::user_information_internal`]. This is implemented in the client
/// module as we do not want other modules to have to interact with the rwlock directly.
pub(crate) async fn set_username(&self, username: String) {
*self.username.write().await = Some(username);
}

/// Used in [`Client::user_information_internal`]. This is implemented in the client
/// module as we do not want other modules to have to interact with the rwlock directly.
pub(crate) async fn set_display_name(&self, display_name: String) {
*self.display_name.write().await = Some(display_name);
pub(crate) async fn set_user_information(&self, user_information: ClientUserInformation) {
*self.user_information.write().await = Some(user_information);
}

/// Sets the xcsrf token of the client. Remember to .await this method.
Expand All @@ -147,14 +140,13 @@ impl Client {
self.xcsrf.read().await.clone()
}

/// Creates a string for the cookie header using the roblosecurity.
/// Returns a copy of the cookie string stored in the client.
/// If the roblosecurity has not been set, [`RoboatError::RoblosecurityNotSet`] is returned.
pub(crate) fn create_cookie_string(&self) -> Result<String, RoboatError> {
// We can continue to keep the reader lock as this function will never request a write lock.
let roblosecurity_opt = &self.roblosecurity;
pub(crate) fn cookie_string(&self) -> Result<HeaderValue, RoboatError> {
let cookie_string_opt = &self.cookie_string;

match roblosecurity_opt {
Some(roblosecurity) => Ok(format!(".ROBLOSECURITY={}", roblosecurity)),
match cookie_string_opt {
Some(cookie) => Ok(cookie.clone()),
None => Err(RoboatError::RoblosecurityNotSet),
}
}
Expand Down Expand Up @@ -205,9 +197,22 @@ impl ClientBuilder {
/// ```
pub fn build(self) -> Client {
Client {
roblosecurity: self.roblosecurity,
cookie_string: self
.roblosecurity
.as_ref()
.map(|x| create_cookie_string_header(x)),
reqwest_client: self.reqwest_client.unwrap_or_default(),
..Default::default()
}
}
}

fn create_cookie_string_header(roblosecurity: &str) -> HeaderValue {
// We panic here because I really really really hope that nobody is using invalid characters in their roblosecurity.
let mut header = HeaderValue::from_str(&format!(".ROBLOSECURITY={}", roblosecurity))
.expect("Invalid roblosecurity characters.");

header.set_sensitive(true);

header
}
20 changes: 9 additions & 11 deletions src/economy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl Client {
pub async fn robux(&self) -> Result<u64, RoboatError> {
let user_id = self.user_id().await?;
let formatted_url = format!("{}{}{}", ROBUX_API_PART_1, user_id, ROBUX_API_PART_2);
let cookie = self.create_cookie_string()?;
let cookie = self.cookie_string()?;

let request_result = self
.reqwest_client
Expand All @@ -110,7 +110,6 @@ impl Client {
///
/// # Argument Notes
/// * The cursor is used to get the a certain page of results. If you want the starting page, use `None`.
/// * The default `limit` is [`Limit::Ten`].
///
/// # Return Value Notes
/// * The first value is a vector of reseller listings.
Expand Down Expand Up @@ -144,7 +143,7 @@ impl Client {
) -> Result<(Vec<Listing>, Option<String>), RoboatError> {
let limit = limit.to_u64();
let cursor = cursor.unwrap_or_default();
let cookie = self.create_cookie_string()?;
let cookie = self.cookie_string()?;

let formatted_url = format!(
"{}{}{}?cursor={}&limit={}",
Expand Down Expand Up @@ -191,7 +190,6 @@ impl Client {
///
/// # Argument Notes
/// * The cursor is used to get the a certain page of results. If you want the starting page, use `None`.
/// * The default `limit` is [`Limit::Hundred`].
///
/// # Return Value Notes
/// * The first value is a vector of user sales.
Expand Down Expand Up @@ -243,7 +241,7 @@ impl Client {
USER_SALES_TRANSACTION_TYPE
);

let cookie = self.create_cookie_string()?;
let cookie = self.cookie_string()?;

let request_result = self
.reqwest_client
Expand All @@ -260,13 +258,13 @@ impl Client {
let mut sales = Vec::new();

for raw_sale in raw.data {
let sale_id = raw_sale.sale_id;
let sale_id = raw_sale.id;
let asset_id = raw_sale.details.id;
let robux_received = raw_sale.currency.amount;
let is_pending = raw_sale.is_pending;
let user_id = raw_sale.user.id;
let user_display_name = raw_sale.user.user_display_name;
let asset_name = raw_sale.details.item_name;
let user_id = raw_sale.agent.id;
let user_display_name = raw_sale.agent.name;
let asset_name = raw_sale.details.name;

let sale = UserSale {
sale_id,
Expand Down Expand Up @@ -398,7 +396,7 @@ mod internal {
TOGGLE_SALE_API_PART_1, item_id, TOGGLE_SALE_API_PART_2, uaid
);

let cookie = self.create_cookie_string()?;
let cookie = self.cookie_string()?;

let json = serde_json::json!({
"price": price,
Expand Down Expand Up @@ -430,7 +428,7 @@ mod internal {
TOGGLE_SALE_API_PART_1, item_id, TOGGLE_SALE_API_PART_2, uaid
);

let cookie = self.create_cookie_string()?;
let cookie = self.cookie_string()?;

let json = serde_json::json!({});

Expand Down
27 changes: 11 additions & 16 deletions src/economy/reqwest_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,25 @@ pub(super) struct CurrencyResponse {
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ResellersResponse {
#[serde(rename = "previousPageCursor")]
pub previous_page_cursor: Option<String>,
#[serde(rename = "nextPageCursor")]
pub next_page_cursor: Option<String>,
pub data: Vec<ListingRaw>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ListingRaw {
#[serde(rename = "userAssetId")]
pub user_asset_id: u64,
pub seller: ResellerRaw,
pub price: u64,
#[serde(rename = "serialNumber")]
pub serial_number: Option<u64>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct ResellerRaw {
#[serde(rename = "hasVerifiedBadge")]
pub has_verified_badge: bool,
pub id: u64,
#[serde(rename = "type")]
Expand All @@ -35,22 +33,19 @@ pub(super) struct ResellerRaw {
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct UserSalesResponse {
#[serde(rename = "previousPageCursor")]
pub previous_page_cursor: Option<String>,
#[serde(rename = "nextPageCursor")]
pub next_page_cursor: Option<String>,
pub data: Vec<SaleRaw>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) struct SaleRaw {
#[serde(rename = "id")]
pub sale_id: u64,
#[serde(rename = "isPending")]
pub id: u64,
pub is_pending: bool,
#[serde(rename = "agent")]
pub user: UserRaw,
pub agent: UserRaw,
pub details: DetailsRaw,
pub currency: CurrencyRaw,
}
Expand All @@ -59,15 +54,15 @@ pub(super) struct SaleRaw {
#[derive(Serialize, Deserialize)]
pub(super) struct UserRaw {
pub id: u64,
#[serde(rename = "name")]
pub user_display_name: String,
/// This is the user's display name.
pub name: String,
}

#[derive(Serialize, Deserialize)]
pub(super) struct DetailsRaw {
pub id: u64,
#[serde(rename = "name")]
pub item_name: String,
/// The name of the item.
pub name: String,
}

#[derive(Serialize, Deserialize)]
Expand Down
Loading

0 comments on commit 9029711

Please sign in to comment.