1import { Page, Frame } from 'rebrowser-playwright';
2
3
4
5
6
7
8interface SolverResponse {
9 errorId: number;
10 errorCode?: string;
11 errorDescription?: string;
12 taskId?: string;
13 status?: string;
14 solution?: {
15 distance?: number;
16 slide_x_proportion?: number;
17 cookie?: string;
18 };
19}
20
21export class DataDomeSolver {
22 private apiKey: string | null;
23 private enabled: boolean;
24 private createTaskUrl = 'https://api.capsolver.com/createTask';
25 private getResultUrl = 'https://api.capsolver.com/getTaskResult';
26
27 constructor(apiKey?: string) {
28 this.apiKey = apiKey || null;
29 this.enabled = !!apiKey && apiKey.length > 10;
30 }
31
32 async isBlocked(page: Page): Promise<boolean> {
33 try {
34 const result = await page.evaluate(() => {
35 const iframe = document.querySelector('iframe[src*="captcha-delivery.com"]');
36 const html = document.documentElement.outerHTML;
37 return {
38 hasIframe: !!iframe,
39 hasGeo: html.includes('geo.captcha-delivery.com'),
40 hasDDObj: html.includes('dd={'),
41 };
42 });
43 return result.hasIframe || result.hasGeo || result.hasDDObj;
44 } catch (e: any) {
45 return false;
46 }
47 }
48
49 async solveDataDome(page: Page, _proxyInfo?: any, maxRetries = 3): Promise<boolean> {
50 try {
51 await page.waitForTimeout(2000);
52
53 const isBlocked = await this.isBlocked(page);
54 if (!isBlocked) {
55 return true;
56 }
57
58
59 const frame = await this.getDataDomeFrame(page);
60 if (!frame) {
61 return this.retry(page, _proxyInfo, maxRetries);
62 }
63
64
65 await frame.waitForTimeout(2000);
66
67
68 const challengeType = await this.detectChallengeType(frame);
69
70 let solved = false;
71
72 if (challengeType === 'simple') {
73 solved = await this.solveSimpleSlider(frame);
74 } else if (challengeType === 'puzzle') {
75 if (this.enabled) {
76 solved = await this.solvePuzzleSlider(frame, page);
77 } else {
78 solved = false;
79 }
80 } else {
81 solved = await this.solveSimpleSlider(frame);
82 }
83
84 if (!solved) {
85 return this.retry(page, _proxyInfo, maxRetries);
86 }
87
88
89 const urlBefore = page.url();
90
91 try {
92 await page.waitForNavigation({ timeout: 5000, waitUntil: 'domcontentloaded' });
93 } catch (e) {
94
95 }
96
97 await page.waitForTimeout(2000);
98
99
100 const pageContent = await page.evaluate(() => {
101 const hasIframe = !!document.querySelector('iframe[src*="captcha-delivery.com"]');
102 const hasSearchInput = !!document.querySelector('input[name="search_query"], input#global-enhancements-search-query');
103 const hasEtsyNav = !!document.querySelector('[data-nav-main], .wt-action-group');
104 return { hasIframe, hasSearchInput, hasEtsyNav };
105 });
106
107 if (pageContent.hasSearchInput || pageContent.hasEtsyNav) {
108 return true;
109 }
110
111
112 try {
113 const afterSlideInfo = await frame.evaluate(() => {
114 const bodyText = document.body?.innerText?.substring(0, 300) || '';
115 const hasAudioFallback = bodyText.includes('audio verification') || bodyText.includes('6 digits');
116 return { hasAudioFallback };
117 });
118
119 if (afterSlideInfo.hasAudioFallback) {
120
121 return false;
122 }
123 } catch (e: any) {
124
125 }
126
127
128 const stillBlocked = await this.isBlocked(page);
129 if (stillBlocked) {
130 return this.retry(page, _proxyInfo, maxRetries);
131 }
132
133 return true;
134
135 } catch (error: any) {
136 return this.retry(page, _proxyInfo, maxRetries);
137 }
138 }
139
140 private async retry(page: Page, proxyInfo: any, maxRetries: number): Promise<boolean> {
141 if (maxRetries > 0) {
142 await page.waitForTimeout(2000);
143 return this.solveDataDome(page, proxyInfo, maxRetries - 1);
144 }
145 return false;
146 }
147
148 private async getDataDomeFrame(page: Page): Promise<Frame | null> {
149 try {
150 await page.waitForSelector('iframe[src*="captcha-delivery.com"]', { timeout: 10000 });
151 const frames = page.frames();
152 for (const frame of frames) {
153 if (frame.url().includes('captcha-delivery.com')) {
154 return frame;
155 }
156 }
157 const iframeEl = await page.$('iframe[src*="captcha-delivery.com"]');
158 if (iframeEl) {
159 return await iframeEl.contentFrame();
160 }
161 return null;
162 } catch (e: any) {
163 return null;
164 }
165 }
166
167 private async detectChallengeType(frame: Frame): Promise<'simple' | 'puzzle' | 'unknown'> {
168 const info = await frame.evaluate(() => {
169 const canvases = document.querySelectorAll('#captcha__puzzle canvas');
170 let hasCanvasContent = false;
171 canvases.forEach((c: any) => {
172 if (c.width > 0 && c.height > 50) {
173 hasCanvasContent = true;
174 }
175 });
176
177 const slider = document.querySelector('.slider');
178 const sliderTarget = document.querySelector('.sliderTarget');
179 const sliderText = document.querySelector('.sliderText');
180
181 const text = sliderText?.textContent || '';
182 const isSimpleText = text.toLowerCase().includes('slide right to secure') ||
183 text.toLowerCase().includes('slide to verify');
184
185 return {
186 hasCanvasContent,
187 hasSlider: !!slider,
188 hasSliderTarget: !!sliderTarget,
189 isSimpleText,
190 };
191 });
192
193 if (info.hasCanvasContent) {
194 return 'puzzle';
195 }
196
197 if (info.hasSlider && !info.hasCanvasContent) {
198 return 'simple';
199 }
200
201 if (info.isSimpleText) {
202 return 'simple';
203 }
204
205 return 'unknown';
206 }
207
208 private async getIframeOffset(page: Page): Promise<{ x: number; y: number }> {
209 try {
210 const iframeEl = await page.$('iframe[src*="captcha-delivery.com"]');
211 if (!iframeEl) {
212 return { x: 0, y: 0 };
213 }
214 const box = await iframeEl.boundingBox();
215 if (!box) {
216 return { x: 0, y: 0 };
217 }
218 return { x: box.x, y: box.y };
219 } catch (e) {
220 return { x: 0, y: 0 };
221 }
222 }
223
224 private async solveSimpleSlider(frame: Frame): Promise<boolean> {
225 try {
226 const page = frame.page();
227
228 const iframeOffset = await this.getIframeOffset(page);
229
230 const sliderInfo = await frame.evaluate(() => {
231 const slider = document.querySelector('.slider');
232 const sliderbg = document.querySelector('.sliderbg');
233 const sliderTarget = document.querySelector('.sliderTarget');
234
235 if (!slider || !sliderbg) {
236 return null;
237 }
238
239 const sliderRect = slider.getBoundingClientRect();
240 const trackRect = sliderbg.getBoundingClientRect();
241 const targetRect = sliderTarget?.getBoundingClientRect();
242
243 return {
244 sliderX: sliderRect.x + sliderRect.width / 2,
245 sliderY: sliderRect.y + sliderRect.height / 2,
246 sliderWidth: sliderRect.width,
247 trackWidth: trackRect.width,
248 targetX: targetRect ? targetRect.x + targetRect.width / 2 : trackRect.x + trackRect.width - 10,
249 targetCenterFromTrackStart: targetRect ? (targetRect.x + targetRect.width / 2 - trackRect.x) : null,
250 };
251 });
252
253 if (!sliderInfo) {
254 return false;
255 }
256
257 const pageX = sliderInfo.sliderX + iframeOffset.x;
258 const pageY = sliderInfo.sliderY + iframeOffset.y;
259
260 let slideDistance: number;
261 if (sliderInfo.targetCenterFromTrackStart !== null) {
262 slideDistance = sliderInfo.targetX - sliderInfo.sliderX;
263 } else {
264 slideDistance = sliderInfo.trackWidth - sliderInfo.sliderWidth - 10;
265 }
266
267 await this.humanLikeSlide(page, pageX, pageY, slideDistance);
268
269 return true;
270
271 } catch (error: any) {
272 return false;
273 }
274 }
275
276 private async solvePuzzleSlider(frame: Frame, page: Page): Promise<boolean> {
277 try {
278 const iframeOffset = await this.getIframeOffset(page);
279
280 const captchaData = await frame.evaluate(() => {
281 const canvases = document.querySelectorAll('#captcha__puzzle canvas');
282 let backgroundBase64 = '';
283 let pieceBase64 = '';
284
285 canvases.forEach((canvas: any, index) => {
286 if (canvas.width > 0 && canvas.height > 0) {
287 try {
288 const base64 = canvas.toDataURL('image/png').split(',')[1];
289 if (index === 0) backgroundBase64 = base64;
290 else if (index === 1) pieceBase64 = base64;
291 } catch (e) {}
292 }
293 });
294
295 const slider = document.querySelector('.slider');
296 const sliderRect = slider?.getBoundingClientRect();
297
298 return {
299 backgroundBase64,
300 pieceBase64: pieceBase64 || backgroundBase64,
301 sliderX: sliderRect?.x ?? 0,
302 sliderY: sliderRect?.y ?? 0,
303 sliderWidth: sliderRect?.width ?? 0,
304 sliderHeight: sliderRect?.height ?? 0,
305 };
306 });
307
308 if (!captchaData.backgroundBase64) {
309 return false;
310 }
311
312 const distance = await this.callVisionEngine(captchaData);
313 if (!distance || distance <= 0) {
314 return false;
315 }
316
317 const startX = captchaData.sliderX + captchaData.sliderWidth / 2 + iframeOffset.x;
318 const startY = captchaData.sliderY + captchaData.sliderHeight / 2 + iframeOffset.y;
319
320 await this.humanLikeSlide(page, startX, startY, distance);
321
322 return true;
323
324 } catch (error: any) {
325 return false;
326 }
327 }
328
329 private async callVisionEngine(captchaData: any): Promise<number | null> {
330 if (!this.apiKey) return null;
331
332 try {
333 const response = await fetch(this.createTaskUrl, {
334 method: 'POST',
335 headers: { 'Content-Type': 'application/json' },
336 body: JSON.stringify({
337 clientKey: this.apiKey,
338 task: {
339 type: 'VisionEngine',
340 module: 'slider_1',
341 image: captchaData.pieceBase64,
342 imageBackground: captchaData.backgroundBase64,
343 },
344 }),
345 });
346
347 const result: SolverResponse = await response.json();
348
349 if (result.errorId !== 0) {
350 return null;
351 }
352
353 if (result.solution) {
354 return result.solution.distance ?? result.solution.slide_x_proportion ?? null;
355 }
356
357 if (result.taskId) {
358 return await this.pollForResult(result.taskId);
359 }
360
361 return null;
362 } catch (error: any) {
363 return null;
364 }
365 }
366
367 private async pollForResult(taskId: string): Promise<number | null> {
368 for (let i = 0; i < 30; i++) {
369 await new Promise(r => setTimeout(r, 2000));
370 try {
371 const response = await fetch(this.getResultUrl, {
372 method: 'POST',
373 headers: { 'Content-Type': 'application/json' },
374 body: JSON.stringify({ clientKey: this.apiKey, taskId }),
375 });
376 const result: SolverResponse = await response.json();
377 if (result.status === 'ready' && result.solution) {
378 return result.solution.distance ?? result.solution.slide_x_proportion ?? null;
379 }
380 if (result.status === 'failed') return null;
381 } catch (e) {}
382 }
383 return null;
384 }
385
386 private async humanLikeSlide(page: Page, startX: number, startY: number, distance: number): Promise<void> {
387
388 const approachX = startX - 50 - Math.random() * 100;
389 const approachY = startY - 30 + Math.random() * 60;
390 await page.mouse.move(approachX, approachY, { steps: 5 });
391 await page.waitForTimeout(200 + Math.random() * 300);
392
393
394 await page.mouse.move(startX, startY, { steps: 8 + Math.floor(Math.random() * 5) });
395 await page.waitForTimeout(150 + Math.random() * 250);
396
397
398 await page.mouse.down();
399 await page.waitForTimeout(80 + Math.random() * 120);
400
401
402 const totalSteps = 25 + Math.floor(Math.random() * 20);
403 const overshoot = 3 + Math.random() * 8;
404
405 for (let i = 1; i <= totalSteps; i++) {
406 const progress = i / totalSteps;
407
408 let easeProgress: number;
409 if (progress < 0.3) {
410 easeProgress = 2 * progress * progress;
411 } else if (progress < 0.85) {
412 const normalized = (progress - 0.3) / 0.55;
413 easeProgress = 0.18 + normalized * 0.75 + (Math.random() - 0.5) * 0.02;
414 } else {
415 const normalized = (progress - 0.85) / 0.15;
416 const target = 0.93 + normalized * 0.07;
417 easeProgress = target + (progress > 0.95 ? overshoot / distance : 0);
418 }
419
420 easeProgress = Math.min(1 + overshoot / distance, Math.max(0, easeProgress));
421
422 const currentX = startX + distance * easeProgress;
423 const wobbleMagnitude = 3 * (1 - progress * 0.7);
424 const wobbleY = startY + (Math.random() - 0.5) * wobbleMagnitude;
425
426 await page.mouse.move(currentX, wobbleY);
427
428 let delay = 10 + Math.random() * 18;
429 if (Math.random() < 0.1) {
430 delay += 50 + Math.random() * 80;
431 }
432 await page.waitForTimeout(delay);
433 }
434
435
436 if (overshoot > 2) {
437 await page.waitForTimeout(50 + Math.random() * 80);
438 await page.mouse.move(startX + distance - 2, startY + (Math.random() - 0.5) * 2, { steps: 3 });
439 }
440
441
442 await page.waitForTimeout(150 + Math.random() * 200);
443
444
445 await page.mouse.up();
446 }
447
448 setProxy(_proxyInfo: any) {}
449}