chromate-rb 0.0.1.pre → 0.0.3.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/CHANGELOG.md +72 -3
  4. data/README.md +33 -6
  5. data/Rakefile +48 -16
  6. data/docker_root/Gemfile +4 -0
  7. data/docker_root/Gemfile.lock +28 -0
  8. data/docker_root/TestInDocker.gif +0 -0
  9. data/docker_root/app.rb +87 -0
  10. data/dockerfiles/Dockerfile +21 -7
  11. data/dockerfiles/README.md +49 -0
  12. data/docs/BOT_BROWSER.md +74 -0
  13. data/docs/README.md +74 -0
  14. data/docs/browser.md +124 -102
  15. data/docs/client.md +126 -0
  16. data/docs/element.md +365 -0
  17. data/docs/elements/checkbox.md +69 -0
  18. data/docs/elements/radio.md +57 -0
  19. data/lib/bot_browser/downloader.rb +64 -0
  20. data/lib/bot_browser/installer.rb +99 -0
  21. data/lib/bot_browser.rb +43 -0
  22. data/lib/chromate/actions/dom.rb +35 -27
  23. data/lib/chromate/actions/navigate.rb +7 -5
  24. data/lib/chromate/actions/screenshot.rb +71 -14
  25. data/lib/chromate/actions/stealth.rb +62 -0
  26. data/lib/chromate/binary.rb +83 -0
  27. data/lib/chromate/browser.rb +120 -24
  28. data/lib/chromate/c_logger.rb +8 -0
  29. data/lib/chromate/client.rb +65 -26
  30. data/lib/chromate/configuration.rb +31 -14
  31. data/lib/chromate/element.rb +119 -16
  32. data/lib/chromate/elements/checkbox.rb +40 -0
  33. data/lib/chromate/elements/option.rb +43 -0
  34. data/lib/chromate/elements/radio.rb +37 -0
  35. data/lib/chromate/elements/select.rb +50 -6
  36. data/lib/chromate/elements/tags.rb +29 -0
  37. data/lib/chromate/exceptions.rb +2 -0
  38. data/lib/chromate/files/agents.json +11 -0
  39. data/lib/chromate/files/stealth.js +199 -0
  40. data/lib/chromate/hardwares/keyboard_controller.rb +45 -0
  41. data/lib/chromate/hardwares/keyboards/virtual_controller.rb +65 -0
  42. data/lib/chromate/hardwares/mouse_controller.rb +55 -11
  43. data/lib/chromate/hardwares/mouses/linux_controller.rb +124 -21
  44. data/lib/chromate/hardwares/mouses/mac_os_controller.rb +6 -6
  45. data/lib/chromate/hardwares/mouses/virtual_controller.rb +95 -7
  46. data/lib/chromate/hardwares/mouses/x11.rb +36 -0
  47. data/lib/chromate/hardwares.rb +19 -3
  48. data/lib/chromate/helpers.rb +22 -15
  49. data/lib/chromate/user_agent.rb +41 -15
  50. data/lib/chromate/version.rb +1 -1
  51. data/lib/chromate.rb +2 -0
  52. data/logo.png +0 -0
  53. data/results/bot.png +0 -0
  54. data/results/brotector.png +0 -0
  55. data/results/cloudflare.png +0 -0
  56. data/results/headers.png +0 -0
  57. data/results/pixelscan.png +0 -0
  58. metadata +45 -2
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'chromate/element'
4
+
5
+ module Chromate
6
+ module Elements
7
+ module Tags
8
+ def select?
9
+ tag_name == 'select'
10
+ end
11
+
12
+ def option?
13
+ tag_name == 'option'
14
+ end
15
+
16
+ def radio?
17
+ tag_name == 'input' && attributes['type'] == 'radio'
18
+ end
19
+
20
+ def checkbox?
21
+ tag_name == 'input' && attributes['type'] == 'checkbox'
22
+ end
23
+
24
+ def base?
25
+ !select? && !option? && !radio? && !checkbox?
26
+ end
27
+ end
28
+ end
29
+ end
@@ -5,5 +5,7 @@ module Chromate
5
5
  class ChromateError < StandardError; end
6
6
  class InvalidBrowserError < ChromateError; end
7
7
  class InvalidPlatformError < ChromateError; end
8
+ class ConnectionTimeoutError < StandardError; end
9
+ class DebugURLError < StandardError; end
8
10
  end
9
11
  end
@@ -0,0 +1,11 @@
1
+ {
2
+ "windows": [
3
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
4
+ ],
5
+ "mac": [
6
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
7
+ ],
8
+ "linux": [
9
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
10
+ ]
11
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Script de Stealth pour masquer l'automatisation
3
+ *
4
+ * Ce script est destiné à être injecté via la commande
5
+ * Page.addScriptToEvaluateOnNewDocument afin qu'il s'exécute
6
+ * avant le chargement du contenu de la page.
7
+ *
8
+ * Il redéfinit plusieurs propriétés du navigateur pour
9
+ * réduire les indices susceptibles d'indiquer qu'une automatisation est en cours.
10
+ */
11
+ (() => {
12
+ 'use strict';
13
+
14
+ // 1) Masquer navigator.webdriver
15
+ (function removeWebDriverProperty() {
16
+ // 1) Récupérer le prototype de Navigator
17
+ const proto = Object.getPrototypeOf(navigator);
18
+
19
+ // Vérifier si la propriété webdriver existe sur le prototype
20
+ if ('webdriver' in proto) {
21
+ try {
22
+ // 2) Tenter de supprimer la propriété si elle est configurable
23
+ const webdriverDescriptor = Object.getOwnPropertyDescriptor(proto, 'webdriver');
24
+ if (webdriverDescriptor && webdriverDescriptor.configurable) {
25
+ delete proto.webdriver;
26
+ } else {
27
+ // 3) Sinon, on essaye de la redéfinir pour qu'elle retourne undefined et ne soit pas énumérable
28
+ Object.defineProperty(proto, 'webdriver', {
29
+ get: () => undefined,
30
+ configurable: false, // on la rend non-configurable pour éviter d'autres re-déclarations
31
+ enumerable: false
32
+ });
33
+ }
34
+ } catch (err) {
35
+ // 4) En cas d'échec (non-configurable), on peut tenter un hack sur l'opérateur 'in'
36
+ // => ATTENTION : ceci peut avoir des effets de bord dans d'autres scripts
37
+ patchInOperatorForNavigator('webdriver');
38
+ }
39
+ } else {
40
+ // Si la propriété n'existe pas sur le prototype, vérifier si elle existe directement sur navigator
41
+ if ('webdriver' in navigator) {
42
+ try {
43
+ delete navigator.webdriver;
44
+ } catch (err) {
45
+ patchInOperatorForNavigator('webdriver');
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Hack optionnel pour empêcher `'webdriver' in navigator` de renvoyer true.
52
+ * On redéfinit la méthode hasOwnProperty / l'opérateur in pour l'objet navigator.
53
+ * Cette approche peut avoir des effets de bord, donc à utiliser en dernier recours.
54
+ */
55
+ function patchInOperatorForNavigator(propName) {
56
+ const originalHasOwn = Object.prototype.hasOwnProperty;
57
+ Object.prototype.hasOwnProperty = function (property) {
58
+ // Si c'est navigator et qu'on teste la propriété 'webdriver', on la cache
59
+ if (this === navigator && property === propName) {
60
+ return false;
61
+ }
62
+ return originalHasOwn.call(this, property);
63
+ };
64
+
65
+ // Variante plus radicale : proxyfier l'objet navigator pour intercepter l'opérateur 'in'.
66
+ // (Non présenté ici, car encore plus invasif.)
67
+ }
68
+ })();
69
+
70
+ // 2) Surcharger navigator.permissions.query (pour ne pas renvoyer "default")
71
+ if (navigator.permissions && navigator.permissions.query) {
72
+ const originalQuery = navigator.permissions.query;
73
+ navigator.permissions.query = (params) => {
74
+ // Ex. : si on interroge les notifications, on renvoie "granted"
75
+ if (params.name === 'notifications') {
76
+ return Promise.resolve({ state: 'granted' });
77
+ }
78
+ // Pour les autres, on tente la requête d'origine, sinon "granted"
79
+ return originalQuery(params).catch(() => ({ state: 'granted' }));
80
+ };
81
+ }
82
+
83
+ // 3) Créer un PluginArray réaliste
84
+ // - On réutilise le prototype existant pour ne pas éveiller de soupçons
85
+ const pluginArrayProto = Object.getPrototypeOf(navigator.plugins);
86
+ function FakePlugin(name, description, filename) {
87
+ this.name = name;
88
+ this.description = description;
89
+ this.filename = filename;
90
+ }
91
+ // On pointe vers Plugin.prototype pour se comporter comme un plugin "réel"
92
+ FakePlugin.prototype = Plugin.prototype;
93
+
94
+ const fakePlugins = [
95
+ new FakePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer'),
96
+ new FakePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai')
97
+ ];
98
+
99
+ Object.defineProperty(navigator, 'plugins', {
100
+ get() {
101
+ const pluginArray = Object.create(pluginArrayProto);
102
+ // On copie les faux plugins dans l’objet
103
+ for (let i = 0; i < fakePlugins.length; i++) {
104
+ pluginArray[i] = fakePlugins[i];
105
+ }
106
+ pluginArray.length = fakePlugins.length;
107
+ return pluginArray;
108
+ }
109
+ });
110
+
111
+ // 4) Aligner navigator.languages avec vos entêtes Accept-Language
112
+ Object.defineProperty(navigator, 'languages', {
113
+ get: () => ['en-US', 'en'],
114
+ configurable: true
115
+ });
116
+
117
+ // 5) Override de getContext pour forcer un contexte WebGL factice
118
+ const originalGetContext = HTMLCanvasElement.prototype.getContext;
119
+ HTMLCanvasElement.prototype.getContext = function (type, ...args) {
120
+ if (['webgl', 'experimental-webgl', 'webgl2'].includes(type)) {
121
+ // Tenter d'obtenir le contexte natif (au cas où)
122
+ let ctx = originalGetContext.apply(this, [type, ...args]);
123
+ if (ctx) {
124
+ // Si un contexte natif est obtenu, retourner ce contexte
125
+ return ctx;
126
+ }
127
+ console.log("No native WebGL context found, returning fake context");
128
+ // Forcer la création d'un contexte factice
129
+ const proto = (window.WebGLRenderingContext && window.WebGLRenderingContext.prototype) || {};
130
+ const fakeContext = Object.create(proto);
131
+
132
+ fakeContext.getParameter = function (param) {
133
+ if (param === 37445) return 'Intel Inc.';
134
+ if (param === 37446) return 'Intel Iris OpenGL Engine';
135
+ return null;
136
+ };
137
+ fakeContext.getSupportedExtensions = function () {
138
+ return ['WEBGL_debug_renderer_info'];
139
+ };
140
+ fakeContext.getExtension = function (name) {
141
+ if (name === 'WEBGL_debug_renderer_info') {
142
+ return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
143
+ }
144
+ return null;
145
+ };
146
+
147
+ // Ajout de stubs pour d'autres méthodes WebGL essentielles
148
+ fakeContext.clear = function () { };
149
+ fakeContext.clearColor = function () { };
150
+ fakeContext.viewport = function () { };
151
+ fakeContext.createShader = function () { return {}; };
152
+ fakeContext.shaderSource = function () { };
153
+ fakeContext.compileShader = function () { };
154
+ fakeContext.createProgram = function () { return {}; };
155
+ fakeContext.attachShader = function () { };
156
+ fakeContext.linkProgram = function () { };
157
+ fakeContext.useProgram = function () { };
158
+
159
+ return fakeContext;
160
+ }
161
+ return originalGetContext.apply(this, [type, ...args]);
162
+ };
163
+
164
+ // 6) Optionnel : Override de toDataURL pour renvoyer une image fixe
165
+ const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
166
+ HTMLCanvasElement.prototype.toDataURL = function (...args) {
167
+ const gl = this.getContext('webgl') || this.getContext('experimental-webgl') || this.getContext('webgl2');
168
+ if (gl) {
169
+ return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII=";
170
+ }
171
+ return originalToDataURL.apply(this, args);
172
+ };
173
+
174
+ // 7) Correction des dimensions d’images
175
+ Object.defineProperty(HTMLImageElement.prototype, 'naturalWidth', {
176
+ get() { return 128; }
177
+ });
178
+ Object.defineProperty(HTMLImageElement.prototype, 'naturalHeight', {
179
+ get() { return 128; }
180
+ });
181
+
182
+ // 8) Randomisation pour varier l'injection
183
+ (function () {
184
+ const randomSuffix = Math.random().toString(36).substring(2);
185
+ document.documentElement.setAttribute('data-stealth', randomSuffix);
186
+ })();
187
+
188
+ // 9) Masquer la modification des fonctions natives
189
+ (function () {
190
+ const originalToString = Function.prototype.toString;
191
+ Function.prototype.toString = function () {
192
+ if (this === navigator.permissions.query) {
193
+ return "function query() { [native code] }";
194
+ }
195
+ return originalToString.apply(this, arguments);
196
+ };
197
+ })();
198
+
199
+ })();
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chromate
4
+ module Hardwares
5
+ class KeyboardController
6
+ attr_accessor :element, :client
7
+
8
+ # @param [Chromate::Element] element
9
+ # @param [Chromate::Client] client
10
+ def initialize(element: nil, client: nil)
11
+ @element = element
12
+ @client = client
13
+ @type_interval = rand(0.05..0.1)
14
+ end
15
+
16
+ # @param [Chromate::Element] element
17
+ # @return [self]
18
+ def set_element(element) # rubocop:disable Naming/AccessorMethodName
19
+ @element = element
20
+ @type_interval = rand(0.05..0.1)
21
+
22
+ self
23
+ end
24
+
25
+ # @param [String] key
26
+ # @return [self]
27
+ def press_key(_key)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ # @param [String] text
32
+ # @return [self]
33
+ def type(text)
34
+ text.each_char do |char|
35
+ sleep(rand(0.01..0.05)) if rand(10).zero?
36
+
37
+ press_key(char)
38
+ sleep(@type_interval)
39
+ end
40
+
41
+ self
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chromate
4
+ module Hardwares
5
+ module Keyboards
6
+ class VirtualController < Chromate::Hardwares::KeyboardController
7
+ def press_key(key = 'Enter')
8
+ params = {
9
+ key: key,
10
+ code: key_to_code(key),
11
+ windowsVirtualKeyCode: key_to_virtual_code(key)
12
+ }
13
+
14
+ params[:text] = key if key.length == 1
15
+
16
+ # Dispatch keyDown event
17
+ client.send_message('Input.dispatchKeyEvent', params.merge(type: 'keyDown'))
18
+
19
+ # Dispatch keyUp event
20
+ client.send_message('Input.dispatchKeyEvent', params.merge(type: 'keyUp'))
21
+
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ # @param [String] key
28
+ # @return [String]
29
+ def key_to_code(key)
30
+ case key
31
+ when 'Enter' then 'Enter'
32
+ when 'Tab' then 'Tab'
33
+ when 'Backspace' then 'Backspace'
34
+ when 'Delete' then 'Delete'
35
+ when 'Escape' then 'Escape'
36
+ when 'ArrowLeft' then 'ArrowLeft'
37
+ when 'ArrowRight' then 'ArrowRight'
38
+ when 'ArrowUp' then 'ArrowUp'
39
+ when 'ArrowDown' then 'ArrowDown'
40
+ else
41
+ "Key#{key.upcase}"
42
+ end
43
+ end
44
+
45
+ # @param [String] key
46
+ # @return [Integer]
47
+ def key_to_virtual_code(key)
48
+ case key
49
+ when 'Enter' then 0x0D
50
+ when 'Tab' then 0x09
51
+ when 'Backspace' then 0x08
52
+ when 'Delete' then 0x2E
53
+ when 'Escape' then 0x1B
54
+ when 'ArrowLeft' then 0x25
55
+ when 'ArrowRight' then 0x27
56
+ when 'ArrowUp' then 0x26
57
+ when 'ArrowDown' then 0x28
58
+ else
59
+ key.upcase.ord
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -6,62 +6,106 @@ module Chromate
6
6
  CLICK_DURATION_RANGE = (0.01..0.1)
7
7
  DOUBLE_CLICK_DURATION_RANGE = (0.1..0.5)
8
8
 
9
- attr_accessor :element, :client, :mouse_position
9
+ def self.reset_mouse_position
10
+ @@mouse_position = { x: 0, y: 0 } # rubocop:disable Style/ClassVars
11
+ end
12
+
13
+ attr_accessor :element, :client
10
14
 
11
15
  # @param [Chromate::Element] element
12
16
  # @param [Chromate::Client] client
13
17
  def initialize(element: nil, client: nil)
14
18
  @element = element
15
19
  @client = client
16
- @mouse_position = { x: 0, y: 0 }
17
20
  end
18
21
 
22
+ # @param [Chromate::Element] element
23
+ # @return [self]
24
+ def set_element(element) # rubocop:disable Naming/AccessorMethodName
25
+ @element = element
26
+
27
+ self
28
+ end
29
+
30
+ # @return [Hash]
31
+ def mouse_position
32
+ @@mouse_position ||= { x: 0, y: 0 } # rubocop:disable Style/ClassVars
33
+ end
34
+
35
+ # @return [self]
19
36
  def hover
20
37
  raise NotImplementedError
21
38
  end
22
39
 
40
+ # @return [self]
23
41
  def click
24
42
  raise NotImplementedError
25
43
  end
26
44
 
45
+ # @return [self]
27
46
  def double_click
28
47
  raise NotImplementedError
29
48
  end
30
49
 
50
+ # @return [self]
31
51
  def right_click
32
52
  raise NotImplementedError
33
53
  end
34
54
 
55
+ # @params [Chromate::Element] element
56
+ # @return [self]
57
+ def drag_and_drop_to(element)
58
+ raise NotImplementedError
59
+ end
60
+
61
+ # @return [Integer]
35
62
  def position_x
36
63
  mouse_position[:x]
37
64
  end
38
65
 
66
+ # @return [Integer]
39
67
  def position_y
40
68
  mouse_position[:y]
41
69
  end
42
70
 
43
71
  private
44
72
 
73
+ # @return [Integer]
45
74
  def target_x
46
75
  element.x + (element.width / 2)
47
76
  end
48
77
 
78
+ # @return [Integer]
49
79
  def target_y
50
80
  element.y + (element.height / 2)
51
81
  end
52
82
 
53
- def bezier_curve(steps: 50) # rubocop:disable Metrics/AbcSize
54
- control_x = (target_x / 2)
55
- control_y = (target_y / 2)
83
+ # @param [Integer] steps
84
+ # @return [Array<Hash>]
85
+ def bezier_curve(steps:, start_x: position_x, start_y: position_y, t_x: target_x, t_y: target_y) # rubocop:disable Metrics/AbcSize
86
+ # Points for the Bézier curve
87
+ control_x1 = start_x + (rand(50..150) * (t_x > start_x ? 1 : -1))
88
+ control_y1 = start_y + (rand(50..150) * (t_y > start_y ? 1 : -1))
89
+ control_x2 = t_x + (rand(50..150) * (t_x > start_x ? -1 : 1))
90
+ control_y2 = t_y + (rand(50..150) * (t_y > start_y ? -1 : 1))
56
91
 
57
- (0..steps).map do |t|
58
- t /= steps.to_f
59
- # Compute the position on the quadratic Bezier curve
60
- new_x = (((1 - t)**2) * position_x) + (2 * (1 - t) * t * control_x) + ((t**2) * target_x)
61
- new_y = (((1 - t)**2) * position_y) + (2 * (1 - t) * t * control_y) + ((t**2) * target_y)
62
- { x: new_x, y: new_y }
92
+ (0..steps).map do |i|
93
+ t = i.to_f / steps
94
+ x = (((1 - t)**3) * start_x) + (3 * ((1 - t)**2) * t * control_x1) + (3 * (1 - t) * (t**2) * control_x2) + ((t**3) * t_x)
95
+ y = (((1 - t)**3) * start_y) + (3 * ((1 - t)**2) * t * control_y1) + (3 * (1 - t) * (t**2) * control_y2) + ((t**3) * t_y)
96
+ { x: x, y: y }
63
97
  end
64
98
  end
99
+
100
+ # @param [Integer] target_x
101
+ # @param [Integer] target_y
102
+ # @return [Hash]
103
+ def update_mouse_position(target_x, target_y)
104
+ @@mouse_position[:x] = target_x
105
+ @@mouse_position[:y] = target_y
106
+
107
+ mouse_position
108
+ end
65
109
  end
66
110
  end
67
111
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'chromate/helpers'
4
- require 'open3'
4
+ require_relative 'x11'
5
5
 
6
6
  module Chromate
7
7
  module Hardwares
@@ -17,23 +17,29 @@ module Chromate
17
17
  raise InvalidPlatformError, 'MouseController is only supported on Linux' unless linux?
18
18
 
19
19
  super
20
+ @display = X11.XOpenDisplay(nil)
21
+ raise 'Impossible d\'ouvrir l\'affichage X11' if @display.null?
22
+
23
+ @root_window = X11.XDefaultRootWindow(@display)
20
24
  end
21
25
 
22
26
  def hover
23
27
  focus_chrome_window
24
- # system("xdotool mousemove_relative --sync -- #{x} #{y}")
25
- system("xdotool mousemove #{target_x} #{target_y}")
26
- current_mouse_position
28
+ smooth_move_to(target_x, target_y)
29
+ update_mouse_position(target_x, target_y)
27
30
  end
28
31
 
29
32
  def click
30
33
  hover
31
34
  simulate_button_event(LEFT_BUTTON, true)
35
+ sleep(rand(CLICK_DURATION_RANGE))
32
36
  simulate_button_event(LEFT_BUTTON, false)
33
37
  end
34
38
 
35
39
  def right_click
40
+ hover
36
41
  simulate_button_event(RIGHT_BUTTON, true)
42
+ sleep(rand(CLICK_DURATION_RANGE))
37
43
  simulate_button_event(RIGHT_BUTTON, false)
38
44
  end
39
45
 
@@ -43,35 +49,132 @@ module Chromate
43
49
  click
44
50
  end
45
51
 
52
+ def drag_and_drop_to(element)
53
+ hover
54
+
55
+ target_x = element.x + (element.width / 2)
56
+ target_y = element.y + (element.height / 2)
57
+ start_x = position_x
58
+ start_y = position_y
59
+ steps = rand(25..50)
60
+ duration = rand(0.1..0.3)
61
+
62
+ # Generate a Bézier curve for natural movement
63
+ points = bezier_curve(steps: steps, start_x: start_x, start_y: start_y, t_x: target_x, t_y: target_y)
64
+
65
+ # Step 1: Press the left mouse button
66
+ simulate_button_event(LEFT_BUTTON, true)
67
+ sleep(rand(CLICK_DURATION_RANGE))
68
+
69
+ # Step 2: Drag the element
70
+ points.each do |point|
71
+ move_mouse_to(point[:x], point[:y])
72
+ sleep(duration / steps)
73
+ end
74
+
75
+ # Step 3: Release the left mouse button
76
+ simulate_button_event(LEFT_BUTTON, false)
77
+
78
+ # Update the mouse position
79
+ update_mouse_position(target_x, target_y)
80
+
81
+ self
82
+ end
83
+
46
84
  private
47
85
 
48
- def focus_chrome_window
49
- # Recherche de la fenêtre Chrome avec xdotool
50
- chrome_window_id = `xdotool search --onlyvisible --name "Chrome"`.strip
86
+ def smooth_move_to(dest_x, dest_y)
87
+ start_x = position_x
88
+ start_y = position_y
89
+
90
+ steps = rand(25..50)
91
+ duration = rand(0.1..0.3)
51
92
 
52
- if chrome_window_id.empty?
53
- puts 'Aucune fenêtre Chrome trouvée'
93
+ # Build a Bézier curve for natural movement
94
+ points = bezier_curve(steps: steps, start_x: start_x, start_y: start_y, t_x: dest_x, t_y: dest_y)
95
+
96
+ # Move the mouse along the Bézier curve
97
+ points.each do |point|
98
+ move_mouse_to(point[:x], point[:y])
99
+ sleep(duration / steps)
100
+ end
101
+ end
102
+
103
+ def move_mouse_to(x_target, y_target)
104
+ X11.XWarpPointer(@display, 0, @root_window, 0, 0, 0, 0, x_target.to_i, y_target.to_i)
105
+ X11.XFlush(@display)
106
+ end
107
+
108
+ def focus_chrome_window
109
+ chrome_window = find_window_by_name(@root_window, 'Chrome')
110
+ if chrome_window.zero?
111
+ Chromate::CLogger.log('No Chrome window found')
54
112
  else
55
- # Active la fenêtre Chrome avec xdotool
56
- system("xdotool windowactivate #{chrome_window_id}")
113
+ X11.XRaiseWindow(@display, chrome_window)
114
+ X11.XSetInputFocus(@display, chrome_window, X11::REVERT_TO_PARENT, 0)
115
+ X11.XFlush(@display)
57
116
  end
58
117
  end
59
118
 
60
- def current_mouse_position
61
- x = nil
62
- y = nil
63
- Open3.popen3('xdotool getmouselocation --shell') do |_, stdout, _, _|
64
- output = stdout.read
65
- x = output.match(/X=(\d+)/)[1].to_i
66
- y = output.match(/Y=(\d+)/)[1].to_i
119
+ def find_window_by_name(window, name) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
120
+ root_return = FFI::MemoryPointer.new(:ulong)
121
+ parent_return = FFI::MemoryPointer.new(:ulong)
122
+ children_return = FFI::MemoryPointer.new(:pointer)
123
+ nchildren_return = FFI::MemoryPointer.new(:uint)
124
+
125
+ status = X11.XQueryTree(@display, window, root_return, parent_return, children_return, nchildren_return)
126
+ return 0 if status.zero?
127
+
128
+ nchildren = nchildren_return.read_uint
129
+ children_ptr = children_return.read_pointer
130
+
131
+ return 0 if nchildren.zero? || children_ptr.null?
132
+
133
+ children = children_ptr.get_array_of_ulong(0, nchildren)
134
+ found_window = 0
135
+
136
+ children.each do |child|
137
+ window_name_ptr = FFI::MemoryPointer.new(:pointer)
138
+ status = X11.XFetchName(@display, child, window_name_ptr)
139
+ if status != 0 && !window_name_ptr.read_pointer.null?
140
+ window_name = window_name_ptr.read_pointer.read_string
141
+ if window_name.include?(name)
142
+ X11.XFree(window_name_ptr.read_pointer)
143
+ found_window = child
144
+ break
145
+ end
146
+ X11.XFree(window_name_ptr.read_pointer)
147
+ end
148
+ # Recursive search for the window
149
+ found_window = find_window_by_name(child, name)
150
+ break if found_window != 0
67
151
  end
68
152
 
69
- { x: x, y: y }
153
+ X11.XFree(children_ptr)
154
+ found_window
155
+ end
156
+
157
+ def current_mouse_position
158
+ root_return = FFI::MemoryPointer.new(:ulong)
159
+ child_return = FFI::MemoryPointer.new(:ulong)
160
+ root_x = FFI::MemoryPointer.new(:int)
161
+ root_y = FFI::MemoryPointer.new(:int)
162
+ win_x = FFI::MemoryPointer.new(:int)
163
+ win_y = FFI::MemoryPointer.new(:int)
164
+ mask_return = FFI::MemoryPointer.new(:uint)
165
+
166
+ X11.XQueryPointer(@display, @root_window, root_return, child_return, root_x, root_y, win_x, win_y, mask_return)
167
+
168
+ { x: root_x.read_int, y: root_y.read_int }
70
169
  end
71
170
 
72
171
  def simulate_button_event(button, press)
73
- action = press ? 'mousedown' : 'mouseup'
74
- system("xdotool #{action} #{button}")
172
+ Xtst.XTestFakeButtonEvent(@display, button, press ? 1 : 0, 0)
173
+ X11.XFlush(@display)
174
+ end
175
+
176
+ def finalize
177
+ X11.XCloseDisplay(@display) if @display && !@display.null?
75
178
  end
76
179
  end
77
180
  end