use crate::utils::trie::Trie;
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WaitForIdleNetwork {
pub timeout: Option<core::time::Duration>,
}
impl WaitForIdleNetwork {
pub fn new(timeout: Option<core::time::Duration>) -> Self {
Self { timeout }
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WaitForSelector {
pub timeout: Option<core::time::Duration>,
pub selector: String,
}
impl WaitForSelector {
pub fn new(timeout: Option<core::time::Duration>, selector: String) -> Self {
Self { timeout, selector }
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WaitForDelay {
pub timeout: Option<core::time::Duration>,
}
impl WaitForDelay {
pub fn new(timeout: Option<core::time::Duration>) -> Self {
Self { timeout }
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WaitFor {
pub selector: Option<WaitForSelector>,
pub idle_network: Option<WaitForIdleNetwork>,
pub delay: Option<WaitForDelay>,
#[cfg_attr(feature = "serde", serde(default))]
pub page_navigations: bool,
}
impl WaitFor {
pub fn new(
timeout: Option<core::time::Duration>,
delay: Option<WaitForDelay>,
page_navigations: bool,
idle_network: bool,
selector: Option<String>,
) -> Self {
Self {
page_navigations,
idle_network: if idle_network {
Some(WaitForIdleNetwork::new(timeout))
} else {
None
},
selector: if selector.is_some() {
Some(WaitForSelector::new(timeout, selector.unwrap_or_default()))
} else {
None
},
delay,
}
}
}
#[derive(
Debug, Clone, PartialEq, Eq, Hash, Default, strum::EnumString, strum::Display, strum::AsRefStr,
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CaptureScreenshotFormat {
#[cfg_attr(feature = "serde", serde(rename = "jpeg"))]
Jpeg,
#[cfg_attr(feature = "serde", serde(rename = "png"))]
#[default]
Png,
#[cfg_attr(feature = "serde", serde(rename = "webp"))]
Webp,
}
impl CaptureScreenshotFormat {
pub fn to_string(&self) -> String {
self.as_ref().to_lowercase()
}
}
#[cfg(feature = "chrome")]
impl From<CaptureScreenshotFormat>
for chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat
{
fn from(format: CaptureScreenshotFormat) -> Self {
match format {
CaptureScreenshotFormat::Jpeg => {
chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat::Jpeg
}
CaptureScreenshotFormat::Png => {
chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat::Png
}
CaptureScreenshotFormat::Webp => {
chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat::Webp
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Viewport {
pub width: u32,
pub height: u32,
pub device_scale_factor: Option<f64>,
pub emulating_mobile: bool,
pub is_landscape: bool,
pub has_touch: bool,
}
impl Default for Viewport {
fn default() -> Self {
Viewport {
width: 800,
height: 600,
device_scale_factor: None,
emulating_mobile: false,
is_landscape: false,
has_touch: false,
}
}
}
impl Viewport {
pub fn new(width: u32, height: u32) -> Self {
Viewport {
width,
height,
..Default::default()
}
}
pub fn set_mobile(&mut self, emulating_mobile: bool) {
self.emulating_mobile = emulating_mobile;
}
pub fn set_landscape(&mut self, is_landscape: bool) {
self.is_landscape = is_landscape;
}
pub fn set_touch(&mut self, has_touch: bool) {
self.has_touch = has_touch;
}
pub fn set_scale_factor(&mut self, device_scale_factor: Option<f64>) {
self.device_scale_factor = device_scale_factor;
}
}
#[cfg(feature = "chrome")]
impl From<Viewport> for chromiumoxide::handler::viewport::Viewport {
fn from(viewport: Viewport) -> Self {
Self {
width: viewport.width,
height: viewport.height,
device_scale_factor: viewport.device_scale_factor,
emulating_mobile: viewport.emulating_mobile,
is_landscape: viewport.is_landscape,
has_touch: viewport.has_touch,
}
}
}
#[doc = "Capture page screenshot.\n[captureScreenshot](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot)"]
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CaptureScreenshotParams {
#[doc = "Image compression format (defaults to png)."]
pub format: Option<CaptureScreenshotFormat>,
#[doc = "Compression quality from range [0..100] (jpeg only)."]
pub quality: Option<i64>,
#[doc = "Capture the screenshot of a given region only."]
pub clip: Option<ClipViewport>,
#[doc = "Capture the screenshot from the surface, rather than the view. Defaults to true."]
pub from_surface: Option<bool>,
#[doc = "Capture the screenshot beyond the viewport. Defaults to false."]
pub capture_beyond_viewport: Option<bool>,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ClipViewport {
#[doc = "X offset in device independent pixels (dip)."]
#[cfg_attr(feature = "serde", serde(rename = "x"))]
pub x: f64,
#[doc = "Y offset in device independent pixels (dip)."]
#[cfg_attr(feature = "serde", serde(rename = "y"))]
pub y: f64,
#[doc = "Rectangle width in device independent pixels (dip)."]
#[cfg_attr(feature = "serde", serde(rename = "width"))]
pub width: f64,
#[doc = "Rectangle height in device independent pixels (dip)."]
#[cfg_attr(feature = "serde", serde(rename = "height"))]
pub height: f64,
#[doc = "Page scale factor."]
#[cfg_attr(feature = "serde", serde(rename = "scale"))]
pub scale: f64,
}
#[cfg(feature = "chrome")]
impl From<ClipViewport> for chromiumoxide::cdp::browser_protocol::page::Viewport {
fn from(viewport: ClipViewport) -> Self {
Self {
x: viewport.x,
y: viewport.y,
height: viewport.height,
width: viewport.width,
scale: viewport.scale,
}
}
}
#[derive(Debug, Default, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScreenShotConfig {
pub params: ScreenshotParams,
pub bytes: bool,
pub save: bool,
pub output_dir: Option<std::path::PathBuf>,
}
impl ScreenShotConfig {
pub fn new(
params: ScreenshotParams,
bytes: bool,
save: bool,
output_dir: Option<std::path::PathBuf>,
) -> Self {
Self {
params,
bytes,
save,
output_dir,
}
}
}
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScreenshotParams {
pub cdp_params: CaptureScreenshotParams,
pub full_page: Option<bool>,
pub omit_background: Option<bool>,
}
impl ScreenshotParams {
pub fn new(
cdp_params: CaptureScreenshotParams,
full_page: Option<bool>,
omit_background: Option<bool>,
) -> Self {
Self {
cdp_params,
full_page,
omit_background,
}
}
}
#[cfg(feature = "chrome")]
impl From<ScreenshotParams> for chromiumoxide::page::ScreenshotParams {
fn from(params: ScreenshotParams) -> Self {
let full_page = if params.full_page.is_some() {
match params.full_page {
Some(v) => v,
_ => false,
}
} else {
match std::env::var("SCREENSHOT_FULL_PAGE") {
Ok(t) => t == "true",
_ => true,
}
};
let omit_background = if params.omit_background.is_some() {
match params.omit_background {
Some(v) => v,
_ => false,
}
} else {
match std::env::var("SCREENSHOT_OMIT_BACKGROUND") {
Ok(t) => t == "true",
_ => true,
}
};
let format = if params.cdp_params.format.is_some() {
match params.cdp_params.format {
Some(v) => v.into(),
_ => chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat::Png,
}
} else {
chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat::Png
};
let params_builder = chromiumoxide::page::ScreenshotParams::builder()
.format(format)
.full_page(full_page)
.omit_background(omit_background);
let params_builder = if params.cdp_params.quality.is_some() {
params_builder.quality(params.cdp_params.quality.unwrap_or(75))
} else {
params_builder
};
let params_builder = if params.cdp_params.clip.is_some() {
match params.cdp_params.clip {
Some(vp) => params_builder.clip(
chromiumoxide::cdp::browser_protocol::page::Viewport::from(vp),
),
_ => params_builder,
}
} else {
params_builder
};
let params_builder = if params.cdp_params.capture_beyond_viewport.is_some() {
match params.cdp_params.capture_beyond_viewport {
Some(capture_beyond_viewport) => {
params_builder.capture_beyond_viewport(capture_beyond_viewport)
}
_ => params_builder,
}
} else {
params_builder
};
let params_builder = if params.cdp_params.from_surface.is_some() {
match params.cdp_params.from_surface {
Some(from_surface) => params_builder.from_surface(from_surface),
_ => params_builder,
}
} else {
params_builder
};
params_builder.build()
}
}
#[doc = "The decision on what to do in response to the authorization challenge. Default means\ndeferring to the default behavior of the net stack, which will likely either the Cancel\nauthentication or display a popup dialog box."]
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum AuthChallengeResponseResponse {
#[default]
Default,
CancelAuth,
ProvideCredentials,
}
#[doc = "Response to an AuthChallenge.\n[AuthChallengeResponse](https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#type-AuthChallengeResponse)"]
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AuthChallengeResponse {
#[doc = "The decision on what to do in response to the authorization challenge. Default means\ndeferring to the default behavior of the net stack, which will likely either the Cancel\nauthentication or display a popup dialog box."]
pub response: AuthChallengeResponseResponse,
#[doc = "The username to provide, possibly empty. Should only be set if response is\nProvideCredentials."]
pub username: Option<String>,
#[doc = "The password to provide, possibly empty. Should only be set if response is\nProvideCredentials."]
pub password: Option<String>,
}
#[cfg(feature = "chrome")]
impl From<AuthChallengeResponse>
for chromiumoxide::cdp::browser_protocol::fetch::AuthChallengeResponse
{
fn from(auth_challenge_response: AuthChallengeResponse) -> Self {
Self {
response: match auth_challenge_response.response {
AuthChallengeResponseResponse::CancelAuth => chromiumoxide::cdp::browser_protocol::fetch::AuthChallengeResponseResponse::CancelAuth ,
AuthChallengeResponseResponse::ProvideCredentials => chromiumoxide::cdp::browser_protocol::fetch::AuthChallengeResponseResponse::ProvideCredentials ,
AuthChallengeResponseResponse::Default => chromiumoxide::cdp::browser_protocol::fetch::AuthChallengeResponseResponse::Default ,
},
username: auth_challenge_response.username,
password: auth_challenge_response.password
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum WebAutomation {
Evaluate(String),
Click(String),
Wait(u64),
WaitForNavigation,
WaitFor(String),
WaitForAndClick(String),
ScrollX(i32),
ScrollY(i32),
Fill {
selector: String,
value: String,
},
InfiniteScroll(u32),
Screenshot {
full_page: bool,
omit_background: bool,
output: String,
},
}
impl WebAutomation {
#[cfg(feature = "chrome")]
pub async fn run(&self, page: &chromiumoxide::Page) {
use crate::utils::wait_for_selector;
use chromiumoxide::cdp::browser_protocol::dom::Rect;
use chromiumoxide::cdp::browser_protocol::dom::ScrollIntoViewIfNeededParams;
use std::time::Duration;
match self {
WebAutomation::Evaluate(js) => {
let _ = page.evaluate(js.as_str()).await;
}
WebAutomation::Click(selector) => match page.find_element(selector).await {
Ok(ele) => {
let _ = ele.click().await;
}
_ => (),
},
WebAutomation::Wait(ms) => {
tokio::time::sleep(Duration::from_millis(*ms).min(Duration::from_secs(60))).await;
}
WebAutomation::WaitFor(selector) => {
wait_for_selector(page, Some(Duration::from_secs(60)), &selector).await;
}
WebAutomation::WaitForNavigation => {
let _ = page.wait_for_navigation().await;
}
WebAutomation::WaitForAndClick(selector) => {
wait_for_selector(page, Some(Duration::from_secs(60)), &selector).await;
match page.find_element(selector).await {
Ok(ele) => {
let _ = ele.click().await;
}
_ => (),
}
}
WebAutomation::ScrollX(px) => {
let mut cmd = ScrollIntoViewIfNeededParams::builder().build();
let rect = Rect::new(*px, 0.0, 10.0, 10.0);
cmd.rect = Some(rect);
let _ = page.execute(cmd);
}
WebAutomation::ScrollY(px) => {
let mut cmd = ScrollIntoViewIfNeededParams::builder().build();
let rect = Rect::new(0.0, *px, 10.0, 10.0);
cmd.rect = Some(rect);
let _ = page.execute(cmd);
}
WebAutomation::Fill { selector, value } => match page.find_element(selector).await {
Ok(ele) => match ele.click().await {
Ok(el) => {
let _ = el.type_str(value).await;
}
_ => (),
},
_ => (),
},
WebAutomation::InfiniteScroll(duration) => {
let _ = page.evaluate(set_dynamic_scroll(*duration)).await;
}
WebAutomation::Screenshot {
full_page,
omit_background,
output,
} => {
let mut cdp_params: CaptureScreenshotParams = CaptureScreenshotParams::default();
cdp_params.format = Some(CaptureScreenshotFormat::Png);
let screenshot_params =
ScreenshotParams::new(cdp_params, Some(*full_page), Some(*omit_background));
let _ = page.save_screenshot(screenshot_params, output).await;
}
}
}
}
pub fn set_dynamic_scroll(timeout: u32) -> String {
let timeout = timeout.min(60000);
let s = string_concat!(
r###"
document.addEventListener('DOMContentLoaded',e=>{let t=null,o=null,n="###,
timeout.to_string(),
r###",a=Date.now(),i=Date.now(),r=()=>{window.scrollTo(0,document.body.scrollHeight)},l=()=>{o&&o.disconnect(),console.log('Stopped checking for new content.')},c=(e,n)=>{e.forEach(e=>{if(e.isIntersecting){i=Date.now();const n=Date.now();if(n-a>=t||n-i>=1e4)return void l();r(),t=document.querySelector('body > *:last-child'),o.observe(t)}})},s=()=>{t&&(o=new IntersectionObserver(c),o.observe(t))},d=()=>{['load','error','abort'].forEach(e=>{window.addEventListener(e,()=>{const e=document.querySelector('body > *:last-child');e!==t&&(i=Date.now(),t=e,o.observe(t))})})},u=()=>{r(),t=document.querySelector('body > *:last-child'),s(),d()};u(),setTimeout(l,n)});
"###
);
s
}
pub type ExecutionScriptsMap = hashbrown::HashMap<String, String>;
pub type AutomationScriptsMap = hashbrown::HashMap<String, Vec<WebAutomation>>;
pub type ExecutionScripts = Trie<String>;
pub type AutomationScripts = Trie<Vec<WebAutomation>>;
pub fn convert_to_trie_execution_scripts(
input: &Option<ExecutionScriptsMap>,
) -> Option<Trie<String>> {
match input {
Some(ref scripts) => {
let mut trie = Trie::new();
for (path, script) in scripts {
trie.insert(path, script.clone());
}
Some(trie)
}
None => None,
}
}
pub fn convert_to_trie_automation_scripts(
input: &Option<AutomationScriptsMap>,
) -> Option<Trie<Vec<WebAutomation>>> {
match input {
Some(ref scripts) => {
let mut trie = Trie::new();
for (path, script_list) in scripts {
trie.insert(path, script_list.clone());
}
Some(trie)
}
None => None,
}
}
#[cfg(feature = "chrome")]
pub async fn eval_execution_scripts(
page: &chromiumoxide::Page,
target_url: &str,
execution_scripts: &Option<ExecutionScripts>,
) {
match execution_scripts {
Some(ref scripts) => match scripts.search(target_url) {
Some(script) => {
let _ = page.evaluate(script.as_str()).await;
}
_ => (),
},
_ => (),
}
}
#[cfg(feature = "chrome")]
pub async fn eval_automation_scripts(
page: &chromiumoxide::Page,
target_url: &str,
automation_scripts: &Option<AutomationScripts>,
) {
if let Some(script_map) = automation_scripts {
if let Some(scripts) = script_map.search(target_url) {
for script in scripts {
let result =
tokio::time::timeout(tokio::time::Duration::from_secs(60), script.run(page))
.await;
match result {
Ok(_) => (),
Err(_) => {
crate::utils::log("Script execution timed out for - ", target_url);
}
}
}
}
}
}