use crate::{
handle_error, handle_error2, Error, ErrorResponse, Result, Tarkov, PROD_ENDPOINT,
RAGFAIR_ENDPOINT,
};
use crate::inventory::{BarterItem, InventoryUpdate, Item, MoveItemRequest, RagfairResponseData};
use crate::market_filter::{Currency, MarketFilter, Owner, SortBy, SortDirection};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, err_derive::Error)]
pub enum RagfairError {
#[error(display = "offer not available yet")]
OfferNotAvailableYet,
#[error(display = "offer not found, sold out or out of stock")]
OfferNotFound,
#[error(display = "barter items provided cannot be found")]
InvalidBarterItems,
#[error(display = "maximum offer count of 3 was reached")]
MaxOfferCount,
#[error(display = "insufficient funds to pay market fee")]
InsufficientTaxFunds,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SearchRequest<'a> {
page: u64,
limit: u64,
sort_type: SortBy,
sort_direction: SortDirection,
currency: Currency,
price_from: u64,
price_to: u64,
quantity_from: u64,
quantity_to: u64,
condition_from: u64,
condition_to: u64,
one_hour_expiration: bool,
remove_bartering: bool,
offer_owner_type: Owner,
only_functional: bool,
update_offer_count: bool,
handbook_id: &'a str,
linked_search_id: &'a str,
needed_search_id: &'a str,
tm: u64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SearchResponse {
#[serde(flatten)]
error: ErrorResponse,
data: Option<SearchResult>,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SearchResult {
pub categories: HashMap<String, u64>,
pub offers: Vec<Offer>,
pub offers_count: u64,
pub selected_category: String,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Offer {
#[serde(rename = "_id")]
pub id: String,
pub int_id: String,
pub user: User,
pub root: String,
pub items: Vec<Item>,
pub items_cost: u64,
pub requirements: Vec<Requirement>,
pub requirements_cost: u64,
pub summary_cost: u64,
pub sell_in_one_piece: bool,
pub start_time: u64,
pub end_time: u64,
pub loyalty_level: u64,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct User {
pub id: String,
pub member_type: u64,
pub nickname: Option<String>,
pub rating: Option<f64>,
pub is_rating_growing: Option<bool>,
pub avatar: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Requirement {
#[serde(rename = "_tpl")]
pub schema_id: String,
pub count: f64,
}
#[derive(Debug, Serialize)]
struct BuyItemRequest<'a> {
#[serde(rename = "Action")]
action: &'a str,
offers: &'a [BuyOffer<'a>],
}
#[derive(Debug, Serialize)]
struct BuyOffer<'a> {
id: &'a str,
count: u64,
items: &'a [BarterItem],
}
#[derive(Debug, Serialize)]
struct BuyItem {
item: String,
count: f64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct BuyItemResponse {
#[serde(flatten)]
error: ErrorResponse,
data: serde_json::Value,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct GetPriceRequest<'a> {
template_id: &'a str,
}
#[derive(Debug, Deserialize)]
struct GetPriceResponse {
#[serde(flatten)]
error: ErrorResponse,
data: Option<Price>,
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Price {
#[serde(rename = "templateId")]
pub schema_id: String,
pub min: f64,
pub max: f64,
pub avg: f64,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SellItemRequest<'a> {
#[serde(rename = "Action")]
action: &'a str,
sell_in_one_piece: bool,
items: &'a [&'a str],
requirements: &'a [SellRequirement],
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct SellRequirement {
#[serde(flatten)]
requirement: Requirement,
level: u64,
side: u8,
only_functional: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SellItemResponse {
#[serde(flatten)]
error: ErrorResponse,
data: serde_json::Value,
}
impl Tarkov {
pub async fn search_market<'a>(
&self,
page: u64,
limit: u64,
filter: MarketFilter<'_>,
) -> Result<SearchResult> {
if limit == 0 {
return Err(Error::InvalidParameters);
}
let body = SearchRequest {
page,
limit,
sort_type: filter.sort_type,
sort_direction: filter.sort_direction,
currency: filter.currency,
price_from: filter.min_price.unwrap_or(0),
price_to: filter.max_price.unwrap_or(0),
quantity_from: filter.min_quantity.unwrap_or(0),
quantity_to: filter.max_quantity.unwrap_or(0),
condition_from: filter.min_condition.unwrap_or(0),
condition_to: filter.max_condition.unwrap_or(0),
one_hour_expiration: filter.expiring_within_hour,
remove_bartering: filter.hide_bartering_offers,
offer_owner_type: filter.owner_type,
only_functional: filter.hide_inoperable_weapons,
update_offer_count: true,
handbook_id: &filter.handbook_id.unwrap_or(""),
linked_search_id: &filter.linked_search_id.unwrap_or(""),
needed_search_id: &filter.required_search_id.unwrap_or(""),
tm: 1,
};
let url = format!("{}/client/ragfair/find", RAGFAIR_ENDPOINT);
let res: SearchResponse = self.post_json(&url, &body).await?;
handle_error(res.error, res.data)
}
pub async fn get_item_price(&self, schema_id: &str) -> Result<Price> {
if schema_id.is_empty() {
return Err(Error::InvalidParameters);
}
let url = format!("{}/client/ragfair/itemMarketPrice", RAGFAIR_ENDPOINT);
let body = GetPriceRequest {
template_id: schema_id,
};
let res: GetPriceResponse = self.post_json(&url, &body).await?;
handle_error(res.error, res.data)
}
pub async fn buy_item(
&self,
offer_id: &str,
quantity: u64,
barter_items: &[BarterItem],
) -> Result<InventoryUpdate> {
if offer_id.is_empty() || quantity == 0 || barter_items.is_empty() {
return Err(Error::InvalidParameters);
}
let url = format!("{}/client/game/profile/items/moving", PROD_ENDPOINT);
let body = &MoveItemRequest {
data: &[BuyItemRequest {
action: "RagFairBuyOffer",
offers: &[BuyOffer {
id: offer_id,
count: quantity,
items: barter_items,
}],
}],
tm: 2,
};
let res: BuyItemResponse = self.post_json(&url, body).await?;
handle_error2(res.error)?;
let res: RagfairResponseData = Deserialize::deserialize(res.data)?;
if !res.errors.is_empty() {
let error = &res.errors[0];
match error.code {
1503 | 1506 | 1507 => return Err(RagfairError::OfferNotFound)?,
_ => return Err(Error::UnknownAPIError(error.code)),
}
}
let items: InventoryUpdate = Deserialize::deserialize(res.items)?;
Ok(items)
}
pub async fn offer_item(
&self,
items: &[&str],
requirements: &[Requirement],
sell_all: bool,
) -> Result<InventoryUpdate> {
if items.is_empty() || requirements.is_empty() {
return Err(Error::InvalidParameters);
}
let url = format!("{}/client/game/profile/items/moving", PROD_ENDPOINT);
let body = &MoveItemRequest {
data: &[SellItemRequest {
action: "RagFairAddOffer",
sell_in_one_piece: sell_all,
items,
requirements: &requirements
.into_iter()
.map(|r| SellRequirement {
requirement: r.to_owned(),
level: 0,
side: 0,
only_functional: false,
})
.collect::<Vec<SellRequirement>>(),
}],
tm: 2,
};
let res: SellItemResponse = self.post_json(&url, body).await?;
handle_error2(res.error)?;
let res: RagfairResponseData = Deserialize::deserialize(res.data)?;
if !res.errors.is_empty() {
let error = &res.errors[0];
return Err(Error::UnknownAPIError(error.code));
}
let items: InventoryUpdate = Deserialize::deserialize(res.items)?;
Ok(items)
}
}