├── .gitattributes ├── tracer ├── gui │ ├── static │ │ ├── assets │ │ │ ├── favicon.ico │ │ │ ├── tracer.webp │ │ │ └── tracer_social_preview.jpg │ │ ├── js │ │ │ └── search.js │ │ └── css │ │ │ └── main.css │ ├── __init__.py │ ├── templates │ │ ├── index.html │ │ └── base.html │ └── app.py ├── __init__.py ├── models │ ├── __init__.py │ ├── textanimation.py │ ├── category.py │ ├── result.py │ ├── parser.py │ ├── websitepool.py │ └── website.py ├── __main__.py ├── loader.py └── tracer.py ├── .editorconfig ├── data ├── logo.txt └── pool.json ├── .github └── ISSUE_TEMPLATE │ ├── false-username-detection.md │ ├── feature_request.md │ └── bug_report.md ├── pyproject.toml ├── tests ├── test_result.py ├── test_tracer.py ├── test_website.py └── test_websitepool.py ├── LICENSE ├── settings.conf ├── .gitignore ├── README.md └── requirements.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /tracer/gui/static/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr3st5an/tracer/HEAD/tracer/gui/static/assets/favicon.ico -------------------------------------------------------------------------------- /tracer/gui/static/assets/tracer.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr3st5an/tracer/HEAD/tracer/gui/static/assets/tracer.webp -------------------------------------------------------------------------------- /tracer/gui/static/assets/tracer_social_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chr3st5an/tracer/HEAD/tracer/gui/static/assets/tracer_social_preview.jpg -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | tab_width = 4 10 | 11 | [*.py] 12 | insert_final_newline = true 13 | 14 | [*.js] 15 | indent_size = 2 16 | insert_final_newline = true 17 | -------------------------------------------------------------------------------- /tracer/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tracer 3 | ~~~~~~ 4 | 5 | A simple username checker for social media websites 6 | """ 7 | 8 | __title__ = "Tracer" 9 | __author__ = "chr3st5an" 10 | __version__ = "1.0.2" 11 | __license__ = "MIT" 12 | 13 | from .models import * 14 | from .tracer import * 15 | from .loader import * 16 | -------------------------------------------------------------------------------- /data/logo.txt: -------------------------------------------------------------------------------- 1 | A tool created by @chr3st5an 2 | ___________ 3 | \__ ___/_______ _____ ____ ____ _______ 4 | | | \_ __ \\__ \ _/ ___\_/ __ \\_ __ \ 5 | | | | | \/ / __ \_\ \___\ ___/ | | \/ 6 | |____| |__| (____ / \___ >\___ >|__| 7 | \/ \/ \/ 8 | - Find them all -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/false-username-detection.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: False username detection 3 | about: Tracer doesn't detect a username on a specific website 4 | title: "[DETECTION ISSUE]" 5 | labels: false result 6 | assignees: chr3st5an 7 | 8 | --- 9 | 10 | **Which website is involved** 11 | The involved website 12 | 13 | **Did you use a VPN / Proxy** 14 | Software that obfuscated your IP address 15 | 16 | **Does the username contain non-ascii characters** 17 | Non-ascii characters are for example emojis 18 | 19 | **How long is the username** 20 | The amount of characters that the username contains 21 | 22 | **Did it work before** 23 | Did Tracer suddenly report a false result 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: chr3st5an 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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "tracer" 3 | version = "1.0.2" 4 | description = "A simple username checker for social media websites" 5 | authors = ["Christian <64144555+chr3st5an@users.noreply.github.com>"] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.8.1" 11 | aiohttp = "^3.8.4" 12 | colorama = "^0.4.6" 13 | aiofiles = "^23.1.0" 14 | pyvis = "^0.3.1" 15 | aiohttp-jinja2 = "^1.5.1" 16 | Jinja2 = "^3.1.2" 17 | 18 | [tool.poetry.group.dev.dependencies] 19 | mypy = "^1.0.1" 20 | isort = "^5.12.0" 21 | flake8 = "^6.0.0" 22 | 23 | [tool.isort] 24 | combine_star = true 25 | combine_as_imports = true 26 | from_first = true 27 | lines_after_imports = 2 28 | length_sort = true 29 | line_length = 120 30 | skip_gitignore = true 31 | use_parentheses = true 32 | 33 | [build-system] 34 | requires = ["poetry-core"] 35 | build-backend = "poetry.core.masonry.api" 36 | -------------------------------------------------------------------------------- /tests/test_result.py: -------------------------------------------------------------------------------- 1 | from tracer import Result, Website 2 | import unittest 3 | 4 | 5 | attributes = { 6 | "website": Website("localhost", "http://localhost/{}", 0), 7 | "status_code": 200, 8 | "successfully": True, 9 | "delay": 4.75, 10 | "host": "localhost", 11 | "url": "http://localhost/tracer" 12 | } 13 | 14 | 15 | class TestResult(unittest.TestCase): 16 | def testDataRepresentation(self): 17 | result = Result(**attributes) 18 | 19 | self.assertIs(result.website, attributes["website"]) 20 | self.assertEqual(result.status_code, attributes["status_code"]) 21 | self.assertEqual(result.successfully, attributes["successfully"]) 22 | self.assertEqual(result.delay, attributes["delay"]) 23 | self.assertEqual(result.host, attributes["host"]) 24 | self.assertEqual(result.url, attributes["url"]) 25 | 26 | 27 | if __name__ == "__main__": 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: chr3st5an 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /tests/test_tracer.py: -------------------------------------------------------------------------------- 1 | from tracer import Tracer 2 | import unittest 3 | 4 | 5 | data = [ 6 | { 7 | "domain": "example.org", 8 | "url": "https://example.org/{}", 9 | "category": 1 10 | }, 11 | { 12 | "domain": "example.com", 13 | "url": "http://example.com/user/{}", 14 | "category": 2 15 | }, 16 | { 17 | "domain": "example.net", 18 | "url": "http://example.net/user/{}", 19 | "category": 2 20 | } 21 | ] 22 | 23 | 24 | class TestTracer(unittest.TestCase): 25 | def testFiltering(self): 26 | tracer = Tracer("example", data, exclude=["example.org"]) 27 | 28 | self.assertEqual(len(tracer.pool), 2) 29 | self.assertEqual(tracer.pool.sites[0].domain, "example.com") 30 | 31 | tracer = Tracer("example", data, only=["example.org"]) 32 | 33 | self.assertEqual(len(tracer.pool), 1) 34 | self.assertEqual(tracer.pool.sites[0].domain, "example.org") 35 | 36 | 37 | if __name__ == "__main__": 38 | unittest.main() 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 chr3st5an 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/test_website.py: -------------------------------------------------------------------------------- 1 | from tracer import Category, Result, Website 2 | from asyncio import iscoroutine 3 | import unittest 4 | 5 | 6 | site = Website("www.example.com", "https://example.com/{}", Category.PROGRAMMING) 7 | 8 | 9 | class TestWebsite(unittest.TestCase): 10 | def testCategory(self): 11 | self.assertEqual(site.category.as_number, Category.PROGRAMMING) 12 | 13 | def testSetUsername(self): 14 | site.set_username("idontexistandyouknowthat") 15 | self.assertEqual(site.username, "idontexistandyouknowthat", "Username didn't get set") 16 | 17 | def testURL(self): 18 | site.set_username('a') 19 | self.assertEqual(site.url, "https://example.com/a", "The username should be in the URL") 20 | 21 | def testSetResult(self): 22 | result = Result(site, 200, True, 5, "", "") 23 | site.set_result(result) 24 | 25 | self.assertIs(site.result, result, "'set_result' doesn't work") 26 | 27 | def testRequestCoro(self): 28 | self.assertTrue(iscoroutine(site.send_request(object)), "'send_request' should return a coro") 29 | 30 | 31 | if __name__ == "__main__": 32 | unittest.main() 33 | -------------------------------------------------------------------------------- /tracer/gui/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from .app import app 26 | -------------------------------------------------------------------------------- /tests/test_websitepool.py: -------------------------------------------------------------------------------- 1 | from tracer import WebsitePool, Website 2 | import unittest 3 | 4 | 5 | pool = WebsitePool() 6 | 7 | 8 | class TestWebsitePool(unittest.TestCase): 9 | def testAdding(self): 10 | site = Website("www.example.com", "", 10) 11 | pool.add(site) 12 | 13 | self.assertIn(site, pool, "Added website is not in the pool") 14 | 15 | pool.remove(lambda w: w is site) 16 | 17 | self.assertNotIn(site, pool, "Removed website is still in the pool") 18 | 19 | def testIterable(self): 20 | try: 21 | for _ in pool: 22 | ... 23 | self.assertTrue(True) 24 | except: 25 | self.fail("Pool is not iterable") 26 | 27 | def testSetName(self): 28 | name = "TestName" 29 | pool.set_name(name) 30 | 31 | self.assertEqual(pool.name, name, "Setting the name for the pool didn't work") 32 | 33 | def testSetUsername(self): 34 | pool.add(Website("www.example.com", "https://example.com/{}", 0)) 35 | pool.set_username('tracerino') 36 | 37 | for website in pool: 38 | self.assertEqual(website.username, 'tracerino') 39 | 40 | pool.remove(lambda website: website.username == 'tracerino') 41 | 42 | 43 | if __name__ == '__main__': 44 | unittest.main() 45 | -------------------------------------------------------------------------------- /tracer/models/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from .textanimation import * 26 | from .websitepool import * 27 | from .category import * 28 | from .website import * 29 | from .result import * 30 | from .parser import * 31 | -------------------------------------------------------------------------------- /tracer/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | import sys 26 | 27 | 28 | if __name__ == "__main__": 29 | major, minor = sys.version_info[:2] 30 | 31 | if (major != 3) or not (6 < minor): 32 | print("This program requires python 3.6 < 3.x") 33 | exit(0) 34 | 35 | from tracer import Tracer 36 | 37 | Tracer.main() 38 | -------------------------------------------------------------------------------- /settings.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | # Set a timeout for each request made 3 | # 4 | # Default: none 5 | # Values: int, none 6 | # Flags: --timeout, -t 7 | # 8 | # Example: 9 | # timeout=5 10 | timeout=none 11 | 12 | # Wether to create a result folder or not. 13 | # This also includes all files that would be 14 | # saved there 15 | # 16 | # Default: on 17 | # Values: on, off 18 | create_file_output=on 19 | 20 | # Exclude specific domain names from the sites pool 21 | # 22 | # Default: '' 23 | # Values: example.com 24 | # Flags: --exclude, -e 25 | # 26 | # Multiple sites can be passed when seperating 27 | # them with a comma 28 | # 29 | # See ./src/data/pool.json for a list of sites 30 | # 31 | # exclude=example.com, example2.com, example3.com 32 | exclude= 33 | 34 | # Exclude specific domain names from the sites pool 35 | # if they belong to the given categories 36 | # 37 | # Default: '' 38 | # Values: category 39 | # Flags: --exclude-category, -E 40 | # 41 | # Multiple categories can be passed when seperating 42 | # them with a comma 43 | # 44 | # See ./src/models/category.py for more info about categories 45 | # 46 | # exclude_category=category1, category2, category3 47 | exclude_category= 48 | 49 | # Exclude sites from the sites pool if they 50 | # do not match the given domain names 51 | # 52 | # Default: '' 53 | # Values: example.com 54 | # Flags: --only, -o 55 | # 56 | # Multiple sites can be passed when seperating 57 | # them with a comma 58 | # 59 | # See ./src/data/pool.json for a list of sites 60 | # 61 | # only=example.com, example2.com, example3.com 62 | only= 63 | 64 | # Exclude sites from the sites pool 65 | # if they do not belong to the given categories 66 | # 67 | # Default: '' 68 | # Values: category 69 | # Flags: --only-category, -O 70 | # 71 | # Multiple categories can be passed when seperating 72 | # them with a comma 73 | # 74 | # See ./src/models/category.py for more info about categories 75 | # 76 | # Example: 77 | # only_category=category1, category2, category3 78 | only_category= 79 | 80 | # Open successfull results in the default 81 | # webbrowser 82 | # 83 | # Default: off 84 | # Values: on, off 85 | # Flags: --browse, -b 86 | browse=off 87 | 88 | # Print additional information while running 89 | # 90 | # Default: off 91 | # Values: on, off 92 | # Flags: --verbose, -v 93 | verbose=off 94 | 95 | # Print all results 96 | # 97 | # Default: off 98 | # Values: on, off 99 | # Flags: --all, -a 100 | all=off 101 | 102 | # Print logo on startup 103 | # 104 | # Default: on 105 | # Values: on, off 106 | print_logo=on 107 | 108 | # Retrieve and print your IP on program startup 109 | # 110 | # Default: off 111 | # Values: on, off 112 | # Flags: --ip-check 113 | ip_check=off 114 | 115 | # Set a timeout for the IP request. 116 | # ip_check must be enabled for this to take action 117 | # 118 | # Default: none 119 | # Values: int, none 120 | # 121 | # Example: 122 | # ip_timeout=5 123 | ip_timeout=none 124 | -------------------------------------------------------------------------------- /tracer/gui/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block scripts %} 3 | 4 | {% endblock %} 5 | {% block main %} 6 | 32 |
33 |
34 | {# Prepare result boxes #} 35 | {% for site in pool %} 36 | 45 | {% endfor %} 46 |
47 |
48 | {% endblock %} -------------------------------------------------------------------------------- /tracer/loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | __all__ = ("load_logo", "load_user_agent", "load_website_data") 26 | 27 | from typing import Any, Callable, Dict, List 28 | from functools import wraps 29 | import secrets 30 | import json 31 | import os 32 | 33 | from colorama import Fore 34 | 35 | 36 | DATA_FOLDER = "../data/" 37 | 38 | 39 | def dir_switcher(__func: Callable[..., Any], /) -> Callable[..., Any]: 40 | """Helper for switching working dirs 41 | 42 | Switch the working directory to the directory 43 | that contains this file and execute the given 44 | function. After execution, switch back to the 45 | original working directory. 46 | 47 | This is necessary for relative paths to work. 48 | """ 49 | 50 | @wraps(__func) 51 | def wrapper(*args: Any, **kwargs: Any) -> Any: 52 | origin = os.getcwd() 53 | os.chdir(os.path.dirname(__file__)) 54 | 55 | result = __func(*args, **kwargs) 56 | 57 | return os.chdir(origin) or result 58 | 59 | return wrapper 60 | 61 | 62 | @dir_switcher 63 | def load_logo() -> str: 64 | try: 65 | with open(f"{DATA_FOLDER}logo.txt") as file: 66 | return file.read() 67 | except FileNotFoundError: 68 | return "" 69 | 70 | 71 | @dir_switcher 72 | def load_user_agent() -> str: 73 | try: 74 | with open(f"{DATA_FOLDER}user_agents.json") as file: 75 | user_agents: List[str] = json.load(file) 76 | 77 | return secrets.choice(user_agents) 78 | except FileNotFoundError: 79 | return "Tracer/1.0 (+https://github.com/chr3st5an/tracer)" 80 | 81 | 82 | @dir_switcher 83 | def load_website_data() -> Dict[str, str]: 84 | try: 85 | with open(f"{DATA_FOLDER}pool.json") as file: 86 | return json.load(file) 87 | except FileNotFoundError: 88 | print(f"{Fore.RED}[ISSUE] Couldn't find the website data " 89 | f"file!{Fore.RESET}") 90 | exit(1) 91 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .vscode/ -------------------------------------------------------------------------------- /tracer/gui/static/js/search.js: -------------------------------------------------------------------------------- 1 | const searchEndpoint = '/api/start_search'; 2 | const domainPattern = 'https:\/\/(?:.*?\\.)*(\\w+?)\\.[a-z]{2,}\/?'; 3 | 4 | 5 | window.addEventListener('load', () => { 6 | let form = document.querySelector('#search-bar form'); 7 | 8 | form.addEventListener('submit', event => { 9 | let username = document.querySelector('#search-bar form input').value; 10 | 11 | // Prevent default form submission 12 | event.preventDefault(); 13 | 14 | if (username.length > 2) { 15 | return startSearch(username); 16 | } 17 | 18 | document.querySelector('#search-bar form input').focus(); 19 | }) 20 | }) 21 | 22 | /** 23 | * Invoked as soon as a search is started 24 | * 25 | * Mainly adjusts visual components and initializes 26 | * result boxes with their corresponding link 27 | */ 28 | const prepareResultsPage = username => { 29 | document.title = `Tracer | Results for ${username}`; 30 | 31 | let searchBar = document.getElementById('search-bar'); 32 | let input = searchBar.getElementsByTagName('input')[0]; 33 | let searchButton = searchBar.getElementsByTagName('button')[0]; 34 | let infoFooter = document.getElementById('info'); 35 | 36 | // Move the search bar up 37 | searchBar.style.marginTop = '1%'; 38 | 39 | infoFooter.style.top = 'inherit'; 40 | infoFooter.style.marginTop = '30px'; 41 | 42 | input.setAttribute('disabled', ''); 43 | input.style.color = '#6272a4'; 44 | input.style.borderBottomColor = input.style.color; 45 | 46 | searchButton.innerHTML = 'Back Home'; 47 | searchButton.type = 'button'; 48 | 49 | searchButton.onclick = () => { 50 | window.open('/', '_self'); 51 | } 52 | 53 | // Initialize the result boxes 54 | for (let box of document.getElementsByClassName('result-box')) { 55 | let url = box.getAttribute('id').replace('{}', username); 56 | let domain = url.match(domainPattern)[1]; // Get the domain of the pre-init url 57 | 58 | box.style.display = "inherit"; // Make the box visible 59 | box.setAttribute('id', domain); 60 | box.addEventListener('click', () => { 61 | window.open(url); 62 | }); 63 | } 64 | } 65 | 66 | /** 67 | * Start the search 68 | * 69 | * Send a POST request to the server with the specified 70 | * username 71 | */ 72 | const startSearch = username => { 73 | let request = new XMLHttpRequest(); 74 | request.open('POST', searchEndpoint, true); 75 | request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 76 | 77 | request.onreadystatechange = () => { 78 | if (request.readyState == 4 && request.status == 200) { 79 | prepareResultsPage(username); 80 | getResults(); 81 | } 82 | } 83 | 84 | request.send(`username=${username}`); 85 | } 86 | 87 | /** 88 | * Retrieve the results from the server 89 | * 90 | * This is done by recursively sending GET 91 | * requests to the server 92 | */ 93 | const getResults = () => { 94 | let request = new XMLHttpRequest(); 95 | request.open('GET', searchEndpoint, true); 96 | 97 | request.onreadystatechange = () => { 98 | if (request.readyState == 4) { 99 | if (request.status == 200 && request.responseText !== 'FINISHED') { 100 | let response; 101 | 102 | try { 103 | response = JSON.parse(request.responseText)['result']; 104 | } catch (e) { 105 | return getResults(); 106 | } 107 | 108 | let box = document.getElementById(response[1].match(domainPattern)[1]); 109 | 110 | box.style.color = (response[0] ? "#50fa7b" : "#ff5555"); 111 | box.style.borderColor = box.style.color; 112 | 113 | // Avoid too many boxes on small devices by removing failed ones 114 | if (!response[0] && window.innerWidth < 992) { 115 | document.getElementById('results').removeChild(box); 116 | } 117 | 118 | return getResults(); 119 | } else { 120 | return null; 121 | } 122 | } 123 | } 124 | 125 | request.send(); 126 | } 127 | -------------------------------------------------------------------------------- /tracer/gui/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 16 | 17 | {% block title %}Tracer | Search{% endblock %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {%- block scripts %}{% endblock %} 38 | 39 | 40 | 45 |
46 | {% block main %}{% endblock %} 47 |
48 | {% block footer %} 49 |
50 |
51 |
52 |
53 |

What is Tracer?

54 |

55 | Tracer is a username checker, which
56 | means that it allows you to check on
57 | which website a username is currently taken.
58 |

59 |
60 |
61 |
62 |
63 |

Some results are wrong!

64 |

65 | Yes, sometimes websites return unexpected
66 | responses which Tracer interprets wrong. 67 |

68 |
69 |
70 |
71 | 83 |
84 | {% endblock %} 85 | 86 | -------------------------------------------------------------------------------- /tracer/gui/static/css/main.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap'); 2 | 3 | 4 | ::placeholder { 5 | color: #f8f8f2; 6 | -webkit-touch-callout: none; 7 | -webkit-user-select: none; 8 | -khtml-user-select: none; 9 | -moz-user-select: none; 10 | -ms-user-select: none; 11 | user-select: none; 12 | } 13 | 14 | ::-ms-input-placeholder { 15 | color: #f8f8f2; 16 | -webkit-touch-callout: none; 17 | -webkit-user-select: none; 18 | -khtml-user-select: none; 19 | -moz-user-select: none; 20 | -ms-user-select: none; 21 | user-select: none; 22 | } 23 | 24 | html { 25 | min-height: 100%; 26 | } 27 | 28 | body { 29 | background-color: #282a36; 30 | overflow-x: hidden; 31 | overflow-y: auto; 32 | } 33 | 34 | button:active { 35 | transform: scale(0.8); 36 | } 37 | 38 | .noselect { 39 | user-select: none; 40 | -webkit-user-select: none; 41 | -moz-user-select: none; 42 | -ms-user-select: none; 43 | } 44 | 45 | #logo { 46 | text-align: center; 47 | } 48 | 49 | #logo img { 50 | width: 25%; 51 | height: auto; 52 | } 53 | 54 | #search-bar { 55 | margin-top: 13%; 56 | position: relative; 57 | text-align: center; 58 | transition: 1.1s all ease; 59 | } 60 | 61 | #question { 62 | color: #44475a; 63 | cursor: help; 64 | } 65 | 66 | #search-bar form { 67 | padding: 35px; 68 | } 69 | 70 | #search-bar input { 71 | background-color: transparent; 72 | border-left: none; 73 | border-top: none; 74 | border-right: none; 75 | border-bottom: 1px solid #ff79c6; 76 | color: #f8f8f2; 77 | font-family: 'Roboto Mono', monospace; 78 | font-size: 15px; 79 | padding: 10px; 80 | transition: 0.7s all ease; 81 | width: 20%; 82 | } 83 | 84 | #search-bar input:focus { 85 | outline: none; 86 | border-bottom: 1px solid #50fa7b; 87 | } 88 | 89 | #search-bar button { 90 | background-color: #ff79c6; 91 | border-radius: 25px; 92 | border-style: none; 93 | cursor: pointer; 94 | font-family: 'Roboto Mono', monospace; 95 | font-size: 15px; 96 | padding: 5px 20px; 97 | transition: 0.7s all ease; 98 | } 99 | 100 | #search-bar button:hover { 101 | background-color: #50fa7b; 102 | } 103 | 104 | #info { 105 | background-color: #44475a; 106 | color: #f8f8f2; 107 | font-family: 'Roboto Mono', monospace; 108 | left: 0; 109 | position: absolute; 110 | top: 100%; 111 | width: 100%; 112 | } 113 | 114 | #wrapper { 115 | display: grid; 116 | grid-template-columns: 1fr 1fr; 117 | margin-top: 10px; 118 | text-align: center; 119 | position: relative; 120 | } 121 | 122 | #footer { 123 | background-color: #282a36; 124 | border-top-left-radius: 35%; 125 | border-top-right-radius: 35%; 126 | bottom: 0; 127 | margin-top: 25px; 128 | text-align: center; 129 | padding: 10px; 130 | } 131 | 132 | #results { 133 | display: grid; 134 | grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr; 135 | } 136 | 137 | .result-box { 138 | display: none; 139 | border-style: dashed; 140 | border-color: #6272a4; 141 | color: #6272a4; 142 | cursor: pointer; 143 | margin: 10px; 144 | padding: 10px; 145 | text-align: center; 146 | transition: 0.7s all ease; 147 | } 148 | 149 | .result-box:hover { 150 | color: #bd93f9; 151 | border-color: #bd93f9; 152 | } 153 | 154 | .click-me { 155 | display: none; 156 | font-size: 10px; 157 | } 158 | 159 | @media only screen and (max-width: 992px) { 160 | #logo img { width: 50%; } 161 | #search-bar { margin-top: 25%; } 162 | #question { display: none; } 163 | #search-bar input { width: 90%; } 164 | #search-bar button { margin-top: 25px; } 165 | #info { display: none; } 166 | #results { grid-template-columns: 1fr 1fr 1fr 1fr; } 167 | .click-me { display: inherit; } 168 | } 169 | 170 | @media only screen and (max-width: 768px) { 171 | #search-bar { margin-top: 35%; } 172 | #logo img { width: 65%; } 173 | #results { grid-template-columns: 1fr 1fr 1fr; } 174 | } 175 | 176 | @media only screen and (max-width: 576px) { 177 | #search-bar { margin-top: 45%; } 178 | #search-bar input { width: 90%; } 179 | #logo img { width: 80%; } 180 | #results { grid-template-columns: 1fr 1fr; } 181 | .result-box { padding: 5px; margin: 5px; } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Maintainer](https://img.shields.io/badge/Maintainer-chr3st5an-purple?style=for-the-badge)](https://github.com/chr3st5an) 4 | [![Python](https://img.shields.io/badge/Python->=3.8.1-blue?style=for-the-badge&logo=python)](https://www.python.org/downloads/) 5 | [![Category](https://img.shields.io/badge/Category-OSINT-brightgreen?style=for-the-badge)](https://en.wikipedia.org/wiki/Open-source_intelligence) 6 | [![License](https://img.shields.io/badge/License-MIT-brightgreen?style=for-the-badge)](https://github.com/chr3st5an/tracer/blob/main/LICENSE) 7 | ![Version](https://img.shields.io/badge/Version-1.0.2-brightgreen?style=for-the-badge) 8 | [![Logo](https://i.imgur.com/HV5KtwO.png)](https://github.com/chr3st5an/tracer) 9 | 10 | ### Tracer 11 | 12 | Tracer detects on which website a username is currently in use! 13 | 14 | **[Official Repository](https://github.com/chr3st5an/tracer)** · **[Report Bug](https://github.com/chr3st5an/tracer/issues)** 15 | 16 |
17 | 18 |
19 | 20 | ## Features 21 | 22 | --- 23 | 24 | Tracer provides the following features: 25 | 26 | - 170+ sites that are checked 27 | 28 | - Filter websites based on their domain or category 29 | 30 | - Limit the pool of sites that will be checked 31 | 32 | - Browser version (GUI) 33 | 34 | - Save the result of each check in a report file 35 | 36 | - Open successful results in your browser 37 | 38 | - Customizability: 39 | 40 | - Use the included config file to change the behavior of Tracer 41 | 42 | - Easy to use 43 | 44 |
45 | 46 | [(Beam me up)](#tracer) 47 | 48 |
49 | 50 |
51 | 52 | ## Built With 53 | 54 | --- 55 | 56 | - ![aiohttp](https://img.shields.io/badge/aiohttp-black?style=for-the-badge&logo=aiohttp) 57 | 58 | - ![jinja](https://img.shields.io/badge/jinja-black?style=for-the-badge&logo=jinja) 59 | 60 |
61 | 62 | ## Getting Started 63 | 64 | --- 65 | 66 | ### Prerequisites 67 | 68 | For Tracer to work, you will need Python 3.7 or later and pip. You can download Python from the [official website](https://www.python.org/downloads/). Python ships with pip. Verify the versions: 69 | 70 | - python 71 | 72 | ```bash 73 | python -V 74 | ``` 75 | 76 | - pip 77 | 78 | ```bash 79 | pip -V # or "python -m pip -V" 80 | ``` 81 | 82 |
83 | 84 | [(Beam me up)](#tracer) 85 | 86 |
87 | 88 | ### Installation 89 | 90 | 1. Clone this repository 91 | 92 | ```bash 93 | git clone https://github.com/chr3st5an/tracer.git 94 | ``` 95 | 96 | > 🛈 If you do not have `git`, you can download this repository by clicking on `Code` > `Download ZIP`. Unzip the folder and open a terminal. 97 | 98 | 2. Navigate into the just downloaded folder 99 | 100 | ```bash 101 | cd tracer/ 102 | ``` 103 | 104 | 3. Install dependencies 105 | 106 | ```bash 107 | pip install -r ./requirements.txt 108 | ``` 109 | 110 |
111 | 112 | [(Beam me up)](#tracer) 113 | 114 |
115 | 116 |
117 | 118 | ## Usage 119 | 120 | --- 121 | 122 | After you installed all dependencies you are ready to run Tracer for the first time 🎉 To do so, open a terminal in the project's root folder and run the following command: 123 | 124 | ```bash 125 | python tracer [OPTIONS] username 126 | ``` 127 | 128 | Where `[OPTIONS]` are optional flags you can pass to Tracer to modify its behavior. More about options [later](#options). 129 | 130 |
131 | 132 | [(Beam me up)](#tracer) 133 | 134 |
135 | 136 |
137 | 138 | ## GUI 139 | 140 | --- 141 | 142 | Tracer also offers a GUI in form of a webapp. You can run the webapp by executing the following command: 143 | 144 | ```bash 145 | python tracer --web tracer 146 | ``` 147 | 148 | This will run the webapp on port 12345. Tracer should automatically open your browser and connect to the webapp. If not, open your browser manually and type `http://127.0.0.1:12345` into the search bar and hit enter. 149 | 150 | ![Browser](https://i.imgur.com/TRRtQMP.png) 151 | 152 |
153 | 154 | [(Beam me up)](#tracer) 155 | 156 |
157 | 158 |
159 | 160 | ## Options 161 | 162 | --- 163 | 164 | For a list of all available commands and options, use the `-h` flag or read the following section 165 | 166 | ```bash 167 | python tracer -h 168 | ``` 169 | 170 |
171 | 172 | Options 173 | 174 | - `-h`, `--help` *print a help message and exit* 175 | 176 | - `-t ` *set a timeout for requests* 177 | 178 | - `-e ` *exclude a domain* 179 | 180 | - `-o ` *only check this domain for the username* 181 | 182 | - `-O ` *only check sites that fall under this category for the username* 183 | 184 | - `-E ` *exclude all sites that fall under this category* 185 | 186 | - `-b` *open sites on which the username got found, in your default browser* 187 | 188 | - `-v` *print additional information while the program runs* 189 | 190 | - `-a` *print all websites* 191 | 192 | - `--web` *run a GUI in form of a local webapp* 193 | 194 | - `--ip-check` *retrieve your public IP address before starting the main program* 195 | 196 |
197 | 198 |
199 | 200 | [(Beam me up)](#tracer) 201 | 202 |
203 | 204 |
205 | 206 | ## License 207 | 208 | --- 209 | 210 | This project is licensed under the **MIT** license. For more information check out the project's license file. 211 | 212 |
213 | 214 | [(Beam me up)](#tracer) 215 | 216 |
217 | 218 |
219 | 220 |
221 | 222 | Buy Me A Coffee 223 | 224 |
225 | -------------------------------------------------------------------------------- /tracer/gui/app.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | import secrets 26 | import asyncio 27 | import os 28 | 29 | from aiohttp import ClientSession, DummyCookieJar, web 30 | from aiohttp.web import Request, RouteTableDef 31 | from jinja2 import FileSystemLoader 32 | import aiohttp_jinja2 33 | 34 | from tracer import ( 35 | load_website_data, 36 | load_user_agent, 37 | WebsitePool, 38 | Website, 39 | ) 40 | 41 | 42 | os.chdir(os.path.realpath(os.path.dirname(__file__))) 43 | 44 | app = web.Application() 45 | cookies = {} 46 | routes = RouteTableDef() 47 | 48 | aiohttp_jinja2.setup( 49 | app=app, 50 | enable_async=True, 51 | loader=FileSystemLoader("./templates") 52 | ) 53 | 54 | 55 | @routes.get("/") 56 | async def index(request: Request): 57 | """Represents the index page 58 | """ 59 | 60 | return await aiohttp_jinja2.render_template_async( 61 | template_name="index.html", 62 | request=request, 63 | context={ 64 | "host": request.host, 65 | "scheme": request.scheme, 66 | "pool": load_website_data() 67 | } 68 | ) 69 | 70 | 71 | @routes.post("/api/start_search") 72 | async def start_search(request: Request): 73 | """Endpoint for starting a username search 74 | 75 | Validate data and return a searchID in form of a cookie. 76 | The client can now send GET requests to this endpoint 77 | in order to retrieve the results of the search. 78 | """ 79 | 80 | username = (await request.post()).get("username", "") 81 | search_id = secrets.token_urlsafe(20) 82 | queue = asyncio.Queue() 83 | 84 | response = web.Response() 85 | response.set_cookie("search_id", search_id, max_age=300) 86 | 87 | cookies[search_id] = [queue, False] 88 | 89 | # Spawn background task 90 | asyncio.create_task( 91 | start_requests(username, queue, cookies[search_id]) 92 | ) 93 | 94 | return response 95 | 96 | 97 | @routes.get("/api/start_search") 98 | async def get_results(request: Request): 99 | """Endpoint for getting the results of a search 100 | 101 | Uses the searchID to identify the client and their 102 | results. 103 | """ 104 | 105 | search_id = request.cookies.get("search_id", "") 106 | 107 | if search_id not in cookies: 108 | return web.Response(status=400, text="Bad Request") 109 | 110 | while True: 111 | try: 112 | result = await asyncio.wait_for(cookies[search_id][0].get(), 1) 113 | 114 | return web.json_response({"result": result}) 115 | except asyncio.TimeoutError: 116 | if cookies[search_id][0].empty() and cookies[search_id][1]: 117 | del cookies[search_id] 118 | 119 | response = web.Response(text="Finished") 120 | response.del_cookie("search_id") 121 | 122 | return response 123 | 124 | 125 | async def start_requests( 126 | username: str, 127 | queue: asyncio.Queue, 128 | cookie: list 129 | ) -> None: 130 | """Send the necessary requests 131 | 132 | Put the incoming responses into the given queue. This 133 | coro is intended to be spawned as a background task. 134 | 135 | Parameters 136 | ---------- 137 | username : str 138 | Check if this username is taken 139 | queue : asyncio.Queue 140 | A queue to put the results into. Results are 141 | given as a list which follow this structure: 142 | `[username_exists : bool, url : str, delay : float]` 143 | cookie : list 144 | Cookie data assigned by `start_search` where the 145 | second element represents the state of this coro. 146 | As soon as this coro finishes, the coro changes 147 | the second element to `True` 148 | """ 149 | 150 | headers = {"User-Agent": load_user_agent()} 151 | cookie_jar = DummyCookieJar() 152 | 153 | async with ClientSession(headers=headers, cookie_jar=cookie_jar) as session: 154 | pool = WebsitePool(*[ 155 | Website.from_dict(data) for data in load_website_data() 156 | ]) 157 | pool.set_username(username) 158 | 159 | requests = pool.start_requests(session) 160 | 161 | async for response in requests: 162 | await queue.put([ 163 | response.successfully, 164 | response.url, 165 | response.ms 166 | ]) 167 | 168 | # Finished 169 | cookie[1] = True 170 | 171 | 172 | @routes.get("/favicon.ico") 173 | async def icon(request: Request) -> web.FileResponse: 174 | return web.FileResponse(path="./static/assets/favicon.ico") 175 | 176 | 177 | routes.static("/static", "./static") 178 | app.router.add_routes(routes) 179 | -------------------------------------------------------------------------------- /tracer/models/textanimation.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | __all__ = ("AsyncTextAnimation",) 26 | 27 | from typing import Any, Callable, Generator, Tuple, Optional 28 | from abc import ABC, abstractmethod 29 | import asyncio 30 | 31 | from colorama import Fore 32 | 33 | 34 | class AbstractTextAnimation(ABC): 35 | @abstractmethod 36 | def set_condition(self, callable): 37 | pass 38 | 39 | @abstractmethod 40 | def set_message(self, message): 41 | pass 42 | 43 | @abstractmethod 44 | async def start(self): 45 | pass 46 | 47 | 48 | class AsyncTextAnimation(AbstractTextAnimation): 49 | """Represents a simple text animation 50 | 51 | Attributes 52 | ---------- 53 | message : str 54 | The message that gets displayed 55 | condition : Callable[..., bool] 56 | A callable object representing the loop condition. As 57 | soon as it returns `False`, the animation will stop 58 | args : Tuple[Any] 59 | Args that get passed to the condition when calling it 60 | colored : bool 61 | If the animation should be colored 62 | 63 | Supported Operations 64 | -------------------- 65 | `str(obj)` 66 | Returns the str representation of the text animation 67 | `len(obj)` 68 | Returns the length of the message 69 | 70 | Note 71 | ---- 72 | While the animation is being displayed, there shouldn't 73 | be any other part in the program writing to the stdout 74 | 75 | Author 76 | ------ 77 | chr3st5an 78 | """ 79 | 80 | def __init__( 81 | self, 82 | message: str, 83 | condition: Callable[..., bool], 84 | *args, 85 | color: bool = True 86 | ): 87 | """Create an animation object 88 | 89 | Parameters 90 | ---------- 91 | message : str 92 | The message that gets displayed 93 | condition : Callable[..., bool] 94 | A callable object representing the loop condition. As 95 | soon as it returns `False`, the animation will stop 96 | *args : Any 97 | Args that get passed to the condition when calling it 98 | color : bool, optional 99 | If the animation should be colored, by default True 100 | """ 101 | 102 | self.set_message(message) 103 | self.set_condition(condition) 104 | 105 | self.__args = args 106 | self.__color = bool(color) 107 | self.__message = None 108 | self.__condition = None 109 | 110 | def __str__(self) -> str: 111 | return (f"<{self.__class__.__qualname__}(message={self.__message!r}, " 112 | f"condition={self.__condition}, args={self.__args}, " 113 | f"colored={self.__color})>") 114 | 115 | def __len__(self) -> int: 116 | return len(self.__message) 117 | 118 | def __await__(self) -> Generator: 119 | spinner_sequence = "|/-\\" 120 | 121 | while self.__condition(*self.__args): 122 | for char in spinner_sequence: 123 | spinner = f"[{Fore.CYAN}{char}{Fore.RESET}]" if self.colored else f"[{char}]" 124 | 125 | print(f"\r{spinner} {self.__message}", end="", flush=True) 126 | 127 | yield from asyncio.sleep(0.1).__await__() 128 | 129 | # Removes the loading message 130 | print("\r" + (" "*(len(self) + 10)), end="\r") 131 | 132 | @property 133 | def message(self) -> Optional[str]: 134 | return self.__message 135 | 136 | @property 137 | def condition(self) -> Optional[Callable[..., bool]]: 138 | return self.__condition 139 | 140 | @property 141 | def args(self) -> Tuple[Any]: 142 | return self.__args 143 | 144 | @property 145 | def colored(self) -> bool: 146 | return self.__color 147 | 148 | def set_message(self, message: str) -> None: 149 | """Assigns a new message to the animation 150 | 151 | Parameters 152 | ---------- 153 | message : str 154 | The message to set for the animation object 155 | """ 156 | 157 | self.__message = message 158 | 159 | def set_condition(self, condition: Callable[..., bool]) -> None: 160 | """Sets a new condition 161 | 162 | Parameters 163 | ---------- 164 | condition : Callable[..., bool] 165 | A callable object representing the loop condition. As 166 | soon as it returns `False`, the animation will stop 167 | """ 168 | 169 | self.__condition = condition 170 | 171 | async def start(self) -> None: 172 | """Displays a loading animation as long as the condition is True 173 | 174 | This is done by creating and awaiting an `asyncio.Task` object. 175 | While the animation is displayed, there shouldn't be any other 176 | function using `print`. 177 | """ 178 | 179 | await self 180 | -------------------------------------------------------------------------------- /tracer/models/category.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | __all__ = ("Category",) 28 | 29 | from abc import ABC, abstractmethod 30 | from typing import Any, Dict, List 31 | from copy import deepcopy 32 | 33 | 34 | class AbstractCategory(ABC): 35 | @property 36 | @abstractmethod 37 | def website(self): 38 | pass 39 | 40 | 41 | class Category(AbstractCategory): 42 | """Used to represent the category of a site 43 | 44 | Attributes 45 | ---------- 46 | website : tracer.Website 47 | The website to which the category belongs to 48 | as_number : int 49 | The int representation of the category 50 | as_str : str 51 | A str containing the category name 52 | 53 | Classmethods 54 | ------------ 55 | cls.to_number(str) -> int 56 | Converts the str name of a category into the 57 | corresponding int value 58 | cls.to_str(int) -> str 59 | Converts the int representation of a category 60 | into the str name of the category 61 | cls.all_categories() -> List[str] 62 | Returns a list with all available categories. 63 | 64 | Supported Operations 65 | -------------------- 66 | `str(obj)` 67 | Returns the str representation of the category 68 | `int(obj)` 69 | Alias for `obj.as_number` 70 | `x == obj` 71 | Compares the int values of both objects 72 | `copy.copy(obj)` 73 | Returns a copy of the category 74 | `copy.deepcopy(obj)` 75 | Returns a deepcopy of the category 76 | 77 | Author 78 | ------ 79 | chr3st5an 80 | """ 81 | 82 | SOCIALMEDIA = 1 83 | XXX = 2 84 | BLOG = 3 85 | ART = 4 86 | PROGRAMMING = 5 87 | VIDEO = 6 88 | MESSAGING = 7 89 | DATING = 8 90 | MUSIC = 9 91 | SPORT = 10 92 | MEMES = 11 93 | OFFICE = 12 94 | NEWS = 13 95 | GAMES = 14 96 | LINKS = 15 97 | OTHER = 16 98 | 99 | @classmethod 100 | def to_number(cls, category: str) -> int: 101 | """Returns the int representation of the given category 102 | 103 | Parameters 104 | ---------- 105 | category : str 106 | The category 107 | 108 | Returns 109 | ------- 110 | int 111 | The int representation of the category. -1 if the 112 | given category doesn't exist 113 | """ 114 | 115 | return cls.__dict__.get(category.upper(), -1) 116 | 117 | @classmethod 118 | def to_str(cls, number: int) -> str: 119 | """Returns the str representation of the given category 120 | 121 | Parameters 122 | ---------- 123 | number : int 124 | Any number 125 | 126 | Returns 127 | ------- 128 | str 129 | The corresponding str representation of the number. 130 | "UNKNOWN" if there is no corresponding 131 | representation. 132 | """ 133 | 134 | try: 135 | return [ 136 | key for key, val in cls.__dict__.items() if val == number 137 | ][0] 138 | except IndexError: 139 | return "UNKNOWN" 140 | 141 | @classmethod 142 | def all_categories(cls) -> List[str]: 143 | """Returns a list with all categories 144 | 145 | Returns 146 | ------- 147 | List[str] 148 | A list containing every category that is part 149 | of this class 150 | """ 151 | 152 | return [attr for attr in dir(cls) if attr.isupper()] 153 | 154 | def __init__( 155 | self, 156 | website, 157 | category_number: int 158 | ): 159 | """Initializes a category 160 | 161 | Parameters 162 | ---------- 163 | website : tracer.Website 164 | The website that belongs to this category 165 | category_number : int 166 | The number of the category to represent with 167 | this object 168 | 169 | Example 170 | ------- 171 | >>> my_category = Category(..., Category.VIDEO) 172 | 173 | >>> print(my_category) # 174 | 175 | >>> print(my_category == Category.VIDEO) # True 176 | """ 177 | 178 | self.__website = website 179 | self.__number = category_number 180 | 181 | def __str__(self) -> str: 182 | return f"<{self.__class__.__qualname__}[{self.as_str}]>" 183 | 184 | def __int__(self) -> int: 185 | return self.__number 186 | 187 | def __eq__(self, other: Any) -> bool: 188 | try: 189 | return int(self) == int(other) 190 | except Exception: 191 | return False 192 | 193 | def __copy__(self) -> Category: 194 | category = self.__class__.__new__(self.__class__) 195 | category.__dict__.update(self.__dict__) 196 | 197 | return category 198 | 199 | def __deepcopy__(self, memo: Dict[int, Any]) -> Category: 200 | category = self.__class__.__new__(self.__class__) 201 | 202 | memo[id(self)] = category 203 | 204 | for k, v in self.__dict__.items(): 205 | setattr(category, k, deepcopy(v, memo)) 206 | 207 | return category 208 | 209 | @property 210 | def website(self): 211 | return self.__website 212 | 213 | @property 214 | def as_number(self) -> int: 215 | return self.__number 216 | 217 | @property 218 | def as_str(self) -> str: 219 | return self.to_str(self.__number).lower() 220 | -------------------------------------------------------------------------------- /tracer/models/result.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | __all__ = ("Result",) 26 | 27 | from typing import Optional 28 | from colorama import Fore 29 | 30 | 31 | class Result(object): 32 | """Represents the result of a HTTP request 33 | 34 | Objects of this class are mainly intended for 35 | view-only use and hence don't offer big 36 | functionalities. Objects of this class are 37 | in a close relationship with `tracer.Website` 38 | instances as the main aim of this class is 39 | to represent the results of the requests performed 40 | by those instances. 41 | 42 | Attributes 43 | ---------- 44 | website : tracer.Website 45 | The website that is associated with the result. 46 | status_code : int 47 | The returned status code of the HTTP request 48 | successfully : bool 49 | If the request was successfully in the sense of 50 | that the username exists. 51 | user_exists : bool 52 | Alias for `obj.successfully`. 53 | delay : float 54 | The amount of seconds it took for a response. 55 | ms : float 56 | Alias for `obj.delay`. 57 | host : str 58 | The domain name of the website. 59 | url : str 60 | The request URL. 61 | timeout : bool 62 | If a TimeoutError occurred while performing 63 | the request. 64 | error : Exception, optional 65 | Any other excpetion that might have occurred while 66 | performing the request. None if none occurred. 67 | 68 | Methods 69 | ------- 70 | obj.verbose(Optional[bool]) -> str 71 | Returns a string containing the key information of the 72 | result. 73 | 74 | Supported Operations 75 | -------------------- 76 | `str(obj)` 77 | Returns the str representation of the result 78 | `bool(obj)` 79 | Alias for `obj.successfully` 80 | 81 | Author 82 | ------ 83 | chr3st5an 84 | """ 85 | 86 | __slots__ = ( 87 | "__website", 88 | "__status_code", 89 | "__successfully", 90 | "__delay", 91 | "__host", 92 | "__url", 93 | "__timeout", 94 | "__error", 95 | ) 96 | 97 | def __init__( 98 | self, 99 | website, 100 | status_code: int, 101 | successfully: bool, 102 | delay: float, 103 | host: str, 104 | url: str, 105 | timeout: bool = False, 106 | error: Optional[Exception] = None 107 | ): 108 | """Represents the result of a request 109 | 110 | Parameters 111 | ---------- 112 | website : tracer.Website 113 | The website to which the result belongs to 114 | status_code : int 115 | The returned status code of the request 116 | successfully : bool 117 | If the request was successfully, alias if the 118 | username exists 119 | delay : float 120 | How many seconds it took for a response 121 | host : str 122 | The host of the website to which a requests got 123 | send (alias the domain) 124 | url : str 125 | The request url 126 | timeout : bool, optional 127 | If the request timed out, by default False 128 | error : Exception, optional 129 | Any exception that might have occurred, by default None 130 | """ 131 | 132 | self.__website = website 133 | self.__status_code = status_code 134 | self.__successfully = successfully 135 | self.__delay = round(delay, 3) 136 | self.__host = host 137 | self.__url = url 138 | self.__timeout = timeout 139 | self.__error = error 140 | 141 | def __str__(self) -> str: 142 | return (f"<{self.__class__.__qualname__}(" 143 | f"user_exists={self.user_exists}, delay={self.delay}, " 144 | f"timeout={self.timeout}, error={bool(self.error)})>") 145 | 146 | def __bool__(self) -> bool: 147 | return self.__successfully 148 | 149 | @property 150 | def website(self): 151 | return self.__website 152 | 153 | @property 154 | def status_code(self) -> int: 155 | return self.__status_code 156 | 157 | @property 158 | def successfully(self) -> bool: 159 | return self.__successfully 160 | 161 | @property 162 | def user_exists(self) -> bool: 163 | """Alias for `result.successfully`""" 164 | 165 | return self.successfully 166 | 167 | @property 168 | def delay(self) -> float: 169 | return self.__delay 170 | 171 | @property 172 | def ms(self) -> float: 173 | """Alias for `result.delay`""" 174 | 175 | return self.delay 176 | 177 | @property 178 | def host(self) -> str: 179 | return self.__host 180 | 181 | @property 182 | def url(self) -> str: 183 | return self.__url 184 | 185 | @property 186 | def timeout(self) -> bool: 187 | return self.__timeout 188 | 189 | @property 190 | def error(self) -> Optional[Exception]: 191 | return self.__error 192 | 193 | def verbose(self, colored: bool = True) -> str: 194 | """Creates a verbose string 195 | 196 | The string returned by this method is mainly 197 | intended for logging purposes. It contains 198 | the delay, host and status code of the request. 199 | 200 | Parameters 201 | ---------- 202 | colored : bool, optional 203 | If the string should be colored, by default True 204 | 205 | Returns 206 | ------- 207 | str 208 | Verbose string 209 | """ 210 | 211 | message = f"{self.ms}s <=> {self.host} <=> {self.status_code}" 212 | 213 | return f"{Fore.CYAN}{message}{Fore.RESET}" if colored else message 214 | -------------------------------------------------------------------------------- /tracer/models/parser.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | __all__ = ("TracerParser",) 26 | 27 | from configparser import ConfigParser 28 | from typing import Any, Dict, Union 29 | from abc import ABC, abstractmethod 30 | from argparse import ArgumentParser 31 | from pathlib import Path 32 | import os 33 | import re 34 | 35 | from .category import Category 36 | 37 | 38 | class AbstractTracerParser(ABC): 39 | @abstractmethod 40 | def parse(self): 41 | pass 42 | 43 | 44 | class TracerParser(AbstractTracerParser): 45 | """Parses relevant args for Tracer 46 | 47 | Attributes 48 | ---------- 49 | config_file : Union[Path, str] 50 | Path to a `.conf` file 51 | 52 | Methods 53 | ------- 54 | obj.parse() -> Dict 55 | Parses args from the config file and from the 56 | CLI. 57 | 58 | Supported Operations 59 | -------------------- 60 | `str(obj)` 61 | Returns the str representation of the parser 62 | 63 | Note 64 | ---- 65 | The given config file has to include a `[DEFAULT]` 66 | header as the parser looks for values defined under 67 | that header. 68 | 69 | Author 70 | ------ 71 | chr3st5an 72 | """ 73 | 74 | __slots__ = ("config_file",) 75 | 76 | def __init__(self, config_file: Union[Path, str]): 77 | """Creates a parser 78 | 79 | Parameters 80 | ---------- 81 | config_file : Union[Path, str] 82 | Path to a `.conf` file 83 | 84 | Note 85 | ---- 86 | The config file has to include a `[DEFAULT]` 87 | header 88 | """ 89 | 90 | self.config_file = config_file 91 | 92 | def __str__(self) -> str: 93 | return f"<{self.__class__.__qualname__}(config_file={self.config_file})>" 94 | 95 | def parse(self) -> Dict[Any, Any]: 96 | """Parses args from the config file and the CLI 97 | 98 | Returns 99 | ------- 100 | Dict[str, str] 101 | A dict containing the given args 102 | """ 103 | 104 | kwargs = self._parse_conf_file() 105 | kwargs.update(self._parse_console()) 106 | 107 | return kwargs 108 | 109 | def _parse_conf_file(self) -> Dict[str, Any]: 110 | """Parses args from the config file 111 | 112 | Returns 113 | ------- 114 | Dict[str, str] 115 | A dict containing the given args 116 | """ 117 | 118 | if not os.path.exists(self.config_file): 119 | return dict() 120 | 121 | parser = ConfigParser() 122 | parser.read(self.config_file) 123 | 124 | settings = dict(parser["DEFAULT"]) 125 | 126 | # Converts the options from the conf file into 127 | # corresponding datatypes 128 | for setting, value in settings.items(): 129 | if value.lower() == "off": 130 | settings[setting] = False 131 | elif value.lower() == "on": 132 | settings[setting] = True 133 | elif value.lower() == "none": 134 | settings[setting] = None 135 | elif value == "": 136 | settings[setting] = list() 137 | elif value.isdigit(): 138 | settings[setting] = float(value) 139 | else: 140 | settings[setting] = re.findall(r"[a-zA-Z0-9.]+", value) 141 | 142 | return settings 143 | 144 | def _parse_console(self) -> Dict[str, Any]: 145 | """Parses provided options from the CLI 146 | 147 | Returns 148 | ------- 149 | Dict[str, Any] 150 | A dict containing the provided options 151 | """ 152 | 153 | parser = ArgumentParser( 154 | prog="tracer", 155 | usage="%(prog)s [options] username", 156 | description=("Check on which website the specified " 157 | "username is in use"), 158 | epilog="A tool created by @chr3st5an", 159 | ) 160 | 161 | parser.add_argument( 162 | "username", 163 | metavar="username", 164 | type=str, 165 | help="The username to check" 166 | ) 167 | parser.add_argument( 168 | "-t", 169 | "--timeout", 170 | type=int, 171 | help="set a timeout for each request", 172 | ) 173 | parser.add_argument( 174 | "-e", 175 | "--exclude", 176 | type=str, 177 | default=list(), 178 | action="append", 179 | help=("exclude a website, e.g. instagram.com. " 180 | "Can be used multiple times"), 181 | ) 182 | parser.add_argument( 183 | "-o", 184 | "--only", 185 | type=str, 186 | default=list(), 187 | action="append", 188 | help=("sent a request only to the given site. Can " 189 | "be used multiple times"), 190 | ) 191 | parser.add_argument( 192 | "-E", 193 | "--exclude-category", 194 | type=str, 195 | default=list(), 196 | action="append", 197 | help=("exclude every website which belongs to the given category. " 198 | f"Categories: {', '.join(Category.all_categories())}"), 199 | ) 200 | parser.add_argument( 201 | "-O", 202 | "--only-category", 203 | type=str, 204 | default=list(), 205 | action="append", 206 | help=("sent requests only to the sites belonging " 207 | "to the given category. See a list of all " 208 | "categories under -E"), 209 | ) 210 | parser.add_argument( 211 | "-b", 212 | "--browse", 213 | default=False, 214 | action="store_true", 215 | help="open successful results in browser", 216 | ) 217 | parser.add_argument( 218 | "-v", 219 | "--verbose", 220 | default=False, 221 | action="store_true", 222 | help="show additional information", 223 | ) 224 | parser.add_argument( 225 | "-a", 226 | "--all", 227 | default=False, 228 | action="store_true", 229 | help="show all results" 230 | ) 231 | parser.add_argument( 232 | "--ip-check", 233 | default=False, 234 | action="store_true", 235 | help=("retrieve and print your IP on program " 236 | "startup and wait 3s before continuing"), 237 | ) 238 | parser.add_argument( 239 | "--web", 240 | default=False, 241 | action="store_true", 242 | help="start a GUI version of Tracer in your browser" 243 | ) 244 | 245 | args = {} 246 | 247 | # Parses the args and filters options out that didn't were provided 248 | for key, value in dict(parser.parse_args()._get_kwargs()).items(): 249 | if not (value == parser.get_default(key)): 250 | args[key] = value 251 | 252 | return args 253 | -------------------------------------------------------------------------------- /tracer/models/websitepool.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | __all__ = ("WebsitePool",) 28 | 29 | from abc import ABC, abstractmethod 30 | from typing import ( 31 | Any, 32 | AsyncGenerator, 33 | Callable, 34 | Dict, 35 | Generator, 36 | Tuple, 37 | Optional 38 | ) 39 | import asyncio 40 | import copy 41 | 42 | from aiohttp import ClientSession 43 | 44 | from .website import Website 45 | from .result import Result 46 | 47 | 48 | class AbstractWebsitePool(ABC): 49 | @property 50 | @abstractmethod 51 | def sites(self): 52 | pass 53 | 54 | @abstractmethod 55 | def set_name(self, name): 56 | pass 57 | 58 | @abstractmethod 59 | def set_username(self, username): 60 | pass 61 | 62 | @abstractmethod 63 | async def start_requests(self, session, *args): 64 | pass 65 | 66 | 67 | class WebsitePool(AbstractWebsitePool): 68 | """Represents a collection of `tracer.Website` instances 69 | 70 | Attributes 71 | ---------- 72 | sites : Tuple[tracer.Website] 73 | All websites that are currently in the pool 74 | name : Optional[str] 75 | The name of the pool 76 | results : Tuple[tracer.Result] 77 | A collection of available results 78 | 79 | Methods 80 | ------- 81 | obj.set_name(str) 82 | Sets the name of the pool 83 | obj.set_username(str) 84 | Sets the username for every website inside the pool 85 | obj.add(tracer.Website) 86 | Adds a website to the pool 87 | obj.remove(Callable[[tracer.Website], bool]) 88 | Removes websites from the pool 89 | obj.start_requests(aiohttp.ClientSession, Optional[float]) 90 | Calls `send_request` of every site inside of the pool 91 | 92 | Supported Operations 93 | -------------------- 94 | `str(obj)` 95 | Returns the str representation of the pool 96 | `len(obj)` 97 | Returns the amount of websites that are currently in 98 | the pool 99 | `iter(obj)` 100 | Returns a generator containing all websites that are 101 | currently in the pool 102 | `x in obj` 103 | Checks if a website is currently in the pool 104 | `copy.copy(obj)` 105 | Returns a copy of the pool 106 | `copy.deepcopy(obj)` 107 | Returns a deepcopy of the pool 108 | 109 | Author 110 | ------ 111 | chr3st5an 112 | """ 113 | 114 | def __init__( 115 | self, 116 | *sites: Website, 117 | name: Optional[str] = None, 118 | allow_duplicates: bool = False 119 | ): 120 | """Initialize a WebsitePool 121 | 122 | Parameters 123 | ---------- 124 | name : str, optional 125 | The name of the WebsitePool, by default None 126 | allow_duplicates : bool 127 | Whether to allow duplicate websites in the pool 128 | or don't, by default False 129 | """ 130 | 131 | self.__sites = list() 132 | self.__allow_duplicates = allow_duplicates 133 | self.__name = None 134 | 135 | self.set_name(name) 136 | 137 | for site in sites: 138 | self.add(site) 139 | 140 | def __str__(self) -> str: 141 | return (f"<{self.__class__.__qualname__}(name={self.__name!r}, " 142 | f"websites={len(self)})>") 143 | 144 | def __len__(self) -> int: 145 | return len(self.__sites) 146 | 147 | def __iter__(self) -> Generator[Website, None, None]: 148 | yield from self.__sites 149 | 150 | def __contains__(self, obj: Any) -> bool: 151 | return isinstance(obj, Website) and any(obj is website for website in self) 152 | 153 | def __copy__(self) -> WebsitePool: 154 | pool = self.__class__.__new__(self.__class__) 155 | pool.__dict__.update(self.__dict__) 156 | 157 | return pool 158 | 159 | def __deepcopy__(self, memo: Dict[int, Any]) -> WebsitePool: 160 | pool = self.__class__.__new__(self.__class__) 161 | 162 | memo[id(self)] = pool 163 | 164 | for k, v in self.__dict__.items(): 165 | setattr(pool, k, copy.deepcopy(v, memo)) 166 | 167 | return pool 168 | 169 | @property 170 | def sites(self) -> Tuple[Website]: 171 | return tuple(self.__sites) 172 | 173 | @property 174 | def name(self) -> Optional[str]: 175 | return self.__name 176 | 177 | @property 178 | def results(self) -> Tuple[Result]: 179 | return tuple([site.result for site in self if site.result]) 180 | 181 | @property 182 | def is_empty(self) -> bool: 183 | return not self 184 | 185 | def set_name(self, name: str) -> None: 186 | """Sets the name for the pool 187 | 188 | Parameters 189 | ---------- 190 | name : str 191 | Name for the pool 192 | """ 193 | 194 | self.__name = name 195 | 196 | def set_username(self, username: str, /) -> None: 197 | """Sets the username for every website within the pool 198 | 199 | Parameters 200 | ---------- 201 | username : str 202 | The username to set for every website 203 | """ 204 | 205 | for site in self.sites: 206 | site.set_username(username) 207 | 208 | def add(self, website: Website) -> None: 209 | """Adds a website to the pool 210 | 211 | Parameters 212 | ---------- 213 | website : tracer.Website 214 | The website to add to the pool 215 | """ 216 | 217 | if not self.__allow_duplicates and website in self: 218 | return None 219 | 220 | self.__sites.append(website) 221 | 222 | def extend(self, pool: WebsitePool, _deepcopy: bool = True) -> None: 223 | """Adds sites from another pool to the own pool 224 | 225 | Parameters 226 | ---------- 227 | pool : tracer.WebsitePool 228 | The pool from which to take the sites 229 | _deepcopy : bool 230 | Whether to generate copies of the sites and 231 | add these or add the original sites, by 232 | default True 233 | """ 234 | 235 | for site in pool: 236 | self.add(copy.deepcopy(site) if _deepcopy else site) 237 | 238 | def remove(self, where: Callable[[Website], bool]) -> None: 239 | """Removes websites from the pool 240 | 241 | Parameters 242 | ---------- 243 | where : Callable[[Website], bool] 244 | A callable which takes in a website object as parameter 245 | and returns a bool. If `True` is returned, then the 246 | website will be removed. 247 | """ 248 | 249 | self.__sites = list(filter(lambda w: not where(w), self.sites)) 250 | 251 | def get(self, where: Callable[[Website], bool]) -> Tuple[Website]: 252 | """Retrieves websites from the pool 253 | 254 | Parameters 255 | ---------- 256 | where : Callable[[Website], bool] 257 | A callable which takes in a website object as parameter 258 | and returns a bool. If `True` is returned, then the 259 | websites will be added to the return tuple. 260 | 261 | Returns 262 | ------- 263 | Tuple[Website] 264 | Tuple of websites that were considered True by the 265 | callable 266 | """ 267 | 268 | return tuple(filter(where, self)) 269 | 270 | def get_by_name(self, name: str, /) -> Optional[Website]: 271 | """Retrieves a website by its name 272 | 273 | Parameters 274 | ---------- 275 | name : str 276 | The name of the website to retrieve 277 | 278 | Returns 279 | ------- 280 | Optional[Website] 281 | Either the website object or `None` if not 282 | found 283 | """ 284 | 285 | result = self.get(lambda w: w.name == name) 286 | 287 | return result[0] if result else None 288 | 289 | async def start_requests( 290 | self, 291 | session: ClientSession, 292 | timeout: Optional[float] = None 293 | ) -> AsyncGenerator[Result, None]: 294 | """Prepares and handles all requests 295 | 296 | Calls `send_request` of every tracer.Website object in the pool and 297 | returns an AsyncGenerator yielding the results of these 298 | requests when available. 299 | 300 | Parameters 301 | ---------- 302 | session : aiohttp.ClientSession 303 | A session object which gets used to make the 304 | requests. 305 | timeout : Union[int, float], optional 306 | Represents the time each request has before a 307 | TimeoutError occurs. 308 | 309 | Returns 310 | ------- 311 | AsyncGenerator[Result, None] 312 | Yields the results of the requests when available. 313 | 314 | Yields 315 | ------ 316 | tracer.Result 317 | The representation of the result of a request 318 | """ 319 | 320 | results = asyncio.Queue() 321 | requests = asyncio.gather(*[ 322 | site.send_request(session, timeout, cb=results.put) for site in self 323 | ]) 324 | 325 | while not (results.empty() and requests.done()): 326 | try: 327 | yield await asyncio.wait_for(results.get(), timeout=0.25) 328 | except asyncio.TimeoutError: 329 | """Force recheck of the loop condition""" 330 | -------------------------------------------------------------------------------- /tracer/tracer.py: -------------------------------------------------------------------------------- 1 | """Tracer 2 | 3 | This script allows to detect on which websites a username is currently 4 | taken. Chances are that there is always the same person behind a username as 5 | long as the username is special enough. 6 | 7 | Arguments can be provided through the CLI or through the config (.conf) file. 8 | 9 | Dependencies: requirements.txt 10 | """ 11 | 12 | __all__ = ("Tracer",) 13 | 14 | from typing import Dict, List, Optional, Union 15 | from threading import Thread 16 | from time import monotonic 17 | from pathlib import Path 18 | import http.cookies 19 | import webbrowser 20 | import asyncio 21 | import json 22 | import os 23 | 24 | from pyvis.network import Network 25 | from aiohttp import ClientSession 26 | from aiohttp.web import run_app 27 | from colorama import Fore 28 | import aiohttp 29 | 30 | from models import * 31 | from loader import * 32 | 33 | 34 | CONFIG = "../settings.conf" 35 | MY_IP = "https://api.myip.com" 36 | 37 | 38 | class Tracer(object): 39 | """Implement the main logic behind Tracer 40 | 41 | Author 42 | ------ 43 | chr3st5an 44 | """ 45 | 46 | @classmethod 47 | def main(cls) -> None: 48 | """Create a tracer instance and call its run coro 49 | 50 | Parse given args and create an event loop 51 | which executes the `run` coro 52 | """ 53 | 54 | # Changing dir so that relative paths make sense 55 | os.chdir(os.path.dirname(__file__)) 56 | 57 | # Parse the configs from the conf file and 58 | # update these with the arguments given by the CLI 59 | kwargs = TracerParser(CONFIG).parse() 60 | 61 | try: 62 | loop = asyncio.new_event_loop() 63 | asyncio.set_event_loop(loop) 64 | 65 | if kwargs.get("web"): 66 | from gui import app 67 | 68 | loop.call_later(1, webbrowser.open, "http://127.0.0.1:12345") 69 | run_app(app, host="127.0.0.1", port=12_345, loop=loop) 70 | else: 71 | loop.run_until_complete(cls(kwargs.pop("username"), **kwargs).run()) 72 | except KeyboardInterrupt: 73 | print("👋 Bye") 74 | finally: 75 | loop.stop() 76 | 77 | def __init__( 78 | self, 79 | username: str, 80 | data: Optional[List[Dict[str, str]]] = None, 81 | user_agent: Optional[str] = None, 82 | **kwargs, 83 | ): 84 | if data is None: 85 | data = load_website_data() 86 | 87 | if user_agent is None: 88 | user_agent = load_user_agent() 89 | 90 | self.username = username 91 | self.kwargs = dict(kwargs) 92 | self.user_agent = user_agent 93 | self.verbose = kwargs.get("verbose", False) 94 | self.pool = WebsitePool(*[Website.from_dict(data_) for data_ in data]) 95 | 96 | self.pool.set_username(self.username) 97 | 98 | self._out_dir = None 99 | self.__filter_sites() 100 | 101 | if kwargs.get("create_file_output"): 102 | self._create_output_dir() 103 | 104 | # When sending a request to TikTok, an annoying message 105 | # is printed by aiohttp. This turns the message off. 106 | http.cookies._is_legal_key = lambda _: True 107 | 108 | def __str__(self) -> str: 109 | return ( 110 | f"<{self.__class__.__qualname__}(username={self.username!r}, " 111 | f"kwargs={self.kwargs}, pool={self.pool})>" 112 | ) 113 | 114 | def __filter_sites(self) -> None: 115 | """Filter the list of sites based on the given arguments""" 116 | 117 | include: List[str] = self.kwargs.get("only", []) + self.kwargs.get( 118 | "only_category", [] 119 | ) 120 | 121 | exclude: List[str] = self.kwargs.get("exclude", []) + self.kwargs.get( 122 | "exclude_category", [] 123 | ) 124 | 125 | if not (include or exclude): 126 | return None 127 | 128 | if exclude: 129 | self.pool.remove( 130 | lambda w: (w.domain in exclude) or (w.category.as_str in exclude) 131 | ) 132 | 133 | if include: 134 | self.pool.remove( 135 | lambda w: not ((w.domain in include) or (w.category.as_str in include)) 136 | ) 137 | 138 | async def run(self) -> None: 139 | """Run the program""" 140 | 141 | if self.kwargs.get("print_logo", True): 142 | print(f"\n{Fore.CYAN}{load_logo()}{Fore.RESET}\n") 143 | 144 | cookie_jar = aiohttp.DummyCookieJar() 145 | headers = {"User-Agent": self.user_agent} 146 | 147 | async with ClientSession(headers=headers, cookie_jar=cookie_jar) as session: 148 | if self.kwargs.get("ip_check"): 149 | await self.retrieve_ip( 150 | session=session, timeout=self.kwargs.get("ip_timeout") 151 | ) 152 | 153 | print(f"[{Fore.CYAN}*{Fore.RESET}] Checking {Fore.CYAN}{self.username}" 154 | f"{Fore.RESET} on {len(self.pool)} sites:\n") 155 | 156 | start = monotonic() 157 | counter = 0 158 | requests = self.pool.start_requests(session, self.kwargs.get("timeout")) 159 | 160 | async for response in requests: 161 | message = f"{response.url} {response.verbose() if self.kwargs.get('verbose') else ''}" 162 | 163 | if not response.successfully: 164 | if self.kwargs.get("all"): 165 | if response.timeout: 166 | print(f"{Fore.RED}[Timeout]{Fore.RESET} {message}") 167 | else: 168 | print(f"{Fore.RED}[-]{Fore.RESET} {message}") 169 | 170 | continue 171 | 172 | print(f"{Fore.GREEN}[+]{Fore.RESET} {message}") 173 | 174 | if self.kwargs.get("browse"): 175 | Thread(target=webbrowser.open, args=(response.url,)).start() 176 | 177 | counter += 1 178 | 179 | print(f"\n[{Fore.CYAN}={Fore.RESET}] Found {Fore.CYAN}{counter}" 180 | f"{Fore.RESET} match(es) in {Fore.CYAN}{monotonic() - start:.2f}" 181 | f"s{Fore.RESET}") 182 | 183 | self.write_report(self._out_dir) 184 | self.draw_graph(self._out_dir) 185 | 186 | def write_report(self, out_dir: Union[str, Path]) -> None: 187 | """Create and write a report file which contains the results 188 | 189 | Parameters 190 | ---------- 191 | out_dir : Union[str, Path] 192 | In which directory to save the report file. If `None` 193 | is given, then no report file is created 194 | """ 195 | 196 | if self._out_dir is None: 197 | return None 198 | 199 | name = f"{out_dir}result.txt" 200 | mode = "w" if os.path.exists(name) else "x" 201 | 202 | with open(name, mode) as file: 203 | file.write(f"{load_logo()}\nReport for {self.username}:\n\n") 204 | 205 | for result in self.pool.results: 206 | file.write(result.url + "\n") 207 | 208 | return None 209 | 210 | def draw_graph(self, out_dir: Optional[Union[str, Path]]) -> None: 211 | """Visualize the results 212 | 213 | Create a HTML file containing a graph and open it 214 | in the default webbrowser 215 | 216 | Parameters 217 | ---------- 218 | out_dir : Union[str, Path] 219 | In which directory to save the HTML file. If `None` 220 | is given, then no graph is created 221 | """ 222 | 223 | if self._out_dir is None: 224 | return None 225 | 226 | net = Network( 227 | height="100%", 228 | width="100%", 229 | bgcolor="#282a36", 230 | font_color="#f8f8f2", 231 | ) 232 | 233 | net.add_node(self.username, color="#ff79c6", title="Username", shape="circle") 234 | 235 | for category in Category.all_categories(): 236 | net.add_node( 237 | n_id=category.title(), 238 | color="#bd93f9", 239 | shape="circle", 240 | title=category.title(), 241 | labelHighlightBold=True, 242 | ) 243 | net.add_edge(self.username, category.title()) 244 | 245 | for site in self.pool: 246 | if site.result.user_exists: 247 | net.add_node( 248 | n_id=site.name, 249 | color="#ff5555", 250 | shape="circle", 251 | title=site.url, 252 | labelHighlightBold=True, 253 | ) 254 | net.add_edge(site.category.as_str.title(), site.name) 255 | 256 | # Settings for the graph 257 | net.toggle_physics(True) 258 | net.set_edge_smooth("dynamic") 259 | 260 | # Save the graph in a html file and open it 261 | # in the default browser 262 | net.show(f"{out_dir}graph.html") 263 | 264 | return None 265 | 266 | async def retrieve_ip( 267 | self, 268 | session: ClientSession, 269 | timeout: Optional[float] = None 270 | ) -> None: 271 | """Retrieve the IP address and prints it 272 | 273 | Sleep for 3 seconds after the IP got retrieved. 274 | 275 | Parameters 276 | ---------- 277 | session : aiohttp.ClientSession 278 | Session used to send a request 279 | timeout : Union[int, float], optional 280 | Set a timeout for the request, by default None 281 | """ 282 | 283 | async def send_request() -> Dict[str, str]: 284 | timeout_ = aiohttp.ClientTimeout(timeout) 285 | 286 | try: 287 | async with session.get(MY_IP, timeout=timeout_) as r: 288 | return json.loads(await r.text()) 289 | except asyncio.TimeoutError: 290 | return {"ip": "0.0.0.0 [TIMEOUT]"} 291 | 292 | response = asyncio.create_task(send_request()) 293 | 294 | await AsyncTextAnimation("Retrieving IP...", lambda: not response.done()) 295 | 296 | print(f"Your IP address is {Fore.CYAN}{response.result()['ip']}{Fore.RESET}\n") 297 | 298 | await asyncio.sleep(3) 299 | 300 | def _create_output_dir(self) -> None: 301 | """Create the directory in which the results are saved""" 302 | 303 | results_dir = "../results/" 304 | 305 | if not os.path.exists(results_dir): 306 | try: 307 | os.mkdir(results_dir) 308 | except PermissionError: 309 | return None 310 | 311 | if not os.path.exists(f"{results_dir}{self.username}"): 312 | os.mkdir(f"{results_dir}{self.username}/") 313 | 314 | self._out_dir = f"{results_dir}{self.username}/" 315 | 316 | 317 | if __name__ == "__main__": 318 | Tracer.main() 319 | -------------------------------------------------------------------------------- /tracer/models/website.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2022 chr3st5an 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | __all__ = ("Website",) 28 | 29 | from typing import Any, Callable, Coroutine, Dict, Optional, Union 30 | from abc import ABC, abstractmethod 31 | from asyncio import TimeoutError 32 | from time import monotonic 33 | from copy import deepcopy 34 | import asyncio 35 | import re 36 | 37 | from aiohttp import ClientSession, ClientResponse, ClientTimeout 38 | 39 | from .category import Category 40 | from .result import Result 41 | 42 | 43 | class AbstractWebsite(ABC): 44 | @classmethod 45 | @abstractmethod 46 | def from_dict(cls, data): 47 | pass 48 | 49 | @abstractmethod 50 | def set_username(self, username): 51 | pass 52 | 53 | @abstractmethod 54 | def set_result(self, result): 55 | pass 56 | 57 | @abstractmethod 58 | async def send_request(self, session, *args): 59 | pass 60 | 61 | 62 | class Website(AbstractWebsite): 63 | """Represents a website 64 | 65 | Attributes 66 | ---------- 67 | name : str 68 | The name of the website, e.g. 'example' 69 | domain : str 70 | The domain name of the website, e.g. 'example.com' 71 | username : str 72 | The username whose existence is being checked on this 73 | website 74 | url : str 75 | The url that represents an users page on this website, 76 | e.g. 'https:\/\/example.com/user/' 77 | true_url : str 78 | The url that gets used to make the HTTP request to this 79 | website 80 | category : int 81 | The category to which the website belongs to 82 | result : tracer.Result 83 | The result of the request, `None` if no request was 84 | started yet 85 | err_ignore_code : bool 86 | Indicates if the returned status code of the request 87 | should be ignored 88 | err_text_pattern : str 89 | Regex pattern that gets applied on the returned text. 90 | If it matches, the username is considered as 91 | 'Not Found' 92 | err_url_pattern : str 93 | Regex pattern that gets applied on the returned url. 94 | If it matches, the username is considered as 95 | 'Not Found' 96 | err_on_dot : bool 97 | Indicates if the website responses with an error 98 | if the username contains a dot. If `True`, no 99 | actual request is send to the website 100 | 101 | Methods 102 | ------- 103 | obj.set_username(str) 104 | Sets a username for the website whose existence is later being 105 | checked 106 | obj.set_result(tracer.Result) 107 | Sets a result for the website 108 | obj.send_request(ClientSession, Optional[float], Optional[Callable]) 109 | Sends a HTTP GET request to the website and checks if 110 | the username exists. Then creates a `tracer.Result` object 111 | and assigns it to itself by using `obj.set_result` 112 | 113 | Classmethods 114 | ------------ 115 | cls.from_dict(dict) -> tracer.Website 116 | Creates a `tracer.Website` object by using the values 117 | of the dict 118 | 119 | Supported Operations 120 | -------------------- 121 | `str(obj)` 122 | Returns the str representation of the website 123 | `x == obj` 124 | Compares if `(1):` the other object is a `tracer.Website` 125 | object and if `(2):` the values are the same 126 | `copy.copy(obj)` 127 | Returns a copy of the website 128 | `copy.deepcopy(obj)` 129 | Returns a deepcopy of the website 130 | 131 | Note 132 | ---- 133 | The URL(s) passed to an object of this class should 134 | be structured as following `https://www.example.com/path/{}`, 135 | where `{}` represents the field into which the username 136 | is placed 137 | 138 | Author 139 | ------ 140 | chr3st5an 141 | """ 142 | 143 | @classmethod 144 | def from_dict(cls, data: Dict[str, str]) -> Website: 145 | """Creates a Website by using the given data 146 | 147 | Parameters 148 | ---------- 149 | data : Dict[str, str] 150 | A dict whose values are used to create the 151 | Website. The dict must include the following 152 | keys: `domain`, `url` & `category` 153 | 154 | Returns 155 | ------- 156 | tracer.Website 157 | The Website that got created 158 | """ 159 | 160 | return cls( 161 | domain=data["domain"], 162 | true_url=data["url"], 163 | category=data["category"], 164 | display_url=data.get("display_url"), 165 | err_ignore_code=data.get("err_ignore_code", False), 166 | err_text_pattern=data.get("err_text_pattern"), 167 | err_url_pattern=data.get("err_url_pattern"), 168 | err_on_dot=data.get("err_on_dot", False) 169 | ) 170 | 171 | def __init__( 172 | self, 173 | domain: str, 174 | true_url: str, 175 | category: int, 176 | display_url: Optional[str] = None, 177 | err_ignore_code: bool = False, 178 | err_text_pattern: Optional[str] = None, 179 | err_url_pattern: Optional[str] = None, 180 | err_on_dot: bool = False 181 | ): 182 | """Creates an instance 183 | 184 | Parameters 185 | ---------- 186 | domain : str 187 | The domain name of the website, e.g. 'example.com' 188 | true_url : str 189 | The url that gets used to make the HTTP request to this 190 | website 191 | category : int 192 | The category to which the website belongs to 193 | display_url : str, optional 194 | The url that represents an users page on this website, 195 | e.g. 'https:\/\/example.com/user/'. If None, then 196 | `true_url` is used, by default None 197 | err_ignore_code : bool 198 | Indicates if the returned status code of the request 199 | should be ignored, by default False 200 | err_text_pattern : str 201 | Regex pattern that gets applied on the returned text. 202 | If it matches, the username is considered as 203 | 'Not Found', by default None 204 | err_url_pattern : str 205 | Regex pattern that gets applied on the returned url. 206 | If it matches, the username is considered as 207 | 'Not Found', by default None 208 | err_on_dot : bool 209 | Indicates if the website responses with an error 210 | if the username contains a dot. If `True`, no 211 | actual request is send to the website, by default 212 | False 213 | """ 214 | 215 | self.__url = display_url if display_url else true_url 216 | self.__true_url = true_url 217 | self.__username = None 218 | self.__result = None 219 | self.__category = Category(self, category) 220 | self.__domain = domain 221 | 222 | self.err_ignore_code = err_ignore_code 223 | self.err_text_pattern = err_text_pattern 224 | self.err_url_pattern = err_url_pattern 225 | self.err_on_dot = err_on_dot 226 | 227 | def __str__(self) -> str: 228 | return (f"<{self.__class__.__qualname__}(name={self.name!r}, " 229 | f"domain={self.domain!r}, url={self.url!r}, " 230 | f"category={self.category}, result={self.result})>") 231 | 232 | def __eq__(self, other: Any) -> bool: 233 | return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ 234 | 235 | def __copy__(self) -> Website: 236 | website = self.__class__.__new__(self.__class__) 237 | website.__dict__.update(self.__dict__) 238 | 239 | return website 240 | 241 | def __deepcopy__(self, memo: Dict[int, Any]) -> Website: 242 | website = self.__class__.__new__(self.__class__) 243 | 244 | memo[id(self)] = website 245 | 246 | for k, v in self.__dict__.items(): 247 | setattr(website, k, deepcopy(v, memo)) 248 | 249 | return website 250 | 251 | @property 252 | def name(self) -> str: 253 | """Return the host name 254 | 255 | `example.com` => `example` 256 | """ 257 | 258 | return self.__domain.split('.')[0] 259 | 260 | @property 261 | def domain(self) -> str: 262 | return self.__domain 263 | 264 | @property 265 | def username(self) -> Optional[str]: 266 | return self.__username 267 | 268 | @property 269 | def category(self) -> Category: 270 | return self.__category 271 | 272 | @property 273 | def result(self) -> Optional[Result]: 274 | return self.__result 275 | 276 | @property 277 | def url(self) -> str: 278 | return self.__url.format(self.username) if self.username else self.__url 279 | 280 | @property 281 | def true_url(self) -> str: 282 | return self.__true_url.format(self.username) if self.username else self.__true_url 283 | 284 | def set_username(self, username: str) -> None: 285 | """Sets a username 286 | 287 | Parameters 288 | ---------- 289 | username : Optional[str] 290 | The username to set for the website 291 | """ 292 | 293 | self.__username = username 294 | 295 | def set_result(self, result: Result) -> None: 296 | """Sets a result 297 | 298 | Parameters 299 | ---------- 300 | result : Optional[Result] 301 | The result for the website 302 | """ 303 | 304 | self.__result = result 305 | 306 | async def send_request( 307 | self, 308 | session: ClientSession, 309 | timeout: Optional[float] = None, 310 | cb: Optional[Callable[[Result], Union[Coroutine, Any]]] = None 311 | ) -> None: 312 | """Sends a GET requests and evaluates the response 313 | 314 | Parameters 315 | ---------- 316 | session : ClientSession 317 | ClientSession to use for the GET request 318 | timeout : Optional[float], optional 319 | How many seconds the request has before a 320 | TimeoutError occurs, by default None 321 | cb : Optional[Callable[[Result], Union[Coroutine, Any]]], optional 322 | Any callable object that gets called when 323 | the result is available. It should only 324 | take in one parameter which is the result, 325 | by default None 326 | 327 | Raises 328 | ------ 329 | TypeError 330 | username not set 331 | """ 332 | 333 | if self.username is None: 334 | raise TypeError( 335 | "Cannot start request without a username being set" 336 | ) 337 | 338 | if "." in self.username and self.err_on_dot: 339 | self.set_result( 340 | result=Result( 341 | website=self, 342 | status_code=400, 343 | successfully=False, 344 | delay=0, 345 | host=self.domain, 346 | url=self.url 347 | ) 348 | ) 349 | 350 | await self.__callback(cb) 351 | 352 | return None 353 | 354 | timeout = ClientTimeout(timeout) 355 | start = monotonic() 356 | 357 | try: 358 | async with session.get(self.true_url, timeout=timeout) as response: 359 | result = Result( 360 | website=self, 361 | status_code=response.status, 362 | successfully=await self.__user_exists(response), 363 | delay=monotonic() - start, 364 | host=response.host, 365 | url=self.url 366 | ) 367 | 368 | response.close() 369 | await response.wait_for_close() 370 | except TimeoutError: 371 | result = Result( 372 | website=self, 373 | status_code=400, 374 | successfully=False, 375 | delay=monotonic() - start, 376 | host=self.domain, 377 | url=self.url, 378 | timeout=True 379 | ) 380 | except Exception as e: 381 | result = Result( 382 | website=self, 383 | status_code=600, 384 | successfully=False, 385 | delay=monotonic() - start, 386 | host=self.domain, 387 | url=self.url, 388 | error=e 389 | ) 390 | finally: 391 | self.set_result(result) 392 | 393 | await self.__callback(cb) 394 | 395 | return None 396 | 397 | async def __user_exists(self, response: ClientResponse) -> bool: 398 | """Check based on the returned response if the username is in use. 399 | 400 | First check if the response status is 200, then 401 | apply the given regex pattern on the returned text. 402 | 403 | Parameters 404 | ---------- 405 | response : aiohttp.ClientResponse 406 | A ClientResponse generated by `aiohttp`. 407 | 408 | Returns 409 | ------- 410 | bool 411 | Indicator if the username exists 412 | """ 413 | 414 | if not (response.status == 200 or self.err_ignore_code): 415 | return False 416 | 417 | await asyncio.sleep(0) 418 | 419 | if self.err_url_pattern: 420 | if re.search(self.err_url_pattern, str(response.url), flags=re.I): 421 | return False 422 | 423 | if self.err_text_pattern: 424 | text = await response.text() 425 | 426 | if re.search(self.err_text_pattern, text, flags=re.S + re.I + re.M): 427 | return False 428 | 429 | return True 430 | 431 | async def __callback( 432 | self, 433 | callback: Optional[Callable[[Result], Union[Coroutine, Any]]] 434 | ) -> Any: 435 | """Async wrapper for the original callback function""" 436 | 437 | if not callable(callback): 438 | return None 439 | 440 | await asyncio.sleep(0) 441 | 442 | if asyncio.iscoroutinefunction(callback): 443 | return await callback(self.result) 444 | 445 | return callback(self.result) 446 | -------------------------------------------------------------------------------- /data/pool.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "url": "https://8tracks.com/{}", 4 | "domain": "8tracks.com", 5 | "err_text_pattern": "8tracks radio", 6 | "err_on_dot": true, 7 | "category": 9 8 | }, 9 | { 10 | "url": "https://9gag.com/u/{}", 11 | "domain": "9gag.com", 12 | "err_text_pattern": "", 13 | "category": 1 14 | }, 15 | { 16 | "url": "https://about.me/{}", 17 | "domain": "about.me", 18 | "err_text_pattern": "about\\.meAcademia\\.edu", 25 | "err_on_dot": true, 26 | "category": 16 27 | }, 28 | { 29 | "url": "https://community.airtable.com/u/{}", 30 | "domain": "airtable.com", 31 | "err_text_pattern": "Airtable Community.*?", 32 | "category": 16 33 | }, 34 | { 35 | "url": "https://aminoapps.com/u/{}", 36 | "domain": "aminoapps.com", 37 | "err_text_pattern": "Amino", 38 | "category": 4 39 | }, 40 | { 41 | "url": "https://www.answers.com/u/{}", 42 | "domain": "answers.com", 43 | "err_text_pattern": "404 Not Found.*?", 44 | "category": 9 45 | }, 46 | { 47 | "url": "https://archive.org/details/@{}", 48 | "domain": "archive.org", 49 | "err_text_pattern": "cannot find.*?", 50 | "category": 16 51 | }, 52 | { 53 | "url": "https://ask.fm/{}", 54 | "domain": "ask.fm", 55 | "err_text_pattern": "Ask\\sand\\sAnswer.*?", 56 | "category": 7 57 | }, 58 | { 59 | "url": "https://bandcamp.com/{}", 60 | "domain": "bandcamp.com", 61 | "err_text_pattern": "Bandcamp", 62 | "category": 9 63 | }, 64 | { 65 | "url": "https://beacons.ai/{}", 66 | "domain": "beacons.ai", 67 | "err_text_pattern": "Beacons", 68 | "category": 16 69 | }, 70 | { 71 | "url": "https://br.bebee.com/bee/{}", 72 | "domain": "bebee.com", 73 | "err_text_pattern": "class=\\\"text-danger bb-expired\\\"", 74 | "category": 16 75 | }, 76 | { 77 | "url": "https://www.behance.net/{}", 78 | "domain": "behance.net", 79 | "err_text_pattern": "Behance ::.+?", 80 | "category": 4 81 | }, 82 | { 83 | "url": "https://bigo.tv/en/{}", 84 | "domain": "bigo.tv", 85 | "err_text_pattern": "ignoreUids:\\[\\]", 86 | "category": 6 87 | }, 88 | { 89 | "url": "https://blip.fm/{}", 90 | "domain": "blip.fm", 91 | "err_text_pattern": "404.*?Page Not Found", 92 | "category": 9 93 | }, 94 | { 95 | "url": "https://{}.blogspot.com/", 96 | "domain": "blogspot.com", 97 | "err_text_pattern": "https://www\\.blogger\\.com/create-blog\\.g?defaultSubdomain=", 98 | "err_on_dot": true, 99 | "category": 3 100 | }, 101 | { 102 | "url": "https://www.buymeacoffee.com/{}", 103 | "domain": "buymeacoffee.com", 104 | "err_text_pattern": "Buy Me a Coffee -.*?", 105 | "category": 16 106 | }, 107 | { 108 | "url": "https://www.buzzfeed.com/{}", 109 | "domain": "buzzfeed.com", 110 | "err_text_pattern": "Page not found", 111 | "category": 16 112 | }, 113 | { 114 | "url": "https://api.caffeine.tv/v1/users/{}", 115 | "display_url": "https://www.caffeine.tv/{}/profile/", 116 | "domain": "caffeine.tv", 117 | "err_text_pattern": "\"_account\":[\"Could not be found.\"]", 118 | "err_on_dot": true, 119 | "category": 6 120 | }, 121 | { 122 | "url": "https://campsite.bio/{}", 123 | "domain": "campsite.bio", 124 | "err_text_pattern": "Campsite", 125 | "category": 15 126 | }, 127 | { 128 | "url": "https://www.canva.com/p/{}/", 129 | "domain": "canva.com", 130 | "err_text_pattern": "Amazingly Simple Graphic.*?", 131 | "category": 4 132 | }, 133 | { 134 | "url": "https://www.caringbridge.org/visit/{}", 135 | "domain": "caringbridge.org", 136 | "err_text_pattern": "CaringBridge", 137 | "err_ignore_code": true, 138 | "category": 16 139 | }, 140 | { 141 | "url": "https://cash.app/${}", 142 | "domain": "cash.app", 143 | "err_text_pattern": "", 144 | "category": 16 145 | }, 146 | { 147 | "url": "https://chaturbate.com/{}/", 148 | "domain": "chaturbate.com", 149 | "err_text_pattern": "

HTTP 404.*?

", 150 | "err_url_pattern": "(?:chaturbate\\.[a-z]+?\\/?)$", 151 | "category": 2 152 | }, 153 | { 154 | "url": "https://www.chess.com/member/{}", 155 | "domain": "chess.com", 156 | "err_text_pattern": "", 157 | "category": 14 158 | }, 159 | { 160 | "url": "https://www.codechef.com/users/{}", 161 | "domain": "codechef.com", 162 | "err_url_pattern": "(?:codechef\\.[a-zA-Z]+?\\/?)$", 163 | "category": 5 164 | }, 165 | { 166 | "url": "https://codepen.io/{}", 167 | "domain": "codepen.io", 168 | "err_text_pattern": "404 on.*?", 169 | "category": 5 170 | }, 171 | { 172 | "url": "https://codeforces.com/profile/{}", 173 | "domain": "codeforces.com", 174 | "err_url_pattern": "(?:codeforces\\.[a-z]+?\\/?)$", 175 | "err_on_dot": true, 176 | "category": 5 177 | }, 178 | { 179 | "url": "https://coderwall.com/{}", 180 | "domain": "coderwall.com", 181 | "err_text_pattern": "", 182 | "err_on_dot": true, 183 | "category": 5 184 | }, 185 | { 186 | "url": "https://codewars.com/users/{}", 187 | "domain": "codewars.com", 188 | "err_text_pattern": "", 189 | "category": 5 190 | }, 191 | { 192 | "url": "https://creativemarket.com/users/{}", 193 | "domain": "creativemarket.com", 194 | "err_text_pattern": "Whoomp, there.*?", 195 | "category": 4 196 | }, 197 | { 198 | "url": "https://crokes.com/{}/", 199 | "domain": "crokes.com", 200 | "err_text_pattern": "Page Not Found.*?", 201 | "category": 1 202 | }, 203 | { 204 | "url": "https://www.crunchyroll.com/user/{}", 205 | "domain": "crunchyroll.com", 206 | "err_text_pattern": "Crunchyroll - Page not found", 207 | "category": 1 208 | }, 209 | { 210 | "url": "https://curiouscat.live/api/v2.1/profile?username={}", 211 | "display_url": "https://curiouscat.live/{}", 212 | "domain": "curiouscat.live", 213 | "err_text_pattern": "\\\"profile_does_not_exist\\\"", 214 | "category": 7 215 | }, 216 | { 217 | "url": "https://dev.to/{}", 218 | "domain": "dev.to", 219 | "err_text_pattern": "The page you.*?", 220 | "err_on_dot": true, 221 | "category": 5 222 | }, 223 | { 224 | "url": "https://deviantart.com/{}", 225 | "domain": "deviantart.com", 226 | "err_text_pattern": "DeviantArt: 404", 227 | "category": 4 228 | }, 229 | { 230 | "url": "https://disqus.com/by/{}/?", 231 | "domain": "disqus.com", 232 | "err_text_pattern": "Page not found.*?", 233 | "category": 16 234 | }, 235 | { 236 | "url": "https://dribbble.com/{}/about", 237 | "domain": "dribbble.com", 238 | "err_text_pattern": "Sorry, the page.*?", 239 | "category": 4 240 | }, 241 | { 242 | "url": "https://dzone.com/users/{}", 243 | "domain": "dzone.com", 244 | "err_url_pattern": "\\/users\\/(?:[^/?=]+?\\/?)$", 245 | "err_on_dot": true, 246 | "category": 5 247 | }, 248 | { 249 | "url": "https://ebay.com/usr/{}", 250 | "domain": "ebay.com", 251 | "err_text_pattern": "ebay profile - error.*?", 252 | "category": 16 253 | }, 254 | { 255 | "url": "https://ello.co/{}", 256 | "domain": "ello.co", 257 | "err_text_pattern": ".+?\\[404\\] Not Found", 258 | "category": 4 259 | }, 260 | { 261 | "url": "https://www.eporner.com/profile/{}/", 262 | "domain": "eporner.com", 263 | "err_text_pattern": "", 264 | "category": 2 265 | }, 266 | { 267 | "url": "https://www.etsy.com/people/{}", 268 | "domain": "etsy.com", 269 | "err_text_pattern": "Etsy -.*?", 270 | "category": 4 271 | }, 272 | { 273 | "url": "https://www.experts-exchange.com/members/{}", 274 | "domain": "experts-exchange.com", 275 | "err_text_pattern": "Not Found", 276 | "err_on_dot": true, 277 | "category": 16 278 | }, 279 | { 280 | "url": "https://facebook.com/{}", 281 | "domain": "facebook.com", 282 | "err_text_pattern": "", 289 | "category": 3 290 | }, 291 | { 292 | "url": "https://fanlink.to/{}", 293 | "domain": "fanlink.to", 294 | "err_text_pattern": "ToneDen -.*?", 295 | "category": 15 296 | }, 297 | { 298 | "url": "https://www.fark.com/users/{}", 299 | "domain": "fark.com", 300 | "err_text_pattern": "FARK\\.com: User profiles: view", 301 | "category": 16 302 | }, 303 | { 304 | "url": "https://www.fiverr.com/{}", 305 | "domain": "fiverr.com", 306 | "err_text_pattern": "Fiverr -.*?", 307 | "err_url_pattern": "(?:fiverr\\.[a-z]+?\\/?)$", 308 | "category": 16 309 | }, 310 | { 311 | "url": "https://flickr.com/photos/{}", 312 | "domain": "flickr.com", 313 | "err_text_pattern": "

404

", 314 | "category": 4 315 | }, 316 | { 317 | "url": "https://flipboard.com/@{}", 318 | "domain": "flipboard.com", 319 | "err_text_pattern": "Flipboard:.+?", 320 | "category": 13 321 | }, 322 | { 323 | "url": "https://members.fotki.com/{}/about/", 324 | "domain": "fotki.com", 325 | "err_text_pattern": "Member Not Found.*?", 326 | "err_on_dot": true, 327 | "category": 1 328 | }, 329 | { 330 | "url": "https://forum.freecodecamp.org/u/{}", 331 | "domain": "freecodecamp.org", 332 | "err_text_pattern": "The freeCodeCamp.*?", 333 | "category": 5 334 | }, 335 | { 336 | "url": "https://www.gaiaonline.com/profiles/{}/", 337 | "domain": "gaiaonline.com", 338 | "err_text_pattern": "", 339 | "err_on_dot": true, 340 | "category": 14 341 | }, 342 | { 343 | "url": "https://auth.geeksforgeeks.org/user/{}/profile", 344 | "domain": "geeksforgeeks.org", 345 | "err_text_pattern": "Login.*?", 346 | "err_url_pattern": ".*?\\?to=.*?", 347 | "category": 5 348 | }, 349 | { 350 | "url": "https://meta.getaether.net/u/{}", 351 | "domain": "getaether.net", 352 | "err_text_pattern": "Meta Aether", 353 | "category": 1 354 | }, 355 | { 356 | "url": "https://giphy.com/{}", 357 | "domain": "giphy.com", 358 | "err_text_pattern": "(?:404 Not Found)|(?:)", 359 | "err_url_pattern": ".*?\\/explore\\/.*?", 360 | "category": 11 361 | }, 362 | { 363 | "url": "https://gitea.com/{}", 364 | "domain": "gitea.com", 365 | "err_text_pattern": "Page Not Found.*?", 366 | "category": 5 367 | }, 368 | { 369 | "url": "https://gitee.com/{}", 370 | "domain": "gitee.com", 371 | "err_text_pattern": "", 372 | "category": 5 373 | }, 374 | { 375 | "url": "https://github.com/{}", 376 | "domain": "github.com", 377 | "err_text_pattern": "Page not found.*?", 378 | "category": 5 379 | }, 380 | { 381 | "url": "https://gitlab.com/{}", 382 | "domain": "gitlab.com", 383 | "err_text_pattern": "Sign in.*?", 384 | "category": 5 385 | }, 386 | { 387 | "url": "https://www.girlsaskguys.com/user/{}", 388 | "domain": "girlsaskguys.com", 389 | "err_text_pattern": "This doesn.*?", 390 | "category": 1 391 | }, 392 | { 393 | "url": "https://www.goodreads.com/{}", 394 | "domain": "goodreads.com", 395 | "err_text_pattern": "Page not found", 396 | "category": 16 397 | }, 398 | { 399 | "url": "https://en.gravatar.com/{}", 400 | "domain": "gravatar.com", 401 | "err_text_pattern": "Gravatar.*?", 402 | "err_url_pattern": "profiles\\/no-such-user", 403 | "err_on_dot": true, 404 | "category": 16 405 | }, 406 | { 407 | "url": "https://{}.gumroad.com/", 408 | "domain": "gumroad.com", 409 | "err_text_pattern": "Page not found.+?", 410 | "err_on_dot": true, 411 | "category": 16 412 | }, 413 | { 414 | "url": "https://www.gutefrage.net/nutzer/{}", 415 | "domain": "gutefrage.net", 416 | "err_text_pattern": "Gutefrage.*?", 417 | "category": 16 418 | }, 419 | { 420 | "url": "https://gfycat.com/@{}", 421 | "domain": "gfycat.com", 422 | "err_text_pattern": "Page not found.*?", 423 | "category": 16 424 | }, 425 | { 426 | "url": "https://hackerone.com/{}?type=user", 427 | "display_url": "https://hackerone.com/{}", 428 | "domain": "hackerone.com", 429 | "err_text_pattern": "

Page not found

", 430 | "err_on_dot": true, 431 | "category": 5 432 | }, 433 | { 434 | "url": "https://{}.hashnode.dev", 435 | "domain": "hashnode.com", 436 | "err_text_pattern": "404

", 437 | "err_on_dot": true, 438 | "category": 5 439 | }, 440 | { 441 | "url": "https://hubpages.com/@{}", 442 | "domain": "hubpages.com", 443 | "err_text_pattern": "Page Not Found", 444 | "err_on_dot": true, 445 | "category": 3 446 | }, 447 | { 448 | "url": "https://hypel.ink/{}", 449 | "domain": "hypel.ink", 450 | "err_text_pattern": "Hype Link", 451 | "category": 15 452 | }, 453 | { 454 | "url": "https://ifttt.com/p/{}", 455 | "domain": "ifttt.com", 456 | "err_text_pattern": "", 457 | "err_on_dot": true, 458 | "category": 16 459 | }, 460 | { 461 | "url": "https://ifunny.co/user/{}", 462 | "domain": "ifunny.co", 463 | "err_text_pattern": "404 - page.*?", 464 | "category": 11 465 | }, 466 | { 467 | "url": "https://imageshack.com/user/{}", 468 | "domain": "imageshack.com", 469 | "err_url_pattern": "imageshack\\.(?:[a-z]+?\\/?)$", 470 | "category": 1 471 | }, 472 | { 473 | "url": "https://imgur.com/search?q=user:{}", 474 | "display_url": "https://imgur.com/user/{}", 475 | "domain": "imgur.com", 476 | "err_text_pattern": "Found.*?0.*?results for", 477 | "err_url_pattern": "search\\?q=.*?", 478 | "err_on_dot": true, 479 | "category": 11 480 | }, 481 | { 482 | "url": "https://instabio.cc/{}", 483 | "domain": "instabio.cc", 484 | "err_text_pattern": "", 485 | "category": 15 486 | }, 487 | { 488 | "url": "https://instagram.com/{}/guides", 489 | "display_url": "https://instagram.com/{}", 490 | "domain": "instagram.com", 491 | "err_text_pattern": "Instagram", 492 | "category": 1 493 | }, 494 | { 495 | "url": "https://issuu.com/{}", 496 | "domain": "issuu.com", 497 | "err_text_pattern": "Issuu - Page.*?", 498 | "category": 3 499 | }, 500 | { 501 | "url": "https://itch.io/profile/{}", 502 | "domain": "itch.io", 503 | "err_text_pattern": "itch\\.io", 504 | "category": 14 505 | }, 506 | { 507 | "url": "https://{}.jimdosite.com/", 508 | "domain": "jimdosite.com", 509 | "err_text_pattern": "", 510 | "err_on_dot": true, 511 | "category": 16 512 | }, 513 | { 514 | "url": "https://community.k6.io/u/{}", 515 | "domain": "k6.io", 516 | "err_text_pattern": "k6 community.*?", 517 | "category": 16 518 | }, 519 | { 520 | "url": "https://www.kaggle.com/{}", 521 | "domain": "kaggle.com", 522 | "err_text_pattern": "Kaggle: Your Home for Data Science", 523 | "err_on_dot": true, 524 | "category": 5 525 | }, 526 | { 527 | "url": "https://ws2.kik.com/user/{}", 528 | "domain": "kik.com", 529 | "err_text_pattern": "Not Found", 530 | "category": 7 531 | }, 532 | { 533 | "url": "https://last.fm/user/{}", 534 | "domain": "last.fm", 535 | "err_text_pattern": "Page\\sNot\\sFound.*?", 536 | "category": 9 537 | }, 538 | { 539 | "url": "https://letterboxd.com/{}/", 540 | "domain": "letterboxd.com", 541 | "err_text_pattern": "Letterboxd.*?", 542 | "category": 6 543 | }, 544 | { 545 | "url": "https://letterpile.com/@{}", 546 | "domain": "letterpile.com", 547 | "err_text_pattern": "LetterPile", 548 | "category": 3 549 | }, 550 | { 551 | "url": "https://lichess.org/@/{}", 552 | "domain": "lichess.org", 553 | "err_text_pattern": "", 554 | "category": 14 555 | }, 556 | { 557 | "url": "https://www.liinks.co/{}", 558 | "domain": "liinks.co", 559 | "err_text_pattern": "Liinks.*?", 560 | "err_url_pattern": "type=NO_USER", 561 | "category": 15 562 | }, 563 | { 564 | "url": "https://line.me/R/ti/p/@{}", 565 | "domain": "line.me", 566 | "err_text_pattern": "", 567 | "category": 7 568 | }, 569 | { 570 | "url": "https://linkedin.com/in/{}", 571 | "domain": "linkedin.com", 572 | "err_url_pattern": "\\.[a-z]+?\\/authwall\\?.*?", 573 | "category": 16 574 | }, 575 | { 576 | "url": "https://linkfly.to/{}", 577 | "domain": "linkfly.to", 578 | "err_text_pattern": "404 - Oops.*?", 579 | "category": 15 580 | }, 581 | { 582 | "url": "https://linktr.ee/{}", 583 | "domain": "linktr.ee", 584 | "err_text_pattern": "\",\"statusCode\":404", 585 | "category": 15 586 | }, 587 | { 588 | "url": "https://{}.livejournal.com/", 589 | "domain": "livejournal.com", 590 | "err_text_pattern": "", 591 | "err_on_dot": true, 592 | "category": 3 593 | }, 594 | { 595 | "url": "https://lnk.bio/{}", 596 | "domain": "lnk.bio", 597 | "err_text_pattern": "Not Found.*?", 598 | "category": 15 599 | }, 600 | { 601 | "url": "https://mastodon.social/@{}", 602 | "domain": "mastodon.social", 603 | "err_text_pattern": "The page you.*?", 604 | "err_on_dot": true, 605 | "category": 1 606 | }, 607 | { 608 | "url": "https://medium.com/@{}", 609 | "domain": "medium.com", 610 | "err_text_pattern": "Medium", 611 | "category": 1 612 | }, 613 | { 614 | "url": "https://meetme.com/{}", 615 | "domain": "meetme.com", 616 | "err_text_pattern": "MeetMe.*?", 617 | "err_on_dot": true, 618 | "category": 16 619 | }, 620 | { 621 | "url": "https://www.memecenter.com/{}", 622 | "domain": "memecenter.com", 623 | "err_text_pattern": "Meme Center -.*?", 624 | "category": 11 625 | }, 626 | { 627 | "url": "https://micro.blog/{}", 628 | "domain": "micro.blog", 629 | "err_text_pattern": "User not found", 630 | "category": 3 631 | }, 632 | { 633 | "url": "https://mix.com/{}", 634 | "domain": "mix.com", 635 | "err_text_pattern": "Mix.*?", 636 | "err_on_dot": true, 637 | "category": 1 638 | }, 639 | { 640 | "url": "https://myanimelist.net/profile/{}", 641 | "domain": "myanimelist.net", 642 | "err_text_pattern": "404 Not.*?", 643 | "err_on_dot": true, 644 | "category": 16 645 | }, 646 | { 647 | "url": "https://myspace.com/{}", 648 | "domain": "myspace.com", 649 | "err_text_pattern": "

Page Not Found

", 650 | "category": 1 651 | }, 652 | { 653 | "url": "https://ngl.link/{}", 654 | "domain": "ngl.link", 655 | "err_text_pattern": "Could not find user", 656 | "category": 16 657 | }, 658 | { 659 | "url": "https://www.npmjs.com/~{}", 660 | "domain": "npmjs.com", 661 | "err_text_pattern": "not found", 662 | "category": 5 663 | }, 664 | { 665 | "url": "https://community.octoprint.org/u/{}", 666 | "domain": "octoprint.org", 667 | "err_text_pattern": "OctoPrint.*?", 668 | "category": 16 669 | }, 670 | { 671 | "url": "https://odysee.com/@{}", 672 | "domain": "odysee.com", 673 | "err_text_pattern": "Odysee", 674 | "category": 6 675 | }, 676 | { 677 | "url": "https://omlet.gg/profile/{}", 678 | "domain": "omlet.gg", 679 | "err_text_pattern": "Omlet Arcade", 680 | "category": 6 681 | }, 682 | { 683 | "url": "https://pastebin.com/u/{}", 684 | "domain": "pastebin.com", 685 | "err_text_pattern": ".+?Not Found.*?", 686 | "category": 5 687 | }, 688 | { 689 | "url": "https://www.patreon.com/{}/creators", 690 | "display_url": "https://www.patreon.com/{}", 691 | "domain": "patreon.com", 692 | "err_text_pattern": "404", 693 | "category": 16 694 | }, 695 | { 696 | "url": "https://www.pexels.com/@{}", 697 | "domain": "pexels.com", 698 | "err_text_pattern": "Error 404.*?", 699 | "category": 16 700 | }, 701 | { 702 | "url": "https://picsart.com/u/{}", 703 | "domain": "picsart.com", 704 | "err_text_pattern": "Page not found", 705 | "category": 1 706 | }, 707 | { 708 | "url": "https://pinterest.com/{}", 709 | "domain": "pinterest.com", 710 | "err_url_pattern": "(?:\/ideas\/)$", 711 | "err_text_pattern": "", 712 | "category": 4 713 | }, 714 | { 715 | "url": "https://pixabay.com/users/{}/", 716 | "domain": "pixabay.com", 717 | "err_text_pattern": "Error 404", 718 | "err_on_dot": true, 719 | "category": 16 720 | }, 721 | { 722 | "url": "https://www.plurk.com/{}", 723 | "domain": "plurk.com", 724 | "err_text_pattern": "User Not Found.*?", 725 | "category": 1 726 | }, 727 | { 728 | "url": "https://pornhub.com/users/{}", 729 | "domain": "pornhub.com", 730 | "err_text_pattern": "Page Not Found", 731 | "category": 2 732 | }, 733 | { 734 | "url": "https://www.prezi.community/u/{}", 735 | "domain": "prezi.community", 736 | "err_text_pattern": "", 737 | "category": 12 738 | }, 739 | { 740 | "url": "https://www.producthunt.com/@{}", 741 | "domain": "producthunt.com", 742 | "err_text_pattern": "Product Hunt:.*?", 743 | "category": 16 744 | }, 745 | { 746 | "url": "https://www.polywork.com/{}", 747 | "domain": "polywork.com", 748 | "err_text_pattern": "The page you were looking.*?", 749 | "err_on_dot": true, 750 | "category": 16 751 | }, 752 | { 753 | "url": "https://www.pscp.tv/{}", 754 | "domain": "pscp.tv", 755 | "err_text_pattern": "404", 756 | "category": 16 757 | }, 758 | { 759 | "url": "https://pypi.org/user/{}/", 760 | "domain": "pypi.org", 761 | "err_text_pattern": "Page Not Found.*?", 762 | "category": 5 763 | }, 764 | { 765 | "url": "https://quizlet.com/{}", 766 | "domain": "quizlet.com", 767 | "err_text_pattern": "", 768 | "err_url_pattern": "\\/\\w+?\\/\\d{9}", 769 | "category": 16 770 | }, 771 | { 772 | "url": "https://quora.com/profile/{}", 773 | "domain": "quora.com", 774 | "err_text_pattern": "", 775 | "category": 16 776 | }, 777 | { 778 | "url": "https://redbubble.com/people/{}", 779 | "domain": "redbubble.com", 780 | "err_text_pattern": "

This is a lost cause\\.

", 781 | "category": 4 782 | }, 783 | { 784 | "url": "https://reddit.com/user/{}", 785 | "domain": "reddit.com", 786 | "err_text_pattern": "Sorry, nobody.*?name\\.", 787 | "err_on_dot": true, 788 | "category": 1 789 | }, 790 | { 791 | "url": "https://www.redgifs.com/users/{}", 792 | "domain": "redgifs.com", 793 | "err_text_pattern": "Page Not Found", 794 | "category": 2 795 | }, 796 | { 797 | "url": "https://redtube.com/users/{}", 798 | "domain": "redtube.com", 799 | "err_text_pattern": "Page Not Found", 800 | "category": 2 801 | }, 802 | { 803 | "url": "https://replit.com/@{}", 804 | "domain": "replit.com", 805 | "err_text_pattern": "Replit - 404 - Replit", 806 | "category": 5 807 | }, 808 | { 809 | "url": "https://www.reverbnation.com/{}", 810 | "domain": "reverbnation.com", 811 | "err_text_pattern": "", 812 | "category": 9 813 | }, 814 | { 815 | "url": "https://sharesome.com/api/users/{}?stats=1&profile=1", 816 | "display_url": "https://sharesome.com/{}", 817 | "domain": "sharesome.com", 818 | "err_text_pattern": "404 \\|.*?", 819 | "err_on_dot": true, 820 | "category": 2 821 | }, 822 | { 823 | "url": "https://shoptly.com/{}", 824 | "domain": "shoptly.com", 825 | "err_text_pattern": "Sell Digital.*?", 826 | "category": 16 827 | }, 828 | { 829 | "url": "https://{}.skyrock.com/profil/", 830 | "domain": "skyrock.com", 831 | "err_text_pattern": "", 832 | "err_on_dot": true, 833 | "category": 3 834 | }, 835 | { 836 | "url": "https://{}.slack.com/", 837 | "domain": "slack.com", 838 | "err_text_pattern": "There's been a gli.*?", 839 | "err_on_dot": true, 840 | "category": 7 841 | }, 842 | { 843 | "url": "https://www.slideshare.net/{}", 844 | "domain": "slideshare.net", 845 | "err_text_pattern": "Username available", 846 | "category": 12 847 | }, 848 | { 849 | "url": "https://snapchat.com/add/{}", 850 | "domain": "snapchat.com", 851 | "err_text_pattern": "Sorry,.*?not\\sfound", 852 | "category": 1 853 | }, 854 | { 855 | "url": "https://soundcloud.com/{}", 856 | "domain": "soundcloud.com", 857 | "err_text_pattern": "Something went wrong.*?", 858 | "category": 9 859 | }, 860 | { 861 | "url": "https://sourceforge.net/u/{}/profile/", 862 | "domain": "sourceforge.net", 863 | "err_text_pattern": "Page not found.*?", 864 | "category": 5 865 | }, 866 | { 867 | "url": "https://spankbang.com/profile/{}", 868 | "domain": "spankbang.com", 869 | "err_text_pattern": "", 870 | "category": 2 871 | }, 872 | { 873 | "url": "https://spreely.com/{}", 874 | "domain": "spreely.com", 875 | "err_text_pattern": "Spreely.*?", 876 | "category": 1 877 | }, 878 | { 879 | "url": "https://open.spotify.com/user/{}", 880 | "domain": "spotify.com", 881 | "err_text_pattern": "Page not found", 882 | "category": 9 883 | }, 884 | { 885 | "url": "https://{}.start.page/", 886 | "domain": "start.page", 887 | "err_text_pattern": "Page not found!", 888 | "err_on_dot": true, 889 | "category": 16 890 | }, 891 | { 892 | "url": "https://steamcommunity.com/id/{}", 893 | "domain": "steamcommunity.com", 894 | "err_text_pattern": ".*?::\\s*?Error.*?", 895 | "category": 14 896 | }, 897 | { 898 | "url": "https://steemit.com/@{}", 899 | "domain": "steemit.com", 900 | "err_text_pattern": "Page Not Found.*?", 901 | "category": 1 902 | }, 903 | { 904 | "url": "https://www.strava.com/athletes/{}", 905 | "domain": "strava.com", 906 | "err_text_pattern": "", 907 | "err_on_dot": true, 908 | "category": 10 909 | }, 910 | { 911 | "url": "https://{}.substack.com/", 912 | "domain": "substack.com", 913 | "err_text_pattern": "Discover Substack.*?", 914 | "err_url_pattern": "\\.\\[a-z]+?\\/discover", 915 | "err_on_dot": true, 916 | "category": 16 917 | }, 918 | { 919 | "url": "https://www.taringa.net/{}", 920 | "domain": "taringa.net", 921 | "err_text_pattern": "Taringa!.*?", 922 | "category": 1 923 | }, 924 | { 925 | "url": "https://t.me/{}", 926 | "domain": "telegram.com", 927 | "err_text_pattern": "", 928 | "err_url_pattern": "telegram\\.org", 929 | "category": 7 930 | }, 931 | { 932 | "url": "https://tellonym.me/{}", 933 | "domain": "tellonym.me", 934 | "err_text_pattern": "TextBin", 941 | "err_on_dot": true, 942 | "category": 5 943 | }, 944 | { 945 | "url": "https://tiktok.com/@{}", 946 | "domain": "tiktok.com", 947 | "err_text_pattern": "Couldn't find this account.*?", 948 | "category": 1 949 | }, 950 | { 951 | "url": "https://tinder.com/@{}", 952 | "domain": "tinder.com", 953 | "err_text_pattern": "(?:Tinder.*?)?", 954 | "category": 8 955 | }, 956 | { 957 | "url": "https://www.tripadvisor.com/Profile/{}", 958 | "domain": "tripadvisor.com", 959 | "err_text_pattern": "", 960 | "category": 16 961 | }, 962 | { 963 | "url": "https://{}.tumblr.com/", 964 | "domain": "tumblr.com", 965 | "err_text_pattern": "", 966 | "err_on_dot": true, 967 | "category": 1 968 | }, 969 | { 970 | "url": "https://twitch.tv/{}", 971 | "domain": "twitch.tv", 972 | "err_text_pattern": "content='@twitch'>Page not found.*?", 979 | "category": 16 980 | }, 981 | { 982 | "url": "https://untappd.com/user/{}", 983 | "domain": "untappd.com", 984 | "err_text_pattern": "", 985 | "category": 16 986 | }, 987 | { 988 | "url": "https://valence.community/visualization/profile/{}", 989 | "domain": "valence.community", 990 | "err_text_pattern": "", 991 | "category": 16 992 | }, 993 | { 994 | "url": "https://www.vice.com/en/contributor/{}", 995 | "domain": "vice.com", 996 | "err_text_pattern": "vice\\.com.*?", 997 | "category": 16 998 | }, 999 | { 1000 | "url": "https://vimeo.com/{}", 1001 | "domain": "vimeo.com", 1002 | "err_text_pattern": "", 1003 | "category": 6 1004 | }, 1005 | { 1006 | "url": "https://vk.com/{}", 1007 | "domain": "vk.com", 1008 | "err_text_pattern": "404 Not.*?", 1009 | "category": 1 1010 | }, 1011 | { 1012 | "url": "https://vsco.co/{}/gallery", 1013 | "domain": "vsco.co", 1014 | "err_text_pattern": "Page not found.*?", 1015 | "category": 1 1016 | }, 1017 | { 1018 | "url": "https://community.w2g.tv/u/{}", 1019 | "domain": "w2g.tv", 1020 | "err_text_pattern": "Watch2Gether.*?", 1021 | "category": 16 1022 | }, 1023 | { 1024 | "url": "https://wakatime.com/@{}", 1025 | "domain": "wakatime.com", 1026 | "err_text_pattern": "404: Not Found - WakaTime", 1027 | "category": 5 1028 | }, 1029 | { 1030 | "url": "https://wattpad.com/user/{}", 1031 | "domain": "wattpad.com", 1032 | "err_text_pattern": "Wattpad -.*?", 1033 | "category": 4 1034 | }, 1035 | { 1036 | "url": "https://{}.webflow.io/", 1037 | "domain": "webflow.com", 1038 | "err_text_pattern": "404 -.*?", 1039 | "err_on_dot": true, 1040 | "category": 16 1041 | }, 1042 | { 1043 | "url": "https://{}.webnode.com/", 1044 | "domain": "webnode.com", 1045 | "err_text_pattern": "We are sorry, but the page .*?\\.webnode\\.com", 1046 | "err_on_dot": true, 1047 | "category": 3 1048 | }, 1049 | { 1050 | "url": "https://{}.weebly.com/", 1051 | "domain": "weebly.com", 1052 | "err_text_pattern": "404 - Page.*?", 1053 | "err_on_dot": true, 1054 | "category": 3 1055 | }, 1056 | { 1057 | "url": "https://weheartit.com/{}", 1058 | "domain": "weheartit.com", 1059 | "err_text_pattern": "The Page.*?", 1060 | "category": 1 1061 | }, 1062 | { 1063 | "url": "https://wikipedia.org/wiki/User:{}", 1064 | "domain": "wikipedia.org", 1065 | "err_text_pattern": "\\\".*?\\\" is not registered on this wiki.*?", 1066 | "category": 16 1067 | }, 1068 | { 1069 | "url": "https://{}.wordpress.com", 1070 | "domain": "wordpress.com", 1071 | "err_url_pattern": "\\/typo\\/\\?subdomain=.+?", 1072 | "err_on_dot": true, 1073 | "category": 5 1074 | }, 1075 | { 1076 | "url": "https://wt.social/u/{}", 1077 | "domain": "wt.social", 1078 | "err_text_pattern": "", 1079 | "category": 1 1080 | }, 1081 | { 1082 | "url": "https://xhamster.com/users/{}", 1083 | "domain": "xhamster.com", 1084 | "err_text_pattern": "User not found", 1085 | "category": 2 1086 | }, 1087 | { 1088 | "url": "https://xvideos.com/profiles/{}", 1089 | "domain": "xvideos.com", 1090 | "err_text_pattern": "", 1091 | "category": 2 1092 | }, 1093 | { 1094 | "url": "https://news.ycombinator.com/user?id={}", 1095 | "domain": "ycombinator.com", 1096 | "err_text_pattern": "No such user\\.", 1097 | "category": 3 1098 | }, 1099 | { 1100 | "url": "https://{}.yelp.com/", 1101 | "domain": "yelp.com", 1102 | "err_url_pattern": "yelp\\.[a-z]+?/[^/?=]+$", 1103 | "err_on_dot": true, 1104 | "err_text_pattern": "Restaurants, Dentists, Bars,.*?", 1105 | "category": 16 1106 | }, 1107 | { 1108 | "url": "https://youtube.com/c/{}", 1109 | "domain": "youtube.com", 1110 | "err_text_pattern": "404\\sNot\\sFound.*?", 1111 | "category": 6 1112 | } 1113 | ] 1114 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.1.0 ; python_full_version >= "3.8.1" and python_version < "4.0" \ 2 | --hash=sha256:9312414ae06472eb6f1d163f555e466a23aed1c8f60c30cccf7121dba2e53eb2 \ 3 | --hash=sha256:edd247df9a19e0db16534d4baaf536d6609a43e1de5401d7a4c1c148753a1635 4 | aiohttp-jinja2==1.5.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 5 | --hash=sha256:45cf00b80ab4dcc19515df13a929826eeb9698e76a3bcfd99112418751f5a061 \ 6 | --hash=sha256:8d149b2a57d91f794b33a394ea5bc66b567f38c74a5a6a9477afc2450f105c01 7 | aiohttp==3.8.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 8 | --hash=sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14 \ 9 | --hash=sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391 \ 10 | --hash=sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2 \ 11 | --hash=sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e \ 12 | --hash=sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9 \ 13 | --hash=sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd \ 14 | --hash=sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4 \ 15 | --hash=sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b \ 16 | --hash=sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41 \ 17 | --hash=sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567 \ 18 | --hash=sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275 \ 19 | --hash=sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54 \ 20 | --hash=sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a \ 21 | --hash=sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef \ 22 | --hash=sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99 \ 23 | --hash=sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da \ 24 | --hash=sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4 \ 25 | --hash=sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e \ 26 | --hash=sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699 \ 27 | --hash=sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04 \ 28 | --hash=sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719 \ 29 | --hash=sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131 \ 30 | --hash=sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e \ 31 | --hash=sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f \ 32 | --hash=sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd \ 33 | --hash=sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f \ 34 | --hash=sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e \ 35 | --hash=sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1 \ 36 | --hash=sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed \ 37 | --hash=sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4 \ 38 | --hash=sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1 \ 39 | --hash=sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777 \ 40 | --hash=sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531 \ 41 | --hash=sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b \ 42 | --hash=sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab \ 43 | --hash=sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8 \ 44 | --hash=sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074 \ 45 | --hash=sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc \ 46 | --hash=sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643 \ 47 | --hash=sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01 \ 48 | --hash=sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36 \ 49 | --hash=sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24 \ 50 | --hash=sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654 \ 51 | --hash=sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d \ 52 | --hash=sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241 \ 53 | --hash=sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51 \ 54 | --hash=sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f \ 55 | --hash=sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2 \ 56 | --hash=sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15 \ 57 | --hash=sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf \ 58 | --hash=sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b \ 59 | --hash=sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71 \ 60 | --hash=sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05 \ 61 | --hash=sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52 \ 62 | --hash=sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3 \ 63 | --hash=sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6 \ 64 | --hash=sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a \ 65 | --hash=sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519 \ 66 | --hash=sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a \ 67 | --hash=sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333 \ 68 | --hash=sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6 \ 69 | --hash=sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d \ 70 | --hash=sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57 \ 71 | --hash=sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c \ 72 | --hash=sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9 \ 73 | --hash=sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea \ 74 | --hash=sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332 \ 75 | --hash=sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5 \ 76 | --hash=sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622 \ 77 | --hash=sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71 \ 78 | --hash=sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb \ 79 | --hash=sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a \ 80 | --hash=sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff \ 81 | --hash=sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945 \ 82 | --hash=sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480 \ 83 | --hash=sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6 \ 84 | --hash=sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9 \ 85 | --hash=sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd \ 86 | --hash=sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f \ 87 | --hash=sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a \ 88 | --hash=sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a \ 89 | --hash=sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949 \ 90 | --hash=sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc \ 91 | --hash=sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75 \ 92 | --hash=sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f \ 93 | --hash=sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10 \ 94 | --hash=sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f 95 | aiosignal==1.3.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 96 | --hash=sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc \ 97 | --hash=sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17 98 | appnope==0.1.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform == "darwin" \ 99 | --hash=sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24 \ 100 | --hash=sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e 101 | asttokens==2.2.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 102 | --hash=sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3 \ 103 | --hash=sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c 104 | async-timeout==4.0.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 105 | --hash=sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15 \ 106 | --hash=sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c 107 | attrs==22.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 108 | --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \ 109 | --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99 110 | backcall==0.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 111 | --hash=sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e \ 112 | --hash=sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255 113 | charset-normalizer==3.0.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 114 | --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \ 115 | --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \ 116 | --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \ 117 | --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \ 118 | --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \ 119 | --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \ 120 | --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \ 121 | --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \ 122 | --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \ 123 | --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \ 124 | --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \ 125 | --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \ 126 | --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \ 127 | --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \ 128 | --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \ 129 | --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \ 130 | --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \ 131 | --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \ 132 | --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \ 133 | --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \ 134 | --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \ 135 | --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \ 136 | --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \ 137 | --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \ 138 | --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \ 139 | --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \ 140 | --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \ 141 | --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \ 142 | --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \ 143 | --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \ 144 | --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \ 145 | --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \ 146 | --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \ 147 | --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \ 148 | --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \ 149 | --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \ 150 | --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \ 151 | --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \ 152 | --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \ 153 | --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \ 154 | --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \ 155 | --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \ 156 | --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \ 157 | --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \ 158 | --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \ 159 | --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \ 160 | --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \ 161 | --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \ 162 | --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \ 163 | --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \ 164 | --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \ 165 | --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \ 166 | --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \ 167 | --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \ 168 | --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \ 169 | --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \ 170 | --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \ 171 | --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \ 172 | --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \ 173 | --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \ 174 | --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \ 175 | --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \ 176 | --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \ 177 | --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \ 178 | --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \ 179 | --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \ 180 | --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \ 181 | --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \ 182 | --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \ 183 | --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \ 184 | --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \ 185 | --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \ 186 | --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \ 187 | --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \ 188 | --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \ 189 | --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \ 190 | --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \ 191 | --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \ 192 | --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \ 193 | --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \ 194 | --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \ 195 | --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \ 196 | --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \ 197 | --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \ 198 | --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \ 199 | --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \ 200 | --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \ 201 | --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8 202 | colorama==0.4.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 203 | --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ 204 | --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 205 | decorator==5.1.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 206 | --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ 207 | --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 208 | executing==1.2.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 209 | --hash=sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc \ 210 | --hash=sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107 211 | frozenlist==1.3.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 212 | --hash=sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c \ 213 | --hash=sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f \ 214 | --hash=sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a \ 215 | --hash=sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784 \ 216 | --hash=sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27 \ 217 | --hash=sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d \ 218 | --hash=sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3 \ 219 | --hash=sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678 \ 220 | --hash=sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a \ 221 | --hash=sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483 \ 222 | --hash=sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8 \ 223 | --hash=sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf \ 224 | --hash=sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99 \ 225 | --hash=sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c \ 226 | --hash=sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48 \ 227 | --hash=sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5 \ 228 | --hash=sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56 \ 229 | --hash=sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e \ 230 | --hash=sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1 \ 231 | --hash=sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401 \ 232 | --hash=sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4 \ 233 | --hash=sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e \ 234 | --hash=sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649 \ 235 | --hash=sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a \ 236 | --hash=sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d \ 237 | --hash=sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0 \ 238 | --hash=sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6 \ 239 | --hash=sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d \ 240 | --hash=sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b \ 241 | --hash=sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6 \ 242 | --hash=sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf \ 243 | --hash=sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef \ 244 | --hash=sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7 \ 245 | --hash=sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842 \ 246 | --hash=sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba \ 247 | --hash=sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420 \ 248 | --hash=sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b \ 249 | --hash=sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d \ 250 | --hash=sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332 \ 251 | --hash=sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936 \ 252 | --hash=sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816 \ 253 | --hash=sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91 \ 254 | --hash=sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420 \ 255 | --hash=sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448 \ 256 | --hash=sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411 \ 257 | --hash=sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4 \ 258 | --hash=sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32 \ 259 | --hash=sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b \ 260 | --hash=sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0 \ 261 | --hash=sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530 \ 262 | --hash=sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669 \ 263 | --hash=sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7 \ 264 | --hash=sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1 \ 265 | --hash=sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5 \ 266 | --hash=sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce \ 267 | --hash=sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4 \ 268 | --hash=sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e \ 269 | --hash=sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2 \ 270 | --hash=sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d \ 271 | --hash=sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9 \ 272 | --hash=sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642 \ 273 | --hash=sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0 \ 274 | --hash=sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703 \ 275 | --hash=sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb \ 276 | --hash=sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1 \ 277 | --hash=sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13 \ 278 | --hash=sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab \ 279 | --hash=sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38 \ 280 | --hash=sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb \ 281 | --hash=sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb \ 282 | --hash=sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81 \ 283 | --hash=sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8 \ 284 | --hash=sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd \ 285 | --hash=sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4 286 | idna==3.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 287 | --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ 288 | --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 289 | ipython==8.10.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 290 | --hash=sha256:b13a1d6c1f5818bd388db53b7107d17454129a70de2b87481d555daede5eb49e \ 291 | --hash=sha256:b38c31e8fc7eff642fc7c597061fff462537cf2314e3225a19c906b7b0d8a345 292 | jedi==0.18.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 293 | --hash=sha256:203c1fd9d969ab8f2119ec0a3342e0b49910045abe6af0a3ae83a5764d54639e \ 294 | --hash=sha256:bae794c30d07f6d910d32a7048af09b5a39ed740918da923c6b780790ebac612 295 | jinja2==3.1.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 296 | --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ 297 | --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 298 | jsonpickle==3.0.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 299 | --hash=sha256:032538804795e73b94ead410800ac387fdb6de98f8882ac957fcd247e3a85200 \ 300 | --hash=sha256:130d8b293ea0add3845de311aaba55e6d706d0bb17bc123bd2c8baf8a39ac77c 301 | markupsafe==2.1.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 302 | --hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \ 303 | --hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \ 304 | --hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \ 305 | --hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \ 306 | --hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \ 307 | --hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \ 308 | --hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \ 309 | --hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \ 310 | --hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \ 311 | --hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \ 312 | --hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \ 313 | --hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \ 314 | --hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \ 315 | --hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \ 316 | --hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \ 317 | --hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \ 318 | --hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \ 319 | --hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \ 320 | --hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \ 321 | --hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \ 322 | --hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \ 323 | --hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \ 324 | --hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \ 325 | --hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \ 326 | --hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \ 327 | --hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \ 328 | --hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \ 329 | --hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \ 330 | --hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \ 331 | --hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \ 332 | --hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \ 333 | --hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \ 334 | --hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \ 335 | --hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \ 336 | --hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \ 337 | --hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \ 338 | --hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \ 339 | --hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \ 340 | --hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \ 341 | --hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \ 342 | --hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \ 343 | --hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \ 344 | --hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \ 345 | --hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \ 346 | --hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \ 347 | --hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \ 348 | --hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \ 349 | --hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \ 350 | --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \ 351 | --hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58 352 | matplotlib-inline==0.1.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 353 | --hash=sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311 \ 354 | --hash=sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304 355 | multidict==6.0.4 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 356 | --hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \ 357 | --hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \ 358 | --hash=sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03 \ 359 | --hash=sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710 \ 360 | --hash=sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161 \ 361 | --hash=sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664 \ 362 | --hash=sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569 \ 363 | --hash=sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067 \ 364 | --hash=sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313 \ 365 | --hash=sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706 \ 366 | --hash=sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2 \ 367 | --hash=sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636 \ 368 | --hash=sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49 \ 369 | --hash=sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93 \ 370 | --hash=sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603 \ 371 | --hash=sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0 \ 372 | --hash=sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60 \ 373 | --hash=sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4 \ 374 | --hash=sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e \ 375 | --hash=sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1 \ 376 | --hash=sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60 \ 377 | --hash=sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951 \ 378 | --hash=sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc \ 379 | --hash=sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe \ 380 | --hash=sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95 \ 381 | --hash=sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d \ 382 | --hash=sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8 \ 383 | --hash=sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed \ 384 | --hash=sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2 \ 385 | --hash=sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775 \ 386 | --hash=sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87 \ 387 | --hash=sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c \ 388 | --hash=sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2 \ 389 | --hash=sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98 \ 390 | --hash=sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3 \ 391 | --hash=sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe \ 392 | --hash=sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78 \ 393 | --hash=sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660 \ 394 | --hash=sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176 \ 395 | --hash=sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e \ 396 | --hash=sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988 \ 397 | --hash=sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c \ 398 | --hash=sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c \ 399 | --hash=sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0 \ 400 | --hash=sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449 \ 401 | --hash=sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f \ 402 | --hash=sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde \ 403 | --hash=sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5 \ 404 | --hash=sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d \ 405 | --hash=sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac \ 406 | --hash=sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a \ 407 | --hash=sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9 \ 408 | --hash=sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca \ 409 | --hash=sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11 \ 410 | --hash=sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35 \ 411 | --hash=sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063 \ 412 | --hash=sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b \ 413 | --hash=sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982 \ 414 | --hash=sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258 \ 415 | --hash=sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1 \ 416 | --hash=sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52 \ 417 | --hash=sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480 \ 418 | --hash=sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7 \ 419 | --hash=sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461 \ 420 | --hash=sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d \ 421 | --hash=sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc \ 422 | --hash=sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779 \ 423 | --hash=sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a \ 424 | --hash=sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547 \ 425 | --hash=sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0 \ 426 | --hash=sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171 \ 427 | --hash=sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf \ 428 | --hash=sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d \ 429 | --hash=sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba 430 | networkx==3.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 431 | --hash=sha256:58058d66b1818043527244fab9d41a51fcd7dcc271748015f3c181b8a90c8e2e \ 432 | --hash=sha256:9a9992345353618ae98339c2b63d8201c381c2944f38a2ab49cb45a4c667e412 433 | parso==0.8.3 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 434 | --hash=sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0 \ 435 | --hash=sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75 436 | pexpect==4.8.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform != "win32" \ 437 | --hash=sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937 \ 438 | --hash=sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c 439 | pickleshare==0.7.5 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 440 | --hash=sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca \ 441 | --hash=sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56 442 | prompt-toolkit==3.0.37 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 443 | --hash=sha256:6a2948ec427dfcc7c983027b1044b355db6aaa8be374f54ad2015471f7d81c5b \ 444 | --hash=sha256:d5d73d4b5eb1a92ba884a88962b157f49b71e06c4348b417dd622b25cdd3800b 445 | ptyprocess==0.7.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" and sys_platform != "win32" \ 446 | --hash=sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 \ 447 | --hash=sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220 448 | pure-eval==0.2.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 449 | --hash=sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350 \ 450 | --hash=sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3 451 | pygments==2.14.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 452 | --hash=sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297 \ 453 | --hash=sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717 454 | pyvis==0.3.1 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 455 | --hash=sha256:aa817caac54afc9a4df38ada6f46636b842a8bd57197bb6aca5084699da04307 456 | six==1.16.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 457 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ 458 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 459 | stack-data==0.6.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 460 | --hash=sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815 \ 461 | --hash=sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8 462 | traitlets==5.9.0 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 463 | --hash=sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8 \ 464 | --hash=sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9 465 | wcwidth==0.2.6 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 466 | --hash=sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e \ 467 | --hash=sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0 468 | yarl==1.8.2 ; python_full_version >= "3.8.1" and python_full_version < "4.0.0" \ 469 | --hash=sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87 \ 470 | --hash=sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89 \ 471 | --hash=sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a \ 472 | --hash=sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08 \ 473 | --hash=sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996 \ 474 | --hash=sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077 \ 475 | --hash=sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901 \ 476 | --hash=sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e \ 477 | --hash=sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee \ 478 | --hash=sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574 \ 479 | --hash=sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165 \ 480 | --hash=sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634 \ 481 | --hash=sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229 \ 482 | --hash=sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b \ 483 | --hash=sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f \ 484 | --hash=sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7 \ 485 | --hash=sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf \ 486 | --hash=sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89 \ 487 | --hash=sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0 \ 488 | --hash=sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1 \ 489 | --hash=sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe \ 490 | --hash=sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf \ 491 | --hash=sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76 \ 492 | --hash=sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951 \ 493 | --hash=sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863 \ 494 | --hash=sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06 \ 495 | --hash=sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562 \ 496 | --hash=sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6 \ 497 | --hash=sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c \ 498 | --hash=sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e \ 499 | --hash=sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1 \ 500 | --hash=sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3 \ 501 | --hash=sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3 \ 502 | --hash=sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778 \ 503 | --hash=sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8 \ 504 | --hash=sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2 \ 505 | --hash=sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b \ 506 | --hash=sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d \ 507 | --hash=sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f \ 508 | --hash=sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c \ 509 | --hash=sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581 \ 510 | --hash=sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918 \ 511 | --hash=sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c \ 512 | --hash=sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e \ 513 | --hash=sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220 \ 514 | --hash=sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37 \ 515 | --hash=sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739 \ 516 | --hash=sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77 \ 517 | --hash=sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6 \ 518 | --hash=sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42 \ 519 | --hash=sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946 \ 520 | --hash=sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5 \ 521 | --hash=sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d \ 522 | --hash=sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146 \ 523 | --hash=sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a \ 524 | --hash=sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83 \ 525 | --hash=sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef \ 526 | --hash=sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80 \ 527 | --hash=sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588 \ 528 | --hash=sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5 \ 529 | --hash=sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2 \ 530 | --hash=sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef \ 531 | --hash=sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826 \ 532 | --hash=sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05 \ 533 | --hash=sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516 \ 534 | --hash=sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0 \ 535 | --hash=sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4 \ 536 | --hash=sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2 \ 537 | --hash=sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0 \ 538 | --hash=sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd \ 539 | --hash=sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8 \ 540 | --hash=sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b \ 541 | --hash=sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1 \ 542 | --hash=sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c 543 | --------------------------------------------------------------------------------