pebbles 0.0.102

A unfinished webautomation framework for working with Firefox and the geckodriver.
Documentation
use crate::{
    result::{PebblesError, PebblesErrorDetails},
    service::Service,
    rect::Rect,
    error, 
    WebElement,
    raise,
};
use rand::Rng;
use serde_json::{json, Value};
use std::rc::Rc;

pub struct Mouse {
    x: i64,
    y: i64,
    service: Rc<Service>,
}

impl Mouse {
    pub fn new(service: Rc<Service>) -> Self {
        Mouse {
            x: 0,
            y: 0,
            service,
        }
    }

    pub fn down(&self, button: i64) -> Result<(), PebblesError> {
        let actions = json!({"actions": [
            {
                "type": "pointer",
                "id": "mouse",
                "parameters": {
                    "pointerType": "mouse"
                },
                "actions": [
                    {"type": "pointerDown", "duration": 0, "button": button}
                ]
            }
        ]});
        self.service.post("/actions", &actions)?;
        Ok(())
    }

    pub fn up(&self, button: i64) -> Result<(), PebblesError> {
        let actions = json!({"actions": [
            {
                "type": "pointer",
                "id": "mouse",
                "parameters": {
                    "pointerType": "mouse"
                },
                "actions": [
                    {"type": "pointerUp", "duration": 0, "button": button}
                ]
            }
        ]});
        self.service.post("/actions", &actions)?;
        Ok(())
    }

    pub fn click(&self, button: i64) -> Result<(), PebblesError> {
        let mut rng = rand::thread_rng();
        let actions = json!({"actions": [
            {
                "type": "pointer",
                "id": "mouse",
                "parameters": {
                    "pointerType": "mouse"
                },
                "actions": [
                    {"type": "pointerDown", "duration": 0, "button": button},
                    {"type": "pause", "duration": rng.gen_range(5..200)},
                    {"type": "pointerUp", "duration": 0, "button": button},
                ]
            }
        ]});
        self.service.post("/actions", &actions)?;
        Ok(())
    }

    pub fn hover(&mut self, webelement:&WebElement, mouse_acceleration_modifier: f64) -> Result<(), PebblesError> {
        let webelement_rect = webelement.rect()?;
        let view_rect = self.viewport_rect()?;
        if !webelement_rect.overlaps(&view_rect) {
            raise!(PebblesError::NotInViewportError, "Can not Move to Element as its not in the viewport.")
        }
        let (point_x, point_y) = webelement_rect.world_point()?;
        self.move_to(point_x - view_rect.x, point_y - view_rect.y, mouse_acceleration_modifier)?;
        Ok(())
    }

    pub fn move_to(&mut self, x: i64, y: i64, acceleration_modifier: f64) -> Result<(), PebblesError> {
        let viewport_rect = self.viewport_rect()?;
        let wind_fluctuation: f64 = 1.0;
        let gravitational_force: f64 = 2.0*(acceleration_modifier+1.0);
        let mut maximum_step = 50.0;

        let mut currentx = self.x;
        let mut currenty = self.y;

        let mut velocityx: f64 = 0.0;
        let mut velocityy: f64 = 0.0;
        let mut wind_forcex: f64 = 0.0;
        let mut wind_forcey: f64 = 0.0;

        let mut rng = rand::thread_rng();

        let mut actions: Value = json!({"actions": [
            {"type": "pointer", "id": "mouse", "parameters": {"pointerType": "mouse"}, "actions": []}
        ]});
        let mut distance = (((x - currentx).pow(2) + (y - currenty).pow(2)) as f64).sqrt();

        let damped_distance = (distance*0.05).clamp(50.0, 100.0);
        let wind_damp_modifier = 1.73205080757;
        let step_damp_modifier = 3.0;

        while distance >= 1.0 {
            // Calculate wind force
            let wind_magnitude = distance.min(wind_fluctuation);

            if distance >= damped_distance {
                wind_forcex =
                    wind_forcex / wind_damp_modifier + (rng.gen_range(0.0..2.0) - 1.0) * wind_magnitude / step_damp_modifier;
                wind_forcey =
                    wind_forcey / wind_damp_modifier + (rng.gen_range(0.0..2.0) - 1.0) * wind_magnitude / step_damp_modifier;
            } else {
                wind_forcex /= wind_damp_modifier;
                wind_forcey /= wind_damp_modifier;
                
                if maximum_step > 12.0 {
                    maximum_step /= step_damp_modifier;
                } else if maximum_step < 3.0 {
                    maximum_step = rng.gen_range(3.0..12.0);
                } else {
                    maximum_step /= 1.5
                }
            }

            // Calculate velocity
            velocityx += wind_forcex + gravitational_force * (x - currentx) as f64 / distance;
            velocityy += wind_forcey + gravitational_force * (y - currenty) as f64 / distance;
            let velocity_magnitude = (velocityx.powi(2) + velocityy.powi(2)).sqrt();
            if velocity_magnitude > maximum_step {
                let velocity_clip =
                    maximum_step / 2.0 + rng.gen_range(0.0..1.0) * maximum_step / 2.0;
                velocityx = (velocityx / velocity_magnitude) * velocity_clip;
                velocityy = (velocityy / velocity_magnitude) * velocity_clip;
            }

            // Update our Position
            if distance > maximum_step {
                currentx += velocityx as i64;
                currenty += velocityy as i64;
            } else {
                currentx = x;
                currenty = y;
            }

            // Create the Action
            let new_action = vec![json!({
                "type": "pointerMove",
                "duration": 10,
                "x": currentx.clamp(0, viewport_rect.width),
                "y": currenty.clamp(0, viewport_rect.height)
            })];

            if let Some(actions_array) = actions["actions"][0]["actions"].as_array_mut() {
                actions_array.extend(new_action);
            }

            // Update our Distance for the next loop
            distance = (((x - currentx).pow(2) + (y - currenty).pow(2)) as f64).sqrt();
        }
        self.service.post("/actions", &actions)?;
        self.x = x;
        self.y = y;
        Ok(())
    }

    pub fn scroll(&self, distance: f64, acceleration_modifier: f64) -> Result<(), PebblesError> {
        fn calculate_acceleration(distance: &f64, acceleration_modifier: f64) -> f64 {
            let normalized_distance = (*distance - 3500.0) / 1000.0;
            let acceleration = 1.2 + (normalized_distance.tanh() + 1.0) * 0.3;
            let random_offset = rand::thread_rng().gen_range(-0.2..0.2);
            (acceleration + random_offset).clamp(1.0, 4.0*(acceleration_modifier+0.25))
        }

        fn steps_to_target(target: i64, step_size: i64) -> i64 {
            let remainder = target % step_size;
            if remainder == 0 {
                0
            } else {
                let lower: i64 = target - remainder;
                let upper: i64 = target - remainder + step_size;
                if (target - lower).abs() <= (target - upper).abs() {
                    lower / step_size
                } else {
                    upper / step_size
                }
            }
        }

        let step_size: i64 = 114;
        if distance.abs() < (step_size/2) as f64 {
            return Ok(());
        }
        let duration_modifier = ((acceleration_modifier*7.0)+1.0) as i64;
        let multiplier: i64 = if distance > 0.0 { 1 } else { -1 };
        let steps: i64 = steps_to_target(distance.abs() as i64, step_size);

        let delta: i64 = step_size * multiplier;
        let acceleration: f64 = calculate_acceleration(&distance, acceleration_modifier);
        let mut duration: i64 = 7000/duration_modifier;
        let mut actions: Value = json!({"actions": [
            {"type": "wheel", "id": "wheel", "actions": []}
        ]});
        for _ in 0..steps {
            duration = ((duration as f64 / acceleration) as i64).clamp(10, 200);
            let new_actions = [
                json!({
                    "type": "scroll",
                    "x": 0,
                    "y": 0,
                    "deltaX": 0,
                    "deltaY": delta,
                    "duration": duration,
                    "origin": "viewport",
                }),
                json!({
                    "type": "pause",
                    "duration": duration / duration_modifier,
                }),
            ];

            if let Some(actions_array) = actions["actions"][0]["actions"].as_array_mut() {
                actions_array.extend(new_actions);
            }
        }
        self.service.post("/actions", &actions)?;
        Ok(())
    }

    fn viewport_rect(&self) -> Result<Rect, PebblesError> {
        fn get_value_as_i64(json_value: &Value, key: &str) -> Result<i64, PebblesError> {
            Ok(json_value.get(key)
                .ok_or_else(|| error!(PebblesError::PhrasingError, "Invalid Javascript Response could not get Window Size from WebDriver.", json_value.clone()))?
                .as_f64()
                .ok_or_else(|| error!(PebblesError::PhrasingError, "Invalid Javascript Response could not get Window Size from WebDriver.", json_value.clone()))? as i64
            )
        }
        let response = self
            .service
            .post(
                "/execute/sync", 
                &json!({"script": "return {\"x\": window.pageXOffset, \"y\": window.pageYOffset, \"width\": window.innerWidth, \"height\": window.innerHeight};", "args": []})
            )?.json::<Value>()?
            .get("value")
            .ok_or_else(|| error!(PebblesError::PhrasingError, "Could not get window size from javascript response."))?.clone();

        let window_x: i64 = get_value_as_i64(&response, "x")?;
        let window_y: i64 = get_value_as_i64(&response, "y")?;
        let window_width: i64 = get_value_as_i64(&response, "width")?;
        let window_height: i64 = get_value_as_i64(&response, "height")?;
        Ok(
            Rect{
                x:window_x,
                y:window_y,
                width:window_width,
                height:window_height,
            }
        )
    }
}