File size: 14,268 Bytes
dad3f62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
"""hCaptcha Playwright automation wrapper.

Runs on your local machine. Detects hCaptcha on faucet pages,
screenshots tiles, calls the vision model API to classify them,
and clicks matching tiles with human-like delays.

Usage:
    python scripts/hcaptcha_bot.py https://faucet-wave.online/tron-faucet/

Or import and use in your existing claimer scripts:
    from scripts.hcaptcha_bot import solve_hcaptcha
    await solve_hcaptcha(page, "https://faucet-wave.online/tron-faucet/")
"""

from __future__ import annotations

import asyncio
import base64
import io
import os
import random
import sys
import time
from typing import Optional

import httpx

try:
    from playwright.async_api import async_playwright, Page, ElementHandle
except ImportError:
    print("ERROR: playwright not installed. Run: pip install playwright && playwright install chromium")
    sys.exit(1)


# Config
API_URL = os.environ.get("CAPTCHA_SOLVER_URL", "http://localhost:8765")
HF_SPACE_URL = os.environ.get("HF_SPACE_URL", "")  # Set this for HF Spaces deployment
SOLVER_URL = HF_SPACE_URL or API_URL
TILE_DELAY_MIN = 0.8  # Min seconds between tile clicks
TILE_DELAY_MAX = 2.5  # Max seconds between tile clicks
CLICK_DELAY_MIN = 0.3
CLICK_DELAY_MAX = 0.8


async def _call_classify_api(
    image_bytes: bytes,
    instruction: str,
    client: httpx.AsyncClient,
) -> dict:
    """Call POST /classify with a tile image."""
    b64 = base64.b64encode(image_bytes).decode("ascii")
    payload = {
        "image_base64": b64,
        "instruction": instruction,
    }
    try:
        r = await client.post(f"{SOLVER_URL}/classify", json=payload, timeout=30)
        r.raise_for_status()
        return r.json()
    except Exception as exc:
        return {"match": False, "confidence": 0.0, "caption": "", "error": str(exc)}


def _extract_instruction(modal_content: str) -> str:
    """Extract hCaptcha instruction text from the challenge header."""
    # Common hCaptcha patterns
    patterns = [
        r"(?:click|find|select|choose)\s+all\s+[^.]+",
        r"(?:click|find|select|choose)\s+the\s+[^.]+",
        r"(?:which|what)\s+[^?]+\?",
    ]
    text_lower = modal_content.lower()
    for pattern in patterns:
        import re
        match = re.search(pattern, text_lower, re.IGNORECASE)
        if match:
            return match.group(0).strip()
    return modal_content.strip()


async def _human_like_mouse_move(page: Page, x: float, y: float) -> None:
    """Move mouse to (x, y) with human-like curve."""
    await page.mouse.move(x, y, steps=random.randint(10, 25))
    await asyncio.sleep(random.uniform(CLICK_DELAY_MIN, CLICK_DELAY_MAX))


async def _click_with_human_delay(page: Page, selector: str) -> None:
    """Click an element with random human-like delay."""
    await asyncio.sleep(random.uniform(TILE_DELAY_MIN, TILE_DELAY_MAX))
    element = await page.query_selector(selector)
    if element:
        box = await element.bounding_box()
        if box:
            # Click slightly off-center for realism
            x = box["x"] + box["width"] * random.uniform(0.3, 0.7)
            y = box["y"] + box["height"] * random.uniform(0.3, 0.7)
            await _human_like_mouse_move(page, x, y)
            await asyncio.sleep(random.uniform(0.1, 0.3))
            await page.mouse.click(x, y)


async def _screenshot_tile(page: Page, tile_index: int, grid_selector: str = ".task-image") -> Optional[bytes]:
    """Screenshot a specific tile from the hCaptcha grid."""
    tiles = await page.query_selector_all(grid_selector)
    if tile_index >= len(tiles):
        return None
    tile = tiles[tile_index]
    screenshot = await tile.screenshot()
    return screenshot


async def solve_hcaptcha(
    page: Page,
    max_retries: int = 3,
    verbose: bool = True,
) -> bool:
    """Solve hCaptcha on the current page.

    Args:
        page: Playwright page with hCaptcha loaded.
        max_retries: Number of attempts before giving up.
        verbose: Print progress info.

    Returns:
        True if captcha was solved successfully, False otherwise.
    """
    for attempt in range(max_retries):
        if verbose:
            print(f"[hCaptcha] Attempt {attempt + 1}/{max_retries}")

        try:
            # Wait for hCaptcha iframe to appear
            hcaptcha_frame = None
            for frame in page.frames:
                if "hcaptcha" in frame.url.lower():
                    hcaptcha_frame = frame
                    break

            if not hcaptcha_frame:
                # Try clicking the checkbox first
                checkbox = await page.query_selector(".h-captcha iframe, iframe[src*='hcaptcha']")
                if checkbox:
                    await checkbox.click()
                    await asyncio.sleep(2)
                    # Re-check frames
                    for frame in page.frames:
                        if "hcaptcha" in frame.url.lower():
                            hcaptcha_frame = frame
                            break

            if not hcaptcha_frame:
                if verbose:
                    print("[hCaptcha] No hCaptcha iframe found")
                return False

            # Wait for challenge to load (if checkbox was already solved)
            await asyncio.sleep(1)

            # Check if already solved (checkbox is green)
            checkbox_frame = None
            for frame in page.frames:
                if "hcaptcha" in frame.url.lower() and "checkbox" in frame.url.lower():
                    checkbox_frame = frame
                    break

            if checkbox_frame:
                is_checked = await checkbox_frame.query_selector(".check")
                if is_checked:
                    if verbose:
                        print("[hCaptcha] Already solved!")
                    return True

            # Look for the challenge iframe (separate from checkbox iframe)
            challenge_frame = None
            for frame in page.frames:
                if "hcaptcha" in frame.url.lower() and "challenge" in frame.url.lower():
                    challenge_frame = frame
                    break

            if not challenge_frame:
                # Challenge might be in the main frame's modal
                challenge_frame = page

            # Extract instruction text
            instruction = ""
            try:
                header = await challenge_frame.query_selector(".challenge-header, .prompt-text, [class*='header']")
                if header:
                    instruction = await header.inner_text()
                else:
                    # Try getting all visible text in the challenge
                    body = await challenge_frame.query_selector("body")
                    if body:
                        all_text = await body.inner_text()
                        instruction = _extract_instruction(all_text)
            except Exception:
                pass

            if not instruction:
                if verbose:
                    print("[hCaptcha] Could not extract instruction")
                continue

            if verbose:
                print(f"[hCaptcha] Instruction: {instruction}")

            # Screenshot all tiles
            tiles = await challenge_frame.query_selector_all(".task-image, [class*='tile'], [class*='image']")
            if not tiles:
                if verbose:
                    print("[hCaptcha] No tiles found in challenge")
                continue

            if verbose:
                print(f"[hCaptcha] Found {len(tiles)} tiles")

            # Classify each tile via API
            matches = []
            async with httpx.AsyncClient() as client:
                for i, tile in enumerate(tiles):
                    try:
                        tile_screenshot = await tile.screenshot()
                        result = await _call_classify_api(tile_screenshot, instruction, client)
                        if result.get("match"):
                            matches.append(i)
                        if verbose:
                            status = "MATCH" if result.get("match") else "skip"
                            caption = result.get("caption", "")
                            conf = result.get("confidence", 0)
                            print(f"  Tile {i + 1}: {status} (conf={conf:.2f}) caption={caption}")
                    except Exception as exc:
                        if verbose:
                            print(f"  Tile {i + 1}: error - {exc}")

            if not matches:
                if verbose:
                    print("[hCaptcha] No matches found, skipping")
                # Click "Get a new challenge" button
                try:
                    refresh_btn = await challenge_frame.query_selector(
                        ".refresh-button, [class*='refresh'], button[aria-label*='refresh']"
                    )
                    if refresh_btn:
                        await refresh_btn.click()
                        await asyncio.sleep(2)
                        continue
                except Exception:
                    pass
                continue

            if verbose:
                print(f"[hCaptcha] Clicking tiles: {[m + 1 for m in matches]}")

            # Click matching tiles with human-like delays
            for idx in matches:
                tile = tiles[idx]
                try:
                    box = await tile.bounding_box()
                    if box:
                        x = box["x"] + box["width"] * random.uniform(0.3, 0.7)
                        y = box["y"] + box["height"] * random.uniform(0.3, 0.7)
                        await _human_like_mouse_move(page, x, y)
                        await asyncio.sleep(random.uniform(0.1, 0.3))
                        await page.mouse.click(x, y)
                        await asyncio.sleep(random.uniform(TILE_DELAY_MIN, TILE_DELAY_MAX))
                except Exception as exc:
                    if verbose:
                        print(f"  Failed to click tile {idx + 1}: {exc}")

            # Click Verify button
            await asyncio.sleep(random.uniform(1.0, 2.0))
            try:
                verify_btn = await challenge_frame.query_selector(
                    ".verify-button, button[type='submit'], [class*='verify']"
                )
                if verify_btn:
                    await verify_btn.click()
                    await asyncio.sleep(3)

                    # Check if solved
                    for frame in page.frames:
                        if "hcaptcha" in frame.url.lower() and "checkbox" in frame.url.lower():
                            is_checked = await frame.query_selector(".check")
                            if is_checked:
                                if verbose:
                                    print("[hCaptcha] Solved successfully!")
                                return True

                    # If we get here, might need another attempt
                    if verbose:
                        print("[hCaptcha] Verification pending, checking...")

            except Exception as exc:
                if verbose:
                    print(f"[hCaptcha] Verify click failed: {exc}")

        except Exception as exc:
            if verbose:
                print(f"[hCaptcha] Attempt {attempt + 1} error: {exc}")

        # Wait before retry
        await asyncio.sleep(random.uniform(2.0, 4.0))

    if verbose:
        print(f"[hCaptcha] Failed after {max_retries} attempts")
    return False


async def main():
    """CLI entry point: solve hCaptcha on a given URL."""
    import argparse
    parser = argparse.ArgumentParser(description="Solve hCaptcha on a faucet page")
    parser.add_argument("url", help="URL of the page with hCaptcha")
    parser.add_argument("--address", help="FaucetPay address to fill in")
    parser.add_argument("--api", default=SOLVER_URL, help="Captcha solver API URL")
    parser.add_argument("--headless", action="store_true", help="Run headless (no browser window)")
    args = parser.parse_args()

    global SOLVER_URL
    SOLVER_URL = args.api

    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=args.headless,
            args=[
                "--disable-blink-features=AutomationControlled",
                "--no-sandbox",
            ],
        )
        context = await browser.new_context(
            viewport={"width": 1280, "height": 800},
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
        )
        page = await context.new_page()

        print(f"[bot] Navigating to {args.url}")
        await page.goto(args.url, wait_until="networkidle")
        await asyncio.sleep(2)

        # Fill in address if provided
        if args.address:
            addr_input = await page.query_selector("#fp-address, input[name='address']")
            if addr_input:
                await addr_input.fill(args.address)
                print(f"[bot] Address filled: {args.address}")

        # Click Claim button to open modal
        claim_btn = await page.query_selector("#faucet-claim-btn, .faucet-btn")
        if claim_btn:
            await claim_btn.click()
            print("[bot] Claim button clicked, modal opened")
            await asyncio.sleep(2)

        # Solve hCaptcha
        solved = await solve_hcaptcha(page, max_retries=3, verbose=True)

        if solved:
            # Click Submit
            submit_btn = await page.query_selector("#faucet-submit-btn, button.faucet-btn")
            if submit_btn:
                await submit_btn.click()
                print("[bot] Submit clicked!")
                await asyncio.sleep(5)

                # Check for success message
                success = await page.query_selector(".faucet-success, [class*='success']")
                if success:
                    msg = await success.inner_text()
                    print(f"[bot] SUCCESS: {msg}")
                else:
                    print("[bot] Claim submitted, check results")
        else:
            print("[bot] Failed to solve hCaptcha")

        await browser.close()


if __name__ == "__main__":
    asyncio.run(main())