├── .flake8 ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── botright ├── __init__.py ├── botright.py ├── extended_typing.py ├── modules │ ├── __init__.py │ ├── faker.py │ ├── geetest.py │ ├── geetest.torchscript │ ├── geetest_helpers.py │ ├── hcaptcha.py │ ├── proxy_manager.py │ └── tmp_dir │ │ └── record_json │ │ ├── image_label_area_select.point.Please click on the rabbit's eye.default.json │ │ └── image_label_binary.None.Please click each image containing a mean of transportation.json └── playwright_mock │ ├── __init__.py │ ├── browser.py │ ├── frame.py │ ├── frame_locator.py │ ├── handles.py │ ├── keyboard.py │ ├── locator.py │ ├── mouse.py │ ├── page.py │ └── routes.py ├── docs ├── CONTRIBUTING.md ├── HISTORY.md ├── botright.rst └── index.rst ├── pyproject.toml ├── requirements-test.txt ├── requirements.txt ├── setup.cfg └── tests ├── __init__.py ├── assets ├── beforeunload.html ├── csp.html ├── digits │ ├── 1.png │ ├── 2.png │ └── 3.png ├── dom.html ├── drag-n-drop.html ├── empty.html ├── error.html ├── es6 │ ├── es6import.js │ ├── es6module.js │ └── es6pathimport.js ├── frames │ ├── frame.html │ ├── frameset.html │ ├── nested-frames.html │ └── two-frames.html ├── grid.html ├── injectedfile.js ├── injectedstyle.css ├── input │ ├── button.html │ ├── fileupload.html │ ├── mouse-helper.js │ ├── scrollable.html │ ├── select.html │ └── textarea.html ├── offscreenbuttons.html ├── playground.html ├── popup │ ├── popup.html │ └── window-open.html ├── shadow.html └── title.html ├── conftest.py ├── dev_test.py ├── playwright_tests ├── __init__.py ├── test_browsercontext.py ├── test_browsercontext_events.py ├── test_element_handle.py ├── test_element_handle_wait_for_element_state.py ├── test_frames.py ├── test_jshandle.py ├── test_locators.py └── test_page.py ├── server.py ├── test_geetestv3.py ├── test_geetestv4.py ├── test_hcaptcha.py ├── test_recaptcha.py ├── test_webrtc.py └── utils.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 200 3 | extend-ignore = E203, E704 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [vinyzu] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: vinyzu 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[BUG] ' 5 | labels: bug, help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Code Sample** 14 | If applicable, add a code sample to replicate the bug. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Desktop (please complete the following information):** 30 | - OS: [e.g. iOS] 31 | - Version [e.g. 22] 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement, question 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about Botright 4 | title: '[Question] ' 5 | labels: question, help wanted, documentation 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe your quesiton** 11 | A clear and concise question. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Screenshots** 17 | If applicable, add screenshots to help explain your problem. 18 | 19 | **Desktop (please complete the following information):** 20 | - OS: [e.g. iOS] 21 | - Version [e.g. 22] 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: reCognizer CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | Linting: 9 | runs-on: windows-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 3.11 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.11' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements-test.txt 20 | - name: (Linting) isort 21 | run: isort . --check-only 22 | - name: (Linting) Flake8 23 | run: flake8 . 24 | - name: (Linting) MyPy 25 | run: mypy botright 26 | - name: (Linting) Black 27 | run: black . --check 28 | 29 | Build: 30 | strategy: 31 | matrix: 32 | os: [windows-latest, ubuntu-latest] 33 | python-version: ['3.8', '3.9', '3.10', '3.11'] 34 | 35 | runs-on: ${{ matrix.os }} 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Set up Python ${{ matrix.python-version }} 39 | uses: actions/setup-python@v4 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | - name: Install dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | pip install -r requirements-test.txt 46 | pip install -e . 47 | python -c "import os; os.environ['TOKENIZERS_PARALLELISM'] = 'false'" 48 | - name: Install Chrome Browser 49 | uses: browser-actions/setup-chrome@v1 50 | - name: Install Chromium Driver 51 | run: python -m playwright install chromium 52 | - name: Install HuggingFace Models 53 | run: | 54 | pip install -U "huggingface_hub[cli]" 55 | huggingface-cli download flavour/CLIP-ViT-B-16-DataComp.XL-s13B-b90K 56 | huggingface-cli download CIDAS/clipseg-rd64-refined 57 | - name: Test with PyTest 58 | run: pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | model/ 31 | 32 | datas/ 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 99 | __pypackages__/ 100 | 101 | # Celery stuff 102 | celerybeat-schedule 103 | celerybeat.pid 104 | 105 | # SageMath parsed files 106 | *.sage.py 107 | 108 | # Environments 109 | .env 110 | .venv 111 | env/ 112 | venv/ 113 | ENV/ 114 | env.bak/ 115 | venv.bak/ 116 | 117 | # Spyder project settings 118 | .spyderproject 119 | .spyproject 120 | 121 | # Rope project settings 122 | .ropeproject 123 | 124 | # mkdocs documentation 125 | /site 126 | 127 | # mypy 128 | .mypy_cache/ 129 | .dmypy.json 130 | dmypy.json 131 | 132 | # Pyre type checker 133 | .pyre/ 134 | 135 | # templates 136 | .github/templates/* 137 | 138 | .idea 139 | .vscode 140 | 141 | # Yolo Models 142 | *.pt 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Botright v0.5.1 2 | ![Tests & Linting](https://github.com/Vinyzu/botright/actions/workflows/ci.yml/badge.svg) 3 | [![](https://img.shields.io/pypi/v/botright.svg?color=1182C3)](https://pypi.org/project/botright/) 4 | [![Downloads](https://static.pepy.tech/badge/botright)](https://pepy.tech/project/botright) 5 | 6 | --- 7 | 8 |
9 |

Sponsors

10 | 11 | Scrapeless Banner 12 | 13 | If you're looking for an automated browser tool focused on bypassing website bot detection mechanisms, I can personally recommend [**Scrapeless Scraping Browser**](https://www.scrapeless.com/en/product/scraping-browser?utm_medium=github&utm_campaign=vinyzu). It's suitable for tasks like web scraping, automated testing, and data collection — especially in scenarios that involve complex anti-bot systems.
14 | [**Scraping Browser**](https://app.scrapeless.com/passport/login?utm_medium=github&utm_campaign=vinyzu) is a cloud-based browser platform built for high-concurrency web scraping and AI automation. It features advanced stealth modes and powerful anti-blocking capabilities, making it easy to handle dynamic websites, anti-bot mechanisms, and CAPTCHA challenges. It supports one-click scraping of single pages or entire websites, and can extract content based on prompts. It offers an efficient, stable, and cost-effective solution for large-scale data collection.

15 | **Key Features:** 16 | 17 | * **High-concurrency scraping support**: Instantly launch 50–10,000+ browser instances with no server restrictions 18 | * **Bypass anti-bot mechanisms**: Automatically handles reCAPTCHA, Cloudflare, WAF, DataDome, and more 19 | * **Highly human-like browsing environment**: Dynamic fingerprint spoofing and simulation of real user behavior 20 | * **70M+ residential IP proxies**: Global coverage with geo-targeting and automatic rotation 21 | * **Real-time debugging and session replay**: Built-in Session Inspector and Live View for real-time browser session monitoring and control 22 | * **Low operating costs**: Proxy usage costs just $1.26–$1.80/GB 23 | * **Plug-and-play**: Compatible with Puppeteer / Playwright / Python / Node.js for easy integration 24 | * **Multiple scraping modes supported**: Single-page extraction / full-site scraping / prompt-based content extraction 25 | 26 | [**Scrapeless**](https://www.scrapeless.com/en?utm_medium=github&utm_campaign=vinyzu) is an all-in-one, highly scalable data scraping tool designed for enterprises and developers. In addition to the Scraping Browser, it also offers [**Scraping API**](https://www.scrapeless.com/en/product/scraping-api?utm_medium=github&utm_campaign=vinyzu), [**Deep SerpAPI**](https://www.scrapeless.com/en/product/deep-serp-api?utm_medium=github&utm_campaign=vinyzu), and [**Proxies**](https://www.scrapeless.com/en/product/proxies?utm_medium=github&utm_campaign=vinyzu) services. 27 | 👉 Learn more: [Scrapeless Scraping Browser Playground](https://app.scrapeless.com/passport/login?utm_medium=github&utm_campaign=vinyzu) | [Scrapeless Scraping Browser Docs](https://docs.scrapeless.com/en/scraping-browser/quickstart/introduction/?utm_medium=github&utm_campaign=vinyzu) 28 | 29 | --- 30 | 31 | [![Evomi Banner](https://my.evomi.com/images/brand/cta.png)](https://evomi.com?utm_source=github&utm_medium=banner&utm_campaign=Vinyzu-Botright) 32 | 33 | [**Evomi**](https://evomi.com?utm_source=github&utm_medium=banner&utm_campaign=Vinyzu-Botright) is your Swiss Quality, affordable Proxy Provider. I can personally recommend them for their High Quality Residential Proxies. 34 | 35 | - 🌍 **Global Presence**: Available in 150+ Countries 36 | - ⚡ **Guaranteed Low Latency** 37 | - 🔒 **Swiss Quality and Privacy** 38 | - 🎁 **Free Trial**: No Credit Card Required 39 | - 🛡️ **99.9% Uptime** 40 | - 🤝 **Special IP Pool selection**: Optimize for fast, quality or quantity of ips 41 | - 🔧 **Easy Integration**: Compatible with most software and programming languages 42 |
43 | 44 | --- 45 | 46 | ## Install it from PyPI 47 | 48 | ```bash 49 | pip install botright 50 | playwright install 51 | ``` 52 | 53 | --- 54 | 55 | ## Usage 56 | 57 | ### Botright is currently only available in async mode. 58 | ### It is fully plugable with your existing playwright code. You only have to change your browser initialization! 59 | 60 | ```py 61 | import asyncio 62 | 63 | import botright 64 | 65 | 66 | async def main(): 67 | botright_client = await botright.Botright() 68 | browser = await botright_client.new_browser() 69 | page = await browser.new_page() 70 | 71 | # Continue by using the Page 72 | await page.goto("https://google.com") 73 | 74 | await botright_client.close() 75 | 76 | 77 | if __name__ == "__main__": 78 | asyncio.run(main()) 79 | ``` 80 | 81 | Read the [Documentation](https://github.com/Vinyzu/Botright/blob/main/docs/index.rst) 82 | 83 | --- 84 | 85 | ## Browser Stealth 86 | 87 | Botright uses a vast amount of techniques to hide its functionality as a bot from websites. 88 | To enhance stealth, since Version 0.3, it uses a real Chromium-based browser from the local machine to start up a botted browser. 89 | For best stealth, you want to install [Ungoogled Chromium](https://ungoogled-software.github.io/ungoogled-chromium-binaries/). 90 | 91 | Furthermore, it uses self-scraped [chrome-fingerprints](https://github.com/Vinyzu/chrome-fingerprints) to build up a fake browser fingerprint and to deceive website into thinking it is legit. 92 | 93 | 94 | | Test | Status | Score | 95 | |-----------------------------------------------------------------------------------------------------|--------|------------------------------------------------------------| 96 | | **reCaptcha Score** | ✔️ | 0.9 | 97 | | => [nopecha.com](https://nopecha.com/demo/recaptcha#v3) | ✔️ | 0.9 | 98 | | => [recaptcha-demo.appspot.com](https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php) | ✔️ | 0.9 | 99 | | => [berstend.github.io](https://berstend.github.io/static/recaptcha/v3-programmatic.html) | ✔️ | 0.9 | 100 | | => [antcpt.com](https://antcpt.com/score_detector/) | ❌❓ | 0.1 (Detects Legitimate Browsers as Bad) | 101 | | [**CreepJS**](https://abrahamjuliot.github.io/creepjs/) | ✔️ | ~65.5% (With Canvas Manipulation 52%) | 102 | | **DataDome** | ✔️ | | 103 | | => [antoinevastel.com](https://antoinevastel.com/bots/datadome) | ✔️ | | 104 | | => [datadome.co](https://datadome.co/bot-tester/) | ✔️ | | 105 | | **Imperva** | ✔️❓ | (Cant find approved Testing Sites) | 106 | | => [ticketmaster.es](https://www.ticketmaster.es/) | ✔️ | | 107 | | **Cloudflare** | ✔️ | | 108 | | => [Turnstile](https://nopecha.com/demo/turnstile) | ✔️ | (Using Undetected-Playwright-Python) | 109 | | => [Interstitial](https://nopecha.com/demo/cloudflare) | ✔️ | (Using Undetected-Playwright-Python) | 110 | | [**SannySoft**](https://bot.sannysoft.com/) | ✔️ | | 111 | | [**Incolumitas**](https://bot.incolumitas.com/) | ✔️ | 0.8-1.0 | 112 | | [**Fingerprint.com**](https://fingerprint.com/products/bot-detection/) | ✔️ | | 113 | | [**IpHey**](https://iphey.com/) | ✔️ | | 114 | | [**BrowserScan**](https://browserscan.net/) | ✔️ | | 115 | | [**PixelScan**](https://pixelscan.net/) | ❓ | (Platform Test Outdated & Maybe caused by WebGL-disabling) | 116 | | [**Bet365**](https://www.bet365.com/#/AC/B1/C1/D1002/E79147586/G40/) | ✔️ | Currently only using `mask_fingerprint=False` | 117 | 118 | 119 | --- 120 | 121 | ## Captcha Solving 122 | 123 | Botright is able to solve a wide viarity of Captchas. 124 | For Documentation of these functions visit [BotrightDocumentation](https://github.com/Vinyzu/Botright/blob/main/docs/botright.rst). 125 | 126 | It uses Computer Vision/Artificial Intelligence and other Methods to solve these Captchas. 127 | 128 | You dont need to pay for any Captcha Solving APIs and you can solve Captchas with just one simple function call. 129 | 130 | Here all Captchas supported as of now: 131 | 132 | | Captcha Type | Supported | Solved By | Success Rate | 133 | |:------------------------------------:|:---------:|:-------------------------------:|--------------| 134 | | `hCaptcha` | ✔️ ❓ | hcaptcha-challenger (outdated) | up to 90% | 135 | | `reCaptcha` | ✔️ | reCognizer | 50%-80% | 136 | | `geeTestv3` Currently Not Available! | 137 | | v3 Intelligent Mode | ✔️ | botrights stealthiness | 100% | 138 | | v3 Slider Captcha | ✔️ | cv2.matchTemplate | 100% | 139 | | v3 Nine Captcha | ✔️ | CLIP Detection | 50% | 140 | | v3 Icon Captcha | ✔️ | cv2.matchTemplate / SSIM / CLIP | 70% | 141 | | v3 Space Captcha | ❌ | Not solvable | 0% | 142 | | `geeTestv4` Currently Not Available! | 143 | | v4 Intelligent Mode | ✔️ | botrights stealthiness | 100% | 144 | | v4 Slider Captcha | ✔️ | cv2.matchTemplate | 100% | 145 | | v4 GoBang Captcha | ✔️ | Math Calculations | 100% | 146 | | v4 Icon Captcha | ✔️ | cv2.matchTemplate / SSIM / CLIP | 60% | 147 | | v4 IconCrush Captcha | ✔️ | Math Calculations | 100% | 148 | 149 | ## Development 150 | 151 | Read the [CONTRIBUTING.md](https://github.com/Vinyzu/Botright/blob/main/docs/CONTRIBUTING.md) file. 152 | 153 | --- 154 | 155 | ## Copyright and License 156 | © [Vinyzu](https://github.com/Vinyzu/) 157 | 158 | [GNU GPL](https://choosealicense.com/licenses/gpl-3.0/) 159 | 160 | (Commercial Usage is allowed, but source, license and copyright has to made available. Botright does not provide and Liability or Warranty) 161 | 162 | --- 163 | 164 | ## Thanks to 165 | 166 | [Kaliiiiiiiiii](https://github.com/kaliiiiiiiiii/) (For shared knowledge of Anti-Browser-Detection Measures) 167 | 168 | [Kaliiiiiiiiii](https://github.com/kaliiiiiiiiii/) (For Main-Authoring [Undetected-Playwright](https://github.com/kaliiiiiiiiii/undetected-playwright-python) (Co-Authored by me) ) 169 | 170 | [QIN2DIM](https://github.com/QIN2DIM/) (For his great AI work) 171 | 172 | [MaxAndolini](https://github.com/MaxAndolini) (For shared knowledge of hCaptcha bypassing) 173 | 174 | [CreativeProxies](https://creativeproxies.com) (For sponsoring me with Proxies) 175 | 176 | --- 177 | 178 | ![Version](https://img.shields.io/badge/Botright-v0.5.1-blue) 179 | ![License](https://img.shields.io/badge/License-GNU%20GPL-green) 180 | ![Python](https://img.shields.io/badge/Python-v3.x-lightgrey) 181 | 182 | [![my-discord](https://img.shields.io/badge/My_Discord-000?style=for-the-badge&logo=google-chat&logoColor=blue)](https://discordapp.com/users/935224495126487150) 183 | [![buy-me-a-coffee](https://img.shields.io/badge/Buy_Me_A_Coffee-000?style=for-the-badge&logo=ko-fi&logoColor=brown)](https://ko-fi.com/vinyzu) 184 | -------------------------------------------------------------------------------- /botright/__init__.py: -------------------------------------------------------------------------------- 1 | from .botright import Botright 2 | from .modules.faker import Faker 3 | from .modules.proxy_manager import ProxyManager 4 | 5 | VERSION = "0.5.1" 6 | 7 | __all__ = ["Botright", "Faker", "ProxyManager", "VERSION"] 8 | -------------------------------------------------------------------------------- /botright/botright.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import os 5 | import shutil 6 | from tempfile import TemporaryDirectory, gettempdir 7 | from typing import Any, Dict, List, Optional 8 | 9 | import browsers 10 | import hcaptcha_challenger as solver 11 | import loguru 12 | from async_class import AsyncObject 13 | from chrome_fingerprints import AsyncFingerprintGenerator 14 | from playwright.async_api import APIResponse, Playwright, async_playwright 15 | from undetected_playwright.async_api import async_playwright as undetected_async_playwright 16 | 17 | from botright.playwright_mock import browser 18 | 19 | from .modules import Faker, ProxyManager 20 | from .playwright_mock import BrowserContext 21 | 22 | logging.getLogger("websockets").setLevel(logging.WARNING) 23 | logging.getLogger("httpx").setLevel(logging.WARNING) 24 | loguru.logger.disable("hcaptcha_challenger") 25 | 26 | 27 | class Botright(AsyncObject): 28 | def __init__( 29 | self, 30 | headless: Optional[bool] = False, 31 | block_images: Optional[bool] = False, 32 | cache_responses: Optional[bool] = False, 33 | user_action_layer: Optional[bool] = False, 34 | scroll_into_view: Optional[bool] = True, 35 | spoof_canvas: Optional[bool] = True, 36 | mask_fingerprint: Optional[bool] = True, 37 | use_undetected_playwright: Optional[bool] = False, 38 | ) -> None: 39 | """ 40 | Initialize a Botright instance with specified configurations. 41 | 42 | Args: 43 | headless (bool, optional): Whether to run the browser in headless mode. Defaults to False. 44 | block_images (bool, optional): Whether to block images in the browser. Defaults to False. 45 | cache_responses (bool, optional): Whether to cache HTTP responses. Defaults to False. 46 | user_action_layer (bool, optional): Whether to enable user action simulation layer. Defaults to False. 47 | scroll_into_view (bool, optional): Whether to scroll elements into view automatically. Defaults to True. 48 | spoof_canvas (bool, optional): Whether to disable canvas fingerprinting protection. Defaults to True. 49 | mask_fingerprint (bool, optional): Whether to mask the browser fingerprint. Defaults to True. 50 | use_undetected_playwright (bool, optional): Whether to use undetected_playwright (TEMP). Defaults to False. 51 | """ 52 | # This Init Function is only for intellisense. 53 | super().__init__() 54 | 55 | async def __ainit__( 56 | self, 57 | headless: Optional[bool] = False, 58 | block_images: Optional[bool] = False, 59 | cache_responses: Optional[bool] = False, 60 | user_action_layer: Optional[bool] = False, 61 | scroll_into_view: Optional[bool] = True, 62 | spoof_canvas: Optional[bool] = True, 63 | mask_fingerprint: Optional[bool] = True, 64 | use_undetected_playwright: Optional[bool] = False, 65 | ) -> None: 66 | """ 67 | Initialize a Botright instance with specified configurations. 68 | 69 | Args: 70 | headless (bool, optional): Whether to run the browser in headless mode. Defaults to False. 71 | block_images (bool, optional): Whether to block images in the browser. Defaults to False. 72 | cache_responses (bool, optional): Whether to cache HTTP responses. Defaults to False. 73 | user_action_layer (bool, optional): Whether to enable user action simulation layer. Defaults to False. 74 | scroll_into_view (bool, optional): Whether to scroll elements into view automatically. Defaults to True. 75 | spoof_canvas (bool, optional): Whether to disable canvas fingerprinting protection. Defaults to True. 76 | mask_fingerprint (bool, optional): Whether to mask the browser fingerprint. Defaults to True. 77 | use_undetected_playwright (bool, optional): Whether to use undetected_playwright . EXPERIMENTAL (TEMP). Defaults to False. 78 | """ 79 | 80 | # Init local-side of the ModelHub 81 | solver.install(upgrade=True) 82 | # Starting Playwright 83 | if use_undetected_playwright: 84 | # (TODO: TEMP) 85 | self.playwright: Playwright = await undetected_async_playwright().start() # type: ignore 86 | else: 87 | self.playwright = await async_playwright().start() 88 | 89 | # Getting Chromium based browser engine and deleting botright temp dirs 90 | self.browser = self.get_browser_engine() 91 | self.delete_botright_temp_dirs() 92 | 93 | # Setting Values 94 | self.headless = headless 95 | self.block_images = block_images 96 | self.cache_responses = cache_responses 97 | self.scroll_into_view = scroll_into_view 98 | self.user_action_layer = user_action_layer 99 | self.mask_fingerprint = mask_fingerprint 100 | self.use_undetected_playwright = use_undetected_playwright 101 | self.cache: Dict[str, APIResponse] = {} 102 | 103 | # '--disable-gpu', '--incognito', '--disable-blink-features=AutomationControlled' 104 | # fmt: off 105 | self.flags = ['--incognito', '--accept-lang=en-US', '--lang=en-US', '--no-pings', '--mute-audio', '--no-first-run', '--no-default-browser-check', '--disable-cloud-import', 106 | '--disable-gesture-typing', '--disable-offer-store-unmasked-wallet-cards', '--disable-offer-upload-credit-cards', '--disable-print-preview', '--disable-voice-input', 107 | '--disable-wake-on-wifi', '--disable-cookie-encryption', '--ignore-gpu-blocklist', '--enable-async-dns', '--enable-simple-cache-backend', '--enable-tcp-fast-open', 108 | '--prerender-from-omnibox=disabled', '--enable-web-bluetooth', '--disable-features=AudioServiceOutOfProcess,IsolateOrigins,site-per-process,TranslateUI,BlinkGenPropertyTrees', 109 | '--aggressive-cache-discard', '--disable-extensions', '--disable-ipc-flooding-protection', '--disable-blink-features=AutomationControlled', '--test-type', 110 | '--enable-features=NetworkService,NetworkServiceInProcess,TrustTokens,TrustTokensAlwaysAllowIssuance', '--disable-component-extensions-with-background-pages', 111 | '--disable-default-apps', '--disable-breakpad', '--disable-component-update', '--disable-domain-reliability', '--disable-sync', '--disable-client-side-phishing-detection', 112 | '--disable-hang-monitor', '--disable-popup-blocking', '--disable-prompt-on-repost', '--metrics-recording-only', '--safebrowsing-disable-auto-update', '--password-store=basic', 113 | '--autoplay-policy=no-user-gesture-required', '--use-mock-keychain', '--force-webrtc-ip-handling-policy=disable_non_proxied_udp', 114 | '--webrtc-ip-handling-policy=disable_non_proxied_udp', '--disable-session-crashed-bubble', '--disable-crash-reporter', '--disable-dev-shm-usage', '--force-color-profile=srgb', 115 | '--disable-translate', '--disable-background-networking', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-infobars', 116 | '--hide-scrollbars', '--disable-renderer-backgrounding', '--font-render-hinting=none', '--disable-logging', '--enable-surface-synchronization', 117 | '--run-all-compositor-stages-before-draw', '--disable-threaded-animation', '--disable-threaded-scrolling', '--disable-checker-imaging', 118 | '--disable-new-content-rendering-timeout', '--disable-image-animation-resync', '--disable-partial-raster', '--blink-settings=primaryHoverType=2,availableHoverTypes=2,' 119 | 'primaryPointerType=4,availablePointerTypes=4', '--disable-layer-tree-host-memory-pressure'] 120 | # fmt: on 121 | 122 | # Collecting items that can be stopped 123 | self.stoppable: List[Any] = [] 124 | self.temp_dirs: List[TemporaryDirectory] = [] # type: ignore 125 | 126 | if spoof_canvas and self.mask_fingerprint: 127 | if self.browser["browser_type"] != "chromium": 128 | self.flags.append("--disable-reading-from-canvas") 129 | else: 130 | self.flags.append("--fingerprinting-canvas-image-data-noise") 131 | 132 | self.fingerprint_generator = AsyncFingerprintGenerator() 133 | 134 | async def new_browser(self, proxy: Optional[str] = None, **launch_arguments) -> BrowserContext: 135 | """ 136 | Create a new Botright browser instance with specified configurations. 137 | 138 | Args: 139 | proxy (str, optional): Proxy server URL to use for the browser. Defaults to None. 140 | **launch_arguments: Additional launch arguments to the browser. See at `Playwright Docs `_. 141 | 142 | Returns: 143 | BrowserContext: A new browser context for web scraping or automation. 144 | """ 145 | 146 | # Calling ProxyManager and Faker to get necessary information for Botright 147 | _proxy: ProxyManager = await ProxyManager(self, proxy) 148 | _faker: Faker = await Faker(self, _proxy) 149 | 150 | # Launching Main Browser 151 | if self.mask_fingerprint: 152 | flags = self.flags + [f"--user-agent={_faker.fingerprint.navigator.user_agent}"] 153 | else: 154 | flags = self.flags 155 | 156 | _browser = await browser.new_browser(self, _proxy, _faker, flags, **launch_arguments) 157 | _browser.proxy = _proxy 158 | _browser.faker = _faker 159 | _browser.user_action_layer = self.user_action_layer 160 | _browser.scroll_into_view = self.scroll_into_view 161 | _browser.mask_fingerprint = self.mask_fingerprint 162 | 163 | await _browser.grant_permissions(["notifications", "geolocation"]) 164 | self.stoppable.append(_browser) 165 | 166 | return _browser 167 | 168 | async def __adel__(self) -> None: 169 | """ 170 | Cleanup method called when the Botright instance is closed. 171 | Closes all associated browser instances and stops the Playwright engine. 172 | """ 173 | for obj in self.stoppable: 174 | try: 175 | await obj.close() 176 | except Exception: 177 | pass 178 | 179 | try: 180 | await self.playwright.stop() 181 | except Exception: 182 | pass 183 | 184 | for temp_dir in self.temp_dirs: 185 | if os.path.exists(temp_dir.name): 186 | try: 187 | temp_dir.cleanup() 188 | except Exception: 189 | pass 190 | 191 | @staticmethod 192 | def get_browser_engine() -> browsers.Browser: 193 | """ 194 | Get the browser engine based on Chromium to use for Playwright based on system availability. 195 | If available, prefers Ungoogled Chromium for stealthier browsing. 196 | 197 | Returns: 198 | browsers.Browser: The selected browser engine. 199 | Raises: 200 | EnvironmentError: If no Chromium based browser is found on the system. 201 | """ 202 | # Ungoogled Chromium preferred (most stealthy) 203 | if chromium := browsers.get("chromium"): 204 | return chromium 205 | print("\033[1;33;48m[WARNING] Ungoogled Chromium not found. Recommended for Canvas Manipulation. Download at https://ungoogled-software.github.io/ungoogled-chromium-binaries/ \033[0m") 206 | 207 | # Chrome preferred (much stealthier) 208 | if chrome := browsers.get("chrome"): 209 | return chrome 210 | 211 | not_supported = ["firefox", "msie", "opera", "msedge"] 212 | for browser_engine in browsers.browsers(): 213 | if browser_engine["browser_type"] not in not_supported: 214 | return browser_engine 215 | 216 | raise EnvironmentError("No Chromium based browser found") 217 | 218 | @staticmethod 219 | def delete_botright_temp_dirs() -> None: 220 | temp_path = gettempdir() 221 | 222 | for temp_dir in os.listdir(temp_path): 223 | # Check if the item is a directory and starts with 'botright-' 224 | if os.path.isdir(os.path.join(temp_path, temp_dir)) and temp_dir.startswith("botright-"): 225 | # If it matches, delete the folder and its contents 226 | shutil.rmtree(os.path.join(temp_path, temp_dir)) 227 | -------------------------------------------------------------------------------- /botright/extended_typing.py: -------------------------------------------------------------------------------- 1 | from botright.playwright_mock import BrowserContext, ElementHandle, Frame, FrameLocator, JSHandle, Keyboard, Locator, Mouse, Page, Request, Route, new_page 2 | 3 | 4 | class NotSupportedError(NotImplementedError): 5 | def __init__(self, message): 6 | super().__init__( 7 | f"{message} \n Some Bindings and Exposures are (currently) not supported. Learn more at https://github.com/kaliiiiiiiiii/undetected-playwright-python/issues/5 and " 8 | "https://github.com/kaliiiiiiiiii/undetected-playwright-python/discussions/6" 9 | ) 10 | 11 | 12 | __all__ = ["ElementHandle", "Frame", "FrameLocator", "JSHandle", "Locator", "Mouse", "Keyboard", "Page", "new_page", "BrowserContext", "Route", "Request", "NotSupportedError"] 13 | -------------------------------------------------------------------------------- /botright/modules/__init__.py: -------------------------------------------------------------------------------- 1 | from .faker import Faker 2 | from .proxy_manager import ProxyManager 3 | 4 | __all__ = ["Faker", "ProxyManager"] 5 | -------------------------------------------------------------------------------- /botright/modules/faker.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | 5 | from async_class import AsyncObject, link 6 | from chrome_fingerprints import ChromeFingerprint 7 | 8 | from .proxy_manager import ProxyManager 9 | 10 | 11 | class Faker(AsyncObject): 12 | locale: str = "" 13 | language_code: str = "" 14 | fingerprint: ChromeFingerprint 15 | 16 | async def __ainit__(self, botright, proxy): 17 | """ 18 | Initialize a Faker instance with a botright instance and a proxy, and generate fake data. 19 | 20 | Args: 21 | botright: An instance of Botright for linking purposes. 22 | proxy: The proxy to be used for generating locale-related fake data. 23 | """ 24 | self.botright = botright 25 | link(self, botright) 26 | 27 | threads = [self.get_computer(), self.get_locale(proxy)] 28 | await asyncio.gather(*threads) 29 | 30 | @staticmethod 31 | def adjust_browser_version(useragent: str, browser_type: str, browser_version: str) -> str: 32 | """ 33 | Adjust the browser version in a user agent string. 34 | 35 | Args: 36 | useragent (str): The user agent string to be adjusted. 37 | browser_type (str): The type of the browser (e.g., "Firefox"). 38 | browser_version (str): The desired browser version (e.g., "92.0"). 39 | 40 | Returns: 41 | str: The adjusted user agent string. 42 | """ 43 | ua_browser_version = [word for word in useragent.split() if browser_type.capitalize() + "/" in word] 44 | browser_version_list = browser_version.split(".")[:2] + ["0", "0"] 45 | browser_version = ".".join(browser_version_list) 46 | return useragent.replace(ua_browser_version[0], f"{browser_type}/{browser_version}") 47 | 48 | async def get_computer(self) -> None: 49 | """ 50 | Generate fake computer-related data such as user agent, vendor, GPU information, screen dimensions, etc. 51 | """ 52 | self.fingerprint = await self.botright.fingerprint_generator.get_fingerprint() 53 | 54 | async def get_locale(self, proxy: ProxyManager) -> None: 55 | """ 56 | Generate fake locale-related data such as locale and language code based on the provided proxy. 57 | 58 | Args: 59 | proxy (ProxyManager): The proxy manager used to determine the locale. 60 | """ 61 | language_dict = { 62 | "AF": ["pr-AF", "pr"], 63 | "AX": ["sw-AX", "sw"], 64 | "AL": ["sq-AL", "sq"], 65 | "DZ": ["ar-DZ", "ar"], 66 | "AS": ["en-AS", "en"], 67 | "AD": ["ca-AD", "ca"], 68 | "AO": ["po-AO", "po"], 69 | "AI": ["en-AI", "en"], 70 | "AG": ["en-AG", "en"], 71 | "AR": ["gr-AR", "gr"], 72 | "AM": ["hy-AM", "hy"], 73 | "AW": ["nl-AW", "nl"], 74 | "AU": ["en-AU", "en"], 75 | "AT": ["ba-AT", "ba"], 76 | "AZ": ["az-AZ", "az"], 77 | "BS": ["en-BS", "en"], 78 | "BH": ["ar-BH", "ar"], 79 | "BD": ["be-BD", "be"], 80 | "BB": ["en-BB", "en"], 81 | "BY": ["be-BY", "be"], 82 | "BE": ["de-BE", "de"], 83 | "BQ": ["en-BQ", "en"], 84 | "BZ": ["bj-BZ", "bj"], 85 | "BJ": ["fr-BJ", "fr"], 86 | "BM": ["en-BM", "en"], 87 | "BT": ["dz-BT", "dz"], 88 | "BO": ["ay-BO", "ay"], 89 | "BA": ["bo-BA", "bo"], 90 | "BW": ["en-BW", "en"], 91 | "BV": ["no-BV", "no"], 92 | "BR": ["po-BR", "po"], 93 | "IO": ["en-IO", "en"], 94 | "BN": ["ms-BN", "ms"], 95 | "BG": ["bu-BG", "bu"], 96 | "BF": ["fr-BF", "fr"], 97 | "BI": ["fr-BI", "fr"], 98 | "KH": ["kh-KH", "kh"], 99 | "CM": ["en-CM", "en"], 100 | "CA": ["en-CA", "en"], 101 | "CV": ["po-CV", "po"], 102 | "KY": ["en-KY", "en"], 103 | "CF": ["fr-CF", "fr"], 104 | "TD": ["ar-TD", "ar"], 105 | "CL": ["sp-CL", "sp"], 106 | "CN": ["zh-CN", "zh"], 107 | "CX": ["en-CX", "en"], 108 | "CC": ["en-CC", "en"], 109 | "CO": ["sp-CO", "sp"], 110 | "KM": ["ar-KM", "ar"], 111 | "CG": ["fr-CG", "fr"], 112 | "CD": ["fr-CD", "fr"], 113 | "CK": ["en-CK", "en"], 114 | "CR": ["sp-CR", "sp"], 115 | "CI": ["fr-CI", "fr"], 116 | "HR": ["hr-HR", "hr"], 117 | "CU": ["sp-CU", "sp"], 118 | "CW": ["en-CW", "en"], 119 | "CY": ["el-CY", "el"], 120 | "CZ": ["ce-CZ", "ce"], 121 | "DK": ["da-DK", "da"], 122 | "DJ": ["ar-DJ", "ar"], 123 | "DM": ["en-DM", "en"], 124 | "DO": ["sp-DO", "sp"], 125 | "EC": ["sp-EC", "sp"], 126 | "EG": ["ar-EG", "ar"], 127 | "SV": ["sp-SV", "sp"], 128 | "GQ": ["fr-GQ", "fr"], 129 | "ER": ["ar-ER", "ar"], 130 | "EE": ["es-EE", "es"], 131 | "ET": ["am-ET", "am"], 132 | "FK": ["en-FK", "en"], 133 | "FO": ["da-FO", "da"], 134 | "FJ": ["en-FJ", "en"], 135 | "FI": ["fi-FI", "fi"], 136 | "FR": ["fr-FR", "fr"], 137 | "GF": ["fr-GF", "fr"], 138 | "PF": ["fr-PF", "fr"], 139 | "TF": ["fr-TF", "fr"], 140 | "GA": ["fr-GA", "fr"], 141 | "GM": ["en-GM", "en"], 142 | "GE": ["ka-GE", "ka"], 143 | "DE": ["de-DE", "de"], 144 | "GH": ["en-GH", "en"], 145 | "GI": ["en-GI", "en"], 146 | "GR": ["el-GR", "el"], 147 | "GL": ["ka-GL", "ka"], 148 | "GD": ["en-GD", "en"], 149 | "GP": ["fr-GP", "fr"], 150 | "GU": ["ch-GU", "ch"], 151 | "GT": ["sp-GT", "sp"], 152 | "GG": ["en-GG", "en"], 153 | "GN": ["fr-GN", "fr"], 154 | "GW": ["po-GW", "po"], 155 | "GY": ["en-GY", "en"], 156 | "HT": ["fr-HT", "fr"], 157 | "HM": ["en-HM", "en"], 158 | "VA": ["it-VA", "it"], 159 | "HN": ["sp-HN", "sp"], 160 | "HK": ["en-HK", "en"], 161 | "HU": ["hu-HU", "hu"], 162 | "IS": ["is-IS", "is"], 163 | "IN": ["en-IN", "en"], 164 | "ID": ["in-ID", "in"], 165 | "IR": ["fa-IR", "fa"], 166 | "IQ": ["ar-IQ", "ar"], 167 | "IE": ["en-IE", "en"], 168 | "IM": ["en-IM", "en"], 169 | "IL": ["ar-IL", "ar"], 170 | "IT": ["it-IT", "it"], 171 | "JM": ["en-JM", "en"], 172 | "JP": ["jp-JP", "jp"], 173 | "JE": ["en-JE", "en"], 174 | "JO": ["ar-JO", "ar"], 175 | "KZ": ["ka-KZ", "ka"], 176 | "KE": ["en-KE", "en"], 177 | "KI": ["en-KI", "en"], 178 | "KP": ["ko-KP", "ko"], 179 | "KR": ["ko-KR", "ko"], 180 | "KW": ["ar-KW", "ar"], 181 | "KG": ["ki-KG", "ki"], 182 | "LA": ["la-LA", "la"], 183 | "LV": ["la-LV", "la"], 184 | "LB": ["ar-LB", "ar"], 185 | "LS": ["en-LS", "en"], 186 | "LR": ["en-LR", "en"], 187 | "LY": ["ar-LY", "ar"], 188 | "LI": ["de-LI", "de"], 189 | "LT": ["li-LT", "li"], 190 | "LU": ["de-LU", "de"], 191 | "MO": ["po-MO", "po"], 192 | "MK": ["mk-MK", "mk"], 193 | "MG": ["fr-MG", "fr"], 194 | "MW": ["en-MW", "en"], 195 | "MY": ["en-MY", "en"], 196 | "MV": ["di-MV", "di"], 197 | "ML": ["fr-ML", "fr"], 198 | "MT": ["en-MT", "en"], 199 | "MH": ["en-MH", "en"], 200 | "MQ": ["fr-MQ", "fr"], 201 | "MR": ["ar-MR", "ar"], 202 | "MU": ["en-MU", "en"], 203 | "YT": ["fr-YT", "fr"], 204 | "MX": ["sp-MX", "sp"], 205 | "FM": ["en-FM", "en"], 206 | "MD": ["ro-MD", "ro"], 207 | "MC": ["fr-MC", "fr"], 208 | "MN": ["mo-MN", "mo"], 209 | "MS": ["en-MS", "en"], 210 | "MA": ["ar-MA", "ar"], 211 | "MZ": ["po-MZ", "po"], 212 | "MM": ["my-MM", "my"], 213 | "NA": ["af-NA", "af"], 214 | "NR": ["en-NR", "en"], 215 | "NP": ["ne-NP", "ne"], 216 | "NL": ["nl-NL", "nl"], 217 | "NC": ["fr-NC", "fr"], 218 | "NZ": ["en-NZ", "en"], 219 | "NI": ["sp-NI", "sp"], 220 | "NE": ["fr-NE", "fr"], 221 | "NG": ["en-NG", "en"], 222 | "NU": ["en-NU", "en"], 223 | "NF": ["en-NF", "en"], 224 | "MP": ["ca-MP", "ca"], 225 | "NO": ["nn-NO", "nn"], 226 | "OM": ["ar-OM", "ar"], 227 | "PK": ["en-PK", "en"], 228 | "PW": ["en-PW", "en"], 229 | "PS": ["ar-PS", "ar"], 230 | "PA": ["sp-PA", "sp"], 231 | "PG": ["en-PG", "en"], 232 | "PY": ["gr-PY", "gr"], 233 | "PE": ["ay-PE", "ay"], 234 | "PH": ["en-PH", "en"], 235 | "PN": ["en-PN", "en"], 236 | "PL": ["po-PL", "po"], 237 | "PT": ["po-PT", "po"], 238 | "PR": ["en-PR", "en"], 239 | "QA": ["ar-QA", "ar"], 240 | "RE": ["fr-RE", "fr"], 241 | "RO": ["ro-RO", "ro"], 242 | "RU": ["ru-RU", "ru"], 243 | "RW": ["en-RW", "en"], 244 | "SH": ["en-SH", "en"], 245 | "KN": ["en-KN", "en"], 246 | "LC": ["en-LC", "en"], 247 | "PM": ["fr-PM", "fr"], 248 | "VC": ["en-VC", "en"], 249 | "WS": ["en-WS", "en"], 250 | "SM": ["it-SM", "it"], 251 | "ST": ["po-ST", "po"], 252 | "SA": ["ar-SA", "ar"], 253 | "SN": ["fr-SN", "fr"], 254 | "SC": ["cr-SC", "cr"], 255 | "SL": ["en-SL", "en"], 256 | "SG": ["zh-SG", "zh"], 257 | "SK": ["sl-SK", "sl"], 258 | "SI": ["sl-SI", "sl"], 259 | "SB": ["en-SB", "en"], 260 | "SO": ["ar-SO", "ar"], 261 | "SS": ["en-SS", "en"], 262 | "SX": ["en-SX", "en"], 263 | "ZA": ["af-ZA", "af"], 264 | "GS": ["en-GS", "en"], 265 | "ES": ["sp-ES", "sp"], 266 | "LK": ["si-LK", "si"], 267 | "SD": ["ar-SD", "ar"], 268 | "SR": ["nl-SR", "nl"], 269 | "SJ": ["no-SJ", "no"], 270 | "SZ": ["en-SZ", "en"], 271 | "SE": ["sw-SE", "sw"], 272 | "CH": ["fr-CH", "fr"], 273 | "SY": ["ar-SY", "ar"], 274 | "TW": ["zh-TW", "zh"], 275 | "TJ": ["ru-TJ", "ru"], 276 | "TZ": ["en-TZ", "en"], 277 | "TH": ["th-TH", "th"], 278 | "TL": ["po-TL", "po"], 279 | "TG": ["fr-TG", "fr"], 280 | "TK": ["en-TK", "en"], 281 | "TO": ["en-TO", "en"], 282 | "TT": ["en-TT", "en"], 283 | "TN": ["ar-TN", "ar"], 284 | "TR": ["tu-TR", "tu"], 285 | "TM": ["ru-TM", "ru"], 286 | "TC": ["en-TC", "en"], 287 | "TV": ["en-TV", "en"], 288 | "UG": ["en-UG", "en"], 289 | "UA": ["uk-UA", "uk"], 290 | "AE": ["ar-AE", "ar"], 291 | "GB": ["en-GB", "en"], 292 | "US": ["en-US", "en"], 293 | "UM": ["en-UM", "en"], 294 | "UY": ["sp-UY", "sp"], 295 | "UZ": ["ru-UZ", "ru"], 296 | "VU": ["bi-VU", "bi"], 297 | "VE": ["sp-VE", "sp"], 298 | "VN": ["vi-VN", "vi"], 299 | "VG": ["en-VG", "en"], 300 | "VI": ["en-VI", "en"], 301 | "WF": ["fr-WF", "fr"], 302 | "EH": ["be-EH", "be"], 303 | "YE": ["ar-YE", "ar"], 304 | "ZM": ["en-ZM", "en"], 305 | "ZW": ["bw-ZW", "bw"], 306 | "RS": ["sr-RS", "sr"], 307 | "ME": ["cn-ME", "cn"], 308 | "XK": ["sq-XK", "sq"], 309 | } 310 | country_code = proxy.country_code 311 | 312 | if country_code in language_dict: 313 | self.locale, self.language_code = language_dict[country_code] 314 | else: 315 | raise ValueError("Proxy Country not supported") 316 | -------------------------------------------------------------------------------- /botright/modules/geetest.torchscript: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinyzu/Botright/8b66fa0d8395b369252b32c327434c29837f7797/botright/modules/geetest.torchscript -------------------------------------------------------------------------------- /botright/modules/hcaptcha.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from typing import TYPE_CHECKING, Optional 5 | 6 | from hcaptcha_challenger.agents import AgentT 7 | 8 | if TYPE_CHECKING: 9 | from botright.extended_typing import BrowserContext, Page 10 | 11 | tmp_dir = Path(__file__).parent.joinpath("tmp_dir") 12 | 13 | 14 | class hCaptcha: 15 | def __init__(self, browser: BrowserContext, page: Page) -> None: 16 | """ 17 | Initialize an hCaptcha solver. 18 | 19 | Args: 20 | browser (BrowserContext): The Playwright browser context to use. 21 | page (Page): The Playwright page where hCaptcha challenges will be solved. 22 | """ 23 | self.browser = browser 24 | self.page = page 25 | 26 | self.retry_times = 8 27 | self.hcaptcha_agent = AgentT.from_page(page=page, tmp_dir=tmp_dir, self_supervised=True) 28 | 29 | async def mock_captcha(self, rq_data: str) -> None: 30 | """ 31 | Mock hCaptcha requests by intercepting network requests to getcaptcha. 32 | 33 | Args: 34 | rq_data (str): The data required for mocking the hCaptcha request. 35 | 36 | This method mocks the hCaptcha request and captures the generated hCaptcha token. 37 | """ 38 | 39 | async def mock_json(route, request): 40 | 41 | payload = {**request.post_data_json, "rqdata": rq_data, "hl": "en"} if rq_data else request.post_data_json 42 | response = await self.page.request.post(request.url, form=payload, headers=request.headers) 43 | await route.fulfill(response=response) 44 | 45 | await self.page.route("https://hcaptcha.com/getcaptcha/**", mock_json) 46 | 47 | async def solve_hcaptcha(self, rq_data: Optional[str] = None) -> Optional[str]: 48 | """ 49 | Solve an hCaptcha challenge. 50 | 51 | Args: 52 | rq_data (Optional[str]): Additional data required for solving the hCaptcha challenge. 53 | 54 | Returns: 55 | Optional[str]: The hCaptcha token if successfully solved; otherwise, None. 56 | 57 | This method captures the hCaptcha token by logging and mocking hCaptcha requests, then simulates clicking the 58 | hCaptcha checkbox to solve the challenge. 59 | """ 60 | # Mocking Captcha Request 61 | if rq_data: 62 | await self.mock_captcha(rq_data) 63 | # Clicking Captcha Checkbox 64 | await self.hcaptcha_agent.handle_checkbox() 65 | 66 | for pth in range(1, self.retry_times): 67 | result = await self.hcaptcha_agent.execute() 68 | if result == self.hcaptcha_agent.status.CHALLENGE_BACKCALL: 69 | await self.page.wait_for_timeout(500) 70 | fl = self.page.frame_locator(self.hcaptcha_agent.HOOK_CHALLENGE) 71 | await fl.locator("//div[@class='refresh button']").click() 72 | elif result == self.hcaptcha_agent.status.CHALLENGE_SUCCESS: 73 | if self.hcaptcha_agent.cr: 74 | captcha_token: str = self.hcaptcha_agent.cr.generated_pass_UUID 75 | return captcha_token 76 | 77 | return f"Exceeded maximum retry times of {self.retry_times}" 78 | 79 | async def get_hcaptcha(self, site_key: Optional[str] = "00000000-0000-0000-0000-000000000000", rq_data: Optional[str] = None) -> Optional[str]: 80 | """ 81 | Get an hCaptcha token for a specific site. 82 | 83 | Args: 84 | site_key (Optional[str]): The site key for the hCaptcha challenge (default is a demo site key). 85 | rq_data (Optional[str]): Additional data required for solving the hCaptcha challenge. 86 | 87 | Returns: 88 | Optional[str]: The hCaptcha token if successfully obtained; otherwise, None. 89 | 90 | This method opens a new page, navigates to a specified hCaptcha demo page with the given site key, and 91 | solves the hCaptcha challenge to obtain the token. 92 | """ 93 | page = await self.browser.new_page() 94 | await page.goto(f"https://accounts.hcaptcha.com/demo?sitekey={site_key}") 95 | return await page.solve_hcaptcha(rq_data=rq_data) 96 | -------------------------------------------------------------------------------- /botright/modules/proxy_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Dict, List, Optional 4 | 5 | import httpx 6 | from async_class import AsyncObject, link 7 | 8 | 9 | class SplitError(Exception): 10 | pass 11 | 12 | 13 | class ProxyCheckError(Exception): 14 | pass 15 | 16 | 17 | class ProxyManager(AsyncObject): 18 | proxy: str = "" 19 | http_proxy: Dict[str, str] = {} 20 | browser_proxy: Optional[Dict[str, str]] = None 21 | plain_proxy: str = "" 22 | _httpx: httpx.AsyncClient 23 | _phttpx: httpx.AsyncClient 24 | ip: str = "" 25 | port: str = "" 26 | username: str = "" 27 | password: str = "" 28 | country: str = "" 29 | country_code: str = "" 30 | region: str = "" 31 | city: str = "" 32 | zip: str = "" 33 | latitude: str = "" 34 | longitude: str = "" 35 | timezone: str = "" 36 | 37 | async def __ainit__(self, botright, proxy: str) -> None: 38 | """ 39 | Initialize a ProxyManager instance with a proxy string and perform proxy checks. 40 | 41 | Args: 42 | botright: An instance of Botright for linking purposes. 43 | proxy (str): The proxy string to be managed and checked. 44 | """ 45 | link(self, botright) 46 | 47 | self.proxy = proxy.strip() if proxy else "" 48 | 49 | self.timeout = httpx.Timeout(20.0, read=None) 50 | self._httpx = httpx.AsyncClient(verify=False) 51 | 52 | if self.proxy: 53 | self.split_proxy() 54 | self.proxy = f"{self.username}:{self.password}@{self.ip}:{self.port}" if self.username else f"{self.ip}:{self.port}" 55 | self.plain_proxy = f"http://{self.proxy}" 56 | self._phttpx = httpx.AsyncClient(proxies={"all://": self.plain_proxy}, verify=False) 57 | self.http_proxy = {"http": self.plain_proxy, "https": self.plain_proxy} 58 | 59 | if self.username: 60 | self.browser_proxy = {"server": f"{self.ip}:{self.port}", "username": self.username, "password": self.password} 61 | else: 62 | self.browser_proxy = {"server": self.plain_proxy} 63 | 64 | await self.check_proxy(self._phttpx) 65 | 66 | else: 67 | self._phttpx = self._httpx 68 | await self.check_proxy(self._phttpx) 69 | 70 | async def __adel__(self) -> None: 71 | await self._httpx.aclose() 72 | await self._phttpx.aclose() 73 | 74 | def split_helper(self, split_proxy: List[str]) -> None: 75 | """ 76 | Helper function to split and parse the proxy string into its components. 77 | 78 | Args: 79 | split_proxy (List[str]): A list containing the components of the proxy string. 80 | """ 81 | if not any([_.isdigit() for _ in split_proxy]): 82 | raise SplitError("No ProxyPort could be detected") 83 | if split_proxy[1].isdigit(): 84 | self.ip, self.port, self.username, self.password = split_proxy 85 | elif split_proxy[3].isdigit(): 86 | self.username, self.password, self.ip, self.port = split_proxy 87 | else: 88 | raise SplitError(f"Proxy Format ({self.proxy}) isnt supported") 89 | 90 | def split_proxy(self) -> None: 91 | split_proxy = self.proxy.split(":") 92 | if len(split_proxy) == 2: 93 | self.ip, self.port = split_proxy 94 | elif len(split_proxy) == 3: 95 | if "@" in self.proxy: 96 | helper = [_.split(":") for _ in self.proxy.split("@")] 97 | split_proxy = [x for y in helper for x in y] 98 | self.split_helper(split_proxy) 99 | else: 100 | raise SplitError(f"Proxy Format ({self.proxy}) isnt supported") 101 | elif len(split_proxy) == 4: 102 | self.split_helper(split_proxy) 103 | else: 104 | raise SplitError(f"Proxy Format ({self.proxy}) isnt supported") 105 | 106 | async def check_proxy(self, httpx_client: httpx.AsyncClient) -> None: 107 | """ 108 | Check the validity of the proxy by making HTTP requests to determine its properties. 109 | 110 | Args: 111 | httpx_client (httpx.AsyncClient): The HTTPX client to use for proxy checks. 112 | """ 113 | get_ip_apis = ["https://api.ipify.org/?format=json", "https://api.myip.com/", "https://get.geojs.io/v1/ip.json", "https://api.ip.sb/jsonip", "https://l2.io/ip.json"] 114 | 115 | for get_ip_api in get_ip_apis: 116 | try: 117 | ip_request = await httpx_client.get(get_ip_api, timeout=self.timeout) 118 | ip = ip_request.json().get("ip") 119 | break 120 | except Exception: 121 | pass 122 | else: 123 | raise ProxyCheckError("Could not get IP-Address of Proxy (Proxy is Invalid/Timed Out)") 124 | 125 | get_geo_apis = { 126 | "http://ip-api.com/json/": ["country", "countryCode", "lat", "lon", "timezone"], 127 | "https://ipapi.co//json": ["country_name", "country", "latitude", "longitude", "timezone"], 128 | "https://api.techniknews.net/ipgeo/": ["country", "countryCode", "lat", "lon", "timezone"], 129 | "https://get.geojs.io/v1/ip/geo/.json": ["country", "country_code", "latitude", "longitude", "timezone"], 130 | } 131 | 132 | for get_geo_api, api_names in get_geo_apis.items(): 133 | try: 134 | api_url = get_geo_api.replace("", ip) 135 | country, country_code, latitude, longitude, timezone = api_names 136 | r = await self._httpx.get(api_url, timeout=self.timeout) 137 | data = r.json() 138 | 139 | self.country = data.get(country) 140 | self.country_code = data.get(country_code) 141 | self.latitude = data.get(latitude) 142 | self.longitude = data.get(longitude) 143 | self.timezone = data.get(timezone) 144 | 145 | assert self.country 146 | break 147 | except Exception: 148 | pass 149 | else: 150 | raise ProxyCheckError("Could not get GeoInformation from proxy (Proxy is probably not Indexed)") 151 | -------------------------------------------------------------------------------- /botright/modules/tmp_dir/record_json/image_label_area_select.point.Please click on the rabbit's eye.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "c": { 3 | "type": "hsw", 4 | "req": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmIjowLCJzIjoyLCJ0IjoidyIsImQiOiJFN1R1R09qYmZEY2Jjak44OWRQT2E1RXpBTTNaRGp1a3p0bk53aHhYVUhseHg3WUJvMk5vUGF2bDNuNElUMi94WnMzd092SWNMai94eFRhN2NzaGJjcUtnTHJJTHd0bWxDVkFnY0EzQUZsSkhma0RBL25RTnQvTGh5UTFhMUxvM1hSKzYveXdYZzBtbjcyUDdWU2VBOUpGZVVqd095NllEWWVBZXVJRmFPMXgrd0poWXRUSUMzN1ZGMzlTSmpGRCs4ZFZwTTNzNDVuU0RVV011ejJQZGI3NWpZa1lia3V2L1dGaDh4RzNzN2hROWdwL0hsaCtxN1ZNS3B5RHcyMHVWIiwibCI6Imh0dHBzOi8vbmV3YXNzZXRzLmhjYXB0Y2hhLmNvbS9jLzNhODRjMTUiLCJpIjoic2hhMjU2LXV4L0ZDV2M4ZS9vWTN1cGFINnRVeWpQN2hMbGhwR295OG45ZVdWdVdNS009IiwiZSI6MTcwNTc3MzQxNSwibiI6ImhzdyIsImMiOjEwMDB9.FgPKoS-nbVAB0PTnrxQdsjkjZ3RBbqBOvwatbEGTQOQ" 5 | }, 6 | "challenge_uri": "", 7 | "key": "E0_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoiVFRKMTFBLzZ1bzlpdC9LK2VsTmlNZVFZZ3hWVU9oa1VhQ3RJRWVtMEpjeHM4RWhoYlR3ZXVITkNtOEJpNjFUem92dmlnWmwvUEJveEYyWC9kU1pOMmtwTVRyNWVPVGxQUE9WYVZYbkJVU1FsUDBVVmtod0tLZnVLME1ta0ViUWxOWWtoVi8yS2lEVmxhVCtqbkNHankyYU5BNlRKMlJma1dXSmpCV2dqQ1BoQWlNMFd5dGdUd1RZWGNIVVVCNFNRdzJoQzFPc1o5L2V4V1hpUi9MMWRrNFEyZ0hXaDVYaFFGeTdzTWR4MXhQM2xZYzBWY2dWY1VITUpycmVQa1lVa0FLcm8xL1B5SW1jUEtLRWk5TEZGS1pidnlwRXgifQ.1KkGNpaPVk16XJC3D-H2Klg_kisHWIG4-I5pAUfE7yw", 8 | "request_config": { 9 | "version": 0, 10 | "shape_type": "point", 11 | "min_points": 1, 12 | "max_points": 1, 13 | "min_shapes_per_image": 1, 14 | "max_shapes_per_image": 1, 15 | "restrict_to_coords": null, 16 | "minimum_selection_area_per_shape": null, 17 | "multiple_choice_max_choices": 1, 18 | "multiple_choice_min_choices": 1, 19 | "overlap_threshold": null, 20 | "answer_type": "str", 21 | "max_value": null, 22 | "min_value": null, 23 | "max_length": null, 24 | "min_length": null, 25 | "sig_figs": null, 26 | "keep_answers_order": null, 27 | "ignore_case": false 28 | }, 29 | "request_type": "image_label_area_select", 30 | "requester_question": { 31 | "en": "Please click on the rabbit's eye" 32 | }, 33 | "requester_question_example": [], 34 | "requester_restricted_answer_set": { 35 | "default": { 36 | "en": "default", 37 | "af": "verstek", 38 | "sq": "default", 39 | "am": "ነባሪ", 40 | "ar": "تقصير", 41 | "hy": "լռելյայն", 42 | "as": "ডিফল্ট", 43 | "ay": "jan walitxa", 44 | "az": "default", 45 | "bm": "fɔlɔ", 46 | "eu": "lehenetsia", 47 | "be": "па змаўчанні", 48 | "bn": "ডিফল্ট", 49 | "bho": "बाकी", 50 | "bs": "default", 51 | "bg": "по подразбиране", 52 | "ca": "per defecte", 53 | "ceb": "default", 54 | "ny": "kusakhulupirika", 55 | "zh": "默认", 56 | "zh-TW": "預設", 57 | "co": "predeterminatu", 58 | "hr": "zadano", 59 | "cs": "výchozí", 60 | "da": "Standard", 61 | "dv": "ޑީފޯލްޓް", 62 | "doi": "डिफाल्ट", 63 | "nl": "standaard", 64 | "eo": "defaŭlte", 65 | "et": "vaikimisi", 66 | "ee": "gᴐmedzeƒe", 67 | "tl": "default", 68 | "fi": "oletuksena", 69 | "fr": "défaut", 70 | "fy": "standert", 71 | "gl": "por defecto", 72 | "lg": "okukosamu", 73 | "ka": "ნაგულისხმევი", 74 | "de": "Standard", 75 | "el": "Προκαθορισμένο", 76 | "gn": "upevakuére", 77 | "gu": "મૂળભૂત", 78 | "ht": "default", 79 | "ha": "tsoho", 80 | "haw": "paʻamau", 81 | "iw": "בְּרִירַת מֶחדָל", 82 | "he": "בְּרִירַת מֶחדָל", 83 | "hi": "गलती करना", 84 | "hmn": "ua ntej", 85 | "hu": "alapértelmezett", 86 | "is": "sjálfgefið", 87 | "ig": "ndabara", 88 | "ilo": "default", 89 | "id": "bawaan", 90 | "ga": "réamhshocraithe", 91 | "it": "predefinito", 92 | "ja": "デフォルト", 93 | "jw": "gawan", 94 | "kn": "ಪೂರ್ವನಿಯೋಜಿತ", 95 | "kk": "әдепкі", 96 | "km": "លំនាំដើម", 97 | "rw": "Mburabuzi", 98 | "gom": "डिफॉल्ट", 99 | "ko": "기본", 100 | "kri": "balans", 101 | "ku": "destçûnî", 102 | "ckb": "بنەڕەتی", 103 | "ky": "демейки", 104 | "lo": "ຄ່າເລີ່ມຕົ້ນ", 105 | "la": "defaltam", 106 | "lv": "noklusējuma", 107 | "ln": "mbeba", 108 | "lt": "numatytas", 109 | "lb": "Default", 110 | "mk": "стандардно", 111 | "mai": "चुकनाइ", 112 | "mg": "toerana misy anao", 113 | "ms": "lalai", 114 | "ml": "സ്ഥിരസ്ഥിതി", 115 | "mt": "default", 116 | "mi": "taunoa", 117 | "mr": "डीफॉल्ट", 118 | "mni-Mtei": "ꯀꯥꯡꯂꯣꯟ ꯏꯟꯗꯕ", 119 | "lus": "hlawhchhamna", 120 | "mn": "анхдагч", 121 | "my": "ပုံသေ", 122 | "ne": "पूर्वनिर्धारित", 123 | "nso": "hlokomologa", 124 | "no": "misligholde", 125 | "or": "ଡିଫଲ୍ଟ", 126 | "om": "durtii", 127 | "ps": "ډیفالټ", 128 | "fa": "پیش فرض", 129 | "pl": "domyślny", 130 | "pt": "padrão", 131 | "pt-BR": "padrão", 132 | "pa": "ਡਿਫਾਲਟ", 133 | "qu": "ñawpaqchasqa", 134 | "ro": "Mod implicit", 135 | "ru": "по умолчанию", 136 | "sm": "faaletonu", 137 | "sa": "मूलभूतम्‌", 138 | "gd": "bunaiteach", 139 | "sr": "Уобичајено", 140 | "st": "ya kamehla", 141 | "sn": "default", 142 | "sd": "ڊفالٽ", 143 | "si": "පෙරනිමිය", 144 | "sk": "predvolená", 145 | "sl": "privzeto", 146 | "so": "default", 147 | "es": "por defecto", 148 | "su": "standar", 149 | "sw": "chaguo-msingi", 150 | "sv": "standard", 151 | "tg": "пешфарз", 152 | "ta": "இயல்புநிலை", 153 | "tt": "Килешү", 154 | "te": "డిఫాల్ట్", 155 | "th": "ค่าเริ่มต้น", 156 | "ti": "ትሑዝ", 157 | "ts": "ku tlula", 158 | "tr": "varsayılan", 159 | "tk": "gaýybana", 160 | "ak": "mfiaseɛ", 161 | "uk": "за замовчуванням", 162 | "ur": "پہلے سے طے شدہ", 163 | "ug": "كۆڭۈلدىكى", 164 | "uz": "standart", 165 | "vi": "mặc định", 166 | "cy": "rhagosodedig", 167 | "xh": "ukungagqibeki", 168 | "yi": "פעליקייַט", 169 | "yo": "aiyipada", 170 | "zu": "okuzenzakalelayo", 171 | "jv": "gawan", 172 | "zh-CN": "默认" 173 | } 174 | }, 175 | "tasklist": [ 176 | { 177 | "datapoint_uri": "https://imgs.hcaptcha.com/tN8yO3SEIQ-NkFxHkSmgoV3ZTGHJQntZXjoSuiUELKrIM9DQrVH4SV7bDLPv6kUqm_8LuQFp7PrTwP7-yx6u_ToTvIQCsUnplbJ_J3eMPgQCbOaZD6GGe6OGMnCPmow2zh11HitI26KS1intPsJcWfmO_uFAEW3bx_4Fx38D_CALQ5PE3SvBLFNVkrUE68OiUwzaVMyPT3Hoahcv9tpG76e0oOgluhetkNqRrK_rNiAa8WrGqVC8fcWvnBqkUp1IIpVRcDDbtRTd4w53mpzAOdI4k9CEyeJ_gCNloWH4Uq62oHGJmmsiBOAHlnXGf0I2ZZZH_kckUuirT5qQJXBdLPfGAEFIed18lUtkvjrI1ZOXULLGFwj3hrAEmJApFOp-m_ZVNRxYiYq6gCCr1DUnhnQ5vm3D8EtgLEzDng99f2535BisRKqaCB", 178 | "task_key": "69cc1188-d980-4af0-8c0c-423ed29fca18" 179 | }, 180 | { 181 | "datapoint_uri": "https://imgs.hcaptcha.com/XkaZxyD1cLGp71jsS3vZACPhwjClOUbc8SQWi4MaOmWsgY5f86HngbZsBdS_Jr6pv3Ywdfxj_x0PpTKTSwyG3wdbBQ8osKuwmzv-1_dlKGJ6JhQpnJiwWNgWEdaU4-h22MVEEGRlvCTS-qOzLE2jGSHOwJ6Qa4_hLfA3O8PE03O23__1Zk4wFJONWzFG7IiChEu6AkRgxAz3lgddgOlEs7LAQazlTJ1lv0i6NTnjMN7djyY9GBE0YUlKJQJ4yZ1UlO9zYOCF2jiL3Gn3SQZCJxam1diJbvinZHEk4x6ewr5DHQ-5JjqQnyWG6f7ODVZNXq7RHmsUHDyELO9oqljXy0URbaWVrL63jNDd4eb-R0gPzx5mAXD-FCcDqgUQpDp3h8yN_hJjKd3b7246hr5BIqCxt7lTGNTZSLgUhAKxAM-G4J5npKd5RF", 182 | "task_key": "dfdd14f5-3b05-42f5-ab21-fa958bf6e0d0" 183 | } 184 | ] 185 | } -------------------------------------------------------------------------------- /botright/modules/tmp_dir/record_json/image_label_binary.None.Please click each image containing a mean of transportation.json: -------------------------------------------------------------------------------- 1 | { 2 | "c": { 3 | "type": "hsw", 4 | "req": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmIjoxLCJzIjoyLCJ0IjoidyIsImQiOiJZMG1pYlIwalFWdnNURmtxTDJZQWlGeFlmalhtMWtrYWN5Ky85QkNpdzZ1OUw2UjFSNllXTDRGcGVzUGt4aVNRb3c5dHhvcFlua2NjOE5DdlJMV2lPd2J0YlFNQWp5bGNhVjhkQlp6dFduM3FiTVpSWEpoK1luZWFDMGRrNHFFNmZCR1Y5MVAra1VXNW8rTnN2QlppNWZiSnhVWjMvNWNraERhZnVKeVVKem1OeXNyZi9BYmFBYnZqUG13bHFmTVRJZ01JdXlIR1V4R1A1WEFaU2dtK2NSQTVUZ3hFakQ1Y2sxNEJqVHlXV1c2YUxlTHNhMXZPN29JU2JXSG8vL20zIiwibCI6Imh0dHBzOi8vbmV3YXNzZXRzLmhjYXB0Y2hhLmNvbS9jLzNhODRjMTUiLCJpIjoic2hhMjU2LXV4L0ZDV2M4ZS9vWTN1cGFINnRVeWpQN2hMbGhwR295OG45ZVdWdVdNS009IiwiZSI6MTcwNjEyMDg5MSwibiI6ImhzdyIsImMiOjEwMDB9.AkxGbxZW_MeRIgEczzYqV0a1_EkVw3mdIUQLCEX4Fk4" 5 | }, 6 | "challenge_uri": "", 7 | "key": "E0_eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoiMUFIcHc1cVBKOFMyYjBXdXFjM0RwOWpzWDl1cXRqbzhYc2VQY2xpV0thMEJxY1RrRHg3eWdzZXJ2MGo2djAvYk9nODRvY1l4NmMvSHZKcEE5bHRwSzhtZk4yNy9QZ1BRdjNtVE81c1dKNXRla3ZDLzZmbUduR2RBRXR5SmF0YXRWeDI1ZlRrUlk0VVNFbkdVR0Z2cEZxUnZRdi83bkswKzZMSVhEcEh3b0I5V3U3YTRJdSt2YW5TN3h1R25oM28xR3ZxcDB5alJGVkRYUEdDcnY3WFhMRVZzblNrRkN1dU84cHlZZVhnVTR1UHBGT3ZJd3hXb2hkeGNtZWlnanVUaDY4V1VXWExvQnpvPUxHOGJYOC9GUnc3bVZ0VysifQ.YZLpG2oHiY6pOFhGJKj3_su3aI9tQI_iV1d1uTkDvyU", 8 | "request_config": { 9 | "version": 0, 10 | "shape_type": null, 11 | "min_points": null, 12 | "max_points": null, 13 | "min_shapes_per_image": null, 14 | "max_shapes_per_image": null, 15 | "restrict_to_coords": null, 16 | "minimum_selection_area_per_shape": null, 17 | "multiple_choice_max_choices": 1, 18 | "multiple_choice_min_choices": 1, 19 | "overlap_threshold": null, 20 | "answer_type": "str", 21 | "max_value": null, 22 | "min_value": null, 23 | "max_length": null, 24 | "min_length": null, 25 | "sig_figs": null, 26 | "keep_answers_order": null, 27 | "ignore_case": false 28 | }, 29 | "request_type": "image_label_binary", 30 | "requester_question": { 31 | "en": "Please click each image containing a mean of transportation" 32 | }, 33 | "requester_question_example": [ 34 | "https://imgs.hcaptcha.com/2xvbDlReh4DiLk1sIwv07OnHaFBhfBCwZXoqf-_u1yzfOGNsPklpF6i_5iM2GPYXmLtkWMhpAiRVkMmUl6XQoD9XLqVARc0KsMi8PSDdNT9GomwZIeB5IeSZ6Vhqg6T3MSCa9QE56GVBUaiPN22VGNwjtzuT4Q7Mq_YXtF92jHkYwePPbdeMkkVR8q0mFPSEXJtjQOmtqzjtX46Cq673rmsv50XOJcxy38s2CAYPmPCvL84yIEPQgqUWmk5wWufTc0WnsPeHeFtyG1jhPk-LrIswW4I2zsVyWj5VZB-YoLaYa7xaNZC1Etv3wPEhrnflAcNBeSpCkMqQ-42v7nT81nlbZiISKhZvt12KKwbsB9T5U1CZGcF8CZ-aoxo-4gxLMuIzefesFx8c0gxv-UIucrulf19aw_Q2sauVogeyXPNfbgoL4hd50j" 35 | ], 36 | "requester_restricted_answer_set": {}, 37 | "tasklist": [ 38 | { 39 | "datapoint_uri": "https://imgs.hcaptcha.com/gQuBxgsI3Q9njOUnD9x0MGzOniBWtTTnVsrSodZ9xDIcn-oBp0_Q3ukPyM6Sb81ezZi14DTY8M_SsMtZIBTWtj850x96anfSs0pNWQeG6DJtpuWk7a52eW1FVhUiykan5gPxgVazYkLKblh__MY1ayrPGTob3omyQIl-8eSiYDSGGX0cZ1uVOBSZWlE1iXGZ90hbGa9ebeWsPihf3FGmbmeyRctMxN6he_AQuiPeaD9e0hCsTuTKQ1cJEmZrT_BfHPRLa3hXlsKnBp78P9614G14eb-PsGBBIMlCzLaKaq8up1OBIJQoB_8scqt73au4O0__MbCTQKgzWp2XSnMq4EysYtLHkO8Sobs4NdFWIrr4egNDMWOJ_LajXKR7FmiewMi6bvmpiv3g0aHmbfKn_D2jTIMAYpT4IXblIgjs8AOmNQojU1y3vp", 40 | "task_key": "a38437fa-0a3a-4c51-95e5-97e0428fda51" 41 | }, 42 | { 43 | "datapoint_uri": "https://imgs.hcaptcha.com/XzLtNLAok8NFMY1izKytRMCIhLfEty3EkkCDnmqDfKnl7NXUTl3uBP00CXS8qSjZ3zPh1-X9PkvEY5_7GBut0wGXErxqxh34Ux7MoUf2gYOGr4ilxA-a3yvpzGN1CtmqNtkTeO5EFUbISfKcBfOltUpGa_Xs3I5R-GMeZfFOp3rp6S6UF7xIVJxtM86i5QFlXvxQD6rU2Bj3BkeXBwwiHv_INA693hRZMPVIIFx-T7yQcr6UboIuqlyhug__lU-5bWdqCO4lj5JErAl3X7JWPbNPMtTebCKrB2Umhd2U7cd4Unge1uxbbu0A6VpFizNex3hEpboQTyjnGX_g69XdowcqZTHQbLmejzWn03Xk-NP958UeBim6xAIE4EyKbglxFDYfxMqa2b2U-kd6sEzF9cSWIW6jbTNsDOuiGgNTdWuAo-DaAxoeMd", 44 | "task_key": "795a603a-0fae-45e1-b7c9-6bf97ec1c89d" 45 | }, 46 | { 47 | "datapoint_uri": "https://imgs.hcaptcha.com/53Mw-TgHiZFbCC_YarVJc-TRXM0UPP39IKF7ui3PECY64uNdb14b5JdRx_kO8v4XUDkM-jvrWUNwNtZwEcO6sAO2iOQSMu_640tJ6_rIux3b33hGunHarxMLr69ibX46ghvs9uyjxaoePoMj6qyxK8Ct4rPEH7333X4bn0TG9_cbm-UYIOVS2KxjYsfx-rY1teLbokjoSPSzOl9IrCAnKxF3zflNlGdRUlY8mkfPAH9whXHbSDQIdUmZsI4EeF46pE_mzBT2tJycVoQxueF2qFdRoWg7s0sw5fNZA6ADPEtEuxvjPBhYHLlww4WShQQYOBvU6YZgHwowuG2F_IGE5c0Hx-D6AmZjN6E5QMtntz2OZ2L7nBfsOqiqu0NodDwPG0boUVk5m_2JT25RatDfp8xlBFCb-8SSf6gltQlFb0ysI3r3GpZGct", 48 | "task_key": "76c0923b-54ef-4eb8-9208-f57ad6b790f6" 49 | }, 50 | { 51 | "datapoint_uri": "https://imgs.hcaptcha.com/D0hDy2OlAo6HpLTiJ2nsxbSKSdB3Bg_2BcLPfC5YE36f6sJwWxw3ulzzNZkO3fpM886PJZgSg-FZCQMLV8WIwGxrhsRGL2N7PPP56WymxXYqeaNShVR4mMgtbY9-ZTwlNZy0nXAoRo7SDjwbdC7xPvSf7IhXGEib6qmPJHdLLlKlwaXSAQH_fOaidEP8bLA6S4vmGi5c6WZM8Fws9sd0QKjxZDzX-lJ43dSZWDv8Qd6QAkPm4VlXztA3PXF0snOxZ0csmc5T339PVYLQ7hoEqldRUzG5g5rUrDZjNZJr9Q3liNfCueQmxPbXGjUZYGY1kqitgFd1WBibOxU2sLwZTn89GZVMErdwlJkTOs4RRUAM7Ca2JdPvgI84zXog1qQYDjXY0GYRckCDQPUX1bR5nCW48BefTVJpmeNkuwCG8OWn_RLE_sYAlp", 52 | "task_key": "8e206455-378e-4d9d-91ab-a8f19b69d759" 53 | }, 54 | { 55 | "datapoint_uri": "https://imgs.hcaptcha.com/NyHM8sjTjn37I09hBHNnhYJvGCPCLNX1FrmU1ylGvdQ1mmDjEI5HJDyEdYy90IKvLcgEgzEPeQHwKBRBZM_6kATqtRBDEpvgTQPpKV_wDi790mtgFe6jOz0XcuIQCSFlcmtqpQqei1tL1i3kz4oRh3RuCDVHiKAmGaMgWbPfYBn-sBLviln46lQJCAZCrNQPcn2Is_sZaUMkOecW8N4aS-EYZwWuea60zGydfJsEnXOtbcZa04RYbUocf05cXg4XolgdoPg6StcxHvK8XTKz6TfOt6g9mUiemQpS_hJ69vOg2rOEBW2r8dmY90sHjFyDhrqHjDcNtt5qD3Hjuwcx1ECwPq3B-FHBYVQJCcELHv6mqFMJkyU0GAH_nbnEh4PQqncE9ntZHG-Zn11ZfL_nTXcK_se8NBrJGvKvqQ4RFHivkZQF0oeHHF", 56 | "task_key": "9210e362-a0e1-47a3-8579-79af9d32536c" 57 | }, 58 | { 59 | "datapoint_uri": "https://imgs.hcaptcha.com/dW6K6rS-Zm4BlkZxVuliEORes2ZIFMcn2b5QJLbt2BOUsdj4wxOu0QZZ226lyqoCBwcPcteRysnNW9qM8OgSBzxWJd8RaU7XjPaQ99I3NAe_21Wh47Xw2853fUJxJHBXVNM0M3Mcn8koipiYkyQBmNOdnXth8jwo4HQyC-DIm6i3yw-WG9EQEpg0mUgDUTiVRmPt-kuT6SpOG4YrcNLmNwSKkTy7qNImkLnxeNs6g89xE8_we0jJlvl1WY_h3jr1Lxa-AiiOHzOt5AKkb62Do_uIhaUNX0M33k1ZJyqkqnM74fVYXqB1qwuAwlwGOs1v55hWhYvCP6rZRCCo2RGVOmK1S8BVcVhXICNjujboz26Gdb4XrvgcwPMUW_dDVfqOd6S_zKUyMIGsP-3SeQg5jRw4C_bgsZyx6t1jjgAIdOpPy9Az7NZuT7", 60 | "task_key": "53a7c617-b8ef-47f4-9645-aa5882d3950d" 61 | }, 62 | { 63 | "datapoint_uri": "https://imgs.hcaptcha.com/MbfXIUyz9zOaJycr4B64hQEt6PoDvw2yQah4t1OuMAuC2qONt-f3QWymdqfgTuDuw3nCpOf56nR1_2kSbifFadc99k9qUWvPkIdHifCFOm8o4P1GBRfo2ihx88Coev4Zmwjd80MVFq9FodtYQBmU3RIGrxhniEJ2QIdnXIf4K-uqmsRkB5YegXq8KH1UJXQGcePegh5S9oS0afYCZCx56pLWJF4tnck6jp21yANRU_fgLe32T0FV04WnxabDZsuE9ndOgJWiddLgi86-6vqAYaskykdTuBpSWYxi9Me89LDCyB-CfzV-7eJmxwpJ9teNw9WMI_kfRl_B19y8oWV_WBHmkY6JhODjmO69Mn3f8bSOW3BEzCINpxJjWj77k0b5Ar3PREUlfIudqmZQkDivmCiscnTeqYGorYTINg1B8wegOSovLMqFUn", 64 | "task_key": "45aacaf2-679b-4674-aac6-cd417fe9f942" 65 | }, 66 | { 67 | "datapoint_uri": "https://imgs.hcaptcha.com/i4wQnNlVfh4elrXuTz9yi-VyEv4x7LaaX-a3KuT9SRGKVCVLHJTzV--t1Ij6P0-1wvVI9NAaj5FClQhX9ng9ubJ10X1qeePh-ih04v33sIRRUA3iyWIYFNMW1zXKCjXE-vlubJAINeycHQSPyPAraMHkfgGglre998i0atYA_WTQkl8XptWcV4i-NH0YYKIA6uygTDUYvm_SKakaYx1BWy-31klKtjjhUj2NQzgMmr7VnMGuWb3nIIeg7McUeFUnFaTPYv2-Qc-25hw97sjboGIUdyk2KHDDe2cfaAp6DHMEqm1kmZYSGoQtWqSlsxymFKPyGymxEbplHL4DxFxg_CL6l8S1cjRg5MUghGhGiyL4fT_cVQts2fCu9MskNcbkquiXMu8KkOrixBiwAEGg7phoxNfSs10Wo_xlLwJrPXzYvxbpHrrTV7", 68 | "task_key": "edab2f76-b372-4956-ac0a-883ff081fdf7" 69 | }, 70 | { 71 | "datapoint_uri": "https://imgs.hcaptcha.com/gq4mzN28KtPPJANQv-Pez7h2Fi8wKYq7UWq555fKP11EzW8PsDjo7PJ9d4awyUbXkm77JwRBfDgPuTCGACG5IdWQsZZm6UE-Y774__x_srKF5vTcl7POEtLWC-B_BsQUep1FTJL4pNrdpzMxi4zp-hoqU8aDHqETW2XlY3LHp0wXiKsx3esMGXTllenIRm7kPJrgl35o1VqbZp0UP93LjB-E438oXxIxFWzcdRBAiNFAvG253bow4ELUJBtXedv_MS6L1DX_Qt9i94THspB_Vi3Qp6yeXfRZz63-TTxz7Rpr11R7spRmvKy3BAPgnWhnM3zBRGugLHaIsTSsy_xkS_vZZXQ14Bny6kqZnbnveT_fA_rMZbr71pRM48oIMrzDbw4bVBM1VDwaJKN7dQA0ul2izdootYYVPV0SoAZOyd10JLbHe5ed9p", 72 | "task_key": "8978a7c2-9e02-4de4-a876-c94dd17fe478" 73 | } 74 | ] 75 | } -------------------------------------------------------------------------------- /botright/playwright_mock/__init__.py: -------------------------------------------------------------------------------- 1 | from .frame import Frame 2 | from .frame_locator import FrameLocator 3 | from .handles import ElementHandle, JSHandle 4 | from .keyboard import Keyboard 5 | from .locator import Locator 6 | from .mouse import Mouse 7 | from .routes import Request, Response, Route 8 | 9 | from .page import Page, new_page # isort:skip 10 | from .browser import BrowserContext # isort:skip 11 | 12 | __all__ = ["ElementHandle", "JSHandle", "Frame", "FrameLocator", "Route", "Response", "Request", "Locator", "Mouse", "Keyboard", "Page", "new_page", "BrowserContext"] 13 | -------------------------------------------------------------------------------- /botright/playwright_mock/frame_locator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, Optional, Pattern, Union 4 | 5 | # from undetected_playwright.async_api import Locator as PlaywrightLocator, FrameLocator as PlaywrightFrameLocator 6 | from playwright.async_api import FrameLocator as PlaywrightFrameLocator 7 | from playwright.async_api import Locator as PlaywrightLocator 8 | 9 | if TYPE_CHECKING: 10 | from . import Locator, Page 11 | 12 | 13 | class FrameLocator(PlaywrightFrameLocator): 14 | def __init__(self, frame_locator: PlaywrightFrameLocator, page: Page): 15 | super().__init__(frame_locator) 16 | self._impl_obj = frame_locator._impl_obj 17 | self._page = page 18 | self._origin_first = frame_locator.first 19 | self._origin_last = frame_locator.last 20 | 21 | self._origin_locator = frame_locator.locator 22 | self._origin_nth = frame_locator.nth 23 | 24 | def locator( 25 | self, 26 | selector_or_locator: Union[PlaywrightLocator, str], 27 | has_text: Optional[Union[str, Pattern[str]]] = None, 28 | has_not_text: Optional[Union[str, Pattern[str]]] = None, 29 | has: Optional[PlaywrightLocator] = None, 30 | has_not: Optional[PlaywrightLocator] = None, 31 | ) -> Locator: 32 | from . import Locator 33 | 34 | _locator = self._origin_locator(selector_or_locator=selector_or_locator, has=has, has_text=has_text, has_not=has_not, has_not_text=has_not_text) 35 | locator = Locator(_locator, self._page) 36 | return locator 37 | 38 | def nth(self, index: int) -> FrameLocator: 39 | _locator = self._origin_nth(index=index) 40 | locator = FrameLocator(_locator, self._page) 41 | return locator 42 | 43 | @property 44 | def first(self) -> FrameLocator: 45 | _locator = self._origin_first 46 | locator = FrameLocator(_locator, self._page) 47 | return locator 48 | 49 | @property 50 | def origin_first(self): 51 | return self._origin_first 52 | 53 | @origin_first.setter 54 | def origin_first(self, value): 55 | self._origin_first = value 56 | 57 | @property 58 | def last(self) -> FrameLocator: 59 | _locator = self._origin_last 60 | locator = FrameLocator(_locator, self._page) 61 | return locator 62 | 63 | @property 64 | def origin_last(self): 65 | return self._origin_last 66 | 67 | @origin_last.setter 68 | def origin_last(self, value): 69 | self._origin_last = value 70 | 71 | def _attach_dyn_prop(self, frame_locator: FrameLocator, prop_name: str, prop: Any) -> None: 72 | """Attach property proper to instance with name prop_name. 73 | 74 | Reference: 75 | * https://stackoverflow.com/a/1355444/509706 76 | * https://stackoverflow.com/questions/48448074 77 | """ 78 | class_name = frame_locator.__class__.__name__ + "Child" 79 | child_class = type(class_name, (frame_locator.__class__,), {prop_name: prop}) 80 | 81 | frame_locator.__class__ = child_class 82 | -------------------------------------------------------------------------------- /botright/playwright_mock/keyboard.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | from typing import TYPE_CHECKING, Optional 5 | 6 | # from undetected_playwright.async_api import Keyboard as PlaywrightKeyboard 7 | from playwright.async_api import Keyboard as PlaywrightKeyboard 8 | 9 | if TYPE_CHECKING: 10 | from . import Page 11 | 12 | 13 | class Keyboard(PlaywrightKeyboard): 14 | def __init__(self, keyboard: PlaywrightKeyboard, page: Page): 15 | super().__init__(keyboard) 16 | self._impl_obj = keyboard._impl_obj 17 | 18 | self._page = page 19 | self._origin_type = keyboard.type 20 | 21 | async def type(self, text: str, *, delay: Optional[float] = None) -> None: 22 | if not delay: 23 | delay = 100 24 | delay = int(delay) 25 | 26 | for char in text: 27 | await self._origin_type(text=char, delay=random.randint(delay - 50, delay + 50)) 28 | await self._page.wait_for_timeout(random.randint(4, 8) * 100) 29 | -------------------------------------------------------------------------------- /botright/playwright_mock/locator.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, Literal, Optional, Pattern, Sequence, Union 4 | 5 | # from undetected_playwright.async_api import Position, Locator as PlaywrightLocator, ElementHandle as PlaywrightElementHandle, Error as PlaywrightError 6 | from playwright.async_api import ElementHandle as PlaywrightElementHandle 7 | from playwright.async_api import Error as PlaywrightError 8 | from playwright.async_api import Locator as PlaywrightLocator 9 | from playwright.async_api import Position 10 | 11 | if TYPE_CHECKING: 12 | from . import ElementHandle, FrameLocator, JSHandle, Page 13 | 14 | 15 | class Locator(PlaywrightLocator): 16 | def __init__(self, locator: PlaywrightLocator, page: Page): 17 | super().__init__(locator) 18 | self._impl_obj = locator._impl_obj 19 | self._page = page 20 | self._origin_first = None 21 | self._origin_last = None 22 | 23 | self.origin_locator = locator.locator 24 | self.origin_evaluate_handle = locator.evaluate_handle 25 | self.origin_frame_locator = locator.frame_locator 26 | self.origin_element_handle = locator.element_handle 27 | self.origin_nth = locator.nth 28 | 29 | self.origin_first = locator.first # type: ignore 30 | self.origin_last = locator.last # type: ignore 31 | 32 | # self._attach_dyn_prop(locator, "first", self.first) 33 | # self._attach_dyn_prop(locator, "last", self.last) 34 | 35 | @property 36 | def page(self) -> Page: 37 | return self._page 38 | 39 | def locator( 40 | self, 41 | selector_or_locator: Union[str, PlaywrightLocator], 42 | has_text: Optional[Union[str, Pattern[str]]] = None, 43 | has_not_text: Optional[Union[str, Pattern[str]]] = None, 44 | has: Optional[PlaywrightLocator] = None, 45 | has_not: Optional[PlaywrightLocator] = None, 46 | ) -> Locator: 47 | _locator = self.origin_locator(selector_or_locator, has=has, has_not=has_not, has_text=has_text, has_not_text=has_not_text) 48 | locator = Locator(_locator, self._page) 49 | return locator 50 | 51 | # JsHandle 52 | async def evaluate_handle(self, expression: str, arg: Optional[Any] = None, timeout: Optional[float] = None) -> Union[JSHandle, ElementHandle]: 53 | from . import ElementHandle, JSHandle 54 | 55 | _js_handle = await self.origin_evaluate_handle(expression=expression, arg=arg, timeout=timeout) 56 | if isinstance(_js_handle, PlaywrightElementHandle): 57 | element_handle = ElementHandle(_js_handle, self._page) 58 | return element_handle 59 | else: 60 | js_handle = JSHandle(_js_handle, self._page) 61 | return js_handle 62 | 63 | # FrameLocator 64 | def frame_locator(self, selector) -> FrameLocator: 65 | from . import FrameLocator 66 | 67 | _frame_locator = self.origin_frame_locator(selector=selector) 68 | frame_locator = FrameLocator(_frame_locator, self._page) 69 | return frame_locator 70 | 71 | # ElementHandle 72 | async def element_handle(self, timeout: Optional[float] = None) -> ElementHandle: 73 | from . import ElementHandle 74 | 75 | _element = await self.origin_element_handle(timeout=timeout) 76 | element = ElementHandle(_element, self._page) 77 | return element 78 | 79 | # Locator 80 | def nth(self, index) -> Locator: 81 | _locator = self.origin_nth(index=index) 82 | locator = Locator(_locator, self._page) 83 | return locator 84 | 85 | @property 86 | def first(self) -> Locator: 87 | _locator = self.origin_first 88 | 89 | locator = Locator(_locator, self._page) 90 | return locator 91 | 92 | @property 93 | def origin_first(self): 94 | return self._origin_first 95 | 96 | @origin_first.setter 97 | def origin_first(self, value): 98 | self._origin_first = value 99 | 100 | @property 101 | def last(self) -> Locator: 102 | _locator = self.origin_last 103 | locator = Locator(_locator, self._page) 104 | return locator 105 | 106 | @property 107 | def origin_last(self): 108 | return self._origin_last 109 | 110 | @origin_last.setter 111 | def origin_last(self, value): 112 | self._origin_last = value 113 | 114 | async def click( 115 | self, 116 | button: Optional[Literal["left", "middle", "right"]] = "left", 117 | click_count: Optional[int] = 1, 118 | delay: Optional[float] = 20.0, 119 | force: Optional[bool] = False, 120 | modifiers: Optional[Sequence[Literal["Alt", "Control", "Meta", "Shift"]]] = None, 121 | no_wait_after: Optional[bool] = False, 122 | position: Optional[Position] = None, 123 | timeout: Optional[float] = None, 124 | trial: Optional[bool] = False, 125 | ) -> None: 126 | modifiers = modifiers or [] 127 | position = position or Position(x=0, y=0) 128 | 129 | if not force: 130 | await self.wait_for(state="attached", timeout=timeout) 131 | 132 | if not trial: 133 | bounding_box = await self.bounding_box() 134 | if not bounding_box: 135 | raise PlaywrightError("Element is not visible") 136 | 137 | if self._page.scroll_into_view: 138 | await self.scroll_into_view_if_needed(timeout=timeout) 139 | 140 | if not await self.is_visible(): 141 | raise PlaywrightError("Element is outside of the viewport") 142 | 143 | x, y, width, height = bounding_box["x"], bounding_box["y"], bounding_box["width"], bounding_box["height"] 144 | if not any(position.values()): 145 | x, y = x + width // 2, y + height // 2 146 | else: 147 | x, y = x + position["x"], y + position["y"] 148 | 149 | for modifier in modifiers: 150 | await self._page.keyboard.down(modifier) 151 | 152 | await self._page.mouse.click(x=int(x), y=y, button=button, click_count=click_count, delay=delay) 153 | 154 | for modifier in modifiers: 155 | await self._page.keyboard.up(modifier) 156 | 157 | async def dblclick( 158 | self, 159 | modifiers: Optional[Sequence[Literal["Alt", "Control", "Meta", "Shift"]]] = None, 160 | position: Optional[Position] = None, 161 | delay: Optional[float] = 20.0, 162 | button: Optional[Literal["left", "middle", "right"]] = None, 163 | timeout: Optional[float] = None, 164 | force: Optional[bool] = None, 165 | no_wait_after: Optional[bool] = None, 166 | trial: Optional[bool] = None, 167 | ) -> None: 168 | modifiers = modifiers or [] 169 | position = position or Position(x=0, y=0) 170 | 171 | if not force: 172 | await self.wait_for(state="attached", timeout=timeout) 173 | 174 | if not trial: 175 | bounding_box = await self.bounding_box() 176 | if not bounding_box: 177 | raise PlaywrightError("Element is not visible") 178 | 179 | if self._page.scroll_into_view: 180 | await self.scroll_into_view_if_needed(timeout=timeout) 181 | 182 | if not await self.is_visible(): 183 | raise PlaywrightError("Element is outside of the viewport") 184 | 185 | x, y, width, height = bounding_box["x"], bounding_box["y"], bounding_box["width"], bounding_box["height"] 186 | if not any(position.values()): 187 | x, y = x + width // 2, y + height // 2 188 | else: 189 | x, y = x + position["x"], y + position["y"] 190 | 191 | for modifier in modifiers: 192 | await self._page.keyboard.down(modifier) 193 | 194 | await self._page.mouse.dblclick(x, y, button=button, delay=delay) 195 | 196 | for modifier in modifiers: 197 | await self._page.keyboard.up(modifier) 198 | 199 | async def check( 200 | self, position: Optional[Position] = None, timeout: Optional[float] = None, force: Optional[bool] = None, no_wait_after: Optional[bool] = None, trial: Optional[bool] = None 201 | ) -> None: 202 | position = position or Position(x=0, y=0) 203 | 204 | if not force: 205 | await self.wait_for(state="attached", timeout=timeout) 206 | 207 | if await self.is_checked(): 208 | return 209 | 210 | if not trial: 211 | bounding_box = await self.bounding_box() 212 | if not bounding_box: 213 | raise PlaywrightError("Element is not visible") 214 | 215 | if self._page.scroll_into_view: 216 | await self.scroll_into_view_if_needed(timeout=timeout) 217 | 218 | if not await self.is_visible(): 219 | raise PlaywrightError("Element is outside of the viewport") 220 | 221 | x, y, width, height = bounding_box["x"], bounding_box["y"], bounding_box["width"], bounding_box["height"] 222 | if not any(position.values()): 223 | x, y = x + width // 2, y + height // 2 224 | else: 225 | x, y = x + position["x"], y + position["y"] 226 | 227 | await self._page.mouse.click(x, y, button="left", click_count=1, delay=20) 228 | 229 | assert await self.is_checked() 230 | 231 | async def uncheck( 232 | self, force: Optional[bool] = False, no_wait_after: Optional[bool] = False, position: Optional[Position] = None, timeout: Optional[float] = None, trial: Optional[bool] = False 233 | ) -> None: 234 | position = position or Position(x=0, y=0) 235 | 236 | if not force: 237 | await self.wait_for(state="attached", timeout=timeout) 238 | 239 | if not await self.is_checked(): 240 | return 241 | 242 | if not trial: 243 | bounding_box = await self.bounding_box() 244 | if not bounding_box: 245 | raise PlaywrightError("Element is not visible") 246 | 247 | if self._page.scroll_into_view: 248 | await self.scroll_into_view_if_needed(timeout=timeout) 249 | 250 | if not await self.is_visible(): 251 | raise PlaywrightError("Element is outside of the viewport") 252 | 253 | x, y, width, height = bounding_box["x"], bounding_box["y"], bounding_box["width"], bounding_box["height"] 254 | if not any(position.values()): 255 | x, y = x + width // 2, y + height // 2 256 | else: 257 | x, y = x + position["x"], y + position["y"] 258 | 259 | await self._page.mouse.click(x, y, button="left", click_count=1, delay=20) 260 | 261 | assert not await self.is_checked() 262 | 263 | async def set_checked( 264 | self, checked: bool, force: Optional[bool] = False, no_wait_after: Optional[bool] = False, position: Optional[Position] = None, timeout: Optional[float] = None, trial: Optional[bool] = False 265 | ) -> None: 266 | position = position or Position(x=0, y=0) 267 | 268 | if not force: 269 | await self.wait_for(state="attached", timeout=timeout) 270 | 271 | if await self.is_checked() == checked: 272 | return 273 | 274 | if not trial: 275 | bounding_box = await self.bounding_box() 276 | if not bounding_box: 277 | raise PlaywrightError("Element is not visible") 278 | 279 | if self._page.scroll_into_view: 280 | await self.scroll_into_view_if_needed(timeout=timeout) 281 | 282 | if not await self.is_visible(): 283 | raise PlaywrightError("Element is outside of the viewport") 284 | 285 | x, y, width, height = bounding_box["x"], bounding_box["y"], bounding_box["width"], bounding_box["height"] 286 | if not any(position.values()): 287 | x, y = x + width // 2, y + height // 2 288 | else: 289 | x, y = x + position["x"], y + position["y"] 290 | 291 | await self._page.mouse.click(x, y, button="left", click_count=1, delay=20) 292 | 293 | assert await self.is_checked() == checked 294 | 295 | async def hover( 296 | self, 297 | force: Optional[bool] = False, 298 | modifiers: Optional[Sequence[Literal["Alt", "Control", "Meta", "Shift"]]] = None, 299 | position: Optional[Position] = None, 300 | timeout: Optional[float] = None, 301 | trial: Optional[bool] = False, 302 | no_wait_after: Optional[bool] = False, 303 | ) -> None: 304 | modifiers = modifiers or [] 305 | position = position or Position(x=0, y=0) 306 | 307 | if not force: 308 | await self.wait_for(state="attached", timeout=timeout) 309 | 310 | if not trial: 311 | bounding_box = await self.bounding_box() 312 | if not bounding_box: 313 | raise PlaywrightError("Element is not visible") 314 | 315 | if self._page.scroll_into_view: 316 | await self.scroll_into_view_if_needed(timeout=timeout) 317 | 318 | if not await self.is_visible(): 319 | raise PlaywrightError("Element is outside of the viewport") 320 | 321 | x, y, width, height = bounding_box["x"], bounding_box["y"], bounding_box["width"], bounding_box["height"] 322 | if not any(position.values()): 323 | x, y = x + width // 2, y + height // 2 324 | else: 325 | x, y = x + position["x"], y + position["y"] 326 | 327 | for modifier in modifiers: 328 | await self._page.keyboard.down(modifier) 329 | 330 | await self._page.mouse.move(x, y) 331 | 332 | for modifier in modifiers: 333 | await self._page.keyboard.up(modifier) 334 | 335 | async def type(self, text: str, delay: Optional[float] = 200.0, no_wait_after: Optional[bool] = False, timeout: Optional[float] = None) -> None: 336 | await self.wait_for(state="attached", timeout=timeout) 337 | 338 | bounding_box = await self.bounding_box() 339 | if not bounding_box: 340 | raise PlaywrightError("Element is not visible") 341 | 342 | if self._page.scroll_into_view: 343 | await self.scroll_into_view_if_needed(timeout=timeout) 344 | 345 | x, y, width, height = bounding_box["x"], bounding_box["y"], bounding_box["width"], bounding_box["height"] 346 | 347 | x, y = x + width // 2, y + height // 2 348 | 349 | await self.click(delay=delay) 350 | 351 | await self._page.keyboard.type(text, delay=delay) 352 | 353 | def _attach_dyn_prop(self, locator: Locator, prop_name: str, prop: Any) -> None: 354 | """Attach property proper to instance with name prop_name.y 355 | 356 | Reference: 357 | * https://stackoverflow.com/a/1355444/509706 358 | * https://stackoverflow.com/questions/48448074 359 | """ 360 | class_name = locator.__class__.__name__ + "Child" 361 | child_class = type(class_name, (locator.__class__,), {prop_name: prop}) 362 | 363 | locator.__class__ = child_class 364 | -------------------------------------------------------------------------------- /botright/playwright_mock/mouse.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import math 4 | import random 5 | from typing import TYPE_CHECKING, Any, List, Literal, NoReturn, Optional, Tuple, Union 6 | 7 | # fmt: off 8 | import numpy as np 9 | # from undetected_playwright.async_api import Mouse as PlaywrightMouse 10 | from playwright.async_api import Mouse as PlaywrightMouse 11 | 12 | # fmt: on 13 | 14 | if TYPE_CHECKING: 15 | from . import Page 16 | 17 | 18 | # From https://github.com/riflosnake/HumanCursor/blob/main/humancursor/utilities/human_curve_generator.py 19 | class HumanizeMouseTrajectory: 20 | def __init__(self, from_point: Tuple[int, int], to_point: Tuple[int, int]) -> None: 21 | self.from_point = from_point 22 | self.to_point = to_point 23 | self.points = self.generate_curve() 24 | 25 | def easeOutQuad(self, n: float) -> float: 26 | if not 0.0 <= n <= 1.0: 27 | raise ValueError("Argument must be between 0.0 and 1.0.") 28 | return -n * (n - 2) 29 | 30 | def generate_curve(self) -> List[Tuple[int, int]]: 31 | """Generates the curve based on arguments below, default values below are automatically modified to cause randomness""" 32 | left_boundary = min(self.from_point[0], self.to_point[0]) - 80 33 | right_boundary = max(self.from_point[0], self.to_point[0]) + 80 34 | down_boundary = min(self.from_point[1], self.to_point[1]) - 80 35 | up_boundary = max(self.from_point[1], self.to_point[1]) + 80 36 | 37 | internalKnots = self.generate_internal_knots(left_boundary, right_boundary, down_boundary, up_boundary, 2) 38 | points = self.generate_points(internalKnots) 39 | points = self.distort_points(points, 1, 1, 0.5) 40 | points = self.tween_points(points, 100) 41 | return points 42 | 43 | def generate_internal_knots( 44 | self, l_boundary: Union[int, float], r_boundary: Union[int, float], d_boundary: Union[int, float], u_boundary: Union[int, float], knots_count: int 45 | ) -> Union[List[Tuple[int, int]], NoReturn]: 46 | """Generates the internal knots of the curve randomly""" 47 | if not (self.check_if_numeric(l_boundary) and self.check_if_numeric(r_boundary) and self.check_if_numeric(d_boundary) and self.check_if_numeric(u_boundary)): 48 | raise ValueError("Boundaries must be numeric values") 49 | if not isinstance(knots_count, int) or knots_count < 0: 50 | knots_count = 0 51 | if l_boundary > r_boundary: 52 | raise ValueError("left_boundary must be less than or equal to right_boundary") 53 | if d_boundary > u_boundary: 54 | raise ValueError("down_boundary must be less than or equal to upper_boundary") 55 | 56 | knotsX = np.random.choice(range(int(l_boundary), int(r_boundary)), size=knots_count) 57 | knotsY = np.random.choice(range(int(d_boundary), int(u_boundary)), size=knots_count) 58 | 59 | knots = list(zip(knotsX, knotsY)) 60 | return knots 61 | 62 | def generate_points(self, knots: List[Tuple[int, int]]) -> List[Tuple[int, int]]: 63 | """Generates the points from BezierCalculator""" 64 | if not self.check_if_list_of_points(knots): 65 | raise ValueError("knots must be valid list of points") 66 | 67 | midPtsCnt = max( 68 | abs(self.from_point[0] - self.to_point[0]), 69 | abs(self.from_point[1] - self.to_point[1]), 70 | 2, 71 | ) 72 | knots = [self.from_point] + knots + [self.to_point] 73 | return BezierCalculator.calculate_points_in_curve(int(midPtsCnt), knots) 74 | 75 | def distort_points(self, points: List[Tuple[int, int]], distortion_mean: int, distortion_st_dev: int, distortion_frequency: float) -> Union[List[Tuple[int, int]], NoReturn]: 76 | """Distorts points by parameters of mean, standard deviation and frequency""" 77 | if not (self.check_if_numeric(distortion_mean) and self.check_if_numeric(distortion_st_dev) and self.check_if_numeric(distortion_frequency)): 78 | raise ValueError("Distortions must be numeric") 79 | if not self.check_if_list_of_points(points): 80 | raise ValueError("points must be valid list of points") 81 | if not (0 <= distortion_frequency <= 1): 82 | raise ValueError("distortion_frequency must be in range [0,1]") 83 | 84 | distorted: List[Tuple[int, int]] = [] 85 | for i in range(1, len(points) - 1): 86 | x, y = points[i] 87 | delta = int(np.random.normal(distortion_mean, distortion_st_dev) if random.random() < distortion_frequency else 0) 88 | distorted.append((x, y + delta)) 89 | distorted = [points[0]] + distorted + [points[-1]] 90 | return distorted 91 | 92 | def tween_points(self, points: List[Tuple[int, int]], target_points: int) -> Union[List[Tuple[int, int]], NoReturn]: 93 | """Modifies points by tween""" 94 | if not self.check_if_list_of_points(points): 95 | raise ValueError("List of points not valid") 96 | if not isinstance(target_points, int) or target_points < 2: 97 | raise ValueError("target_points must be an integer greater or equal to 2") 98 | 99 | res: List[Tuple[int, int]] = [] 100 | for i in range(target_points): 101 | index = int(self.easeOutQuad(float(i) / (target_points - 1)) * (len(points) - 1)) 102 | res += (points[index],) 103 | return res 104 | 105 | @staticmethod 106 | def check_if_numeric(val: Any) -> bool: 107 | """Checks if value is proper numeric value""" 108 | return isinstance(val, (float, int, np.integer, np.float32, np.float64)) 109 | 110 | def check_if_list_of_points(self, list_of_points: List[Tuple[int, int]]) -> bool: 111 | """Checks if list of points is valid""" 112 | try: 113 | 114 | def point(p): 115 | return (len(p) == 2) and self.check_if_numeric(p[0]) and self.check_if_numeric(p[1]) 116 | 117 | return all(map(point, list_of_points)) 118 | except (KeyError, TypeError): 119 | return False 120 | 121 | 122 | class BezierCalculator: 123 | @staticmethod 124 | def binomial(n: int, k: int): 125 | """Returns the binomial coefficient "n choose k" """ 126 | return math.factorial(n) / float(math.factorial(k) * math.factorial(n - k)) 127 | 128 | @staticmethod 129 | def bernstein_polynomial_point(x: int, i: int, n: int): 130 | """Calculate the i-th component of a bernstein polynomial of degree n""" 131 | return BezierCalculator.binomial(n, i) * (x**i) * ((1 - x) ** (n - i)) 132 | 133 | @staticmethod 134 | def bernstein_polynomial(points: List[Tuple[int, int]]): 135 | """ 136 | Given list of control points, returns a function, which given a point [0,1] returns 137 | a point in the Bezier described by these points 138 | """ 139 | 140 | def bernstein(t): 141 | n = len(points) - 1 142 | x = y = 0 143 | for i, point in enumerate(points): 144 | bern = BezierCalculator.bernstein_polynomial_point(t, i, n) 145 | x += point[0] * bern 146 | y += point[1] * bern 147 | return x, y 148 | 149 | return bernstein 150 | 151 | @staticmethod 152 | def calculate_points_in_curve(n: int, points: List[Tuple[int, int]]) -> List[Tuple[int, int]]: 153 | """ 154 | Given list of control points, returns n points in the Bézier curve, 155 | described by these points 156 | """ 157 | curvePoints: List[Tuple[int, int]] = [] 158 | bernstein_polynomial = BezierCalculator.bernstein_polynomial(points) 159 | for i in range(n): 160 | t = i / (n - 1) 161 | curvePoints += (bernstein_polynomial(t),) 162 | return curvePoints 163 | 164 | 165 | class Mouse(PlaywrightMouse): 166 | last_x: int = 0 167 | last_y: int = 0 168 | 169 | def __init__(self, mouse: PlaywrightMouse, page: Page): 170 | super().__init__(mouse) 171 | self._impl_obj = mouse._impl_obj 172 | self._page = page 173 | self._mouse = mouse 174 | 175 | self._origin_move = mouse.move 176 | self._origin_dblclick = mouse.dblclick 177 | 178 | self.last_x = 0 179 | self.last_y = 0 180 | 181 | async def click( 182 | self, 183 | x: Union[int, float], 184 | y: Union[int, float], 185 | button: Optional[Literal["left", "middle", "right"]] = "left", 186 | click_count: Optional[int] = 1, 187 | delay: Optional[float] = 20.0, 188 | humanly: Optional[bool] = True, 189 | ) -> None: 190 | delay = delay or 20.0 191 | # Move mouse humanly to the Coordinates and wait some random time 192 | await self.move(x, y) # , humanly 193 | await self._page.wait_for_timeout(random.randint(4, 8) * 50) 194 | 195 | # Clicking the Coordinates 196 | await self.down(button=button, click_count=click_count) 197 | # Waiting as delay 198 | await self._page.wait_for_timeout(delay) 199 | await self.up(button=button, click_count=click_count) 200 | 201 | # Waiting random time 202 | await self._page.wait_for_timeout(random.randint(4, 8) * 50) 203 | 204 | async def dblclick( 205 | self, x: Union[int, float], y: Union[int, float], button: Optional[Literal["left", "middle", "right"]] = "left", delay: Optional[float] = 20.0, humanly: Optional[bool] = True 206 | ) -> None: 207 | delay = delay or 20.0 208 | # Move mouse humanly to the Coordinates and wait some random time 209 | await self.move(x, y, humanly) 210 | await self._page.wait_for_timeout(random.randint(4, 8) * 50) 211 | 212 | # Clicking the Coordinates 213 | # await self.down(button=button) 214 | # # Waiting as delay 215 | # await self._page.wait_for_timeout(delay) 216 | # await self.up(button=button) 217 | # 218 | # # Waiting short random time 219 | # await self._page.wait_for_timeout(random.randint(8, 14) * 10) 220 | # # Clicking the Coordinates 221 | # await self.down(button=button) 222 | # # Waiting as delay 223 | # await self._page.wait_for_timeout(delay) 224 | # await self.up(button=button) 225 | await self._origin_dblclick(x, y, button=button, delay=random.randint(8, 14) * 10) 226 | 227 | # Waiting random time 228 | await self._page.wait_for_timeout(random.randint(4, 8) * 50) 229 | 230 | async def move(self, x: Union[int, float], y: Union[int, float], steps: Optional[int] = 1, humanly: Optional[bool] = True, sex=False) -> None: 231 | # If you want to move in a straight line 232 | if not humanly: 233 | await self._origin_move(x=x, y=y, steps=steps) 234 | return 235 | 236 | if x == self.last_x and y == self.last_y: 237 | await self._page.wait_for_timeout(random.randint(1, 10)) 238 | return 239 | 240 | humanized_points = HumanizeMouseTrajectory((int(self.last_x), int(self.last_y)), (int(x), int(y))) 241 | 242 | # Move Mouse to new random locations 243 | for x, y in humanized_points.points: 244 | await self._origin_move(x=x, y=y) 245 | # await page.wait_for_timeout(random.randint(1, 5)) 246 | 247 | # Set LastX and LastY cause Playwright does not have mouse.current_location 248 | self.last_x, self.last_y = humanized_points.points[-1] 249 | -------------------------------------------------------------------------------- /botright/playwright_mock/routes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | # from undetected_playwright.async_api import Route as PlaywrightRoute, Request as PlaywrightRequest, Response as PlaywrightResponse 6 | from playwright.async_api import Request as PlaywrightRequest 7 | from playwright.async_api import Response as PlaywrightResponse 8 | from playwright.async_api import Route as PlaywrightRoute 9 | 10 | if TYPE_CHECKING: 11 | from . import Page 12 | 13 | from . import Frame 14 | 15 | 16 | class Request(PlaywrightRequest): 17 | def __init__(self, request: PlaywrightRequest, page: Page): 18 | super().__init__(request) 19 | self._impl_obj = request._impl_obj 20 | self._page = page 21 | 22 | self._frame = request.frame 23 | self._redirected_from = request.redirected_from 24 | self._redirected_to = request.redirected_to 25 | 26 | self.origin_response = request.response 27 | 28 | @property 29 | def frame(self): 30 | return Frame(self._frame, self._page) 31 | 32 | @property 33 | def redirected_from(self): 34 | if not self._redirected_from: 35 | return False 36 | return Request(self._redirected_from, self._page) 37 | 38 | @property 39 | def redirected_to(self): 40 | if not self._redirected_to: 41 | return False 42 | return Request(self._redirected_to, self._page) 43 | 44 | async def response(self): 45 | _response = await self.origin_response() 46 | if not _response: 47 | return None 48 | 49 | return Response(_response, self._page) 50 | 51 | 52 | class Response(PlaywrightResponse): 53 | def __init__(self, response: PlaywrightResponse, page: Page): 54 | super().__init__(response) 55 | self._impl_obj = response._impl_obj 56 | self._page = page 57 | 58 | self._frame = response.frame 59 | self._request = response.request 60 | 61 | @property 62 | def frame(self): 63 | return Frame(self._frame, self._page) 64 | 65 | @property 66 | def request(self): 67 | return Request(self._request, self._page) 68 | 69 | 70 | class Route(PlaywrightRoute): 71 | def __init__(self, route: PlaywrightRoute, page: Page): 72 | super().__init__(route) 73 | self._impl_obj = route._impl_obj 74 | self._page = page 75 | 76 | self._request = route.request 77 | 78 | @property 79 | def request(self): 80 | return Request(self._request, self._page) 81 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to develop on this project 2 | 3 | Botright welcomes contributions from the community. 4 | 5 | **You need PYTHON3!** 6 | 7 | This instructions are for linux base systems. (Linux, MacOS, BSD, etc.) 8 | ## Setting up your own fork of this repo. 9 | 10 | - On github interface click on `Fork` button. 11 | - Clone your fork of this repo. `git clone git@github.com:YOUR_GIT_USERNAME/botright.git` 12 | - Enter the directory `cd botright` 13 | - Add upstream repo `git remote add upstream https://github.com/Vinyzu/botright` 14 | 15 | ## Setting up your own virtual environment 16 | 17 | Run `make virtualenv` to create a virtual environment. 18 | then activate it with `source .venv/bin/activate`. 19 | 20 | ## Install the project in develop mode 21 | 22 | Run `make install` to install the project in develop mode. 23 | 24 | ## Run the tests to ensure everything is working 25 | 26 | Run `make test` to run the tests. 27 | 28 | ## Create a new branch to work on your contribution 29 | 30 | Run `git checkout -b my_contribution` 31 | 32 | ## Make your changes 33 | 34 | Edit the files using your preferred editor. (we recommend VIM or VSCode) 35 | 36 | ## Format the code 37 | 38 | Run `make fmt` to format the code. 39 | 40 | ## Run the linter 41 | 42 | Run `make lint` to run the linter. 43 | 44 | ## Test your changes 45 | 46 | Run `make test` to run the tests. 47 | 48 | Ensure code coverage report shows `100%` coverage, add tests to your PR. 49 | 50 | ## Build the docs locally 51 | 52 | Run `make docs` to build the docs. 53 | 54 | Ensure your new changes are documented. 55 | 56 | ## Commit your changes 57 | 58 | This project uses [conventional git commit messages](https://www.conventionalcommits.org/en/v1.0.0/). 59 | 60 | Example: `fix(package): update setup.py arguments 🎉` (emojis are fine too) 61 | 62 | ## Push your changes to your fork 63 | 64 | Run `git push origin my_contribution` 65 | 66 | ## Submit a pull request 67 | 68 | On github interface, click on `Pull Request` button. 69 | 70 | Wait CI to run and one of the developers will review your PR. 71 | ## Makefile utilities 72 | 73 | This project comes with a `Makefile` that contains a number of useful utility. 74 | 75 | ```bash 76 | ❯ make 77 | Usage: make 78 | 79 | Targets: 80 | help: ## Show the help. 81 | install: ## Install the project in dev mode. 82 | fmt: ## Format code using black & isort. 83 | lint: ## Run pep8, black, mypy linters. 84 | test: lint ## Run tests and generate coverage report. 85 | watch: ## Run tests on every change. 86 | clean: ## Clean unused files. 87 | virtualenv: ## Create a virtual environment. 88 | release: ## Create a new tag for release. 89 | docs: ## Build the documentation. 90 | switch-to-poetry: ## Switch to poetry package manager. 91 | init: ## Initialize the project based on an application template. 92 | ``` 93 | 94 | ## Making a new release 95 | 96 | This project uses [semantic versioning](https://semver.org/) and tags releases with `X.Y.Z` 97 | Every time a new tag is created and pushed to the remote repo, github actions will 98 | automatically create a new release on github and trigger a release on PyPI. 99 | 100 | For this to work you need to setup a secret called `PIPY_API_TOKEN` on the project settings>secrets, 101 | this token can be generated on [pypi.org](https://pypi.org/account/). 102 | 103 | To trigger a new release all you need to do is. 104 | 105 | 1. If you have changes to add to the repo 106 | * Make your changes following the steps described above. 107 | * Commit your changes following the [conventional git commit messages](https://www.conventionalcommits.org/en/v1.0.0/). 108 | 2. Run the tests to ensure everything is working. 109 | 4. Run `make release` to create a new tag and push it to the remote repo. 110 | 111 | the `make release` will ask you the version number to create the tag, ex: type `0.1.1` when you are asked. 112 | 113 | > **CAUTION**: The make release will change local changelog files and commit all the unstaged changes you have. 114 | -------------------------------------------------------------------------------- /docs/HISTORY.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.5 (2024-01-15) 5 | ------------------ 6 | - Added Github Actions with Tox, Mypy & Flake8. [Vinyzu] 7 | - Using my new projects chrome-fingerprints and recognizer. [Vinyzu] 8 | - Disabled GeeTest Files and Requirements as theyre currently unsupported. [Vinyzu] 9 | - Added Proper Typing and Docstrings for IntelliSense. [Vinyzu] 10 | - Added Custom Types in botright.extended_typing. [Vinyzu] 11 | - Reworked File Structure & Names. [Vinyzu] 12 | - Reworked Installation & Set Files. [Vinyzu] 13 | - Deleted Unimportant Files. [Vinyzu] 14 | 15 | 0.4 (2023-10-06) 16 | ------------------ 17 | - Better Stealth. [Vinyzu] 18 | - Added Support and Recommendation for Ungoogled Chromium. [Vinyzu] 19 | - Using Bayesian Network to generate Fingerprints. [Vinyzu] 20 | - Improved Code Runtime & Generated DocStrings. [Vinyzu] 21 | - Added Detection Tests (see dev_test.py). [Vinyzu] 22 | - Removed unnecessary Extension. [Vinyzu] 23 | - Added Requirements. [Vinyzu] 24 | 25 | 0.3 (2023-09-15) 26 | ------------------ 27 | - Better Stealth. [Vinyzu] 28 | - Better Mouse Movement. [Vinyzu] 29 | - Improved Code Runtime & Implemented Type Hints. [Vinyzu] 30 | - Implementation of hcaptcha-challenger & recaptcha-challenger. [Vinyzu] 31 | - Removed unnecessary Files. [Vinyzu] 32 | 33 | 0.2 (2022-09-21) 34 | ------------------ 35 | - New Features: reCaptchaSolver, geeTestv3, geeTestv4. [Vinyzu] 36 | - Bug Fixes. [Vinyzu] 37 | - Stability and Speed Fixes. [Vinyzu] 38 | 39 | 0.1 (2022-09-21) 40 | ------------------ 41 | - Initial Release. [Vinyzu] 42 | -------------------------------------------------------------------------------- /docs/botright.rst: -------------------------------------------------------------------------------- 1 | Initialization 2 | -------------- 3 | 4 | Botright_ 5 | ~~~~~~~~ 6 | 7 | - ``await botright.Botright()`` 8 | .. 9 | 10 | Initialize a Botright Session 11 | 12 | +--------------------------------------+--------------------------------------+ 13 | | Kwargs | Usage | 14 | +======================================+======================================+ 15 | | ``headless`` (bool) | Whether to run browser in | 16 | | | headless mode. Defaults to | 17 | | | ``False`` | 18 | +--------------------------------------+--------------------------------------+ 19 | | ``block_images`` (bool) | Wether to block images to lower | 20 | | | Network Usage. Defaults to ``False`` | 21 | +--------------------------------------+--------------------------------------+ 22 | | ``cache_responses`` (bool) | Whether to Cache certain responses. | 23 | | | to lower Network Usage. | 24 | | | Defaults to ``False`` | 25 | +--------------------------------------+--------------------------------------+ 26 | | ``user_action_layer`` (bool) | Shows what the Bot is doing in the | 27 | | | Browser GUI. Defaults to ``True`` | 28 | +--------------------------------------+--------------------------------------+ 29 | | ``scroll_into_view`` (bool) | Whether to scroll every Element | 30 | | | into View. Defaults to ``True`` | 31 | +--------------------------------------+--------------------------------------+ 32 | | ``spoof_canvas`` (bool) | Whether to disable canvas fingerprint| 33 | | | protection. Defaults to ``True`` | 34 | +--------------------------------------+--------------------------------------+ 35 | | ``mask_fingerprint`` (bool) | Wether to mask browser fingerprints | 36 | | | Disables spoofing a fake fingerprint | 37 | | | boosts stealth. Defaults to ``False``| 38 | +--------------------------------------+--------------------------------------+ 39 | | ``spoof_canvas`` (bool) | Whether to disable canvas fingerprint| 40 | | | protection. Defaults to ``True`` | 41 | +--------------------------------------+--------------------------------------+ 42 | | ``use_undetected_playwright`` (bool) | hether to use undetected_playwright. | 43 | | | Only Temporary. Defaults to ``False``| 44 | +--------------------------------------+--------------------------------------+ 45 | 46 | - returns: ``Botright`` 47 | 48 | -------------- 49 | 50 | NewBrowser_ 51 | ~~~~~~~~~~ 52 | 53 | - ``await botright_client.new_browser()`` 54 | .. 55 | 56 | Create a new Botright browser instance with specified configurations. 57 | 58 | +-------------------------------------+--------------------------------+ 59 | | Kwargs | Usage | 60 | +=====================================+================================+ 61 | | ``proxy`` (str) | Used to pass a | 62 | | | ProxyServer-Address. Example: | 63 | | | ``username:password@ip:port``. | 64 | | | Defaults to ``None`` | 65 | +-------------------------------------+--------------------------------+ 66 | | ``**PlaywrightContextArgs`` | See | 67 | | | `ContextDocs `__ | 70 | | | for further possible | 71 | | | Arguments. Defaults to | 72 | | | ``None`` | 73 | +-------------------------------------+--------------------------------+ 74 | 75 | - returns: ``botright.extended_typing.Browser`` 76 | 77 | -------------- 78 | 79 | Captcha Solving 80 | -------------- 81 | 82 | Get a hCaptcha Key with Sitekey & rqData_ 83 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 84 | 85 | - ``await page.get_hcaptcha()`` 86 | .. 87 | 88 | Spawns a new Page and Solves Captcha 89 | 90 | +-------------------------------------+--------------------------------+ 91 | | Kwargs | Usage | 92 | +=====================================+================================+ 93 | | ``sitekey`` (str) | Specify the Sitekey to solve | 94 | | | the Captcha with. Defaults to | 95 | | | ``00000000 | 96 | | | -0000-0000-0000-000000000000`` | 97 | +-------------------------------------+--------------------------------+ 98 | | ``rqdata`` (str) | Specify rqData to mock the | 99 | | | Captcha with. Defaults to | 100 | | | ``None`` | 101 | +-------------------------------------+--------------------------------+ 102 | 103 | - returns: ``hCaptchaKey (str)`` 104 | 105 | -------------- 106 | 107 | Solve hCaptcha_ 108 | ~~~~~~~~~~~~~~ 109 | 110 | - ``await page.solve_hcaptcha()`` 111 | .. 112 | 113 | Solves a hCaptcha on the given Page 114 | 115 | +------------------+--------------------------------------------------+ 116 | | Kwargs | Usage | 117 | +==================+==================================================+ 118 | | ``rqdata`` (str) | Specify rqData to mock the Captcha with. | 119 | | | Defaults to ``None`` | 120 | +------------------+--------------------------------------------------+ 121 | 122 | - returns: ``hCaptchaKey (str)`` 123 | 124 | -------------- 125 | 126 | Solve reCaptcha_ 127 | ~~~~~~~~~~~~~~~ 128 | 129 | - ``await page.solve_recaptcha()`` 130 | .. 131 | 132 | Solves a reCaptcha on the given Page 133 | 134 | | 135 | 136 | - returns: ``reCaptchaKey`` 137 | 138 | -------------- 139 | 140 | Solve geeTest_ 141 | ~~~~~~~~~~~~~ 142 | 143 | - ``await page.solve_geetest()`` 144 | .. 145 | 146 | Solves a geeTest (v3 or v4) on the given Page 147 | 148 | +-------------------------------------+--------------------------------+ 149 | | Kwargs | Usage | 150 | +=====================================+================================+ 151 | | ``mode`` (str) | Specify Mode to solve | 152 | | | IconCaptchas with. Defaults to | 153 | | | ``"canny"``. Supported Modes: | 154 | | | “canny”, “clip”, “ssim”, | 155 | | | “random” | 156 | +-------------------------------------+--------------------------------+ 157 | 158 | - returns: ``geeTestKey`` 159 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to Botright! 2 | ==================== 3 | 4 | For full documentation of changes (compared to Playwright) visit 5 | `Botright Documentation `__. Except of these changes, you 6 | can use Botright after the 7 | `Playwright Docs `__ 8 | 9 | Installation 10 | ------------ 11 | 12 | Pip 13 | ~~~ 14 | 15 | |PyPI version| 16 | 17 | .. code:: bash 18 | 19 | pip install --upgrade pip 20 | pip install botright 21 | playwright install 22 | python -c 'import hcaptcha_challenger; solver.install(clip=True)' 23 | 24 | Usage 25 | ----- 26 | 27 | Once installed, you can ``import`` Botright in a Python script, and 28 | launch a firefox browser. 29 | 30 | .. code:: py 31 | 32 | import asyncio 33 | import botright 34 | 35 | 36 | async def main(): 37 | botright_client = await botright.Botright() 38 | browser = await botright_client.new_browser() 39 | page = await browser.new_page() 40 | 41 | await page.goto("http://playwright.dev") 42 | print(await page.title()) 43 | 44 | await botright_client.close() 45 | 46 | if __name__ == "__main__": 47 | asyncio.run(main()) 48 | 49 | Captchas 50 | -------- 51 | 52 | Botright is able to solve a wide variety of Captchas. For Documentation 53 | of these functions visit `BotrightDocumentation `__. 54 | 55 | Here all Captchas supported as of now 56 | 57 | +----------------------+-----------+---------------------------------+---------------------------+ 58 | | Captcha Type | Supported | Solved By | Success Rate | 59 | +======================+===========+=================================+===========================+ 60 | | **hCaptcha** | Yes | hcaptcha-challenger | Up to 90% | 61 | +----------------------+-----------+---------------------------------+---------------------------+ 62 | | | | | | 63 | +----------------------+-----------+---------------------------------+---------------------------+ 64 | | **reCaptcha** | Yes | reCognizer | 50%-80% | 65 | +----------------------+-----------+---------------------------------+---------------------------+ 66 | | | | | | 67 | +----------------------+-----------+---------------------------------+---------------------------+ 68 | | **geeTestv3** | Temp. Not | | | 69 | +----------------------+-----------+---------------------------------+---------------------------+ 70 | | v3 Intelligent Mode | Yes | botright´s stealthiness | 100% | 71 | +----------------------+-----------+---------------------------------+---------------------------+ 72 | | v3 Slider Captcha | Yes | cv2.matchTemplate | 100% | 73 | +----------------------+-----------+---------------------------------+---------------------------+ 74 | | v3 Nine Captcha | Yes | CLIP Detection | 50% | 75 | +----------------------+-----------+---------------------------------+---------------------------+ 76 | | v3 Icon Captcha | Yes | cv2.matchTemplate / SSIM / CLIP | 70% | 77 | +----------------------+-----------+---------------------------------+---------------------------+ 78 | | v3 Space Captcha | No | Not solvable | 0% | 79 | +----------------------+-----------+---------------------------------+---------------------------+ 80 | | | | | | 81 | +----------------------+-----------+---------------------------------+---------------------------+ 82 | | **geeTestv4** | Temp. Not | | | 83 | +----------------------+-----------+---------------------------------+---------------------------+ 84 | | v4 Intelligent Mode | Yes | botright´s stealthiness | 100% | 85 | +----------------------+-----------+---------------------------------+---------------------------+ 86 | | v4 Slider Captcha | Yes | cv2.matchTemplate | 100% | 87 | +----------------------+-----------+---------------------------------+---------------------------+ 88 | | v4 GoBang Captcha | Yes | Math Calculations | 100% | 89 | +----------------------+-----------+---------------------------------+---------------------------+ 90 | | v4 Icon Captcha | Yes | cv2.matchTemplate / SSIM / CLIP | 60% | 91 | +----------------------+-----------+---------------------------------+---------------------------+ 92 | | v4 IconCrush Captcha | Yes | Math Calculations | 100% | 93 | +----------------------+-----------+---------------------------------+---------------------------+ 94 | 95 | Proxies 96 | -------- 97 | 98 | Botright currently only supports HTTP(S) proxies. 99 | You can use almost every common format, but if you want to go safe, use ``ip:port`` or ``username:password@ip:port`` for auth proxies. 100 | 101 | First script 102 | ------------ 103 | 104 | In our first script, we will navigate to `Creep.js `__ and 105 | take a screenshot in Chromium. 106 | 107 | .. code:: py 108 | 109 | import asyncio 110 | import botright 111 | 112 | 113 | async def main(): 114 | botright_client = await botright.Botright() 115 | browser = await botright_client.new_browser() 116 | page = await browser.new_page() 117 | 118 | await page.goto("https://abrahamjuliot.github.io/creepjs/") 119 | await page.wait_for_timeout(5000) # Wait for stats to load 120 | await page.screenshot(path="example.png", full_page=True) 121 | 122 | await botright_client.close() 123 | 124 | if __name__ == "__main__": 125 | asyncio.run(main()) 126 | 127 | Interactive mode (REPL) 128 | ----------------------- 129 | 130 | You can launch the interactive python REPL: 131 | 132 | .. code:: bash 133 | 134 | python -m asyncio 135 | 136 | and then launch Botright within it for quick experimentation: 137 | 138 | .. code:: py 139 | 140 | >>> import botright 141 | >>> botright_client = await botright.Botright() 142 | # Pass headless=False to botright.Botright() to see the browser UI 143 | >>> browser = await botright_client.new_browser() 144 | >>> page = await browser.new_page() 145 | >>> await page.goto("https://abrahamjuliot.github.io/creepjs/") 146 | >>> await page.wait_for_timeout(5000) # Wait for stats to load 147 | >>> await page.screenshot(path="example.png", full_page=True) 148 | >>> await botright_client.close() 149 | >>> botright_client = await botright.Botright() 150 | # Pass headless=False to botright.Botright() to see the browser UI 151 | >>> browser = await botright_client.new_browser() 152 | >>> page = await browser.new_page() 153 | >>> await page.goto("https://abrahamjuliot.github.io/creepjs/") 154 | >>> await page.wait_for_timeout(5000) # Wait for stats to load 155 | >>> await page.screenshot(path="example.png", full_page=True) 156 | >>> await botright_client.close() 157 | 158 | Pyinstaller 159 | ----------- 160 | 161 | You can use Botright with `Pyinstaller `__ 162 | to create standalone executables. 163 | 164 | .. code:: py 165 | 166 | # main.py 167 | import asyncio 168 | import botright 169 | 170 | 171 | async def main(): 172 | botright_client = await botright.Botright() 173 | browser = await botright_client.new_browser() 174 | page = await browser.new_page() 175 | 176 | page.goto("http://whatsmyuseragent.org/") 177 | page.screenshot(path="example.png") 178 | 179 | await botright_client.close() 180 | 181 | if __name__ == "__main__": 182 | asyncio.run(main()) 183 | 184 | If you want to bundle browsers with the executables: 185 | 186 | .. code:: bash 187 | 188 | PLAYWRIGHT_BROWSERS_PATH=0 playwright install firefox 189 | pyinstaller -F main.py 190 | 191 | .. code:: batch 192 | 193 | set PLAYWRIGHT_BROWSERS_PATH=0 194 | playwright install firefox 195 | pyinstaller -F main.py 196 | 197 | .. code:: powershell 198 | 199 | $env:PLAYWRIGHT_BROWSERS_PATH="0" 200 | playwright install firefox 201 | pyinstaller -F main.py 202 | 203 | Known issues 204 | ------------ 205 | 206 | Threading 207 | ~~~~~~~~~ 208 | 209 | Botright´s API is not thread-safe. If you are using Botright in a 210 | multi-threaded environment, you should create a botright instance per 211 | thread. See `threading 212 | issue `__ for 213 | more details. 214 | For asynchronous usage, you should probably use asyncio.gather(*threads) instead. 215 | 216 | .. |PyPI version| image:: https://badge.fury.io/py/botright.svg 217 | :target: https://pypi.python.org/pypi/botright/ 218 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=68.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.pytest.ini_options] 6 | testpaths = [ 7 | "tests", 8 | ] 9 | filterwarnings = [ 10 | "ignore::DeprecationWarning", 11 | ] 12 | 13 | [tool.mypy] 14 | mypy_path = "botright" 15 | check_untyped_defs = true 16 | disallow_any_generics = true 17 | ignore_missing_imports = true 18 | no_implicit_optional = true 19 | show_error_codes = true 20 | strict_equality = true 21 | warn_redundant_casts = true 22 | warn_return_any = true 23 | warn_unreachable = true 24 | warn_unused_configs = true 25 | no_implicit_reexport = true 26 | disable_error_code = "method-assign" 27 | 28 | [tool.black] 29 | line-length = 200 30 | 31 | [tool.isort] 32 | py_version = 310 33 | line_length = 200 34 | multi_line_output = 7 -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # This requirements are for development and testing only, not for production. 2 | flake8==7.0.0 3 | pytest==8.1.1 4 | pytest_asyncio==0.23.6 5 | mypy==1.9.0 6 | types-setuptools==69.5.0.20240415 7 | twisted==24.3.0 8 | playwright==1.42.0 9 | black==24.4.0 10 | isort==5.13.2 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # General Requirements 2 | async_class #==0.5.0 3 | httpx==0.27.0 4 | playwright==1.42.0 5 | undetected-playwright-patch>=1.40.0.post1700587210000 6 | pybrowsers==0.5.2 7 | chrome-fingerprints==1.1 8 | 9 | # Math and Others 10 | numpy==1.26.4 11 | # scipy==1.11.4 12 | # Image Processing 13 | # Pillow==10.2.0 14 | 15 | 16 | # Artificial Intelligence 17 | hcaptcha_challenger>=0.10.1.post2 18 | recognizer==1.4 19 | # yolov5==7.0.13 20 | # sentence_transformers #==2.2.2 21 | # easyocr==1.7.1 22 | # opencv-python~=4.9.0.80 23 | setuptools~=69.5.1 24 | loguru==0.7.2 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = botright 3 | version = attr: botright.VERSION 4 | description = Botright, the most advance undetected, fingerprint-changing, captcha-solving, open-source automation framework. Build on Playwright, its as easy to use as it is to extend your code. Solving your Captchas for free with AI. 5 | long_description = file: README.md 6 | long_description_content_type = text/markdown 7 | author = Vinyzu 8 | url = https://github.com/Vinyzu/Botright 9 | license = GNU General Public License v3.0 10 | license_file = LICENSE 11 | keywords = botright, playwright, browser, automation, recaptcha_challenger, recaptcha-solver, hcaptcha_challenger, hcaptcha-solver, geetest_challenger, geetest-solver 12 | project_urls = 13 | Source = https://github.com/Vinyzu/Botright 14 | Documentation = https://github.com/Vinyzu/Botright/blob/main/docs/index.rst 15 | Tracker = https://github.com/Vinyzu/Botright/issues 16 | classifiers = 17 | Topic :: Scientific/Engineering 18 | Topic :: Scientific/Engineering :: Artificial Intelligence 19 | Topic :: Software Development 20 | Topic :: Software Development :: Libraries 21 | Topic :: Software Development :: Libraries :: Python Modules 22 | Topic :: Internet :: WWW/HTTP :: Browsers 23 | License :: OSI Approved :: Apache Software License 24 | Programming Language :: Python :: 3 25 | 26 | [options] 27 | zip_safe = no 28 | python_requires = >=3.8 29 | packages = find: 30 | install_requires = 31 | async_class 32 | httpx>=0.24.1 33 | playwright>=1.40.0 34 | undetected-playwright-patch 35 | pybrowsers>=0.5.2 36 | chrome-fingerprints 37 | numpy 38 | hcaptcha_challenger>=0.10.1.post1 39 | recognizer 40 | setuptools 41 | loguru 42 | 43 | # GeeTest Removed 44 | # scipy>=1.11.2 45 | # yolov5 46 | # easyocr==1.7.1 47 | # sentence_transformers 48 | # opencv-python~=4.8.0.76 49 | # Pillow 50 | 51 | 52 | [options.package_data] 53 | * = requirements.txt, geetest.torchscript 54 | 55 | [options.packages.find] 56 | include = botright, botright.*, LICENSE 57 | exclude = tests, .github 58 | 59 | [options.extras_require] 60 | testing = 61 | pytest 62 | mypy 63 | flake8 64 | black 65 | isort 66 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinyzu/Botright/8b66fa0d8395b369252b32c327434c29837f7797/tests/__init__.py -------------------------------------------------------------------------------- /tests/assets/beforeunload.html: -------------------------------------------------------------------------------- 1 |
beforeunload demo.
2 | -------------------------------------------------------------------------------- /tests/assets/csp.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/digits/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinyzu/Botright/8b66fa0d8395b369252b32c327434c29837f7797/tests/assets/digits/1.png -------------------------------------------------------------------------------- /tests/assets/digits/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinyzu/Botright/8b66fa0d8395b369252b32c327434c29837f7797/tests/assets/digits/2.png -------------------------------------------------------------------------------- /tests/assets/digits/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinyzu/Botright/8b66fa0d8395b369252b32c327434c29837f7797/tests/assets/digits/3.png -------------------------------------------------------------------------------- /tests/assets/dom.html: -------------------------------------------------------------------------------- 1 |
Text, 2 | more text
3 | 4 | -------------------------------------------------------------------------------- /tests/assets/drag-n-drop.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | 34 | 35 |
36 |

37 | Select this element, drag it to the Drop Zone and then release the selection to move the element.

38 |
39 |
Drop Zone
40 | -------------------------------------------------------------------------------- /tests/assets/empty.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinyzu/Botright/8b66fa0d8395b369252b32c327434c29837f7797/tests/assets/empty.html -------------------------------------------------------------------------------- /tests/assets/error.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/es6/es6import.js: -------------------------------------------------------------------------------- 1 | import num from './es6module.js'; 2 | window.__es6injected = num; -------------------------------------------------------------------------------- /tests/assets/es6/es6module.js: -------------------------------------------------------------------------------- 1 | export default 42; -------------------------------------------------------------------------------- /tests/assets/es6/es6pathimport.js: -------------------------------------------------------------------------------- 1 | import num from './es6/es6module.js'; 2 | window.__es6injected = num; -------------------------------------------------------------------------------- /tests/assets/frames/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 |
Hi, I'm frame
-------------------------------------------------------------------------------- /tests/assets/frames/frameset.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/assets/frames/nested-frames.html: -------------------------------------------------------------------------------- 1 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /tests/assets/frames/two-frames.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /tests/assets/grid.html: -------------------------------------------------------------------------------- 1 | 28 | 29 | -------------------------------------------------------------------------------- /tests/assets/injectedfile.js: -------------------------------------------------------------------------------- 1 | window.__injected = 42; 2 | window.injected = 123; 3 | window.__injectedError = new Error('hi'); -------------------------------------------------------------------------------- /tests/assets/injectedstyle.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: red; 3 | } -------------------------------------------------------------------------------- /tests/assets/input/button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Button test 5 | 6 | 7 | 8 | 9 | 31 | 32 | -------------------------------------------------------------------------------- /tests/assets/input/fileupload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | File upload test 5 | 6 | 7 |
8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /tests/assets/input/mouse-helper.js: -------------------------------------------------------------------------------- 1 | // This injects a box into the page that moves with the mouse; 2 | // Useful for debugging 3 | (function(){ 4 | const box = document.createElement('div'); 5 | box.classList.add('mouse-helper'); 6 | const styleElement = document.createElement('style'); 7 | styleElement.innerHTML = ` 8 | .mouse-helper { 9 | pointer-events: none; 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | width: 20px; 14 | height: 20px; 15 | background: rgba(0,0,0,.4); 16 | border: 1px solid white; 17 | border-radius: 10px; 18 | margin-left: -10px; 19 | margin-top: -10px; 20 | transition: background .2s, border-radius .2s, border-color .2s; 21 | } 22 | .mouse-helper.button-1 { 23 | transition: none; 24 | background: rgba(0,0,0,0.9); 25 | } 26 | .mouse-helper.button-2 { 27 | transition: none; 28 | border-color: rgba(0,0,255,0.9); 29 | } 30 | .mouse-helper.button-3 { 31 | transition: none; 32 | border-radius: 4px; 33 | } 34 | .mouse-helper.button-4 { 35 | transition: none; 36 | border-color: rgba(255,0,0,0.9); 37 | } 38 | .mouse-helper.button-5 { 39 | transition: none; 40 | border-color: rgba(0,255,0,0.9); 41 | } 42 | `; 43 | document.head.appendChild(styleElement); 44 | document.body.appendChild(box); 45 | document.addEventListener('mousemove', event => { 46 | box.style.left = event.pageX + 'px'; 47 | box.style.top = event.pageY + 'px'; 48 | updateButtons(event.buttons); 49 | }, true); 50 | document.addEventListener('mousedown', event => { 51 | updateButtons(event.buttons); 52 | box.classList.add('button-' + event.which); 53 | }, true); 54 | document.addEventListener('mouseup', event => { 55 | updateButtons(event.buttons); 56 | box.classList.remove('button-' + event.which); 57 | }, true); 58 | function updateButtons(buttons) { 59 | for (let i = 0; i < 5; i++) 60 | box.classList.toggle('button-' + i, buttons & (1 << i)); 61 | } 62 | })(); -------------------------------------------------------------------------------- /tests/assets/input/scrollable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scrollable test 5 | 6 | 7 | 8 | 22 | 23 | -------------------------------------------------------------------------------- /tests/assets/input/select.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selection Test 5 | 6 | 7 | 24 | 68 | 69 | -------------------------------------------------------------------------------- /tests/assets/input/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Textarea test 5 | 6 | 7 | 8 | 9 |
10 |
Plain div
11 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /tests/assets/offscreenbuttons.html: -------------------------------------------------------------------------------- 1 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | -------------------------------------------------------------------------------- /tests/assets/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Playground 5 | 6 | 7 | 8 | 9 |
First div
10 |
11 | Second div 12 | Inner span 13 |
14 | 15 | -------------------------------------------------------------------------------- /tests/assets/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup 5 | 8 | 9 | 10 | I am a popup 11 | 12 | -------------------------------------------------------------------------------- /tests/assets/popup/window-open.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Popup test 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /tests/assets/shadow.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/assets/title.html: -------------------------------------------------------------------------------- 1 | Woof-Woof -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | import pytest_asyncio 4 | from playwright._impl._path_utils import get_file_dirname 5 | 6 | import botright 7 | 8 | from .server import Server, test_server 9 | from .utils import utils as utils_object 10 | 11 | _dirname = get_file_dirname() 12 | 13 | 14 | @pytest_asyncio.fixture 15 | def assetdir(): 16 | return _dirname / "assets" 17 | 18 | 19 | @pytest_asyncio.fixture 20 | def utils(): 21 | yield utils_object 22 | 23 | 24 | @pytest_asyncio.fixture(autouse=True, scope="session") 25 | def run_around_tests(): 26 | test_server.start() 27 | yield 28 | test_server.stop() 29 | 30 | 31 | @pytest_asyncio.fixture 32 | def server() -> Generator[Server, None, None]: 33 | yield test_server.server 34 | 35 | 36 | @pytest_asyncio.fixture 37 | async def botright_client(): 38 | botright_client = await botright.Botright(headless=True) 39 | yield botright_client 40 | await botright_client.close() 41 | 42 | 43 | @pytest_asyncio.fixture 44 | async def browser(botright_client, **launch_arguments): 45 | browser = await botright_client.new_browser(**launch_arguments) 46 | yield browser 47 | await browser.close() 48 | 49 | 50 | @pytest_asyncio.fixture 51 | async def page(browser): 52 | page = await browser.new_page() 53 | yield page 54 | await page.close() 55 | -------------------------------------------------------------------------------- /tests/dev_test.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import botright 4 | 5 | 6 | async def main(): 7 | botright_client = await botright.Botright(headless=False) 8 | browser = await botright_client.new_browser() 9 | 10 | single_url = ["https://abrahamjuliot.github.io/creepjs/", "https://nowsecure.nl/#relax"] 11 | # webrtc_urls = ["https://browserleaks.com/webrtc", "https://www.expressvpn.com/en/webrtc-leak-test", "https://www.vpnmentor.com/tools/ip-leak-test-vpns-tor/", 12 | # "https://surfshark.com/en/webrtc-leak-test", "https://hide.me/en/webrtc-leak-test", "https://www.hidemyass.com/en-en/webrtc-leak-test", "https://abrahamjuliot.github.io/creepjs/"] 13 | # all_urls = ["https://abrahamjuliot.github.io/creepjs/", "https://hmaker.github.io/selenium-detector/", "https://arh.antoinevastel.com/bots/", "https://antoinevastel.com/bots/datadome", 14 | # "https://datadome.co/bot-tester/", "https://mihneamanolache.github.io/simple-sw-test/", "https://bot.sannysoft.com/", "https://bot.incolumitas.com/", 15 | # "https://www.whatismybrowser.com/detect/client-hints/", "https://nopecha.com/demo/recaptcha#v3", "https://iphey.com/", "https://www.browserscan.net/", "https://pixelscan.net", 16 | # "https://fingerprint.com/products/bot-detection/", "https://nowsecure.nl/#relax"] 17 | # 18 | for url in single_url: 19 | page = await browser.new_page() 20 | await page.goto(url) 21 | 22 | # await page.goto("https://www.aircanada.com/aeroplan/redeem/availability/outbound?org0=JFK&dest0=LHR&departureDate0=2024-01-01&lang=en-CA") 23 | 24 | await page.wait_for_timeout(900000) 25 | 26 | await botright_client.close() 27 | 28 | 29 | if __name__ == "__main__": 30 | asyncio.run(main()) 31 | -------------------------------------------------------------------------------- /tests/playwright_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vinyzu/Botright/8b66fa0d8395b369252b32c327434c29837f7797/tests/playwright_tests/__init__.py -------------------------------------------------------------------------------- /tests/playwright_tests/test_browsercontext.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | import sys 4 | 5 | import pytest 6 | from playwright.async_api import Error 7 | 8 | from botright.extended_typing import Page 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_pages_should_return_all_of_the_pages(browser): 13 | page = await browser.new_page() 14 | second = await browser.new_page() 15 | all_pages = browser.pages 16 | assert len(all_pages) == 2 17 | assert page in all_pages 18 | assert second in all_pages 19 | 20 | 21 | @pytest.mark.asyncio 22 | async def test_pages_should_close_all_belonging_pages_once_closing_context(browser): 23 | await browser.new_page() 24 | assert len(browser.pages) == 1 25 | await browser.close() 26 | assert browser.pages == [] 27 | 28 | 29 | @pytest.mark.asyncio 30 | async def test_expose_binding_should_work(browser): 31 | binding_source = [] 32 | 33 | def binding(source, a, b): 34 | binding_source.append(source) 35 | return a + b 36 | 37 | await browser.expose_binding("add", lambda source, a, b: binding(source, a, b)) 38 | 39 | page = await browser.new_page() 40 | result = await page.evaluate("add(5, 6)") 41 | assert binding_source[0]["context"] == browser 42 | assert binding_source[0]["page"] == page 43 | assert binding_source[0]["frame"] == page.main_frame 44 | assert result == 11 45 | 46 | 47 | @pytest.mark.asyncio 48 | async def test_expose_function_should_work(browser): 49 | await browser.expose_function("add", lambda a, b: a + b) 50 | page = await browser.new_page() 51 | await page.expose_function("mul", lambda a, b: a * b) 52 | await browser.expose_function("sub", lambda a, b: a - b) 53 | result = await page.evaluate( 54 | """async function() { 55 | return { mul: await mul(9, 4), add: await add(9, 4), sub: await sub(9, 4) } 56 | }""" 57 | ) 58 | 59 | assert result == {"mul": 36, "add": 13, "sub": 5} 60 | 61 | 62 | @pytest.mark.asyncio 63 | async def test_expose_function_should_throw_for_duplicate_registrations(browser): 64 | await browser.expose_function("foo", lambda: None) 65 | await browser.expose_function("bar", lambda: None) 66 | with pytest.raises(Error) as exc_info: 67 | await browser.expose_function("foo", lambda: None) 68 | assert exc_info.value.message == 'Function "foo" has been already registered' 69 | page = await browser.new_page() 70 | with pytest.raises(Error) as exc_info: 71 | await page.expose_function("foo", lambda: None) 72 | assert exc_info.value.message == 'Function "foo" has been already registered in the browser context' 73 | await page.expose_function("baz", lambda: None) 74 | with pytest.raises(Error) as exc_info: 75 | await browser.expose_function("baz", lambda: None) 76 | assert exc_info.value.message == 'Function "baz" has been already registered in one of the pages' 77 | 78 | 79 | @pytest.mark.asyncio 80 | async def test_expose_function_should_be_callable_from_inside_add_init_script(browser): 81 | args = [] 82 | await browser.expose_function("woof", lambda arg: args.append(arg)) 83 | await browser.add_init_script("woof('context')") 84 | page = await browser.new_page() 85 | await page.evaluate("undefined") 86 | assert args == ["context"] 87 | args = [] 88 | await page.add_init_script("woof('page')") 89 | await page.reload() 90 | assert args == ["context", "page"] 91 | 92 | 93 | @pytest.mark.asyncio 94 | async def test_expose_bindinghandle_should_work(browser): 95 | targets = [] 96 | 97 | def logme(t): 98 | targets.append(t) 99 | return 17 100 | 101 | page = await browser.new_page() 102 | await page.expose_binding("logme", lambda source, t: logme(t), handle=True) 103 | result = await page.evaluate("logme({ foo: 42 })") 104 | assert (await targets[0].evaluate("x => x.foo")) == 42 105 | assert result == 17 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_route_should_intercept(browser, server): 110 | intercepted = [] 111 | 112 | def handle(route, request): 113 | intercepted.append(True) 114 | assert "empty.html" in request.url 115 | assert request.headers["user-agent"] 116 | assert request.method == "GET" 117 | assert request.post_data is None 118 | assert request.is_navigation_request 119 | assert request.resource_type == "document" 120 | assert request.frame == page.main_frame 121 | assert request.frame.url == "about:blank" 122 | asyncio.create_task(route.continue_()) 123 | 124 | await browser.route("**/empty.html", lambda route, request: handle(route, request)) 125 | page = await browser.new_page() 126 | response = await page.goto(server.EMPTY_PAGE) 127 | assert response.ok 128 | assert intercepted == [True] 129 | await browser.close() 130 | 131 | 132 | @pytest.mark.asyncio 133 | async def test_route_should_unroute(browser, server): 134 | page = await browser.new_page() 135 | 136 | intercepted = [] 137 | 138 | def handler(route, request, ordinal): 139 | intercepted.append(ordinal) 140 | asyncio.create_task(route.continue_()) 141 | 142 | await browser.route("**/*", lambda route, request: handler(route, request, 1)) 143 | await browser.route("**/empty.html", lambda route, request: handler(route, request, 2)) 144 | await browser.route("**/empty.html", lambda route, request: handler(route, request, 3)) 145 | 146 | def handler4(route, request): 147 | handler(route, request, 4) 148 | 149 | await browser.route(re.compile("empty.html"), handler4) 150 | 151 | await page.goto(server.EMPTY_PAGE) 152 | assert intercepted == [4] 153 | 154 | intercepted = [] 155 | await browser.unroute(re.compile("empty.html"), handler4) 156 | await page.goto(server.EMPTY_PAGE) 157 | assert intercepted == [3] 158 | 159 | intercepted = [] 160 | await browser.unroute("**/empty.html") 161 | await page.goto(server.EMPTY_PAGE) 162 | assert intercepted == [1] 163 | 164 | 165 | @pytest.mark.asyncio 166 | async def test_route_should_yield_to_page_route(browser, server): 167 | await browser.route( 168 | "**/empty.html", 169 | lambda route, request: asyncio.create_task(route.fulfill(status=200, body="context")), 170 | ) 171 | 172 | page = await browser.new_page() 173 | await page.route( 174 | "**/empty.html", 175 | lambda route, request: asyncio.create_task(route.fulfill(status=200, body="page")), 176 | ) 177 | 178 | response = await page.goto(server.EMPTY_PAGE) 179 | assert response.ok 180 | assert await response.text() == "page" 181 | 182 | 183 | @pytest.mark.asyncio 184 | async def test_route_should_fall_back_to_context_route(browser, server): 185 | await browser.route( 186 | "**/empty.html", 187 | lambda route, request: asyncio.create_task(route.fulfill(status=200, body="context")), 188 | ) 189 | 190 | page = await browser.new_page() 191 | await page.route( 192 | "**/non-empty.html", 193 | lambda route, request: asyncio.create_task(route.fulfill(status=200, body="page")), 194 | ) 195 | 196 | response = await page.goto(server.EMPTY_PAGE) 197 | assert response.ok 198 | assert await response.text() == "context" 199 | 200 | 201 | @pytest.mark.asyncio 202 | async def test_offline_should_emulate_navigator_online(browser): 203 | page = await browser.new_page() 204 | assert await page.evaluate("window.navigator.onLine") 205 | await browser.set_offline(True) 206 | assert await page.evaluate("window.navigator.onLine") is False 207 | await browser.set_offline(False) 208 | assert await page.evaluate("window.navigator.onLine") 209 | 210 | 211 | @pytest.mark.asyncio 212 | async def test_page_event_should_have_url(browser, server): 213 | page = await browser.new_page() 214 | async with browser.expect_page() as other_page_info: 215 | await page.evaluate("url => window.open(url)", server.EMPTY_PAGE) 216 | other_page = await other_page_info.value 217 | assert other_page.url == server.EMPTY_PAGE 218 | 219 | 220 | @pytest.mark.asyncio 221 | async def test_page_event_should_have_url_after_domcontentloaded(browser, server): 222 | page = await browser.new_page() 223 | async with browser.expect_page() as other_page_info: 224 | await page.evaluate("url => window.open(url)", server.EMPTY_PAGE) 225 | other_page = await other_page_info.value 226 | await other_page.wait_for_load_state("domcontentloaded") 227 | assert other_page.url == server.EMPTY_PAGE 228 | 229 | 230 | @pytest.mark.asyncio 231 | async def test_page_event_should_have_about_blank_url_with_domcontentloaded(browser): 232 | page = await browser.new_page() 233 | async with browser.expect_page() as other_page_info: 234 | await page.evaluate("url => window.open(url)", "about:blank") 235 | other_page = await other_page_info.value 236 | await other_page.wait_for_load_state("domcontentloaded") 237 | assert other_page.url == "about:blank" 238 | 239 | 240 | @pytest.mark.asyncio 241 | async def test_page_event_should_have_about_blank_for_empty_url_with_domcontentloaded(browser): 242 | page = await browser.new_page() 243 | async with browser.expect_page() as other_page_info: 244 | await page.evaluate("window.open()") 245 | other_page = await other_page_info.value 246 | await other_page.wait_for_load_state("domcontentloaded") 247 | assert other_page.url == "about:blank" 248 | 249 | 250 | @pytest.mark.asyncio 251 | async def test_page_event_should_report_when_a_new_page_is_created_and_closed(browser, server): 252 | page = await browser.new_page() 253 | async with browser.expect_page() as page_info: 254 | await page.evaluate("url => window.open(url)", server.CROSS_PROCESS_PREFIX + "/empty.html") 255 | other_page = Page(await page_info.value, browser, browser.faker) 256 | 257 | # The url is about:blank in FF when 'page' event is fired. 258 | assert server.CROSS_PROCESS_PREFIX + "/empty.html" in other_page.url 259 | assert await other_page.evaluate("['Hello', 'world'].join(' ')") == "Hello world" 260 | assert await other_page.query_selector("body") 261 | 262 | all_pages = browser.pages 263 | assert page in all_pages 264 | assert other_page in browser.pages 265 | 266 | close_event_received = [] 267 | other_page.once("close", lambda: close_event_received.append(True)) 268 | await other_page.close() 269 | assert close_event_received == [True] 270 | 271 | all_pages = browser.pages 272 | assert page in all_pages 273 | assert other_page not in all_pages 274 | 275 | 276 | @pytest.mark.asyncio 277 | async def test_page_event_should_report_initialized_pages(browser): 278 | async with browser.expect_page() as page_info: 279 | await browser.new_page() 280 | new_page = await page_info.value 281 | assert new_page.url == "about:blank" 282 | 283 | async with browser.expect_page() as popup_info: 284 | await new_page.evaluate("window.open('about:blank')") 285 | popup = await popup_info.value 286 | assert popup.url == "about:blank" 287 | 288 | 289 | @pytest.mark.asyncio 290 | async def test_page_event_should_have_an_opener(browser, server): 291 | page = await browser.new_page() 292 | await page.goto(server.EMPTY_PAGE) 293 | async with browser.expect_page() as page_info: 294 | await page.goto(server.PREFIX + "/popup/window-open.html") 295 | popup = Page(await page_info.value, browser, browser.faker) 296 | assert popup.url == server.PREFIX + "/popup/popup.html" 297 | assert await popup.opener() == page 298 | assert await page.opener() is None 299 | 300 | 301 | @pytest.mark.asyncio 302 | async def test_page_event_should_fire_page_lifecycle_events(browser, server): 303 | events = [] 304 | 305 | def handle_page(page): 306 | events.append("CREATED: " + page.url) 307 | page.on("close", lambda: events.append("DESTROYED: " + page.url)) 308 | 309 | browser.on("page", handle_page) 310 | 311 | page = await browser.new_page() 312 | await page.goto(server.EMPTY_PAGE) 313 | await page.close() 314 | assert events == ["CREATED: about:blank", f"DESTROYED: {server.EMPTY_PAGE}"] 315 | 316 | 317 | @pytest.mark.asyncio 318 | async def test_page_event_should_work_with_shift_clicking(browser, server): 319 | # WebKit: Shift+Click does not open a new window. 320 | page = await browser.new_page() 321 | await page.goto(server.EMPTY_PAGE) 322 | await page.set_content('yo') 323 | async with browser.expect_page() as page_info: 324 | await page.click("a", modifiers=["Shift"]) 325 | popup = await page_info.value 326 | assert await popup.opener() is None 327 | 328 | 329 | @pytest.mark.asyncio 330 | async def test_page_event_should_work_with_ctrl_clicking(browser, server): 331 | # Firefox: reports an opener in this case. 332 | # WebKit: Ctrl+Click does not open a new tab. 333 | page = await browser.new_page() 334 | await page.goto(server.EMPTY_PAGE) 335 | await page.set_content('yo') 336 | async with browser.expect_page() as popup_info: 337 | await page.click("a", modifiers=["Meta" if (sys.platform == "darwin") else "Control"]) 338 | popup = await popup_info.value 339 | assert await popup.opener() is None 340 | -------------------------------------------------------------------------------- /tests/playwright_tests/test_browsercontext_events.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from botright.extended_typing import Page 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_console_event_should_work(page: Page) -> None: 10 | [message, _] = await asyncio.gather( 11 | page.context.wait_for_event("console"), 12 | page.evaluate("() => console.log('hello')"), 13 | ) 14 | assert message.text == "hello" 15 | assert Page(message.page, page.browser, page.faker) == page 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_console_event_should_work_in_popup(page: Page) -> None: 20 | [message, popup, _] = await asyncio.gather( 21 | page.context.wait_for_event("console"), 22 | page.wait_for_event("popup"), 23 | page.evaluate( 24 | """() => { 25 | const win = window.open(''); 26 | win.console.log('hello'); 27 | }""" 28 | ), 29 | ) 30 | assert message.text == "hello" 31 | assert message.page == popup 32 | 33 | 34 | @pytest.mark.asyncio 35 | async def test_console_event_should_work_in_popup_2(page: Page) -> None: 36 | [message, popup, _] = await asyncio.gather( 37 | page.context.wait_for_event("console", lambda msg: msg.type == "log"), 38 | page.context.wait_for_event("page"), 39 | page.evaluate( 40 | """async () => { 41 | const win = window.open('javascript:console.log("hello")'); 42 | await new Promise(f => setTimeout(f, 0)); 43 | win.close(); 44 | }""" 45 | ), 46 | ) 47 | assert message.text == "hello" 48 | assert message.page == popup 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_console_event_should_work_in_immediately_closed_popup(page: Page) -> None: 53 | [message, popup, _] = await asyncio.gather( 54 | page.context.wait_for_event("console"), 55 | page.wait_for_event("popup"), 56 | page.evaluate( 57 | """async () => { 58 | const win = window.open(); 59 | win.console.log('hello'); 60 | win.close(); 61 | }""" 62 | ), 63 | ) 64 | assert message.text == "hello" 65 | assert message.page == popup 66 | 67 | 68 | @pytest.mark.asyncio 69 | async def test_dialog_event_should_work1(page: Page) -> None: 70 | prompt_task = None 71 | 72 | async def open_dialog() -> None: 73 | nonlocal prompt_task 74 | prompt_task = asyncio.create_task(page.evaluate("() => prompt('hey?')")) 75 | 76 | [dialog1, dialog2, _] = await asyncio.gather( 77 | page.context.wait_for_event("dialog"), 78 | page.wait_for_event("dialog"), 79 | open_dialog(), 80 | ) 81 | assert dialog1 == dialog2 82 | assert dialog1.message == "hey?" 83 | assert Page(dialog1.page, page.browser, page.faker) == page 84 | await dialog1.accept("hello") 85 | assert await prompt_task == "hello" 86 | 87 | 88 | @pytest.mark.asyncio 89 | async def test_dialog_event_should_work_in_popup(page: Page) -> None: 90 | prompt_task = None 91 | 92 | async def open_dialog() -> None: 93 | nonlocal prompt_task 94 | prompt_task = asyncio.create_task(page.evaluate("() => window.open('').prompt('hey?')")) 95 | 96 | [dialog, popup, _] = await asyncio.gather( 97 | page.context.wait_for_event("dialog"), 98 | page.wait_for_event("popup"), 99 | open_dialog(), 100 | ) 101 | assert dialog.message == "hey?" 102 | assert dialog.page == popup 103 | await dialog.accept("hello") 104 | assert await prompt_task == "hello" 105 | 106 | 107 | @pytest.mark.asyncio 108 | async def test_dialog_event_should_work_in_popup_2(page: Page) -> None: 109 | promise = asyncio.create_task(page.evaluate("() => window.open('javascript:prompt(\"hey?\")')")) 110 | dialog = await page.context.wait_for_event("dialog") 111 | assert dialog.message == "hey?" 112 | assert dialog.page is None 113 | await dialog.accept("hello") 114 | await promise 115 | 116 | 117 | @pytest.mark.asyncio 118 | async def test_dialog_event_should_work_in_immdiately_closed_popup(page: Page) -> None: 119 | [message, popup, _] = await asyncio.gather( 120 | page.context.wait_for_event("console"), 121 | page.wait_for_event("popup"), 122 | page.evaluate( 123 | """() => { 124 | const win = window.open(); 125 | win.console.log('hello'); 126 | win.close(); 127 | }""" 128 | ), 129 | ) 130 | assert message.text == "hello" 131 | assert message.page == popup 132 | 133 | 134 | @pytest.mark.asyncio 135 | async def test_console_event_should_work_with_context_manager(page: Page) -> None: 136 | async with page.context.expect_console_message() as cm_info: 137 | await page.evaluate("() => console.log('hello')") 138 | message = await cm_info.value 139 | assert message.text == "hello" 140 | assert Page(message.page, page.browser, page.faker) == page 141 | 142 | 143 | @pytest.mark.asyncio 144 | async def test_page_error_event_should_work(page: Page) -> None: 145 | async with page.context.expect_event("weberror") as page_error_info: 146 | await page.set_content('') 147 | page_error = await page_error_info.value 148 | assert Page(page_error.page, page.browser, page.faker) == page 149 | assert "boom" in page_error.error.stack 150 | -------------------------------------------------------------------------------- /tests/playwright_tests/test_element_handle_wait_for_element_state.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | from playwright.async_api import Error 5 | 6 | 7 | async def give_it_a_chance_to_resolve(page): 8 | for i in range(5): 9 | await page.evaluate("() => new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)))") 10 | 11 | 12 | async def wait_for_state(div, state, done): 13 | await div.wait_for_element_state(state) 14 | done[0] = True 15 | 16 | 17 | async def wait_for_state_to_throw(div, state): 18 | with pytest.raises(Error) as exc_info: 19 | await div.wait_for_element_state(state) 20 | return exc_info 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_should_wait_for_visible(page): 25 | await page.set_content('
content
') 26 | div = await page.query_selector("div") 27 | done = [False] 28 | promise = asyncio.create_task(wait_for_state(div, "visible", done)) 29 | await give_it_a_chance_to_resolve(page) 30 | assert done[0] is False 31 | await div.evaluate('div => div.style.display = "block"') 32 | await promise 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_should_wait_for_already_visible(page): 37 | await page.set_content("
content
") 38 | div = await page.query_selector("div") 39 | await div.wait_for_element_state("visible") 40 | 41 | 42 | @pytest.mark.asyncio 43 | async def test_should_timeout_waiting_for_visible(page): 44 | await page.set_content('
content
') 45 | div = await page.query_selector("div") 46 | with pytest.raises(Error) as exc_info: 47 | await div.wait_for_element_state("visible", timeout=1000) 48 | assert "Timeout 1000ms exceeded" in exc_info.value.message 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_should_throw_waiting_for_visible_when_detached(page): 53 | await page.set_content('
content
') 54 | div = await page.query_selector("div") 55 | promise = asyncio.create_task(wait_for_state_to_throw(div, "visible")) 56 | await div.evaluate("div => div.remove()") 57 | exc_info = await promise 58 | assert "Element is not attached to the DOM" in exc_info.value.message 59 | 60 | 61 | @pytest.mark.asyncio 62 | async def test_should_wait_for_hidden(page): 63 | await page.set_content("
content
") 64 | div = await page.query_selector("div") 65 | done = [False] 66 | promise = asyncio.create_task(wait_for_state(div, "hidden", done)) 67 | await give_it_a_chance_to_resolve(page) 68 | assert done[0] is False 69 | await div.evaluate('div => div.style.display = "none"') 70 | await promise 71 | 72 | 73 | @pytest.mark.asyncio 74 | async def test_should_wait_for_already_hidden(page): 75 | await page.set_content("
") 76 | div = await page.query_selector("div") 77 | await div.wait_for_element_state("hidden") 78 | 79 | 80 | @pytest.mark.asyncio 81 | async def test_should_wait_for_hidden_when_detached(page): 82 | await page.set_content("
content
") 83 | div = await page.query_selector("div") 84 | done = [False] 85 | promise = asyncio.create_task(wait_for_state(div, "hidden", done)) 86 | await give_it_a_chance_to_resolve(page) 87 | assert done[0] is False 88 | await div.evaluate("div => div.remove()") 89 | await promise 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_should_wait_for_enabled_button(page): 94 | await page.set_content("") 95 | span = await page.query_selector("text=Target") 96 | done = [False] 97 | promise = asyncio.create_task(wait_for_state(span, "enabled", done)) 98 | await give_it_a_chance_to_resolve(page) 99 | assert done[0] is False 100 | await span.evaluate("span => span.parentElement.disabled = false") 101 | await promise 102 | 103 | 104 | @pytest.mark.asyncio 105 | async def test_should_throw_waiting_for_enabled_when_detached(page): 106 | await page.set_content("") 107 | button = await page.query_selector("button") 108 | promise = asyncio.create_task(wait_for_state_to_throw(button, "enabled")) 109 | await button.evaluate("button => button.remove()") 110 | exc_info = await promise 111 | assert "Element is not attached to the DOM" in exc_info.value.message 112 | 113 | 114 | @pytest.mark.asyncio 115 | async def test_should_wait_for_disabled_button(page): 116 | await page.set_content("") 117 | span = await page.query_selector("text=Target") 118 | done = [False] 119 | promise = asyncio.create_task(wait_for_state(span, "disabled", done)) 120 | await give_it_a_chance_to_resolve(page) 121 | assert done[0] is False 122 | await span.evaluate("span => span.parentElement.disabled = true") 123 | await promise 124 | 125 | 126 | @pytest.mark.asyncio 127 | async def test_should_wait_for_editable_input(page): 128 | await page.set_content("") 129 | input = await page.query_selector("input") 130 | done = [False] 131 | promise = asyncio.create_task(wait_for_state(input, "editable", done)) 132 | await give_it_a_chance_to_resolve(page) 133 | assert done[0] is False 134 | await input.evaluate("input => input.readOnly = false") 135 | await promise 136 | -------------------------------------------------------------------------------- /tests/playwright_tests/test_frames.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | from playwright.async_api import Error, Page 5 | 6 | 7 | @pytest.mark.asyncio 8 | async def test_evaluate_handle(page, server): 9 | await page.goto(server.EMPTY_PAGE) 10 | main_frame = page.main_frame 11 | assert main_frame.page == page 12 | window_handle = await main_frame.evaluate_handle("window") 13 | assert window_handle 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_frame_element(page, utils, server): 18 | await page.goto(server.EMPTY_PAGE) 19 | frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE) 20 | await utils.attach_frame(page, "frame2", server.EMPTY_PAGE) 21 | frame3 = await utils.attach_frame(page, "frame3", server.EMPTY_PAGE) 22 | frame1handle1 = await page.query_selector("#frame1") 23 | frame1handle2 = await frame1.frame_element() 24 | frame3handle1 = await page.query_selector("#frame3") 25 | frame3handle2 = await frame3.frame_element() 26 | assert await frame1handle1.evaluate("(a, b) => a === b", frame1handle2) 27 | assert await frame3handle1.evaluate("(a, b) => a === b", frame3handle2) 28 | assert await frame1handle1.evaluate("(a, b) => a === b", frame3handle1) is False 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_frame_element_with_content_frame(page, utils, server): 33 | await page.goto(server.EMPTY_PAGE) 34 | frame = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE) 35 | handle = await frame.frame_element() 36 | content_frame = await handle.content_frame() 37 | assert content_frame == frame 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_frame_element_throw_when_detached(page, utils, server): 42 | await page.goto(server.EMPTY_PAGE) 43 | frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE) 44 | await page.eval_on_selector("#frame1", "e => e.remove()") 45 | error = None 46 | try: 47 | await frame1.frame_element() 48 | except Error as e: 49 | error = e 50 | assert error.message == "Frame has been detached." 51 | 52 | 53 | @pytest.mark.asyncio 54 | async def test_evaluate_throw_for_detached_frames(page, utils, server): 55 | frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE) 56 | await utils.detach_frame(page, "frame1") 57 | error = None 58 | try: 59 | await frame1.evaluate("7 * 8") 60 | except Error as e: 61 | error = e 62 | assert "Frame was detached" in error.message 63 | 64 | 65 | @pytest.mark.asyncio 66 | async def test_evaluate_isolated_between_frames(page, utils, server): 67 | await page.goto(server.EMPTY_PAGE) 68 | await utils.attach_frame(page, "frame1", server.EMPTY_PAGE) 69 | assert len(page.frames) == 2 70 | [frame1, frame2] = page.frames 71 | assert frame1 != frame2 72 | 73 | await asyncio.gather(frame1.evaluate("window.a = 1"), frame2.evaluate("window.a = 2")) 74 | [a1, a2] = await asyncio.gather(frame1.evaluate("window.a"), frame2.evaluate("window.a")) 75 | assert a1 == 1 76 | assert a2 == 2 77 | 78 | 79 | @pytest.mark.asyncio 80 | async def test_should_handle_nested_frames(page, utils, server): 81 | await page.goto(server.PREFIX + "/frames/nested-frames.html") 82 | assert utils.dump_frames(page.main_frame) == [ 83 | "http://localhost:/frames/nested-frames.html", 84 | " http://localhost:/frames/frame.html (aframe)", 85 | " http://localhost:/frames/two-frames.html (2frames)", 86 | " http://localhost:/frames/frame.html (dos)", 87 | " http://localhost:/frames/frame.html (uno)", 88 | ] 89 | 90 | 91 | @pytest.mark.asyncio 92 | async def test_should_send_events_when_frames_are_manipulated_dynamically(page, utils, server): 93 | await page.goto(server.EMPTY_PAGE) 94 | # validate frameattached events 95 | attached_frames = [] 96 | page.on("frameattached", lambda frame: attached_frames.append(frame)) 97 | await utils.attach_frame(page, "frame1", "./assets/frame.html") 98 | assert len(attached_frames) == 1 99 | assert "/assets/frame.html" in attached_frames[0].url 100 | 101 | # validate framenavigated events 102 | navigated_frames = [] 103 | page.on("framenavigated", lambda frame: navigated_frames.append(frame)) 104 | await page.evaluate( 105 | """() => { 106 | frame = document.getElementById('frame1') 107 | frame.src = './empty.html' 108 | return new Promise(x => frame.onload = x) 109 | }""" 110 | ) 111 | 112 | assert len(navigated_frames) == 1 113 | assert navigated_frames[0].url == server.EMPTY_PAGE 114 | 115 | # validate framedetached events 116 | detached_frames = [] 117 | page.on("framedetached", lambda frame: detached_frames.append(frame)) 118 | await utils.detach_frame(page, "frame1") 119 | assert len(detached_frames) == 1 120 | assert detached_frames[0].is_detached() 121 | 122 | 123 | @pytest.mark.asyncio 124 | async def test_framenavigated_when_navigating_on_anchor_urls(page, server): 125 | await page.goto(server.EMPTY_PAGE) 126 | async with page.expect_event("framenavigated"): 127 | await page.goto(server.EMPTY_PAGE + "#foo") 128 | assert page.url == server.EMPTY_PAGE + "#foo" 129 | 130 | 131 | @pytest.mark.asyncio 132 | async def test_persist_main_frame_on_cross_process_navigation(page: Page, server): 133 | await page.goto(server.EMPTY_PAGE) 134 | main_frame = page.main_frame 135 | await page.goto(server.CROSS_PROCESS_PREFIX + "/empty.html") 136 | assert page.main_frame == main_frame 137 | 138 | 139 | @pytest.mark.asyncio 140 | async def test_should_not_send_attach_detach_events_for_main_frame(page, server): 141 | has_events = [] 142 | page.on("frameattached", lambda frame: has_events.append(True)) 143 | page.on("framedetached", lambda frame: has_events.append(True)) 144 | await page.goto(server.EMPTY_PAGE) 145 | assert has_events == [] 146 | 147 | 148 | @pytest.mark.asyncio 149 | async def test_detach_child_frames_on_navigation(page, server): 150 | attached_frames = [] 151 | detached_frames = [] 152 | navigated_frames = [] 153 | page.on("frameattached", lambda frame: attached_frames.append(frame)) 154 | page.on("framedetached", lambda frame: detached_frames.append(frame)) 155 | page.on("framenavigated", lambda frame: navigated_frames.append(frame)) 156 | await page.goto(server.PREFIX + "/frames/nested-frames.html") 157 | assert len(attached_frames) == 4 158 | assert len(detached_frames) == 0 159 | assert len(navigated_frames) == 5 160 | 161 | attached_frames = [] 162 | detached_frames = [] 163 | navigated_frames = [] 164 | await page.goto(server.EMPTY_PAGE) 165 | assert len(attached_frames) == 0 166 | assert len(detached_frames) == 4 167 | assert len(navigated_frames) == 1 168 | 169 | 170 | @pytest.mark.asyncio 171 | async def test_framesets(page, server): 172 | attached_frames = [] 173 | detached_frames = [] 174 | navigated_frames = [] 175 | page.on("frameattached", lambda frame: attached_frames.append(frame)) 176 | page.on("framedetached", lambda frame: detached_frames.append(frame)) 177 | page.on("framenavigated", lambda frame: navigated_frames.append(frame)) 178 | await page.goto(server.PREFIX + "/frames/frameset.html") 179 | await page.wait_for_timeout(100) # Small wait for loading 180 | assert len(attached_frames) == 4 181 | assert len(detached_frames) == 0 182 | assert len(navigated_frames) == 5 183 | 184 | attached_frames = [] 185 | detached_frames = [] 186 | navigated_frames = [] 187 | await page.goto(server.EMPTY_PAGE) 188 | await page.wait_for_timeout(100) # Small wait for loading 189 | assert len(attached_frames) == 0 190 | assert len(detached_frames) == 4 191 | assert len(navigated_frames) == 1 192 | 193 | 194 | @pytest.mark.asyncio 195 | async def test_frame_from_inside_shadow_dom(page, server): 196 | await page.goto(server.PREFIX + "/shadow.html") 197 | await page.evaluate( 198 | """async url => { 199 | frame = document.createElement('iframe'); 200 | frame.src = url; 201 | document.body.shadowRoot.appendChild(frame); 202 | await new Promise(x => frame.onload = x); 203 | }""", 204 | server.EMPTY_PAGE, 205 | ) 206 | assert len(page.frames) == 2 207 | assert page.frames[1].url == server.EMPTY_PAGE 208 | 209 | 210 | @pytest.mark.asyncio 211 | async def test_frame_name(page, utils, server): 212 | await utils.attach_frame(page, "theFrameId", server.EMPTY_PAGE) 213 | await page.evaluate( 214 | """url => { 215 | frame = document.createElement('iframe'); 216 | frame.name = 'theFrameName'; 217 | frame.src = url; 218 | document.body.appendChild(frame); 219 | return new Promise(x => frame.onload = x); 220 | }""", 221 | server.EMPTY_PAGE, 222 | ) 223 | assert page.frames[0].name == "" 224 | assert page.frames[1].name == "theFrameId" 225 | assert page.frames[2].name == "theFrameName" 226 | 227 | 228 | @pytest.mark.asyncio 229 | async def test_frame_parent(page, utils, server): 230 | await utils.attach_frame(page, "frame1", server.EMPTY_PAGE) 231 | await utils.attach_frame(page, "frame2", server.EMPTY_PAGE) 232 | assert page.frames[0].parent_frame is None 233 | assert page.frames[1].parent_frame == page.main_frame 234 | assert page.frames[2].parent_frame == page.main_frame 235 | 236 | 237 | @pytest.mark.asyncio 238 | async def test_should_report_different_frame_instance_when_frame_re_attaches(page, utils, server): 239 | frame1 = await utils.attach_frame(page, "frame1", server.EMPTY_PAGE) 240 | await page.evaluate( 241 | """() => { 242 | window.frame = document.querySelector('#frame1') 243 | window.frame.remove() 244 | }""" 245 | ) 246 | 247 | assert frame1.is_detached() 248 | async with page.expect_event("frameattached") as frame2_info: 249 | await page.evaluate("() => document.body.appendChild(window.frame)") 250 | 251 | frame2 = await frame2_info.value 252 | assert frame2.is_detached() is False 253 | assert frame1 != frame2 254 | 255 | 256 | @pytest.mark.asyncio 257 | async def test_strict_mode(page: Page, server): 258 | await page.goto(server.EMPTY_PAGE) 259 | await page.set_content( 260 | """ 261 | 262 | 263 | """ 264 | ) 265 | with pytest.raises(Error): 266 | await page.text_content("button", strict=True) 267 | with pytest.raises(Error): 268 | await page.query_selector("button", strict=True) 269 | -------------------------------------------------------------------------------- /tests/playwright_tests/test_jshandle.py: -------------------------------------------------------------------------------- 1 | import json 2 | import math 3 | from datetime import datetime 4 | 5 | import pytest 6 | from playwright.async_api import Page 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_jshandle_evaluate_work(page: Page): 11 | window_handle = await page.evaluate_handle("window") 12 | assert window_handle 13 | assert repr(window_handle) == f"" 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_jshandle_evaluate_accept_object_handle_as_argument(page): 18 | navigator_handle = await page.evaluate_handle("navigator") 19 | text = await page.evaluate("e => e.userAgent", navigator_handle) 20 | assert "Mozilla" in text 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_jshandle_evaluate_accept_handle_to_primitive_types(page): 25 | handle = await page.evaluate_handle("5") 26 | is_five = await page.evaluate("e => Object.is(e, 5)", handle) 27 | assert is_five 28 | 29 | 30 | @pytest.mark.asyncio 31 | async def test_jshandle_evaluate_accept_nested_handle(page): 32 | foo = await page.evaluate_handle('({ x: 1, y: "foo" })') 33 | result = await page.evaluate("({ foo }) => foo", {"foo": foo}) 34 | assert result == {"x": 1, "y": "foo"} 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_jshandle_evaluate_accept_nested_window_handle(page): 39 | foo = await page.evaluate_handle("window") 40 | result = await page.evaluate("({ foo }) => foo === window", {"foo": foo}) 41 | assert result 42 | 43 | 44 | @pytest.mark.asyncio 45 | async def test_jshandle_evaluate_accept_multiple_nested_handles(page): 46 | foo = await page.evaluate_handle('({ x: 1, y: "foo" })') 47 | bar = await page.evaluate_handle("5") 48 | baz = await page.evaluate_handle('["baz"]') 49 | result = await page.evaluate( 50 | "x => JSON.stringify(x)", 51 | {"a1": {"foo": foo}, "a2": {"bar": bar, "arr": [{"baz": baz}]}}, 52 | ) 53 | assert json.loads(result) == { 54 | "a1": {"foo": {"x": 1, "y": "foo"}}, 55 | "a2": {"bar": 5, "arr": [{"baz": ["baz"]}]}, 56 | } 57 | 58 | 59 | @pytest.mark.asyncio 60 | async def test_jshandle_evaluate_should_work_for_circular_objects(page): 61 | a = {"x": 1} 62 | a["y"] = a 63 | result = await page.evaluate("a => { a.y.x += 1; return a; }", a) 64 | assert result["x"] == 2 65 | assert result["y"]["x"] == 2 66 | assert result == result["y"] 67 | 68 | 69 | @pytest.mark.asyncio 70 | async def test_jshandle_evaluate_accept_same_nested_object_multiple_times(page): 71 | foo = {"x": 1} 72 | assert await page.evaluate("x => x", {"foo": foo, "bar": [foo], "baz": {"foo": foo}}) == {"foo": {"x": 1}, "bar": [{"x": 1}], "baz": {"foo": {"x": 1}}} 73 | 74 | 75 | @pytest.mark.asyncio 76 | async def test_jshandle_evaluate_accept_object_handle_to_unserializable_value(page): 77 | handle = await page.evaluate_handle("() => Infinity") 78 | assert await page.evaluate("e => Object.is(e, Infinity)", handle) 79 | 80 | 81 | @pytest.mark.asyncio 82 | async def test_jshandle_evaluate_pass_configurable_args(page): 83 | result = await page.evaluate( 84 | """arg => { 85 | if (arg.foo !== 42) 86 | throw new Error('Not a 42'); 87 | arg.foo = 17; 88 | if (arg.foo !== 17) 89 | throw new Error('Not 17'); 90 | delete arg.foo; 91 | if (arg.foo === 17) 92 | throw new Error('Still 17'); 93 | return arg; 94 | }""", 95 | {"foo": 42}, 96 | ) 97 | assert result == {} 98 | 99 | 100 | @pytest.mark.asyncio 101 | async def test_jshandle_properties_get_property(page): 102 | handle1 = await page.evaluate_handle( 103 | """() => ({ 104 | one: 1, 105 | two: 2, 106 | three: 3 107 | })""" 108 | ) 109 | handle2 = await handle1.get_property("two") 110 | assert await handle2.json_value() == 2 111 | 112 | 113 | @pytest.mark.asyncio 114 | async def test_jshandle_properties_work_with_undefined_null_and_empty(page): 115 | handle = await page.evaluate_handle( 116 | """() => ({ 117 | undefined: undefined, 118 | null: null, 119 | })""" 120 | ) 121 | undefined_handle = await handle.get_property("undefined") 122 | assert await undefined_handle.json_value() is None 123 | null_handle = await handle.get_property("null") 124 | assert await null_handle.json_value() is None 125 | empty_handle = await handle.get_property("empty") 126 | assert await empty_handle.json_value() is None 127 | 128 | 129 | @pytest.mark.asyncio 130 | async def test_jshandle_properties_work_with_unserializable_values(page): 131 | handle = await page.evaluate_handle( 132 | """() => ({ 133 | infinity: Infinity, 134 | negInfinity: -Infinity, 135 | nan: NaN, 136 | negZero: -0 137 | })""" 138 | ) 139 | infinity_handle = await handle.get_property("infinity") 140 | assert await infinity_handle.json_value() == float("inf") 141 | neg_infinity_handle = await handle.get_property("negInfinity") 142 | assert await neg_infinity_handle.json_value() == float("-inf") 143 | nan_handle = await handle.get_property("nan") 144 | assert math.isnan(await nan_handle.json_value()) is True 145 | neg_zero_handle = await handle.get_property("negZero") 146 | assert await neg_zero_handle.json_value() == float("-0") 147 | 148 | 149 | @pytest.mark.asyncio 150 | async def test_jshandle_properties_get_properties(page): 151 | handle = await page.evaluate_handle('() => ({ foo: "bar" })') 152 | properties = await handle.get_properties() 153 | assert "foo" in properties 154 | foo = properties["foo"] 155 | assert await foo.json_value() == "bar" 156 | 157 | 158 | @pytest.mark.asyncio 159 | async def test_jshandle_properties_return_empty_map_for_non_objects(page): 160 | handle = await page.evaluate_handle("123") 161 | properties = await handle.get_properties() 162 | assert properties == {} 163 | 164 | 165 | @pytest.mark.asyncio 166 | async def test_jshandle_json_value_work(page): 167 | handle = await page.evaluate_handle('() => ({foo: "bar"})') 168 | json = await handle.json_value() 169 | assert json == {"foo": "bar"} 170 | 171 | 172 | @pytest.mark.asyncio 173 | async def test_jshandle_json_value_work_with_dates(page): 174 | handle = await page.evaluate_handle('() => new Date("2020-05-27T01:31:38.506Z")') 175 | json = await handle.json_value() 176 | assert json == datetime.fromisoformat("2020-05-27T01:31:38.506") 177 | 178 | 179 | @pytest.mark.asyncio 180 | async def test_jshandle_json_value_should_work_for_circular_object(page): 181 | handle = await page.evaluate_handle("const a = {}; a.b = a; a") 182 | a = {} 183 | a["b"] = a 184 | result = await handle.json_value() 185 | # Node test looks like the below, but assert isn't smart enough to handle this: 186 | # assert await handle.json_value() == a 187 | assert result["b"] == result 188 | 189 | 190 | @pytest.mark.asyncio 191 | async def test_jshandle_as_element_work(page): 192 | handle = await page.evaluate_handle("document.body") 193 | element = handle.as_element() 194 | assert element is not None 195 | 196 | 197 | @pytest.mark.asyncio 198 | async def test_jshandle_as_element_return_none_for_non_elements(page): 199 | handle = await page.evaluate_handle("2") 200 | element = handle.as_element() 201 | assert element is None 202 | 203 | 204 | @pytest.mark.asyncio 205 | async def test_jshandle_to_string_work_for_primitives(page): 206 | number_handle = await page.evaluate_handle("2") 207 | assert str(number_handle) == "2" 208 | string_handle = await page.evaluate_handle('"a"') 209 | assert str(string_handle) == "a" 210 | 211 | 212 | @pytest.mark.asyncio 213 | async def test_jshandle_to_string_work_for_complicated_objects(page): 214 | handle = await page.evaluate_handle("window") 215 | assert str(handle) == "Window" 216 | 217 | 218 | @pytest.mark.asyncio 219 | async def test_jshandle_to_string_work_for_promises(page): 220 | handle = await page.evaluate_handle("({b: Promise.resolve(123)})") 221 | b_handle = await handle.get_property("b") 222 | assert str(b_handle) == "Promise" 223 | -------------------------------------------------------------------------------- /tests/server.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import asyncio 3 | import contextlib 4 | import gzip 5 | import mimetypes 6 | import socket 7 | import threading 8 | from contextlib import closing 9 | from http import HTTPStatus 10 | from typing import Any, Callable, Dict, Generator, Generic, Optional, Set, Tuple, TypeVar, cast 11 | from urllib.parse import urlparse 12 | 13 | from playwright._impl._path_utils import get_file_dirname 14 | from twisted.internet import reactor as _twisted_reactor 15 | from twisted.internet.selectreactor import SelectReactor 16 | from twisted.web import http 17 | 18 | _dirname = get_file_dirname() 19 | reactor = cast(SelectReactor, _twisted_reactor) 20 | 21 | 22 | def find_free_port() -> int: 23 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s: 24 | s.bind(("", 0)) 25 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 26 | return s.getsockname()[1] 27 | 28 | 29 | T = TypeVar("T") 30 | 31 | 32 | class ExpectResponse(Generic[T]): 33 | def __init__(self) -> None: 34 | self._value: T 35 | 36 | @property 37 | def value(self) -> T: 38 | if not hasattr(self, "_value"): 39 | raise ValueError("no received value") 40 | return self._value 41 | 42 | 43 | class TestServerRequest(http.Request): 44 | __test__ = False 45 | channel: "TestServerHTTPChannel" 46 | post_body: Optional[bytes] = None 47 | 48 | def process(self) -> None: 49 | server = self.channel.factory.server_instance 50 | if self.content: 51 | self.post_body = self.content.read() 52 | self.content.seek(0, 0) 53 | else: 54 | self.post_body = None 55 | uri = urlparse(self.uri.decode()) 56 | path = uri.path 57 | 58 | request_subscriber = server.request_subscribers.get(path) 59 | if request_subscriber: 60 | request_subscriber._loop.call_soon_threadsafe(request_subscriber.set_result, self) 61 | server.request_subscribers.pop(path) 62 | 63 | if server.auth.get(path): 64 | authorization_header = self.requestHeaders.getRawHeaders("authorization") 65 | creds_correct = False 66 | if authorization_header: 67 | creds_correct = server.auth.get(path) == ( 68 | self.getUser().decode(), 69 | self.getPassword().decode(), 70 | ) 71 | if not creds_correct: 72 | self.setHeader(b"www-authenticate", 'Basic realm="Secure Area"') 73 | self.setResponseCode(HTTPStatus.UNAUTHORIZED) 74 | self.finish() 75 | return 76 | if server.csp.get(path): 77 | self.setHeader(b"Content-Security-Policy", server.csp[path]) 78 | if server.routes.get(path): 79 | server.routes[path](self) 80 | return 81 | file_content = None 82 | try: 83 | file_content = (server.static_path / path[1:]).read_bytes() 84 | content_type = mimetypes.guess_type(path)[0] 85 | if content_type and content_type.startswith("text/"): 86 | content_type += "; charset=utf-8" 87 | self.setHeader(b"Content-Type", content_type) 88 | self.setHeader(b"Cache-Control", "no-cache, no-store") 89 | if path in server.gzip_routes: 90 | self.setHeader("Content-Encoding", "gzip") 91 | self.write(gzip.compress(file_content)) 92 | else: 93 | self.setHeader(b"Content-Length", str(len(file_content))) 94 | self.write(file_content) 95 | self.setResponseCode(HTTPStatus.OK) 96 | except (FileNotFoundError, IsADirectoryError, PermissionError): 97 | self.setResponseCode(HTTPStatus.NOT_FOUND) 98 | self.finish() 99 | 100 | 101 | class TestServerHTTPChannel(http.HTTPChannel): 102 | factory: "TestServerFactory" 103 | requestFactory = TestServerRequest 104 | 105 | 106 | class TestServerFactory(http.HTTPFactory): 107 | server_instance: "Server" 108 | protocol = TestServerHTTPChannel 109 | 110 | 111 | class Server: 112 | protocol = "http" 113 | 114 | def __init__(self) -> None: 115 | self.PORT = find_free_port() 116 | self.EMPTY_PAGE = f"{self.protocol}://localhost:{self.PORT}/empty.html" 117 | self.PREFIX = f"{self.protocol}://localhost:{self.PORT}" 118 | self.CROSS_PROCESS_PREFIX = f"{self.protocol}://127.0.0.1:{self.PORT}" 119 | # On Windows, this list can be empty, reporting text/plain for scripts. 120 | mimetypes.add_type("text/html", ".html") 121 | mimetypes.add_type("text/css", ".css") 122 | mimetypes.add_type("application/javascript", ".js") 123 | mimetypes.add_type("image/png", ".png") 124 | mimetypes.add_type("font/woff2", ".woff2") 125 | 126 | def __repr__(self) -> str: 127 | return self.PREFIX 128 | 129 | @abc.abstractmethod 130 | def listen(self, factory: TestServerFactory) -> None: 131 | pass 132 | 133 | def start(self) -> None: 134 | request_subscribers: Dict[str, asyncio.Future] = {} 135 | auth: Dict[str, Tuple[str, str]] = {} 136 | csp: Dict[str, str] = {} 137 | routes: Dict[str, Callable[[TestServerRequest], Any]] = {} 138 | gzip_routes: Set[str] = set() 139 | self.request_subscribers = request_subscribers 140 | self.auth = auth 141 | self.csp = csp 142 | self.routes = routes 143 | self.gzip_routes = gzip_routes 144 | self.static_path = _dirname / "assets" 145 | factory = TestServerFactory() 146 | factory.server_instance = self 147 | self.listen(factory) 148 | 149 | async def wait_for_request(self, path: str) -> TestServerRequest: 150 | if path in self.request_subscribers: 151 | return await self.request_subscribers[path] 152 | future: asyncio.Future["TestServerRequest"] = asyncio.Future() 153 | self.request_subscribers[path] = future 154 | return await future 155 | 156 | @contextlib.contextmanager 157 | def expect_request(self, path: str) -> Generator[ExpectResponse[TestServerRequest], None, None]: 158 | future = asyncio.create_task(self.wait_for_request(path)) 159 | 160 | cb_wrapper: ExpectResponse[TestServerRequest] = ExpectResponse() 161 | 162 | def done_cb(task: asyncio.Task) -> None: 163 | cb_wrapper._value = future.result() 164 | 165 | future.add_done_callback(done_cb) 166 | yield cb_wrapper 167 | 168 | def set_auth(self, path: str, username: str, password: str) -> None: 169 | self.auth[path] = (username, password) 170 | 171 | def set_csp(self, path: str, value: str) -> None: 172 | self.csp[path] = value 173 | 174 | def reset(self) -> None: 175 | if self.request_subscribers: 176 | self.request_subscribers.clear() 177 | self.auth.clear() 178 | self.csp.clear() 179 | self.gzip_routes.clear() 180 | self.routes.clear() 181 | 182 | def set_route(self, path: str, callback: Callable[[TestServerRequest], Any]) -> None: 183 | self.routes[path] = callback 184 | 185 | def enable_gzip(self, path: str) -> None: 186 | self.gzip_routes.add(path) 187 | 188 | def set_redirect(self, from_: str, to: str) -> None: 189 | def handle_redirect(request: http.Request) -> None: 190 | request.setResponseCode(HTTPStatus.FOUND) 191 | request.setHeader("location", to) 192 | request.finish() 193 | 194 | self.set_route(from_, handle_redirect) 195 | 196 | 197 | class HTTPServer(Server): 198 | def listen(self, factory: http.HTTPFactory) -> None: 199 | reactor.listenTCP(self.PORT, factory, interface="127.0.0.1") 200 | try: 201 | reactor.listenTCP(self.PORT, factory, interface="::1") 202 | except Exception: 203 | pass 204 | 205 | 206 | class TestServer: 207 | def __init__(self) -> None: 208 | self.server = HTTPServer() 209 | 210 | def start(self) -> None: 211 | self.server.start() 212 | self.thread = threading.Thread(target=lambda: reactor.run(installSignalHandlers=False)) 213 | self.thread.start() 214 | 215 | def stop(self) -> None: 216 | reactor.stop() 217 | self.thread.join() 218 | 219 | def reset(self) -> None: 220 | self.server.reset() 221 | 222 | 223 | test_server = TestServer() 224 | -------------------------------------------------------------------------------- /tests/test_geetestv3.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | @pytest.mark.skip(reason="Geetest currently not supported") # @pytest.mark.xfail 6 | async def test_intelligent_captcha(page): 7 | await page.goto("https://www.geetest.com/en/demo") 8 | 9 | result = await page.solve_geetest() 10 | assert result 11 | 12 | 13 | @pytest.mark.asyncio 14 | @pytest.mark.skip(reason="Geetest currently not supported") # @pytest.mark.xfail 15 | async def test_slider_captcha(page): 16 | await page.goto("https://www.geetest.com/en/demo") 17 | 18 | checkbox = page.locator('[class="tab-item tab-item-1"]') 19 | await checkbox.click() 20 | 21 | await page.wait_for_timeout(2000) 22 | 23 | result = await page.solve_geetest() 24 | assert result 25 | 26 | 27 | @pytest.mark.asyncio 28 | @pytest.mark.skip(reason="Geetest currently not supported") # @pytest.mark.xfail 29 | async def test_icon_captcha(page): 30 | await page.goto("https://www.geetest.com/en/demo") 31 | 32 | checkbox = page.locator('[class="tab-item tab-item-2"]') 33 | await checkbox.click() 34 | 35 | await page.wait_for_timeout(2000) 36 | 37 | result = await page.solve_geetest() 38 | assert result 39 | -------------------------------------------------------------------------------- /tests/test_geetestv4.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | @pytest.mark.skip(reason="Geetest currently not supported") # @pytest.mark.xfail 6 | async def test_intelligent_captcha(page): 7 | await page.goto("https://www.geetest.com/en/adaptive-captcha-demo") 8 | 9 | result = await page.solve_geetest() 10 | assert result 11 | 12 | 13 | @pytest.mark.asyncio 14 | @pytest.mark.skip(reason="Geetest currently not supported") # @pytest.mark.xfail 15 | async def test_slider_captcha(page): 16 | await page.goto("https://www.geetest.com/en/adaptive-captcha-demo") 17 | 18 | checkbox = page.locator('[class="tab-item tab-item-1"]') 19 | await checkbox.click() 20 | 21 | await page.wait_for_timeout(2000) 22 | 23 | result = await page.solve_geetest() 24 | assert result 25 | 26 | 27 | @pytest.mark.asyncio 28 | @pytest.mark.skip(reason="Geetest currently not supported") # @pytest.mark.xfail 29 | async def test_icon_captcha(page): 30 | await page.goto("https://www.geetest.com/en/adaptive-captcha-demo") 31 | 32 | checkbox = page.locator('[class="tab-item tab-item-2"]') 33 | await checkbox.click() 34 | 35 | await page.wait_for_timeout(2000) 36 | 37 | result = await page.solve_geetest(mode="canny") 38 | assert result 39 | 40 | 41 | @pytest.mark.asyncio 42 | @pytest.mark.skip(reason="Geetest currently not supported") # @pytest.mark.xfail 43 | async def test_gobang_captcha(page): 44 | await page.goto("https://www.geetest.com/en/adaptive-captcha-demo") 45 | 46 | checkbox = page.locator('[class="tab-item tab-item-3"]') 47 | await checkbox.click() 48 | 49 | await page.wait_for_timeout(2000) 50 | 51 | result = await page.solve_geetest() 52 | assert result 53 | 54 | 55 | @pytest.mark.asyncio 56 | @pytest.mark.skip(reason="Geetest currently not supported") # @pytest.mark.xfail 57 | async def test_iconcrush_captcha(page): 58 | await page.goto("https://www.geetest.com/en/adaptive-captcha-demo") 59 | 60 | checkbox = page.locator('[class="tab-item tab-item-4"]') 61 | await checkbox.click() 62 | 63 | await page.wait_for_timeout(2000) 64 | 65 | result = await page.solve_geetest() 66 | assert result 67 | -------------------------------------------------------------------------------- /tests/test_hcaptcha.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.asyncio 5 | @pytest.mark.xfail 6 | async def test_solve_hcaptcha(page): 7 | await page.goto("https://accounts.hcaptcha.com/demo?sitekey=00000000-0000-0000-0000-000000000000") 8 | 9 | result = await page.solve_hcaptcha() 10 | assert result 11 | 12 | 13 | @pytest.mark.asyncio 14 | @pytest.mark.xfail 15 | async def test_get_hcaptcha(page): 16 | result = await page.get_hcaptcha() 17 | assert result 18 | -------------------------------------------------------------------------------- /tests/test_recaptcha.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_recaptcha(page): 8 | await page.goto("https://www.google.com/recaptcha/api2/demo") 9 | 10 | with suppress(RecursionError): 11 | result = await page.solve_recaptcha() 12 | assert result 13 | -------------------------------------------------------------------------------- /tests/test_webrtc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from botright.extended_typing import Page 4 | 5 | 6 | @pytest.mark.asyncio 7 | async def test_browserleaks(page: Page): 8 | await page.goto("https://browserleaks.com/webrtc") 9 | await page.wait_for_timeout(2000) 10 | 11 | leak_check = await page.locator("[id='rtc-leak']").text_content() 12 | assert "No Leak" in leak_check 13 | 14 | 15 | @pytest.mark.asyncio 16 | async def test_expressvpn(page: Page): 17 | await page.goto("https://www.expressvpn.com/en/webrtc-leak-test") 18 | await page.wait_for_timeout(2000) 19 | 20 | leak_checks = page.locator("[class='title green']") 21 | leak_check0 = await leak_checks.nth(0).is_visible() 22 | leak_check1 = await leak_checks.nth(1).is_visible() 23 | print(leak_check0, leak_check1) 24 | assert leak_check0 or leak_check1 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_surfshark(page: Page): 29 | await page.goto("https://surfshark.com/en/webrtc-leak-test") 30 | await page.wait_for_timeout(2000) 31 | 32 | leak_check = await page.locator("[data-test='webrtc-leak-test-checker']").inner_html() 33 | assert "No WebRTC leaks detected" in leak_check 34 | 35 | 36 | @pytest.mark.asyncio 37 | async def test_hide_me(page: Page): 38 | await page.goto("https://hide.me/en/webrtc-leak-test") 39 | await page.wait_for_timeout(2000) 40 | 41 | leak_check = await page.locator("[data-type='webrtc']").inner_html() 42 | assert "WebRTC is disabled" in leak_check 43 | 44 | 45 | @pytest.mark.skip(reason="Hidemyass is maight maight detect wrong / detect js instead of webrtc") 46 | @pytest.mark.asyncio 47 | async def test_hidemyass(page: Page): 48 | await page.goto("https://www.hidemyass.com/webrtc-leak-test?nogeoip") 49 | await page.wait_for_timeout(2000) 50 | 51 | leak_check = page.locator("[class *= 'header-message protected']") 52 | assert await leak_check.is_visible() 53 | 54 | 55 | @pytest.mark.asyncio 56 | async def test_ovpn(page: Page): 57 | await page.goto("https://www.ovpn.com/en/webrtc-leak-test") 58 | await page.wait_for_timeout(2000) 59 | 60 | leak_check = await page.locator("[class='protected-wrapper protected']").text_content() 61 | assert leak_check == "No IP leak" 62 | 63 | 64 | @pytest.mark.asyncio 65 | async def test_browserscan(page: Page): 66 | await page.goto("https://www.browserscan.net/webrtc") 67 | await page.wait_for_timeout(2000) 68 | 69 | leak_checks = page.locator("[class *= '_webrtc__item__ip']") 70 | for leak_check in await leak_checks.all_text_contents(): 71 | assert leak_check.lower() == "disabled" 72 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import List, cast 3 | 4 | from undetected_playwright.async_api import Error, Selectors, ViewportSize 5 | 6 | from botright.extended_typing import ElementHandle, Frame, Page 7 | 8 | 9 | class Utils: 10 | async def attach_frame(self, page: Page, frame_id: str, url: str): 11 | handle = await page.evaluate_handle( 12 | """async ({ frame_id, url }) => { 13 | const frame = document.createElement('iframe'); 14 | frame.src = url; 15 | frame.id = frame_id; 16 | document.body.appendChild(frame); 17 | await new Promise(x => frame.onload = x); 18 | return frame; 19 | }""", 20 | {"frame_id": frame_id, "url": url}, 21 | ) 22 | return await cast(ElementHandle, handle.as_element()).content_frame() 23 | 24 | async def detach_frame(self, page: Page, frame_id: str): 25 | await page.evaluate("frame_id => document.getElementById(frame_id).remove()", frame_id) 26 | 27 | def dump_frames(self, frame: Frame, indentation: str = "") -> List[str]: 28 | indentation = indentation or "" 29 | description = re.sub(r":\d+/", ":/", frame.url) 30 | if frame.name: 31 | description += " (" + frame.name + ")" 32 | result = [indentation + description] 33 | sorted_frames = sorted(frame.child_frames, key=lambda frame: frame.url + frame.name) 34 | for child in sorted_frames: 35 | result = result + utils.dump_frames(child, " " + indentation) 36 | return result 37 | 38 | async def verify_viewport(self, page: Page, width: int, height: int): 39 | assert cast(ViewportSize, page.viewport_size)["width"] == width 40 | assert cast(ViewportSize, page.viewport_size)["height"] == height 41 | assert await page.evaluate("window.innerWidth") == width 42 | assert await page.evaluate("window.innerHeight") == height 43 | 44 | async def register_selector_engine(self, selectors: Selectors, *args, **kwargs) -> None: 45 | try: 46 | await selectors.register(*args, **kwargs) 47 | except Error as exc: 48 | if "has been already registered" not in exc.message: 49 | raise exc 50 | 51 | 52 | utils = Utils() 53 | --------------------------------------------------------------------------------