pebbles 0.0.1

A unfinished webautomation framework for working with Firefox and the geckodriver.
Documentation
use crate::{
    result::{PebblesError, PebblesErrorDetails},
    Service,
};
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 move_to(&mut self, x: i64, y: i64, speed: f64) -> Result<(), PebblesError> {
        // the system I have for speed does not work but I am going to add it at some point so leaving it here.
        let viewport_width: i64 = match &self
            .service
            .post(
                "/execute/sync",
                &json!({"script": "return window.innerWidth;", "args": []}),
            )?
            .json::<Value>()?["value"]
            .as_i64()
        {
            Some(value) => value.clone() as i64,
            None => {
                return Err(PebblesError::PhrasingError(PebblesErrorDetails::new(
                    "Could not load page width for mouse movment.",
                )))
            }
        };
        let viewport_height: i64 = match &self
            .service
            .post(
                "/execute/sync",
                &json!({"script": "return window.innerHeight;", "args": []}),
            )?
            .json::<Value>()?["value"]
            .as_i64()
        {
            Some(value) => value.clone() as i64,
            None => {
                return Err(PebblesError::PhrasingError(PebblesErrorDetails::new(
                    "Could not load page height for mouse movment.",
                )))
            }
        };
        let wind_fluctuation: f64 = 3.0;
        let gravitational_force: f64 = 9.0;
        let damped_distance = 12.0;
        let mut maximum_step = 25.0;

        let sqrt3 = 1.73205080757;
        let sqrt5 = 2.2360679775;

        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();
        while distance >= 1.0 {
            // Calculate wind force
            let wind_magnitude = distance.min(wind_fluctuation);

            if distance >= damped_distance {
                wind_forcex =
                    wind_forcex / sqrt3 + (rng.gen_range(0.0..2.0) - 1.0) * wind_magnitude / sqrt5;
                wind_forcey =
                    wind_forcey / sqrt3 + (rng.gen_range(0.0..2.0) - 1.0) * wind_magnitude / sqrt5;
            } else {
                wind_forcex /= sqrt3;
                wind_forcey /= sqrt3;

                if maximum_step < 3.0 {
                    maximum_step = rng.gen_range(0.0..3.0) + 3.0;
                } else {
                    maximum_step /= sqrt5;
                }
            }

            // 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
            currentx += velocityx as i64;
            currenty += velocityy as i64;

            // Create the Action
            let new_action = vec![json!({
                "type": "pointerMove",
                "duration": 10,
                "x": currentx.clamp(0, viewport_width),
                "y": currenty.clamp(0, viewport_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) -> Result<(), PebblesError> {
        fn calculate_acceleration(distance: &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.1, 3.0)
        }

        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 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;
        println!("{} delta", delta);
        let acceleration: f64 = calculate_acceleration(&distance);
        let mut duration: i64 = 1000 / (distance.abs() as i64 / step_size);
        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 / 2,
                }),
            ];

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