use std::fmt;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::acquisition::StrategyUsed;
pub const DEFAULT_QUEUE_INTERVAL_MS: u64 = 5_000;
pub const DEFAULT_QUEUE_MAX_RETRIES: u32 = 3;
pub const DEFAULT_CHALLENGE_SOLVE_BUDGET_MS: u64 = 30_000;
pub const DEFAULT_HARD_BLOCK_ESCALATION: StrategyUsed = StrategyUsed::StickyProxyBrowserSession;
pub const DEFAULT_TRANSIENT_FOLLOW_REDIRECT: bool = true;
pub const DEFAULT_MAX_TRANSIENT_HOPS: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InterstitialKind {
Queue,
Challenge,
HardBlock,
Transient,
}
impl InterstitialKind {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Queue => "queue",
Self::Challenge => "challenge",
Self::HardBlock => "hard_block",
Self::Transient => "transient",
}
}
#[must_use]
pub const fn is_queue(self) -> bool {
matches!(self, Self::Queue)
}
#[must_use]
pub const fn is_hard_block(self) -> bool {
matches!(self, Self::HardBlock)
}
#[must_use]
pub const fn is_challenge(self) -> bool {
matches!(self, Self::Challenge)
}
#[must_use]
pub const fn is_transient(self) -> bool {
matches!(self, Self::Transient)
}
}
impl fmt::Display for InterstitialKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InterstitialSeverity {
Retryable,
RequiresSolve,
Terminal,
}
impl InterstitialSeverity {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Retryable => "retryable",
Self::RequiresSolve => "requires_solve",
Self::Terminal => "terminal",
}
}
#[must_use]
pub const fn for_kind(kind: InterstitialKind) -> Self {
match kind {
InterstitialKind::Queue | InterstitialKind::Transient => Self::Retryable,
InterstitialKind::Challenge => Self::RequiresSolve,
InterstitialKind::HardBlock => Self::Terminal,
}
}
}
impl fmt::Display for InterstitialSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "route")]
pub enum InterstitialRoute {
WaitAndRetry {
#[serde(with = "duration_ms")]
interval: Duration,
max_retries: u32,
queue_position: Option<u32>,
},
ChallengeSolve {
vendor_hint: Option<String>,
allowed_strategies: Vec<StrategyUsed>,
#[serde(with = "duration_ms")]
solve_budget: Duration,
},
HardBlock {
escalate_to: StrategyUsed,
rotate_session: bool,
refresh_sticky: bool,
},
Transient {
follow_redirect: bool,
max_hops: u32,
},
}
impl InterstitialRoute {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::WaitAndRetry { .. } => "wait_and_retry",
Self::ChallengeSolve { .. } => "challenge_solve",
Self::HardBlock { .. } => "hard_block",
Self::Transient { .. } => "transient",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InterstitialPolicy {
#[serde(with = "duration_ms")]
pub queue_interval: Duration,
pub queue_max_retries: u32,
#[serde(with = "duration_ms")]
pub challenge_solve_budget: Duration,
pub hard_block_escalation: StrategyUsed,
pub transient_follow_redirect: bool,
pub max_transient_hops: u32,
pub short_circuit_on_classified: bool,
}
impl Default for InterstitialPolicy {
fn default() -> Self {
Self {
queue_interval: Duration::from_millis(DEFAULT_QUEUE_INTERVAL_MS),
queue_max_retries: DEFAULT_QUEUE_MAX_RETRIES,
challenge_solve_budget: Duration::from_millis(DEFAULT_CHALLENGE_SOLVE_BUDGET_MS),
hard_block_escalation: DEFAULT_HARD_BLOCK_ESCALATION,
transient_follow_redirect: DEFAULT_TRANSIENT_FOLLOW_REDIRECT,
max_transient_hops: DEFAULT_MAX_TRANSIENT_HOPS,
short_circuit_on_classified: true,
}
}
}
impl InterstitialPolicy {
#[must_use]
pub const fn with_queue_interval(mut self, interval: Duration) -> Self {
self.queue_interval = interval;
self
}
#[must_use]
pub const fn with_queue_max_retries(mut self, max_retries: u32) -> Self {
self.queue_max_retries = max_retries;
self
}
#[must_use]
pub const fn with_challenge_solve_budget(mut self, budget: Duration) -> Self {
self.challenge_solve_budget = budget;
self
}
#[must_use]
pub const fn with_hard_block_escalation(mut self, strategy: StrategyUsed) -> Self {
self.hard_block_escalation = strategy;
self
}
#[must_use]
pub const fn with_transient_follow_redirect(mut self, follow: bool) -> Self {
self.transient_follow_redirect = follow;
self
}
#[must_use]
pub const fn with_max_transient_hops(mut self, max_hops: u32) -> Self {
self.max_transient_hops = max_hops;
self
}
#[must_use]
pub const fn with_short_circuit(mut self, short_circuit: bool) -> Self {
self.short_circuit_on_classified = short_circuit;
self
}
}
mod duration_ms {
use std::time::Duration;
use serde::{Deserialize, Deserializer, Serializer};
#[allow(clippy::cast_possible_truncation)]
pub fn serialize<S: Serializer>(value: &Duration, ser: S) -> Result<S::Ok, S::Error> {
let ms = value.as_millis();
let n = if ms > u128::from(u64::MAX) {
u64::MAX
} else {
ms as u64
};
ser.serialize_u64(n)
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Duration, D::Error> {
let ms = u64::deserialize(de)?;
Ok(Duration::from_millis(ms))
}
}