use crate::{Client, Limit, RoboatError};
use reqwest::header;
use serde::{Deserialize, Serialize};
mod request_types;
const ROBUX_API_PART_1: &str = "https://economy.roblox.com/v1/users/";
const ROBUX_API_PART_2: &str = "/currency";
const RESELLERS_API_PART_1: &str = "https://economy.roblox.com/v1/assets/";
const RESELLERS_API_PART_2: &str = "/resellers";
const TRANSACTIONS_API_PART_1: &str = "https://economy.roblox.com/v2/users/";
const TRANSACTIONS_API_PART_2: &str = "/transactions";
const TOGGLE_SALE_API_PART_1: &str = "https://economy.roblox.com/v1/assets/";
const TOGGLE_SALE_API_PART_2: &str = "/resellable-copies/";
const USER_SALES_TRANSACTION_TYPE: &str = "Sale";
#[non_exhaustive]
#[derive(
thiserror::Error,
Debug,
Default,
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Serialize,
Deserialize,
)]
pub enum PurchaseTradableLimitedError {
#[default]
#[error("Pending Transaction.")]
PendingTransaction,
#[error("Item Not For Sale.")]
ItemNotForSale,
#[error("Not Enough Robux.")]
NotEnoughRobux,
#[error("Price Changed")]
PriceChanged,
#[error("Cannot Buy Own Item")]
CannotBuyOwnItem,
#[error("Unknown Roblox Error Message: {0}")]
UnknownRobloxErrorMsg(String),
}
#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
pub struct Reseller {
pub user_id: u64,
pub name: String,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
pub struct Listing {
pub uaid: u64,
pub price: u64,
pub reseller: Reseller,
pub serial_number: Option<u64>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
pub struct UserSale {
pub sale_id: u64,
pub is_pending: bool,
pub user_id: u64,
pub user_display_name: String,
pub robux_received: u64,
pub asset_id: u64,
pub asset_name: String,
}
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.cookie_string()?;
let request_result = self
.reqwest_client
.get(formatted_url)
.header(header::COOKIE, cookie)
.send()
.await;
let response = Self::validate_request_result(request_result).await?;
let raw = Self::parse_to_raw::<request_types::CurrencyResponse>(response).await?;
let robux = raw.robux;
Ok(robux)
}
pub async fn resellers(
&self,
item_id: u64,
limit: Limit,
cursor: Option<String>,
) -> Result<(Vec<Listing>, Option<String>), RoboatError> {
let limit = limit.to_u64();
let cursor = cursor.unwrap_or_default();
let cookie = self.cookie_string()?;
let formatted_url = format!(
"{}{}{}?cursor={}&limit={}",
RESELLERS_API_PART_1, item_id, RESELLERS_API_PART_2, cursor, limit
);
let request_result = self
.reqwest_client
.get(formatted_url)
.header(header::COOKIE, cookie)
.send()
.await;
let response = Self::validate_request_result(request_result).await?;
let raw = Self::parse_to_raw::<request_types::ResellersResponse>(response).await?;
let next_page_cursor = raw.next_page_cursor;
let mut listings = Vec::new();
for listing in raw.data {
let reseller = Reseller {
user_id: listing.seller.id,
name: listing.seller.name,
};
let listing = Listing {
uaid: listing.user_asset_id,
price: listing.price,
reseller,
serial_number: listing.serial_number,
};
listings.push(listing);
}
Ok((listings, next_page_cursor))
}
pub async fn user_sales(
&self,
limit: Limit,
cursor: Option<String>,
) -> Result<(Vec<UserSale>, Option<String>), RoboatError> {
let limit = limit.to_u64();
let cursor = cursor.unwrap_or_default();
let user_id = self.user_id().await?;
let formatted_url = format!(
"{}{}{}?cursor={}&limit={}&transactionType={}",
TRANSACTIONS_API_PART_1,
user_id,
TRANSACTIONS_API_PART_2,
cursor,
limit,
USER_SALES_TRANSACTION_TYPE
);
let cookie = self.cookie_string()?;
let request_result = self
.reqwest_client
.get(formatted_url)
.header(header::COOKIE, cookie)
.send()
.await;
let response = Self::validate_request_result(request_result).await?;
let raw = Self::parse_to_raw::<request_types::UserSalesResponse>(response).await?;
let next_page_cursor = raw.next_page_cursor;
let mut sales = Vec::new();
for raw_sale in raw.data {
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.agent.id;
let user_display_name = raw_sale.agent.name;
let asset_name = raw_sale.details.name;
let sale = UserSale {
sale_id,
asset_id,
robux_received,
is_pending,
user_id,
user_display_name,
asset_name,
};
sales.push(sale);
}
Ok((sales, next_page_cursor))
}
pub async fn put_limited_on_sale(
&self,
item_id: u64,
uaid: u64,
price: u64,
) -> Result<(), RoboatError> {
match self
.put_limited_on_sale_internal(item_id, uaid, price)
.await
{
Ok(x) => Ok(x),
Err(e) => match e {
RoboatError::InvalidXcsrf(new_xcsrf) => {
self.set_xcsrf(new_xcsrf).await;
self.put_limited_on_sale_internal(item_id, uaid, price)
.await
}
_ => Err(e),
},
}
}
pub async fn take_limited_off_sale(&self, item_id: u64, uaid: u64) -> Result<(), RoboatError> {
match self.take_limited_off_sale_internal(item_id, uaid).await {
Ok(x) => Ok(x),
Err(e) => match e {
RoboatError::InvalidXcsrf(new_xcsrf) => {
self.set_xcsrf(new_xcsrf).await;
self.take_limited_off_sale_internal(item_id, uaid).await
}
_ => Err(e),
},
}
}
pub async fn purchase_tradable_limited(
&self,
product_id: u64,
seller_id: u64,
uaid: u64,
price: u64,
) -> Result<(), RoboatError> {
match self
.purchase_limited_internal(product_id, price, seller_id, uaid)
.await
{
Ok(x) => Ok(x),
Err(e) => match e {
RoboatError::InvalidXcsrf(new_xcsrf) => {
self.set_xcsrf(new_xcsrf).await;
self.purchase_limited_internal(product_id, price, seller_id, uaid)
.await
}
_ => Err(e),
},
}
}
}
mod internal {
use super::{
request_types, PurchaseTradableLimitedError, TOGGLE_SALE_API_PART_1, TOGGLE_SALE_API_PART_2,
};
use crate::{Client, RoboatError, CONTENT_TYPE, USER_AGENT, XCSRF_HEADER};
use reqwest::header;
impl Client {
pub(super) async fn put_limited_on_sale_internal(
&self,
item_id: u64,
uaid: u64,
price: u64,
) -> Result<(), RoboatError> {
let formatted_url = format!(
"{}{}{}{}",
TOGGLE_SALE_API_PART_1, item_id, TOGGLE_SALE_API_PART_2, uaid
);
let cookie = self.cookie_string()?;
let json = serde_json::json!({
"price": price,
});
let request_result = self
.reqwest_client
.patch(formatted_url)
.header(header::COOKIE, cookie)
.header(XCSRF_HEADER, self.xcsrf().await)
.json(&json)
.send()
.await;
let _ = Self::validate_request_result(request_result).await?;
Ok(())
}
pub(super) async fn take_limited_off_sale_internal(
&self,
item_id: u64,
uaid: u64,
) -> Result<(), RoboatError> {
let formatted_url = format!(
"{}{}{}{}",
TOGGLE_SALE_API_PART_1, item_id, TOGGLE_SALE_API_PART_2, uaid
);
let cookie = self.cookie_string()?;
let json = serde_json::json!({});
let request_result = self
.reqwest_client
.patch(formatted_url)
.header(header::COOKIE, cookie)
.header(XCSRF_HEADER, self.xcsrf().await)
.json(&json)
.send()
.await;
let _ = Self::validate_request_result(request_result).await?;
Ok(())
}
pub(super) async fn purchase_limited_internal(
&self,
product_id: u64,
price: u64,
seller_id: u64,
uaid: u64,
) -> Result<(), RoboatError> {
let formatted_url = format!(
"https://economy.roblox.com/v1/purchases/products/{}",
product_id
);
let cookie = self.cookie_string()?;
let json = serde_json::json!({
"expectedCurrency": 1,
"expectedPrice": price,
"expectedSellerId": seller_id,
"userAssetId": uaid,
});
let request_result = self
.reqwest_client
.post(formatted_url)
.header(header::COOKIE, cookie)
.header(XCSRF_HEADER, self.xcsrf().await)
.header(header::USER_AGENT, USER_AGENT)
.header(header::CONTENT_TYPE, CONTENT_TYPE)
.json(&json)
.send()
.await;
let response = Self::validate_request_result(request_result).await?;
let raw =
Self::parse_to_raw::<request_types::PurchaseLimitedResponse>(response).await?;
match raw.purchased {
true => Ok(()),
false => match raw.error_msg.as_str() {
"You have a pending transaction. Please wait 1 minute and try again." => {
Err(RoboatError::PurchaseTradableLimitedError(
PurchaseTradableLimitedError::CannotBuyOwnItem,
))
}
"You already own this item." => Err(RoboatError::PurchaseTradableLimitedError(
PurchaseTradableLimitedError::CannotBuyOwnItem,
)),
"This item is not for sale." => Err(RoboatError::PurchaseTradableLimitedError(
PurchaseTradableLimitedError::ItemNotForSale,
)),
"You do not have enough Robux to purchase this item." => {
Err(RoboatError::PurchaseTradableLimitedError(
PurchaseTradableLimitedError::NotEnoughRobux,
))
}
"This item has changed price. Please try again." => {
Err(RoboatError::PurchaseTradableLimitedError(
PurchaseTradableLimitedError::PriceChanged,
))
}
_ => Err(RoboatError::PurchaseTradableLimitedError(
PurchaseTradableLimitedError::UnknownRobloxErrorMsg(
raw.error_msg.as_str().to_string(),
),
)),
},
}
}
}
}