use crate::{Client, Limit, RoboatError, User};
use reqwest::header;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
mod request_types;
const TRADES_API: &str = "https://trades.roblox.com/v1/trades/";
const TRADE_DETAILS_API: &str = "https://trades.roblox.com/v1/trades/{trade_id}";
const DECLINE_TRADE_API: &str = "https://trades.roblox.com/v1/trades/{trade_id}/decline";
const SEND_TRADE_API: &str = "https://trades.roblox.com/v1/trades/send";
const ACCEPT_TRADE_API: &str = "https://trades.roblox.com/v1/trades/{trade_id}/accept";
const TRADE_COUNT_API: &str = "https://trades.roblox.com/v1/trades/inbound/count";
const SORT_ORDER: &str = "Desc";
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize, Copy,
)]
#[allow(missing_docs)]
pub enum TradeType {
Inbound,
Outbound,
Completed,
#[default]
Inactive,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
pub struct Trade {
pub trade_id: u64,
pub partner: User,
pub is_active: bool,
pub status: TradeStatus,
}
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize, Copy,
)]
#[allow(missing_docs)]
pub enum TradeStatus {
Open,
Completed,
Declined,
#[default]
Expired,
RejectedDueToError,
}
impl std::str::FromStr for TradeStatus {
type Err = RoboatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"Open" => Ok(Self::Open),
"Completed" => Ok(Self::Completed),
"Declined" => Ok(Self::Declined),
"Expired" => Ok(Self::Expired),
"RejectedDueToError" => Ok(Self::RejectedDueToError),
_ => Err(RoboatError::MalformedResponse),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
pub struct TradeDetails {
pub partner: User,
pub your_items: Vec<TradeItem>,
pub your_robux: u64,
pub partner_items: Vec<TradeItem>,
pub partner_robux: u64,
pub created: String,
pub expiration: Option<String>,
pub is_active: bool,
pub status: TradeStatus,
}
#[allow(missing_docs)]
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
pub struct TradeItem {
pub item_id: u64,
pub serial_number: Option<u64>,
pub uaid: u64,
pub name: String,
pub rap: u64,
}
impl Client {
pub async fn trades(
&self,
trade_type: TradeType,
limit: Limit,
cursor: Option<String>,
) -> Result<(Vec<Trade>, Option<String>), RoboatError> {
let limit = limit.to_u64();
let cursor = cursor.unwrap_or_default();
let cookie_string = self.cookie_string()?;
let trade_type_str = match trade_type {
TradeType::Inbound => "inbound",
TradeType::Outbound => "outbound",
TradeType::Completed => "completed",
TradeType::Inactive => "inactive",
};
let formatted_url = format!(
"{}{}?sortOrder={}&cursor={}&limit={}",
TRADES_API, trade_type_str, SORT_ORDER, cursor, limit
);
let request_result = self
.reqwest_client
.get(&formatted_url)
.header(header::COOKIE, cookie_string)
.send()
.await;
let response = Self::validate_request_result(request_result).await?;
let raw = Self::parse_to_raw::<request_types::InboundTradesResponse>(response).await?;
let next_cursor = raw.next_page_cursor;
let mut trades = Vec::new();
for trade in raw.data {
let partner = User {
user_id: trade.user.id as u64,
username: trade.user.name,
display_name: trade.user.display_name,
};
let trade = Trade {
trade_id: trade.id as u64,
partner,
is_active: trade.is_active,
status: trade.status,
};
trades.push(trade);
}
Ok((trades, next_cursor))
}
pub async fn trade_details(&self, trade_id: u64) -> Result<TradeDetails, RoboatError> {
let formatted_url = TRADE_DETAILS_API.replace("{trade_id}", &trade_id.to_string());
let cookie_string = self.cookie_string()?;
let response_result = self
.reqwest_client
.get(&formatted_url)
.header(header::COOKIE, cookie_string)
.send()
.await;
let response = Self::validate_request_result(response_result).await?;
let raw = Self::parse_to_raw::<request_types::TradeDetailsResponse>(response).await?;
let partner = User {
user_id: raw.offers[1].user.id as u64,
username: raw.offers[1].user.name.clone(),
display_name: raw.offers[1].user.display_name.clone(),
};
let mut your_items: Vec<TradeItem> = Vec::new();
for item in &raw.offers[0].user_assets {
let trade_item = TradeItem {
item_id: item.asset_id as u64,
serial_number: item.serial_number.map(|x| x as u64),
uaid: item.id as u64,
name: item.name.clone(),
rap: item.recent_average_price as u64,
};
your_items.push(trade_item);
}
let mut partner_items: Vec<TradeItem> = Vec::new();
for item in &raw.offers[1].user_assets {
let trade_item = TradeItem {
item_id: item.asset_id as u64,
serial_number: item.serial_number.map(|x| x as u64),
uaid: item.id as u64,
name: item.name.clone(),
rap: item.recent_average_price as u64,
};
partner_items.push(trade_item);
}
let your_robux = raw.offers[0].robux as u64;
let partner_robux = raw.offers[1].robux as u64;
let created = raw.created;
let expiration = raw.expiration;
let is_active = raw.is_active;
let trade_status = TradeStatus::from_str(&raw.status)?;
let trade_details = TradeDetails {
partner,
your_items,
partner_items,
your_robux,
partner_robux,
created,
expiration,
is_active,
status: trade_status,
};
Ok(trade_details)
}
pub async fn decline_trade(&self, trade_id: u64) -> Result<(), RoboatError> {
match self.decline_trade_internal(trade_id).await {
Ok(x) => Ok(x),
Err(e) => match e {
RoboatError::InvalidXcsrf(new_xcsrf) => {
self.set_xcsrf(new_xcsrf).await;
self.decline_trade_internal(trade_id).await
}
_ => Err(e),
},
}
}
pub async fn send_trade(
&self,
partner_id: u64,
your_item_uaids: Vec<u64>,
your_robux: u64,
partner_item_uaids: Vec<u64>,
partner_robux: u64,
) -> Result<u64, RoboatError> {
match self
.send_trade_internal(
partner_id,
your_item_uaids.clone(),
your_robux,
partner_item_uaids.clone(),
partner_robux,
)
.await
{
Ok(x) => Ok(x),
Err(e) => match e {
RoboatError::InvalidXcsrf(new_xcsrf) => {
self.set_xcsrf(new_xcsrf).await;
self.send_trade_internal(
partner_id,
your_item_uaids,
your_robux,
partner_item_uaids,
partner_robux,
)
.await
}
_ => Err(e),
},
}
}
pub async fn accept_trade(&self, trade_id: u64) -> Result<(), RoboatError> {
match self.accept_trade_internal(trade_id).await {
Ok(x) => Ok(x),
Err(e) => match e {
RoboatError::InvalidXcsrf(new_xcsrf) => {
self.set_xcsrf(new_xcsrf).await;
self.accept_trade_internal(trade_id).await
}
_ => Err(e),
},
}
}
pub async fn trade_count(&self) -> Result<u64, RoboatError> {
let cookie_string = self.cookie_string()?;
let response_result = self
.reqwest_client
.get(TRADE_COUNT_API)
.header(header::COOKIE, cookie_string)
.send()
.await;
let response = Self::validate_request_result(response_result).await?;
let raw = Self::parse_to_raw::<request_types::TradeCountResponse>(response).await?;
Ok(raw.count)
}
}
mod internal {
use super::{request_types, ACCEPT_TRADE_API, DECLINE_TRADE_API, SEND_TRADE_API};
use crate::{Client, RoboatError, XCSRF_HEADER};
use reqwest::header;
impl Client {
pub(super) async fn decline_trade_internal(
&self,
trade_id: u64,
) -> Result<(), RoboatError> {
let formatted_url = DECLINE_TRADE_API.replace("{trade_id}", &trade_id.to_string());
let cookie_string = self.cookie_string()?;
let xcsrf = self.xcsrf().await;
let response_result = self
.reqwest_client
.post(&formatted_url)
.header(header::COOKIE, cookie_string)
.header(XCSRF_HEADER, xcsrf)
.send()
.await;
Self::validate_request_result(response_result).await?;
Ok(())
}
pub(super) async fn send_trade_internal(
&self,
partner_id: u64,
your_item_uaids: Vec<u64>,
your_robux: u64,
partner_item_uaids: Vec<u64>,
partner_robux: u64,
) -> Result<u64, RoboatError> {
let cookie_string = self.cookie_string()?;
let xcsrf = self.xcsrf().await;
let user_id = self.user_id().await?;
let user_trade_offer = request_types::SendTradeOffer {
user_id,
user_asset_ids: your_item_uaids,
robux: your_robux,
};
let partner_trade_offer = request_types::SendTradeOffer {
user_id: partner_id,
user_asset_ids: partner_item_uaids,
robux: partner_robux,
};
let body = request_types::SendTradeBody {
offers: vec![partner_trade_offer, user_trade_offer],
};
let response_result = self
.reqwest_client
.post(SEND_TRADE_API)
.header(header::COOKIE, cookie_string)
.header(XCSRF_HEADER, xcsrf)
.json(&body)
.send()
.await;
let response = Self::validate_request_result(response_result).await?;
let raw = Self::parse_to_raw::<request_types::SendTradeResponse>(response).await?;
Ok(raw.id)
}
pub(super) async fn accept_trade_internal(&self, trade_id: u64) -> Result<(), RoboatError> {
let formatted_url = ACCEPT_TRADE_API.replace("{trade_id}", &trade_id.to_string());
let cookie_string = self.cookie_string()?;
let xcsrf = self.xcsrf().await;
let response_result = self
.reqwest_client
.post(&formatted_url)
.header(header::COOKIE, cookie_string)
.header(XCSRF_HEADER, xcsrf)
.send()
.await;
Self::validate_request_result(response_result).await?;
Ok(())
}
}
}