thirtyfour_mouse/
lib.rs

1use async_trait::async_trait;
2use enterpolation::bezier::Bezier;
3use enterpolation::bspline::BSpline;
4use enterpolation::{easing, linear::Linear, Curve};
5use rand::{rng, Rng};
6use std::ops::Div;
7use std::time::Duration;
8use thirtyfour::action_chain::ActionChain;
9use thirtyfour::error::{WebDriverError, WebDriverResult};
10use thirtyfour::{WebDriver, WebElement};
11
12#[derive(Default, Debug, Clone)]
13pub struct MouseAction {
14    interpolation: MouseInterpolation,
15    start_action: MouseButtonAction,
16    end_action: MouseButtonAction,
17    duration: Duration,
18    jitter_amount: i64,
19}
20
21#[derive(Default, Debug, Clone)]
22pub enum MouseButtonAction {
23    #[default]
24    None,
25    LeftClick,
26    LeftHold,
27    LeftRelease,
28    RightClick,
29}
30
31#[derive(Default, Debug, Clone)]
32pub enum MouseInterpolation {
33    #[default]
34    Linear,
35    Spline,
36}
37
38impl MouseAction {
39    pub fn new(
40        interpolation: MouseInterpolation,
41        start_action: MouseButtonAction,
42        end_action: MouseButtonAction,
43        duration: Option<Duration>,
44        jitter_amount: Option<i64>,
45    ) -> Self {
46        let jitter_amount = jitter_amount.unwrap_or(0);
47        let mut duration = duration.unwrap_or(Duration::from_millis(500));
48
49        // Each Action takes between 5-9ms with it averaging out to 7ms
50        let divider: u32 = 7;
51        if duration.as_millis() <= divider as u128 {
52            duration = Duration::from_millis(1);
53        } else {
54            duration = duration.div(divider);
55        }
56
57        MouseAction {
58            interpolation,
59            start_action,
60            end_action,
61            duration,
62            jitter_amount,
63        }
64    }
65}
66
67pub enum MouseTarget<'a> {
68    Position { x: f64, y: f64 },
69    WebElement(&'a WebElement),
70}
71
72#[async_trait]
73pub trait MouseActionExt {
74    async fn mouse_action<'a>(
75        &self,
76        action: MouseAction,
77        target_element: MouseTarget<'a>,
78    ) -> WebDriverResult<()>;
79}
80
81#[async_trait]
82impl MouseActionExt for WebDriver {
83    /// Simulate mouse movement across a path over a duration
84    ///
85    /// Note: There is no guarantee the duration is exact, but should be close
86    async fn mouse_action<'a>(
87        &self,
88        action: MouseAction,
89        target_element: MouseTarget<'a>,
90    ) -> WebDriverResult<()> {
91        let mouse_x_ret = self
92            .execute(r#"return window.tf_m_mouse_x || -1;"#, Vec::new())
93            .await?;
94        let mut mouse_x = mouse_x_ret.convert::<i64>()?;
95
96        let mouse_y_ret = self
97            .execute(r#"return window.tf_m_mouse_y || -1;"#, Vec::new())
98            .await?;
99        let mut mouse_y = mouse_y_ret.convert::<i64>()?;
100
101        if mouse_x <= -1 || mouse_y <= -1 {
102            self.execute(
103                r#"
104                window.tf_m_mouse_x = window.tf_m_mouse_x || -1;
105                window.tf_m_mouse_y = window.tf_m_mouse_y || -1;
106
107                document.addEventListener("mousemove", (event) => {
108                   window.tf_m_mouse_x = event.clientX;
109                   window.tf_m_mouse_y = event.clientY;
110                });"#,
111                Vec::new(),
112            )
113            .await?;
114
115            self.action_chain().move_by_offset(1, 1).perform().await?;
116
117            let mouse_x_ret = self
118                .execute(r#"return window.tf_m_mouse_x || -1;"#, Vec::new())
119                .await?;
120            mouse_x = mouse_x_ret.convert::<i64>()?;
121
122            let mouse_y_ret = self
123                .execute(r#"return window.tf_m_mouse_y || -1;"#, Vec::new())
124                .await?;
125            mouse_y = mouse_y_ret.convert::<i64>()?;
126
127            if mouse_x <= -1 || mouse_y <= -1 {
128                return Err(WebDriverError::CommandRecvError(
129                    "Failed to get mouse position".to_string(),
130                ));
131            }
132        }
133
134        let (pos_x, pos_y, width, height) = match target_element {
135            MouseTarget::Position { x, y } => (x, y, 0.00, 0.00),
136            MouseTarget::WebElement(we) => {
137                let rect = we.rect().await?;
138
139                (rect.x, rect.y, rect.width, rect.height)
140            }
141        };
142
143        let half_width = div(width, 2.00) as i64;
144        let half_height = div(height, 2.00) as i64;
145        let target_pos_x = pos_x as i64 + half_width; // Middle of element
146        let target_pos_y = pos_y as i64 + half_height; // Middle of element
147
148        let quarter_width = half_width.checked_div(2).unwrap_or(0);
149        let quarter_height = half_height.checked_div(2).unwrap_or(0);
150        let final_pos_x = target_pos_x + rng().random_range(-quarter_width..=quarter_width);
151        let final_pos_y = target_pos_y + rng().random_range(-quarter_height..=quarter_height);
152
153        let mut positions = match &action.interpolation {
154            MouseInterpolation::Linear => create_linear_steps(
155                mouse_x,
156                mouse_y,
157                final_pos_x,
158                final_pos_y,
159                action.duration.as_millis() as usize,
160            ),
161            MouseInterpolation::Spline => create_spline_steps(
162                mouse_x,
163                mouse_y,
164                final_pos_x,
165                final_pos_y,
166                action.duration.as_millis() as usize,
167            ),
168        };
169
170        if action.jitter_amount > 0 {
171            jitter(&mut positions, action.jitter_amount);
172        }
173
174        let action_chain = self.action_chain_with_delay(None, Some(Duration::ZERO));
175        let mut action_chain = action.start_action.action(action_chain);
176
177        for point in positions {
178            action_chain = action_chain.move_to(point.0, point.1);
179        }
180
181        action.end_action.action(action_chain).perform().await?;
182
183        Ok(())
184    }
185}
186
187impl MouseButtonAction {
188    fn action(&self, action_chain: ActionChain) -> ActionChain {
189        match self {
190            MouseButtonAction::None => action_chain,
191            MouseButtonAction::LeftClick => action_chain.click(),
192            MouseButtonAction::LeftHold => action_chain.click_and_hold(),
193            MouseButtonAction::LeftRelease => action_chain.release(),
194            MouseButtonAction::RightClick => action_chain.context_click(),
195        }
196    }
197}
198
199fn jitter(input: &mut [(i64, i64)], amount: i64) {
200    input.iter_mut().for_each(|(x, y)| {
201        let add_jitter = rng().random_bool(1.00 / 5.00);
202        if add_jitter {
203            *x += rng().random_range(-amount..=amount);
204            *y += rng().random_range(-amount..=amount);
205        }
206    })
207}
208
209fn create_spline_steps(
210    start_x: i64,
211    start_y: i64,
212    end_x: i64,
213    end_y: i64,
214    steps: usize,
215) -> Vec<(i64, i64)> {
216    let x_min = start_x.min(end_x);
217    let x_max = start_x.max(end_x);
218    let y_min = start_y.min(end_y);
219    let y_max = start_y.max(end_y);
220
221    let mut rng = rng();
222    let x_offset_one = rng.random_range(x_min..x_max);
223    let y_offset_one = rng.random_range(y_min..y_max);
224
225    let linear_x = Linear::builder()
226        .elements([start_x as f64, x_offset_one as f64, end_x as f64])
227        .equidistant()
228        .normalized()
229        .easing(easing::Plateau::new(0.00))
230        .build()
231        .unwrap();
232
233    let bezier_y = Bezier::builder()
234        .elements([start_y as f64, y_offset_one as f64, end_y as f64])
235        .normalized::<f64>()
236        .constant::<3>()
237        .build()
238        .unwrap();
239
240    let bspline_y = BSpline::builder()
241        .clamped()
242        .elements([start_y as f64, y_offset_one as f64, end_y as f64])
243        .knots(bezier_y.domain())
244        .dynamic()
245        .build()
246        .unwrap();
247
248    linear_x
249        .take(steps)
250        .zip(bspline_y.take(steps))
251        .map(|(mut x, mut y)| {
252            if x.is_sign_negative() {
253                x = 0.00;
254            }
255            if y.is_sign_negative() {
256                y = 0.00;
257            }
258            (x as i64, y as i64)
259        })
260        .collect::<Vec<_>>()
261}
262
263fn create_linear_steps(
264    start_x: i64,
265    start_y: i64,
266    end_x: i64,
267    end_y: i64,
268    steps: usize,
269) -> Vec<(i64, i64)> {
270    let linear_x = Linear::builder()
271        .elements([start_x as f64, end_x as f64])
272        .equidistant::<f64>()
273        .normalized()
274        .easing(easing::Plateau::new(0.1))
275        .build()
276        .unwrap();
277
278    let linear_y = Linear::builder()
279        .elements([start_y as f64, end_y as f64])
280        .equidistant::<f64>()
281        .normalized()
282        .easing(easing::Plateau::new(0.1))
283        .build()
284        .unwrap();
285
286    linear_x
287        .take(steps)
288        .zip(linear_y.take(steps))
289        .map(|(mut x, mut y)| {
290            if x.is_sign_negative() {
291                x = 0.00;
292            }
293            if y.is_sign_negative() {
294                y = 0.00;
295            }
296            (x as i64, y as i64)
297        })
298        .collect::<Vec<_>>()
299}
300
301fn div(lhs: f64, rhs: f64) -> f64 {
302    if rhs == 0.00 || lhs == 0.00 {
303        // Division by 0
304        return 0.00;
305    }
306    lhs / rhs
307}