pub mod browser;
pub mod parsing;
pub mod site;
pub mod types;
use std::path::Path;
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, COOKIE, REFERER, USER_AGENT};
use tail_fin_common::TailFinError;
pub use browser::ShopeeBrowserClient;
pub use site::{ShopeeRegion, ShopeeSite};
pub use types::{
AccountInfo, CartItem, CartPreview, Category, CategoryDetail, CategoryPage, Discover,
FlashSaleItem, HomepageBundle, MallShop, ProductDetail, ProductModel, RecommendedItem,
RelatedItems, Review, Reviews, SearchItem, SearchResults, ShopInfo, ShopItems, UserMatch,
UserSearchResults,
};
const UA: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
pub struct ShopeeHttpClient {
client: reqwest::Client,
region: ShopeeRegion,
csrf_token: String,
}
impl ShopeeHttpClient {
pub fn from_cookie_file(path: &Path, region: ShopeeRegion) -> Result<Self, TailFinError> {
let cookies = tail_fin_common::cookies::load_netscape_file(path)?;
if cookies.is_empty() {
return Err(TailFinError::AuthRequired);
}
let csrf_token = cookies
.iter()
.find(|(n, _)| n == "csrftoken")
.map(|(_, v)| v.clone())
.unwrap_or_default();
let cookie_header = tail_fin_common::cookies::build_cookie_header(&cookies);
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(UA));
headers.insert(
ACCEPT,
HeaderValue::from_static("application/json, text/plain, */*"),
);
let referer = format!("{}/", region.base_url());
headers.insert(
REFERER,
HeaderValue::from_str(&referer).map_err(|e| {
TailFinError::Api(format!("invalid referer for region {region:?}: {e}"))
})?,
);
headers.insert(
COOKIE,
HeaderValue::from_str(&cookie_header)
.map_err(|e| TailFinError::Api(format!("cookie header: {e}")))?,
);
if !csrf_token.is_empty() {
headers.insert(
"x-csrftoken",
HeaderValue::from_str(&csrf_token)
.map_err(|e| TailFinError::Api(format!("csrftoken header: {e}")))?,
);
}
headers.insert("x-shopee-language", HeaderValue::from_static("zh-Hant"));
let client = reqwest::Client::builder()
.default_headers(headers)
.build()
.map_err(|e| TailFinError::Api(format!("HTTP client error: {e}")))?;
Ok(Self {
client,
region,
csrf_token,
})
}
pub fn region(&self) -> ShopeeRegion {
self.region
}
pub fn csrf_token(&self) -> &str {
&self.csrf_token
}
pub async fn me(&self) -> Result<AccountInfo, TailFinError> {
let url = format!(
"{}/api/v4/account/basic/get_account_info",
self.region.base_url()
);
let resp = self
.client
.get(&url)
.send()
.await
.map_err(|e| TailFinError::Api(format!("request: {e}")))?;
if !resp.status().is_success() {
return Err(TailFinError::Api(format!(
"Shopee {} HTTP {}",
self.region.id(),
resp.status()
)));
}
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| TailFinError::Parse(format!("body: {e}")))?;
let err_code = body.get("error").and_then(|v| v.as_i64()).unwrap_or(0);
if err_code != 0 {
let msg = body
.get("error_msg")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if err_code == 44 || msg.contains("must login") || msg.contains("login") {
return Err(TailFinError::AuthRequired);
}
return Err(TailFinError::Api(format!("Shopee error {err_code}: {msg}")));
}
let data = body
.get("data")
.ok_or_else(|| TailFinError::Parse("missing `data` in account_info response".into()))?
.clone();
let mut info: AccountInfo = serde_json::from_value(data.clone())
.map_err(|e| TailFinError::Parse(format!("AccountInfo: {e}")))?;
info.raw = Some(data);
Ok(info)
}
}