File size: 15,085 Bytes
ee826ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
/**
 * captcha-solver.js — Sistema de Resolución de CAPTCHAs con IA
 * ==============================================================
 * Sin servicios de pago. Todo con IA disponible.
 *
 * ESTRATEGIA POR TIPO DE CAPTCHA:
 *
 * ① reCAPTCHA v2 (imagen) → Gemini Vision analiza cada tile
 *   Research: "YOLOv8 fine-tuned achieves 100% success on reCAPTCHAv2 image challenges"
 *   Nosotros usamos Gemini Vision (gratis) con el mismo enfoque de clasificación
 *
 * ② reCAPTCHA v2 Audio → Whisper/SpeechRecognition gratis
 *   Research: "Audio transcription with wit.ai: 70-80% success, FREE"
 *   GitHub: njraladdin/recaptcha-v2-solver — método audio
 *
 * ③ reCAPTCHA v3 (behavioral) → NO hay que resolverlo: se evita con buen stealth
 *   Research: "reCAPTCHA v3 uses behavioral scoring. Good behavior simulation
 *              achieves 0.9 trust score without solving any challenge"
 *
 * ④ hCaptcha → Mismo enfoque que reCAPTCHA v2 con Gemini Vision
 *
 * ⑤ Cloudflare Turnstile → JavaScript challenge, se resuelve solo con buen stealth
 *
 * ⑥ Text CAPTCHAs → OCR via Gemini Vision
 *
 * ⑦ CAPTCHAs irresolubles → Notificar al owner para resolución manual
 */

import { readConfig } from './utils.js';
const config = readConfig();

// ── Gemini Vision helper ──────────────────────────────────────────────────────
async function callGeminiVision(base64Image, prompt, mimeType = 'image/jpeg') {
  const keys = config.ai?.gemini?.keys ?? [config.ai?.gemini?.apiKey].filter(Boolean);
  const key  = keys[0];
  if (!key) return null;

  const res = await fetch(
    `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`,
    {
      method : 'POST',
      headers: { 'Content-Type': 'application/json' },
      body   : JSON.stringify({
        contents: [{ parts: [
          { text: prompt },
          { inline_data: { mime_type: mimeType, data: base64Image } },
        ]}],
        generationConfig: { maxOutputTokens: 200, temperature: 0.1 },
      }),
      signal: AbortSignal.timeout(15000),
    }
  );
  if (!res.ok) return null;
  return (await res.json()).candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? null;
}

// ── Audio download y transcripción ──────────────────────────────────────────
async function transcribeAudio(audioUrl) {
  try {
    // Descargar el audio del CAPTCHA
    const audioRes  = await fetch(audioUrl, { signal: AbortSignal.timeout(15000) });
    const audioData = Buffer.from(await audioRes.arrayBuffer());
    const base64    = audioData.toString('base64');

    // Usar Gemini para transcribir (soporta audio nativo)
    const keys = config.ai?.gemini?.keys ?? [config.ai?.gemini?.apiKey].filter(Boolean);
    const key  = keys[0];
    if (!key) return null;

    const res = await fetch(
      `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${key}`,
      {
        method : 'POST',
        headers: { 'Content-Type': 'application/json' },
        body   : JSON.stringify({
          contents: [{ parts: [
            { text: 'This is a CAPTCHA audio challenge. Transcribe the spoken digits or words exactly. Reply with ONLY the numbers/words, nothing else.' },
            { inline_data: { mime_type: 'audio/mp3', data: base64 } },
          ]}],
          generationConfig: { maxOutputTokens: 50, temperature: 0 },
        }),
        signal: AbortSignal.timeout(20000),
      }
    );
    if (!res.ok) return null;
    const text = (await res.json()).candidates?.[0]?.content?.parts?.[0]?.text?.trim();
    // Limpiar respuesta: solo números y letras
    return text?.replace(/[^a-zA-Z0-9\s]/g, '').trim() ?? null;
  } catch {
    return null;
  }
}

function sleep(ms) { return new Promise(r => setTimeout(r, Math.round(ms))); }

// ═══════════════════════════════════════════════════════════════════════════════
// SOLUCIONADORES POR TIPO
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Resolver reCAPTCHA v2 con imágenes
 * Estrategia: screenshot de cada tile → Gemini Vision clasifica → clic en los correctos
 */
export async function solveRecaptchaV2Images(page) {
  console.log('[CAPTCHA] 🔍 Intentando resolver reCAPTCHA v2 por imágenes...');

  try {
    // Intentar primero el checkbox "I'm not a robot" (a veces pasa solo con buen stealth)
    const checkbox = await page.$('.recaptcha-checkbox-border, #recaptcha-anchor');
    if (checkbox) {
      await checkbox.click();
      await sleep(2000 + Math.random() * 1000);

      // Verificar si el checkbox se marcó sin challenge
      const solved = await page.evaluate(() => {
        const resp = document.querySelector('textarea#g-recaptcha-response, [name="g-recaptcha-response"]');
        return resp && resp.value.length > 10;
      }).catch(() => false);

      if (solved) {
        console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto sin challenge (buen stealth)');
        return { success: true, method: 'stealth_no_challenge' };
      }
    }

    // Hay challenge de imágenes — resolver con visión
    // Obtener el iframe del CAPTCHA
    const frame = page.frames().find(f => f.url().includes('recaptcha'));
    if (!frame) return { success: false, method: 'no_frame' };

    // Obtener la instrucción del challenge
    const instruction = await frame.$eval('.rc-imageselect-desc-wrapper', el => el.textContent?.trim() ?? '').catch(() => '');
    console.log('[CAPTCHA] Instrucción:', instruction.slice(0, 80));

    // Hacer screenshot del área de imágenes
    const grid = await frame.$('.rc-imageselect-table');
    if (!grid) return { success: false, method: 'no_grid' };

    const gridScreenshot = await grid.screenshot({ type: 'jpeg', quality: 90 });
    const base64Grid     = gridScreenshot.toString('base64');

    // Gemini Vision: analizar qué tiles seleccionar
    const prompt = `This is a reCAPTCHA image grid challenge. The instruction says: "${instruction}"

Analyze each tile in the grid (numbered left-to-right, top-to-bottom starting from 1).
Reply with ONLY the tile numbers that match the instruction, separated by commas.
Example: "1,3,7" or "2,5,6,9"
If no tiles match, reply "none".
Do not explain, just give the numbers.`;

    const answer = await callGeminiVision(base64Grid, prompt);
    if (!answer || answer.toLowerCase() === 'none') {
      console.log('[CAPTCHA] Gemini no encontró tiles para seleccionar');
      return { success: false, method: 'vision_no_match' };
    }

    // Parsear números de tiles
    const tileNumbers = answer.split(',').map(n => parseInt(n.trim())).filter(n => !isNaN(n) && n >= 1);
    console.log('[CAPTCHA] Tiles a seleccionar:', tileNumbers);

    // Hacer clic en los tiles indicados con comportamiento humano
    for (const tileNum of tileNumbers) {
      const tile = await frame.$(`.rc-imageselect-tile:nth-child(${tileNum}), .rc-image-tile-target:nth-child(${tileNum})`);
      if (tile) {
        const box = await tile.boundingBox();
        if (box) {
          const x = box.x + box.width * (0.2 + Math.random() * 0.6);
          const y = box.y + box.height * (0.2 + Math.random() * 0.6);
          await page.mouse.move(x, y);
          await sleep(200 + Math.random() * 300);
          await page.mouse.click(x, y);
          await sleep(300 + Math.random() * 400);
        }
      }
    }

    // Hacer clic en Verify
    await sleep(1000 + Math.random() * 500);
    const verifyBtn = await frame.$('#recaptcha-verify-button, .rc-button-default');
    if (verifyBtn) {
      await verifyBtn.click();
      await sleep(2000 + Math.random() * 1000);
    }

    // Verificar resultado
    const token = await page.evaluate(() => {
      const t = document.querySelector('textarea[name="g-recaptcha-response"]');
      return t?.value ?? '';
    }).catch(() => '');

    if (token.length > 20) {
      console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto por visión');
      return { success: true, method: 'vision', token };
    }

    return { success: false, method: 'vision_unverified' };

  } catch (e) {
    console.warn('[CAPTCHA] Error en reCAPTCHA v2:', e.message);
    return { success: false, error: e.message };
  }
}

/**
 * Resolver reCAPTCHA v2 por audio (más fiable que imágenes)
 * Estrategia: solicitar versión audio → descargar → transcribir con Gemini → escribir
 */
export async function solveRecaptchaV2Audio(page) {
  console.log('[CAPTCHA] 🎵 Intentando reCAPTCHA v2 por audio...');

  try {
    const frame = page.frames().find(f => f.url().includes('recaptcha'));
    if (!frame) return { success: false, method: 'no_frame' };

    // Cambiar a modo audio
    const audioBtn = await frame.$('#recaptcha-audio-button');
    if (!audioBtn) {
      // Intentar via checkbox primero
      const checkbox = await frame.$('#recaptcha-anchor');
      if (checkbox) { await checkbox.click(); await sleep(1500); }
    }

    const audioBtnRetry = await frame.$('#recaptcha-audio-button');
    if (!audioBtnRetry) return { success: false, method: 'no_audio_button' };

    await audioBtnRetry.click();
    await sleep(2000);

    // Obtener la URL del audio
    const audioUrl = await frame.$eval('.rc-audiochallenge-tdownload-link', el => el.href).catch(() => null);
    if (!audioUrl) return { success: false, method: 'no_audio_url' };

    // Transcribir con Gemini
    const transcription = await transcribeAudio(audioUrl);
    if (!transcription) return { success: false, method: 'transcription_failed' };

    console.log('[CAPTCHA] Transcripción:', transcription);

    // Escribir la respuesta en el campo
    const input = await frame.$('#audio-response');
    if (!input) return { success: false, method: 'no_input' };

    await input.click();
    await sleep(200);
    await page.keyboard.type(transcription.toLowerCase().replace(/\s+/g, ' '), { delay: 80 + Math.random() * 40 });
    await sleep(500);

    // Verificar
    const verifyBtn = await frame.$('#recaptcha-verify-button');
    if (verifyBtn) { await verifyBtn.click(); await sleep(2000); }

    const token = await page.evaluate(() =>
      document.querySelector('textarea[name="g-recaptcha-response"]')?.value ?? ''
    ).catch(() => '');

    if (token.length > 20) {
      console.log('[CAPTCHA] ✅ reCAPTCHA v2 resuelto por audio');
      return { success: true, method: 'audio', token };
    }

    return { success: false, method: 'audio_unverified' };

  } catch (e) {
    return { success: false, error: e.message };
  }
}

/**
 * Resolver text CAPTCHA (imagen con texto/números distorsionados)
 */
export async function solveTextCaptcha(page, imageSelector) {
  try {
    const imgEl = await page.$(imageSelector);
    if (!imgEl) return null;

    const imgBase64 = (await imgEl.screenshot({ type: 'jpeg', quality: 95 })).toString('base64');
    const answer    = await callGeminiVision(imgBase64,
      'This is a text CAPTCHA image with distorted characters. Read the characters carefully and reply with ONLY the text shown, no explanation.'
    );

    return answer?.replace(/\s/g, '') ?? null;
  } catch { return null; }
}

/**
 * Detector principal de CAPTCHA + resolutor automático
 */
export async function detectAndSolve(page, notifyOwner = null) {
  console.log('[CAPTCHA] Escaneando página...');

  // Detectar tipo de CAPTCHA presente
  const captchaInfo = await page.evaluate(() => {
    const body = document.body.innerHTML.toLowerCase();
    const text  = document.body.textContent.toLowerCase();
    return {
      hasRecaptchaV2  : !!document.querySelector('iframe[src*="recaptcha"]') || body.includes('g-recaptcha'),
      hasHcaptcha     : body.includes('hcaptcha') || !!document.querySelector('iframe[src*="hcaptcha"]'),
      hasCloudflare   : text.includes('checking your browser') || text.includes('verifying you are') || body.includes('cf-challenge'),
      hasTurnstile    : body.includes('turnstile') || !!document.querySelector('[data-sitekey]'),
      hasTextCaptcha  : !!document.querySelector('img[src*="captcha"], img[alt*="captcha"]'),
    };
  }).catch(() => ({}));

  console.log('[CAPTCHA] Detectado:', JSON.stringify(captchaInfo));

  // 1. Cloudflare — solo stealth, no hay challenge que resolver directamente
  if (captchaInfo.hasCloudflare) {
    console.log('[CAPTCHA] Cloudflare detectado — esperando resolución automática...');
    await sleep(5000 + Math.random() * 3000);
    // Si sigue ahí después de 8s, notificar
    const stillCloudflare = await page.evaluate(() =>
      document.body.textContent.toLowerCase().includes('checking your browser')
    ).catch(() => false);

    if (stillCloudflare && notifyOwner) {
      await notifyOwner('⚠️ Cloudflare no se resolvió automáticamente. Puede necesitar proxy de mayor calidad.');
    }
    return { type: 'cloudflare', solved: !stillCloudflare };
  }

  // 2. reCAPTCHA v2 — intentar audio primero (más fiable), luego imagen
  if (captchaInfo.hasRecaptchaV2) {
    // Intentar método audio
    let result = await solveRecaptchaV2Audio(page);
    if (!result.success) {
      // Fallback a imágenes
      result = await solveRecaptchaV2Images(page);
    }

    if (!result.success && notifyOwner) {
      await notifyOwner('⚠️ reCAPTCHA v2 no pudo resolverse automáticamente. Requiere intervención manual.');
    }
    return { type: 'recaptcha_v2', ...result };
  }

  // 3. hCaptcha — mismo enfoque que reCAPTCHA v2
  if (captchaInfo.hasHcaptcha) {
    console.log('[CAPTCHA] hCaptcha detectado — intentando resolver...');
    // hCaptcha tiene estructura similar a reCAPTCHA v2
    const result = await solveRecaptchaV2Images(page); // funciona por similitud
    if (!result.success && notifyOwner) {
      await notifyOwner('⚠️ hCaptcha detectado. Requiere intervención manual.');
    }
    return { type: 'hcaptcha', ...result };
  }

  // 4. Text CAPTCHA
  if (captchaInfo.hasTextCaptcha) {
    const answer = await solveTextCaptcha(page, 'img[alt*="captcha"], img[src*="captcha"]');
    return { type: 'text_captcha', solved: !!answer, answer };
  }

  return { type: 'none', solved: true };
}

export async function getCaptchaSolverStats() {
  const geminiAvailable = !!(config.ai?.gemini?.keys?.length || config.ai?.gemini?.apiKey);
  return {
    geminiVision: geminiAvailable,
    audioSolver : geminiAvailable,
    supportedTypes: ['recaptcha_v2_image', 'recaptcha_v2_audio', 'hcaptcha', 'text_captcha', 'cloudflare_basic'],
    notSupported  : ['recaptcha_v3_enterprise', 'funcaptcha_advanced', 'geetest_v4'],
  };
}