"""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())