use std::time::Duration;
use async_trait::async_trait;
use tail_fin_core::{
AuthFailureKind, BrowserSession, FailureIndicators, SessionStatus, Site, SiteError,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShopeeRegion {
Tw,
Sg,
My,
Id,
Vn,
Ph,
Th,
Br,
}
impl ShopeeRegion {
pub fn region_code(&self) -> &'static str {
match self {
Self::Tw => "tw",
Self::Sg => "sg",
Self::My => "my",
Self::Id => "id",
Self::Vn => "vn",
Self::Ph => "ph",
Self::Th => "th",
Self::Br => "br",
}
}
pub fn id(&self) -> &'static str {
match self {
Self::Tw => "shopee-tw",
Self::Sg => "shopee-sg",
Self::My => "shopee-my",
Self::Id => "shopee-id",
Self::Vn => "shopee-vn",
Self::Ph => "shopee-ph",
Self::Th => "shopee-th",
Self::Br => "shopee-br",
}
}
pub fn base_url(&self) -> &'static str {
match self {
Self::Tw => "https://shopee.tw",
Self::Sg => "https://shopee.sg",
Self::My => "https://shopee.com.my",
Self::Id => "https://shopee.co.id",
Self::Vn => "https://shopee.vn",
Self::Ph => "https://shopee.ph",
Self::Th => "https://shopee.co.th",
Self::Br => "https://shopee.com.br",
}
}
pub fn cookie_domain_pattern(&self) -> &'static str {
match self {
Self::Tw => ".shopee.tw",
Self::Sg => ".shopee.sg",
Self::My => ".shopee.com.my",
Self::Id => ".shopee.co.id",
Self::Vn => ".shopee.vn",
Self::Ph => ".shopee.ph",
Self::Th => ".shopee.co.th",
Self::Br => ".shopee.com.br",
}
}
pub fn parse(s: &str) -> Option<Self> {
let code = s.strip_prefix("shopee-").unwrap_or(s);
match code {
"tw" => Some(Self::Tw),
"sg" => Some(Self::Sg),
"my" => Some(Self::My),
"id" => Some(Self::Id),
"vn" => Some(Self::Vn),
"ph" => Some(Self::Ph),
"th" => Some(Self::Th),
"br" => Some(Self::Br),
_ => None,
}
}
}
pub struct ShopeeSite(pub ShopeeRegion);
#[async_trait]
impl Site for ShopeeSite {
fn id(&self) -> &'static str {
self.0.id()
}
fn display_name(&self) -> &'static str {
match self.0 {
ShopeeRegion::Tw => "Shopee Taiwan",
ShopeeRegion::Sg => "Shopee Singapore",
ShopeeRegion::My => "Shopee Malaysia",
ShopeeRegion::Id => "Shopee Indonesia",
ShopeeRegion::Vn => "Shopee Vietnam",
ShopeeRegion::Ph => "Shopee Philippines",
ShopeeRegion::Th => "Shopee Thailand",
ShopeeRegion::Br => "Shopee Brazil",
}
}
fn cookie_domain_patterns(&self) -> &'static [&'static str] {
match self.0 {
ShopeeRegion::Tw => &[".shopee.tw"],
ShopeeRegion::Sg => &[".shopee.sg"],
ShopeeRegion::My => &[".shopee.com.my"],
ShopeeRegion::Id => &[".shopee.co.id"],
ShopeeRegion::Vn => &[".shopee.vn"],
ShopeeRegion::Ph => &[".shopee.ph"],
ShopeeRegion::Th => &[".shopee.co.th"],
ShopeeRegion::Br => &[".shopee.com.br"],
}
}
fn refresh_url(&self) -> &'static str {
match self.0 {
ShopeeRegion::Tw => "https://shopee.tw/",
ShopeeRegion::Sg => "https://shopee.sg/",
ShopeeRegion::My => "https://shopee.com.my/",
ShopeeRegion::Id => "https://shopee.co.id/",
ShopeeRegion::Vn => "https://shopee.vn/",
ShopeeRegion::Ph => "https://shopee.ph/",
ShopeeRegion::Th => "https://shopee.co.th/",
ShopeeRegion::Br => "https://shopee.com.br/",
}
}
fn refresh_interval_min(&self) -> Duration {
Duration::from_secs(180)
}
async fn validate(&self, session: &BrowserSession) -> Result<SessionStatus, SiteError> {
let status = session.http_ping(self.refresh_url()).await.map_err(|e| {
SiteError::ValidationFailed {
site: self.id(),
reason: format!("homepage ping: {e}"),
}
})?;
Ok(match status {
200 => SessionStatus::Valid,
401 | 403 => SessionStatus::Expired,
429 => SessionStatus::Blocked {
reason: "rate limited".into(),
retry_after: Some(Duration::from_secs(300)),
},
0 => SessionStatus::Unknown,
other => SessionStatus::Degrading {
estimated_expiry: None,
hint: format!("unexpected HTTP {other}"),
},
})
}
fn detect_auth_failure(&self, indicators: &FailureIndicators) -> Option<AuthFailureKind> {
match indicators.status {
Some(401) | Some(403) => Some(AuthFailureKind::CookieExpired),
Some(429) => Some(AuthFailureKind::RateLimited {
retry_after: Duration::from_secs(300),
}),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn region_parse_round_trips() {
for r in [
ShopeeRegion::Tw,
ShopeeRegion::Sg,
ShopeeRegion::My,
ShopeeRegion::Id,
ShopeeRegion::Vn,
ShopeeRegion::Ph,
ShopeeRegion::Th,
ShopeeRegion::Br,
] {
assert_eq!(ShopeeRegion::parse(r.id()), Some(r));
assert_eq!(ShopeeRegion::parse(r.region_code()), Some(r));
let cookie_host = r.cookie_domain_pattern().trim_start_matches('.');
assert!(
r.base_url().contains(cookie_host),
"base_url {} should contain cookie host {}",
r.base_url(),
cookie_host
);
}
}
#[test]
fn region_parse_rejects_unknown() {
assert!(ShopeeRegion::parse("us").is_none());
assert!(ShopeeRegion::parse("shopee-us").is_none());
assert!(ShopeeRegion::parse("").is_none());
}
#[test]
fn region_id_is_kebab_case() {
for r in [ShopeeRegion::Tw, ShopeeRegion::Br] {
assert!(r.id().starts_with("shopee-"));
assert!(!r.id().contains('_'));
}
}
#[test]
fn site_id_matches_region() {
let s = ShopeeSite(ShopeeRegion::Tw);
assert_eq!(s.id(), "shopee-tw");
assert_eq!(s.cookie_domain_patterns(), &[".shopee.tw"]);
assert_eq!(s.refresh_url(), "https://shopee.tw/");
}
}