From a17430de0781178eeb7d53d8fe140e7c6e178b0b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Mar 2026 09:50:36 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20stakereload=20branding=20=E2=80=94=20si?= =?UTF-8?q?te-specific=20workflow=20and=20profile=20presets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a dedicated `stakereload` module with: - `StakereloadSite` enum (Standard / Xs) with base-URL helpers - `StakereloadWorkflow` wrapper around `IgnitionPage` providing `open_home`, `goto_path`, `page_state`, `wait_for_cf_clearance`, `title`, `current_url`, `cf_clearance_cookie`, and human-like scroll helpers scoped to stakereload.com and stakereloadxs.com - `PageState` enum (Ready / CloudflareChallenge / Unknown) for post-navigation state detection - `IgnitionProfile::stakereload()` preset — Windows RTX 3080, 16 GB, 12 cores - `IgnitionProfile::stakereloadxs()` preset — Windows RTX 4080, 16 GB, 12 cores (distinct GPU fingerprint to avoid cross-site correlation) - `examples/stakereload_workflow.rs` — end-to-end branded demo - Fix pre-existing `redundant_pattern_matching` clippy lint in all examples https://claude.ai/code/session_01GVkkdEB14qb5qhU5UhHpa7 --- examples/bot_detection_test.rs | 2 +- examples/fused_gaming.rs | 2 +- examples/profile_demo.rs | 2 +- examples/stakereload_workflow.rs | 114 ++++++++++++++ examples/stealth_bot.rs | 2 +- src/lib.rs | 2 + src/profiles.rs | 23 +++ src/stakereload.rs | 255 +++++++++++++++++++++++++++++++ 8 files changed, 398 insertions(+), 4 deletions(-) create mode 100644 examples/stakereload_workflow.rs create mode 100644 src/stakereload.rs diff --git a/examples/bot_detection_test.rs b/examples/bot_detection_test.rs index a45038da..5623cbe7 100644 --- a/examples/bot_detection_test.rs +++ b/examples/bot_detection_test.rs @@ -21,7 +21,7 @@ async fn main() -> Result<()> { ) .await?; - tokio::spawn(async move { while let Some(_) = handler.next().await {} }); + tokio::spawn(async move { while handler.next().await.is_some() {} }); // Create page and apply profile let page = browser.new_page("about:blank").await?; diff --git a/examples/fused_gaming.rs b/examples/fused_gaming.rs index 33d5da96..ad46ae0c 100644 --- a/examples/fused_gaming.rs +++ b/examples/fused_gaming.rs @@ -37,7 +37,7 @@ async fn main() -> Result<()> { ) .await?; - tokio::spawn(async move { while let Some(_) = handler.next().await {} }); + tokio::spawn(async move { while handler.next().await.is_some() {} }); // Visit each Fused Gaming domain for target in TARGETS { diff --git a/examples/profile_demo.rs b/examples/profile_demo.rs index 0694da9f..c2afb8ca 100644 --- a/examples/profile_demo.rs +++ b/examples/profile_demo.rs @@ -56,7 +56,7 @@ async fn main() -> Result<()> { ) .await?; - tokio::spawn(async move { while let Some(_) = handler.next().await {} }); + tokio::spawn(async move { while handler.next().await.is_some() {} }); // Create page with stealth let page = browser.new_page("about:blank").await?; diff --git a/examples/stakereload_workflow.rs b/examples/stakereload_workflow.rs new file mode 100644 index 00000000..55bcaf3f --- /dev/null +++ b/examples/stakereload_workflow.rs @@ -0,0 +1,114 @@ +//! Stakereload branding — workflow demo for stakereload.com and stakereloadxs.com +//! +//! Demonstrates the site-specific [`StakereloadWorkflow`] wrapper with per-site +//! profile presets, page-state detection, and Cloudflare challenge awareness. +//! +//! # Usage +//! ```bash +//! cargo run --example stakereload_workflow +//! ``` + +use anyhow::Result; +use futures::StreamExt; +use ignition::stakereload::{StakereloadSite, StakereloadWorkflow}; +use ignition::{Browser, BrowserConfig, IgnitionPage, IgnitionProfile}; +use std::time::Duration; + +#[tokio::main] +async fn main() -> Result<()> { + println!("=== Stakereload Branding Workflow ===\n"); + + // Launch a single shared browser instance + let (browser, mut handler) = Browser::launch( + BrowserConfig::builder() + .viewport(None) + .build() + .map_err(|e| anyhow::anyhow!(e))?, + ) + .await?; + + tokio::spawn(async move { while handler.next().await.is_some() {} }); + + // ── Standard site ────────────────────────────────────────────────────────── + { + println!("[1/2] stakereload.com"); + + let profile = IgnitionProfile::stakereload().build(); + println!(" Profile : {}", profile); + + let page = browser.new_page("about:blank").await?; + let ignition = IgnitionPage::new(page); + ignition.apply_profile(&profile).await?; + + // Settle before navigation so the bootstrap script is registered + tokio::time::sleep(Duration::from_millis(100)).await; + + let wf = StakereloadWorkflow::new(ignition, StakereloadSite::Standard); + wf.open_home().await?; + + let state = wf.page_state().await?; + println!(" State : {:?}", state); + + match state { + ignition::stakereload::PageState::CloudflareChallenge => { + println!(" CF challenge detected — waiting up to 30 s for clearance…"); + let cleared = wf + .wait_for_cf_clearance(Duration::from_secs(30), Duration::from_secs(2)) + .await?; + println!(" CF cleared: {}", cleared); + } + ignition::stakereload::PageState::Ready => { + let title = wf.title().await?; + let url = wf.current_url().await?; + println!(" Title : {:?}", title); + println!(" URL : {:?}", url); + } + ignition::stakereload::PageState::Unknown => { + println!(" Page state unknown — inspect manually"); + } + } + } + + // ── XS site ──────────────────────────────────────────────────────────────── + { + println!("\n[2/2] stakereloadxs.com"); + + // XS variant uses a distinct RTX 4080 fingerprint + let profile = IgnitionProfile::stakereloadxs().build(); + println!(" Profile : {}", profile); + + let page = browser.new_page("about:blank").await?; + let ignition = IgnitionPage::new(page); + ignition.apply_profile(&profile).await?; + + tokio::time::sleep(Duration::from_millis(100)).await; + + let wf = StakereloadWorkflow::new(ignition, StakereloadSite::Xs); + wf.open_home().await?; + + let state = wf.page_state().await?; + println!(" State : {:?}", state); + + match state { + ignition::stakereload::PageState::CloudflareChallenge => { + println!(" CF challenge detected — waiting up to 30 s for clearance…"); + let cleared = wf + .wait_for_cf_clearance(Duration::from_secs(30), Duration::from_secs(2)) + .await?; + println!(" CF cleared: {}", cleared); + } + ignition::stakereload::PageState::Ready => { + let title = wf.title().await?; + let url = wf.current_url().await?; + println!(" Title : {:?}", title); + println!(" URL : {:?}", url); + } + ignition::stakereload::PageState::Unknown => { + println!(" Page state unknown — inspect manually"); + } + } + } + + println!("\nStakereload workflow demo complete."); + Ok(()) +} diff --git a/examples/stealth_bot.rs b/examples/stealth_bot.rs index 8575e165..dabb2a8b 100644 --- a/examples/stealth_bot.rs +++ b/examples/stealth_bot.rs @@ -14,7 +14,7 @@ async fn main() -> Result<()> { ) .await?; - tokio::spawn(async move { while let Some(_) = handler.next().await {} }); + tokio::spawn(async move { while handler.next().await.is_some() {} }); // CRITICAL: Create page with about:blank FIRST println!("Creating page..."); diff --git a/src/lib.rs b/src/lib.rs index 1ea435d2..e54406e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,5 +80,7 @@ pub use crate::stealth::*; pub mod profiles; pub use crate::profiles::*; +pub mod stakereload; + // Re-export useful CDP types for request interception pub use chromiumoxide_cdp::cdp::browser_protocol::network::ResourceType; diff --git a/src/profiles.rs b/src/profiles.rs index 36035bda..c7dda857 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -204,6 +204,29 @@ impl IgnitionProfile { .cpu_cores(12) } + /// Profile preset tuned for **stakereload.com**. + /// + /// Identical hardware to [`fused_gaming`](Self::fused_gaming) but scoped + /// to the Standard site variant for clarity in multi-site automation code. + pub fn stakereload() -> IgnitionProfileBuilder { + Self::windows() + .gpu(Gpu::NvidiaRTX3080) + .memory_gb(16) + .cpu_cores(12) + } + + /// Profile preset tuned for **stakereloadxs.com**. + /// + /// Uses a slightly different RTX 4080 GPU fingerprint to present a distinct + /// hardware identity from the Standard variant, reducing cross-site + /// correlation signals that some anti-bot systems track. + pub fn stakereloadxs() -> IgnitionProfileBuilder { + Self::windows() + .gpu(Gpu::NvidiaRTX4080) + .memory_gb(16) + .cpu_cores(12) + } + // Getters pub fn os(&self) -> Os { self.os diff --git a/src/stakereload.rs b/src/stakereload.rs new file mode 100644 index 00000000..aa34c1fa --- /dev/null +++ b/src/stakereload.rs @@ -0,0 +1,255 @@ +//! Stakereload branding — site-specific workflow for stakereload.com and stakereloadxs.com. +//! +//! Provides a high-level [`StakereloadWorkflow`] wrapper around [`IgnitionPage`] with +//! constants, selectors, and action sequences tailored to both site variants. +//! +//! # Quick start +//! +//! ```no_run +//! use ignition::stakereload::{StakereloadSite, StakereloadWorkflow}; +//! use ignition::{Browser, BrowserConfig, IgnitionPage, IgnitionProfile}; +//! use futures::StreamExt; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! let profile = IgnitionProfile::stakereload().build(); +//! +//! let (browser, mut handler) = Browser::launch(BrowserConfig::builder().build()?).await?; +//! tokio::spawn(async move { while let Some(_) = handler.next().await {} }); +//! +//! let page = browser.new_page("about:blank").await?; +//! let ignition = IgnitionPage::new(page); +//! ignition.apply_profile(&profile).await?; +//! +//! let wf = StakereloadWorkflow::new(ignition, StakereloadSite::Standard); +//! wf.open_home().await?; +//! +//! let state = wf.page_state().await?; +//! println!("State: {:?}", state); +//! Ok(()) +//! } +//! ``` + +use crate::ignition::IgnitionPage; +use anyhow::Result; +use std::time::Duration; + +// ─── URL constants ──────────────────────────────────────────────────────────── + +/// Base URL for the standard Stakereload site. +pub const URL_STANDARD: &str = "https://stakereload.com"; + +/// Base URL for the XS (extended/alternative) Stakereload site. +pub const URL_XS: &str = "https://stakereloadxs.com"; + +// ─── CSS selectors ──────────────────────────────────────────────────────────── + +/// Selector for the Cloudflare Turnstile challenge iframe. +pub const SEL_CF_CHALLENGE: &str = "iframe[src*='challenges.cloudflare.com']"; + +/// Selector for the Cloudflare interstitial title text. +pub const SEL_CF_TITLE: &str = "#challenge-running, .lds-ring, #cf-content"; + +/// Selector for a generic page-ready indicator (main content wrapper). +pub const SEL_MAIN_CONTENT: &str = "main, #app, #root, .main-content, body > div"; + +// ─── Site variant ───────────────────────────────────────────────────────────── + +/// Identifies which Stakereload site variant is being automated. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StakereloadSite { + /// + Standard, + /// + Xs, +} + +impl StakereloadSite { + /// Returns the base URL for this variant. + pub fn base_url(self) -> &'static str { + match self { + StakereloadSite::Standard => URL_STANDARD, + StakereloadSite::Xs => URL_XS, + } + } + + /// Human-readable label used in log output. + pub fn label(self) -> &'static str { + match self { + StakereloadSite::Standard => "stakereload.com", + StakereloadSite::Xs => "stakereloadxs.com", + } + } +} + +// ─── Page state ─────────────────────────────────────────────────────────────── + +/// Observed state of the page after a navigation attempt. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PageState { + /// Page loaded and main content is visible. + Ready, + /// Cloudflare challenge (Turnstile / interstitial) is in progress. + CloudflareChallenge, + /// Page loaded but could not be identified as ready or challenged. + Unknown, +} + +// ─── Workflow ───────────────────────────────────────────────────────────────── + +/// High-level automation workflow for Stakereload sites. +/// +/// Wraps an [`IgnitionPage`] with site-specific helpers so callers can +/// express actions at the level of the Stakereload product rather than raw CDP. +#[derive(Debug, Clone)] +pub struct StakereloadWorkflow { + ignition: IgnitionPage, + site: StakereloadSite, +} + +impl StakereloadWorkflow { + /// Create a new workflow for the given site variant. + /// + /// The supplied `ignition` page must already have a profile applied via + /// [`IgnitionPage::apply_profile`] before any navigation methods are called. + pub fn new(ignition: IgnitionPage, site: StakereloadSite) -> Self { + Self { ignition, site } + } + + /// Returns the underlying [`IgnitionPage`] for low-level access. + pub fn page(&self) -> &IgnitionPage { + &self.ignition + } + + /// Returns the site variant this workflow is targeting. + pub fn site(&self) -> StakereloadSite { + self.site + } + + // ── Navigation ──────────────────────────────────────────────────────────── + + /// Navigate to the site's home page and wait for an initial render. + /// + /// A short settle delay is included to allow client-side frameworks to + /// hydrate before subsequent interactions. + pub async fn open_home(&self) -> Result<()> { + self.ignition.goto(self.site.base_url()).await?; + tokio::time::sleep(Duration::from_millis(1500)).await; + Ok(()) + } + + /// Navigate to an arbitrary path within the current site. + /// + /// # Example + /// ```no_run + /// wf.goto_path("/promotions").await?; + /// ``` + pub async fn goto_path(&self, path: &str) -> Result<()> { + let url = format!("{}{}", self.site.base_url(), path); + self.ignition.goto(&url).await?; + tokio::time::sleep(Duration::from_millis(1500)).await; + Ok(()) + } + + // ── Page state detection ────────────────────────────────────────────────── + + /// Detect the current state of the page. + /// + /// Returns [`PageState::CloudflareChallenge`] if a Turnstile or Cloudflare + /// interstitial is detected, [`PageState::Ready`] when main content is + /// present, or [`PageState::Unknown`] otherwise. + pub async fn page_state(&self) -> Result { + // Check for Cloudflare challenge markers + let cf_check = format!( + "document.querySelector('{}') !== null || document.querySelector('{}') !== null", + SEL_CF_CHALLENGE, SEL_CF_TITLE + ); + if let Some(val) = self.ignition.evaluate(&cf_check).await? { + if val.as_bool().unwrap_or(false) { + return Ok(PageState::CloudflareChallenge); + } + } + + // Check for main content + let content_check = format!( + "document.querySelector('{}') !== null", + SEL_MAIN_CONTENT + ); + if let Some(val) = self.ignition.evaluate(&content_check).await? { + if val.as_bool().unwrap_or(false) { + return Ok(PageState::Ready); + } + } + + Ok(PageState::Unknown) + } + + /// Wait until the page leaves a Cloudflare challenge state, polling every + /// `interval` until `timeout` is reached. Returns `true` if the challenge + /// cleared, `false` if it timed out. + pub async fn wait_for_cf_clearance( + &self, + timeout: Duration, + interval: Duration, + ) -> Result { + let deadline = tokio::time::Instant::now() + timeout; + loop { + if tokio::time::Instant::now() >= deadline { + return Ok(false); + } + match self.page_state().await? { + PageState::CloudflareChallenge => { + tokio::time::sleep(interval).await; + } + _ => return Ok(true), + } + } + } + + // ── Page metadata ───────────────────────────────────────────────────────── + + /// Read the document title via stealth execution. + pub async fn title(&self) -> Result> { + let val = self.ignition.evaluate("document.title").await?; + Ok(val.and_then(|v| v.as_str().map(str::to_owned))) + } + + /// Read the current page URL as seen by the browser. + pub async fn current_url(&self) -> Result> { + self.ignition.url().await + } + + // ── Cookie helpers ──────────────────────────────────────────────────────── + + /// Read the `cf_clearance` cookie value if present. + /// + /// This cookie is set by Cloudflare after a successful challenge and is + /// required for subsequent requests to bypass the interstitial. + pub async fn cf_clearance_cookie(&self) -> Result> { + let val = self + .ignition + .evaluate( + "document.cookie.split(';').map(c=>c.trim())\ + .find(c=>c.startsWith('cf_clearance='))\ + ?.split('=').slice(1).join('=') ?? null", + ) + .await?; + Ok(val.and_then(|v| { + let s = v.as_str().map(str::to_owned)?; + if s.is_empty() { None } else { Some(s) } + })) + } + + // ── Human-like interaction helpers ──────────────────────────────────────── + + /// Scroll the page down by `pixels` with human-like easing. + pub async fn scroll_down(&self, pixels: i32) -> Result<()> { + self.ignition.scroll_human(pixels).await + } + + /// Scroll back to the top of the page. + pub async fn scroll_to_top(&self) -> Result<()> { + self.ignition.evaluate("window.scrollTo(0,0)").await?; + Ok(()) + } +}